From 34bb9d171432cc85de97929bf7bf9f60aa447c68 Mon Sep 17 00:00:00 2001 From: blam Date: Sat, 28 Nov 2020 22:11:56 +0100 Subject: [PATCH 001/131] feat: add thugboat --- .tugboat/config.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .tugboat/config.yml diff --git a/.tugboat/config.yml b/.tugboat/config.yml new file mode 100644 index 0000000000..ef037c0d28 --- /dev/null +++ b/.tugboat/config.yml @@ -0,0 +1,4 @@ +services: + backstage: + build: + dockerfile: ./packages/backend/Dockerfile From dbea1562dcd808d4350e66deb37f64ea7f47133d Mon Sep 17 00:00:00 2001 From: blam Date: Sat, 28 Nov 2020 22:21:56 +0100 Subject: [PATCH 002/131] chore: rework tugboat build --- .tugboat/config.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.tugboat/config.yml b/.tugboat/config.yml index ef037c0d28..612cbdb5af 100644 --- a/.tugboat/config.yml +++ b/.tugboat/config.yml @@ -1,4 +1,9 @@ services: backstage: - build: - dockerfile: ./packages/backend/Dockerfile + image: tugboatqa/node:lts + commands: + init: + - yarn + - yarn build + start: + - yarn start-backend From b303ee9fb170cc639efdc3b883263c3147ea4fc3 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 1 Dec 2020 13:11:40 +0100 Subject: [PATCH 003/131] chore: updating tugboat config to build and tsc --- .tugboat/config.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.tugboat/config.yml b/.tugboat/config.yml index 612cbdb5af..14cfd6dc85 100644 --- a/.tugboat/config.yml +++ b/.tugboat/config.yml @@ -3,7 +3,8 @@ services: image: tugboatqa/node:lts commands: init: - - yarn + - yarn install + - yarn tsc - yarn build start: - yarn start-backend From 4ee32a78f140f69f68d875cd610d74e0d31202f0 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 1 Dec 2020 20:08:47 +0100 Subject: [PATCH 004/131] chore: set as default --- .tugboat/config.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.tugboat/config.yml b/.tugboat/config.yml index 14cfd6dc85..b3195d6254 100644 --- a/.tugboat/config.yml +++ b/.tugboat/config.yml @@ -1,10 +1,11 @@ services: backstage: image: tugboatqa/node:lts + default: true commands: init: - yarn install - yarn tsc - yarn build start: - - yarn start-backend + - APP_CONFIG_backend_listen_port=80 yarn start-backend From e0f22774801788fd2584879c19eb2dfd923ebf46 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 2 Dec 2020 10:24:59 +0100 Subject: [PATCH 005/131] chore: fixing container port --- .tugboat/config.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.tugboat/config.yml b/.tugboat/config.yml index b3195d6254..ef696e28de 100644 --- a/.tugboat/config.yml +++ b/.tugboat/config.yml @@ -1,6 +1,7 @@ services: backstage: image: tugboatqa/node:lts + expose: 7000 default: true commands: init: @@ -8,4 +9,4 @@ services: - yarn tsc - yarn build start: - - APP_CONFIG_backend_listen_port=80 yarn start-backend + - yarn start-backend From 8caf5cb0eacc980ace3e295c94a35c9df4b0e12f Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 3 Dec 2020 00:15:30 +0100 Subject: [PATCH 006/131] chore: make the start-backend command backgrounded --- .tugboat/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.tugboat/config.yml b/.tugboat/config.yml index ef696e28de..3bc8865493 100644 --- a/.tugboat/config.yml +++ b/.tugboat/config.yml @@ -9,4 +9,4 @@ services: - yarn tsc - yarn build start: - - yarn start-backend + - yarn start-backend & From 83006ba83ab31387bb0e10055f756fdfee790f6a Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 3 Dec 2020 00:17:51 +0100 Subject: [PATCH 007/131] chore: fixing some base build preview things --- .tugboat/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.tugboat/config.yml b/.tugboat/config.yml index 3bc8865493..20f716785c 100644 --- a/.tugboat/config.yml +++ b/.tugboat/config.yml @@ -4,7 +4,7 @@ services: expose: 7000 default: true commands: - init: + build: - yarn install - yarn tsc - yarn build From 285d21640318741b241579e5ee070fc343ed0db9 Mon Sep 17 00:00:00 2001 From: Nir Gazit Date: Tue, 19 Jan 2021 16:59:39 +0200 Subject: [PATCH 008/131] Added support for multiple kafka clusters --- plugins/kafka-backend/README.md | 6 +- plugins/kafka-backend/config.d.ts | 15 ++-- .../src/config/ClusterReader.test.ts | 45 ++++++++++ .../kafka-backend/src/config/ClusterReader.ts | 37 ++++++++ .../kafka-backend/src/service/router.test.ts | 89 ++++++++++++++++--- plugins/kafka-backend/src/service/router.ts | 37 +++++--- plugins/kafka-backend/src/types/types.ts | 23 +++++ plugins/kafka/README.md | 8 +- plugins/kafka/src/api/KafkaBackendClient.ts | 5 +- plugins/kafka/src/api/types.ts | 1 + .../useConsumerGroupsForEntity.test.tsx | 63 +++++++++---- .../useConsumerGroupsForEntity.ts | 11 ++- ...useConsumerGroupsOffsetsForEntity.test.tsx | 4 +- .../useConsumerGroupsOffsetsForEntity.ts | 7 +- 14 files changed, 295 insertions(+), 56 deletions(-) create mode 100644 plugins/kafka-backend/src/config/ClusterReader.test.ts create mode 100644 plugins/kafka-backend/src/config/ClusterReader.ts create mode 100644 plugins/kafka-backend/src/types/types.ts diff --git a/plugins/kafka-backend/README.md b/plugins/kafka-backend/README.md index b2271a2911..13078ea816 100644 --- a/plugins/kafka-backend/README.md +++ b/plugins/kafka-backend/README.md @@ -23,6 +23,8 @@ Example: ```yaml kafka: clientId: backstage - brokers: - - localhost:9092 + clusters: + name: prod + brokers: + - localhost:9092 ``` diff --git a/plugins/kafka-backend/config.d.ts b/plugins/kafka-backend/config.d.ts index 0eeeb42197..122e76487a 100644 --- a/plugins/kafka-backend/config.d.ts +++ b/plugins/kafka-backend/config.d.ts @@ -16,11 +16,14 @@ export interface Config { kafka?: { clientId: string; - brokers: string[]; - ssl?: { - ca: string[]; - key: string; - cert: string; - }; + clusters: { + name: string; + brokers: string[]; + ssl?: { + ca: string[]; + key: string; + cert: string; + }; + }[]; }; } diff --git a/plugins/kafka-backend/src/config/ClusterReader.test.ts b/plugins/kafka-backend/src/config/ClusterReader.test.ts new file mode 100644 index 0000000000..0529684425 --- /dev/null +++ b/plugins/kafka-backend/src/config/ClusterReader.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 '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; +import { getClusterDetails } from './ClusterReader'; + +describe('getClusterDetails', () => { + it('empty clusters return empty cluster details', async () => { + const config = new ConfigReader({ clusters: [] }); + + const result = getClusterDetails(config.getConfigArray('clusters')); + + expect(result).toStrictEqual([]); + }); + + it('two clusters return two cluster details', async () => { + const config = new ConfigReader({ + clusters: [ + { name: 'cluster1', brokers: ['a', 'b'] }, + { name: 'cluster2', brokers: ['d'] }, + ], + }); + + const result = getClusterDetails(config.getConfigArray('clusters')); + + expect(result).toStrictEqual([ + { name: 'cluster1', brokers: ['a', 'b'] }, + { name: 'cluster2', brokers: ['d'] }, + ]); + }); +}); diff --git a/plugins/kafka-backend/src/config/ClusterReader.ts b/plugins/kafka-backend/src/config/ClusterReader.ts new file mode 100644 index 0000000000..a4e6a4e4fe --- /dev/null +++ b/plugins/kafka-backend/src/config/ClusterReader.ts @@ -0,0 +1,37 @@ +/* + * 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 { ConnectionOptions } from 'tls'; +import { ClusterDetails } from '../types/types'; + +export function getClusterDetails(config: Config[]): ClusterDetails[] { + return config.map(clusterConfig => { + const clusterDetails = { + name: clusterConfig.getString('name'), + brokers: clusterConfig.getStringArray('brokers'), + }; + const sslConfig = clusterConfig.getOptional('kafka.ssl'); + if (sslConfig) { + return { + ...clusterDetails, + ssl: sslConfig as ConnectionOptions, + }; + } + + return clusterDetails; + }); +} diff --git a/plugins/kafka-backend/src/service/router.test.ts b/plugins/kafka-backend/src/service/router.test.ts index 5e9de6cf2f..83eacc5b48 100644 --- a/plugins/kafka-backend/src/service/router.test.ts +++ b/plugins/kafka-backend/src/service/router.test.ts @@ -16,22 +16,34 @@ import request from 'supertest'; import express from 'express'; -import { makeRouter } from './router'; +import { makeRouter, ClusterApi } from './router'; import { getVoidLogger } from '@backstage/backend-common'; import { KafkaApi } from './KafkaApi'; import { when } from 'jest-when'; describe('router', () => { let app: express.Express; - let kafkaApi: jest.Mocked; + let apis: ClusterApi[]; + let devKafkaApi: jest.Mocked; + let prodKafkaApi: jest.Mocked; beforeAll(async () => { - kafkaApi = { + devKafkaApi = { fetchTopicOffsets: jest.fn(), fetchGroupOffsets: jest.fn(), }; - const router = makeRouter(getVoidLogger(), kafkaApi); + prodKafkaApi = { + fetchTopicOffsets: jest.fn(), + fetchGroupOffsets: jest.fn(), + }; + + apis = [ + { name: 'dev', api: devKafkaApi }, + { name: 'prod', api: prodKafkaApi }, + ]; + + const router = makeRouter(getVoidLogger(), apis); app = express().use(router); }); @@ -39,7 +51,7 @@ describe('router', () => { jest.resetAllMocks(); }); - describe('get /consumer/:consumerId/offsets', () => { + describe('get /:clusterId/consumer/:consumerId/offsets', () => { it('returns topic and group offsets', async () => { const topic1Offsets = [ { id: 1, offset: '500' }, @@ -60,15 +72,17 @@ describe('router', () => { partitions: [{ id: 1, offset: '456' }], }, ]; - when(kafkaApi.fetchTopicOffsets) + when(prodKafkaApi.fetchTopicOffsets) .calledWith('topic1') .mockResolvedValue(topic1Offsets); - when(kafkaApi.fetchTopicOffsets) + when(prodKafkaApi.fetchTopicOffsets) .calledWith('topic2') .mockResolvedValue(topic2Offsets); - kafkaApi.fetchGroupOffsets.mockResolvedValue(groupOffsets); + when(prodKafkaApi.fetchGroupOffsets) + .calledWith('hey') + .mockResolvedValue(groupOffsets); - const response = await request(app).get('/consumer/hey/offsets'); + const response = await request(app).get('/prod/consumer/hey/offsets'); expect(response.status).toEqual(200); expect(response.body.consumerId).toEqual('hey'); @@ -95,11 +109,64 @@ describe('router', () => { }); it('handles internal error correctly', async () => { - kafkaApi.fetchGroupOffsets.mockRejectedValue(Error('oh no')); + prodKafkaApi.fetchGroupOffsets.mockRejectedValue(Error('oh no')); - const response = await request(app).get('/consumer/hey/offsets'); + const response = await request(app).get('/prod/consumer/hey/offsets'); expect(response.status).toEqual(500); }); + + it('uses correct kafka cluster', async () => { + const topic1ProdOffsets = [{ id: 1, offset: '500' }]; + const topic1DevOffsets = [{ id: 1, offset: '1234' }]; + const groupProdOffsets = [ + { + topic: 'topic1', + partitions: [{ id: 1, offset: '100' }], + }, + ]; + const groupDevOffsets = [ + { + topic: 'topic1', + partitions: [{ id: 1, offset: '567' }], + }, + ]; + when(prodKafkaApi.fetchTopicOffsets) + .calledWith('topic1') + .mockResolvedValue(topic1ProdOffsets); + when(prodKafkaApi.fetchGroupOffsets) + .calledWith('hey') + .mockResolvedValue(groupProdOffsets); + when(devKafkaApi.fetchTopicOffsets) + .calledWith('topic1') + .mockResolvedValue(topic1DevOffsets); + when(devKafkaApi.fetchGroupOffsets) + .calledWith('hey') + .mockResolvedValue(groupDevOffsets); + + const prodResponse = await request(app).get('/prod/consumer/hey/offsets'); + const devResponse = await request(app).get('/dev/consumer/hey/offsets'); + + expect(prodResponse.status).toEqual(200); + expect(prodResponse.body.consumerId).toEqual('hey'); + expect(prodResponse.body.offsets).toIncludeSameMembers([ + { + topic: 'topic1', + partitionId: 1, + groupOffset: '100', + topicOffset: '500', + }, + ]); + expect(devResponse.status).toEqual(200); + expect(devResponse.body.consumerId).toEqual('hey'); + expect(devResponse.body.offsets).toIncludeSameMembers([ + { + topic: 'topic1', + partitionId: 1, + groupOffset: '567', + topicOffset: '1234', + }, + ]); + }); }); }); diff --git a/plugins/kafka-backend/src/service/router.ts b/plugins/kafka-backend/src/service/router.ts index 2bbc877fe2..d6e5ab08a7 100644 --- a/plugins/kafka-backend/src/service/router.ts +++ b/plugins/kafka-backend/src/service/router.ts @@ -20,31 +20,43 @@ import { Logger } from 'winston'; import { Config } from '@backstage/config'; import { KafkaApi, KafkaJsApiImpl } from './KafkaApi'; import _ from 'lodash'; -import { ConnectionOptions } from 'tls'; +import { getClusterDetails } from '../config/clusterReader'; export interface RouterOptions { logger: Logger; config: Config; } +export interface ClusterApi { + name: string; + api: KafkaApi; +} + export const makeRouter = ( logger: Logger, - kafkaApi: KafkaApi, + kafkaApis: ClusterApi[], ): express.Router => { const router = Router(); router.use(express.json()); - router.get('/consumer/:consumerId/offsets', async (req, res) => { + const kafkaApiByClusterName = _.keyBy(kafkaApis, item => item.name); + + router.get('/consumers/:clusterId/:consumerId/offsets', async (req, res) => { + const clusterId = req.params.clusterId; const consumerId = req.params.consumerId; - logger.debug(`Fetch consumer group ${consumerId} offsets`); + logger.info( + `Fetch consumer group ${consumerId} offsets from cluster ${clusterId}`, + ); - const groupOffsets = await kafkaApi.fetchGroupOffsets(consumerId); + const kafkaApi = kafkaApiByClusterName[clusterId]; + + const groupOffsets = await kafkaApi.api.fetchGroupOffsets(consumerId); const groupWithTopicOffsets = await Promise.all( groupOffsets.map(async ({ topic, partitions }) => { const topicOffsets = _.keyBy( - await kafkaApi.fetchTopicOffsets(topic), + await kafkaApi.api.fetchTopicOffsets(topic), partition => partition.id, ); @@ -70,12 +82,15 @@ export async function createRouter( logger.info('Initializing Kafka backend'); const clientId = options.config.getString('kafka.clientId'); - const brokers = options.config.getStringArray('kafka.brokers'); - const sslConfig = options.config.getOptional('kafka.ssl'); - const ssl = sslConfig ? (sslConfig as ConnectionOptions) : undefined; + const clusters = getClusterDetails( + options.config.getConfigArray('kafka.clusters'), + ); - const kafkaApi = new KafkaJsApiImpl({ clientId, brokers, logger, ssl }); + const kafkaApis = clusters.map(cluster => ({ + name: cluster.name, + api: new KafkaJsApiImpl({ clientId, logger, ...cluster }), + })); - return makeRouter(logger, kafkaApi); + return makeRouter(logger, kafkaApis); } diff --git a/plugins/kafka-backend/src/types/types.ts b/plugins/kafka-backend/src/types/types.ts new file mode 100644 index 0000000000..5f70998dd8 --- /dev/null +++ b/plugins/kafka-backend/src/types/types.ts @@ -0,0 +1,23 @@ +/* + * 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 { ConnectionOptions } from 'tls'; + +export interface ClusterDetails { + name: string; + brokers: string[]; + ssl?: ConnectionOptions; +} diff --git a/plugins/kafka/README.md b/plugins/kafka/README.md index 7d947ae41b..c41c328741 100644 --- a/plugins/kafka/README.md +++ b/plugins/kafka/README.md @@ -65,8 +65,10 @@ import { Router as KafkaRouter } from '@backstage/plugin-kafka'; ```yaml kafka: clientId: backstage - brokers: - - localhost:9092 + clusters: + - name: cluster-name + brokers: + - localhost:9092 ``` 6. Add `kafka.apache.org/consumer-groups` annotation to your services: @@ -77,7 +79,7 @@ kind: Component metadata: # ... annotations: - kafka.apache.org/consumer-groups: consumer-group-name + kafka.apache.org/consumer-groups: cluster-name/consumer-group-name spec: type: service ``` diff --git a/plugins/kafka/src/api/KafkaBackendClient.ts b/plugins/kafka/src/api/KafkaBackendClient.ts index 2112dd0f17..45b1616fe0 100644 --- a/plugins/kafka/src/api/KafkaBackendClient.ts +++ b/plugins/kafka/src/api/KafkaBackendClient.ts @@ -43,8 +43,11 @@ export class KafkaBackendClient implements KafkaApi { } async getConsumerGroupOffsets( + clusterId: string, consumerGroup: string, ): Promise { - return await this.getRequired(`/consumer/${consumerGroup}/offsets`); + return await this.getRequired( + `/consumers/${clusterId}/${consumerGroup}/offsets`, + ); } } diff --git a/plugins/kafka/src/api/types.ts b/plugins/kafka/src/api/types.ts index 9c2239156b..c4307d5c1f 100644 --- a/plugins/kafka/src/api/types.ts +++ b/plugins/kafka/src/api/types.ts @@ -34,6 +34,7 @@ export type ConsumerGroupOffsetsResponse = { export interface KafkaApi { getConsumerGroupOffsets( + clusterId: string, consumerGroup: string, ): Promise; } diff --git a/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsForEntity.test.tsx b/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsForEntity.test.tsx index 44693d5bbc..483986d4ec 100644 --- a/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsForEntity.test.tsx +++ b/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsForEntity.test.tsx @@ -20,21 +20,7 @@ import { EntityContext } from '@backstage/plugin-catalog'; import { Entity } from '@backstage/catalog-model'; describe('useConsumerGroupOffsets', () => { - const entity: Entity = { - apiVersion: 'v1', - kind: 'Component', - metadata: { - name: 'test', - annotations: { - 'kafka.apache.org/consumer-groups': 'consumer', - }, - }, - spec: { - owner: 'guest', - type: 'Website', - lifecycle: 'development', - }, - }; + let entity: Entity; const wrapper = ({ children }: PropsWithChildren<{}>) => { return ( @@ -46,8 +32,51 @@ describe('useConsumerGroupOffsets', () => { const subject = () => renderHook(useConsumerGroupsForEntity, { wrapper }); - it('returns correct consumer group for annotation', async () => { + it('returns correct cluster and consumer group for annotation', async () => { + entity = { + apiVersion: 'v1', + kind: 'Component', + metadata: { + name: 'test', + annotations: { + 'kafka.apache.org/consumer-groups': 'prod/consumer', + }, + }, + spec: { + owner: 'guest', + type: 'Website', + lifecycle: 'development', + }, + }; const { result } = subject(); - expect(result.current).toBe('consumer'); + expect(result.current).toStrictEqual({ + clusterId: 'prod', + consumerGroup: 'consumer', + }); + }); + + it('fails on missing cluster', async () => { + entity = { + apiVersion: 'v1', + kind: 'Component', + metadata: { + name: 'test', + annotations: { + 'kafka.apache.org/consumer-groups': 'consumer', + }, + }, + spec: { + owner: 'guest', + type: 'Website', + lifecycle: 'development', + }, + }; + const { result } = subject(); + expect(() => result.current).toThrowError(); + expect(result.error).toStrictEqual( + new Error( + `Failed to parse kafka consumer group annotation: got "consumer"`, + ), + ); }); }); diff --git a/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsForEntity.ts b/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsForEntity.ts index 5003c2f5b5..0afec42870 100644 --- a/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsForEntity.ts +++ b/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsForEntity.ts @@ -19,6 +19,15 @@ import { KAFKA_CONSUMER_GROUP_ANNOTATION } from '../../constants'; export const useConsumerGroupsForEntity = () => { const { entity } = useEntity(); + const annotation = + entity.metadata.annotations?.[KAFKA_CONSUMER_GROUP_ANNOTATION] ?? ''; + const [clusterId, consumerGroup] = annotation.split('/'); - return entity.metadata.annotations?.[KAFKA_CONSUMER_GROUP_ANNOTATION] ?? ''; + if (!clusterId || !consumerGroup) { + throw new Error( + `Failed to parse kafka consumer group annotation: got "${annotation}"`, + ); + } + + return { clusterId, consumerGroup }; }; diff --git a/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsOffsetsForEntity.test.tsx b/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsOffsetsForEntity.test.tsx index 28f0ff7c28..16bf2f7665 100644 --- a/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsOffsetsForEntity.test.tsx +++ b/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsOffsetsForEntity.test.tsx @@ -45,7 +45,7 @@ describe('useConsumerGroupOffsets', () => { metadata: { name: 'test', annotations: { - 'kafka.apache.org/consumer-groups': consumerGroupOffsets.consumerId, + 'kafka.apache.org/consumer-groups': `cluster/${consumerGroupOffsets.consumerId}`, }, }, spec: { @@ -75,7 +75,7 @@ describe('useConsumerGroupOffsets', () => { it('returns correct consumer group for annotation', async () => { when(mockKafkaApi.getConsumerGroupOffsets) - .calledWith(consumerGroupOffsets.consumerId) + .calledWith('cluster', consumerGroupOffsets.consumerId) .mockResolvedValue(consumerGroupOffsets); const { result, waitForNextUpdate } = subject(); diff --git a/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsOffsetsForEntity.ts b/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsOffsetsForEntity.ts index eb7e105989..4081b59796 100644 --- a/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsOffsetsForEntity.ts +++ b/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsOffsetsForEntity.ts @@ -20,13 +20,16 @@ import { kafkaApiRef } from '../../api/types'; import { useConsumerGroupsForEntity } from './useConsumerGroupsForEntity'; export const useConsumerGroupsOffsetsForEntity = () => { - const consumerGroup = useConsumerGroupsForEntity(); + const { clusterId, consumerGroup } = useConsumerGroupsForEntity(); const api = useApi(kafkaApiRef); const errorApi = useApi(errorApiRef); const { loading, value: topics, retry } = useAsyncRetry(async () => { try { - const response = await api.getConsumerGroupOffsets(consumerGroup); + const response = await api.getConsumerGroupOffsets( + clusterId, + consumerGroup, + ); return response.offsets; } catch (e) { errorApi.post(e); From 5c0fb3b8443781ff874def5911d07dad4567f3a4 Mon Sep 17 00:00:00 2001 From: Nir Gazit Date: Tue, 19 Jan 2021 17:17:55 +0200 Subject: [PATCH 009/131] Fixed incorrect import --- plugins/kafka-backend/src/service/router.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/kafka-backend/src/service/router.ts b/plugins/kafka-backend/src/service/router.ts index d6e5ab08a7..465347ed8f 100644 --- a/plugins/kafka-backend/src/service/router.ts +++ b/plugins/kafka-backend/src/service/router.ts @@ -20,7 +20,7 @@ import { Logger } from 'winston'; import { Config } from '@backstage/config'; import { KafkaApi, KafkaJsApiImpl } from './KafkaApi'; import _ from 'lodash'; -import { getClusterDetails } from '../config/clusterReader'; +import { getClusterDetails } from '../config/ClusterReader'; export interface RouterOptions { logger: Logger; From 7112565f45bbd6ab2bd05e7e0a5d85e85b4e613f Mon Sep 17 00:00:00 2001 From: Nir Gazit Date: Thu, 21 Jan 2021 12:32:02 +0200 Subject: [PATCH 010/131] Added support for multiple consumed topics --- app-config.yaml | 14 ++-- .../kafka-backend/src/service/router.test.ts | 48 +++++++------ .../ConsumerGroupOffsets.tsx | 19 ++++-- .../useConsumerGroupsForEntity.test.tsx | 68 +++++++++++++++++-- .../useConsumerGroupsForEntity.ts | 24 +++++-- ...useConsumerGroupsOffsetsForEntity.test.tsx | 16 +++-- .../useConsumerGroupsOffsetsForEntity.ts | 25 ++++--- 7 files changed, 158 insertions(+), 56 deletions(-) diff --git a/app-config.yaml b/app-config.yaml index 02afb8ac41..7017b0eee8 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -99,6 +99,13 @@ kubernetes: - 'config' clusters: [] +kafka: + clientId: backstage + clusters: + - name: cluster + brokers: + - localhost:9092 + integrations: github: - host: github.com @@ -225,6 +232,8 @@ catalog: # Backstage example groups and users - type: file target: ../catalog-model/examples/acme-corp.yaml + - type: file + target: ../../local.yaml scaffolder: github: @@ -372,8 +381,3 @@ homepage: timezone: 'Asia/Tokyo' pagerduty: eventsBaseUrl: 'https://events.pagerduty.com/v2' - -kafka: - clientId: backstage - brokers: - - localhost:9092 diff --git a/plugins/kafka-backend/src/service/router.test.ts b/plugins/kafka-backend/src/service/router.test.ts index 400729627b..42cd92ddfb 100644 --- a/plugins/kafka-backend/src/service/router.test.ts +++ b/plugins/kafka-backend/src/service/router.test.ts @@ -51,7 +51,7 @@ describe('router', () => { jest.resetAllMocks(); }); - describe('get /:clusterId/consumer/:consumerId/offsets', () => { + describe('get /consumers/clusterId/:consumerId/offsets', () => { it('returns topic and group offsets', async () => { const topic1Offsets = [ { id: 1, offset: '500' }, @@ -82,7 +82,7 @@ describe('router', () => { .calledWith('hey') .mockResolvedValue(groupOffsets); - const response = await request(app).get('/prod/consumer/hey/offsets'); + const response = await request(app).get('/consumers/prod/hey/offsets'); expect(response.status).toEqual(200); expect(response.body.consumerId).toEqual('hey'); @@ -114,7 +114,7 @@ describe('router', () => { it('handles internal error correctly', async () => { prodKafkaApi.fetchGroupOffsets.mockRejectedValue(Error('oh no')); - const response = await request(app).get('/prod/consumer/hey/offsets'); + const response = await request(app).get('/consumers/prod/hey/offsets'); expect(response.status).toEqual(500); }); @@ -147,29 +147,35 @@ describe('router', () => { .calledWith('hey') .mockResolvedValue(groupDevOffsets); - const prodResponse = await request(app).get('/prod/consumer/hey/offsets'); - const devResponse = await request(app).get('/dev/consumer/hey/offsets'); + const prodResponse = await request(app).get( + '/consumers/prod/hey/offsets', + ); + const devResponse = await request(app).get('/consumers/dev/hey/offsets'); expect(prodResponse.status).toEqual(200); expect(prodResponse.body.consumerId).toEqual('hey'); - expect(prodResponse.body.offsets).toIncludeSameMembers([ - { - topic: 'topic1', - partitionId: 1, - groupOffset: '100', - topicOffset: '500', - }, - ]); + expect(new Set(prodResponse.body.offsets)).toStrictEqual( + new Set([ + { + topic: 'topic1', + partitionId: 1, + groupOffset: '100', + topicOffset: '500', + }, + ]), + ); expect(devResponse.status).toEqual(200); expect(devResponse.body.consumerId).toEqual('hey'); - expect(devResponse.body.offsets).toIncludeSameMembers([ - { - topic: 'topic1', - partitionId: 1, - groupOffset: '567', - topicOffset: '1234', - }, - ]); + expect(new Set(devResponse.body.offsets)).toStrictEqual( + new Set([ + { + topic: 'topic1', + partitionId: 1, + groupOffset: '567', + topicOffset: '1234', + }, + ]), + ); }); }); }); diff --git a/plugins/kafka/src/components/ConsumerGroupOffsets/ConsumerGroupOffsets.tsx b/plugins/kafka/src/components/ConsumerGroupOffsets/ConsumerGroupOffsets.tsx index 448d420bc0..7928393526 100644 --- a/plugins/kafka/src/components/ConsumerGroupOffsets/ConsumerGroupOffsets.tsx +++ b/plugins/kafka/src/components/ConsumerGroupOffsets/ConsumerGroupOffsets.tsx @@ -15,7 +15,7 @@ */ import { Table, TableColumn } from '@backstage/core'; -import { Box, Typography } from '@material-ui/core'; +import { Box, Grid, Typography } from '@material-ui/core'; import RetryIcon from '@material-ui/icons/Replay'; import React from 'react'; import { useConsumerGroupsOffsetsForEntity } from './useConsumerGroupsOffsetsForEntity'; @@ -62,6 +62,7 @@ const generatedColumns: TableColumn[] = [ type Props = { loading: boolean; retry: () => void; + clusterId: string; consumerGroup: string; topics?: TopicPartitionInfo[]; }; @@ -69,6 +70,7 @@ type Props = { export const ConsumerGroupOffsets = ({ loading, topics, + clusterId, consumerGroup, retry, }: Props) => { @@ -87,7 +89,7 @@ export const ConsumerGroupOffsets = ({ title={ - Consumed Topics for {consumerGroup} + Consumed Topics for {consumerGroup} ({clusterId}) } @@ -98,6 +100,15 @@ export const ConsumerGroupOffsets = ({ export const KafkaTopicsForConsumer = () => { const [tableProps, { retry }] = useConsumerGroupsOffsetsForEntity(); - - return ; + return ( + + {tableProps.consumerGroupsTopics?.map(consumerGroup => ( + + ))} + + ); }; diff --git a/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsForEntity.test.tsx b/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsForEntity.test.tsx index 483986d4ec..3bee4e2e88 100644 --- a/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsForEntity.test.tsx +++ b/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsForEntity.test.tsx @@ -49,10 +49,66 @@ describe('useConsumerGroupOffsets', () => { }, }; const { result } = subject(); - expect(result.current).toStrictEqual({ - clusterId: 'prod', - consumerGroup: 'consumer', - }); + expect(result.current).toStrictEqual([ + { + clusterId: 'prod', + consumerGroup: 'consumer', + }, + ]); + }); + + it('returns correct cluster and consumer group for multiple consumers', async () => { + entity = { + apiVersion: 'v1', + kind: 'Component', + metadata: { + name: 'test', + annotations: { + 'kafka.apache.org/consumer-groups': + 'prod/consumer,dev/another-consumer', + }, + }, + spec: { + owner: 'guest', + type: 'Website', + lifecycle: 'development', + }, + }; + const { result } = subject(); + expect(result.current).toStrictEqual([ + { clusterId: 'prod', consumerGroup: 'consumer' }, + { + clusterId: 'dev', + consumerGroup: 'another-consumer', + }, + ]); + }); + + it('returns correct cluster and consumer group for annotation with extra spaces', async () => { + entity = { + apiVersion: 'v1', + kind: 'Component', + metadata: { + name: 'test', + annotations: { + 'kafka.apache.org/consumer-groups': + ' prod/consumer , dev/another-consumer ', + }, + }, + spec: { + owner: 'guest', + type: 'Website', + lifecycle: 'development', + }, + }; + const { result } = subject(); + expect(result.current).toStrictEqual([ + { clusterId: 'prod', consumerGroup: 'consumer' }, + { + clusterId: 'dev', + consumerGroup: 'another-consumer', + }, + ]); }); it('fails on missing cluster', async () => { @@ -62,7 +118,7 @@ describe('useConsumerGroupOffsets', () => { metadata: { name: 'test', annotations: { - 'kafka.apache.org/consumer-groups': 'consumer', + 'kafka.apache.org/consumer-groups': 'dev/another,consumer', }, }, spec: { @@ -75,7 +131,7 @@ describe('useConsumerGroupOffsets', () => { expect(() => result.current).toThrowError(); expect(result.error).toStrictEqual( new Error( - `Failed to parse kafka consumer group annotation: got "consumer"`, + `Failed to parse kafka consumer group annotation: got "dev/another,consumer"`, ), ); }); diff --git a/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsForEntity.ts b/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsForEntity.ts index 0afec42870..522a6273fe 100644 --- a/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsForEntity.ts +++ b/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsForEntity.ts @@ -15,19 +15,29 @@ */ import { useEntity } from '@backstage/plugin-catalog'; +import { useMemo } from 'react'; import { KAFKA_CONSUMER_GROUP_ANNOTATION } from '../../constants'; export const useConsumerGroupsForEntity = () => { const { entity } = useEntity(); const annotation = entity.metadata.annotations?.[KAFKA_CONSUMER_GROUP_ANNOTATION] ?? ''; - const [clusterId, consumerGroup] = annotation.split('/'); - if (!clusterId || !consumerGroup) { - throw new Error( - `Failed to parse kafka consumer group annotation: got "${annotation}"`, - ); - } + const consumerList = useMemo(() => { + return annotation.split(',').map(consumer => { + const [clusterId, consumerGroup] = consumer.split('/'); - return { clusterId, consumerGroup }; + if (!clusterId || !consumerGroup) { + throw new Error( + `Failed to parse kafka consumer group annotation: got "${annotation}"`, + ); + } + return { + clusterId: clusterId.trim(), + consumerGroup: consumerGroup.trim(), + }; + }); + }, [annotation]); + + return consumerList; }; diff --git a/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsOffsetsForEntity.test.tsx b/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsOffsetsForEntity.test.tsx index 16bf2f7665..b7e3a29e80 100644 --- a/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsOffsetsForEntity.test.tsx +++ b/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsOffsetsForEntity.test.tsx @@ -45,7 +45,7 @@ describe('useConsumerGroupOffsets', () => { metadata: { name: 'test', annotations: { - 'kafka.apache.org/consumer-groups': `cluster/${consumerGroupOffsets.consumerId}`, + 'kafka.apache.org/consumer-groups': `prod/${consumerGroupOffsets.consumerId}`, }, }, spec: { @@ -74,16 +74,24 @@ describe('useConsumerGroupOffsets', () => { renderHook(useConsumerGroupsOffsetsForEntity, { wrapper }); it('returns correct consumer group for annotation', async () => { + mockKafkaApi.getConsumerGroupOffsets.mockResolvedValue( + consumerGroupOffsets, + ); when(mockKafkaApi.getConsumerGroupOffsets) - .calledWith('cluster', consumerGroupOffsets.consumerId) + .calledWith('prod', consumerGroupOffsets.consumerId) .mockResolvedValue(consumerGroupOffsets); const { result, waitForNextUpdate } = subject(); await waitForNextUpdate(); const [tableProps] = result.current; - expect(tableProps.consumerGroup).toBe(consumerGroupOffsets.consumerId); - expect(tableProps.topics).toBe(consumerGroupOffsets.offsets); + expect(tableProps.consumerGroupsTopics).toStrictEqual([ + { + clusterId: 'prod', + consumerGroup: consumerGroupOffsets.consumerId, + topics: consumerGroupOffsets.offsets, + }, + ]); }); it('posts an error to the error api', async () => { diff --git a/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsOffsetsForEntity.ts b/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsOffsetsForEntity.ts index 4081b59796..8cb30040e9 100644 --- a/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsOffsetsForEntity.ts +++ b/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsOffsetsForEntity.ts @@ -20,28 +20,35 @@ import { kafkaApiRef } from '../../api/types'; import { useConsumerGroupsForEntity } from './useConsumerGroupsForEntity'; export const useConsumerGroupsOffsetsForEntity = () => { - const { clusterId, consumerGroup } = useConsumerGroupsForEntity(); + const consumers = useConsumerGroupsForEntity(); const api = useApi(kafkaApiRef); const errorApi = useApi(errorApiRef); - const { loading, value: topics, retry } = useAsyncRetry(async () => { + const { + loading, + value: consumerGroupsTopics, + retry, + } = useAsyncRetry(async () => { try { - const response = await api.getConsumerGroupOffsets( - clusterId, - consumerGroup, + return await Promise.all( + consumers.map(async ({ clusterId, consumerGroup }) => { + const response = await api.getConsumerGroupOffsets( + clusterId, + consumerGroup, + ); + return { clusterId, consumerGroup, topics: response.offsets }; + }), ); - return response.offsets; } catch (e) { errorApi.post(e); throw e; } - }, [api, errorApi, consumerGroup]); + }, [consumers, api, errorApi]); return [ { loading, - consumerGroup, - topics, + consumerGroupsTopics, }, { retry, From 234e7d985188a1f740125b82857c431962d0dca2 Mon Sep 17 00:00:00 2001 From: Nir Gazit Date: Thu, 21 Jan 2021 12:33:51 +0200 Subject: [PATCH 011/131] Added changeset --- .changeset/fast-flowers-tickle.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/fast-flowers-tickle.md diff --git a/.changeset/fast-flowers-tickle.md b/.changeset/fast-flowers-tickle.md new file mode 100644 index 0000000000..4a27890980 --- /dev/null +++ b/.changeset/fast-flowers-tickle.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-kafka': patch +'@backstage/plugin-kafka-backend': patch +--- + +Added support for multiple kafka clusters and multiple consumers per component. From 4f4fd9c307155df3e4a3cc4e3ea597ebd92d5252 Mon Sep 17 00:00:00 2001 From: Nir Gazit Date: Thu, 21 Jan 2021 12:37:33 +0200 Subject: [PATCH 012/131] Removed debug entry added to app-config by mistake --- app-config.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/app-config.yaml b/app-config.yaml index 7017b0eee8..c150ef5902 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -232,8 +232,6 @@ catalog: # Backstage example groups and users - type: file target: ../catalog-model/examples/acme-corp.yaml - - type: file - target: ../../local.yaml scaffolder: github: From 4b5bd5a458379f0d257357c1af883b3a7effb843 Mon Sep 17 00:00:00 2001 From: Nir Gazit Date: Thu, 21 Jan 2021 13:18:06 +0200 Subject: [PATCH 013/131] Fixed spelling error --- .changeset/fast-flowers-tickle.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/fast-flowers-tickle.md b/.changeset/fast-flowers-tickle.md index 4a27890980..193b9b4459 100644 --- a/.changeset/fast-flowers-tickle.md +++ b/.changeset/fast-flowers-tickle.md @@ -3,4 +3,4 @@ '@backstage/plugin-kafka-backend': patch --- -Added support for multiple kafka clusters and multiple consumers per component. +Added support for multiple Kafka clusters and multiple consumers per component. From 9488ea7df398306b7b128a62303cb634e7460d51 Mon Sep 17 00:00:00 2001 From: Nir Gazit Date: Thu, 21 Jan 2021 14:07:32 +0200 Subject: [PATCH 014/131] Fixed bug in ConsumerGroupOffsets test --- .../ConsumerGroupOffsets/ConsumerGroupOffsets.test.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/kafka/src/components/ConsumerGroupOffsets/ConsumerGroupOffsets.test.tsx b/plugins/kafka/src/components/ConsumerGroupOffsets/ConsumerGroupOffsets.test.tsx index ea0f7e1ddb..9cedc5e135 100644 --- a/plugins/kafka/src/components/ConsumerGroupOffsets/ConsumerGroupOffsets.test.tsx +++ b/plugins/kafka/src/components/ConsumerGroupOffsets/ConsumerGroupOffsets.test.tsx @@ -27,6 +27,7 @@ describe('ConsumerGroupOffsets', () => { const rendered = await renderInTestApp( {}} From 49a67732f97f52301fd85478111185eb00fe921e Mon Sep 17 00:00:00 2001 From: Dominik Henneke Date: Thu, 21 Jan 2021 17:52:24 +0100 Subject: [PATCH 015/131] Ask the SonarQube server for all support metrics prior to querying them for a project --- .changeset/loud-terms-kiss.md | 5 ++ .../sonarqube/src/api/SonarQubeClient.test.ts | 59 ++++++++++++++++++- plugins/sonarqube/src/api/SonarQubeClient.ts | 19 +++++- 3 files changed, 77 insertions(+), 6 deletions(-) create mode 100644 .changeset/loud-terms-kiss.md diff --git a/.changeset/loud-terms-kiss.md b/.changeset/loud-terms-kiss.md new file mode 100644 index 0000000000..da50e56e3a --- /dev/null +++ b/.changeset/loud-terms-kiss.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-sonarqube': patch +--- + +Ask the SonarQube server for all support metrics prior to querying them for a project. diff --git a/plugins/sonarqube/src/api/SonarQubeClient.test.ts b/plugins/sonarqube/src/api/SonarQubeClient.test.ts index 1b3b5c5709..e5cbc2ccdf 100644 --- a/plugins/sonarqube/src/api/SonarQubeClient.test.ts +++ b/plugins/sonarqube/src/api/SonarQubeClient.test.ts @@ -29,7 +29,31 @@ describe('SonarQubeClient', () => { const mockBaseUrl = 'http://backstage:9191/api/proxy'; const discoveryApi = UrlPatternDiscovery.compile(mockBaseUrl); - const setupHandlers = () => { + const setupHandlers = ( + metricKeys = [ + 'alert_status', + 'bugs', + 'reliability_rating', + 'vulnerabilities', + 'security_rating', + 'security_hotspots_reviewed', + 'security_review_rating', + 'code_smells', + 'sqale_rating', + 'coverage', + 'duplicated_lines_density', + ], + ) => { + server.use( + rest.get(`${mockBaseUrl}/sonarqube/metrics/search`, (_, res, ctx) => { + return res( + ctx.json({ + metrics: metricKeys.map(k => ({ key: k })), + }), + ); + }), + ); + server.use( rest.get(`${mockBaseUrl}/sonarqube/components/show`, (req, res, ctx) => { expect(req.url.searchParams.toString()).toBe('component=our-service'); @@ -46,8 +70,9 @@ describe('SonarQubeClient', () => { server.use( rest.get(`${mockBaseUrl}/sonarqube/measures/search`, (req, res, ctx) => { expect(req.url.searchParams.toString()).toBe( - 'projectKeys=our-service&metricKeys=alert_status%2Cbugs%2Creliability_rating%2Cvulnerabilities%2Csecurity_rating%2Csecurity_hotspots_reviewed%2Csecurity_review_rating%2Ccode_smells%2Csqale_rating%2Ccoverage%2Cduplicated_lines_density', + `projectKeys=our-service&metricKeys=${metricKeys.join('%2C')}`, ); + return res( ctx.json({ measures: [ @@ -111,7 +136,7 @@ describe('SonarQubeClient', () => { value: '1.0', component: 'our-service', }, - ], + ].filter(m => metricKeys.includes(m.metric)), } as MeasuresWrapper), ); }), @@ -187,4 +212,32 @@ describe('SonarQubeClient', () => { 'http://a.instance.local/component_measures?id=our-service&metric=coverage&resolved=false&view=list', ); }); + + it('should only request selected metrics', async () => { + setupHandlers(['alert_status', 'bugs']); + + const client = new SonarQubeClient({ + discoveryApi, + baseUrl: 'http://a.instance.local', + }); + + const summary = await client.getFindingSummary('our-service'); + + expect(summary).toEqual( + expect.objectContaining({ + lastAnalysis: '2020-01-01T00:00:00Z', + metrics: { + alert_status: 'OK', + bugs: '2', + }, + projectUrl: 'http://a.instance.local/dashboard?id=our-service', + }) as FindingSummary, + ); + expect(summary?.getIssuesUrl('CODE_SMELL')).toEqual( + 'http://a.instance.local/project/issues?id=our-service&types=CODE_SMELL&resolved=false', + ); + expect(summary?.getComponentMeasuresUrl('COVERAGE')).toEqual( + 'http://a.instance.local/component_measures?id=our-service&metric=coverage&resolved=false&view=list', + ); + }); }); diff --git a/plugins/sonarqube/src/api/SonarQubeClient.ts b/plugins/sonarqube/src/api/SonarQubeClient.ts index 061919dd41..35035b33c5 100644 --- a/plugins/sonarqube/src/api/SonarQubeClient.ts +++ b/plugins/sonarqube/src/api/SonarQubeClient.ts @@ -43,6 +43,13 @@ export class SonarQubeClient implements SonarQubeApi { return undefined; } + private async getSupportedMetrics(): Promise { + const result = await this.callApi<{ metrics: Array<{ key: string }> }>( + 'metrics/search', + ); + return result?.metrics?.map(m => m.key) ?? []; + } + async getFindingSummary( componentKey?: string, ): Promise { @@ -71,10 +78,16 @@ export class SonarQubeClient implements SonarQubeApi { duplicated_lines_density: undefined, }; + // select the metrics that are supported by the SonarQube instance + const supportedMetrics = await this.getSupportedMetrics(); + const metricKeys = Object.keys(metrics).filter(m => + supportedMetrics.includes(m), + ); + const measures = await this.callApi( - `measures/search?projectKeys=${componentKey}&metricKeys=${Object.keys( - metrics, - ).join(',')}`, + `measures/search?projectKeys=${componentKey}&metricKeys=${metricKeys.join( + ',', + )}`, ); if (!measures) { return undefined; From 6800da78d9237da704105d9d49797c51b31be4c6 Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Thu, 21 Jan 2021 22:26:34 +0100 Subject: [PATCH 016/131] integration: Fix default branch API url for hosted bitbucket server --- .changeset/eleven-lamps-hide.md | 5 +++++ packages/integration/src/bitbucket/core.test.ts | 6 +++--- packages/integration/src/bitbucket/core.ts | 8 ++++++-- 3 files changed, 14 insertions(+), 5 deletions(-) create mode 100644 .changeset/eleven-lamps-hide.md diff --git a/.changeset/eleven-lamps-hide.md b/.changeset/eleven-lamps-hide.md new file mode 100644 index 0000000000..809de25c82 --- /dev/null +++ b/.changeset/eleven-lamps-hide.md @@ -0,0 +1,5 @@ +--- +'@backstage/integration': patch +--- + +Fix default branch API url for custom hosted Bitbucket server diff --git a/packages/integration/src/bitbucket/core.test.ts b/packages/integration/src/bitbucket/core.test.ts index 39707976fe..1287ab4d66 100644 --- a/packages/integration/src/bitbucket/core.test.ts +++ b/packages/integration/src/bitbucket/core.test.ts @@ -116,7 +116,7 @@ describe('bitbucket core', () => { }; worker.use( rest.get( - 'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/branches/default', + 'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/default-branch', (_, res, ctx) => res( ctx.status(200), @@ -144,7 +144,7 @@ describe('bitbucket core', () => { }; worker.use( rest.get( - 'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/branches/default', + 'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/default-branch', (_, res, ctx) => res( ctx.status(200), @@ -231,7 +231,7 @@ describe('bitbucket core', () => { }; worker.use( rest.get( - 'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/branches/default', + 'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/default-branch', (_, res, ctx) => res( ctx.status(200), diff --git a/packages/integration/src/bitbucket/core.ts b/packages/integration/src/bitbucket/core.ts index ae61df497d..f5235d8189 100644 --- a/packages/integration/src/bitbucket/core.ts +++ b/packages/integration/src/bitbucket/core.ts @@ -31,9 +31,10 @@ export async function getBitbucketDefaultBranch( const { name: repoName, owner: project, resource } = parseGitUrl(url); const isHosted = resource === 'bitbucket.org'; + // Bitbucket Server https://docs.atlassian.com/bitbucket-server/rest/7.9.0/bitbucket-rest.html#idp184 const branchUrl = isHosted ? `${config.apiBaseUrl}/repositories/${project}/${repoName}` - : `${config.apiBaseUrl}/projects/${project}/repos/${repoName}/branches/default`; + : `${config.apiBaseUrl}/projects/${project}/repos/${repoName}/default-branch`; const response = await fetch(branchUrl, getBitbucketRequestOptions(config)); if (!response.ok) { @@ -50,7 +51,10 @@ export async function getBitbucketDefaultBranch( defaultBranch = displayId; } if (!defaultBranch) { - throw new Error(`Failed to read default branch from ${branchUrl}`); + throw new Error( + `Failed to read default branch from ${branchUrl}. ` + + `Response ${response.status} ${response.json()}`, + ); } return defaultBranch; } From fac91bcc5fcdc02954722f37f884cb70a75cda03 Mon Sep 17 00:00:00 2001 From: Tim Hansen Date: Fri, 22 Jan 2021 13:05:58 -0700 Subject: [PATCH 017/131] Add support for additional breakdowns of daily cost data Hide expansion option if not enough breakdowns --- .changeset/cost-insights-yellow-trees-love.md | 6 + plugins/cost-insights/src/client.ts | 14 ++- ...art.tsx => CostOverviewBreakdownChart.tsx} | 115 +++++++++--------- .../CostOverviewCard.test.tsx | 89 ++++++++++++++ .../CostOverviewCard/CostOverviewCard.tsx | 45 ++++--- plugins/cost-insights/src/types/Cost.ts | 2 +- plugins/cost-insights/src/utils/mockData.ts | 15 +++ 7 files changed, 209 insertions(+), 77 deletions(-) create mode 100644 .changeset/cost-insights-yellow-trees-love.md rename plugins/cost-insights/src/components/CostOverviewCard/{CostOverviewByProductChart.tsx => CostOverviewBreakdownChart.tsx} (67%) create mode 100644 plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.test.tsx diff --git a/.changeset/cost-insights-yellow-trees-love.md b/.changeset/cost-insights-yellow-trees-love.md new file mode 100644 index 0000000000..19eea0dfe2 --- /dev/null +++ b/.changeset/cost-insights-yellow-trees-love.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-cost-insights': minor +--- + +Add support for additional breakdowns of daily cost data. +This changes the type of Cost.groupedCosts returned by CostInsightsApi.getGroupDailyCost. diff --git a/plugins/cost-insights/src/client.ts b/plugins/cost-insights/src/client.ts index 0ba57f76fa..c43deba6ae 100644 --- a/plugins/cost-insights/src/client.ts +++ b/plugins/cost-insights/src/client.ts @@ -33,11 +33,12 @@ import { UnlabeledDataflowAlert, } from '../src/utils/alerts'; import { - trendlineOf, + aggregationFor, changeOf, entityOf, getGroupedProducts, - aggregationFor, + getGroupedProjects, + trendlineOf, } from './utils/mockData'; export class ExampleCostInsightsClient implements CostInsightsApi { @@ -101,7 +102,10 @@ export class ExampleCostInsightsClient implements CostInsightsApi { trendline: trendlineOf(aggregation), // Optional field on Cost which needs to be supplied in order to see // the product breakdown view in the top panel. - groupedCosts: getGroupedProducts(intervals), + groupedCosts: { + product: getGroupedProducts(intervals), + project: getGroupedProjects(intervals), + }, }, ); @@ -119,7 +123,9 @@ export class ExampleCostInsightsClient implements CostInsightsApi { trendline: trendlineOf(aggregation), // Optional field on Cost which needs to be supplied in order to see // the product breakdown view in the top panel. - groupedCosts: getGroupedProducts(intervals), + groupedCosts: { + product: getGroupedProducts(intervals), + }, }, ); diff --git a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewByProductChart.tsx b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewBreakdownChart.tsx similarity index 67% rename from plugins/cost-insights/src/components/CostOverviewCard/CostOverviewByProductChart.tsx rename to plugins/cost-insights/src/components/CostOverviewCard/CostOverviewBreakdownChart.tsx index b562434c5e..feec23933f 100644 --- a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewByProductChart.tsx +++ b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewBreakdownChart.tsx @@ -56,26 +56,26 @@ import { BarChartLegendOptions } from '../BarChart/BarChartLegend'; dayjs.extend(utc); -export type CostOverviewByProductChartProps = { - costsByProduct: Cost[]; +export type CostOverviewBreakdownChartProps = { + costBreakdown: Cost[]; }; const LOW_COST_THRESHOLD = 0.1; -export const CostOverviewByProductChart = ({ - costsByProduct, -}: CostOverviewByProductChartProps) => { +export const CostOverviewBreakdownChart = ({ + costBreakdown, +}: CostOverviewBreakdownChartProps) => { const theme = useTheme(); const classes = useStyles(theme); const lastCompleteBillingDate = useLastCompleteBillingDate(); const { duration } = useFilters(mapFiltersToProps); const [isExpanded, setExpanded] = useState(false); - if (!costsByProduct) { + if (!costBreakdown) { return null; } - const flattenedAggregation = costsByProduct + const flattenedAggregation = costBreakdown .map(cost => cost.aggregation) .flat(); @@ -87,44 +87,46 @@ export const CostOverviewByProductChart = ({ lastCompleteBillingDate, ); const currentPeriodTotal = totalCost - previousPeriodTotal; - const otherProducts: string[] = []; + const canExpand = costBreakdown.length >= 8; + const otherCategoryIds: string[] = []; - const productsByDate = costsByProduct.reduce((prodByDate, product) => { - const productTotal = aggregationSum(product.aggregation); - // Group products with less than 10% of the total cost into "Other" category - // when we have >= 8 products. - const isOtherProduct = - costsByProduct.length >= 8 && - productTotal < totalCost * LOW_COST_THRESHOLD; + const breakdownsByDate = costBreakdown.reduce( + (breakdownByDate, breakdown) => { + const breakdownTotal = aggregationSum(breakdown.aggregation); + // Group breakdown items with less than 10% of the total cost into "Other" category if needed + const isOtherCategory = + canExpand && breakdownTotal < totalCost * LOW_COST_THRESHOLD; - const updatedProdByDate = { ...prodByDate }; - if (isOtherProduct) { - otherProducts.push(product.id); - } - product.aggregation.forEach(curAggregation => { - const productCostsForDate = updatedProdByDate[curAggregation.date] || {}; + const updatedBreakdownByDate = { ...breakdownByDate }; + if (isOtherCategory) { + otherCategoryIds.push(breakdown.id); + } + breakdown.aggregation.forEach(curAggregation => { + const costsForDate = updatedBreakdownByDate[curAggregation.date] || {}; - updatedProdByDate[curAggregation.date] = { - ...productCostsForDate, - [product.id]: - (productCostsForDate[product.id] || 0) + curAggregation.amount, - }; - }); + updatedBreakdownByDate[curAggregation.date] = { + ...costsForDate, + [breakdown.id]: + (costsForDate[breakdown.id] || 0) + curAggregation.amount, + }; + }); - return updatedProdByDate; - }, {} as Record>); + return updatedBreakdownByDate; + }, + {} as Record>, + ); - const chartData: Record[] = Object.keys(productsByDate).map( + const chartData: Record[] = Object.keys(breakdownsByDate).map( date => { - const costsForDate = Object.keys(productsByDate[date]).reduce( - (dateCosts, product) => { - // Group costs for products that belong to 'Other' in the chart. - const cost = productsByDate[date][product]; - const productCost = - !isExpanded && otherProducts.includes(product) + const costsForDate = Object.keys(breakdownsByDate[date]).reduce( + (dateCosts, breakdown) => { + // Group costs for items that belong to 'Other' in the chart. + const cost = breakdownsByDate[date][breakdown]; + const breakdownCost = + !isExpanded && otherCategoryIds.includes(breakdown) ? { Other: (dateCosts.Other || 0) + cost } - : { [product]: cost }; - return { ...dateCosts, ...productCost }; + : { [breakdown]: cost }; + return { ...dateCosts, ...breakdownCost }; }, {} as Record, ); @@ -135,40 +137,41 @@ export const CostOverviewByProductChart = ({ }, ); - const sortedProducts = costsByProduct.sort( + const sortedBreakdowns = costBreakdown.sort( (a, b) => aggregationSum(a.aggregation) - aggregationSum(b.aggregation), ); const renderAreas = () => { - const separatedProducts = sortedProducts - // Check that product is a separate group and hasn't been added to 'Other' + const separatedBreakdowns = sortedBreakdowns + // Check that the breakdown is a separate group and hasn't been added to 'Other' .filter( - product => - product.id !== 'Other' && !otherProducts.includes(product.id), + breakdown => + breakdown.id !== 'Other' && !otherCategoryIds.includes(breakdown.id), ) - .map(product => product.id); + .map(breakdown => breakdown.id); // Keep 'Other' category at the bottom of the stack - const productsToDisplay = isExpanded - ? sortedProducts.map(product => product.id) - : ['Other', ...separatedProducts]; + const breakdownsToDisplay = isExpanded + ? sortedBreakdowns.map(breakdown => breakdown.id) + : ['Other', ...separatedBreakdowns]; - return productsToDisplay.map((product, i) => { - // Logic to handle case where there are more products than data viz colors. - const productColor = + return breakdownsToDisplay.map((breakdown, i) => { + // Logic to handle case where there are more items than data viz colors. + const color = theme.palette.dataViz[ - (productsToDisplay.length - 1 - i) % + (breakdownsToDisplay.length - 1 - i) % (theme.palette.dataViz.length - 1) ]; return ( setExpanded(true)} style={{ - cursor: product === 'Other' && !isExpanded ? 'pointer' : null, + cursor: breakdown === 'Other' && !isExpanded ? 'pointer' : null, }} /> ); @@ -206,7 +209,7 @@ export const CostOverviewByProductChart = ({ {items.reverse().map((item, index) => ( ))} - {!isExpanded ? expandText : null} + {canExpand && !isExpanded ? expandText : null} ); }; diff --git a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.test.tsx b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.test.tsx new file mode 100644 index 0000000000..6fca8f67c9 --- /dev/null +++ b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.test.tsx @@ -0,0 +1,89 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { fireEvent } from '@testing-library/react'; +import { renderInTestApp } from '@backstage/test-utils'; +import { CostOverviewCard } from './CostOverviewCard'; +import { Cost } from '../../types'; +import { + changeOf, + getGroupedProducts, + getGroupedProjects, + MockAggregatedDailyCosts, + trendlineOf, +} from '../../utils/mockData'; +import { + MockBillingDateProvider, + MockConfigProvider, + MockFilterProvider, + MockScrollProvider, +} from '../../utils/tests'; +import { CostInsightsThemeProvider } from '../CostInsightsPage/CostInsightsThemeProvider'; + +const mockGroupDailyCost: Cost = { + id: 'test-group', + aggregation: MockAggregatedDailyCosts, + change: changeOf(MockAggregatedDailyCosts), + trendline: trendlineOf(MockAggregatedDailyCosts), +}; + +function renderInContext(children: JSX.Element) { + return renderInTestApp( + + + + + {children} + + + + , + ); +} + +describe('', () => { + it('Renders without exploding', async () => { + const { getByText } = await renderInContext( + , + ); + expect(getByText('Cloud Cost')).toBeInTheDocument(); + }); + + it('Shows breakdown tabs if provided', async () => { + const mockDailyCostWithBreakdowns = { + ...mockGroupDailyCost, + groupedCosts: { + product: getGroupedProducts('R2/P90D/2021-01-01'), + project: getGroupedProjects('R2/P90D/2021-01-01'), + }, + }; + const { getByText } = await renderInContext( + , + ); + expect(getByText('Cloud Cost')).toBeInTheDocument(); + expect(getByText('Breakdown by product')).toBeInTheDocument(); + expect(getByText('Breakdown by project')).toBeInTheDocument(); + + fireEvent.click(getByText('Breakdown by product')); + expect(getByText('Cloud Cost By Product')).toBeInTheDocument(); + + fireEvent.click(getByText('Breakdown by project')); + expect(getByText('Cloud Cost By Project')).toBeInTheDocument(); + }); +}); diff --git a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.tsx b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.tsx index 39adc4bbf1..614e045b20 100644 --- a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.tsx +++ b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.tsx @@ -14,22 +14,22 @@ * limitations under the License. */ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { Box, Card, CardContent, Divider, - useTheme, Tab, Tabs, + useTheme, } from '@material-ui/core'; import { CostOverviewChart } from './CostOverviewChart'; -import { CostOverviewByProductChart } from './CostOverviewByProductChart'; +import { CostOverviewBreakdownChart } from './CostOverviewBreakdownChart'; import { CostOverviewHeader } from './CostOverviewHeader'; import { MetricSelect } from '../MetricSelect'; import { PeriodSelect } from '../PeriodSelect'; -import { useScroll, useFilters, useConfig } from '../../hooks'; +import { useConfig, useFilters, useScroll } from '../../hooks'; import { mapFiltersToProps } from './selector'; import { DefaultNavigation } from '../../utils/navigation'; import { findAlways } from '../../utils/assert'; @@ -49,6 +49,15 @@ export const CostOverviewCard = ({ const config = useConfig(); const [tabIndex, setTabIndex] = useState(0); + // Reset tabIndex if breakdowns available change + useEffect(() => { + // Intentionally off-by-one to account for the overview tab + const lastIndex = Object.keys(dailyCostData.groupedCosts ?? {}).length; + if (tabIndex > lastIndex) { + setTabIndex(0); + } + }, [dailyCostData, tabIndex, setTabIndex]); + const { ScrollAnchor } = useScroll(DefaultNavigation.CostOverviewCard); const { setDuration, setProject, setMetric, ...filters } = useFilters( mapFiltersToProps, @@ -59,14 +68,18 @@ export const CostOverviewCard = ({ : null; const styles = useOverviewTabsStyles(theme); + const breakdownTabs = Object.keys(dailyCostData.groupedCosts ?? {}).map( + key => ({ + id: key, + label: `Breakdown by ${key}`, + title: `Cloud Cost By ${key.charAt(0).toUpperCase() + key.slice(1)}`, + }), + ); const tabs = [ { id: 'overview', label: 'Total cost', title: 'Cloud Cost' }, - { - id: 'breakdown', - label: 'Breakdown by product', - title: 'Cloud Cost By Product', - }, - ]; + ].concat(breakdownTabs); + // tabIndex can temporarily be invalid while the useEffect above processes + const safeTabIndex = tabIndex > tabs.length - 1 ? 0 : tabIndex; const OverviewTabs = () => { return ( @@ -74,7 +87,7 @@ export const CostOverviewCard = ({ setTabIndex(index)} - value={tabIndex} + value={safeTabIndex} > {tabs.map((tab, index) => ( {dailyCostData.groupedCosts && } - + - {tabIndex === 0 ? ( + {safeTabIndex === 0 ? ( ) : ( - )} diff --git a/plugins/cost-insights/src/types/Cost.ts b/plugins/cost-insights/src/types/Cost.ts index dd28bba6b0..c3b63946af 100644 --- a/plugins/cost-insights/src/types/Cost.ts +++ b/plugins/cost-insights/src/types/Cost.ts @@ -23,5 +23,5 @@ export interface Cost { aggregation: DateAggregation[]; change?: ChangeStatistic; trendline?: Trendline; - groupedCosts?: Cost[]; + groupedCosts?: Record; } diff --git a/plugins/cost-insights/src/utils/mockData.ts b/plugins/cost-insights/src/utils/mockData.ts index f84473b84f..14ff9aecd1 100644 --- a/plugins/cost-insights/src/utils/mockData.ts +++ b/plugins/cost-insights/src/utils/mockData.ts @@ -1064,3 +1064,18 @@ export const getGroupedProducts = (intervals: string) => [ aggregation: aggregationFor(intervals, 250), }, ]; + +export const getGroupedProjects = (intervals: string) => [ + { + id: 'project-a', + aggregation: aggregationFor(intervals, 1_700), + }, + { + id: 'project-b', + aggregation: aggregationFor(intervals, 350), + }, + { + id: 'project-c', + aggregation: aggregationFor(intervals, 1_300), + }, +]; From 6811112282b2c5b0fd275f1335dae3af12e814f1 Mon Sep 17 00:00:00 2001 From: Jonah Back Date: Sat, 23 Jan 2021 16:00:18 -0800 Subject: [PATCH 018/131] feature: add aws iam auth translator for kubernetes --- .changeset/chatty-cooks-begin.md | 6 ++ plugins/kubernetes-backend/package.json | 3 + plugins/kubernetes-backend/schema.d.ts | 4 +- .../AwsIamKubernetesAuthTranslator.test.ts | 63 ++++++++++++++++ .../AwsIamKubernetesAuthTranslator.ts | 73 +++++++++++++++++++ .../KubernetesAuthTranslatorGenerator.test.ts | 8 ++ .../KubernetesAuthTranslatorGenerator.ts | 4 + plugins/kubernetes-backend/src/types/types.ts | 2 +- plugins/kubernetes/schema.d.ts | 4 +- yarn.lock | 70 ++++++++++++++++++ 10 files changed, 232 insertions(+), 5 deletions(-) create mode 100644 .changeset/chatty-cooks-begin.md create mode 100644 plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.test.ts create mode 100644 plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts diff --git a/.changeset/chatty-cooks-begin.md b/.changeset/chatty-cooks-begin.md new file mode 100644 index 0000000000..e32d22d532 --- /dev/null +++ b/.changeset/chatty-cooks-begin.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-kubernetes': patch +'@backstage/plugin-kubernetes-backend': patch +--- + +Add AWS auth provider for Kubernetes diff --git a/plugins/kubernetes-backend/package.json b/plugins/kubernetes-backend/package.json index 56c6707427..005708c059 100644 --- a/plugins/kubernetes-backend/package.json +++ b/plugins/kubernetes-backend/package.json @@ -31,11 +31,14 @@ "clean": "backstage-cli clean" }, "dependencies": { + "@aws-sdk/credential-provider-node": "^3.3.0", "@backstage/backend-common": "^0.4.3", "@backstage/catalog-model": "^0.6.1", "@backstage/config": "^0.1.2", "@kubernetes/client-node": "^0.13.2", + "@types/aws4": "^1.5.1", "@types/express": "^4.17.6", + "aws4": "^1.11.0", "compression": "^1.7.4", "cors": "^2.8.5", "express": "^4.17.1", diff --git a/plugins/kubernetes-backend/schema.d.ts b/plugins/kubernetes-backend/schema.d.ts index 862cb6d2ce..31b6a2022b 100644 --- a/plugins/kubernetes-backend/schema.d.ts +++ b/plugins/kubernetes-backend/schema.d.ts @@ -20,8 +20,8 @@ export interface Config { clusters: { url: string; name: string; - serviceAccountToken: string; - authProvider: 'serviceAccount'; + serviceAccountToken: string | undefined; + authProvider: 'aws' | 'google' | 'serviceAccount'; }[]; }; } diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.test.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.test.ts new file mode 100644 index 0000000000..4411658ee6 --- /dev/null +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.test.ts @@ -0,0 +1,63 @@ +/* + * 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. + */ +const mockCredentialProvider = jest.fn(); +jest.mock('@aws-sdk/credential-provider-node', () => { + return { + defaultProvider: () => mockCredentialProvider, + }; +}); + +import { AwsIamKubernetesAuthTranslator } from './AwsIamKubernetesAuthTranslator'; + +describe('AwsIamKubernetesAuthTranslator tests', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + it('returns a signed url for aws credentials', async () => { + const authTranslator = new AwsIamKubernetesAuthTranslator(); + + mockCredentialProvider.mockImplementation(async () => { + // These credentials are not real. + // Pulled from example in docs: https://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html + return { + accessKeyId: 'AKIAIOSFODNN7EXAMPLE', + secretKeyId: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY', + }; + }); + + const clusterDetails = await authTranslator.decorateClusterDetailsWithAuth({ + name: 'test-cluster', + url: '', + authProvider: 'aws', + }); + expect(clusterDetails.serviceAccountToken).toBeDefined(); + }); + + it('throws when unable to get aws credentials', async () => { + const authTranslator = new AwsIamKubernetesAuthTranslator(); + + mockCredentialProvider.mockImplementation(async () => { + throw new Error('not implemented'); + }); + + const promise = authTranslator.decorateClusterDetailsWithAuth({ + name: 'test-cluster', + url: '', + authProvider: 'aws', + }); + await expect(promise).rejects.toThrow('not implemented'); + }); +}); diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts new file mode 100644 index 0000000000..3b5ea59f18 --- /dev/null +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts @@ -0,0 +1,73 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { defaultProvider } from '@aws-sdk/credential-provider-node'; +import { sign } from 'aws4'; +import { KubernetesAuthTranslator } from './types'; +import { ClusterDetails } from '..'; + +const base64 = (str: string) => + Buffer.from(str.toString(), 'binary').toString('base64'); +const prepend = (prep: string) => (str: string) => prep + str; +const replace = (search: string | RegExp, substitution: string) => ( + str: string, +) => str.replace(search, substitution); +const pipe = (fns: ReadonlyArray) => (thing: string): string => + fns.reduce((val, fn) => fn(val), thing); +const removePadding = replace(/=+$/, ''); +const makeUrlSafe = pipe([replace('+', '-'), replace('/', '_')]); + +export class AwsIamKubernetesAuthTranslator + implements KubernetesAuthTranslator { + async getBearerToken(clusterName: string): Promise { + const credentialProvider = defaultProvider(); + const credentials = await credentialProvider(); + const request = { + host: `sts.amazonaws.com`, + path: `/?Action=GetCallerIdentity&Version=2011-06-15&X-Amz-Expires=60`, + headers: { + 'x-k8s-aws-id': clusterName, + }, + signQuery: true, + }; + const signedRequest = sign(request, { + accessKeyId: credentials.accessKeyId, + secretAccessKey: credentials.secretAccessKey, + sessionToken: credentials.sessionToken, + }); + + return pipe([ + (signed: any) => `https://${signed.host}${signed.path}`, + base64, + removePadding, + makeUrlSafe, + prepend('k8s-aws-v1.'), + ])(signedRequest); + } + + async decorateClusterDetailsWithAuth( + clusterDetails: ClusterDetails, + ): Promise { + const clusterDetailsWithAuthToken: ClusterDetails = Object.assign( + {}, + clusterDetails, + ); + + clusterDetailsWithAuthToken.serviceAccountToken = await this.getBearerToken( + clusterDetails.name, + ); + return clusterDetailsWithAuthToken; + } +} diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/KubernetesAuthTranslatorGenerator.test.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/KubernetesAuthTranslatorGenerator.test.ts index 3b27eb012a..ec2f6ef120 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/KubernetesAuthTranslatorGenerator.test.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/KubernetesAuthTranslatorGenerator.test.ts @@ -18,6 +18,7 @@ import { KubernetesAuthTranslator } from './types'; import { GoogleKubernetesAuthTranslator } from './GoogleKubernetesAuthTranslator'; import { KubernetesAuthTranslatorGenerator } from './KubernetesAuthTranslatorGenerator'; import { ServiceAccountKubernetesAuthTranslator } from './ServiceAccountKubernetesAuthTranslator'; +import { AwsIamKubernetesAuthTranslator } from './AwsIamKubernetesAuthTranslator'; describe('getKubernetesAuthTranslatorInstance', () => { const sut = KubernetesAuthTranslatorGenerator; @@ -29,6 +30,13 @@ describe('getKubernetesAuthTranslatorInstance', () => { expect(authTranslator instanceof GoogleKubernetesAuthTranslator).toBe(true); }); + it('can return an auth translator for aws auth', () => { + const authTranslator: KubernetesAuthTranslator = sut.getKubernetesAuthTranslatorInstance( + 'aws', + ); + expect(authTranslator instanceof AwsIamKubernetesAuthTranslator).toBe(true); + }); + it('can return an auth translator for serviceAccount auth', () => { const authTranslator: KubernetesAuthTranslator = sut.getKubernetesAuthTranslatorInstance( 'serviceAccount', diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/KubernetesAuthTranslatorGenerator.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/KubernetesAuthTranslatorGenerator.ts index f7cfc92112..4ae2aff35a 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/KubernetesAuthTranslatorGenerator.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/KubernetesAuthTranslatorGenerator.ts @@ -17,6 +17,7 @@ import { KubernetesAuthTranslator } from './types'; import { GoogleKubernetesAuthTranslator } from './GoogleKubernetesAuthTranslator'; import { ServiceAccountKubernetesAuthTranslator } from './ServiceAccountKubernetesAuthTranslator'; +import { AwsIamKubernetesAuthTranslator } from './AwsIamKubernetesAuthTranslator'; export class KubernetesAuthTranslatorGenerator { static getKubernetesAuthTranslatorInstance( @@ -26,6 +27,9 @@ export class KubernetesAuthTranslatorGenerator { case 'google': { return new GoogleKubernetesAuthTranslator(); } + case 'aws': { + return new AwsIamKubernetesAuthTranslator(); + } case 'serviceAccount': { return new ServiceAccountKubernetesAuthTranslator(); } diff --git a/plugins/kubernetes-backend/src/types/types.ts b/plugins/kubernetes-backend/src/types/types.ts index cf6576153b..86191c6d72 100644 --- a/plugins/kubernetes-backend/src/types/types.ts +++ b/plugins/kubernetes-backend/src/types/types.ts @@ -148,4 +148,4 @@ export interface KubernetesFetchError { export type ServiceLocatorMethod = 'multiTenant' | 'http'; // TODO implement http export type ClusterLocatorMethod = 'config'; -export type AuthProviderType = 'google' | 'serviceAccount'; +export type AuthProviderType = 'google' | 'serviceAccount' | 'aws'; diff --git a/plugins/kubernetes/schema.d.ts b/plugins/kubernetes/schema.d.ts index f3f3c98f8c..39598edb39 100644 --- a/plugins/kubernetes/schema.d.ts +++ b/plugins/kubernetes/schema.d.ts @@ -35,11 +35,11 @@ export interface Config { /** * @visibility secret */ - serviceAccountToken: string; + serviceAccountToken: string | undefined; /** * @visibility frontend */ - authProvider: 'serviceAccount'; + authProvider: 'aws' | 'google' | 'serviceAccount'; }[]; }; } diff --git a/yarn.lock b/yarn.lock index 6710c7e8e7..87637142c0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -263,6 +263,15 @@ "@aws-sdk/property-provider" "3.1.0" tslib "^1.8.0" +"@aws-sdk/credential-provider-env@3.3.0": + version "3.3.0" + resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.3.0.tgz#7930e504a7a79ab98a9fd34befc5c84b8c4df679" + integrity sha512-kyqZMlGdH/05IhuXLBUXtj5+hhRfYiHFcJLc3ts/uiwCixswVHPAYHgyWm9ajFkmWtpz6ih+0LoYryhPbYu01A== + dependencies: + "@aws-sdk/property-provider" "3.3.0" + "@aws-sdk/types" "3.1.0" + tslib "^1.8.0" + "@aws-sdk/credential-provider-imds@3.1.0": version "3.1.0" resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-imds/-/credential-provider-imds-3.1.0.tgz#33d48753dc00bddce79d2aa8076a7cb5bf8562df" @@ -271,6 +280,15 @@ "@aws-sdk/property-provider" "3.1.0" tslib "^1.8.0" +"@aws-sdk/credential-provider-imds@3.3.0": + version "3.3.0" + resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-imds/-/credential-provider-imds-3.3.0.tgz#ff0cf5489853c16d23fc99d7bae425587e836c40" + integrity sha512-Cx0YMnO/ScGQVDns006bLbqOxNURGN2Xm21bCY0l0ZUJCdJ2va1/9q1rljDyw2KvdzZNQVRQII3uUgj/Oq/K+g== + dependencies: + "@aws-sdk/property-provider" "3.3.0" + "@aws-sdk/types" "3.1.0" + tslib "^1.8.0" + "@aws-sdk/credential-provider-ini@3.1.0": version "3.1.0" resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.1.0.tgz#1a6cf9ab9fa1450d4472b9e371099b0c0283349b" @@ -280,6 +298,16 @@ "@aws-sdk/shared-ini-file-loader" "3.1.0" tslib "^1.8.0" +"@aws-sdk/credential-provider-ini@3.3.0": + version "3.3.0" + resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.3.0.tgz#55fe8f391b72d30e650ba8bc680e82bbeacbbfe5" + integrity sha512-zawNFJoasXiaV5n0H3/KNOi7mAZ7mHpG1+nBEkoWhZ31lIUM9+heGPcxKCbf/pMQjiOebUqL1OpWe4uSWxIVMw== + dependencies: + "@aws-sdk/property-provider" "3.3.0" + "@aws-sdk/shared-ini-file-loader" "3.1.0" + "@aws-sdk/types" "3.1.0" + tslib "^1.8.0" + "@aws-sdk/credential-provider-node@3.1.0": version "3.1.0" resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.1.0.tgz#89bc8803752a3e580c6f2410306c7edad6be7fa2" @@ -292,6 +320,19 @@ "@aws-sdk/property-provider" "3.1.0" tslib "^1.8.0" +"@aws-sdk/credential-provider-node@^3.3.0": + version "3.3.0" + resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.3.0.tgz#5c97323fa7b23590070d06aa7b1be8d93b2bf4be" + integrity sha512-PPBNzPq8fHk9dEQTTE4iJi6ZWtmo057Lc+I8Rlzmvz6NthK9iKiU819tfaxVBb6ZR7bLP0BuDiCi4G1lD+rQnQ== + dependencies: + "@aws-sdk/credential-provider-env" "3.3.0" + "@aws-sdk/credential-provider-imds" "3.3.0" + "@aws-sdk/credential-provider-ini" "3.3.0" + "@aws-sdk/credential-provider-process" "3.3.0" + "@aws-sdk/property-provider" "3.3.0" + "@aws-sdk/types" "3.1.0" + tslib "^1.8.0" + "@aws-sdk/credential-provider-process@3.1.0": version "3.1.0" resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.1.0.tgz#ff817b29a9760c463b77be3ce49375eaeb753ef3" @@ -302,6 +343,17 @@ "@aws-sdk/shared-ini-file-loader" "3.1.0" tslib "^1.8.0" +"@aws-sdk/credential-provider-process@3.3.0": + version "3.3.0" + resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.3.0.tgz#9de0984bd6dd0f5e40cff3672d7dd19e8cd43074" + integrity sha512-7oOF1j6ydUq43P3SsasiIpbxMKCmT0C+XwggHTGiVxNtX+QZiH1vdMf8otA7puLEey0iY5wTAIEcZhC6HenojA== + dependencies: + "@aws-sdk/credential-provider-ini" "3.3.0" + "@aws-sdk/property-provider" "3.3.0" + "@aws-sdk/shared-ini-file-loader" "3.1.0" + "@aws-sdk/types" "3.1.0" + tslib "^1.8.0" + "@aws-sdk/eventstream-marshaller@3.1.0": version "3.1.0" resolved "https://registry.npmjs.org/@aws-sdk/eventstream-marshaller/-/eventstream-marshaller-3.1.0.tgz#65a217e37abcaa162276ccb1d4487d42431d1534" @@ -634,6 +686,14 @@ dependencies: tslib "^1.8.0" +"@aws-sdk/property-provider@3.3.0": + version "3.3.0" + resolved "https://registry.npmjs.org/@aws-sdk/property-provider/-/property-provider-3.3.0.tgz#49979cb1a3e5562d51807c7403c5fd48cb9f2cdc" + integrity sha512-JTyjtXVNhFczL9IfgwXD55F6DqXL50PhfZxFW92t5dDj5VtWpOL74BbuxHQxHBgnQv1FKLr6N9cr7gfXWexDug== + dependencies: + "@aws-sdk/types" "3.1.0" + tslib "^1.8.0" + "@aws-sdk/protocol-http@3.1.0": version "3.1.0" resolved "https://registry.npmjs.org/@aws-sdk/protocol-http/-/protocol-http-3.1.0.tgz#7da0ebcf02a40a8300f3bd52f9206f25fdf1ca7f" @@ -6147,6 +6207,11 @@ resolved "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.0.tgz#14264692a9d6e2fa4db3df5e56e94b5e25647ac0" integrity sha512-iIgQNzCm0v7QMhhe4Jjn9uRh+I6GoPmt03CbEtwx3ao8/EfoQcmgtqH4vQ5Db/lxiIGaWDv6nwvunuh0RyX0+A== +"@types/aws4@^1.5.1": + version "1.5.1" + resolved "https://registry.npmjs.org/@types/aws4/-/aws4-1.5.1.tgz#361fadab198a030ab398269183ae3fa86e958ed9" + integrity sha1-Nh+tqxmKAwqzmCaRg64/qG6Vjtk= + "@types/babel__core@^7.0.0", "@types/babel__core@^7.1.7": version "7.1.9" resolved "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.9.tgz#77e59d438522a6fb898fa43dc3455c6e72f3963d" @@ -8580,6 +8645,11 @@ aws-sign2@~0.7.0: resolved "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= +aws4@^1.11.0: + version "1.11.0" + resolved "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59" + integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== + aws4@^1.8.0: version "1.9.1" resolved "https://registry.npmjs.org/aws4/-/aws4-1.9.1.tgz#7e33d8f7d449b3f673cd72deb9abdc552dbe528e" From 8387849f7ced3ee86c6ca3d4fde3da9d7c2d59cb Mon Sep 17 00:00:00 2001 From: Jonah Back Date: Sat, 23 Jan 2021 17:31:07 -0800 Subject: [PATCH 019/131] move types/aws4 to devDeps --- plugins/kubernetes-backend/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/kubernetes-backend/package.json b/plugins/kubernetes-backend/package.json index 0bf4487b54..eeb91de777 100644 --- a/plugins/kubernetes-backend/package.json +++ b/plugins/kubernetes-backend/package.json @@ -36,7 +36,6 @@ "@backstage/catalog-model": "^0.7.0", "@backstage/config": "^0.1.2", "@kubernetes/client-node": "^0.13.2", - "@types/aws4": "^1.5.1", "@types/express": "^4.17.6", "aws4": "^1.11.0", "compression": "^1.7.4", @@ -53,6 +52,7 @@ }, "devDependencies": { "@backstage/cli": "^0.4.7", + "@types/aws4": "^1.5.1", "supertest": "^4.0.2" }, "files": [ From f5ed88501ba3c687a1e2f75ec340ae7e02c951fc Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Sun, 24 Jan 2021 16:54:01 +0100 Subject: [PATCH 020/131] backend: URL Reader readTree should not expect the extracted directory name If readTree can work without it receiving the top level directory name, it will be a relief. We won't have to deduce the directory name from the API responses or by any other means. --- .../src/reading/BitbucketUrlReader.ts | 24 +------------- .../src/reading/GithubUrlReader.ts | 28 +--------------- .../src/reading/GitlabUrlReader.ts | 26 +-------------- .../reading/tree/ReadTreeResponseFactory.ts | 9 +++--- .../reading/tree/TarArchiveResponse.test.ts | 32 ++++++------------- .../src/reading/tree/TarArchiveResponse.ts | 26 ++++++++++++--- .../reading/tree/ZipArchiveResponse.test.ts | 31 ++++++------------ .../src/reading/tree/ZipArchiveResponse.ts | 29 +++++++++++++---- packages/backend-common/src/reading/types.ts | 3 ++ 9 files changed, 72 insertions(+), 136 deletions(-) diff --git a/packages/backend-common/src/reading/BitbucketUrlReader.ts b/packages/backend-common/src/reading/BitbucketUrlReader.ts index e9727e04cf..bbccfe4794 100644 --- a/packages/backend-common/src/reading/BitbucketUrlReader.ts +++ b/packages/backend-common/src/reading/BitbucketUrlReader.ts @@ -121,31 +121,9 @@ export class BitbucketUrlReader implements UrlReader { throw new Error(message); } - // Get the filename of archive from the header of the response - const contentDispositionHeader = archiveBitbucketResponse.headers.get( - 'content-disposition', - ) as string; - if (!contentDispositionHeader) { - throw new Error( - `Failed to read tree from ${url}. ` + - 'Bitbucket API response for downloading archive does not contain content-disposition header ', - ); - } - const fileNameRegEx = new RegExp( - /^attachment; filename=(?.*).zip$/, - ); - const archiveFileName = contentDispositionHeader.match(fileNameRegEx) - ?.groups?.fileName; - if (!archiveFileName) { - throw new Error( - `Failed to read tree from ${url}. Bitbucket API response for downloading archive has an unexpected ` + - `format of content-disposition header ${contentDispositionHeader} `, - ); - } - return await this.treeResponseFactory.fromZipArchive({ stream: (archiveBitbucketResponse.body as unknown) as Readable, - path: `${archiveFileName}/${filepath}`, + subpath: filepath, etag: lastCommitShortHash, filter: options?.filter, }); diff --git a/packages/backend-common/src/reading/GithubUrlReader.ts b/packages/backend-common/src/reading/GithubUrlReader.ts index 6c7cefe2ef..4f1aa7c07b 100644 --- a/packages/backend-common/src/reading/GithubUrlReader.ts +++ b/packages/backend-common/src/reading/GithubUrlReader.ts @@ -166,37 +166,11 @@ export class GithubUrlReader implements UrlReader { throw new Error(message); } - // Get the filename of archive from the header of the response - const contentDispositionHeader = archive.headers.get( - 'content-disposition', - ) as string; - if (!contentDispositionHeader) { - throw new Error( - `Failed to read tree from ${url}. ` + - 'GitHub API response for downloading archive does not contain content-disposition header ', - ); - } - const fileNameRegEx = new RegExp( - /^attachment; filename=(?.*).tar.gz$/, - ); - const archiveFileName = contentDispositionHeader.match(fileNameRegEx) - ?.groups?.fileName; - if (!archiveFileName) { - throw new Error( - `Failed to read tree from ${url}. GitHub API response for downloading archive has an unexpected ` + - `format of content-disposition header ${contentDispositionHeader} `, - ); - } - - // The path includes the name of the directory inside the tarball and a sub path - // if requested in readTree. - const path = `${archiveFileName}/${filepath}`; - return await this.deps.treeResponseFactory.fromTarArchive({ // TODO(Rugvip): Underlying implementation of fetch will be node-fetch, we probably want // to stick to using that in exclusively backend code. stream: (archive.body as unknown) as Readable, - path, + subpath: filepath, etag: commitSha, filter: options?.filter, }); diff --git a/packages/backend-common/src/reading/GitlabUrlReader.ts b/packages/backend-common/src/reading/GitlabUrlReader.ts index 654f4f9a85..22316f6895 100644 --- a/packages/backend-common/src/reading/GitlabUrlReader.ts +++ b/packages/backend-common/src/reading/GitlabUrlReader.ts @@ -140,33 +140,9 @@ export class GitlabUrlReader implements UrlReader { throw new Error(message); } - // Get the filename of archive from the header of the response - const contentDispositionHeader = archiveGitLabResponse.headers.get( - 'content-disposition', - ) as string; - if (!contentDispositionHeader) { - throw new Error( - `Failed to read tree from ${url}. ` + - 'GitLab API response for downloading archive does not contain content-disposition header ', - ); - } - const fileNameRegEx = new RegExp( - /^attachment; filename="(?.*).zip"$/, - ); - const archiveFileName = contentDispositionHeader.match(fileNameRegEx) - ?.groups?.fileName; - if (!archiveFileName) { - throw new Error( - `Failed to read tree from ${url}. GitLab API response for downloading archive has an unexpected ` + - `format of content-disposition header ${contentDispositionHeader} `, - ); - } - - const path = filepath ? `${archiveFileName}/${filepath}/` : ''; - return await this.treeResponseFactory.fromZipArchive({ stream: (archiveGitLabResponse.body as unknown) as Readable, - path, + subpath: filepath, etag: commitSha, filter: options?.filter, }); diff --git a/packages/backend-common/src/reading/tree/ReadTreeResponseFactory.ts b/packages/backend-common/src/reading/tree/ReadTreeResponseFactory.ts index 7332154d09..05ecdb0fc3 100644 --- a/packages/backend-common/src/reading/tree/ReadTreeResponseFactory.ts +++ b/packages/backend-common/src/reading/tree/ReadTreeResponseFactory.ts @@ -24,8 +24,9 @@ import { ZipArchiveResponse } from './ZipArchiveResponse'; type FromArchiveOptions = { // A binary stream of a tar archive. stream: Readable; - // If set, the root of the tree will be set to the given directory path. - path?: string; + // If unset, the files at the root of the tree will be read. + // subpath must not contain the name of the top level directory. + subpath?: string; // etag of the blob etag: string; // Filter passed on from the ReadTreeOptions @@ -45,7 +46,7 @@ export class ReadTreeResponseFactory { async fromTarArchive(options: FromArchiveOptions): Promise { return new TarArchiveResponse( options.stream, - options.path ?? '', + options.subpath ?? '', this.workDir, options.etag, options.filter, @@ -55,7 +56,7 @@ export class ReadTreeResponseFactory { async fromZipArchive(options: FromArchiveOptions): Promise { return new ZipArchiveResponse( options.stream, - options.path ?? '', + options.subpath ?? '', this.workDir, options.etag, options.filter, diff --git a/packages/backend-common/src/reading/tree/TarArchiveResponse.test.ts b/packages/backend-common/src/reading/tree/TarArchiveResponse.test.ts index 2cbfc4a89e..2b76bea9e3 100644 --- a/packages/backend-common/src/reading/tree/TarArchiveResponse.test.ts +++ b/packages/backend-common/src/reading/tree/TarArchiveResponse.test.ts @@ -38,7 +38,7 @@ describe('TarArchiveResponse', () => { it('should read files', async () => { const stream = fs.createReadStream('/test-archive.tar.gz'); - const res = new TarArchiveResponse(stream, 'mock-main/', '/tmp', 'etag'); + const res = new TarArchiveResponse(stream, '', '/tmp', 'etag'); const files = await res.files(); expect(files).toEqual([ @@ -61,12 +61,8 @@ describe('TarArchiveResponse', () => { it('should read files with filter', async () => { const stream = fs.createReadStream('/test-archive.tar.gz'); - const res = new TarArchiveResponse( - stream, - 'mock-main/', - '/tmp', - 'etag', - path => path.endsWith('.yml'), + const res = new TarArchiveResponse(stream, '', '/tmp', 'etag', path => + path.endsWith('.yml'), ); const files = await res.files(); @@ -83,7 +79,7 @@ describe('TarArchiveResponse', () => { it('should read as archive and files', async () => { const stream = fs.createReadStream('/test-archive.tar.gz'); - const res = new TarArchiveResponse(stream, 'mock-main/', '/tmp', 'etag'); + const res = new TarArchiveResponse(stream, '', '/tmp', 'etag'); const buffer = await res.archive(); await expect(res.archive()).rejects.toThrow( @@ -115,24 +111,18 @@ describe('TarArchiveResponse', () => { const res = new TarArchiveResponse(stream, '', '/tmp', 'etag'); const dir = await res.dir(); - await expect( - fs.readFile(resolvePath(dir, 'mock-main/mkdocs.yml'), 'utf8'), + fs.readFile(resolvePath(dir, 'mkdocs.yml'), 'utf8'), ).resolves.toBe('site_name: Test\n'); await expect( - fs.readFile(resolvePath(dir, 'mock-main/docs/index.md'), 'utf8'), + fs.readFile(resolvePath(dir, 'docs/index.md'), 'utf8'), ).resolves.toBe('# Test\n'); }); it('should extract archive into directory with a subpath', async () => { const stream = fs.createReadStream('/test-archive.tar.gz'); - const res = new TarArchiveResponse( - stream, - 'mock-main/docs/', - '/tmp', - 'etag', - ); + const res = new TarArchiveResponse(stream, 'docs', '/tmp', 'etag'); const dir = await res.dir(); expect(dir).toMatch(/^[\/\\]tmp[\/\\].*$/); @@ -144,12 +134,8 @@ describe('TarArchiveResponse', () => { it('should extract archive into directory with a subpath and filter', async () => { const stream = fs.createReadStream('/test-archive.tar.gz'); - const res = new TarArchiveResponse( - stream, - 'mock-main/', - '/tmp', - 'etag', - path => path.endsWith('.yml'), + const res = new TarArchiveResponse(stream, '', '/tmp', 'etag', path => + path.endsWith('.yml'), ); const dir = await res.dir({ targetDir: '/tmp' }); diff --git a/packages/backend-common/src/reading/tree/TarArchiveResponse.ts b/packages/backend-common/src/reading/tree/TarArchiveResponse.ts index 5927eb75a1..96d3a5d495 100644 --- a/packages/backend-common/src/reading/tree/TarArchiveResponse.ts +++ b/packages/backend-common/src/reading/tree/TarArchiveResponse.ts @@ -30,6 +30,10 @@ import { const TarParseStream = (Parse as unknown) as { new (): ParseStream }; const pipeline = promisify(pipelineCb); +// Matches a directory name + one `/` at the start of any string, +// containing any character except `/` one or more times, and ending with a `/` +// e.g. Will match `dirA/` in `dirA/dirB/file.ext` +const directoryNameRegex = /^[^\/]+\//; /** * Wraps a tar archive stream into a tree response reader. @@ -78,14 +82,18 @@ export class TarArchiveResponse implements ReadTreeResponse { return; } + // File path relative to the root extracted directory. Will remove the + // top level dir name from the path since its name is hard to predetermine. + const relativePath = entry.path.replace(directoryNameRegex, ''); + if (this.subPath) { - if (!entry.path.startsWith(this.subPath)) { + if (!relativePath.startsWith(this.subPath)) { entry.resume(); return; } } - const path = entry.path.slice(this.subPath.length); + const path = relativePath.slice(this.subPath.length); if (this.filter) { if (!this.filter(path)) { entry.resume(); @@ -97,7 +105,10 @@ export class TarArchiveResponse implements ReadTreeResponse { await pipeline(entry, concatStream(resolve)); }); - files.push({ path, content: () => content }); + files.push({ + path, + content: () => content, + }); entry.resume(); }); @@ -138,7 +149,9 @@ export class TarArchiveResponse implements ReadTreeResponse { options?.targetDir ?? (await fs.mkdtemp(path.join(this.workDir, 'backstage-'))); - const strip = this.subPath ? this.subPath.split('/').length - 1 : 0; + // Equivalent of tar --strip-components=N + // When no subPath is given, remove just 1 top level directory + const strip = this.subPath ? this.subPath.split('/').length : 1; await pipeline( this.stream, @@ -146,7 +159,10 @@ export class TarArchiveResponse implements ReadTreeResponse { strip, cwd: dir, filter: path => { - if (this.subPath && !path.startsWith(this.subPath)) { + // File path relative to the root extracted directory. Will remove the + // top level dir name from the path since its name is hard to predetermine. + const relativePath = path.replace(directoryNameRegex, ''); + if (this.subPath && !relativePath.startsWith(this.subPath)) { return false; } if (this.filter) { diff --git a/packages/backend-common/src/reading/tree/ZipArchiveResponse.test.ts b/packages/backend-common/src/reading/tree/ZipArchiveResponse.test.ts index b42ec79d81..3bcaa5e0e3 100644 --- a/packages/backend-common/src/reading/tree/ZipArchiveResponse.test.ts +++ b/packages/backend-common/src/reading/tree/ZipArchiveResponse.test.ts @@ -38,7 +38,7 @@ describe('ZipArchiveResponse', () => { it('should read files', async () => { const stream = fs.createReadStream('/test-archive.zip'); - const res = new ZipArchiveResponse(stream, 'mock-main/', '/tmp', 'etag'); + const res = new ZipArchiveResponse(stream, '', '/tmp', 'etag'); const files = await res.files(); expect(files).toEqual([ @@ -61,12 +61,8 @@ describe('ZipArchiveResponse', () => { it('should read files with filter', async () => { const stream = fs.createReadStream('/test-archive.zip'); - const res = new ZipArchiveResponse( - stream, - 'mock-main/', - '/tmp', - 'etag', - path => path.endsWith('.yml'), + const res = new ZipArchiveResponse(stream, '', '/tmp', 'etag', path => + path.endsWith('.yml'), ); const files = await res.files(); @@ -83,7 +79,7 @@ describe('ZipArchiveResponse', () => { it('should read as archive and files', async () => { const stream = fs.createReadStream('/test-archive.zip'); - const res = new ZipArchiveResponse(stream, 'mock-main/', '/tmp', 'etag'); + const res = new ZipArchiveResponse(stream, '', '/tmp', 'etag'); const buffer = await res.archive(); await expect(res.archive()).rejects.toThrow( @@ -117,22 +113,17 @@ describe('ZipArchiveResponse', () => { const dir = await res.dir(); await expect( - fs.readFile(resolvePath(dir, 'mock-main/mkdocs.yml'), 'utf8'), + fs.readFile(resolvePath(dir, 'mkdocs.yml'), 'utf8'), ).resolves.toBe('site_name: Test\n'); await expect( - fs.readFile(resolvePath(dir, 'mock-main/docs/index.md'), 'utf8'), + fs.readFile(resolvePath(dir, 'docs/index.md'), 'utf8'), ).resolves.toBe('# Test\n'); }); it('should extract archive into directory with a subpath', async () => { const stream = fs.createReadStream('/test-archive.zip'); - const res = new ZipArchiveResponse( - stream, - 'mock-main/docs/', - '/tmp', - 'etag', - ); + const res = new ZipArchiveResponse(stream, 'docs/', '/tmp', 'etag'); const dir = await res.dir(); expect(dir).toMatch(/^[\/\\]tmp[\/\\].*$/); @@ -144,12 +135,8 @@ describe('ZipArchiveResponse', () => { it('should extract archive into directory with a subpath and filter', async () => { const stream = fs.createReadStream('/test-archive.zip'); - const res = new ZipArchiveResponse( - stream, - 'mock-main/', - '/tmp', - 'etag', - path => path.endsWith('.yml'), + const res = new ZipArchiveResponse(stream, '', '/tmp', 'etag', path => + path.endsWith('.yml'), ); const dir = await res.dir({ targetDir: '/tmp' }); diff --git a/packages/backend-common/src/reading/tree/ZipArchiveResponse.ts b/packages/backend-common/src/reading/tree/ZipArchiveResponse.ts index 07d34faaa3..37ff32741f 100644 --- a/packages/backend-common/src/reading/tree/ZipArchiveResponse.ts +++ b/packages/backend-common/src/reading/tree/ZipArchiveResponse.ts @@ -25,6 +25,11 @@ import { ReadTreeResponseDirOptions, } from '../types'; +// Matches a directory name + one `/` at the start of any string, +// containing any character except / one or more times, and ending with a `/` +// e.g. Will match `dirA/` in `dirA/dirB/file.ext` +const directoryNameRegex = /^[^\/]+\//; + /** * Wraps a zip archive stream into a tree response reader. */ @@ -60,18 +65,26 @@ export class ZipArchiveResponse implements ReadTreeResponse { this.read = true; } - private getPath(entry: Entry): string { - return entry.path.slice(this.subPath.length); + // Will remove the top level dir name from the path since its name is hard to predetermine. + private stripTopDirectory(path: string): string { + return path.replace(directoryNameRegex, ''); + } + + // File path relative to the root extracted directory or a sub directory if subpath is set. + private getInnerPath(path: string): string { + return path.slice(this.subPath.length); } private shouldBeIncluded(entry: Entry): boolean { + const strippedPath = this.stripTopDirectory(entry.path); + if (this.subPath) { - if (!entry.path.startsWith(this.subPath)) { + if (!strippedPath.startsWith(this.subPath)) { return false; } } if (this.filter) { - return this.filter(this.getPath(entry)); + return this.filter(this.getInnerPath(entry.path)); } return true; } @@ -91,7 +104,7 @@ export class ZipArchiveResponse implements ReadTreeResponse { if (this.shouldBeIncluded(entry)) { files.push({ - path: this.getPath(entry), + path: this.getInnerPath(this.stripTopDirectory(entry.path)), content: () => entry.buffer(), }); } else { @@ -115,7 +128,7 @@ export class ZipArchiveResponse implements ReadTreeResponse { .pipe(unzipper.Parse()) .on('entry', (entry: Entry) => { if (entry.type === 'File' && this.shouldBeIncluded(entry)) { - archive.append(entry, { name: this.getPath(entry) }); + archive.append(entry, { name: this.getInnerPath(entry.path) }); } else { entry.autodrain(); } @@ -139,7 +152,9 @@ export class ZipArchiveResponse implements ReadTreeResponse { // Ignore directory entries since we handle that with the file entries // as a zip can have files with directories without directory entries if (entry.type === 'File' && this.shouldBeIncluded(entry)) { - const entryPath = this.getPath(entry); + const entryPath = this.getInnerPath( + this.stripTopDirectory(entry.path), + ); const dirname = path.dirname(entryPath); if (dirname) { await fs.mkdirp(path.join(dir, dirname)); diff --git a/packages/backend-common/src/reading/types.ts b/packages/backend-common/src/reading/types.ts index e98f760d8f..f9dfbe9aff 100644 --- a/packages/backend-common/src/reading/types.ts +++ b/packages/backend-common/src/reading/types.ts @@ -81,6 +81,9 @@ export type ReadTreeResponseDirOptions = { }; export type ReadTreeResponse = { + /** + * files() returns an array of all the files inside the tree and corresponding functions to read their content. + */ files(): Promise; archive(): Promise; From bd3c3cb960902c862cac3b10eed12c7971d33b9e Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Sun, 24 Jan 2021 17:25:24 +0100 Subject: [PATCH 021/131] Add tests for .dir() method in readTree response --- .../src/reading/AzureUrlReader.test.ts | 28 ++++++++++++- .../src/reading/BitbucketUrlReader.test.ts | 40 ++++++++++++++++++- .../src/reading/GithubUrlReader.test.ts | 40 ++++++++++++++++++- .../src/reading/GitlabUrlReader.test.ts | 40 ++++++++++++++++++- 4 files changed, 144 insertions(+), 4 deletions(-) diff --git a/packages/backend-common/src/reading/AzureUrlReader.test.ts b/packages/backend-common/src/reading/AzureUrlReader.test.ts index 20f8feba42..30c086ea08 100644 --- a/packages/backend-common/src/reading/AzureUrlReader.test.ts +++ b/packages/backend-common/src/reading/AzureUrlReader.test.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -import fs from 'fs'; +import fs from 'fs-extra'; +import mockFs from 'mock-fs'; import path from 'path'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; @@ -139,6 +140,16 @@ describe('AzureUrlReader', () => { }); describe('readTree', () => { + beforeEach(() => { + mockFs({ + '/tmp': mockFs.directory(), + }); + }); + + afterEach(() => { + mockFs.restore(); + }); + const repoBuffer = fs.readFileSync( path.resolve('src', 'reading', '__fixtures__', 'mock-main.zip'), ); @@ -200,6 +211,21 @@ describe('AzureUrlReader', () => { expect(indexMarkdownFile.toString()).toBe('# Test\n'); }); + it('creates a directory with the wanted files', async () => { + const response = await processor.readTree( + 'https://dev.azure.com/organization/project/_git/repository', + ); + + const dir = await response.dir({ targetDir: '/tmp' }); + + await expect( + fs.readFile(path.join(dir, 'mkdocs.yml'), 'utf8'), + ).resolves.toBe('site_name: Test\n'); + await expect( + fs.readFile(path.join(dir, 'docs', 'index.md'), 'utf8'), + ).resolves.toBe('# Test\n'); + }); + it('throws a NotModifiedError when given a etag in options', async () => { const fnAzure = async () => { await processor.readTree( diff --git a/packages/backend-common/src/reading/BitbucketUrlReader.test.ts b/packages/backend-common/src/reading/BitbucketUrlReader.test.ts index 9661368b5e..9103ecc7f8 100644 --- a/packages/backend-common/src/reading/BitbucketUrlReader.test.ts +++ b/packages/backend-common/src/reading/BitbucketUrlReader.test.ts @@ -16,7 +16,8 @@ import { ConfigReader } from '@backstage/config'; import { msw } from '@backstage/test-utils'; -import fs from 'fs'; +import fs from 'fs-extra'; +import mockFs from 'mock-fs'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import path from 'path'; @@ -53,6 +54,16 @@ describe('BitbucketUrlReader', () => { }); describe('readTree', () => { + beforeEach(() => { + mockFs({ + '/tmp': mockFs.directory(), + }); + }); + + afterEach(() => { + mockFs.restore(); + }); + const worker = setupServer(); msw.setupDefaultHandlers(worker); @@ -155,6 +166,21 @@ describe('BitbucketUrlReader', () => { expect(mkDocsFile.toString()).toBe('site_name: Test\n'); }); + it('creates a directory with the wanted files', async () => { + const response = await bitbucketProcessor.readTree( + 'https://bitbucket.org/backstage/mock', + ); + + const dir = await response.dir({ targetDir: '/tmp' }); + + await expect( + fs.readFile(path.join(dir, 'mkdocs.yml'), 'utf8'), + ).resolves.toBe('site_name: Test\n'); + await expect( + fs.readFile(path.join(dir, 'docs', 'index.md'), 'utf8'), + ).resolves.toBe('# Test\n'); + }); + it('uses private bitbucket host', async () => { const response = await hostedBitbucketProcessor.readTree( 'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/docs?at=some-branch', @@ -185,6 +211,18 @@ describe('BitbucketUrlReader', () => { expect(indexMarkdownFile.toString()).toBe('# Test\n'); }); + it('creates a directory with the wanted files with a subpath', async () => { + const response = await bitbucketProcessor.readTree( + 'https://bitbucket.org/backstage/mock/src/master/docs', + ); + + const dir = await response.dir({ targetDir: '/tmp' }); + + await expect( + fs.readFile(path.join(dir, 'index.md'), 'utf8'), + ).resolves.toBe('# Test\n'); + }); + it('throws a NotModifiedError when given a etag in options', async () => { const fnBitbucket = async () => { await bitbucketProcessor.readTree( diff --git a/packages/backend-common/src/reading/GithubUrlReader.test.ts b/packages/backend-common/src/reading/GithubUrlReader.test.ts index 080e8b1d5b..975be85ccc 100644 --- a/packages/backend-common/src/reading/GithubUrlReader.test.ts +++ b/packages/backend-common/src/reading/GithubUrlReader.test.ts @@ -17,7 +17,8 @@ import { ConfigReader } from '@backstage/config'; import { GithubCredentialsProvider } from '@backstage/integration'; import { msw } from '@backstage/test-utils'; -import fs from 'fs'; +import fs from 'fs-extra'; +import mockFs from 'mock-fs'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import path from 'path'; @@ -107,6 +108,16 @@ describe('GithubUrlReader', () => { }); describe('readTree', () => { + beforeEach(() => { + mockFs({ + '/tmp': mockFs.directory(), + }); + }); + + afterEach(() => { + mockFs.restore(); + }); + const repoBuffer = fs.readFileSync( path.resolve( 'src', @@ -227,6 +238,21 @@ describe('GithubUrlReader', () => { expect(indexMarkdownFile.toString()).toBe('# Test\n'); }); + it('creates a directory with the wanted files', async () => { + const response = await githubProcessor.readTree( + 'https://github.com/backstage/mock', + ); + + const dir = await response.dir({ targetDir: '/tmp' }); + + await expect( + fs.readFile(path.join(dir, 'mkdocs.yml'), 'utf8'), + ).resolves.toBe('site_name: Test\n'); + await expect( + fs.readFile(path.join(dir, 'docs', 'index.md'), 'utf8'), + ).resolves.toBe('# Test\n'); + }); + it('should use the headers from the credentials provider to the fetch request', async () => { expect.assertions(2); @@ -293,6 +319,18 @@ describe('GithubUrlReader', () => { expect(indexMarkdownFile.toString()).toBe('# Test\n'); }); + it('creates a directory with the wanted files with subpath', async () => { + const response = await githubProcessor.readTree( + 'https://github.com/backstage/mock/tree/main/docs', + ); + + const dir = await response.dir({ targetDir: '/tmp' }); + + await expect( + fs.readFile(path.join(dir, 'index.md'), 'utf8'), + ).resolves.toBe('# Test\n'); + }); + it('throws a NotModifiedError when given a etag in options', async () => { const fnGithub = async () => { await githubProcessor.readTree('https://github.com/backstage/mock', { diff --git a/packages/backend-common/src/reading/GitlabUrlReader.test.ts b/packages/backend-common/src/reading/GitlabUrlReader.test.ts index c0736f769d..90acfd1ab9 100644 --- a/packages/backend-common/src/reading/GitlabUrlReader.test.ts +++ b/packages/backend-common/src/reading/GitlabUrlReader.test.ts @@ -16,7 +16,8 @@ import { ConfigReader } from '@backstage/config'; import { msw } from '@backstage/test-utils'; -import fs from 'fs'; +import fs from 'fs-extra'; +import mockFs from 'mock-fs'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import path from 'path'; @@ -153,6 +154,16 @@ describe('GitlabUrlReader', () => { }); describe('readTree', () => { + beforeEach(() => { + mockFs({ + '/tmp': mockFs.directory(), + }); + }); + + afterEach(() => { + mockFs.restore(); + }); + const archiveBuffer = fs.readFileSync( path.resolve('src', 'reading', '__fixtures__', 'gitlab-archive.zip'), ); @@ -254,6 +265,21 @@ describe('GitlabUrlReader', () => { expect(indexMarkdownFile.toString()).toBe('# Test\n'); }); + it('creates a directory with the wanted files', async () => { + const response = await gitlabProcessor.readTree( + 'https://gitlab.com/backstage/mock', + ); + + const dir = await response.dir({ targetDir: '/tmp' }); + + await expect( + fs.readFile(path.join(dir, 'mkdocs.yml'), 'utf8'), + ).resolves.toBe('site_name: Test\n'); + await expect( + fs.readFile(path.join(dir, 'docs', 'index.md'), 'utf8'), + ).resolves.toBe('# Test\n'); + }); + it('returns the wanted files from hosted gitlab', async () => { worker.use( rest.get( @@ -296,6 +322,18 @@ describe('GitlabUrlReader', () => { expect(indexMarkdownFile.toString()).toBe('# Test\n'); }); + it('creates a directory with the wanted files with subpath', async () => { + const response = await gitlabProcessor.readTree( + 'https://gitlab.com/backstage/mock/tree/main/docs', + ); + + const dir = await response.dir({ targetDir: '/tmp' }); + + await expect( + fs.readFile(path.join(dir, 'index.md'), 'utf8'), + ).resolves.toBe('# Test\n'); + }); + it('throws a NotModifiedError when given a etag in options', async () => { const fnGitlab = async () => { await gitlabProcessor.readTree('https://gitlab.com/backstage/mock', { From 664dd08c95801870a9434ec8d4b76b979a3bd4e0 Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Sun, 24 Jan 2021 19:03:39 +0100 Subject: [PATCH 022/131] backend-common: Add changeset about readTree archive directory name improvement --- .changeset/dull-ears-glow.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/dull-ears-glow.md diff --git a/.changeset/dull-ears-glow.md b/.changeset/dull-ears-glow.md new file mode 100644 index 0000000000..ea08b3b37e --- /dev/null +++ b/.changeset/dull-ears-glow.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +URL Reader's readTree: Fix bug with github.com URLs. From a91aa6bf2a32a96eacae41e550486882b4a832b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Mon, 25 Jan 2021 15:05:18 +0100 Subject: [PATCH 023/131] catalog: support supplying a custom catalog descriptor file parser --- .changeset/green-boats-attend.md | 5 ++++ .../src/ingestion/LocationReaders.ts | 4 +++- .../processors/UrlReaderProcessor.test.ts | 5 ++-- .../processors/UrlReaderProcessor.ts | 10 +++++--- .../src/ingestion/processors/types.ts | 13 +++++++++++ .../src/ingestion/processors/util/parse.ts | 11 ++++++++- .../src/service/CatalogBuilder.test.ts | 23 +++++++++++++++++++ .../src/service/CatalogBuilder.ts | 20 ++++++++++++++++ 8 files changed, 84 insertions(+), 7 deletions(-) create mode 100644 .changeset/green-boats-attend.md diff --git a/.changeset/green-boats-attend.md b/.changeset/green-boats-attend.md new file mode 100644 index 0000000000..ae2324291e --- /dev/null +++ b/.changeset/green-boats-attend.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Support supplying a custom catalog descriptor file parser diff --git a/plugins/catalog-backend/src/ingestion/LocationReaders.ts b/plugins/catalog-backend/src/ingestion/LocationReaders.ts index cb703b8e15..046b6bdf76 100644 --- a/plugins/catalog-backend/src/ingestion/LocationReaders.ts +++ b/plugins/catalog-backend/src/ingestion/LocationReaders.ts @@ -32,6 +32,7 @@ import { CatalogProcessorEntityResult, CatalogProcessorErrorResult, CatalogProcessorLocationResult, + CatalogProcessorParser, CatalogProcessorResult, } from './processors/types'; import { LocationReader, ReadLocationResult } from './types'; @@ -41,6 +42,7 @@ const MAX_DEPTH = 10; type Options = { reader: UrlReader; + parser: CatalogProcessorParser; logger: Logger; config: Config; processors: CatalogProcessor[]; @@ -137,7 +139,6 @@ export class LocationReaders implements LocationReader { if (emitResult.type === 'relation') { throw new Error('readLocation may not emit entity relations'); } - emit(emitResult); }; @@ -149,6 +150,7 @@ export class LocationReaders implements LocationReader { item.location, item.optional, validatedEmit, + this.options.parser, ) ) { return; diff --git a/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.test.ts index aac4235e3a..49b6fe71e0 100644 --- a/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.test.ts +++ b/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.test.ts @@ -25,6 +25,7 @@ import { CatalogProcessorErrorResult, CatalogProcessorResult, } from './types'; +import { defaultEntityDataParser } from './util/parse'; describe('UrlReaderProcessor', () => { const mockApiOrigin = 'http://localhost'; @@ -52,7 +53,7 @@ describe('UrlReaderProcessor', () => { ); const generated = (await new Promise(emit => - processor.readLocation(spec, false, emit), + processor.readLocation(spec, false, emit, defaultEntityDataParser), )) as CatalogProcessorEntityResult; expect(generated.type).toBe('entity'); @@ -81,7 +82,7 @@ describe('UrlReaderProcessor', () => { ); const generated = (await new Promise(emit => - processor.readLocation(spec, false, emit), + processor.readLocation(spec, false, emit, defaultEntityDataParser), )) as CatalogProcessorErrorResult; expect(generated.type).toBe('error'); diff --git a/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.ts index 04f5ee738a..9daae29d3b 100644 --- a/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.ts @@ -18,8 +18,11 @@ import { UrlReader } from '@backstage/backend-common'; import { LocationSpec } from '@backstage/catalog-model'; import { Logger } from 'winston'; import * as result from './results'; -import { CatalogProcessor, CatalogProcessorEmit } from './types'; -import { parseEntityYaml } from './util/parse'; +import { + CatalogProcessor, + CatalogProcessorEmit, + CatalogProcessorParser, +} from './types'; // TODO(Rugvip): Added for backwards compatibility when moving to UrlReader, this // can be removed in a bit @@ -43,6 +46,7 @@ export class UrlReaderProcessor implements CatalogProcessor { location: LocationSpec, optional: boolean, emit: CatalogProcessorEmit, + parser: CatalogProcessorParser, ): Promise { if (deprecatedTypes.includes(location.type)) { // TODO(Rugvip): Remove this warning a month or two into 2021, and remove support for the deprecated types. @@ -57,7 +61,7 @@ export class UrlReaderProcessor implements CatalogProcessor { try { const data = await this.options.reader.read(location.target); - for (const parseResult of parseEntityYaml(data, location)) { + for await (const parseResult of parser({ data, location })) { emit(parseResult); } } catch (error) { diff --git a/plugins/catalog-backend/src/ingestion/processors/types.ts b/plugins/catalog-backend/src/ingestion/processors/types.ts index 1bf30567bc..f7e11d5616 100644 --- a/plugins/catalog-backend/src/ingestion/processors/types.ts +++ b/plugins/catalog-backend/src/ingestion/processors/types.ts @@ -27,12 +27,15 @@ export type CatalogProcessor = { * @param location The location to read * @param optional Whether a missing target should trigger an error * @param emit A sink for items resulting from the read + * @param parser A parser, that is able to take the raw catalog descriptor + * data and turn it into the actual result pieces. * @returns True if handled by this processor, false otherwise */ readLocation?( location: LocationSpec, optional: boolean, emit: CatalogProcessorEmit, + parser: CatalogProcessorParser, ): Promise; /** @@ -100,6 +103,16 @@ export type CatalogProcessor = { ): Promise; }; +/** + * A parser, that is able to take the raw catalog descriptor data and turn it + * into the actual result pieces. The default implementation performs a YAML + * document parsing. + */ +export type CatalogProcessorParser = (options: { + data: Buffer; + location: LocationSpec; +}) => AsyncIterable; + export type CatalogProcessorEmit = (generated: CatalogProcessorResult) => void; export type CatalogProcessorLocationResult = { diff --git a/plugins/catalog-backend/src/ingestion/processors/util/parse.ts b/plugins/catalog-backend/src/ingestion/processors/util/parse.ts index c3dcd42d62..aa24968d6d 100644 --- a/plugins/catalog-backend/src/ingestion/processors/util/parse.ts +++ b/plugins/catalog-backend/src/ingestion/processors/util/parse.ts @@ -18,7 +18,7 @@ import { Entity, LocationSpec } from '@backstage/catalog-model'; import lodash from 'lodash'; import yaml from 'yaml'; import * as result from '../results'; -import { CatalogProcessorResult } from '../types'; +import { CatalogProcessorParser, CatalogProcessorResult } from '../types'; export function* parseEntityYaml( data: Buffer, @@ -50,3 +50,12 @@ export function* parseEntityYaml( } } } + +export const defaultEntityDataParser: CatalogProcessorParser = async function* defaultEntityDataParser({ + data, + location, +}) { + for (const e of parseEntityYaml(data, location)) { + yield e; + } +}; diff --git a/plugins/catalog-backend/src/service/CatalogBuilder.test.ts b/plugins/catalog-backend/src/service/CatalogBuilder.test.ts index 37337f76f1..695902fec9 100644 --- a/plugins/catalog-backend/src/service/CatalogBuilder.test.ts +++ b/plugins/catalog-backend/src/service/CatalogBuilder.test.ts @@ -20,6 +20,7 @@ import { ConfigReader } from '@backstage/config'; import Knex from 'knex'; import yaml from 'yaml'; import { DatabaseManager } from '../database'; +import { CatalogProcessorParser } from '../ingestion'; import * as result from '../ingestion/processors/results'; import { CatalogBuilder, CatalogEnvironment } from './CatalogBuilder'; @@ -209,4 +210,26 @@ describe('CatalogBuilder', () => { }), ]); }); + + it('setEntityDataParser works', async () => { + const mockParser: CatalogProcessorParser = jest + .fn() + .mockImplementation(() => {}); + + const builder = new CatalogBuilder(env) + .setEntityDataParser(mockParser) + .replaceProcessors([ + { + async readLocation(_location, _optional, _emit, parser) { + expect(parser).toBe(mockParser); + return true; + }, + }, + ]); + + const { higherOrderOperation } = await builder.build(); + await higherOrderOperation.addLocation({ type: 'x', target: 'y' }); + + expect.assertions(1); + }); }); diff --git a/plugins/catalog-backend/src/service/CatalogBuilder.ts b/plugins/catalog-backend/src/service/CatalogBuilder.ts index f6170135fd..938a406e56 100644 --- a/plugins/catalog-backend/src/service/CatalogBuilder.ts +++ b/plugins/catalog-backend/src/service/CatalogBuilder.ts @@ -39,6 +39,7 @@ import { AnnotateLocationEntityProcessor, BuiltinKindsEntityProcessor, CatalogProcessor, + CatalogProcessorParser, CodeOwnersProcessor, FileReaderProcessor, GithubOrgReaderProcessor, @@ -60,6 +61,7 @@ import { textPlaceholderResolver, yamlPlaceholderResolver, } from '../ingestion/processors/PlaceholderProcessor'; +import { defaultEntityDataParser } from '../ingestion/processors/util/parse'; import { LocationAnalyzer } from '../ingestion/types'; export type CatalogEnvironment = { @@ -96,6 +98,7 @@ export class CatalogBuilder { private fieldFormatValidators: Partial; private processors: CatalogProcessor[]; private processorsReplace: boolean; + private parser: CatalogProcessorParser | undefined; constructor(env: CatalogEnvironment) { this.env = env; @@ -105,6 +108,7 @@ export class CatalogBuilder { this.fieldFormatValidators = {}; this.processors = []; this.processorsReplace = false; + this.parser = undefined; } /** @@ -197,6 +201,20 @@ export class CatalogBuilder { return this; } + /** + * Sets up the catalog to use a custom parser for entity data. + * + * This is the function that gets called immediately after some raw entity + * specification data has been read from a remote source, and needs to be + * parsed and emitted as structured data. + * + * @param parser The custom parser + */ + setEntityDataParser(parser: CatalogProcessorParser): CatalogBuilder { + this.parser = parser; + return this; + } + /** * Wires up and returns all of the component parts of the catalog */ @@ -211,9 +229,11 @@ export class CatalogBuilder { const policy = this.buildEntityPolicy(); const processors = this.buildProcessors(); const rulesEnforcer = CatalogRulesEnforcer.fromConfig(config); + const parser = this.parser || defaultEntityDataParser; const locationReader = new LocationReaders({ ...this.env, + parser, processors, rulesEnforcer, policy, From 38f710b366ffec2f3ef78d74d6fcc78de8a0bef0 Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Mon, 25 Jan 2021 16:10:50 +0100 Subject: [PATCH 024/131] Fix commits url for bitbucket server --- .../src/reading/BitbucketUrlReader.ts | 35 ++++++++++++++----- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/packages/backend-common/src/reading/BitbucketUrlReader.ts b/packages/backend-common/src/reading/BitbucketUrlReader.ts index e9727e04cf..86dd7e32b0 100644 --- a/packages/backend-common/src/reading/BitbucketUrlReader.ts +++ b/packages/backend-common/src/reading/BitbucketUrlReader.ts @@ -161,13 +161,18 @@ export class BitbucketUrlReader implements UrlReader { } private async getLastCommitShortHash(url: string): Promise { - const { name: repoName, owner: project, ref } = parseGitUrl(url); + const { resource, name: repoName, owner: project, ref } = parseGitUrl(url); let branch = ref; if (!branch) { branch = await getBitbucketDefaultBranch(url, this.config); } - const commitsApiUrl = `${this.config.apiBaseUrl}/repositories/${project}/${repoName}/commits/${branch}`; + + const isHosted = resource === 'bitbucket.org'; + // Bitbucket Server https://docs.atlassian.com/bitbucket-server/rest/7.9.0/bitbucket-rest.html#idp222 + const commitsApiUrl = isHosted + ? `${this.config.apiBaseUrl}/repositories/${project}/${repoName}/commits/${branch}` + : `${this.config.apiBaseUrl}/projects/${project}/repos/${repoName}/commits`; const commitsResponse = await fetch( commitsApiUrl, @@ -182,14 +187,26 @@ export class BitbucketUrlReader implements UrlReader { } const commits = await commitsResponse.json(); - if ( - commits && - commits.values && - commits.values.length > 0 && - commits.values[0].hash - ) { - return commits.values[0].hash.substring(0, 12); + if (isHosted) { + if ( + commits && + commits.values && + commits.values.length > 0 && + commits.values[0].hash + ) { + return commits.values[0].hash.substring(0, 12); + } + } else { + if ( + commits && + commits.values && + commits.values.length > 0 && + commits.values[0].id + ) { + return commits.values[0].id.substring(0, 12); + } } + throw new Error(`Failed to read response from ${commitsApiUrl}`); } } From 31e7b2a1fde62ed8e175345862cef1fee936a834 Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Mon, 25 Jan 2021 16:26:02 +0100 Subject: [PATCH 025/131] Fix tests using wrong commits url for bitbucket server --- .../backend-common/src/reading/BitbucketUrlReader.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend-common/src/reading/BitbucketUrlReader.test.ts b/packages/backend-common/src/reading/BitbucketUrlReader.test.ts index 9661368b5e..6265718571 100644 --- a/packages/backend-common/src/reading/BitbucketUrlReader.test.ts +++ b/packages/backend-common/src/reading/BitbucketUrlReader.test.ts @@ -126,12 +126,12 @@ describe('BitbucketUrlReader', () => { ), ), rest.get( - 'https://api.bitbucket.mycompany.net/rest/api/1.0/repositories/backstage/mock/commits/some-branch', + 'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/commits', (_, res, ctx) => res( ctx.status(200), ctx.json({ - values: [{ hash: '12ab34cd56ef78gh90ij12kl34mn56op78qr90st' }], + values: [{ id: '12ab34cd56ef78gh90ij12kl34mn56op78qr90st' }], }), ), ), From 0b11823463af065498b11a44caf768742bcff36c Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Mon, 25 Jan 2021 17:37:55 +0100 Subject: [PATCH 026/131] Add `EntityRefLinks` that shows one or multiple entity ref links. Add `EntityRefLinks` that shows one or multiple entity ref links. Change the about card and catalog table to use `EntityRefLinks` due to the nature of relations to support multiple relations per type. This simplifies usage of relations and support to visualize edge-cases, e.g. duplicate data ingested into the catalog, like two ownerOf relationships for the same component. --- .changeset/forty-jobs-occur.md | 8 +++ .../src/components/AboutCard/AboutContent.tsx | 41 +++++------- .../components/CatalogTable/CatalogTable.tsx | 61 +++++++---------- .../EntityRefLink/EntityRefLinks.test.tsx | 66 +++++++++++++++++++ .../EntityRefLink/EntityRefLinks.tsx | 40 +++++++++++ .../src/components/EntityRefLink/index.ts | 1 + 6 files changed, 155 insertions(+), 62 deletions(-) create mode 100644 .changeset/forty-jobs-occur.md create mode 100644 plugins/catalog/src/components/EntityRefLink/EntityRefLinks.test.tsx create mode 100644 plugins/catalog/src/components/EntityRefLink/EntityRefLinks.tsx diff --git a/.changeset/forty-jobs-occur.md b/.changeset/forty-jobs-occur.md new file mode 100644 index 0000000000..b1620a7535 --- /dev/null +++ b/.changeset/forty-jobs-occur.md @@ -0,0 +1,8 @@ +--- +'@backstage/plugin-catalog': patch +--- + +Add `EntityRefLinks` that shows one or multiple entity ref links. + +Change the about card and catalog table to use `EntityRefLinks` due to the +nature of relations to support multiple relations per type. diff --git a/plugins/catalog/src/components/AboutCard/AboutContent.tsx b/plugins/catalog/src/components/AboutCard/AboutContent.tsx index b2d029c927..82652b3986 100644 --- a/plugins/catalog/src/components/AboutCard/AboutContent.tsx +++ b/plugins/catalog/src/components/AboutCard/AboutContent.tsx @@ -21,7 +21,7 @@ import { } from '@backstage/catalog-model'; import { Chip, Grid, makeStyles, Typography } from '@material-ui/core'; import React from 'react'; -import { EntityRefLink } from '../EntityRefLink'; +import { EntityRefLinks } from '../EntityRefLink'; import { getEntityRelations } from '../getEntityRelations'; import { AboutField } from './AboutField'; @@ -41,17 +41,17 @@ export const AboutContent = ({ entity }: Props) => { const isDomain = entity.kind.toLowerCase() === 'domain'; const isResource = entity.kind.toLowerCase() === 'resource'; const isComponent = entity.kind.toLowerCase() === 'component'; - const [partOfSystemRelation] = getEntityRelations(entity, RELATION_PART_OF, { + const partOfSystemRelations = getEntityRelations(entity, RELATION_PART_OF, { kind: 'system', }); - const [partOfComponentRelation] = getEntityRelations( + const partOfComponentRelations = getEntityRelations( entity, RELATION_PART_OF, { kind: 'component', }, ); - const [partOfDomainRelation] = getEntityRelations(entity, RELATION_PART_OF, { + const partOfDomainRelations = getEntityRelations(entity, RELATION_PART_OF, { kind: 'domain', }); const ownedByRelations = getEntityRelations(entity, RELATION_OWNED_BY); @@ -64,12 +64,7 @@ export const AboutContent = ({ entity }: Props) => { - {ownedByRelations.map((t, i) => ( - - {i > 0 && ', '} - - - ))} + {isSystem && ( { value="No Domain" gridSizes={{ xs: 12, sm: 6, lg: 4 }} > - {partOfDomainRelation && ( - - )} + )} {!isSystem && !isDomain && ( @@ -91,22 +84,20 @@ export const AboutContent = ({ entity }: Props) => { value="No System" gridSizes={{ xs: 12, sm: 6, lg: 4 }} > - {partOfSystemRelation && ( - - )} + )} - {isComponent && partOfComponentRelation && ( + {isComponent && partOfComponentRelations.length > 0 && ( - diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx index 8741cb6157..18c61c2a04 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx @@ -20,17 +20,19 @@ import { RELATION_PART_OF, } from '@backstage/catalog-model'; import { Table, TableColumn, TableProps } from '@backstage/core'; -import { Chip, Link } from '@material-ui/core'; +import { Chip } from '@material-ui/core'; import Edit from '@material-ui/icons/Edit'; import OpenInNew from '@material-ui/icons/OpenInNew'; import { Alert } from '@material-ui/lab'; import React from 'react'; -import { generatePath, Link as RouterLink } from 'react-router-dom'; import { findLocationForEntityMeta } from '../../data/utils'; import { useStarredEntities } from '../../hooks/useStarredEntities'; -import { entityRoute, entityRouteParams } from '../../routes'; import { createEditLink } from '../createEditLink'; -import { EntityRefLink, formatEntityRefTitle } from '../EntityRefLink'; +import { + EntityRefLink, + EntityRefLinks, + formatEntityRefTitle, +} from '../EntityRefLink'; import { favouriteEntityIcon, favouriteEntityTooltip, @@ -40,7 +42,7 @@ import { getEntityRelations } from '../getEntityRelations'; type EntityRow = Entity & { row: { partOfSystemRelationTitle?: string; - partOfSystemRelation?: EntityName; + partOfSystemRelations: EntityName[]; ownedByRelationsTitle?: string; ownedByRelations: EntityName[]; }; @@ -52,43 +54,27 @@ const columns: TableColumn[] = [ field: 'metadata.name', highlight: true, render: entity => ( - - {entity.metadata.name} - + {entity.metadata.name} ), }, { title: 'System', field: 'row.partOfSystemRelationTitle', render: entity => ( - <> - {entity.row.partOfSystemRelation && ( - - )} - + ), }, { title: 'Owner', field: 'row.ownedByRelationsTitle', render: entity => ( - <> - {entity.row.ownedByRelations.map((t, i) => ( - - {i > 0 && ', '} - - - ))} - + ), }, { @@ -182,7 +168,7 @@ export const CatalogTable = ({ ]; const rows = entities.map(e => { - const [partOfSystemRelation] = getEntityRelations(e, RELATION_PART_OF, { + const partOfSystemRelations = getEntityRelations(e, RELATION_PART_OF, { kind: 'system', }); const ownedByRelations = getEntityRelations(e, RELATION_OWNED_BY); @@ -194,12 +180,13 @@ export const CatalogTable = ({ .map(r => formatEntityRefTitle(r, { defaultKind: 'group' })) .join(', '), ownedByRelations, - partOfSystemRelationTitle: partOfSystemRelation - ? formatEntityRefTitle(partOfSystemRelation, { - defaultKind: 'system', - }) - : undefined, - partOfSystemRelation, + partOfSystemRelationTitle: + partOfSystemRelations.length > 0 + ? formatEntityRefTitle(partOfSystemRelations[0], { + defaultKind: 'system', + }) + : undefined, + partOfSystemRelations, }, }; }); diff --git a/plugins/catalog/src/components/EntityRefLink/EntityRefLinks.test.tsx b/plugins/catalog/src/components/EntityRefLink/EntityRefLinks.test.tsx new file mode 100644 index 0000000000..9bc26b0954 --- /dev/null +++ b/plugins/catalog/src/components/EntityRefLink/EntityRefLinks.test.tsx @@ -0,0 +1,66 @@ +/* + * 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 { render } from '@testing-library/react'; +import React from 'react'; +import { MemoryRouter } from 'react-router'; +import { EntityRefLinks } from './EntityRefLinks'; + +describe('', () => { + it('renders a single link', () => { + const entityNames = [ + { + kind: 'Component', + namespace: 'default', + name: 'software', + }, + ]; + const { getByText } = render(, { + wrapper: MemoryRouter, + }); + expect(getByText('component:software')).toHaveAttribute( + 'href', + '/catalog/default/component/software', + ); + }); + + it('renders multiple links', () => { + const entityNames = [ + { + kind: 'Component', + namespace: 'default', + name: 'software', + }, + { + kind: 'API', + namespace: 'default', + name: 'interface', + }, + ]; + const { getByText } = render(, { + wrapper: MemoryRouter, + }); + expect(getByText(',')).toBeInTheDocument(); + expect(getByText('component:software')).toHaveAttribute( + 'href', + '/catalog/default/component/software', + ); + expect(getByText('api:interface')).toHaveAttribute( + 'href', + '/catalog/default/api/interface', + ); + }); +}); diff --git a/plugins/catalog/src/components/EntityRefLink/EntityRefLinks.tsx b/plugins/catalog/src/components/EntityRefLink/EntityRefLinks.tsx new file mode 100644 index 0000000000..3c8beaec6f --- /dev/null +++ b/plugins/catalog/src/components/EntityRefLink/EntityRefLinks.tsx @@ -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 { Entity, EntityName } from '@backstage/catalog-model'; +import React from 'react'; +import { EntityRefLink } from './EntityRefLink'; + +type EntityRefLinksProps = { + entityRefs: (Entity | EntityName)[]; + defaultKind?: string; +}; + +// TODO: Move into a shared helper package +export const EntityRefLinks = ({ + entityRefs, + defaultKind, +}: EntityRefLinksProps) => { + return ( + <> + {entityRefs.map((r, i) => ( + + {i > 0 && ', '} + + + ))} + + ); +}; diff --git a/plugins/catalog/src/components/EntityRefLink/index.ts b/plugins/catalog/src/components/EntityRefLink/index.ts index 76a7da38c5..9e6e440514 100644 --- a/plugins/catalog/src/components/EntityRefLink/index.ts +++ b/plugins/catalog/src/components/EntityRefLink/index.ts @@ -14,4 +14,5 @@ * limitations under the License. */ export { EntityRefLink } from './EntityRefLink'; +export { EntityRefLinks } from './EntityRefLinks'; export { formatEntityRefTitle } from './format'; From 398f2693945c1a7ec59cadd9c0184f09de6134e8 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Mon, 25 Jan 2021 18:08:55 +0100 Subject: [PATCH 027/131] Add pictures to users and groups --- .../catalog-model/examples/acme/backstage-group.yaml | 2 +- packages/catalog-model/examples/acme/org.yaml | 2 +- .../catalog-model/examples/acme/team-a-group.yaml | 10 +++++----- .../catalog-model/examples/acme/team-b-group.yaml | 12 ++++++------ .../catalog-model/examples/acme/team-c-group.yaml | 12 ++++++------ .../catalog-model/examples/acme/team-d-group.yaml | 6 +++--- 6 files changed, 22 insertions(+), 22 deletions(-) diff --git a/packages/catalog-model/examples/acme/backstage-group.yaml b/packages/catalog-model/examples/acme/backstage-group.yaml index 3aecfa0023..f83587532c 100644 --- a/packages/catalog-model/examples/acme/backstage-group.yaml +++ b/packages/catalog-model/examples/acme/backstage-group.yaml @@ -8,6 +8,6 @@ spec: profile: displayName: Backstage email: backstage@example.com - picture: https://example.com/groups/backstage.jpeg + picture: https://avatars.dicebear.com/api/identicon/backstage@example.com.svg?background=%23fff&margin=25 parent: infrastructure children: [team-a, team-b] diff --git a/packages/catalog-model/examples/acme/org.yaml b/packages/catalog-model/examples/acme/org.yaml index 2b44183bc5..05afc265c0 100644 --- a/packages/catalog-model/examples/acme/org.yaml +++ b/packages/catalog-model/examples/acme/org.yaml @@ -8,7 +8,7 @@ spec: profile: displayName: ACME Corp email: info@example.com - picture: https://example.com/logo.jpeg + picture: https://avatars.dicebear.com/api/identicon/info@example.com.svg?background=%23fff&margin=25 children: [infrastructure] --- apiVersion: backstage.io/v1alpha1 diff --git a/packages/catalog-model/examples/acme/team-a-group.yaml b/packages/catalog-model/examples/acme/team-a-group.yaml index ed736dce50..e343209d5f 100644 --- a/packages/catalog-model/examples/acme/team-a-group.yaml +++ b/packages/catalog-model/examples/acme/team-a-group.yaml @@ -8,7 +8,7 @@ spec: profile: # Intentional no displayName for testing email: team-a@example.com - picture: https://example.com/groups/team-a.jpeg + picture: https://avatars.dicebear.com/api/identicon/team-a@example.com.svg?background=%23fff&margin=25 parent: backstage children: [] --- @@ -20,7 +20,7 @@ spec: profile: # Intentional no displayName for testing email: breanna-davison@example.com - picture: https://example.com/staff/breanna.jpeg + picture: https://avatars.dicebear.com/api/avataaars/breanna-davison@example.com.svg?background=%23fff memberOf: [team-a] --- apiVersion: backstage.io/v1alpha1 @@ -31,7 +31,7 @@ spec: profile: displayName: Janelle Dawe email: janelle-dawe@example.com - picture: https://example.com/staff/janelle.jpeg + picture: https://avatars.dicebear.com/api/avataaars/janelle-dawe@example.com.svg?background=%23fff memberOf: [team-a] --- apiVersion: backstage.io/v1alpha1 @@ -42,7 +42,7 @@ spec: profile: displayName: Nigel Manning email: nigel-manning@example.com - picture: https://example.com/staff/nigel.jpeg + picture: https://avatars.dicebear.com/api/avataaars/nigel-manning@example.com.svg?background=%23fff memberOf: [team-a] --- # This user is added as an example, to make it more easy for the "Guest" @@ -56,5 +56,5 @@ spec: profile: displayName: Guest User email: guest@example.com - picture: https://example.com/staff/the-ceos-dog.jpeg + picture: https://avatars.dicebear.com/api/avataaars/guest@example.com.svg?background=%23fff memberOf: [team-a] diff --git a/packages/catalog-model/examples/acme/team-b-group.yaml b/packages/catalog-model/examples/acme/team-b-group.yaml index 9ed50b04c2..20ab8721ea 100644 --- a/packages/catalog-model/examples/acme/team-b-group.yaml +++ b/packages/catalog-model/examples/acme/team-b-group.yaml @@ -8,7 +8,7 @@ spec: profile: displayName: Team B email: team-b@example.com - picture: https://example.com/groups/team-b.jpeg + picture: https://avatars.dicebear.com/api/identicon/team-b@example.com.svg?background=%23fff&margin=25 parent: backstage children: [] --- @@ -20,7 +20,7 @@ spec: profile: displayName: Amelia Park email: amelia-park@example.com - picture: https://example.com/staff/amelia.jpeg + picture: https://avatars.dicebear.com/api/avataaars/amelia-park@example.com.svg?background=%23fff memberOf: [team-b] --- apiVersion: backstage.io/v1alpha1 @@ -31,7 +31,7 @@ spec: profile: displayName: Colette Brock email: colette-brock@example.com - picture: https://example.com/staff/colette.jpeg + picture: https://avatars.dicebear.com/api/avataaars/colette-brock@example.com.svg?background=%23fff memberOf: [team-b] --- apiVersion: backstage.io/v1alpha1 @@ -42,7 +42,7 @@ spec: profile: displayName: Jenny Doe email: jenny-doe@example.com - picture: https://example.com/staff/jenny.jpeg + picture: https://avatars.dicebear.com/api/avataaars/jenny-doe@example.com.svg?background=%23fff memberOf: [team-b] --- apiVersion: backstage.io/v1alpha1 @@ -53,7 +53,7 @@ spec: profile: displayName: Jonathon Page email: jonathon-page@example.com - picture: https://example.com/staff/jonathon.jpeg + picture: https://avatars.dicebear.com/api/avataaars/jonathon-page@example.com.svg?background=%23fff memberOf: [team-b] --- apiVersion: backstage.io/v1alpha1 @@ -64,5 +64,5 @@ spec: profile: displayName: Justine Barrow email: justine-barrow@example.com - picture: https://example.com/staff/justine.jpeg + picture: https://avatars.dicebear.com/api/avataaars/justine-barrow@example.com.svg?background=%23fff memberOf: [team-b] diff --git a/packages/catalog-model/examples/acme/team-c-group.yaml b/packages/catalog-model/examples/acme/team-c-group.yaml index 7f151dbb1e..639b591d61 100644 --- a/packages/catalog-model/examples/acme/team-c-group.yaml +++ b/packages/catalog-model/examples/acme/team-c-group.yaml @@ -8,7 +8,7 @@ spec: profile: displayName: Team C email: team-c@example.com - picture: https://example.com/groups/team-c.jpeg + picture: https://avatars.dicebear.com/api/identicon/team-c@example.com.svg?background=%23fff&margin=25 parent: boxoffice children: [] --- @@ -20,7 +20,7 @@ spec: profile: displayName: Calum Leavy email: calum-leavy@example.com - picture: https://example.com/staff/calum.jpeg + picture: https://avatars.dicebear.com/api/avataaars/calum-leavy@example.com.svg?background=%23fff memberOf: [team-c] --- apiVersion: backstage.io/v1alpha1 @@ -31,7 +31,7 @@ spec: profile: displayName: Frank Tiernan email: frank-tiernan@example.com - picture: https://example.com/staff/frank.jpeg + picture: https://avatars.dicebear.com/api/avataaars/frank-tiernan@example.com.svg?background=%23fff memberOf: [team-c] --- apiVersion: backstage.io/v1alpha1 @@ -42,7 +42,7 @@ spec: profile: displayName: Peadar MacMahon email: peadar-macmahon@example.com - picture: https://example.com/staff/peadar.jpeg + picture: https://avatars.dicebear.com/api/avataaars/peadar-macmahon@example.com.svg?background=%23fff memberOf: [team-c] --- apiVersion: backstage.io/v1alpha1 @@ -53,7 +53,7 @@ spec: profile: displayName: Sarah Gilroy email: sarah-gilroy@example.com - picture: https://example.com/staff/sarah.jpeg + picture: https://avatars.dicebear.com/api/avataaars/sarah-gilroy@example.com.svg?background=%23fff memberOf: [team-c] --- apiVersion: backstage.io/v1alpha1 @@ -64,5 +64,5 @@ spec: profile: displayName: Tara MacGovern email: tara-macgovern@example.com - picture: https://example.com/staff/tara.jpeg + picture: https://avatars.dicebear.com/api/avataaars/tara-macgovern@example.com.svg?background=%23fff memberOf: [team-c] diff --git a/packages/catalog-model/examples/acme/team-d-group.yaml b/packages/catalog-model/examples/acme/team-d-group.yaml index a0d3c2351a..898ed8be6c 100644 --- a/packages/catalog-model/examples/acme/team-d-group.yaml +++ b/packages/catalog-model/examples/acme/team-d-group.yaml @@ -8,7 +8,7 @@ spec: profile: displayName: Team D email: team-d@example.com - picture: https://example.com/groups/team-d.jpeg + picture: https://avatars.dicebear.com/api/identicon/team-d@example.com.svg?background=%23fff&margin=25 parent: boxoffice children: [] --- @@ -20,7 +20,7 @@ spec: profile: displayName: Eva MacDowell email: eva-macdowell@example.com - picture: https://example.com/staff/eva.jpeg + picture: https://avatars.dicebear.com/api/avataaars/eva-macdowell@example.com.svg?background=%23fff memberOf: [team-d] --- apiVersion: backstage.io/v1alpha1 @@ -31,5 +31,5 @@ spec: profile: displayName: Lucy Sheehan email: lucy-sheehan@example.com - picture: https://example.com/staff/lucy.jpeg + picture: https://avatars.dicebear.com/api/avataaars/lucy-sheehan@example.com.svg?background=%23fff memberOf: [team-d] From 8abed46a1f6a042191deb41820bd9346acb68e05 Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Mon, 25 Jan 2021 14:42:10 -0500 Subject: [PATCH 028/131] Reorg Kubernetes section --- .../backstage-k8s-2-deployments.png | Bin 0 -> 127862 bytes docs/features/kubernetes/configuration.md | 129 +++++++++++++++++ docs/features/kubernetes/index.md | 131 +++--------------- docs/features/kubernetes/installation.md | 119 ++++++++++++++++ microsite/sidebars.json | 6 +- 5 files changed, 270 insertions(+), 115 deletions(-) create mode 100644 docs/assets/features/kubernetes/backstage-k8s-2-deployments.png create mode 100644 docs/features/kubernetes/configuration.md create mode 100644 docs/features/kubernetes/installation.md diff --git a/docs/assets/features/kubernetes/backstage-k8s-2-deployments.png b/docs/assets/features/kubernetes/backstage-k8s-2-deployments.png new file mode 100644 index 0000000000000000000000000000000000000000..4a9f2b06faaeff248fa6ca7746c8a296997b0dbb GIT binary patch literal 127862 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 yn +``` + +### Label selector query annotation + +You can write your own custom label selector query that Backstage will use to +lookup the objects (similar to `kubectl --selector="your query here"`). Review +the +[labels and selectors Kubernetes documentation](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/) +for more info. + +```yaml +'backstage.io/kubernetes-label-selector': 'app=my-app,component=front-end' +``` diff --git a/docs/features/kubernetes/index.md b/docs/features/kubernetes/index.md index c468fdb116..26794eca10 100644 --- a/docs/features/kubernetes/index.md +++ b/docs/features/kubernetes/index.md @@ -5,123 +5,26 @@ sidebar_label: Overview description: Monitoring Kubernetes based services with the service catalog --- -Kubernetes in Backstage is a way to monitor your service's current status when -it is deployed on Kubernetes. +Kubernetes in Backstage is a tool that's designed around the needs of service +owners, not cluster admins. Now developers can easily check the health of their +services no matter how or where those services are deployed — whether it's on a +local host for testing or in production on dozens of clusters around the world. -## Configuration +It will elevate the visibility of errors where identified, and provide drill +down about the deployments, pods, and other objects for a service. -Example: +![Kubernetes plugin screenshot](../../assets/features/kubernetes/backstage-k8s-2-deployments.png) -```yaml -kubernetes: - serviceLocatorMethod: 'multiTenant' - clusterLocatorMethods: - - 'config' - clusters: - - url: http://127.0.0.1:9999 - name: minikube - authProvider: 'serviceAccount' - serviceAccountToken: - $env: K8S_MINIKUBE_TOKEN - - url: http://127.0.0.2:9999 - name: gke-cluster-1 - authProvider: 'google' -``` +The feature is made up of two plugins: +[`@backstage/plugin-kubernetes`](https://github.com/backstage/backstage/tree/master/plugins/kubernetes) +and +[`@backstage/plugin-kubernetes-backend`](https://github.com/backstage/backstage/tree/master/plugins/kubernetes-backend). -### serviceLocatorMethod +The frontend plugin exposes information to the end user in a digestible way, +while the backend wraps the mechanics to connect to Kubernetes clusters to +collect the relevant information. -This configures how to determine which clusters a component is running in. +## Let's use it! -Currently, the only valid value is: - -- `multiTenant` - This configuration assumes that all components run on all the - provided clusters. - -### clusterLocatorMethods - -This is an array used to determine where to retrieve cluster configuration from. - -Currently, the only valid cluster locator method is: - -- `config` - This cluster locator method will read cluster information from your - app-config (see below). - -### clusters - -Used by the `config` cluster locator method to construct Kubernetes clients. - -### clusters.\*.url - -The base URL to the Kubernetes control plane. Can be found by using the -"Kubernetes master" result from running the `kubectl cluster-info` command. - -### clusters.\*.name - -A name to represent this cluster, this must be unique within the `clusters` -array. Users will see this value in the Service Catalog Kubernetes plugin. - -### clusters.\*.authProvider - -This determines how the Kubernetes client authenticates with the Kubernetes -cluster. Valid values are: - -| Value | Description | -| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `serviceAccount` | This will use a Kubernetes [service account](https://kubernetes.io/docs/reference/access-authn-authz/service-accounts-admin/) to access the Kubernetes API. When this is used the `serviceAccountToken` field should also be set. | -| `google` | This will use a user's Google auth token from the [Google auth plugin](https://backstage.io/docs/auth/) to access the Kubernetes API. | - -### clusters.\*.serviceAccount (optional) - -The service account token to be used when using the `serviceAccount` auth -provider. - -## Role Based Access Control - -The current RBAC permissions required are read-only cluster wide, for the -following objects: - -- pods -- services -- configmaps -- deployments -- replicasets -- horizontalpodautoscalers -- ingresses - -## Surfacing your Kubernetes components as part of an entity - -There are two ways to surface your Kubernetes components as part of an entity. -The label selector takes precedence over the annotation/service id. - -### Common `backstage.io/kubernetes-id` label - -#### Adding the entity annotation - -In order for Backstage to detect that an entity has Kubernetes components, the -following annotation should be added to the entity's `catalog-info.yaml`: - -```yaml -annotations: - 'backstage.io/kubernetes-id': dice-roller -``` - -#### Labeling Kubernetes components - -In order for Kubernetes components to show up in the service catalog as a part -of an entity, Kubernetes components themselves can have the following label: - -```yaml -'backstage.io/kubernetes-id': -``` - -### Label selector query annotation - -You can write your own custom label selector query that Backstage will use to -lookup the objects (similar to `kubectl --selector="your query here"`). Review -the -[labels and selectors Kubernetes documentation](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/) -for more info. - -```yaml -'backstage.io/kubernetes-label-selector': 'app=my-app,component=front-end' -``` +To get started, first you must [install the Kubernetes plugins](installation.md) +and then [configure them](configuration.md). diff --git a/docs/features/kubernetes/installation.md b/docs/features/kubernetes/installation.md new file mode 100644 index 0000000000..c2153131c0 --- /dev/null +++ b/docs/features/kubernetes/installation.md @@ -0,0 +1,119 @@ +--- +id: installation +title: Installation +description: Installing Kubernetes plugin +--- + +The Kubernetes feature is a plugin to Backstage, and it is exposed as a tab when +viewing entities in the software catalog. + +If you haven't setup Backstage already, start +[here](../../getting-started/index.md). + +## Adding the Kubernetes frontend plugin + +The first step is to add the frontend Kubernetes plugin to your Backstage +application. Navigate to your new Backstage application directory. And then to +your `packages/app` directory, and install the `@backstage/plugin-kubernetes` +package. + +```bash +cd my-backstage-app/ +cd packages/app +yarn add @backstage/plugin-kubernetes +``` + +Once the package has been installed, you need to import the plugin in your app. +Add the following to `packages/app/src/plugins.ts`: + +`plugins.ts`: + +```typescript +export { plugin as Kubernetes } from '@backstage/plugin-kubernetes'; +``` + +Now, add the "Kubernetes" tab to the catalog entity page. In +`packages/app/src/components/catalog/EntityPage.tsx`, you'll add a router to get +to the tab, and add the tab itself. + +`EntityPage.tsx`: + +```tsx +import { Router as KubernetesRouter } from '@backstage/plugin-kubernetes'; + +// ... + +const ServiceEntityPage = ({ entity }: { entity: Entity }) => ( + + // ... + } + /> + // ... + +); +``` + +That's it! But now, we need the Kubernetes Backend plugin for the frontend to +work. + +## Adding Kubernetes Backend plugin + +Navigate to `packages/backend` of your Backstage app, and install the +`@backstage/plugin-kubernetes-backend` package. + +```bash +cd my-backstage-app/ +cd packages/backend +yarn add @backstage/plugin-kubernetes-backend +``` + +Create a file called `kubernetes.ts` inside `packages/backend/src/plugins/` and +add the following + +`kubernetes.ts`: + +```typescript +import { createRouter } from '@backstage/plugin-kubernetes-backend'; +import { PluginEnvironment } from '../types'; + +export default async function createPlugin({ + logger, + config, +}: PluginEnvironment) { + return await createRouter({ logger, config }); +} +``` + +And import the plugin to `packages/backend/src/index.ts`. There are three lines +of code you'll need to add, and they should be added near similar code in your +existing Backstage backend. + +`index.ts`: + +```typescript +import kubernetes from './plugins/kubernetes'; + +// ... + +const kubernetesEnv = useHotMemoize(module, () => createEnv('kubernetes')); + +// ... + +apiRouter.use('/kubernetes', await kubernetes(kubernetesEnv)); +``` + +That's it! The Kubernetes frontend and backend have now been added to your +Backstage app. + +## Running Backstage locally + +Start the frontend and the backend app by +[running backstage locally](../../getting-started/running-backstage-locally.md). + +## Configuration + +After installing the plugins in the code, you'll need to them +[configure them](configuration.md). diff --git a/microsite/sidebars.json b/microsite/sidebars.json index c2ccea8740..e8d3ab1409 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -39,7 +39,11 @@ { "type": "subcategory", "label": "Kubernetes", - "ids": ["features/kubernetes/overview"] + "ids": [ + "features/kubernetes/overview", + "features/kubernetes/installation", + "features/kubernetes/configuration" + ] }, { "type": "subcategory", From 975fe1ad38173780b5f5f50d61e2053296c48511 Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Mon, 25 Jan 2021 14:42:33 -0500 Subject: [PATCH 029/131] Point to main docs site --- plugins/kubernetes-backend/README.md | 82 +++------------------------- plugins/kubernetes/README.md | 61 ++++++++------------- 2 files changed, 31 insertions(+), 112 deletions(-) diff --git a/plugins/kubernetes-backend/README.md b/plugins/kubernetes-backend/README.md index a9fdc14707..b724babca3 100644 --- a/plugins/kubernetes-backend/README.md +++ b/plugins/kubernetes-backend/README.md @@ -1,83 +1,19 @@ # Kubernetes Backend -WORK IN PROGRESS +This is the backend part of the Kubernetes plugin for Backstage. It is called by and responds to requests from the frontend [`@backstage/plugin-kubernetes`](https://github.com/backstage/backstage/tree/master/plugins/kubernetes) plugin. -This is the backend part of the Kubernetes plugin. +It directly interfaces with the Kubernetes API control plane to obtain information about objects that will then be presented at the front end. -It responds to Kubernetes requests from the frontend. +## Introduction -## Configuration +See our announcement blog post [New Backstage feature: Kubernetes for Service Owners](https://backstage.io/blog/2021/01/12/new-backstage-feature-kubernetes-for-service-owners) to learn more about the motivation behind developing the plugin. -### serviceLocatorMethod +## Setup & Configuration -This configures how to determine which clusters a component is running in. +This plugin must be explicitly added to a Backstage app, along with it's peer frontend plugin. -Currently, the only valid serviceLocatorMethod is: +The plugin requires configuration in the Backstage `app-config.yaml` to connect to a Kubernetes API control plane. -#### multiTenant +In addition, configuration of an entity's `catalog-info.yaml` helps identify which specific Kubernetes object(s) should be presented on a specific entity catalog page. -This configuration assumes that all components run on all the provided clusters. - -### clusterLocatorMethods - -This is used to determine where to retrieve cluster configuration from. - -Currently, the only valid serviceLocatorMethod is: - -#### config - -This clusterLocatorMethod will read cluster information in from config - -Example: - -```yaml -kubernetes: - serviceLocatorMethod: 'multiTenant' - clusterLocatorMethods: - - 'config' - clusters: - - url: http://127.0.0.1:9999 - name: minikube - serviceAccountToken: - authProvider: 'serviceAccount' - - url: http://127.0.0.2:9999 - name: gke-cluster-1 - authProvider: 'google' -``` - -##### clusters - -Used by the `config` `clusterLocatorMethods` to construct Kubernetes clients. - -###### url - -The base url to the Kubernetes control plane. Can be found by using the `Kubernetes master` result from running the `kubectl cluster-info` command. - -###### name - -A name to represent this cluster, this must be unique within the `clusters` array. Users will see this value in the Service Catalog Kubernetes plugin. - -###### authProvider - -This determines how the Kubernetes client authenticate with the Kubernetes cluster. Valid values are: - -| Value | Description | -| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `serviceAccount` | This will use a Kubernetes [service account](https://kubernetes.io/docs/reference/access-authn-authz/service-accounts-admin/) to access the Kubernetes API. When this is used the `serviceAccountToken` field should also be set. | -| `google` | This will use a user's google auth token from the [google auth plugin](https://backstage.io/docs/auth/) to access the Kubernetes API. | - -###### serviceAccount (optional) - -The service account token to be used when using the `authProvider`, `serviceAccount`. - -## RBAC - -The current RBAC permissions required are read-only cluster wide, for the following objects: - -- pods -- services -- configmaps -- deployments -- replicasets -- horizontalpodautoscalers -- ingresses +For more information, see the [formal documentation about the Kubernetes feature in Backstage](https://backstage.io/docs/features/kubernetes/overview). diff --git a/plugins/kubernetes/README.md b/plugins/kubernetes/README.md index a343764b70..15dfd16b17 100644 --- a/plugins/kubernetes/README.md +++ b/plugins/kubernetes/README.md @@ -1,9 +1,29 @@ -# kubernetes +# Kubernetes -Welcome to the kubernetes plugin! +Welcome to the Backstage Kubernetes frontend plugin! + +This plugin exposes information about your entity-specific Kubernetes objects with a desire to provide value to the service owner, rather than just a Kubernetes cluster administrator. + +It will elevate the visibility of errors where identified, and provide drill down about the deployments, pods, and other objects for a service. + +It directly interfaces with the [Kubernetes Backend Plugin (`@backstage-plugin-kubernetes-backend`)](https://github.com/backstage/backstage/tree/master/plugins/kubernetes-backend). _This plugin was created through the Backstage CLI_ +## Introduction + +See our announcement blog post [New Backstage feature: Kubernetes for Service Owners](https://backstage.io/blog/2021/01/12/new-backstage-feature-kubernetes-for-service-owners) to learn more about the motivation behind developing the plugin. + +## Setup & Configuration + +This plugin must be explicitly added to a Backstage app, along with it's peer backend plugin. + +It requires configuration in the Backstage `app-config.yaml` to connect to a Kubernetes API control plane. + +In addition, configuration of an entity's `catalog-info.yaml` helps identify which specific Kubernetes object(s) should be presented on a specific entity catalog page. + +For more information, see the [formal documentation about the Kubernetes feature in Backstage](https://backstage.io/docs/features/kubernetes/overview). + ## 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 [/kubernetes](http://localhost:3000/kubernetes). @@ -11,40 +31,3 @@ Your plugin has been added to the example app in this repository, meaning you'll 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. - -## Surfacing your Kubernetes components as part of an entity - -There are two ways to surface your kubernetes components as part of an entity. -The label selector takes precedence over the annotation/service id. - -### Common `backstage.io/kubernetes-id` label - -#### Adding the entity annotation - -In order for Backstage to detect that an entity has Kubernetes components, -the following annotation should be added to the entity. - -```yaml -annotations: - 'backstage.io/kubernetes-id': dice-roller -``` - -#### Labeling Kubernetes components - -In order for Kubernetes components to show up in the service catalog -as a part of an entity, Kubernetes components must be labeled with the following label: - -```yaml -'backstage.io/kubernetes-id': -``` - -### label selector query annotation - -#### Adding a label selector query annotation - -You can write your own custom label selector query that backstage will use to lookup the objects (similar to `kubectl --selector="your query here"`) -review the documentation [here](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/) for more info - -```yaml -'backstage.io/kubernetes-label-selector': 'app=my-app,component=front-end' -``` From 45621d88eaeca7c32d37b6d8d1cfa23a352afa3f Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Mon, 25 Jan 2021 14:46:48 -0500 Subject: [PATCH 030/131] Update descriptions --- docs/features/kubernetes/configuration.md | 3 ++- docs/features/kubernetes/installation.md | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/features/kubernetes/configuration.md b/docs/features/kubernetes/configuration.md index 5c702d9d66..1ecca6b53f 100644 --- a/docs/features/kubernetes/configuration.md +++ b/docs/features/kubernetes/configuration.md @@ -2,7 +2,8 @@ id: configuration title: Configuring Kubernetes integration sidebar_label: Configuration -description: Monitoring Kubernetes based services with the service catalog +# prettier-ignore +description: Configuring the Kubernetes integration for Backstage expose your entity's objects --- Configuring the Backstage Kubernetes integration involves two steps: diff --git a/docs/features/kubernetes/installation.md b/docs/features/kubernetes/installation.md index c2153131c0..9988ee8221 100644 --- a/docs/features/kubernetes/installation.md +++ b/docs/features/kubernetes/installation.md @@ -1,7 +1,7 @@ --- id: installation title: Installation -description: Installing Kubernetes plugin +description: Installing Kubernetes plugin into Backstage --- The Kubernetes feature is a plugin to Backstage, and it is exposed as a tab when From e6904cb3d6b2b3480b4574bcce2505f9c2c436cb Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Mon, 25 Jan 2021 14:49:55 -0500 Subject: [PATCH 031/131] Tweak wording --- docs/features/kubernetes/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/kubernetes/configuration.md b/docs/features/kubernetes/configuration.md index 1ecca6b53f..3e138a7a0b 100644 --- a/docs/features/kubernetes/configuration.md +++ b/docs/features/kubernetes/configuration.md @@ -9,7 +9,7 @@ description: Configuring the Kubernetes integration for Backstage expose your en Configuring the Backstage Kubernetes integration involves two steps: 1. Enabling the backend to collect objects from your Kubernetes cluster(s). -2. Surfacing your Kubernetes as part of a catalog entity +2. Surfacing your Kubernetes objects in catalog entities ## Configuring Kubernetes Clusters From 682b2c6a1002fbb4d5c24a04dc16a0ba3590ccaf Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Mon, 25 Jan 2021 14:51:32 -0500 Subject: [PATCH 032/131] Fix typo --- docs/features/kubernetes/installation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/kubernetes/installation.md b/docs/features/kubernetes/installation.md index 9988ee8221..9e531de655 100644 --- a/docs/features/kubernetes/installation.md +++ b/docs/features/kubernetes/installation.md @@ -115,5 +115,5 @@ Start the frontend and the backend app by ## Configuration -After installing the plugins in the code, you'll need to them +After installing the plugins in the code, you'll need to then [configure them](configuration.md). From ca748637f2bc0c3417248fe23d840b812b1e5510 Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Mon, 25 Jan 2021 15:58:15 -0500 Subject: [PATCH 033/131] Change category from Feedback to Data Display --- packages/core/src/components/ProgressBars/Gauge.stories.tsx | 2 +- .../core/src/components/ProgressBars/LinearGauge.stories.tsx | 2 +- packages/core/src/components/Status/Status.stories.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core/src/components/ProgressBars/Gauge.stories.tsx b/packages/core/src/components/ProgressBars/Gauge.stories.tsx index 7882714529..ab9c263f05 100644 --- a/packages/core/src/components/ProgressBars/Gauge.stories.tsx +++ b/packages/core/src/components/ProgressBars/Gauge.stories.tsx @@ -20,7 +20,7 @@ import { Gauge } from './Gauge'; const containerStyle = { width: 300 }; export default { - title: 'Feedback/Gauge', + title: 'Data Display/Gauge', component: Gauge, }; diff --git a/packages/core/src/components/ProgressBars/LinearGauge.stories.tsx b/packages/core/src/components/ProgressBars/LinearGauge.stories.tsx index a745d210b4..fa3c7c00f0 100644 --- a/packages/core/src/components/ProgressBars/LinearGauge.stories.tsx +++ b/packages/core/src/components/ProgressBars/LinearGauge.stories.tsx @@ -20,7 +20,7 @@ import { LinearGauge } from './LinearGauge'; const containerStyle = { width: 300 }; export default { - title: 'Feedback/LinearGauge', + title: 'Data Display/LinearGauge', component: LinearGauge, }; diff --git a/packages/core/src/components/Status/Status.stories.tsx b/packages/core/src/components/Status/Status.stories.tsx index 560d07ad87..205645e1ec 100644 --- a/packages/core/src/components/Status/Status.stories.tsx +++ b/packages/core/src/components/Status/Status.stories.tsx @@ -27,7 +27,7 @@ import { Table } from '../Table'; import { InfoCard } from '../../layout/InfoCard'; export default { - title: 'Feedback/Status', + title: 'Data Display/Status', component: StatusOK, }; From 9ba282f7c7bd402a64dcc79c5287c824df0505e6 Mon Sep 17 00:00:00 2001 From: Tim Hansen Date: Mon, 25 Jan 2021 20:15:50 -0700 Subject: [PATCH 034/131] Update comments, use MUI's capitalize --- plugins/cost-insights/src/client.ts | 8 ++++---- .../src/components/CostOverviewCard/CostOverviewCard.tsx | 3 ++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/plugins/cost-insights/src/client.ts b/plugins/cost-insights/src/client.ts index c43deba6ae..fa9a1d23f5 100644 --- a/plugins/cost-insights/src/client.ts +++ b/plugins/cost-insights/src/client.ts @@ -100,8 +100,8 @@ export class ExampleCostInsightsClient implements CostInsightsApi { aggregation: aggregation, change: changeOf(aggregation), trendline: trendlineOf(aggregation), - // Optional field on Cost which needs to be supplied in order to see - // the product breakdown view in the top panel. + // Optional field providing cost groupings / breakdowns keyed by the type. In this example, + // daily cost grouped by cloud product OR by project / billing account. groupedCosts: { product: getGroupedProducts(intervals), project: getGroupedProjects(intervals), @@ -121,8 +121,8 @@ export class ExampleCostInsightsClient implements CostInsightsApi { aggregation: aggregation, change: changeOf(aggregation), trendline: trendlineOf(aggregation), - // Optional field on Cost which needs to be supplied in order to see - // the product breakdown view in the top panel. + // Optional field providing cost groupings / breakdowns keyed by the type. In this example, + // daily project cost grouped by cloud product. groupedCosts: { product: getGroupedProducts(intervals), }, diff --git a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.tsx b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.tsx index 614e045b20..56414c9a0e 100644 --- a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.tsx +++ b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.tsx @@ -17,6 +17,7 @@ import React, { useEffect, useState } from 'react'; import { Box, + capitalize, Card, CardContent, Divider, @@ -72,7 +73,7 @@ export const CostOverviewCard = ({ key => ({ id: key, label: `Breakdown by ${key}`, - title: `Cloud Cost By ${key.charAt(0).toUpperCase() + key.slice(1)}`, + title: `Cloud Cost By ${capitalize(key)}`, }), ); const tabs = [ From 9f2a02ae044232b3b19305f19a3bf2a99cd2ca4e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Jan 2021 04:50:16 +0000 Subject: [PATCH 035/131] chore(deps): bump @octokit/request from 5.4.12 to 5.4.13 Bumps [@octokit/request](https://github.com/octokit/request.js) from 5.4.12 to 5.4.13. - [Release notes](https://github.com/octokit/request.js/releases) - [Commits](https://github.com/octokit/request.js/compare/v5.4.12...v5.4.13) Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 28602e734f..be71215485 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4774,9 +4774,9 @@ once "^1.4.0" "@octokit/request@^5.2.0", "@octokit/request@^5.3.0", "@octokit/request@^5.4.11", "@octokit/request@^5.4.12": - version "5.4.12" - resolved "https://registry.npmjs.org/@octokit/request/-/request-5.4.12.tgz#b04826fa934670c56b135a81447be2c1723a2ffc" - integrity sha512-MvWYdxengUWTGFpfpefBBpVmmEYfkwMoxonIB3sUGp5rhdgwjXL1ejo6JbgzG/QD9B/NYt/9cJX1pxXeSIUCkg== + version "5.4.13" + resolved "https://registry.npmjs.org/@octokit/request/-/request-5.4.13.tgz#eec5987b3e96f984fc5f41967e001170c6d23a18" + integrity sha512-WcNRH5XPPtg7i1g9Da5U9dvZ6YbTffw9BN2rVezYiE7couoSyaRsw0e+Tl8uk1fArHE7Dn14U7YqUDy59WaqEw== dependencies: "@octokit/endpoint" "^6.0.1" "@octokit/request-error" "^2.0.0" From 7e03ef6b437d36e1d3319b4ba33a4002b5dd8269 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Jan 2021 04:59:40 +0000 Subject: [PATCH 036/131] chore(deps-dev): bump @types/mini-css-extract-plugin from 0.9.1 to 1.2.2 Bumps [@types/mini-css-extract-plugin](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/mini-css-extract-plugin) from 0.9.1 to 1.2.2. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/mini-css-extract-plugin) Signed-off-by: dependabot[bot] --- packages/cli/package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 0d1605f752..a98534ecb6 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -125,7 +125,7 @@ "@types/html-webpack-plugin": "^3.2.2", "@types/http-proxy": "^1.17.4", "@types/inquirer": "^7.3.1", - "@types/mini-css-extract-plugin": "^0.9.1", + "@types/mini-css-extract-plugin": "^1.2.2", "@types/mock-fs": "^4.13.0", "@types/node": "^13.7.2", "@types/react-dev-utils": "^9.0.4", diff --git a/yarn.lock b/yarn.lock index 28602e734f..abcf58206a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6828,10 +6828,10 @@ resolved "https://registry.npmjs.org/@types/mime/-/mime-2.0.1.tgz#dc488842312a7f075149312905b5e3c0b054c79d" integrity sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw== -"@types/mini-css-extract-plugin@^0.9.1": - version "0.9.1" - resolved "https://registry.npmjs.org/@types/mini-css-extract-plugin/-/mini-css-extract-plugin-0.9.1.tgz#d4bdde5197326fca039d418f4bdda03dc74dc451" - integrity sha512-+mN04Oszdz9tGjUP/c1ReVwJXxSniLd7lF++sv+8dkABxVNthg6uccei+4ssKxRHGoMmPxdn7uBdJWONSJGTGQ== +"@types/mini-css-extract-plugin@^1.2.2": + version "1.2.2" + resolved "https://registry.npmjs.org/@types/mini-css-extract-plugin/-/mini-css-extract-plugin-1.2.2.tgz#e6031da8d60777b3da3f5b4daf285437d7b6580b" + integrity sha512-EoHBJ4rcrd5j7weAFE4yU1gxedx53EFCWKso03G7DW0h2YvtwjKYz/NnuFHudcQDI1HpTLqoQFTwEgfJxygYCw== dependencies: "@types/webpack" "*" From 965ca841f2192e43b9a2e092dd4d4121828f569e Mon Sep 17 00:00:00 2001 From: Nir Gazit Date: Tue, 26 Jan 2021 09:22:04 +0200 Subject: [PATCH 037/131] Updated changeset with breaking changes documentation --- .changeset/fast-flowers-tickle.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/.changeset/fast-flowers-tickle.md b/.changeset/fast-flowers-tickle.md index 193b9b4459..48051c197b 100644 --- a/.changeset/fast-flowers-tickle.md +++ b/.changeset/fast-flowers-tickle.md @@ -4,3 +4,28 @@ --- Added support for multiple Kafka clusters and multiple consumers per component. +Note that this introduces several breaking changes. + +1. Configuration in `app-config.yaml` has changed to support the ability to configure multiple clusters. This means you are required to update the configs in the following way: + +```diff +kafka: + clientId: backstage +- brokers: +- - localhost:9092 ++ clusters: ++ - name: prod ++ brokers: ++ - localhost:9092 +``` + +2. Configuration of services has changed as well to support multiple clusters: + +```diff + annotations: +- kafka.apache.org/consumer-groups: consumer ++ kafka.apache.org/consumer-groups: prod/consumer +``` + +3. Kafka Backend API has changed, so querying offsets of a consumer group is now done with the following query path: + `/consumers/${clusterId}/${consumerGroup}/offsets` From 1f1b8ba77bb0741f1ed59d85a4832b480ffce543 Mon Sep 17 00:00:00 2001 From: Nir Gazit Date: Tue, 26 Jan 2021 09:23:11 +0200 Subject: [PATCH 038/131] Updated version changes to be minor --- .changeset/fast-flowers-tickle.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/fast-flowers-tickle.md b/.changeset/fast-flowers-tickle.md index 48051c197b..3eacb6f1a0 100644 --- a/.changeset/fast-flowers-tickle.md +++ b/.changeset/fast-flowers-tickle.md @@ -1,6 +1,6 @@ --- -'@backstage/plugin-kafka': patch -'@backstage/plugin-kafka-backend': patch +'@backstage/plugin-kafka': minor +'@backstage/plugin-kafka-backend': minor --- Added support for multiple Kafka clusters and multiple consumers per component. From 2d4a11423778add265b101c4238280101c4df36a Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Tue, 26 Jan 2021 10:54:06 +0100 Subject: [PATCH 039/131] Remove heading from the adr template --- docs/architecture-decisions/adr000-template.md | 2 -- docs/architecture-decisions/adr010-luxon-date-library.md | 2 -- 2 files changed, 4 deletions(-) diff --git a/docs/architecture-decisions/adr000-template.md b/docs/architecture-decisions/adr000-template.md index 538afccf87..5e6edd8dba 100644 --- a/docs/architecture-decisions/adr000-template.md +++ b/docs/architecture-decisions/adr000-template.md @@ -4,8 +4,6 @@ title: ADR000: [TITLE] description: Architecture Decision Record (ADR) for [TITLE] [DESCRIPTION] --- -# ADR000: [title] - ## Context diff --git a/docs/architecture-decisions/adr010-luxon-date-library.md b/docs/architecture-decisions/adr010-luxon-date-library.md index b4c1ddedba..43f18f0f69 100644 --- a/docs/architecture-decisions/adr010-luxon-date-library.md +++ b/docs/architecture-decisions/adr010-luxon-date-library.md @@ -4,8 +4,6 @@ title: ADR010: Use the Luxon Date Library description: Architecture Decision Record (ADR) for Luxon Date Library --- -# ADR010: Use the Luxon Date Library - ## Context Date formatting (e.g. `a day ago`) and calculations are common within Backstage. From 6327bb8770a78c8696928d885c8f302cfc70cbc9 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 26 Jan 2021 13:33:56 +0100 Subject: [PATCH 040/131] chore: Added example multi-stage with the new backstage bundle command --- .../docker/multi-stage-with-bundle/Dockerfile | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 contrib/docker/multi-stage-with-bundle/Dockerfile diff --git a/contrib/docker/multi-stage-with-bundle/Dockerfile b/contrib/docker/multi-stage-with-bundle/Dockerfile new file mode 100644 index 0000000000..5e6ce770de --- /dev/null +++ b/contrib/docker/multi-stage-with-bundle/Dockerfile @@ -0,0 +1,42 @@ +# Stage 1 - Create yarn install skeleton layer +FROM node:14-buster AS packages + +WORKDIR /app +COPY package.json yarn.lock ./ + +# Uncomment this line if building a non create-app version +# COPY packages packages +COPY plugins plugins + +RUN find packages \! -name "package.json" -mindepth 2 -maxdepth 2 -print | xargs rm -rf + +# Stage 2 - Install dependencies and build packages +FROM node:14-buster AS build + +WORKDIR /app +COPY --from=packages /app . + +RUN yarn install --network-timeout 600000 && rm -rf "$(yarn cache dir)" + +COPY . . + +RUN yarn tsc +RUN yarn --cwd packages/backend backstage-cli backend:bundle --build-dependencies + +# Stage 3 - Build the actual backend image and install production dependencies +FROM node:14-buster + +WORKDIR /app + +# Copy from build stage +COPY --from=build /app/yarn.lock /app/package.json /app/packages/backend/dist/skeleton.tar.gz ./ +RUN tar xzf skeleton.tar.gz && rm skeleton.tar.gz + +RUN yarn install --production --network-timeout 600000 && rm -rf "$(yarn cache dir)" + +COPY --from=build /app/packages/backend/dist/bundle.tar.gz . +RUN tar xzf bundle.tar.gz && rm bundle.tar.gz + +COPY app-config.yaml app-config.production.yaml ./ + +CMD ["node", "packages/backend", "--config", "app-config.yaml", "--config", "app-config.production.yaml"] \ No newline at end of file From 294b7b087d222112134c914ca6222c37fc231ffd Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 26 Jan 2021 13:41:15 +0100 Subject: [PATCH 041/131] chore: fix newline on new file --- contrib/docker/multi-stage-with-bundle/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/docker/multi-stage-with-bundle/Dockerfile b/contrib/docker/multi-stage-with-bundle/Dockerfile index 5e6ce770de..8289a49046 100644 --- a/contrib/docker/multi-stage-with-bundle/Dockerfile +++ b/contrib/docker/multi-stage-with-bundle/Dockerfile @@ -39,4 +39,4 @@ RUN tar xzf bundle.tar.gz && rm bundle.tar.gz COPY app-config.yaml app-config.production.yaml ./ -CMD ["node", "packages/backend", "--config", "app-config.yaml", "--config", "app-config.production.yaml"] \ No newline at end of file +CMD ["node", "packages/backend", "--config", "app-config.yaml", "--config", "app-config.production.yaml"] From 00448abb0343cbab1ff8aa3ce6393ec0c3688158 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 26 Jan 2021 15:47:23 +0100 Subject: [PATCH 042/131] chore: updating docs reference to integrations condfig --- .../software-templates/installation.md | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/docs/features/software-templates/installation.md b/docs/features/software-templates/installation.md index 724b86aed1..6d516eef3a 100644 --- a/docs/features/software-templates/installation.md +++ b/docs/features/software-templates/installation.md @@ -173,7 +173,12 @@ and access to a running Docker daemon. You can create a GitHub access token docs on creating private GitHub access tokens is available [here](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token). Note that the need for private GitHub access tokens will be replaced with GitHub -Apps integration further down the line. +Apps integration further down the line by using the existing `integrations` +config. + +> Note: Some of this config may already be setup part of your `app-config.yaml`. +> We're moving away from the duplciated config for authentication in the +> `scaffolder` section and using `integrations` instead. #### GitHub @@ -187,11 +192,14 @@ by specifying `visibility` option. Valid options are `public`, `private` and public within the enterprise. ```yaml -scaffolder: +integrations: github: - token: - $env: GITHUB_TOKEN - visibility: public # or 'internal' or 'private' + - host: github.com + token: + $env: GITHUB_TOKEN + +scaffolder: + visibility: public # or 'internal' or 'private' ``` #### GitLab @@ -201,10 +209,9 @@ allows to configure the private access token and the base URL of a GitLab instance: ```yaml -scaffolder: +integrations: gitlab: - api: - baseUrl: https://gitlab.com + - host: gitlab.com token: $env: GITLAB_TOKEN ``` @@ -218,10 +225,9 @@ will hopefully support on-prem installations as well but that has not been verified. ```yaml -scaffolder: +integrations: azure: - baseUrl: https://dev.azure.com/{your-organization} - api: + - host: dev.azure.com token: $env: AZURE_TOKEN ``` From 46533ed01b75c8751f6c7c05ffb1794098f8cd2c Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 26 Jan 2021 15:53:32 +0100 Subject: [PATCH 043/131] chore: fix the config key for scaffolderr --- docs/features/software-templates/installation.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/features/software-templates/installation.md b/docs/features/software-templates/installation.md index 6d516eef3a..c2f95f26ea 100644 --- a/docs/features/software-templates/installation.md +++ b/docs/features/software-templates/installation.md @@ -199,7 +199,8 @@ integrations: $env: GITHUB_TOKEN scaffolder: - visibility: public # or 'internal' or 'private' + github: + visibility: public # or 'internal' or 'private' ``` #### GitLab From ad52b78b8dd50331cf535a461a1e3ae51cacd97f Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 26 Jan 2021 15:55:34 +0100 Subject: [PATCH 044/131] chore: reworking the files so that it's moved to docs instea --- .../docker/multi-stage-with-bundle/Dockerfile | 42 ------------ docs/getting-started/deployment-other.md | 64 ++++++++++++++++--- 2 files changed, 56 insertions(+), 50 deletions(-) delete mode 100644 contrib/docker/multi-stage-with-bundle/Dockerfile diff --git a/contrib/docker/multi-stage-with-bundle/Dockerfile b/contrib/docker/multi-stage-with-bundle/Dockerfile deleted file mode 100644 index 8289a49046..0000000000 --- a/contrib/docker/multi-stage-with-bundle/Dockerfile +++ /dev/null @@ -1,42 +0,0 @@ -# Stage 1 - Create yarn install skeleton layer -FROM node:14-buster AS packages - -WORKDIR /app -COPY package.json yarn.lock ./ - -# Uncomment this line if building a non create-app version -# COPY packages packages -COPY plugins plugins - -RUN find packages \! -name "package.json" -mindepth 2 -maxdepth 2 -print | xargs rm -rf - -# Stage 2 - Install dependencies and build packages -FROM node:14-buster AS build - -WORKDIR /app -COPY --from=packages /app . - -RUN yarn install --network-timeout 600000 && rm -rf "$(yarn cache dir)" - -COPY . . - -RUN yarn tsc -RUN yarn --cwd packages/backend backstage-cli backend:bundle --build-dependencies - -# Stage 3 - Build the actual backend image and install production dependencies -FROM node:14-buster - -WORKDIR /app - -# Copy from build stage -COPY --from=build /app/yarn.lock /app/package.json /app/packages/backend/dist/skeleton.tar.gz ./ -RUN tar xzf skeleton.tar.gz && rm skeleton.tar.gz - -RUN yarn install --production --network-timeout 600000 && rm -rf "$(yarn cache dir)" - -COPY --from=build /app/packages/backend/dist/bundle.tar.gz . -RUN tar xzf bundle.tar.gz && rm bundle.tar.gz - -COPY app-config.yaml app-config.production.yaml ./ - -CMD ["node", "packages/backend", "--config", "app-config.yaml", "--config", "app-config.production.yaml"] diff --git a/docs/getting-started/deployment-other.md b/docs/getting-started/deployment-other.md index 55582ee77a..900173a6c5 100644 --- a/docs/getting-started/deployment-other.md +++ b/docs/getting-started/deployment-other.md @@ -4,19 +4,67 @@ title: Other description: Documentation on different ways of Deployment --- -## Deploying Locally +## Docker -### Try on Docker +Here we have an example Dockerfile that you can use to build everything together +in one container. This Dockerfile uses multi-stage builds, and a +`backend:bundle` command from the CLI. -Run the following commands if you have Docker environment +It also provides caching on the `yarn install`'s so that you don't have to do it +unless absolutely necessary. -```bash -$ yarn install -$ yarn docker-build -$ docker run --rm -it -p 7000:7000 -e APP_ENV=production -e NODE_ENV=development example-backend:latest +```Dockerfile +# Stage 1 - Create yarn install skeleton layer +FROM node:14-buster AS packages + +WORKDIR /app +COPY package.json yarn.lock ./ + +COPY packages packages + +# Uncomment this line if you have a local plugins folder +# COPY plugins plugins + +RUN find packages \! -name "package.json" -mindepth 2 -maxdepth 2 -print | xargs rm -rf + +# Stage 2 - Install dependencies and build packages +FROM node:14-buster AS build + +WORKDIR /app +COPY --from=packages /app . + +RUN yarn install --network-timeout 600000 && rm -rf "$(yarn cache dir)" + +COPY . . + +RUN yarn tsc +RUN yarn --cwd packages/backend backstage-cli backend:bundle --build-dependencies + +# Stage 3 - Build the actual backend image and install production dependencies +FROM node:14-buster + +WORKDIR /app + +# Copy from build stage +COPY --from=build /app/yarn.lock /app/package.json /app/packages/backend/dist/skeleton.tar.gz ./ +RUN tar xzf skeleton.tar.gz && rm skeleton.tar.gz + +RUN yarn install --production --network-timeout 600000 && rm -rf "$(yarn cache dir)" + +COPY --from=build /app/packages/backend/dist/bundle.tar.gz . +RUN tar xzf bundle.tar.gz && rm bundle.tar.gz + +COPY app-config.yaml app-config.production.yaml ./ + +CMD ["node", "packages/backend", "--config", "app-config.yaml", "--config", "app-config.production.yaml"] ``` -Then open http://localhost:7000 on your browser. +You can add the Dockerfile to the root of your project, and run the following: + +```sh +$ docker build -t eaxmple-deployment . +$ docker run -p 7000:7000 example-deployment +``` ## Heroku From 6cc06d4e7f7630c6747b2a8bc0fa884cb7637366 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 26 Jan 2021 15:56:33 +0100 Subject: [PATCH 045/131] chore: fixing spelling of docs --- docs/getting-started/deployment-other.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/getting-started/deployment-other.md b/docs/getting-started/deployment-other.md index 900173a6c5..6cc90c88fb 100644 --- a/docs/getting-started/deployment-other.md +++ b/docs/getting-started/deployment-other.md @@ -62,7 +62,7 @@ CMD ["node", "packages/backend", "--config", "app-config.yaml", "--config", "app You can add the Dockerfile to the root of your project, and run the following: ```sh -$ docker build -t eaxmple-deployment . +$ docker build -t example-deployment . $ docker run -p 7000:7000 example-deployment ``` From 2f547a668fea41611515737741136ebd49ecfc26 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 26 Jan 2021 16:03:26 +0100 Subject: [PATCH 046/131] chore: fix spelling mistake --- docs/features/software-templates/installation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/software-templates/installation.md b/docs/features/software-templates/installation.md index c2f95f26ea..c0bc10fbe0 100644 --- a/docs/features/software-templates/installation.md +++ b/docs/features/software-templates/installation.md @@ -177,7 +177,7 @@ Apps integration further down the line by using the existing `integrations` config. > Note: Some of this config may already be setup part of your `app-config.yaml`. -> We're moving away from the duplciated config for authentication in the +> We're moving away from the duplicated config for authentication in the > `scaffolder` section and using `integrations` instead. #### GitHub From e77a8dc14e905e728bcb685cbeee56fad73d01d6 Mon Sep 17 00:00:00 2001 From: Ben Lambert Date: Tue, 26 Jan 2021 17:02:03 +0100 Subject: [PATCH 047/131] Update docs/features/software-templates/installation.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw --- docs/features/software-templates/installation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/software-templates/installation.md b/docs/features/software-templates/installation.md index c0bc10fbe0..a00de1aa5e 100644 --- a/docs/features/software-templates/installation.md +++ b/docs/features/software-templates/installation.md @@ -176,7 +176,7 @@ Note that the need for private GitHub access tokens will be replaced with GitHub Apps integration further down the line by using the existing `integrations` config. -> Note: Some of this config may already be setup part of your `app-config.yaml`. +> Note: Some of this configuration may already be set up as part of your `app-config.yaml`. > We're moving away from the duplicated config for authentication in the > `scaffolder` section and using `integrations` instead. From cb99b8fc1dcbb8b08e531e4842258ac9eda39775 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 26 Jan 2021 17:07:27 +0100 Subject: [PATCH 048/131] chore: prettier makes pretty --- docs/features/software-templates/installation.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/features/software-templates/installation.md b/docs/features/software-templates/installation.md index a00de1aa5e..8d2fa7727d 100644 --- a/docs/features/software-templates/installation.md +++ b/docs/features/software-templates/installation.md @@ -176,9 +176,9 @@ Note that the need for private GitHub access tokens will be replaced with GitHub Apps integration further down the line by using the existing `integrations` config. -> Note: Some of this configuration may already be set up as part of your `app-config.yaml`. -> We're moving away from the duplicated config for authentication in the -> `scaffolder` section and using `integrations` instead. +> Note: Some of this configuration may already be set up as part of your +> `app-config.yaml`. We're moving away from the duplicated config for +> authentication in the `scaffolder` section and using `integrations` instead. #### GitHub From 2dee00292cbdf35d52b43401cea738eaded29e36 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 26 Jan 2021 17:34:52 +0100 Subject: [PATCH 049/131] chore: review comments --- docs/getting-started/deployment-other.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/getting-started/deployment-other.md b/docs/getting-started/deployment-other.md index 6cc90c88fb..569ef6e898 100644 --- a/docs/getting-started/deployment-other.md +++ b/docs/getting-started/deployment-other.md @@ -13,6 +13,9 @@ in one container. This Dockerfile uses multi-stage builds, and a It also provides caching on the `yarn install`'s so that you don't have to do it unless absolutely necessary. +> Note: This Dockerfile assumes that you're running SQLite, or your +> configuration is setup to connect to an external PostgreSQL Database. + ```Dockerfile # Stage 1 - Create yarn install skeleton layer FROM node:14-buster AS packages @@ -66,6 +69,8 @@ $ docker build -t example-deployment . $ docker run -p 7000:7000 example-deployment ``` +Once complete, open your browser at `http://localhost:7000` if running locally. + ## Heroku Deploying to Heroku is relatively easy following these steps. From 8080d1ef4fb0b263f08568d605406ce6c8d6728a Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 26 Jan 2021 17:39:28 +0100 Subject: [PATCH 050/131] chore: change the wording slightly --- docs/getting-started/deployment-other.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/docs/getting-started/deployment-other.md b/docs/getting-started/deployment-other.md index 569ef6e898..6436bdb195 100644 --- a/docs/getting-started/deployment-other.md +++ b/docs/getting-started/deployment-other.md @@ -62,14 +62,21 @@ COPY app-config.yaml app-config.production.yaml ./ CMD ["node", "packages/backend", "--config", "app-config.yaml", "--config", "app-config.production.yaml"] ``` -You can add the Dockerfile to the root of your project, and run the following: +You can add the Dockerfile to the root of your project, and run the following to +build the container under a specified tag. ```sh $ docker build -t example-deployment . -$ docker run -p 7000:7000 example-deployment ``` -Once complete, open your browser at `http://localhost:7000` if running locally. +To run the image locally you can run: + +```sh +$ docker run -p -it 7000:7000 example-deployment +``` + +You should then start to get logs in your terminal, and then you can open your +browser at `http://localhost:7000` ## Heroku From fb118a6cf25c32a5a6e38f804a47299385fd3a59 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Tue, 26 Jan 2021 15:40:14 +0100 Subject: [PATCH 051/131] Add ADR for Plugin package Structure --- .../adr011-plugin-package-structure.md | 74 +++++++++++++++++++ microsite/sidebars.json | 3 +- mkdocs.yml | 1 + 3 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 docs/architecture-decisions/adr011-plugin-package-structure.md diff --git a/docs/architecture-decisions/adr011-plugin-package-structure.md b/docs/architecture-decisions/adr011-plugin-package-structure.md new file mode 100644 index 0000000000..9133e5b7c2 --- /dev/null +++ b/docs/architecture-decisions/adr011-plugin-package-structure.md @@ -0,0 +1,74 @@ +--- +id: adrs-adr011 +title: ADR011: Plugin Package Structure +description: Architecture Decision Record (ADR) for Plugin Package Structure +--- + +## Context + +A core feature of Backstage is the extensibility via plugins. The Backstage +repository is open for contributions of plugins. Even most of the core features +are implemented as plugins. A plugin consists of one or multiple packages in the +`plugins/` directory. Up till now, we have a simple conventions for naming +plugin packages: Plugins are named `x`, with the option of having a related +backend plugin called `x-backend` (where `x` is the plugin name, like `catalog` +or `techdocs`). There is a need for sharing code between the frontend and +backend of a plugin, between backend plugins, or components and hooks between +different frontend plugins +([some examples](https://github.com/backstage/backstage/issues/3655#issuecomment-758166746)). +This results in emerging plugin packages with shared code, like +`packages/catalog-client` or `packages/techdocs-common`. + +> There is a common phrase in software development: +> [Naming things is hard](https://martinfowler.com/bliki/TwoHardThings.html) + +To keep the contributed plugins consistent, this Architecture Decision Record +provides rules for naming plugin packages. + +## Decision + +We will place all plugin related code in the `plugins/` directory. The +`packages/` directory is reserved for core package of Backstage. + +We follow this structure for plugin packages (where `x` is the plugin name, for +example `catalog` or `techdocs`): + +- `x`: Contains the main frontend code of the plugin. +- `x-backend`: Contains the main backend code of the plugin. +- `x-react`: Contains shared widgets, hooks and similar that both the plugin + itself (`x`) and third-party frontend plugins can depend on. +- `x-node`: Contains utilities for backends that both the plugin backend itself + (`x-backend`) and third-party backend plugins can depend on. +- `x-common`: An isomorphic package with platform agnostic models, clients, and + utilities that all packages above or any third-party plugin package can depend + on. + +We prefix the package names with `@backstage/plugin-`. + +This structure is based on a +[suggestion in issue #3655](https://github.com/backstage/backstage/issues/3655#issuecomment-758166746). + +## Consequences + +We will actively migrate existing packages that are part of a plugin to the +`plugins/` folder. This affects packages like: + +- `packages/techdocs-common` which should be moved to `plugins/techdocs-node` + and named `@backstage/plugin-techdocs-node`. +- `packages/catalog-client` which will be part of a future + `plugins/catalog-common` and named `@backstage/plugin-catalog-common`. +- While the new location of `packages/catalog-model` should be + `plugins/catalog-common` we might want to do an exception here, as it's a very + central package. + +The limited set of rules might not be sufficient in the future. If additional +packages are required, we will revisit this decision and extend the pattern. + +If possible, we will add tools, such as lint rules, to help enforce the package +names and dependencies between them or CLI commands to generate these packages. + +The distinction between core packages and plugins helps us to setup +[CODEOWNERS](https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners) +in the repository. We can set the code owners for the `packages/` folder to the +core team and create additional rules (like `plugins/x*`) for plugin +maintainers. diff --git a/microsite/sidebars.json b/microsite/sidebars.json index c2ccea8740..eb2d503476 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -184,7 +184,8 @@ "architecture-decisions/adrs-adr007", "architecture-decisions/adrs-adr008", "architecture-decisions/adrs-adr009", - "architecture-decisions/adrs-adr010" + "architecture-decisions/adrs-adr010", + "architecture-decisions/adrs-adr011" ], "Contribute": ["../CONTRIBUTING"], "Support": ["support/support", "support/project-structure"], diff --git a/mkdocs.yml b/mkdocs.yml index a2cbf11b4c..6236d1c1a0 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -119,6 +119,7 @@ nav: - ADR008 - Default Catalog File Name: 'architecture-decisions/adr008-default-catalog-file-name.md' - ADR009 - Entity References: 'architecture-decisions/adr009-entity-references.md' - ADR010 - Luxon Date Library: 'architecture-decisions/adr010-luxon-date-library.md' + - ADR011 - Plugin Package Structure: 'architecture-decisions/adr011-plugin-package-structure.md' - Contribute: '../CONTRIBUTING.md' - Support: - 'support/support.md' From f8b652614e0965a6dd5fd90bca6e66a7e05c0297 Mon Sep 17 00:00:00 2001 From: Jonah Back Date: Tue, 26 Jan 2021 10:50:12 -0800 Subject: [PATCH 052/131] fix: change issuer to iss to conform to JWT spec. Make issuer verification optional --- plugins/auth-backend/src/providers/aws-alb/provider.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/plugins/auth-backend/src/providers/aws-alb/provider.ts b/plugins/auth-backend/src/providers/aws-alb/provider.ts index 41dc5cf916..a5db2869b5 100644 --- a/plugins/auth-backend/src/providers/aws-alb/provider.ts +++ b/plugins/auth-backend/src/providers/aws-alb/provider.ts @@ -34,7 +34,7 @@ const ALB_JWT_HEADER = 'x-amzn-oidc-data'; */ type AwsAlbAuthProviderOptions = { region: string; - issuer: string; + issuer?: string; identityResolutionCallback: ExperimentalIdentityResolver; }; export const getJWTHeaders = (input: string) => { @@ -70,10 +70,7 @@ export class AwsAlbAuthProvider implements AuthProviderRouteHandlers { const key = await this.getKey(headers.kid); const payload = JWT.verify(jwt, key); - if ( - this.options.issuer !== '' && - headers.issuer !== this.options.issuer - ) { + if (this.options.issuer && headers.iss !== this.options.issuer) { throw new Error('issuer mismatch on JWT'); } @@ -116,7 +113,7 @@ export const createAwsAlbProvider = ({ identityResolver, }: AuthProviderFactoryOptions) => { const region = config.getString('region'); - const issuer = config.getString('iss'); + const issuer = config.getOptionalString('iss'); if (identityResolver !== undefined) { return new AwsAlbAuthProvider(logger, catalogApi, { region, From 4eaa060572b7c5285dfa426c201fc03e3760a347 Mon Sep 17 00:00:00 2001 From: Jonah Back Date: Tue, 26 Jan 2021 10:54:14 -0800 Subject: [PATCH 053/131] Fix test, add changeset for ALB provider bug fix --- .changeset/stale-tools-confess.md | 5 +++++ plugins/auth-backend/src/providers/aws-alb/provider.test.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/stale-tools-confess.md diff --git a/.changeset/stale-tools-confess.md b/.changeset/stale-tools-confess.md new file mode 100644 index 0000000000..f5e6a6da02 --- /dev/null +++ b/.changeset/stale-tools-confess.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Fix issuer check diff --git a/plugins/auth-backend/src/providers/aws-alb/provider.test.ts b/plugins/auth-backend/src/providers/aws-alb/provider.test.ts index bae809971e..d05b6bbfaf 100644 --- a/plugins/auth-backend/src/providers/aws-alb/provider.test.ts +++ b/plugins/auth-backend/src/providers/aws-alb/provider.test.ts @@ -80,7 +80,7 @@ describe('AwsALBAuthProvider', () => { const mockResponseSend = jest.fn(); const mockRequest = ({ header: jest.fn(() => { - return 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImZvbyIsImlzc3VlciI6ImZvbyJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.zUkMYAuMwC1T0tyHMpxXrkbFDa4aGhB8d9um_tI2hsI'; + return 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImZvbyIsImlzcyI6ImZvbyJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.T2BNS4G-6RoiFnXc8Q8TiwdWzTpNitY8jcsGM3N3-Yo'; }), } as unknown) as express.Request; const mockRequestWithoutJwt = ({ From 5ad2d8b3be154037538244b61030d3230962bdfc Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 26 Jan 2021 20:14:27 +0100 Subject: [PATCH 054/131] chore: updating config for tugboat updating --- .tugboat/config.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.tugboat/config.yml b/.tugboat/config.yml index 20f716785c..065cc4d526 100644 --- a/.tugboat/config.yml +++ b/.tugboat/config.yml @@ -8,5 +8,9 @@ services: - yarn install - yarn tsc - yarn build + update: + - yarn install + - yarn tsc + - yarn build start: - yarn start-backend & From 99e53017d03b00fa1d10e4eabdb0e2e55df1df95 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 26 Jan 2021 20:19:30 +0100 Subject: [PATCH 055/131] chore: reworking some more steps to see if this is how you build --- .tugboat/config.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.tugboat/config.yml b/.tugboat/config.yml index 065cc4d526..4e393f2a6d 100644 --- a/.tugboat/config.yml +++ b/.tugboat/config.yml @@ -4,13 +4,13 @@ services: expose: 7000 default: true commands: - build: + init: - yarn install + build: - yarn tsc - yarn build update: - yarn install - - yarn tsc - - yarn build + - env start: - - yarn start-backend & + - node packages/backend --config app-config.yaml --config .tugboat/tugboat.app-config.production.yaml & From da8b9ef1d8d2c961e1a50cc181028f24c96499c0 Mon Sep 17 00:00:00 2001 From: Jonah Back Date: Tue, 26 Jan 2021 11:41:41 -0800 Subject: [PATCH 056/131] add fields to config type for aws-alb provider --- plugins/auth-backend/config.d.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugins/auth-backend/config.d.ts b/plugins/auth-backend/config.d.ts index c748090711..84f71b52b9 100644 --- a/plugins/auth-backend/config.d.ts +++ b/plugins/auth-backend/config.d.ts @@ -72,6 +72,10 @@ export interface Config { onelogin?: { development: { [key: string]: string }; }; + awsalb?: { + issuer?: string; + region: string; + }; }; }; } From 59f9a5baad6294496af40b361528f5b98660da41 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 26 Jan 2021 20:44:57 +0100 Subject: [PATCH 057/131] feat: add in the tugboat config --- .tugboat/tugboat.app-config.production.yaml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .tugboat/tugboat.app-config.production.yaml diff --git a/.tugboat/tugboat.app-config.production.yaml b/.tugboat/tugboat.app-config.production.yaml new file mode 100644 index 0000000000..f606574d89 --- /dev/null +++ b/.tugboat/tugboat.app-config.production.yaml @@ -0,0 +1,13 @@ +app: + title: Backstage Tugboat Preview + baseUrl: + $env: TUGBOAT_DEFAULT_SERVICE_URL + +backend: + baseUrl: + $env: TUGBOAT_DEFAULT_SERVICE_URL + cors: + origin: + $env: TUGBOAT_DEFAULT_SERVICE_URL + methods: [GET, POST, PUT, DELETE] + credentials: true From e877faa6eb5595605e5524f11168ce3926063099 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 26 Jan 2021 20:53:23 +0100 Subject: [PATCH 058/131] chore: fixing config --- .tugboat/config.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.tugboat/config.yml b/.tugboat/config.yml index 4e393f2a6d..aaca7f6d86 100644 --- a/.tugboat/config.yml +++ b/.tugboat/config.yml @@ -11,6 +11,7 @@ services: - yarn build update: - yarn install - - env + - yarn tsc + - yarn build start: - node packages/backend --config app-config.yaml --config .tugboat/tugboat.app-config.production.yaml & From e20b984a374ecef1de0dd3349f77c5bd67f7a322 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 26 Jan 2021 21:37:49 +0100 Subject: [PATCH 059/131] chore: updating base image --- .tugboat/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.tugboat/config.yml b/.tugboat/config.yml index aaca7f6d86..0c8c0329e3 100644 --- a/.tugboat/config.yml +++ b/.tugboat/config.yml @@ -1,6 +1,6 @@ services: backstage: - image: tugboatqa/node:lts + image: node:lts-alpine expose: 7000 default: true commands: From f50d284fb8373cf59f0de59d40e9c1b99089e7e1 Mon Sep 17 00:00:00 2001 From: Jonah Back Date: Tue, 26 Jan 2021 13:27:59 -0800 Subject: [PATCH 060/131] Update .changeset/stale-tools-confess.md Co-authored-by: Patrik Oldsberg --- .changeset/stale-tools-confess.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/stale-tools-confess.md b/.changeset/stale-tools-confess.md index f5e6a6da02..10f2203010 100644 --- a/.changeset/stale-tools-confess.md +++ b/.changeset/stale-tools-confess.md @@ -2,4 +2,4 @@ '@backstage/plugin-auth-backend': patch --- -Fix issuer check +Fix AWS ALB issuer check From a7c0da02e8d8515e47880e12d6bbc73bbdb966f8 Mon Sep 17 00:00:00 2001 From: Alan Crosswell Date: Sun, 17 Jan 2021 18:16:19 -0500 Subject: [PATCH 061/131] pass in the specified git branch ref when cloning a single branch --- packages/backend-common/src/scm/git.ts | 3 ++- .../scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/backend-common/src/scm/git.ts b/packages/backend-common/src/scm/git.ts index b0a1df6541..55a8746d6e 100644 --- a/packages/backend-common/src/scm/git.ts +++ b/packages/backend-common/src/scm/git.ts @@ -86,13 +86,14 @@ export class Git { return git.commit({ fs, dir, message, author, committer }); } - async clone({ url, dir }: { url: string; dir: string }): Promise { + async clone({ url, dir, ref }: { url: string; dir: string; ref?: string }): Promise { this.config.logger?.info(`Cloning repo {dir=${dir},url=${url}}`); return git.clone({ fs, http, url, dir, + ref, singleBranch: true, depth: 1, onProgress: this.onProgressHandler(), diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts index 1fca2ff0f9..7824687155 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts @@ -42,6 +42,7 @@ export class GitlabPreparer implements PreparerBase { const parsedGitLocation = parseGitUrl(location); const repositoryCheckoutUrl = parsedGitLocation.toString('https'); + const ref = parsedGitLocation.toString('ref'); const tempDir = await fs.promises.mkdtemp( path.join(workingDirectory, templateId), ); @@ -61,6 +62,7 @@ export class GitlabPreparer implements PreparerBase { await git.clone({ url: repositoryCheckoutUrl, + ref: ref, dir: tempDir, }); From 97df886e1c2ca1e2ba55bf1e6f04c2d4e8c16192 Mon Sep 17 00:00:00 2001 From: Alan Crosswell Date: Tue, 19 Jan 2021 10:32:15 -0500 Subject: [PATCH 062/131] KISS: no need for toString() --- .../scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts index 7824687155..0ce5466915 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts @@ -42,7 +42,7 @@ export class GitlabPreparer implements PreparerBase { const parsedGitLocation = parseGitUrl(location); const repositoryCheckoutUrl = parsedGitLocation.toString('https'); - const ref = parsedGitLocation.toString('ref'); + const ref = parsedGitLocation.ref; const tempDir = await fs.promises.mkdtemp( path.join(workingDirectory, templateId), ); From 26a3a6cf030d3d7e926a0b23d2adb5ef9cd4e2a6 Mon Sep 17 00:00:00 2001 From: Alan Crosswell Date: Thu, 21 Jan 2021 15:37:49 -0500 Subject: [PATCH 063/131] bump patch-level versions --- .changeset/breezy-meals-lie.md | 6 ++++++ packages/backend-common/package.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 .changeset/breezy-meals-lie.md diff --git a/.changeset/breezy-meals-lie.md b/.changeset/breezy-meals-lie.md new file mode 100644 index 0000000000..d9bc3f3d22 --- /dev/null +++ b/.changeset/breezy-meals-lie.md @@ -0,0 +1,6 @@ +--- +'@backstage/backend-common': patch +'@backstage/plugin-scaffolder-backend': patch +--- + +include ref when cloning from GitLab diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index c698351615..350b7bd8f8 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.5.0", + "version": "0.5.1", "main": "src/index.ts", "types": "src/index.ts", "private": false, From 85cdcb2c6b6fb9ccfa85fef2945c0bafbb827a3f Mon Sep 17 00:00:00 2001 From: Alan Crosswell Date: Thu, 21 Jan 2021 15:47:56 -0500 Subject: [PATCH 064/131] Prettier --- packages/backend-common/src/scm/git.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/backend-common/src/scm/git.ts b/packages/backend-common/src/scm/git.ts index 55a8746d6e..5b24d23b99 100644 --- a/packages/backend-common/src/scm/git.ts +++ b/packages/backend-common/src/scm/git.ts @@ -86,7 +86,15 @@ export class Git { return git.commit({ fs, dir, message, author, committer }); } - async clone({ url, dir, ref }: { url: string; dir: string; ref?: string }): Promise { + async clone({ + url, + dir, + ref, + }: { + url: string; + dir: string; + ref?: string; + }): Promise { this.config.logger?.info(`Cloning repo {dir=${dir},url=${url}}`); return git.clone({ fs, From c7de259c929c6feab8cf1fef1c76e7b3b8e6f5d4 Mon Sep 17 00:00:00 2001 From: Alan Crosswell Date: Thu, 21 Jan 2021 16:14:09 -0500 Subject: [PATCH 065/131] add ref parameter to clone --- .../src/scaffolder/stages/prepare/gitlab.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts index 19cbf793e4..8ea1d77859 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts @@ -89,6 +89,7 @@ describe('GitLabPreparer', () => { expect(mockGitClient.clone).toHaveBeenCalledWith({ url: 'https://gitlab.com/benjdlambert/backstage-graphql-template', dir: expect.any(String), + ref: expect.any(String), }); }); @@ -113,6 +114,7 @@ describe('GitLabPreparer', () => { expect(mockGitClient.clone).toHaveBeenCalledWith({ url: 'https://gitlab.com/benjdlambert/backstage-graphql-template', dir: expect.any(String), + ref: expect.any(String), }); }); From 9ba95e5085a8ee5b76fdc01387ea74223d11b23b Mon Sep 17 00:00:00 2001 From: Alan Crosswell Date: Thu, 21 Jan 2021 16:40:21 -0500 Subject: [PATCH 066/131] undo manual package version bump done in error --- packages/backend-common/package.json | 2 +- plugins/scaffolder-backend/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index 350b7bd8f8..c698351615 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.5.1", + "version": "0.5.0", "main": "src/index.ts", "types": "src/index.ts", "private": false, diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index cd7dab3444..914b151eeb 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend", - "version": "0.5.1", + "version": "0.5.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", From ea21f9c12b083af45b29a88c35ce3dc4cc115fe9 Mon Sep 17 00:00:00 2001 From: Alan Crosswell Date: Fri, 22 Jan 2021 13:18:21 -0500 Subject: [PATCH 067/131] pass in the specified git branch ref when cloning a single branch --- .../src/scaffolder/stages/prepare/github.test.ts | 2 ++ .../scaffolder-backend/src/scaffolder/stages/prepare/github.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts index b43545f5fc..4051e5d30d 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts @@ -89,6 +89,7 @@ describe('GitHubPreparer', () => { expect(mockGitClient.clone).toHaveBeenCalledWith({ url: 'https://github.com/benjdlambert/backstage-graphql-template', dir: expect.any(String), + ref: expect.any(String), }); }); @@ -100,6 +101,7 @@ describe('GitHubPreparer', () => { expect(mockGitClient.clone).toHaveBeenCalledWith({ url: 'https://github.com/benjdlambert/backstage-graphql-template', dir: expect.any(String), + ref: expect.any(String), }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts index cfeff454e7..02cbb1c5cd 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts @@ -42,6 +42,7 @@ export class GithubPreparer implements PreparerBase { const parsedGitLocation = parseGitUrl(location); const repositoryCheckoutUrl = parsedGitLocation.toString('https'); + const ref = parsedGitLocation.ref; const tempDir = await fs.promises.mkdtemp( path.join(workingDirectory, templateId), ); @@ -63,6 +64,7 @@ export class GithubPreparer implements PreparerBase { await git.clone({ url: repositoryCheckoutUrl, + ref: ref, dir: tempDir, }); From 00d9d6ae454f61ff16335b65265ffb5ae331b7db Mon Sep 17 00:00:00 2001 From: Alan Crosswell Date: Fri, 22 Jan 2021 15:01:01 -0500 Subject: [PATCH 068/131] Add missing ref fix that was applied to github and gitlab. --- .../src/scaffolder/stages/prepare/bitbucket.test.ts | 2 ++ .../src/scaffolder/stages/prepare/bitbucket.ts | 9 +++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.test.ts index ff74fffdd7..32822ae289 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.test.ts @@ -89,6 +89,7 @@ describe('BitbucketPreparer', () => { expect(mockGitClient.clone).toHaveBeenCalledWith({ url: 'https://bitbucket.org/backstage-project/backstage-repo', dir: expect.any(String), + ref: expect.any(String), }); }); @@ -113,6 +114,7 @@ describe('BitbucketPreparer', () => { expect(mockGitClient.clone).toHaveBeenCalledWith({ url: 'https://bitbucket.org/backstage-project/backstage-repo', dir: expect.any(String), + ref: expect.any(String), }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts index 5020a5f658..04f48e3005 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts @@ -49,15 +49,15 @@ export class BitbucketPreparer implements PreparerBase { const logger = opts.logger; const templateId = template.metadata.name; - const repo = parseGitUrl(location); - const repositoryCheckoutUrl = repo.toString('https'); - + const parsedGitLocation = parseGitUrl(location); + const repositoryCheckoutUrl = parsedGitLocation.toString('https'); + const ref = parsedGitLocation.ref; const tempDir = await fs.promises.mkdtemp( path.join(workingDirectory, templateId), ); const templateDirectory = path.join( - `${path.dirname(repo.filepath)}`, + `${path.dirname(parsedGitLocation.filepath)}`, template.spec.path ?? '.', ); @@ -73,6 +73,7 @@ export class BitbucketPreparer implements PreparerBase { await git.clone({ url: repositoryCheckoutUrl, + ref: ref, dir: tempDir, }); From b34d2514d716c010d4ea0bcb8d9ade2e19677f57 Mon Sep 17 00:00:00 2001 From: Alan Crosswell Date: Fri, 22 Jan 2021 15:29:20 -0500 Subject: [PATCH 069/131] improve the changset description --- .changeset/breezy-meals-lie.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.changeset/breezy-meals-lie.md b/.changeset/breezy-meals-lie.md index d9bc3f3d22..cbdd876614 100644 --- a/.changeset/breezy-meals-lie.md +++ b/.changeset/breezy-meals-lie.md @@ -3,4 +3,16 @@ '@backstage/plugin-scaffolder-backend': patch --- -include ref when cloning from GitLab +Honor the branch ref in the url when cloning. + +This fixes a bug in the scaffolder prepare stage where a non-default branch +was specified in the scaffolder URL but the default branch was cloned. +For example, even though the `other` branch is specified in this example, the +`master` branch was actually cloned: + +```yaml +catalog: + locations: + - type: url + target: https://github.com/backstage/backstage/blob/other/plugins/scaffolder-backend/sample-templates/docs-template/template.yaml +``` From c05baec7fdcd599bc72d7d8023cbbbede8634d9c Mon Sep 17 00:00:00 2001 From: Alan Crosswell Date: Sat, 23 Jan 2021 14:12:18 -0500 Subject: [PATCH 070/131] Fix gitlab prepare fails with a 404. A GitLab URL not ending in .git requires a redirect which isomorphic-git doesn't follow, resulting in a 404. This incorporates @ruloweb's fix in #4134. --- .changeset/breezy-meals-lie.md | 2 ++ .../src/scaffolder/stages/prepare/gitlab.test.ts | 4 ++-- .../src/scaffolder/stages/prepare/gitlab.ts | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.changeset/breezy-meals-lie.md b/.changeset/breezy-meals-lie.md index cbdd876614..4ca4f1ac35 100644 --- a/.changeset/breezy-meals-lie.md +++ b/.changeset/breezy-meals-lie.md @@ -16,3 +16,5 @@ catalog: - type: url target: https://github.com/backstage/backstage/blob/other/plugins/scaffolder-backend/sample-templates/docs-template/template.yaml ``` + +This also fixes a 404 in the prepare stage for GitLab URLs. diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts index 8ea1d77859..0b77fd813b 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts @@ -87,7 +87,7 @@ describe('GitLabPreparer', () => { await preparer.prepare(mockEntity, { logger: getVoidLogger() }); expect(mockGitClient.clone).toHaveBeenCalledWith({ - url: 'https://gitlab.com/benjdlambert/backstage-graphql-template', + url: 'https://gitlab.com/benjdlambert/backstage-graphql-template.git', dir: expect.any(String), ref: expect.any(String), }); @@ -112,7 +112,7 @@ describe('GitLabPreparer', () => { await preparer.prepare(mockEntity, { logger: getVoidLogger() }); expect(mockGitClient.clone).toHaveBeenCalledWith({ - url: 'https://gitlab.com/benjdlambert/backstage-graphql-template', + url: 'https://gitlab.com/benjdlambert/backstage-graphql-template.git', dir: expect.any(String), ref: expect.any(String), }); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts index 0ce5466915..4368c1c242 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts @@ -41,6 +41,7 @@ export class GitlabPreparer implements PreparerBase { const templateId = template.metadata.name; const parsedGitLocation = parseGitUrl(location); + parsedGitLocation.git_suffix = true; const repositoryCheckoutUrl = parsedGitLocation.toString('https'); const ref = parsedGitLocation.ref; const tempDir = await fs.promises.mkdtemp( From 119c6386fda3e4262dadf60355749f4ceadbd4d5 Mon Sep 17 00:00:00 2001 From: Alan Crosswell Date: Sat, 23 Jan 2021 17:21:39 -0500 Subject: [PATCH 071/131] Revert "undo manual package version bump done in error" This reverts commit 44c5ec6feb5109ae97c1a66b885da3c6440de0d5. --- packages/backend-common/package.json | 2 +- plugins/scaffolder-backend/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index c698351615..350b7bd8f8 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.5.0", + "version": "0.5.1", "main": "src/index.ts", "types": "src/index.ts", "private": false, diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 914b151eeb..cd7dab3444 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend", - "version": "0.5.0", + "version": "0.5.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", From aae1dca6c6f4da6011c0999fa4e0743fa6838468 Mon Sep 17 00:00:00 2001 From: Alan Crosswell Date: Sat, 23 Jan 2021 17:23:47 -0500 Subject: [PATCH 072/131] Revert "bump patch-level versions" This reverts commit 96c99d75ae6523c049e6f51a3f72e7d47591a324. --- packages/backend-common/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index 350b7bd8f8..c698351615 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.5.1", + "version": "0.5.0", "main": "src/index.ts", "types": "src/index.ts", "private": false, From ea59a2b363b32fe42d1d096608ceea90dc827c0e Mon Sep 17 00:00:00 2001 From: Alan Crosswell Date: Sat, 23 Jan 2021 17:42:26 -0500 Subject: [PATCH 073/131] Azure branch support --- .../src/scaffolder/stages/prepare/azure.test.ts | 4 +++- .../scaffolder-backend/src/scaffolder/stages/prepare/azure.ts | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.test.ts index 8dd72c35e9..89ca8e2f8f 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.test.ts @@ -45,7 +45,7 @@ describe('AzurePreparer', () => { metadata: { annotations: { [LOCATION_ANNOTATION]: - 'url:https://dev.azure.com/backstage-org/backstage-project/_git/template-repo?path=%2Ftemplate.yaml', + 'url:https://dev.azure.com/backstage-org/backstage-project/_git/template-repo?path=%2Ftemplate.yaml&version=GBmaster', }, name: 'graphql-starter', title: 'GraphQL Service', @@ -112,6 +112,7 @@ describe('AzurePreparer', () => { url: 'https://dev.azure.com/backstage-org/backstage-project/_git/template-repo', dir: expect.any(String), + ref: 'master', }); }); @@ -124,6 +125,7 @@ describe('AzurePreparer', () => { url: 'https://dev.azure.com/backstage-org/backstage-project/_git/template-repo', dir: expect.any(String), + ref: 'master', }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts index 8b204fa799..67f089965d 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts @@ -42,6 +42,7 @@ export class AzurePreparer implements PreparerBase { const parsedGitLocation = parseGitUrl(location); const repositoryCheckoutUrl = parsedGitLocation.toString('https'); + const ref = parsedGitLocation.ref; const tempDir = await fs.promises.mkdtemp( path.join(workingDirectory, templateId), ); @@ -63,6 +64,7 @@ export class AzurePreparer implements PreparerBase { await git.clone({ url: repositoryCheckoutUrl, + ref: ref, dir: tempDir, }); From 6ddfdca12364838218ce8b7a1e479ca63af95a04 Mon Sep 17 00:00:00 2001 From: Jonah Back Date: Tue, 26 Jan 2021 16:54:52 -0800 Subject: [PATCH 074/131] fix aws alb auth provider key caching --- plugins/auth-backend/src/providers/aws-alb/provider.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/auth-backend/src/providers/aws-alb/provider.ts b/plugins/auth-backend/src/providers/aws-alb/provider.ts index a5db2869b5..61ea10947e 100644 --- a/plugins/auth-backend/src/providers/aws-alb/provider.ts +++ b/plugins/auth-backend/src/providers/aws-alb/provider.ts @@ -95,13 +95,13 @@ export class AwsAlbAuthProvider implements AuthProviderRouteHandlers { async getKey(keyId: string): Promise { const optionalCacheKey = this.keyCache.get(keyId); if (optionalCacheKey) { - return optionalCacheKey; + return crypto.createPublicKey(optionalCacheKey); } const keyText: string = await fetch( `https://public-keys.auth.elb.${this.options.region}.amazonaws.com/${keyId}`, ).then(response => response.text()); const keyValue = crypto.createPublicKey(keyText); - this.keyCache.set(keyId, keyValue); + this.keyCache.set(keyId, keyValue.export({ format: 'pem', type: 'spki' })); return keyValue; } } From d7b1d317f623734790ddcf7e333c12753d43d97d Mon Sep 17 00:00:00 2001 From: Jonah Back Date: Tue, 26 Jan 2021 16:59:40 -0800 Subject: [PATCH 075/131] Add changeset for alb aws provider fix --- .changeset/fuzzy-points-whisper.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fuzzy-points-whisper.md diff --git a/.changeset/fuzzy-points-whisper.md b/.changeset/fuzzy-points-whisper.md new file mode 100644 index 0000000000..d1af2b2dcf --- /dev/null +++ b/.changeset/fuzzy-points-whisper.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Fixed serialization issue with caching of public keys in AWS ALB auth provider From adee7b3482b1d4d496ac6f59c6b53f76ccfb0c4e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 27 Jan 2021 05:01:17 +0000 Subject: [PATCH 076/131] chore(deps-dev): bump @storybook/react from 6.1.14 to 6.1.15 Bumps [@storybook/react](https://github.com/storybookjs/storybook/tree/HEAD/app/react) from 6.1.14 to 6.1.15. - [Release notes](https://github.com/storybookjs/storybook/releases) - [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md) - [Commits](https://github.com/storybookjs/storybook/commits/v6.1.15/app/react) Signed-off-by: dependabot[bot] --- yarn.lock | 211 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 109 insertions(+), 102 deletions(-) diff --git a/yarn.lock b/yarn.lock index 7c743bc5b3..36a133fd37 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5301,17 +5301,17 @@ global "^4.3.2" regenerator-runtime "^0.13.7" -"@storybook/addons@6.1.14", "@storybook/addons@^6.1.11": - version "6.1.14" - resolved "https://registry.npmjs.org/@storybook/addons/-/addons-6.1.14.tgz#2b81304bbe696923df95cdcf85cfc592d10f4065" - integrity sha512-HlpmV7aejp/MeW8bo/WKME3i71gi0men9qcwoovjDjnSF6jXoNLT336a5udKXdHqYSZgzdyURlgLtilCWkWaJQ== +"@storybook/addons@6.1.15", "@storybook/addons@^6.1.11": + version "6.1.15" + resolved "https://registry.npmjs.org/@storybook/addons/-/addons-6.1.15.tgz#09eb8d962f58bd20b4ac2f83b515831c83226352" + integrity sha512-ENyHapLFOG93VaoQXPX8O3IWjLRyVBox9C9P20LMruKX/SfXAXx20qsoAWKKPGssopyOin17aoQX9pj+lFmCZQ== dependencies: - "@storybook/api" "6.1.14" - "@storybook/channels" "6.1.14" - "@storybook/client-logger" "6.1.14" - "@storybook/core-events" "6.1.14" - "@storybook/router" "6.1.14" - "@storybook/theming" "6.1.14" + "@storybook/api" "6.1.15" + "@storybook/channels" "6.1.15" + "@storybook/client-logger" "6.1.15" + "@storybook/core-events" "6.1.15" + "@storybook/router" "6.1.15" + "@storybook/theming" "6.1.15" core-js "^3.0.1" global "^4.3.2" regenerator-runtime "^0.13.7" @@ -5341,20 +5341,20 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" -"@storybook/api@6.1.14": - version "6.1.14" - resolved "https://registry.npmjs.org/@storybook/api/-/api-6.1.14.tgz#20035dd336aba1c5a0f8c83c8c14a2edaf4db891" - integrity sha512-gWcC/xEW8HL5DsocLujHBUdoNsl4YW1Zx1Y4SBbLCyrhj8v4JudJpylwJpOUBDe/GESXq1zqvNKvUPtI8DQNyw== +"@storybook/api@6.1.15": + version "6.1.15" + resolved "https://registry.npmjs.org/@storybook/api/-/api-6.1.15.tgz#285ba42f7a8efcd3bd0e586a5e978487d826fbb4" + integrity sha512-C4D08e2ZbSe62nNKtmh9YBraoWb2j6Chw8VCkuj91kuKHh3YDNc1gjj5Fi+KYZwIcy0EllzW3RFQs+YR1/Vg1g== dependencies: "@reach/router" "^1.3.3" - "@storybook/channels" "6.1.14" - "@storybook/client-logger" "6.1.14" - "@storybook/core-events" "6.1.14" + "@storybook/channels" "6.1.15" + "@storybook/client-logger" "6.1.15" + "@storybook/core-events" "6.1.15" "@storybook/csf" "0.0.1" - "@storybook/router" "6.1.14" + "@storybook/router" "6.1.15" "@storybook/semver" "^7.3.2" - "@storybook/theming" "6.1.14" - "@types/reach__router" "^1.3.5" + "@storybook/theming" "6.1.15" + "@types/reach__router" "^1.3.7" core-js "^3.0.1" fast-deep-equal "^3.1.1" global "^4.3.2" @@ -5379,14 +5379,14 @@ qs "^6.6.0" telejson "^5.0.2" -"@storybook/channel-postmessage@6.1.14": - version "6.1.14" - resolved "https://registry.npmjs.org/@storybook/channel-postmessage/-/channel-postmessage-6.1.14.tgz#41f3115895010dad9fb30f4ac381e4f904b1e50c" - integrity sha512-If83dXXW9mKIRuvuWhWa/zkEw/F0FDgikp33x8436J3rWCh3recp27kffFRrKG0YDMpFSk/Ci5G47E9zn9SCjw== +"@storybook/channel-postmessage@6.1.15": + version "6.1.15" + resolved "https://registry.npmjs.org/@storybook/channel-postmessage/-/channel-postmessage-6.1.15.tgz#80ea2346d18496f9710dd7f87fd2a9eca46ef36f" + integrity sha512-Es4B5zpLrW28KSbY8FhGVEDgUnKspJ7wPuJyKExUpZ5L9w52RkTD6lRnVPzLUfoQ4luPsExy5fiuo878/Wc9ag== dependencies: - "@storybook/channels" "6.1.14" - "@storybook/client-logger" "6.1.14" - "@storybook/core-events" "6.1.14" + "@storybook/channels" "6.1.15" + "@storybook/client-logger" "6.1.15" + "@storybook/core-events" "6.1.15" core-js "^3.0.1" global "^4.3.2" qs "^6.6.0" @@ -5401,10 +5401,10 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" -"@storybook/channels@6.1.14": - version "6.1.14" - resolved "https://registry.npmjs.org/@storybook/channels/-/channels-6.1.14.tgz#c479190ebb853a603f3ed90fc470534a02eb46eb" - integrity sha512-vP19IB2FXj8SiFbQ9ETljEBienL+KRMLgMzz3Ta3nZj/OfjJJbIuj42ZfexQGV4mS0Bo+OW+qT7VMIY6fulnFw== +"@storybook/channels@6.1.15": + version "6.1.15" + resolved "https://registry.npmjs.org/@storybook/channels/-/channels-6.1.15.tgz#22bb06a671a5ae09d2537bcf63aaf90d7f6b9f6b" + integrity sha512-HIKHDeL/0BDk9a7xc2PLiFFoHjUMKUd2djhUGdeKgdKqoWejp4JJ60fI68+2QuSRbkB8k+rAwmuWJzV7EfB5fg== dependencies: core-js "^3.0.1" ts-dedent "^2.0.0" @@ -5434,16 +5434,16 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" -"@storybook/client-api@6.1.14": - version "6.1.14" - resolved "https://registry.npmjs.org/@storybook/client-api/-/client-api-6.1.14.tgz#6daf56743cc72e13f05fff3d2ac554897cc9f9fd" - integrity sha512-pIDSlS48bhJdtgNg7sXV1NmLJtB0ebRHJI9htIiqtL7EGQenb4+Bbwflhj1j51OEkuM+bQsAAZxq5deiUQEGVw== +"@storybook/client-api@6.1.15": + version "6.1.15" + resolved "https://registry.npmjs.org/@storybook/client-api/-/client-api-6.1.15.tgz#8f8ead111459b94621571bdb2276f8a0aace17b1" + integrity sha512-iwuDlgNdB6Y4OidlhWPob3tEIax9taymdKEe9by4rLJ3nfXu7viHcvCAjN24oI4NFW3NZsmtqJotgftRYk0r1Q== dependencies: - "@storybook/addons" "6.1.14" - "@storybook/channel-postmessage" "6.1.14" - "@storybook/channels" "6.1.14" - "@storybook/client-logger" "6.1.14" - "@storybook/core-events" "6.1.14" + "@storybook/addons" "6.1.15" + "@storybook/channel-postmessage" "6.1.15" + "@storybook/channels" "6.1.15" + "@storybook/client-logger" "6.1.15" + "@storybook/core-events" "6.1.15" "@storybook/csf" "0.0.1" "@types/qs" "^6.9.0" "@types/webpack-env" "^1.15.3" @@ -5466,10 +5466,10 @@ core-js "^3.0.1" global "^4.3.2" -"@storybook/client-logger@6.1.14": - version "6.1.14" - resolved "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-6.1.14.tgz#216b9c1332ffa3a3473dad837780a3b14f686bae" - integrity sha512-NSO8nVsp6o0eoQ1Drlu66KXpl6DPuq02Kj8AhttGzvqSYB50SV4CV+wceBcg77tIVu5QmQ+71hAEVXhx7sjRHA== +"@storybook/client-logger@6.1.15": + version "6.1.15" + resolved "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-6.1.15.tgz#b558d6ecbee82c038d684717d8c598eaa4a9324d" + integrity sha512-lUpatG8SxzrUapWMsIPWiR+5qRVT5ebn8tGHQeBeRHXbdmEqyq5DOlrotLUemkA5nNTCs1pMFNvKSpCHznG+fg== dependencies: core-js "^3.0.1" global "^4.3.2" @@ -5500,15 +5500,15 @@ react-textarea-autosize "^8.1.1" ts-dedent "^2.0.0" -"@storybook/components@6.1.14": - version "6.1.14" - resolved "https://registry.npmjs.org/@storybook/components/-/components-6.1.14.tgz#4ea47edfa0a3e4a26882aa5a1eb90c1ec86e6f71" - integrity sha512-Nxsp/9o1tqfY8s6RBWNHyM03A5D9k56Kr/4VNa++CbDrz1+TIxpYlDgS4sllUlXyvICLfk3sUtg3KS5CPl2iZA== +"@storybook/components@6.1.15": + version "6.1.15" + resolved "https://registry.npmjs.org/@storybook/components/-/components-6.1.15.tgz#b4a2af23ee6b9cba4c255191eae3d3463e29bfb7" + integrity sha512-lPbA/zyBfctdlpDhRTcRFLWlZPJ3PB4+wI0FUvYs69iG3/bNbQPYu8vRmNhCZOsaGt+b+dik4Tfcth8Bu+eQug== dependencies: "@popperjs/core" "^2.5.4" - "@storybook/client-logger" "6.1.14" + "@storybook/client-logger" "6.1.15" "@storybook/csf" "0.0.1" - "@storybook/theming" "6.1.14" + "@storybook/theming" "6.1.15" "@types/overlayscrollbars" "^1.9.0" "@types/react-color" "^3.0.1" "@types/react-syntax-highlighter" "11.0.4" @@ -5533,17 +5533,17 @@ dependencies: core-js "^3.0.1" -"@storybook/core-events@6.1.14": - version "6.1.14" - resolved "https://registry.npmjs.org/@storybook/core-events/-/core-events-6.1.14.tgz#a3165e32cefd6be7326bbad4b8140653bdfa0426" - integrity sha512-tpM3VDvzqgRY7S17CRglgt1625rxNoyEwrLQiNcZkUPyO0rpaacPqVEbPCtcTmUeboI1bLdnSQIjT9B0/Y2Pww== +"@storybook/core-events@6.1.15": + version "6.1.15" + resolved "https://registry.npmjs.org/@storybook/core-events/-/core-events-6.1.15.tgz#f66e30cbed8afdb8df2254d2aa47fe139e641c60" + integrity sha512-2sz02hdGZshanoq83jaB+goAcapVEWrxe+RJZn/gu2OymlEioWNjPPtOVGgi5DNIiJFnYvc66adayNwX39+tDA== dependencies: core-js "^3.0.1" -"@storybook/core@6.1.14": - version "6.1.14" - resolved "https://registry.npmjs.org/@storybook/core/-/core-6.1.14.tgz#17e724a5b94d6e1bb557e213b8176660d2d14762" - integrity sha512-lHKZmfLAo2VGtF/yrZkkWMYgmFRNKbzIDxYJGp8USyUQyTfEpz2qqJlBdoD6rxr1hFPM2954tIKwh8iPhT2PFQ== +"@storybook/core@6.1.15": + version "6.1.15" + resolved "https://registry.npmjs.org/@storybook/core/-/core-6.1.15.tgz#7ff8c314d3857497bf2e26c69a1fa93ef37301aa" + integrity sha512-mQeKAXcowUwF+pOdWZEFwb5M6sz4yv5cOv1vTci3/1pMmB8QpYlH+P61p4lsRO17Vlak70h18TworPka/4+mhA== dependencies: "@babel/core" "^7.12.3" "@babel/plugin-proposal-class-properties" "^7.12.1" @@ -5567,20 +5567,20 @@ "@babel/preset-react" "^7.12.1" "@babel/preset-typescript" "^7.12.1" "@babel/register" "^7.12.1" - "@storybook/addons" "6.1.14" - "@storybook/api" "6.1.14" - "@storybook/channel-postmessage" "6.1.14" - "@storybook/channels" "6.1.14" - "@storybook/client-api" "6.1.14" - "@storybook/client-logger" "6.1.14" - "@storybook/components" "6.1.14" - "@storybook/core-events" "6.1.14" + "@storybook/addons" "6.1.15" + "@storybook/api" "6.1.15" + "@storybook/channel-postmessage" "6.1.15" + "@storybook/channels" "6.1.15" + "@storybook/client-api" "6.1.15" + "@storybook/client-logger" "6.1.15" + "@storybook/components" "6.1.15" + "@storybook/core-events" "6.1.15" "@storybook/csf" "0.0.1" - "@storybook/node-logger" "6.1.14" - "@storybook/router" "6.1.14" + "@storybook/node-logger" "6.1.15" + "@storybook/router" "6.1.15" "@storybook/semver" "^7.3.2" - "@storybook/theming" "6.1.14" - "@storybook/ui" "6.1.14" + "@storybook/theming" "6.1.15" + "@storybook/ui" "6.1.15" "@types/glob-base" "^0.3.0" "@types/micromatch" "^4.0.1" "@types/node-fetch" "^2.5.4" @@ -5654,10 +5654,10 @@ dependencies: lodash "^4.17.15" -"@storybook/node-logger@6.1.14": - version "6.1.14" - resolved "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-6.1.14.tgz#e5294f986e3ec5c67b2738895b9d16c9a2b667fa" - integrity sha512-3jrw7coAwFXZu4qK1vm54bCPhNRvxjG+7jISbhhocDoNIv0nLWL3+tJyrC5/k/XHQiUlLkhEzpMaASADmkttNw== +"@storybook/node-logger@6.1.15": + version "6.1.15" + resolved "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-6.1.15.tgz#fcf786d3a323feb6821e40e26f98a513a60d1a79" + integrity sha512-lrO0ei3W7BRci2iUkWTr/rXgHkzxwZTrlkx0iBzbQQRy7K1AJ9bjzhurCH9B8C9XGLmn60LXT81RWD3iCLZjcw== dependencies: "@types/npmlog" "^4.1.2" chalk "^4.0.0" @@ -5666,16 +5666,16 @@ pretty-hrtime "^1.0.3" "@storybook/react@^6.1.11": - version "6.1.14" - resolved "https://registry.npmjs.org/@storybook/react/-/react-6.1.14.tgz#436e9b90096b1d7c83f7f073b5baf47212b2e425" - integrity sha512-M99wHjc/5z+Wz1FdFaScVs6dyAi/6PdcIx5Fyip6Qd8aKwm1XyYoOMql5Vu3Cf560feDYCKS4phzyEZ7EJy+EQ== + version "6.1.15" + resolved "https://registry.npmjs.org/@storybook/react/-/react-6.1.15.tgz#e17d00b05b8980ad381ba701805309ed46d1fdcd" + integrity sha512-7WoYLOZuAlzgQsL9oy4JCr9NcB4NBCuxslPSncN5l/7ewGXgfVXTAOMOfw+EVNrtUeVJU2fC8gFiHVl0SJpTZw== dependencies: "@babel/preset-flow" "^7.12.1" "@babel/preset-react" "^7.12.1" "@pmmmwh/react-refresh-webpack-plugin" "^0.4.2" - "@storybook/addons" "6.1.14" - "@storybook/core" "6.1.14" - "@storybook/node-logger" "6.1.14" + "@storybook/addons" "6.1.15" + "@storybook/core" "6.1.15" + "@storybook/node-logger" "6.1.15" "@storybook/semver" "^7.3.2" "@types/webpack-env" "^1.15.3" babel-plugin-add-react-displayname "^0.0.5" @@ -5704,13 +5704,13 @@ memoizerific "^1.11.3" qs "^6.6.0" -"@storybook/router@6.1.14": - version "6.1.14" - resolved "https://registry.npmjs.org/@storybook/router/-/router-6.1.14.tgz#f6aef8c9dabf19bf06dddd80907e66369261fdde" - integrity sha512-rMaUCYzgfVLwFWo3A1Q/weSv8FBqCLmHY+3+t6ao7OV6NYjR0XgLKRzHrXq1uYdbMxWeIKhN2tIt/LR43bmDjQ== +"@storybook/router@6.1.15": + version "6.1.15" + resolved "https://registry.npmjs.org/@storybook/router/-/router-6.1.15.tgz#e0cd7440a2ddc9b265e506b1cb590d3eeab56476" + integrity sha512-HlxDkGpiTSxXCJuqRoZ9Viq6Y/h/7efI8LPhhopr50qWRBTh/PEQzDqWBXG3sj8ISmi9GyUaTSAuqRwdA3lJQQ== dependencies: "@reach/router" "^1.3.3" - "@types/reach__router" "^1.3.5" + "@types/reach__router" "^1.3.7" core-js "^3.0.1" global "^4.3.2" memoizerific "^1.11.3" @@ -5759,15 +5759,15 @@ resolve-from "^5.0.0" ts-dedent "^2.0.0" -"@storybook/theming@6.1.14": - version "6.1.14" - resolved "https://registry.npmjs.org/@storybook/theming/-/theming-6.1.14.tgz#fecb66cab22d3b3218b4a98a9c210eb8a7be91e8" - integrity sha512-S+t30y4FqBTXWoVr+dtxVJ/ywiQGHBclBd9aUunbdCV4mMFra5InNo2CWn+RJlNEauLZ93gRIEzSFchIbzLk1A== +"@storybook/theming@6.1.15": + version "6.1.15" + resolved "https://registry.npmjs.org/@storybook/theming/-/theming-6.1.15.tgz#01083ab89904dd959429b0b3fd1c76bd0ecc59ef" + integrity sha512-88IdYaPzp4NMKf/GKBrPggxD6/d/lkdQ4SNowXxN9g9eONd9M7HtTbjuJGRCbGMJ52xGcbpj2exEnAqKQ2iodA== dependencies: "@emotion/core" "^10.1.1" "@emotion/is-prop-valid" "^0.8.6" "@emotion/styled" "^10.0.23" - "@storybook/client-logger" "6.1.14" + "@storybook/client-logger" "6.1.15" core-js "^3.0.1" deep-object-diff "^1.1.0" emotion-theming "^10.0.19" @@ -5777,21 +5777,21 @@ resolve-from "^5.0.0" ts-dedent "^2.0.0" -"@storybook/ui@6.1.14": - version "6.1.14" - resolved "https://registry.npmjs.org/@storybook/ui/-/ui-6.1.14.tgz#766d696480ee6f6a5a0454ccb2f101c38a0eb9d2" - integrity sha512-DTW2TM05jTMKxh8LzUGk3g5a528PgJxrtgODFU6zzwSg2+LwdmSDtd1HAxopt2vpfTyQyX+6WN2H+lMNwfQTAQ== +"@storybook/ui@6.1.15": + version "6.1.15" + resolved "https://registry.npmjs.org/@storybook/ui/-/ui-6.1.15.tgz#a0f6c49fcf81cf172cd2de4c8dba2be1296891f6" + integrity sha512-quyhJWlOxhk95he7s5/TSYM3eEsaz3s4+98kUZE6r3ssME8u6zDvqa/qa6EWs5/nvZ2V3+12efIzCNbiiT3v3g== dependencies: "@emotion/core" "^10.1.1" - "@storybook/addons" "6.1.14" - "@storybook/api" "6.1.14" - "@storybook/channels" "6.1.14" - "@storybook/client-logger" "6.1.14" - "@storybook/components" "6.1.14" - "@storybook/core-events" "6.1.14" - "@storybook/router" "6.1.14" + "@storybook/addons" "6.1.15" + "@storybook/api" "6.1.15" + "@storybook/channels" "6.1.15" + "@storybook/client-logger" "6.1.15" + "@storybook/components" "6.1.15" + "@storybook/core-events" "6.1.15" + "@storybook/router" "6.1.15" "@storybook/semver" "^7.3.2" - "@storybook/theming" "6.1.14" + "@storybook/theming" "6.1.15" "@types/markdown-to-jsx" "^6.11.0" copy-to-clipboard "^3.0.8" core-js "^3.0.1" @@ -7093,6 +7093,13 @@ "@types/history" "*" "@types/react" "*" +"@types/reach__router@^1.3.7": + version "1.3.7" + resolved "https://registry.npmjs.org/@types/reach__router/-/reach__router-1.3.7.tgz#de8ab374259ae7f7499fc1373b9697a5f3cd6428" + integrity sha512-cyBEb8Ef3SJNH5NYEIDGPoMMmYUxROatuxbICusVRQIqZUB85UCt6R2Ok60tKS/TABJsJYaHyNTW3kqbpxlMjg== + dependencies: + "@types/react" "*" + "@types/react-color@^3.0.1": version "3.0.4" resolved "https://registry.npmjs.org/@types/react-color/-/react-color-3.0.4.tgz#c63daf012ad067ac0127bdd86725f079d02082bd" From 4f6d779b78195f6ad86f303cd6c3e79933344b47 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 27 Jan 2021 09:42:50 +0100 Subject: [PATCH 077/131] chore: run the built main for now, --- .tugboat/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.tugboat/config.yml b/.tugboat/config.yml index 0c8c0329e3..af30015047 100644 --- a/.tugboat/config.yml +++ b/.tugboat/config.yml @@ -14,4 +14,4 @@ services: - yarn tsc - yarn build start: - - node packages/backend --config app-config.yaml --config .tugboat/tugboat.app-config.production.yaml & + - node packages/backend/dist/main --config app-config.yaml --config .tugboat/tugboat.app-config.production.yaml & From f683dfbee5af261cd344022d75c30d119775fdbc Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 27 Jan 2021 10:02:19 +0100 Subject: [PATCH 078/131] chore: fixing config again --- .tugboat/config.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.tugboat/config.yml b/.tugboat/config.yml index af30015047..d130f60554 100644 --- a/.tugboat/config.yml +++ b/.tugboat/config.yml @@ -1,6 +1,6 @@ services: backstage: - image: node:lts-alpine + image: tugboatqa/node:lts expose: 7000 default: true commands: @@ -14,4 +14,5 @@ services: - yarn tsc - yarn build start: - - node packages/backend/dist/main --config app-config.yaml --config .tugboat/tugboat.app-config.production.yaml & + # This should be production, but we need to run backend:bundle like the dockerfile does. + - yarn start-backend --config app-config.yaml --config .tugboat/tugboat.app-config.production.yaml & From 233d519ca19ac180495a58a6a40d39e9aeebda09 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 27 Jan 2021 10:23:53 +0100 Subject: [PATCH 079/131] chore: think this is what it's supposed to look like haha --- .tugboat/config.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.tugboat/config.yml b/.tugboat/config.yml index d130f60554..bdef387f3f 100644 --- a/.tugboat/config.yml +++ b/.tugboat/config.yml @@ -8,11 +8,9 @@ services: - yarn install build: - yarn tsc - - yarn build + - yarn build --config app-config.yaml --config .tugboat/tugboat.app-config.production.yaml update: - yarn install - - yarn tsc - - yarn build start: # This should be production, but we need to run backend:bundle like the dockerfile does. - yarn start-backend --config app-config.yaml --config .tugboat/tugboat.app-config.production.yaml & From 732701e4a7972d424cdd9b0cb75140261c359819 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 27 Jan 2021 10:42:31 +0100 Subject: [PATCH 080/131] chore: Remove the build-config flag --- .tugboat/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.tugboat/config.yml b/.tugboat/config.yml index bdef387f3f..4694800e50 100644 --- a/.tugboat/config.yml +++ b/.tugboat/config.yml @@ -8,7 +8,7 @@ services: - yarn install build: - yarn tsc - - yarn build --config app-config.yaml --config .tugboat/tugboat.app-config.production.yaml + - yarn build update: - yarn install start: From 15932ee95b808a0e3b961ab7417aae4b22b7eadd Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 27 Jan 2021 11:17:12 +0100 Subject: [PATCH 081/131] chore: try different config --- .tugboat/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.tugboat/config.yml b/.tugboat/config.yml index 4694800e50..e06b95d7ef 100644 --- a/.tugboat/config.yml +++ b/.tugboat/config.yml @@ -13,4 +13,4 @@ services: - yarn install start: # This should be production, but we need to run backend:bundle like the dockerfile does. - - yarn start-backend --config app-config.yaml --config .tugboat/tugboat.app-config.production.yaml & + - node packages/backend/dist/main --config app-config.yaml --config .tugboat/tugboat.app-config.production.yaml & From 1aa7817271f79fd20c168d6c362607de8a82edc1 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 27 Jan 2021 11:31:27 +0100 Subject: [PATCH 082/131] chore: hopefully getting something to boot now? --- .tugboat/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.tugboat/config.yml b/.tugboat/config.yml index e06b95d7ef..831ea6a9b6 100644 --- a/.tugboat/config.yml +++ b/.tugboat/config.yml @@ -13,4 +13,4 @@ services: - yarn install start: # This should be production, but we need to run backend:bundle like the dockerfile does. - - node packages/backend/dist/main --config app-config.yaml --config .tugboat/tugboat.app-config.production.yaml & + - yarn start-backend --config $PWD/app-config.yaml --config $PWD/.tugboat/tugboat.app-config.production.yaml & From 04c6781e6ca4673fc4e9ff6698e8451f67e30081 Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Wed, 27 Jan 2021 11:42:20 +0100 Subject: [PATCH 083/131] Extra care for tmp directory on Windows --- packages/backend-common/src/reading/AzureUrlReader.test.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/backend-common/src/reading/AzureUrlReader.test.ts b/packages/backend-common/src/reading/AzureUrlReader.test.ts index 30c086ea08..c937f8a40c 100644 --- a/packages/backend-common/src/reading/AzureUrlReader.test.ts +++ b/packages/backend-common/src/reading/AzureUrlReader.test.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import * as os from 'os'; import fs from 'fs-extra'; import mockFs from 'mock-fs'; import path from 'path'; @@ -32,6 +33,8 @@ const treeResponseFactory = ReadTreeResponseFactory.create({ config: new ConfigReader({}), }); +const tmpDir = os.platform() === 'win32' ? 'C:\\tmp' : '/tmp'; + describe('AzureUrlReader', () => { const worker = setupServer(); msw.setupDefaultHandlers(worker); @@ -142,7 +145,7 @@ describe('AzureUrlReader', () => { describe('readTree', () => { beforeEach(() => { mockFs({ - '/tmp': mockFs.directory(), + [tmpDir]: mockFs.directory(), }); }); @@ -216,7 +219,7 @@ describe('AzureUrlReader', () => { 'https://dev.azure.com/organization/project/_git/repository', ); - const dir = await response.dir({ targetDir: '/tmp' }); + const dir = await response.dir({ targetDir: tmpDir }); await expect( fs.readFile(path.join(dir, 'mkdocs.yml'), 'utf8'), From 45f49bd12c0d1255b19850ce7cfd72df6c3042c9 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 27 Jan 2021 13:48:57 +0100 Subject: [PATCH 084/131] chore: performance! --- .tugboat/config.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.tugboat/config.yml b/.tugboat/config.yml index 831ea6a9b6..7ee1a775e9 100644 --- a/.tugboat/config.yml +++ b/.tugboat/config.yml @@ -7,8 +7,7 @@ services: init: - yarn install build: - - yarn tsc - - yarn build + - yarn workspace example-app build update: - yarn install start: From e3d850a9e460ef2c131c4bc47d5d736db95e8d72 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 27 Jan 2021 13:56:37 +0100 Subject: [PATCH 085/131] chore: try skip yarn for tty problems --- .tugboat/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.tugboat/config.yml b/.tugboat/config.yml index 7ee1a775e9..be952d601c 100644 --- a/.tugboat/config.yml +++ b/.tugboat/config.yml @@ -12,4 +12,4 @@ services: - yarn install start: # This should be production, but we need to run backend:bundle like the dockerfile does. - - yarn start-backend --config $PWD/app-config.yaml --config $PWD/.tugboat/tugboat.app-config.production.yaml & + - cd packages/backend && node_modules/.bin/backstage-cli backend:dev --config ../../app-config.yaml --config ../../.tugboat/tugboat.app-config.production.yaml & From f1d1ef4635523577f640bdcc406df22ef17aed17 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 27 Jan 2021 14:35:11 +0100 Subject: [PATCH 086/131] chore: updating config --- .tugboat/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.tugboat/config.yml b/.tugboat/config.yml index be952d601c..2678f39540 100644 --- a/.tugboat/config.yml +++ b/.tugboat/config.yml @@ -12,4 +12,4 @@ services: - yarn install start: # This should be production, but we need to run backend:bundle like the dockerfile does. - - cd packages/backend && node_modules/.bin/backstage-cli backend:dev --config ../../app-config.yaml --config ../../.tugboat/tugboat.app-config.production.yaml & + - cd packages/backend && node_modules/.bin/backstage-cli backend:dev --config ../../app-config.yaml --config ../../.tugboat/tugboat.app-config.production.yaml From 43290ea99126444cb09723c65e53cc6489210f76 Mon Sep 17 00:00:00 2001 From: Alan Crosswell Date: Tue, 26 Jan 2021 17:35:09 -0500 Subject: [PATCH 087/131] update version of git-url-parse to set ref for Azure DevOps --- packages/backend-common/package.json | 2 +- plugins/scaffolder-backend/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index c698351615..e556a75062 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -43,7 +43,7 @@ "express": "^4.17.1", "express-promise-router": "^3.0.3", "fs-extra": "^9.0.1", - "git-url-parse": "^11.4.3", + "git-url-parse": "^11.4.4", "helmet": "^4.0.0", "isomorphic-git": "^1.8.0", "knex": "^0.21.6", diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index cd7dab3444..e118a1d7f9 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -48,7 +48,7 @@ "express": "^4.17.1", "express-promise-router": "^3.0.3", "fs-extra": "^9.0.0", - "git-url-parse": "^11.4.3", + "git-url-parse": "^11.4.4", "globby": "^11.0.0", "helmet": "^4.0.0", "isomorphic-git": "^1.8.0", From 3179c98c85c0240dc4842ff6bde1682bd6a1eeca Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 27 Jan 2021 14:49:04 +0100 Subject: [PATCH 088/131] chore: updating config again --- .tugboat/config.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.tugboat/config.yml b/.tugboat/config.yml index 2678f39540..2be043cd78 100644 --- a/.tugboat/config.yml +++ b/.tugboat/config.yml @@ -5,11 +5,11 @@ services: default: true commands: init: - - yarn install + - mkdir -p /etc/service/node + - echo "#!/bin/sh" > /etc/service/node/run + - echo "yarn --cwd ${TUGBOAT_ROOT} start-backend --config ${TUGBOAT_ROOT}/app-config.yaml --config ${TUGBOAT_ROOT}/.tugboat/tugboat.app-config.production.yaml" >> /etc/service/node/run + - chmod +x /etc/service/node/run build: - yarn workspace example-app build update: - yarn install - start: - # This should be production, but we need to run backend:bundle like the dockerfile does. - - cd packages/backend && node_modules/.bin/backstage-cli backend:dev --config ../../app-config.yaml --config ../../.tugboat/tugboat.app-config.production.yaml From e9b9fa2ffef7d9bfbe99061455cc1788083a2b15 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 27 Jan 2021 16:38:27 +0100 Subject: [PATCH 089/131] chore: added a note about .dockerignore --- docs/getting-started/deployment-other.md | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/docs/getting-started/deployment-other.md b/docs/getting-started/deployment-other.md index 6436bdb195..92c5622593 100644 --- a/docs/getting-started/deployment-other.md +++ b/docs/getting-started/deployment-other.md @@ -62,8 +62,26 @@ COPY app-config.yaml app-config.production.yaml ./ CMD ["node", "packages/backend", "--config", "app-config.yaml", "--config", "app-config.production.yaml"] ``` -You can add the Dockerfile to the root of your project, and run the following to -build the container under a specified tag. +Before building you should also include a `.dockerignore`. This will greatly +improve the context bootup of Docker as we are no longer sending all of the +`node_modules` into the context. It also helps us avoid some limitations and +errors that may occur when trying to share the `node_modules` folder to inside +the build. + +You can add the following contents to the root of your repository at +`.dockerignore` and it might look something like the following: + +```dockerignore +.git +node_modules +packages/*/node_modules +plugins/*/node_modules +plugins/*/dist +``` + +Once you have added both the `Dockerfile` and `.dockerignore` to the root of +your project, and run the following to build the container under a specified +tag. ```sh $ docker build -t example-deployment . From 73c299c9ee00d9475602777c157c08eae6195ca3 Mon Sep 17 00:00:00 2001 From: Alan Crosswell Date: Wed, 27 Jan 2021 11:54:03 -0500 Subject: [PATCH 090/131] upgrade git-url-parse to 11.4.4 --- packages/backend-common/package.json | 2 +- packages/integration/package.json | 2 +- packages/techdocs-common/package.json | 2 +- plugins/catalog-backend/package.json | 2 +- plugins/catalog-import/package.json | 2 +- plugins/catalog/package.json | 2 +- plugins/scaffolder-backend/package.json | 2 +- plugins/scaffolder/package.json | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index c698351615..e556a75062 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -43,7 +43,7 @@ "express": "^4.17.1", "express-promise-router": "^3.0.3", "fs-extra": "^9.0.1", - "git-url-parse": "^11.4.3", + "git-url-parse": "^11.4.4", "helmet": "^4.0.0", "isomorphic-git": "^1.8.0", "knex": "^0.21.6", diff --git a/packages/integration/package.json b/packages/integration/package.json index fd5268dced..6c96a8562b 100644 --- a/packages/integration/package.json +++ b/packages/integration/package.json @@ -31,7 +31,7 @@ "dependencies": { "@backstage/config": "^0.1.2", "cross-fetch": "^3.0.6", - "git-url-parse": "^11.4.3", + "git-url-parse": "^11.4.4", "@octokit/rest": "^18.0.12", "@octokit/auth-app": "^2.10.5", "luxon": "^1.25.0" diff --git a/packages/techdocs-common/package.json b/packages/techdocs-common/package.json index 93825511cb..dd4e4d8c35 100644 --- a/packages/techdocs-common/package.json +++ b/packages/techdocs-common/package.json @@ -48,7 +48,7 @@ "dockerode": "^3.2.1", "express": "^4.17.1", "fs-extra": "^9.0.1", - "git-url-parse": "^11.4.3", + "git-url-parse": "^11.4.4", "js-yaml": "^4.0.0", "json5": "^2.1.3", "mime-types": "^2.1.27", diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index b2db27770b..eaf1f69938 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -43,7 +43,7 @@ "express": "^4.17.1", "express-promise-router": "^3.0.3", "fs-extra": "^9.0.0", - "git-url-parse": "^11.4.3", + "git-url-parse": "^11.4.4", "knex": "^0.21.6", "ldapjs": "^2.2.0", "lodash": "^4.17.15", diff --git a/plugins/catalog-import/package.json b/plugins/catalog-import/package.json index b2c9991bb8..2f9784a23e 100644 --- a/plugins/catalog-import/package.json +++ b/plugins/catalog-import/package.json @@ -39,7 +39,7 @@ "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", "@octokit/rest": "^18.0.12", - "git-url-parse": "^11.4.3", + "git-url-parse": "^11.4.4", "react": "^16.13.1", "react-dom": "^16.13.1", "react-hook-form": "^6.6.0", diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index be1681c596..0887930726 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -40,7 +40,7 @@ "@material-ui/lab": "4.0.0-alpha.45", "@types/react": "^16.9", "classnames": "^2.2.6", - "git-url-parse": "^11.4.3", + "git-url-parse": "^11.4.4", "moment": "^2.26.0", "react": "^16.13.1", "react-dom": "^16.13.1", diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index cd7dab3444..e118a1d7f9 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -48,7 +48,7 @@ "express": "^4.17.1", "express-promise-router": "^3.0.3", "fs-extra": "^9.0.0", - "git-url-parse": "^11.4.3", + "git-url-parse": "^11.4.4", "globby": "^11.0.0", "helmet": "^4.0.0", "isomorphic-git": "^1.8.0", diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 3c98972aa8..483dc548ea 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -40,7 +40,7 @@ "@rjsf/core": "^2.4.0", "@rjsf/material-ui": "^2.4.0", "classnames": "^2.2.6", - "git-url-parse": "^11.4.3", + "git-url-parse": "^11.4.4", "moment": "^2.26.0", "react": "^16.13.1", "react-dom": "^16.13.1", From 9dd057662453459db97582657861ec996c28bd8c Mon Sep 17 00:00:00 2001 From: Alan Crosswell Date: Wed, 27 Jan 2021 12:00:32 -0500 Subject: [PATCH 091/131] changeset --- .changeset/empty-coats-film.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .changeset/empty-coats-film.md diff --git a/.changeset/empty-coats-film.md b/.changeset/empty-coats-film.md new file mode 100644 index 0000000000..02b0225102 --- /dev/null +++ b/.changeset/empty-coats-film.md @@ -0,0 +1,12 @@ +--- +'@backstage/backend-common': patch +'@backstage/integration': patch +'@backstage/techdocs-common': patch +'@backstage/plugin-catalog': patch +'@backstage/plugin-catalog-backend': patch +'@backstage/plugin-catalog-import': patch +'@backstage/plugin-scaffolder': patch +'@backstage/plugin-scaffolder-backend': patch +--- + +Upgrade [git-url-parse](https://www.npmjs.com/package/git-url-parse) to [v11.4.4](https://github.com/IonicaBizau/git-url-parse/pull/125) which fixes parsing an Azure DevOps branch ref. From e346f96540de9107f7775b5f8523e766023552f7 Mon Sep 17 00:00:00 2001 From: Alan Crosswell Date: Wed, 27 Jan 2021 14:17:19 -0500 Subject: [PATCH 092/131] forgot to push yarn.lock --- yarn.lock | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/yarn.lock b/yarn.lock index 7c743bc5b3..1dcbe1733d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14180,6 +14180,13 @@ git-url-parse@^11.4.3: dependencies: git-up "^4.0.0" +git-url-parse@^11.4.4: + version "11.4.4" + resolved "https://registry.npmjs.org/git-url-parse/-/git-url-parse-11.4.4.tgz#5d747debc2469c17bc385719f7d0427802d83d77" + integrity sha512-Y4o9o7vQngQDIU9IjyCmRJBin5iYjI5u9ZITnddRZpD7dcCFQj2sL2XuMNbLRE4b4B/4ENPsp2Q8P44fjAZ0Pw== + dependencies: + git-up "^4.0.0" + gitconfiglocal@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/gitconfiglocal/-/gitconfiglocal-1.0.0.tgz#41d045f3851a5ea88f03f24ca1c6178114464b9b" From 8f59f5bbc21236b3b50a05273166e5e3cf559fa3 Mon Sep 17 00:00:00 2001 From: Alan Crosswell Date: Wed, 27 Jan 2021 14:19:58 -0500 Subject: [PATCH 093/131] forgot to push yarn.lock --- yarn.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/yarn.lock b/yarn.lock index 36a133fd37..6b634511b4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14180,10 +14180,10 @@ git-url-parse@^11.1.2: dependencies: git-up "^4.0.0" -git-url-parse@^11.4.3: - version "11.4.3" - resolved "https://registry.npmjs.org/git-url-parse/-/git-url-parse-11.4.3.tgz#1610284edf1f14964180f5b3399ec68b692cfd87" - integrity sha512-LZTTk0nqJnKN48YRtOpR8H5SEfp1oM2tls90NuZmBxN95PnCvmuXGzqQ4QmVirBgKx2KPYfPGteX3/raWjKenQ== +git-url-parse@^11.4.4: + version "11.4.4" + resolved "https://registry.npmjs.org/git-url-parse/-/git-url-parse-11.4.4.tgz#5d747debc2469c17bc385719f7d0427802d83d77" + integrity sha512-Y4o9o7vQngQDIU9IjyCmRJBin5iYjI5u9ZITnddRZpD7dcCFQj2sL2XuMNbLRE4b4B/4ENPsp2Q8P44fjAZ0Pw== dependencies: git-up "^4.0.0" From 1f675381a9143ec63037270f2821ea5c51dedb64 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 27 Jan 2021 21:59:44 +0100 Subject: [PATCH 094/131] chore: fixing vale stuff --- docs/getting-started/deployment-other.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/getting-started/deployment-other.md b/docs/getting-started/deployment-other.md index 92c5622593..418a6b3dd3 100644 --- a/docs/getting-started/deployment-other.md +++ b/docs/getting-started/deployment-other.md @@ -63,8 +63,8 @@ CMD ["node", "packages/backend", "--config", "app-config.yaml", "--config", "app ``` Before building you should also include a `.dockerignore`. This will greatly -improve the context bootup of Docker as we are no longer sending all of the -`node_modules` into the context. It also helps us avoid some limitations and +improve the context boot up time of Docker as we are no longer sending all of +the `node_modules` into the context. It also helps us avoid some limitations and errors that may occur when trying to share the `node_modules` folder to inside the build. From 83a879c4cb4debe5531074be22acc5b430fc27e5 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 28 Jan 2021 00:01:55 +0100 Subject: [PATCH 095/131] chore: add a simple github action workflow --- .github/workflows/tugboat.yml | 57 +++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 .github/workflows/tugboat.yml diff --git a/.github/workflows/tugboat.yml b/.github/workflows/tugboat.yml new file mode 100644 index 0000000000..4c52ce99d6 --- /dev/null +++ b/.github/workflows/tugboat.yml @@ -0,0 +1,57 @@ +name: Tugboat E2E Tests +on: deployment_status + +jobs: + run: + # When the deployment event is success + if: github.event.deployment_status.state == 'success' + name: Run test suite against tugboat + runs-on: ubuntu-latest + steps: + # Set an initial commit status message to indicate that the tests are + # running. + - name: set pending status + uses: actions/github-script@v3 + with: + github-token: ${{secrets.GH_SERVICE_ACCOUNT_TOKEN}} + debug: true + script: | + return github.repos.createCommitStatus({ + owner: context.repo.owner, + repo: context.repo.repo, + sha: context.sha, + state: 'pending', + context: 'Nightwatch.js tests', + description: 'Running tests', + target_url: "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + }); + - uses: actions/checkout@v1 + - uses: actions/setup-node@v1 + with: + node-version: '14' + - name: get deployment status + id: get-status-env + uses: actions/github-script@v3 + with: + github-token: ${{secrets.GH_SERVICE_ACCOUNT_TOKEN}} + result-encoding: string + script: | + const result = await github.repos.getDeploymentStatus({ + owner: context.repo.owner, + repo: context.repo.repo, + deployment_id: context.payload.deployment.id, + status_id: context.payload.deployment_status.id, + headers: { + 'Accept': 'application/vnd.github.ant-man-preview+json' + }, + }); + console.log(result); + return result.data.environment_url; + - name: echo tugboat preview url + run: | + echo ${{ steps.get-status-env.outputs.result }} + # The first time you hit a Tugboat URL it can take a while to load, so + # we visit it once here to prime it. Otherwise the very first test + # will often timeout. + curl ${{ steps.get-status-env.outputs.result }} + From 3148751354df0cb62324d65ef64c66f90e1c53dc Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 28 Jan 2021 00:12:52 +0100 Subject: [PATCH 096/131] chore: update workfloew --- .github/workflows/tugboat.yml | 107 ++++++++++++++++++---------------- 1 file changed, 57 insertions(+), 50 deletions(-) diff --git a/.github/workflows/tugboat.yml b/.github/workflows/tugboat.yml index 4c52ce99d6..da67be58b4 100644 --- a/.github/workflows/tugboat.yml +++ b/.github/workflows/tugboat.yml @@ -2,56 +2,63 @@ name: Tugboat E2E Tests on: deployment_status jobs: - run: - # When the deployment event is success + run-tests: + # Only run after a successful Tugboat deployment. if: github.event.deployment_status.state == 'success' - name: Run test suite against tugboat + name: Run tests against Tugboat runs-on: ubuntu-latest - steps: - # Set an initial commit status message to indicate that the tests are - # running. - - name: set pending status - uses: actions/github-script@v3 - with: - github-token: ${{secrets.GH_SERVICE_ACCOUNT_TOKEN}} - debug: true - script: | - return github.repos.createCommitStatus({ - owner: context.repo.owner, - repo: context.repo.repo, - sha: context.sha, - state: 'pending', - context: 'Nightwatch.js tests', - description: 'Running tests', - target_url: "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" - }); - - uses: actions/checkout@v1 - - uses: actions/setup-node@v1 - with: - node-version: '14' - - name: get deployment status - id: get-status-env - uses: actions/github-script@v3 - with: - github-token: ${{secrets.GH_SERVICE_ACCOUNT_TOKEN}} - result-encoding: string - script: | - const result = await github.repos.getDeploymentStatus({ - owner: context.repo.owner, - repo: context.repo.repo, - deployment_id: context.payload.deployment.id, - status_id: context.payload.deployment_status.id, - headers: { - 'Accept': 'application/vnd.github.ant-man-preview+json' - }, - }); - console.log(result); - return result.data.environment_url; - - name: echo tugboat preview url - run: | - echo ${{ steps.get-status-env.outputs.result }} - # The first time you hit a Tugboat URL it can take a while to load, so - # we visit it once here to prime it. Otherwise the very first test - # will often timeout. - curl ${{ steps.get-status-env.outputs.result }} + steps: + # Set an initial commit status message to indicate that the tests are + # running. + - name: set pending status + uses: actions/github-script@v3 + with: + github-token: ${{secrets.GH_SERVICE_ACCOUNT_TOKEN}} + debug: true + script: | + return github.repos.createCommitStatus({ + owner: context.repo.owner, + repo: context.repo.repo, + sha: context.sha, + state: 'pending', + context: 'Nightwatch.js tests', + description: 'Running tests', + target_url: "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + }); + - uses: actions/checkout@v1 + - uses: actions/setup-node@v1 + with: + node-version: '14' + + # This is required because the environment_url param that Tugboat uses + # to tell us where the preview is located isn't supported unless you + # specify the custom Accept header when getting the deployment_status, + # and GitHub actions doesn't do that by default. So instead we have to + # load the status object manually and get the data we need. + # https://developer.github.com/changes/2016-04-06-deployment-and-deployment-status-enhancements/ + - name: get deployment status + id: get-status-env + uses: actions/github-script@v3 + with: + github-token: ${{secrets.GH_SERVICE_ACCOUNT_TOKEN}} + result-encoding: string + script: | + const result = await github.repos.getDeploymentStatus({ + owner: context.repo.owner, + repo: context.repo.repo, + deployment_id: context.payload.deployment.id, + status_id: context.payload.deployment_status.id, + headers: { + 'Accept': 'application/vnd.github.ant-man-preview+json' + }, + }); + console.log(result); + return result.data.environment_url; + - name: echo tugboat preview url + run: | + echo ${{ steps.get-status-env.outputs.result }} + # The first time you hit a Tugboat URL it can take a while to load, so + # we visit it once here to prime it. Otherwise the very first test + # will often timeout. + curl ${{ steps.get-status-env.outputs.result }} From fca9de6c93b53395a994bfa2271f3e5ba84d7c3f Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 28 Jan 2021 00:24:28 +0100 Subject: [PATCH 097/131] chore: update workflow again --- .github/workflows/tugboat.yml | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/.github/workflows/tugboat.yml b/.github/workflows/tugboat.yml index da67be58b4..53fcd2e873 100644 --- a/.github/workflows/tugboat.yml +++ b/.github/workflows/tugboat.yml @@ -1,7 +1,29 @@ name: Tugboat E2E Tests on: deployment_status - jobs: + set-pending: + if: github.event.deployment_status.state != 'success' && github.event.deployment_status.state != 'failed' + name: Run tests against Tugboat + runs-on: ubuntu-latest + steps: + # Set an initial commit status message to indicate that the tests are + # running. + - name: set pending status + uses: actions/github-script@v3 + with: + github-token: ${{secrets.GH_SERVICE_ACCOUNT_TOKEN}} + debug: true + script: | + return github.repos.createCommitStatus({ + owner: context.repo.owner, + repo: context.repo.repo, + sha: context.sha, + state: 'pending', + context: 'Backstage Tugboat E2E Tests', + description: 'Running tests', + target_url: "https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}" + }); + run-tests: # Only run after a successful Tugboat deployment. if: github.event.deployment_status.state == 'success' @@ -21,9 +43,9 @@ jobs: repo: context.repo.repo, sha: context.sha, state: 'pending', - context: 'Nightwatch.js tests', + context: 'Backstage Tugboat E2E Tests', description: 'Running tests', - target_url: "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + target_url: "https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}" }); - uses: actions/checkout@v1 @@ -57,8 +79,8 @@ jobs: return result.data.environment_url; - name: echo tugboat preview url run: | - echo ${{ steps.get-status-env.outputs.result }} + echo ${{steps.get-status-env.outputs.result}} # The first time you hit a Tugboat URL it can take a while to load, so # we visit it once here to prime it. Otherwise the very first test # will often timeout. - curl ${{ steps.get-status-env.outputs.result }} + curl ${{steps.get-status-env.outputs.result}} From a4716e47276475f2424471df7fd126bcc9f484bb Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 28 Jan 2021 00:50:44 +0100 Subject: [PATCH 098/131] chore: fixing deployments for real --- .github/workflows/tugboat.yml | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/.github/workflows/tugboat.yml b/.github/workflows/tugboat.yml index 53fcd2e873..cce4df01de 100644 --- a/.github/workflows/tugboat.yml +++ b/.github/workflows/tugboat.yml @@ -3,7 +3,7 @@ on: deployment_status jobs: set-pending: if: github.event.deployment_status.state != 'success' && github.event.deployment_status.state != 'failed' - name: Run tests against Tugboat + name: Set pending waiting for Tugboat runs-on: ubuntu-latest steps: # Set an initial commit status message to indicate that the tests are @@ -20,14 +20,14 @@ jobs: sha: context.sha, state: 'pending', context: 'Backstage Tugboat E2E Tests', - description: 'Running tests', + description: 'Waiting for Tugboat to complete deployment', target_url: "https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}" }); run-tests: # Only run after a successful Tugboat deployment. if: github.event.deployment_status.state == 'success' - name: Run tests against Tugboat + name: Run tests against Tugboat deployment runs-on: ubuntu-latest steps: # Set an initial commit status message to indicate that the tests are @@ -44,7 +44,7 @@ jobs: sha: context.sha, state: 'pending', context: 'Backstage Tugboat E2E Tests', - description: 'Running tests', + description: 'Running against tugboat preview', target_url: "https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}" }); @@ -79,8 +79,4 @@ jobs: return result.data.environment_url; - name: echo tugboat preview url run: | - echo ${{steps.get-status-env.outputs.result}} - # The first time you hit a Tugboat URL it can take a while to load, so - # we visit it once here to prime it. Otherwise the very first test - # will often timeout. curl ${{steps.get-status-env.outputs.result}} From 408962dcead8301ae3885a7769264f117ea83fe0 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 28 Jan 2021 01:17:52 +0100 Subject: [PATCH 099/131] chore: last github workflow plz --- .github/workflows/tugboat.yml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/.github/workflows/tugboat.yml b/.github/workflows/tugboat.yml index cce4df01de..3d6980c81e 100644 --- a/.github/workflows/tugboat.yml +++ b/.github/workflows/tugboat.yml @@ -80,3 +80,32 @@ jobs: - name: echo tugboat preview url run: | curl ${{steps.get-status-env.outputs.result}} + # Update the commit status with a fail or success. + - name: set status + if: ${{ success() }} + uses: actions/github-script@v3 + with: + github-token: ${{secrets.GH_SERVICE_ACCOUNT_TOKEN}} + script: | + return github.repos.createCommitStatus({ + owner: context.repo.owner, + repo: context.repo.repo, + sha: context.sha, + state: "success", + context: 'Backstage Tugboat E2E Tests', + target_url: "https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}" + }); + - name: set status + if: ${{ failure() }} || ${{ cancelled() }} + uses: actions/github-script@v3 + with: + github-token: ${{secrets.GH_SERVICE_ACCOUNT_TOKEN}} + script: | + return github.repos.createCommitStatus({ + owner: context.repo.owner, + repo: context.repo.repo, + sha: context.sha, + state: "error", + context: 'Backstage Tugboat E2E Tests', + target_url: "https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}" + }); From f2b7817d4f9cca4740da984b819a905573349531 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 28 Jan 2021 04:48:11 +0000 Subject: [PATCH 100/131] chore(deps): bump core-js from 3.6.5 to 3.8.3 Bumps [core-js](https://github.com/zloirock/core-js) from 3.6.5 to 3.8.3. - [Release notes](https://github.com/zloirock/core-js/releases) - [Changelog](https://github.com/zloirock/core-js/blob/master/CHANGELOG.md) - [Commits](https://github.com/zloirock/core-js/compare/v3.6.5...v3.8.3) Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 6b634511b4..aefde4902f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10787,9 +10787,9 @@ core-js-pure@^3.0.0, core-js-pure@^3.0.1: integrity sha512-epIhRLkXdgv32xIUFaaAry2wdxZYBi6bgM7cB136dzzXXa+dFyRLTZeLUJxnd8ShrmyVXBub63n2NHo2JAt8Cw== core-js@3, core-js@^3.0.1, core-js@^3.0.4, core-js@^3.5.0, core-js@^3.6.0, core-js@^3.6.5: - version "3.6.5" - resolved "https://registry.npmjs.org/core-js/-/core-js-3.6.5.tgz#7395dc273af37fb2e50e9bd3d9fe841285231d1a" - integrity sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA== + version "3.8.3" + resolved "https://registry.npmjs.org/core-js/-/core-js-3.8.3.tgz#c21906e1f14f3689f93abcc6e26883550dd92dd0" + integrity sha512-KPYXeVZYemC2TkNEkX/01I+7yd+nX3KddKwZ1Ww7SKWdI2wQprSgLmrTddT8nw92AjEklTsPBoSdQBhbI1bQ6Q== core-js@^2.4.0, core-js@^2.5.7, core-js@^2.6.10, core-js@^2.6.5: version "2.6.11" From 99460a64371c4b2604e151088f86463fb6635c2b Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 28 Jan 2021 10:45:16 +0100 Subject: [PATCH 101/131] chore: updating workflow --- .github/workflows/tugboat.yml | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/.github/workflows/tugboat.yml b/.github/workflows/tugboat.yml index 3d6980c81e..fdb2e1ddc4 100644 --- a/.github/workflows/tugboat.yml +++ b/.github/workflows/tugboat.yml @@ -80,7 +80,20 @@ jobs: - name: echo tugboat preview url run: | curl ${{steps.get-status-env.outputs.result}} - # Update the commit status with a fail or success. + - name: set status + if: ${{ failure() }} + uses: actions/github-script@v3 + with: + github-token: ${{secrets.GH_SERVICE_ACCOUNT_TOKEN}} + script: | + return github.repos.createCommitStatus({ + owner: context.repo.owner, + repo: context.repo.repo, + sha: context.sha, + state: "error", + context: 'Backstage Tugboat E2E Tests', + target_url: "https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}" + }); - name: set status if: ${{ success() }} uses: actions/github-script@v3 @@ -95,17 +108,3 @@ jobs: context: 'Backstage Tugboat E2E Tests', target_url: "https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}" }); - - name: set status - if: ${{ failure() }} || ${{ cancelled() }} - uses: actions/github-script@v3 - with: - github-token: ${{secrets.GH_SERVICE_ACCOUNT_TOKEN}} - script: | - return github.repos.createCommitStatus({ - owner: context.repo.owner, - repo: context.repo.repo, - sha: context.sha, - state: "error", - context: 'Backstage Tugboat E2E Tests', - target_url: "https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}" - }); From cda741034da2dc5ec22099e1d0c56854c201a0ef Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 28 Jan 2021 10:46:08 +0000 Subject: [PATCH 102/131] Version Packages --- .changeset/breezy-meals-lie.md | 20 ---------- .changeset/chatty-cooks-begin.md | 6 --- .changeset/clean-rocks-ring.md | 18 --------- .changeset/cost-insights-curvy-dingos-live.md | 5 --- .changeset/cost-insights-yellow-trees-love.md | 6 --- .changeset/dull-ears-glow.md | 5 --- .changeset/eleven-lamps-hide.md | 5 --- .changeset/empty-coats-film.md | 12 ------ .changeset/fast-flowers-tickle.md | 31 -------------- .changeset/five-games-grin.md | 5 --- .changeset/forty-jobs-occur.md | 8 ---- .changeset/fuzzy-points-whisper.md | 5 --- .changeset/green-boats-attend.md | 5 --- .changeset/happy-crabs-punch.md | 12 ------ .changeset/healthy-cameras-suffer.md | 5 --- .changeset/honest-jokes-rush.md | 5 --- .changeset/loud-terms-kiss.md | 5 --- .changeset/lucky-guests-mate.md | 5 --- .changeset/stale-tools-confess.md | 5 --- .changeset/techdocs-dirty-rivers-hope.md | 5 --- .changeset/tender-parrots-itch.md | 5 --- packages/app/CHANGELOG.md | 21 ++++++++++ packages/app/package.json | 16 ++++---- packages/backend-common/CHANGELOG.md | 30 ++++++++++++++ packages/backend-common/package.json | 8 ++-- packages/backend/CHANGELOG.md | 22 ++++++++++ packages/backend/package.json | 20 +++++----- packages/catalog-client/package.json | 2 +- packages/catalog-model/package.json | 2 +- packages/cli/CHANGELOG.md | 27 +++++++++++++ packages/cli/package.json | 6 +-- packages/config-loader/CHANGELOG.md | 18 +++++++++ packages/config-loader/package.json | 2 +- packages/core-api/package.json | 2 +- packages/core/package.json | 2 +- packages/create-app/CHANGELOG.md | 40 +++++++++++++++++++ packages/create-app/package.json | 20 +++++----- packages/dev-utils/package.json | 4 +- packages/integration/CHANGELOG.md | 7 ++++ packages/integration/package.json | 4 +- packages/techdocs-common/CHANGELOG.md | 13 ++++++ packages/techdocs-common/package.json | 8 ++-- packages/test-utils/package.json | 2 +- packages/theme/package.json | 2 +- plugins/api-docs/package.json | 4 +- plugins/app-backend/CHANGELOG.md | 13 ++++++ plugins/app-backend/package.json | 8 ++-- plugins/auth-backend/CHANGELOG.md | 12 ++++++ plugins/auth-backend/package.json | 6 +-- plugins/catalog-backend/CHANGELOG.md | 11 +++++ plugins/catalog-backend/package.json | 6 +-- plugins/catalog-graphql/package.json | 4 +- plugins/catalog-import/CHANGELOG.md | 11 +++++ plugins/catalog-import/package.json | 8 ++-- plugins/catalog/CHANGELOG.md | 13 ++++++ plugins/catalog/package.json | 6 +-- plugins/circleci/package.json | 4 +- plugins/cloudbuild/package.json | 4 +- plugins/cost-insights/CHANGELOG.md | 11 +++++ plugins/cost-insights/package.json | 4 +- plugins/explore/package.json | 2 +- plugins/fossa/package.json | 2 +- plugins/gcp-projects/package.json | 2 +- plugins/github-actions/package.json | 6 +-- plugins/gitops-profiles/package.json | 2 +- plugins/graphiql/package.json | 2 +- plugins/graphql/package.json | 4 +- plugins/jenkins/package.json | 4 +- plugins/kafka-backend/CHANGELOG.md | 38 ++++++++++++++++++ plugins/kafka-backend/package.json | 6 +-- plugins/kafka/CHANGELOG.md | 37 +++++++++++++++++ plugins/kafka/package.json | 6 +-- plugins/kubernetes-backend/CHANGELOG.md | 10 +++++ plugins/kubernetes-backend/package.json | 6 +-- plugins/kubernetes/CHANGELOG.md | 9 +++++ plugins/kubernetes/package.json | 6 +-- plugins/lighthouse/package.json | 4 +- plugins/newrelic/package.json | 2 +- plugins/org/package.json | 4 +- plugins/pagerduty/package.json | 2 +- plugins/proxy-backend/package.json | 4 +- plugins/register-component/package.json | 4 +- plugins/rollbar-backend/package.json | 4 +- plugins/rollbar/package.json | 4 +- plugins/scaffolder-backend/CHANGELOG.md | 28 +++++++++++++ plugins/scaffolder-backend/package.json | 8 ++-- plugins/scaffolder/CHANGELOG.md | 9 +++++ plugins/scaffolder/package.json | 6 +-- plugins/search/package.json | 4 +- plugins/sentry/package.json | 4 +- plugins/sonarqube/CHANGELOG.md | 6 +++ plugins/sonarqube/package.json | 4 +- plugins/tech-radar/package.json | 2 +- plugins/techdocs-backend/package.json | 6 +-- plugins/techdocs/package.json | 6 +-- plugins/user-settings/package.json | 2 +- plugins/welcome/package.json | 2 +- 97 files changed, 523 insertions(+), 315 deletions(-) delete mode 100644 .changeset/breezy-meals-lie.md delete mode 100644 .changeset/chatty-cooks-begin.md delete mode 100644 .changeset/clean-rocks-ring.md delete mode 100644 .changeset/cost-insights-curvy-dingos-live.md delete mode 100644 .changeset/cost-insights-yellow-trees-love.md delete mode 100644 .changeset/dull-ears-glow.md delete mode 100644 .changeset/eleven-lamps-hide.md delete mode 100644 .changeset/empty-coats-film.md delete mode 100644 .changeset/fast-flowers-tickle.md delete mode 100644 .changeset/five-games-grin.md delete mode 100644 .changeset/forty-jobs-occur.md delete mode 100644 .changeset/fuzzy-points-whisper.md delete mode 100644 .changeset/green-boats-attend.md delete mode 100644 .changeset/happy-crabs-punch.md delete mode 100644 .changeset/healthy-cameras-suffer.md delete mode 100644 .changeset/honest-jokes-rush.md delete mode 100644 .changeset/loud-terms-kiss.md delete mode 100644 .changeset/lucky-guests-mate.md delete mode 100644 .changeset/stale-tools-confess.md delete mode 100644 .changeset/techdocs-dirty-rivers-hope.md delete mode 100644 .changeset/tender-parrots-itch.md diff --git a/.changeset/breezy-meals-lie.md b/.changeset/breezy-meals-lie.md deleted file mode 100644 index 4ca4f1ac35..0000000000 --- a/.changeset/breezy-meals-lie.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -'@backstage/backend-common': patch -'@backstage/plugin-scaffolder-backend': patch ---- - -Honor the branch ref in the url when cloning. - -This fixes a bug in the scaffolder prepare stage where a non-default branch -was specified in the scaffolder URL but the default branch was cloned. -For example, even though the `other` branch is specified in this example, the -`master` branch was actually cloned: - -```yaml -catalog: - locations: - - type: url - target: https://github.com/backstage/backstage/blob/other/plugins/scaffolder-backend/sample-templates/docs-template/template.yaml -``` - -This also fixes a 404 in the prepare stage for GitLab URLs. diff --git a/.changeset/chatty-cooks-begin.md b/.changeset/chatty-cooks-begin.md deleted file mode 100644 index e32d22d532..0000000000 --- a/.changeset/chatty-cooks-begin.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/plugin-kubernetes': patch -'@backstage/plugin-kubernetes-backend': patch ---- - -Add AWS auth provider for Kubernetes diff --git a/.changeset/clean-rocks-ring.md b/.changeset/clean-rocks-ring.md deleted file mode 100644 index 706f3640c2..0000000000 --- a/.changeset/clean-rocks-ring.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -'@backstage/cli': minor ---- - -We've bumped the `@eslint-typescript` packages to the latest, which now add some additional rules that might cause lint failures. -The main one which could become an issue is the [no-use-before-define](https://eslint.org/docs/rules/no-use-before-define) rule. - -Every plugin and app has the ability to override these rules if you want to ignore them for now. - -You can reset back to the default behaviour by using the following in your own `.eslint.js` - -```js -rules: { - 'no-use-before-define': 'off' -} -``` - -Because of the nature of this change, we're unable to provide a grace period for the update :( diff --git a/.changeset/cost-insights-curvy-dingos-live.md b/.changeset/cost-insights-curvy-dingos-live.md deleted file mode 100644 index f15d3b0492..0000000000 --- a/.changeset/cost-insights-curvy-dingos-live.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-cost-insights': patch ---- - -Allow expand functionality to top panel product chart tooltip. diff --git a/.changeset/cost-insights-yellow-trees-love.md b/.changeset/cost-insights-yellow-trees-love.md deleted file mode 100644 index 19eea0dfe2..0000000000 --- a/.changeset/cost-insights-yellow-trees-love.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/plugin-cost-insights': minor ---- - -Add support for additional breakdowns of daily cost data. -This changes the type of Cost.groupedCosts returned by CostInsightsApi.getGroupDailyCost. diff --git a/.changeset/dull-ears-glow.md b/.changeset/dull-ears-glow.md deleted file mode 100644 index ea08b3b37e..0000000000 --- a/.changeset/dull-ears-glow.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-common': patch ---- - -URL Reader's readTree: Fix bug with github.com URLs. diff --git a/.changeset/eleven-lamps-hide.md b/.changeset/eleven-lamps-hide.md deleted file mode 100644 index 809de25c82..0000000000 --- a/.changeset/eleven-lamps-hide.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/integration': patch ---- - -Fix default branch API url for custom hosted Bitbucket server diff --git a/.changeset/empty-coats-film.md b/.changeset/empty-coats-film.md deleted file mode 100644 index 02b0225102..0000000000 --- a/.changeset/empty-coats-film.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@backstage/backend-common': patch -'@backstage/integration': patch -'@backstage/techdocs-common': patch -'@backstage/plugin-catalog': patch -'@backstage/plugin-catalog-backend': patch -'@backstage/plugin-catalog-import': patch -'@backstage/plugin-scaffolder': patch -'@backstage/plugin-scaffolder-backend': patch ---- - -Upgrade [git-url-parse](https://www.npmjs.com/package/git-url-parse) to [v11.4.4](https://github.com/IonicaBizau/git-url-parse/pull/125) which fixes parsing an Azure DevOps branch ref. diff --git a/.changeset/fast-flowers-tickle.md b/.changeset/fast-flowers-tickle.md deleted file mode 100644 index 3eacb6f1a0..0000000000 --- a/.changeset/fast-flowers-tickle.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -'@backstage/plugin-kafka': minor -'@backstage/plugin-kafka-backend': minor ---- - -Added support for multiple Kafka clusters and multiple consumers per component. -Note that this introduces several breaking changes. - -1. Configuration in `app-config.yaml` has changed to support the ability to configure multiple clusters. This means you are required to update the configs in the following way: - -```diff -kafka: - clientId: backstage -- brokers: -- - localhost:9092 -+ clusters: -+ - name: prod -+ brokers: -+ - localhost:9092 -``` - -2. Configuration of services has changed as well to support multiple clusters: - -```diff - annotations: -- kafka.apache.org/consumer-groups: consumer -+ kafka.apache.org/consumer-groups: prod/consumer -``` - -3. Kafka Backend API has changed, so querying offsets of a consumer group is now done with the following query path: - `/consumers/${clusterId}/${consumerGroup}/offsets` diff --git a/.changeset/five-games-grin.md b/.changeset/five-games-grin.md deleted file mode 100644 index 492c92880c..0000000000 --- a/.changeset/five-games-grin.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -Add `--lax` option to `config:print` and `config:check`, which causes all environment variables to be assumed to be set. diff --git a/.changeset/forty-jobs-occur.md b/.changeset/forty-jobs-occur.md deleted file mode 100644 index b1620a7535..0000000000 --- a/.changeset/forty-jobs-occur.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -'@backstage/plugin-catalog': patch ---- - -Add `EntityRefLinks` that shows one or multiple entity ref links. - -Change the about card and catalog table to use `EntityRefLinks` due to the -nature of relations to support multiple relations per type. diff --git a/.changeset/fuzzy-points-whisper.md b/.changeset/fuzzy-points-whisper.md deleted file mode 100644 index d1af2b2dcf..0000000000 --- a/.changeset/fuzzy-points-whisper.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-auth-backend': patch ---- - -Fixed serialization issue with caching of public keys in AWS ALB auth provider diff --git a/.changeset/green-boats-attend.md b/.changeset/green-boats-attend.md deleted file mode 100644 index ae2324291e..0000000000 --- a/.changeset/green-boats-attend.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': patch ---- - -Support supplying a custom catalog descriptor file parser diff --git a/.changeset/happy-crabs-punch.md b/.changeset/happy-crabs-punch.md deleted file mode 100644 index 4c68dbea1a..0000000000 --- a/.changeset/happy-crabs-punch.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@backstage/config-loader': patch ---- - -Added support for environment variable substitutions in string configuration values using a `${VAR}` placeholder. All environment variables must be available, or the entire expression will be evaluated to `undefined`. To escape a substitution, use `$${...}`, which will end up as `${...}`. - -For example: - -```yaml -app: - baseUrl: https://${BASE_HOST} -``` diff --git a/.changeset/healthy-cameras-suffer.md b/.changeset/healthy-cameras-suffer.md deleted file mode 100644 index 13d74011dc..0000000000 --- a/.changeset/healthy-cameras-suffer.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-kubernetes': patch ---- - -Improve error reporting for plugin misconfiguration. diff --git a/.changeset/honest-jokes-rush.md b/.changeset/honest-jokes-rush.md deleted file mode 100644 index 31ec6202da..0000000000 --- a/.changeset/honest-jokes-rush.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-auth-backend': patch ---- - -Use .text instead of .json for ALB key response diff --git a/.changeset/loud-terms-kiss.md b/.changeset/loud-terms-kiss.md deleted file mode 100644 index da50e56e3a..0000000000 --- a/.changeset/loud-terms-kiss.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-sonarqube': patch ---- - -Ask the SonarQube server for all support metrics prior to querying them for a project. diff --git a/.changeset/lucky-guests-mate.md b/.changeset/lucky-guests-mate.md deleted file mode 100644 index fd733aac75..0000000000 --- a/.changeset/lucky-guests-mate.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/config-loader': minor ---- - -Removed support for the deprecated `$data` placeholder. diff --git a/.changeset/stale-tools-confess.md b/.changeset/stale-tools-confess.md deleted file mode 100644 index 10f2203010..0000000000 --- a/.changeset/stale-tools-confess.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-auth-backend': patch ---- - -Fix AWS ALB issuer check diff --git a/.changeset/techdocs-dirty-rivers-hope.md b/.changeset/techdocs-dirty-rivers-hope.md deleted file mode 100644 index 3f49afa529..0000000000 --- a/.changeset/techdocs-dirty-rivers-hope.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/techdocs-common': patch ---- - -Add rate limiter for concurrent execution of file uploads in AWS and Google publishers diff --git a/.changeset/tender-parrots-itch.md b/.changeset/tender-parrots-itch.md deleted file mode 100644 index 00757b9b22..0000000000 --- a/.changeset/tender-parrots-itch.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/config-loader': minor ---- - -Enable further processing of configuration files included using the `$include` placeholder. Meaning that for example for example `$env` includes will be processed as usual in included files. diff --git a/packages/app/CHANGELOG.md b/packages/app/CHANGELOG.md index d6157f560f..d39f705546 100644 --- a/packages/app/CHANGELOG.md +++ b/packages/app/CHANGELOG.md @@ -1,5 +1,26 @@ # example-app +## 0.2.13 + +### Patch Changes + +- Updated dependencies [681111228] +- Updated dependencies [12a56cdfe] +- Updated dependencies [8b7ef9f8b] +- Updated dependencies [fac91bcc5] +- Updated dependencies [9dd057662] +- Updated dependencies [234e7d985] +- Updated dependencies [ef7957be4] +- Updated dependencies [0b1182346] +- Updated dependencies [a6e3b9596] + - @backstage/plugin-kubernetes@0.3.7 + - @backstage/cli@0.5.0 + - @backstage/plugin-cost-insights@0.6.0 + - @backstage/plugin-catalog@0.2.14 + - @backstage/plugin-catalog-import@0.3.6 + - @backstage/plugin-scaffolder@0.4.1 + - @backstage/plugin-kafka@0.2.0 + ## 0.2.12 ### Patch Changes diff --git a/packages/app/package.json b/packages/app/package.json index 7734dfc244..36fe7651e6 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,18 +1,18 @@ { "name": "example-app", - "version": "0.2.12", + "version": "0.2.13", "private": true, "bundled": true, "dependencies": { "@backstage/catalog-model": "^0.7.0", - "@backstage/cli": "^0.4.7", + "@backstage/cli": "^0.5.0", "@backstage/core": "^0.5.0", "@backstage/plugin-api-docs": "^0.4.3", - "@backstage/plugin-catalog": "^0.2.12", - "@backstage/plugin-catalog-import": "^0.3.5", + "@backstage/plugin-catalog": "^0.2.14", + "@backstage/plugin-catalog-import": "^0.3.6", "@backstage/plugin-circleci": "^0.2.6", "@backstage/plugin-cloudbuild": "^0.2.7", - "@backstage/plugin-cost-insights": "^0.5.7", + "@backstage/plugin-cost-insights": "^0.6.0", "@backstage/plugin-explore": "^0.2.3", "@backstage/plugin-gcp-projects": "^0.2.3", "@backstage/plugin-github-actions": "^0.3.0", @@ -20,14 +20,14 @@ "@backstage/plugin-graphiql": "^0.2.6", "@backstage/plugin-org": "^0.3.4", "@backstage/plugin-jenkins": "^0.3.6", - "@backstage/plugin-kafka": "^0.1.1", - "@backstage/plugin-kubernetes": "^0.3.6", + "@backstage/plugin-kafka": "^0.2.0", + "@backstage/plugin-kubernetes": "^0.3.7", "@backstage/plugin-lighthouse": "^0.2.8", "@backstage/plugin-newrelic": "^0.2.3", "@backstage/plugin-pagerduty": "0.2.6", "@backstage/plugin-register-component": "^0.2.7", "@backstage/plugin-rollbar": "^0.2.8", - "@backstage/plugin-scaffolder": "^0.4.0", + "@backstage/plugin-scaffolder": "^0.4.1", "@backstage/plugin-sentry": "^0.3.3", "@backstage/plugin-search": "^0.2.6", "@backstage/plugin-tech-radar": "^0.3.3", diff --git a/packages/backend-common/CHANGELOG.md b/packages/backend-common/CHANGELOG.md index 84265c9897..3cb57f9056 100644 --- a/packages/backend-common/CHANGELOG.md +++ b/packages/backend-common/CHANGELOG.md @@ -1,5 +1,35 @@ # @backstage/backend-common +## 0.5.1 + +### Patch Changes + +- 26a3a6cf0: Honor the branch ref in the url when cloning. + + This fixes a bug in the scaffolder prepare stage where a non-default branch + was specified in the scaffolder URL but the default branch was cloned. + For example, even though the `other` branch is specified in this example, the + `master` branch was actually cloned: + + ```yaml + catalog: + locations: + - type: url + target: https://github.com/backstage/backstage/blob/other/plugins/scaffolder-backend/sample-templates/docs-template/template.yaml + ``` + + This also fixes a 404 in the prepare stage for GitLab URLs. + +- 664dd08c9: URL Reader's readTree: Fix bug with github.com URLs. +- 9dd057662: Upgrade [git-url-parse](https://www.npmjs.com/package/git-url-parse) to [v11.4.4](https://github.com/IonicaBizau/git-url-parse/pull/125) which fixes parsing an Azure DevOps branch ref. +- Updated dependencies [6800da78d] +- Updated dependencies [9dd057662] +- Updated dependencies [ef7957be4] +- Updated dependencies [ef7957be4] +- Updated dependencies [ef7957be4] + - @backstage/integration@0.3.1 + - @backstage/config-loader@0.5.0 + ## 0.5.0 ### Minor Changes diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index e556a75062..78077c2ce7 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.5.0", + "version": "0.5.1", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -31,8 +31,8 @@ "dependencies": { "@backstage/cli-common": "^0.1.1", "@backstage/config": "^0.1.2", - "@backstage/config-loader": "^0.4.1", - "@backstage/integration": "^0.3.0", + "@backstage/config-loader": "^0.5.0", + "@backstage/integration": "^0.3.1", "@types/cors": "^2.8.6", "@types/express": "^4.17.6", "archiver": "^5.0.2", @@ -66,7 +66,7 @@ } }, "devDependencies": { - "@backstage/cli": "^0.4.7", + "@backstage/cli": "^0.5.0", "@backstage/test-utils": "^0.1.5", "@types/archiver": "^5.1.0", "@types/compression": "^1.7.0", diff --git a/packages/backend/CHANGELOG.md b/packages/backend/CHANGELOG.md index 1d840d8761..215e4e7296 100644 --- a/packages/backend/CHANGELOG.md +++ b/packages/backend/CHANGELOG.md @@ -1,5 +1,27 @@ # example-backend +## 0.2.13 + +### Patch Changes + +- Updated dependencies [26a3a6cf0] +- Updated dependencies [681111228] +- Updated dependencies [664dd08c9] +- Updated dependencies [9dd057662] +- Updated dependencies [234e7d985] +- Updated dependencies [d7b1d317f] +- Updated dependencies [a91aa6bf2] +- Updated dependencies [39b05b9ae] +- Updated dependencies [4eaa06057] + - @backstage/backend-common@0.5.1 + - @backstage/plugin-scaffolder-backend@0.5.2 + - @backstage/plugin-kubernetes-backend@0.2.6 + - @backstage/plugin-catalog-backend@0.5.5 + - @backstage/plugin-kafka-backend@0.2.0 + - @backstage/plugin-auth-backend@0.2.12 + - example-app@0.2.13 + - @backstage/plugin-app-backend@0.3.5 + ## 0.2.12 ### Patch Changes diff --git a/packages/backend/package.json b/packages/backend/package.json index 269089a400..aa00407952 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,6 +1,6 @@ { "name": "example-backend", - "version": "0.2.12", + "version": "0.2.13", "main": "dist/index.cjs.js", "types": "src/index.ts", "license": "Apache-2.0", @@ -27,24 +27,24 @@ "migrate:create": "knex migrate:make -x ts" }, "dependencies": { - "@backstage/backend-common": "^0.5.0", + "@backstage/backend-common": "^0.5.1", "@backstage/catalog-model": "^0.7.0", "@backstage/config": "^0.1.2", - "@backstage/plugin-app-backend": "^0.3.4", - "@backstage/plugin-auth-backend": "^0.2.11", - "@backstage/plugin-catalog-backend": "^0.5.4", + "@backstage/plugin-app-backend": "^0.3.5", + "@backstage/plugin-auth-backend": "^0.2.12", + "@backstage/plugin-catalog-backend": "^0.5.5", "@backstage/plugin-graphql-backend": "^0.1.5", - "@backstage/plugin-kubernetes-backend": "^0.2.5", - "@backstage/plugin-kafka-backend": "^0.1.1", + "@backstage/plugin-kubernetes-backend": "^0.2.6", + "@backstage/plugin-kafka-backend": "^0.2.0", "@backstage/plugin-proxy-backend": "^0.2.4", "@backstage/plugin-rollbar-backend": "^0.1.7", - "@backstage/plugin-scaffolder-backend": "^0.5.0", + "@backstage/plugin-scaffolder-backend": "^0.5.2", "@backstage/plugin-techdocs-backend": "^0.5.4", "@gitbeaker/node": "^28.0.2", "@octokit/rest": "^18.0.12", "azure-devops-node-api": "^10.1.1", "dockerode": "^3.2.1", - "example-app": "^0.2.12", + "example-app": "^0.2.13", "express": "^4.17.1", "express-promise-router": "^3.0.3", "knex": "^0.21.6", @@ -54,7 +54,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.4.7", + "@backstage/cli": "^0.5.0", "@types/dockerode": "^3.2.1", "@types/express": "^4.17.6", "@types/express-serve-static-core": "^4.17.5" diff --git a/packages/catalog-client/package.json b/packages/catalog-client/package.json index 0b68446463..c1cbd6157f 100644 --- a/packages/catalog-client/package.json +++ b/packages/catalog-client/package.json @@ -34,7 +34,7 @@ "cross-fetch": "^3.0.6" }, "devDependencies": { - "@backstage/cli": "^0.4.7", + "@backstage/cli": "^0.5.0", "@types/jest": "^26.0.7", "msw": "^0.21.2" }, diff --git a/packages/catalog-model/package.json b/packages/catalog-model/package.json index e03ce8c465..0b05f489a1 100644 --- a/packages/catalog-model/package.json +++ b/packages/catalog-model/package.json @@ -38,7 +38,7 @@ "yup": "^0.29.3" }, "devDependencies": { - "@backstage/cli": "^0.4.7", + "@backstage/cli": "^0.5.0", "@types/express": "^4.17.6", "@types/jest": "^26.0.7", "@types/lodash": "^4.14.151", diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 7998d408e0..29cce97ae9 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,32 @@ # @backstage/cli +## 0.5.0 + +### Minor Changes + +- 12a56cdfe: We've bumped the `@eslint-typescript` packages to the latest, which now add some additional rules that might cause lint failures. + The main one which could become an issue is the [no-use-before-define](https://eslint.org/docs/rules/no-use-before-define) rule. + + Every plugin and app has the ability to override these rules if you want to ignore them for now. + + You can reset back to the default behaviour by using the following in your own `.eslint.js` + + ```js + rules: { + 'no-use-before-define': 'off' + } + ``` + + Because of the nature of this change, we're unable to provide a grace period for the update :( + +### Patch Changes + +- ef7957be4: Add `--lax` option to `config:print` and `config:check`, which causes all environment variables to be assumed to be set. +- Updated dependencies [ef7957be4] +- Updated dependencies [ef7957be4] +- Updated dependencies [ef7957be4] + - @backstage/config-loader@0.5.0 + ## 0.4.7 ### Patch Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index a98534ecb6..ad45a33d93 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.4.7", + "version": "0.5.0", "private": false, "publishConfig": { "access": "public" @@ -30,7 +30,7 @@ "dependencies": { "@backstage/cli-common": "^0.1.1", "@backstage/config": "^0.1.2", - "@backstage/config-loader": "^0.4.1", + "@backstage/config-loader": "^0.5.0", "@hot-loader/react-dom": "^16.13.0", "@lerna/package-graph": "^3.18.5", "@lerna/project": "^3.18.0", @@ -113,7 +113,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/backend-common": "^0.5.0", + "@backstage/backend-common": "^0.5.1", "@backstage/config": "^0.1.2", "@backstage/core": "^0.5.0", "@backstage/dev-utils": "^0.1.8", diff --git a/packages/config-loader/CHANGELOG.md b/packages/config-loader/CHANGELOG.md index a5e34ad91e..2079c85096 100644 --- a/packages/config-loader/CHANGELOG.md +++ b/packages/config-loader/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/config-loader +## 0.5.0 + +### Minor Changes + +- ef7957be4: Removed support for the deprecated `$data` placeholder. +- ef7957be4: Enable further processing of configuration files included using the `$include` placeholder. Meaning that for example for example `$env` includes will be processed as usual in included files. + +### Patch Changes + +- ef7957be4: Added support for environment variable substitutions in string configuration values using a `${VAR}` placeholder. All environment variables must be available, or the entire expression will be evaluated to `undefined`. To escape a substitution, use `${...}`, which will end up as `${...}`. + + For example: + + ```yaml + app: + baseUrl: https://${BASE_HOST} + ``` + ## 0.4.1 ### Patch Changes diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json index 476c57b014..45ceb36e19 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.4.1", + "version": "0.5.0", "private": false, "publishConfig": { "access": "public", diff --git a/packages/core-api/package.json b/packages/core-api/package.json index 85f9d3a921..bbea58e846 100644 --- a/packages/core-api/package.json +++ b/packages/core-api/package.json @@ -42,7 +42,7 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.4.5", + "@backstage/cli": "^0.5.0", "@backstage/test-utils": "^0.1.6", "@backstage/test-utils-core": "^0.1.1", "@testing-library/jest-dom": "^5.10.1", diff --git a/packages/core/package.json b/packages/core/package.json index a937502247..325689e179 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -65,7 +65,7 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.4.7", + "@backstage/cli": "^0.5.0", "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", diff --git a/packages/create-app/CHANGELOG.md b/packages/create-app/CHANGELOG.md index e33d8cc633..d2faa14876 100644 --- a/packages/create-app/CHANGELOG.md +++ b/packages/create-app/CHANGELOG.md @@ -1,5 +1,45 @@ # @backstage/create-app +## 1.0.0 + +### Patch Changes + +- Updated dependencies [26a3a6cf0] +- Updated dependencies [12a56cdfe] +- Updated dependencies [664dd08c9] +- Updated dependencies [9dd057662] +- Updated dependencies [ef7957be4] +- Updated dependencies [0b1182346] +- Updated dependencies [d7b1d317f] +- Updated dependencies [a91aa6bf2] +- Updated dependencies [39b05b9ae] +- Updated dependencies [4eaa06057] + - @backstage/backend-common@0.5.1 + - @backstage/plugin-scaffolder-backend@0.5.2 + - @backstage/cli@0.5.0 + - @backstage/plugin-catalog@0.2.14 + - @backstage/plugin-catalog-backend@0.5.5 + - @backstage/plugin-catalog-import@0.3.6 + - @backstage/plugin-scaffolder@0.4.1 + - @backstage/plugin-auth-backend@0.2.12 + - @backstage/catalog-model@0.7.0 + - @backstage/core@0.5.0 + - @backstage/test-utils@0.1.6 + - @backstage/theme@0.2.2 + - @backstage/plugin-api-docs@0.4.3 + - @backstage/plugin-app-backend@0.3.5 + - @backstage/plugin-circleci@0.2.6 + - @backstage/plugin-explore@0.2.3 + - @backstage/plugin-github-actions@0.3.0 + - @backstage/plugin-lighthouse@0.2.8 + - @backstage/plugin-proxy-backend@0.2.4 + - @backstage/plugin-rollbar-backend@0.1.7 + - @backstage/plugin-search@0.2.6 + - @backstage/plugin-tech-radar@0.3.3 + - @backstage/plugin-techdocs@0.5.4 + - @backstage/plugin-techdocs-backend@0.5.4 + - @backstage/plugin-user-settings@0.2.4 + ## 0.3.6 ### Patch Changes diff --git a/packages/create-app/package.json b/packages/create-app/package.json index 60dd150566..448b320346 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.3.6", + "version": "1.0.0", "private": false, "publishConfig": { "access": "public" @@ -44,26 +44,26 @@ "ts-node": "^8.6.2" }, "peerDependencies": { - "@backstage/backend-common": "^0.5.0", + "@backstage/backend-common": "^0.5.1", "@backstage/catalog-model": "^0.7.0", - "@backstage/cli": "^0.4.7", + "@backstage/cli": "^0.5.0", "@backstage/config": "^0.1.2", "@backstage/core": "^0.5.0", "@backstage/plugin-api-docs": "^0.4.3", - "@backstage/plugin-app-backend": "^0.3.4", - "@backstage/plugin-auth-backend": "^0.2.11", - "@backstage/plugin-catalog": "^0.2.13", - "@backstage/plugin-catalog-backend": "^0.5.4", - "@backstage/plugin-catalog-import": "^0.3.5", + "@backstage/plugin-app-backend": "^0.3.5", + "@backstage/plugin-auth-backend": "^0.2.12", + "@backstage/plugin-catalog": "^0.2.14", + "@backstage/plugin-catalog-backend": "^0.5.5", + "@backstage/plugin-catalog-import": "^0.3.6", "@backstage/plugin-circleci": "^0.2.6", "@backstage/plugin-explore": "^0.2.3", "@backstage/plugin-github-actions": "^0.3.0", "@backstage/plugin-lighthouse": "^0.2.8", "@backstage/plugin-proxy-backend": "^0.2.4", "@backstage/plugin-rollbar-backend": "^0.1.7", - "@backstage/plugin-scaffolder": "^0.4.0", + "@backstage/plugin-scaffolder": "^0.4.1", "@backstage/plugin-search": "^0.2.6", - "@backstage/plugin-scaffolder-backend": "^0.5.1", + "@backstage/plugin-scaffolder-backend": "^0.5.2", "@backstage/plugin-tech-radar": "^0.3.3", "@backstage/plugin-techdocs": "^0.5.4", "@backstage/plugin-techdocs-backend": "^0.5.4", diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index 629bc7c87e..93a5247c82 100644 --- a/packages/dev-utils/package.json +++ b/packages/dev-utils/package.json @@ -31,7 +31,7 @@ "dependencies": { "@backstage/core": "^0.5.0", "@backstage/catalog-model": "^0.7.0", - "@backstage/plugin-catalog": "^0.2.12", + "@backstage/plugin-catalog": "^0.2.14", "@backstage/test-utils": "^0.1.5", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", @@ -47,7 +47,7 @@ "react-router-dom": "6.0.0-beta.0" }, "devDependencies": { - "@backstage/cli": "^0.4.7", + "@backstage/cli": "^0.5.0", "@types/jest": "^26.0.7", "@types/node": "^12.0.0" }, diff --git a/packages/integration/CHANGELOG.md b/packages/integration/CHANGELOG.md index 804fc0c63e..11eb164535 100644 --- a/packages/integration/CHANGELOG.md +++ b/packages/integration/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/integration +## 0.3.1 + +### Patch Changes + +- 6800da78d: Fix default branch API url for custom hosted Bitbucket server +- 9dd057662: Upgrade [git-url-parse](https://www.npmjs.com/package/git-url-parse) to [v11.4.4](https://github.com/IonicaBizau/git-url-parse/pull/125) which fixes parsing an Azure DevOps branch ref. + ## 0.3.0 ### Minor Changes diff --git a/packages/integration/package.json b/packages/integration/package.json index 6c96a8562b..06a83ea883 100644 --- a/packages/integration/package.json +++ b/packages/integration/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/integration", - "version": "0.3.0", + "version": "0.3.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -37,7 +37,7 @@ "luxon": "^1.25.0" }, "devDependencies": { - "@backstage/cli": "^0.4.7", + "@backstage/cli": "^0.5.0", "@backstage/test-utils": "^0.1.5", "@types/jest": "^26.0.7", "@types/luxon": "^1.25.0", diff --git a/packages/techdocs-common/CHANGELOG.md b/packages/techdocs-common/CHANGELOG.md index 7bff39d3c4..568df3157c 100644 --- a/packages/techdocs-common/CHANGELOG.md +++ b/packages/techdocs-common/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/techdocs-common +## 0.3.6 + +### Patch Changes + +- 9dd057662: Upgrade [git-url-parse](https://www.npmjs.com/package/git-url-parse) to [v11.4.4](https://github.com/IonicaBizau/git-url-parse/pull/125) which fixes parsing an Azure DevOps branch ref. +- db2328c88: Add rate limiter for concurrent execution of file uploads in AWS and Google publishers +- Updated dependencies [26a3a6cf0] +- Updated dependencies [664dd08c9] +- Updated dependencies [6800da78d] +- Updated dependencies [9dd057662] + - @backstage/backend-common@0.5.1 + - @backstage/integration@0.3.1 + ## 0.3.5 ### Patch Changes diff --git a/packages/techdocs-common/package.json b/packages/techdocs-common/package.json index dd4e4d8c35..64c5c19425 100644 --- a/packages/techdocs-common/package.json +++ b/packages/techdocs-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/techdocs-common", "description": "Common functionalities for TechDocs, to be shared between techdocs-backend plugin and techdocs-cli", - "version": "0.3.5", + "version": "0.3.6", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -37,10 +37,10 @@ }, "dependencies": { "@aws-sdk/client-s3": "^3.1.0", - "@backstage/backend-common": "^0.5.0", + "@backstage/backend-common": "^0.5.1", "@backstage/catalog-model": "^0.7.0", "@backstage/config": "^0.1.2", - "@backstage/integration": "^0.3.0", + "@backstage/integration": "^0.3.1", "@google-cloud/storage": "^5.6.0", "@types/dockerode": "^3.2.1", "@types/express": "^4.17.6", @@ -59,7 +59,7 @@ }, "devDependencies": { "@aws-sdk/types": "3.1.0", - "@backstage/cli": "^0.4.7", + "@backstage/cli": "^0.5.0", "@types/fs-extra": "^9.0.5", "@types/git-url-parse": "^9.0.0", "@types/js-yaml": "^3.12.5", diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index 8c5a7c1f39..10f0e89c14 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -45,7 +45,7 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.4.3", + "@backstage/cli": "^0.5.0", "@types/jest": "^26.0.7", "@types/node": "^12.0.0" }, diff --git a/packages/theme/package.json b/packages/theme/package.json index 54f43a1443..24edf81bb6 100644 --- a/packages/theme/package.json +++ b/packages/theme/package.json @@ -31,7 +31,7 @@ "@material-ui/core": "^4.11.0" }, "devDependencies": { - "@backstage/cli": "^0.4.1" + "@backstage/cli": "^0.5.0" }, "files": [ "dist" diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index d9c0dc0968..5e3c42bfdf 100644 --- a/plugins/api-docs/package.json +++ b/plugins/api-docs/package.json @@ -32,7 +32,7 @@ "@asyncapi/react-component": "^0.18.2", "@backstage/catalog-model": "^0.7.0", "@backstage/core": "^0.5.0", - "@backstage/plugin-catalog": "^0.2.12", + "@backstage/plugin-catalog": "^0.2.14", "@backstage/theme": "^0.2.2", "@material-icons/font": "^1.0.2", "@material-ui/core": "^4.11.0", @@ -49,7 +49,7 @@ "swagger-ui-react": "^3.37.2" }, "devDependencies": { - "@backstage/cli": "^0.4.7", + "@backstage/cli": "^0.5.0", "@backstage/dev-utils": "^0.1.8", "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/app-backend/CHANGELOG.md b/plugins/app-backend/CHANGELOG.md index 90a4eac95d..514e32633a 100644 --- a/plugins/app-backend/CHANGELOG.md +++ b/plugins/app-backend/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-app-backend +## 0.3.5 + +### Patch Changes + +- Updated dependencies [26a3a6cf0] +- Updated dependencies [664dd08c9] +- Updated dependencies [9dd057662] +- Updated dependencies [ef7957be4] +- Updated dependencies [ef7957be4] +- Updated dependencies [ef7957be4] + - @backstage/backend-common@0.5.1 + - @backstage/config-loader@0.5.0 + ## 0.3.4 ### Patch Changes diff --git a/plugins/app-backend/package.json b/plugins/app-backend/package.json index f55da9042a..f397ea1475 100644 --- a/plugins/app-backend/package.json +++ b/plugins/app-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-app-backend", - "version": "0.3.4", + "version": "0.3.5", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,8 +29,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.5.0", - "@backstage/config-loader": "^0.4.0", + "@backstage/backend-common": "^0.5.1", + "@backstage/config-loader": "^0.5.0", "@backstage/config": "^0.1.2", "@types/express": "^4.17.6", "express": "^4.17.1", @@ -40,7 +40,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.4.7", + "@backstage/cli": "^0.5.0", "@types/supertest": "^2.0.8", "msw": "^0.20.5", "supertest": "^4.0.2" diff --git a/plugins/auth-backend/CHANGELOG.md b/plugins/auth-backend/CHANGELOG.md index 0df0fa1938..e3ff6aaa43 100644 --- a/plugins/auth-backend/CHANGELOG.md +++ b/plugins/auth-backend/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-auth-backend +## 0.2.12 + +### Patch Changes + +- d7b1d317f: Fixed serialization issue with caching of public keys in AWS ALB auth provider +- 39b05b9ae: Use .text instead of .json for ALB key response +- 4eaa06057: Fix AWS ALB issuer check +- Updated dependencies [26a3a6cf0] +- Updated dependencies [664dd08c9] +- Updated dependencies [9dd057662] + - @backstage/backend-common@0.5.1 + ## 0.2.11 ### Patch Changes diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 27cb725faa..b2198165a5 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend", - "version": "0.2.11", + "version": "0.2.12", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,7 +29,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.5.0", + "@backstage/backend-common": "^0.5.1", "@backstage/catalog-client": "^0.3.5", "@backstage/catalog-model": "^0.7.0", "@backstage/config": "^0.1.2", @@ -65,7 +65,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.4.7", + "@backstage/cli": "^0.5.0", "@types/body-parser": "^1.19.0", "@types/cookie-parser": "^1.4.2", "@types/express-session": "^1.17.2", diff --git a/plugins/catalog-backend/CHANGELOG.md b/plugins/catalog-backend/CHANGELOG.md index 77620c1cea..0e446bf0f7 100644 --- a/plugins/catalog-backend/CHANGELOG.md +++ b/plugins/catalog-backend/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-catalog-backend +## 0.5.5 + +### Patch Changes + +- 9dd057662: Upgrade [git-url-parse](https://www.npmjs.com/package/git-url-parse) to [v11.4.4](https://github.com/IonicaBizau/git-url-parse/pull/125) which fixes parsing an Azure DevOps branch ref. +- a91aa6bf2: Support supplying a custom catalog descriptor file parser +- Updated dependencies [26a3a6cf0] +- Updated dependencies [664dd08c9] +- Updated dependencies [9dd057662] + - @backstage/backend-common@0.5.1 + ## 0.5.4 ### Patch Changes diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index eaf1f69938..264bda0556 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend", - "version": "0.5.4", + "version": "0.5.5", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,7 +31,7 @@ "dependencies": { "@aws-sdk/client-organizations": "^3.2.0", "@azure/msal-node": "^1.0.0-beta.3", - "@backstage/backend-common": "^0.5.0", + "@backstage/backend-common": "^0.5.1", "@backstage/catalog-model": "^0.7.0", "@backstage/config": "^0.1.2", "@octokit/graphql": "^4.5.8", @@ -57,7 +57,7 @@ "yup": "^0.29.3" }, "devDependencies": { - "@backstage/cli": "^0.4.7", + "@backstage/cli": "^0.5.0", "@backstage/test-utils": "^0.1.6", "@types/core-js": "^2.5.4", "@types/git-url-parse": "^9.0.0", diff --git a/plugins/catalog-graphql/package.json b/plugins/catalog-graphql/package.json index 4533a541f0..c81c29fc3e 100644 --- a/plugins/catalog-graphql/package.json +++ b/plugins/catalog-graphql/package.json @@ -30,7 +30,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.5.0", + "@backstage/backend-common": "^0.5.1", "@backstage/catalog-model": "^0.7.0", "@backstage/config": "^0.1.2", "@graphql-modules/core": "^0.7.17", @@ -42,7 +42,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.4.7", + "@backstage/cli": "^0.5.0", "@backstage/test-utils": "^0.1.5", "@graphql-codegen/cli": "^1.17.7", "@graphql-codegen/typescript": "^1.17.7", diff --git a/plugins/catalog-import/CHANGELOG.md b/plugins/catalog-import/CHANGELOG.md index 2d75ae6af4..c6e4995c70 100644 --- a/plugins/catalog-import/CHANGELOG.md +++ b/plugins/catalog-import/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-catalog-import +## 0.3.6 + +### Patch Changes + +- 9dd057662: Upgrade [git-url-parse](https://www.npmjs.com/package/git-url-parse) to [v11.4.4](https://github.com/IonicaBizau/git-url-parse/pull/125) which fixes parsing an Azure DevOps branch ref. +- Updated dependencies [6800da78d] +- Updated dependencies [9dd057662] +- Updated dependencies [0b1182346] + - @backstage/integration@0.3.1 + - @backstage/plugin-catalog@0.2.14 + ## 0.3.5 ### Patch Changes diff --git a/plugins/catalog-import/package.json b/plugins/catalog-import/package.json index 2f9784a23e..5a4dbf8798 100644 --- a/plugins/catalog-import/package.json +++ b/plugins/catalog-import/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-import", - "version": "0.3.5", + "version": "0.3.6", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,8 +32,8 @@ "dependencies": { "@backstage/catalog-model": "^0.7.0", "@backstage/core": "^0.5.0", - "@backstage/integration": "^0.3.0", - "@backstage/plugin-catalog": "^0.2.12", + "@backstage/integration": "^0.3.1", + "@backstage/plugin-catalog": "^0.2.14", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -49,7 +49,7 @@ "yaml": "^1.10.0" }, "devDependencies": { - "@backstage/cli": "^0.4.7", + "@backstage/cli": "^0.5.0", "@backstage/dev-utils": "^0.1.8", "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/catalog/CHANGELOG.md b/plugins/catalog/CHANGELOG.md index efacf6fbad..3920d8696d 100644 --- a/plugins/catalog/CHANGELOG.md +++ b/plugins/catalog/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-catalog +## 0.2.14 + +### Patch Changes + +- 9dd057662: Upgrade [git-url-parse](https://www.npmjs.com/package/git-url-parse) to [v11.4.4](https://github.com/IonicaBizau/git-url-parse/pull/125) which fixes parsing an Azure DevOps branch ref. +- 0b1182346: Add `EntityRefLinks` that shows one or multiple entity ref links. + + Change the about card and catalog table to use `EntityRefLinks` due to the + nature of relations to support multiple relations per type. + +- Updated dependencies [9dd057662] + - @backstage/plugin-scaffolder@0.4.1 + ## 0.2.13 ### Patch Changes diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index 0887930726..90bdd2f3c7 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog", - "version": "0.2.13", + "version": "0.2.14", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,7 +33,7 @@ "@backstage/catalog-client": "^0.3.5", "@backstage/catalog-model": "^0.7.0", "@backstage/core": "^0.5.0", - "@backstage/plugin-scaffolder": "^0.4.0", + "@backstage/plugin-scaffolder": "^0.4.1", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -51,7 +51,7 @@ "swr": "^0.3.0" }, "devDependencies": { - "@backstage/cli": "^0.4.7", + "@backstage/cli": "^0.5.0", "@backstage/dev-utils": "^0.1.8", "@backstage/test-utils": "^0.1.6", "@microsoft/microsoft-graph-types": "^1.25.0", diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json index fbc6605419..ddae98a566 100644 --- a/plugins/circleci/package.json +++ b/plugins/circleci/package.json @@ -33,7 +33,7 @@ "dependencies": { "@backstage/catalog-model": "^0.7.0", "@backstage/core": "^0.5.0", - "@backstage/plugin-catalog": "^0.2.12", + "@backstage/plugin-catalog": "^0.2.14", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -50,7 +50,7 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.4.7", + "@backstage/cli": "^0.5.0", "@backstage/dev-utils": "^0.1.8", "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/cloudbuild/package.json b/plugins/cloudbuild/package.json index 0826ac972a..9a05a652b9 100644 --- a/plugins/cloudbuild/package.json +++ b/plugins/cloudbuild/package.json @@ -32,7 +32,7 @@ "dependencies": { "@backstage/catalog-model": "^0.7.0", "@backstage/core": "^0.5.0", - "@backstage/plugin-catalog": "^0.2.12", + "@backstage/plugin-catalog": "^0.2.14", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -47,7 +47,7 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.4.7", + "@backstage/cli": "^0.5.0", "@backstage/dev-utils": "^0.1.8", "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/cost-insights/CHANGELOG.md b/plugins/cost-insights/CHANGELOG.md index b666c66864..a979f1476b 100644 --- a/plugins/cost-insights/CHANGELOG.md +++ b/plugins/cost-insights/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-cost-insights +## 0.6.0 + +### Minor Changes + +- fac91bcc5: Add support for additional breakdowns of daily cost data. + This changes the type of Cost.groupedCosts returned by CostInsightsApi.getGroupDailyCost. + +### Patch Changes + +- 8b7ef9f8b: Allow expand functionality to top panel product chart tooltip. + ## 0.5.7 ### Patch Changes diff --git a/plugins/cost-insights/package.json b/plugins/cost-insights/package.json index bd573d36de..72a9e92633 100644 --- a/plugins/cost-insights/package.json +++ b/plugins/cost-insights/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-cost-insights", - "version": "0.5.7", + "version": "0.6.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -55,7 +55,7 @@ "yup": "^0.29.3" }, "devDependencies": { - "@backstage/cli": "^0.4.7", + "@backstage/cli": "^0.5.0", "@backstage/dev-utils": "^0.1.8", "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/explore/package.json b/plugins/explore/package.json index e4acc34435..c6668c9c4d 100644 --- a/plugins/explore/package.json +++ b/plugins/explore/package.json @@ -42,7 +42,7 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.4.7", + "@backstage/cli": "^0.5.0", "@backstage/dev-utils": "^0.1.8", "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/fossa/package.json b/plugins/fossa/package.json index 27f6ad0966..b69039fa00 100644 --- a/plugins/fossa/package.json +++ b/plugins/fossa/package.json @@ -43,7 +43,7 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.4.7", + "@backstage/cli": "^0.5.0", "@backstage/dev-utils": "^0.1.8", "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/gcp-projects/package.json b/plugins/gcp-projects/package.json index 71f1d83c99..085e08e2a3 100644 --- a/plugins/gcp-projects/package.json +++ b/plugins/gcp-projects/package.json @@ -41,7 +41,7 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.4.7", + "@backstage/cli": "^0.5.0", "@backstage/dev-utils": "^0.1.8", "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/github-actions/package.json b/plugins/github-actions/package.json index c3b05b0369..7b7cf539f9 100644 --- a/plugins/github-actions/package.json +++ b/plugins/github-actions/package.json @@ -34,8 +34,8 @@ "dependencies": { "@backstage/catalog-model": "^0.7.0", "@backstage/core": "^0.5.0", - "@backstage/integration": "^0.3.0", - "@backstage/plugin-catalog": "^0.2.12", + "@backstage/integration": "^0.3.1", + "@backstage/plugin-catalog": "^0.2.14", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -50,7 +50,7 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.4.7", + "@backstage/cli": "^0.5.0", "@backstage/dev-utils": "^0.1.8", "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/gitops-profiles/package.json b/plugins/gitops-profiles/package.json index 2b98f0179d..447c3cbb81 100644 --- a/plugins/gitops-profiles/package.json +++ b/plugins/gitops-profiles/package.json @@ -42,7 +42,7 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.4.7", + "@backstage/cli": "^0.5.0", "@backstage/dev-utils": "^0.1.8", "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/graphiql/package.json b/plugins/graphiql/package.json index 1b95db6e39..850b29108e 100644 --- a/plugins/graphiql/package.json +++ b/plugins/graphiql/package.json @@ -43,7 +43,7 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.4.7", + "@backstage/cli": "^0.5.0", "@backstage/dev-utils": "^0.1.8", "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/graphql/package.json b/plugins/graphql/package.json index f5214a3210..99d369b1a7 100644 --- a/plugins/graphql/package.json +++ b/plugins/graphql/package.json @@ -29,7 +29,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.5.0", + "@backstage/backend-common": "^0.5.1", "@backstage/config": "^0.1.2", "@backstage/plugin-catalog-graphql": "^0.2.6", "@graphql-modules/core": "^0.7.17", @@ -45,7 +45,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.4.7", + "@backstage/cli": "^0.5.0", "@types/supertest": "^2.0.8", "eslint-plugin-graphql": "^4.0.0", "msw": "^0.20.5", diff --git a/plugins/jenkins/package.json b/plugins/jenkins/package.json index c511553699..a99b9937b1 100644 --- a/plugins/jenkins/package.json +++ b/plugins/jenkins/package.json @@ -33,7 +33,7 @@ "dependencies": { "@backstage/catalog-model": "^0.7.0", "@backstage/core": "^0.5.0", - "@backstage/plugin-catalog": "^0.2.13", + "@backstage/plugin-catalog": "^0.2.14", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -47,7 +47,7 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.4.7", + "@backstage/cli": "^0.5.0", "@backstage/dev-utils": "^0.1.8", "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/kafka-backend/CHANGELOG.md b/plugins/kafka-backend/CHANGELOG.md index f8dbdbd00b..1e4701af16 100644 --- a/plugins/kafka-backend/CHANGELOG.md +++ b/plugins/kafka-backend/CHANGELOG.md @@ -1,5 +1,43 @@ # @backstage/plugin-kafka-backend +## 0.2.0 + +### Minor Changes + +- 234e7d985: Added support for multiple Kafka clusters and multiple consumers per component. + Note that this introduces several breaking changes. + + 1. Configuration in `app-config.yaml` has changed to support the ability to configure multiple clusters. This means you are required to update the configs in the following way: + + ```diff + kafka: + clientId: backstage + - brokers: + - - localhost:9092 + + clusters: + + - name: prod + + brokers: + + - localhost:9092 + ``` + + 2. Configuration of services has changed as well to support multiple clusters: + + ```diff + annotations: + - kafka.apache.org/consumer-groups: consumer + + kafka.apache.org/consumer-groups: prod/consumer + ``` + + 3. Kafka Backend API has changed, so querying offsets of a consumer group is now done with the following query path: + `/consumers/${clusterId}/${consumerGroup}/offsets` + +### Patch Changes + +- Updated dependencies [26a3a6cf0] +- Updated dependencies [664dd08c9] +- Updated dependencies [9dd057662] + - @backstage/backend-common@0.5.1 + ## 0.1.1 ### Patch Changes diff --git a/plugins/kafka-backend/package.json b/plugins/kafka-backend/package.json index 0c7eff2193..311464b47d 100644 --- a/plugins/kafka-backend/package.json +++ b/plugins/kafka-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kafka-backend", - "version": "0.1.1", + "version": "0.2.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,7 +31,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.5.0", + "@backstage/backend-common": "^0.5.1", "@backstage/catalog-model": "^0.7.0", "@backstage/config": "^0.1.2", "@types/express": "^4.17.6", @@ -42,7 +42,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.4.7", + "@backstage/cli": "^0.5.0", "@types/jest-when": "^2.7.2", "@types/lodash": "^4.14.151", "jest-when": "^3.1.0", diff --git a/plugins/kafka/CHANGELOG.md b/plugins/kafka/CHANGELOG.md index 01479f8186..92f49a1d35 100644 --- a/plugins/kafka/CHANGELOG.md +++ b/plugins/kafka/CHANGELOG.md @@ -1,5 +1,42 @@ # @backstage/plugin-kafka +## 0.2.0 + +### Minor Changes + +- 234e7d985: Added support for multiple Kafka clusters and multiple consumers per component. + Note that this introduces several breaking changes. + + 1. Configuration in `app-config.yaml` has changed to support the ability to configure multiple clusters. This means you are required to update the configs in the following way: + + ```diff + kafka: + clientId: backstage + - brokers: + - - localhost:9092 + + clusters: + + - name: prod + + brokers: + + - localhost:9092 + ``` + + 2. Configuration of services has changed as well to support multiple clusters: + + ```diff + annotations: + - kafka.apache.org/consumer-groups: consumer + + kafka.apache.org/consumer-groups: prod/consumer + ``` + + 3. Kafka Backend API has changed, so querying offsets of a consumer group is now done with the following query path: + `/consumers/${clusterId}/${consumerGroup}/offsets` + +### Patch Changes + +- Updated dependencies [9dd057662] +- Updated dependencies [0b1182346] + - @backstage/plugin-catalog@0.2.14 + ## 0.1.1 ### Patch Changes diff --git a/plugins/kafka/package.json b/plugins/kafka/package.json index 306b65a653..eef14439fd 100644 --- a/plugins/kafka/package.json +++ b/plugins/kafka/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kafka", - "version": "0.1.1", + "version": "0.2.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -22,7 +22,7 @@ "dependencies": { "@backstage/catalog-model": "^0.7.0", "@backstage/core": "^0.5.0", - "@backstage/plugin-catalog": "^0.2.12", + "@backstage/plugin-catalog": "^0.2.14", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -33,7 +33,7 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.4.7", + "@backstage/cli": "^0.5.0", "@backstage/dev-utils": "^0.1.8", "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/kubernetes-backend/CHANGELOG.md b/plugins/kubernetes-backend/CHANGELOG.md index 279cb8a5ba..08a3a66657 100644 --- a/plugins/kubernetes-backend/CHANGELOG.md +++ b/plugins/kubernetes-backend/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-kubernetes-backend +## 0.2.6 + +### Patch Changes + +- 681111228: Add AWS auth provider for Kubernetes +- Updated dependencies [26a3a6cf0] +- Updated dependencies [664dd08c9] +- Updated dependencies [9dd057662] + - @backstage/backend-common@0.5.1 + ## 0.2.5 ### Patch Changes diff --git a/plugins/kubernetes-backend/package.json b/plugins/kubernetes-backend/package.json index eeb91de777..e69a82b738 100644 --- a/plugins/kubernetes-backend/package.json +++ b/plugins/kubernetes-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes-backend", - "version": "0.2.5", + "version": "0.2.6", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,7 +32,7 @@ }, "dependencies": { "@aws-sdk/credential-provider-node": "^3.3.0", - "@backstage/backend-common": "^0.5.0", + "@backstage/backend-common": "^0.5.1", "@backstage/catalog-model": "^0.7.0", "@backstage/config": "^0.1.2", "@kubernetes/client-node": "^0.13.2", @@ -51,7 +51,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.4.7", + "@backstage/cli": "^0.5.0", "@types/aws4": "^1.5.1", "supertest": "^4.0.2" }, diff --git a/plugins/kubernetes/CHANGELOG.md b/plugins/kubernetes/CHANGELOG.md index c1f09038ba..f16728e1fe 100644 --- a/plugins/kubernetes/CHANGELOG.md +++ b/plugins/kubernetes/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-kubernetes +## 0.3.7 + +### Patch Changes + +- 681111228: Add AWS auth provider for Kubernetes +- a6e3b9596: Improve error reporting for plugin misconfiguration. +- Updated dependencies [681111228] + - @backstage/plugin-kubernetes-backend@0.2.6 + ## 0.3.6 ### Patch Changes diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index 96d700c3bb..1d985c0d10 100644 --- a/plugins/kubernetes/package.json +++ b/plugins/kubernetes/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes", - "version": "0.3.6", + "version": "0.3.7", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,7 +34,7 @@ "@backstage/catalog-model": "^0.7.0", "@backstage/config": "^0.1.2", "@backstage/core": "^0.5.0", - "@backstage/plugin-kubernetes-backend": "^0.2.5", + "@backstage/plugin-kubernetes-backend": "^0.2.6", "@backstage/theme": "^0.2.2", "@kubernetes/client-node": "^0.13.2", "@material-ui/core": "^4.11.0", @@ -47,7 +47,7 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.4.7", + "@backstage/cli": "^0.5.0", "@backstage/dev-utils": "^0.1.8", "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/lighthouse/package.json b/plugins/lighthouse/package.json index 162da40fd0..3463842bc5 100644 --- a/plugins/lighthouse/package.json +++ b/plugins/lighthouse/package.json @@ -34,7 +34,7 @@ "@backstage/catalog-model": "^0.7.0", "@backstage/config": "^0.1.2", "@backstage/core": "^0.5.0", - "@backstage/plugin-catalog": "^0.2.12", + "@backstage/plugin-catalog": "^0.2.14", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -46,7 +46,7 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.4.7", + "@backstage/cli": "^0.5.0", "@backstage/dev-utils": "^0.1.8", "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/newrelic/package.json b/plugins/newrelic/package.json index 57f146521b..2f43c9cbeb 100644 --- a/plugins/newrelic/package.json +++ b/plugins/newrelic/package.json @@ -41,7 +41,7 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.4.7", + "@backstage/cli": "^0.5.0", "@backstage/dev-utils": "^0.1.8", "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/org/package.json b/plugins/org/package.json index 976caffdbb..fbb3bd1afb 100644 --- a/plugins/org/package.json +++ b/plugins/org/package.json @@ -22,7 +22,7 @@ "dependencies": { "@backstage/catalog-model": "^0.7.0", "@backstage/core": "^0.5.0", - "@backstage/plugin-catalog": "^0.2.12", + "@backstage/plugin-catalog": "^0.2.14", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -33,7 +33,7 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.4.7", + "@backstage/cli": "^0.5.0", "@backstage/dev-utils": "^0.1.8", "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/pagerduty/package.json b/plugins/pagerduty/package.json index 174f62d5e7..0e87bdc2d7 100644 --- a/plugins/pagerduty/package.json +++ b/plugins/pagerduty/package.json @@ -44,7 +44,7 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.4.7", + "@backstage/cli": "^0.5.0", "@backstage/dev-utils": "^0.1.8", "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/proxy-backend/package.json b/plugins/proxy-backend/package.json index 94825e8691..ae828cb2ec 100644 --- a/plugins/proxy-backend/package.json +++ b/plugins/proxy-backend/package.json @@ -28,7 +28,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.5.0", + "@backstage/backend-common": "^0.5.1", "@backstage/config": "^0.1.2", "@types/express": "^4.17.6", "express": "^4.17.1", @@ -42,7 +42,7 @@ "yup": "^0.29.3" }, "devDependencies": { - "@backstage/cli": "^0.4.7", + "@backstage/cli": "^0.5.0", "@types/http-proxy-middleware": "^0.19.3", "@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 99f7931c2b..29b629de9b 100644 --- a/plugins/register-component/package.json +++ b/plugins/register-component/package.json @@ -32,7 +32,7 @@ "dependencies": { "@backstage/catalog-model": "^0.7.0", "@backstage/core": "^0.5.0", - "@backstage/plugin-catalog": "^0.2.12", + "@backstage/plugin-catalog": "^0.2.14", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -45,7 +45,7 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.4.7", + "@backstage/cli": "^0.5.0", "@backstage/dev-utils": "^0.1.8", "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/rollbar-backend/package.json b/plugins/rollbar-backend/package.json index 39a2b17369..522f5a3b4c 100644 --- a/plugins/rollbar-backend/package.json +++ b/plugins/rollbar-backend/package.json @@ -30,7 +30,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.5.0", + "@backstage/backend-common": "^0.5.1", "@backstage/config": "^0.1.2", "@types/express": "^4.17.6", "axios": "^0.21.1", @@ -47,7 +47,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.4.7", + "@backstage/cli": "^0.5.0", "@types/supertest": "^2.0.8", "supertest": "^4.0.2" }, diff --git a/plugins/rollbar/package.json b/plugins/rollbar/package.json index c37ca53a82..f3a21709db 100644 --- a/plugins/rollbar/package.json +++ b/plugins/rollbar/package.json @@ -33,7 +33,7 @@ "dependencies": { "@backstage/catalog-model": "^0.7.0", "@backstage/core": "^0.5.0", - "@backstage/plugin-catalog": "^0.2.12", + "@backstage/plugin-catalog": "^0.2.14", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -47,7 +47,7 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.4.7", + "@backstage/cli": "^0.5.0", "@backstage/dev-utils": "^0.1.8", "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/scaffolder-backend/CHANGELOG.md b/plugins/scaffolder-backend/CHANGELOG.md index bcf43cf229..353e2321cf 100644 --- a/plugins/scaffolder-backend/CHANGELOG.md +++ b/plugins/scaffolder-backend/CHANGELOG.md @@ -1,5 +1,33 @@ # @backstage/plugin-scaffolder-backend +## 0.5.2 + +### Patch Changes + +- 26a3a6cf0: Honor the branch ref in the url when cloning. + + This fixes a bug in the scaffolder prepare stage where a non-default branch + was specified in the scaffolder URL but the default branch was cloned. + For example, even though the `other` branch is specified in this example, the + `master` branch was actually cloned: + + ```yaml + catalog: + locations: + - type: url + target: https://github.com/backstage/backstage/blob/other/plugins/scaffolder-backend/sample-templates/docs-template/template.yaml + ``` + + This also fixes a 404 in the prepare stage for GitLab URLs. + +- 9dd057662: Upgrade [git-url-parse](https://www.npmjs.com/package/git-url-parse) to [v11.4.4](https://github.com/IonicaBizau/git-url-parse/pull/125) which fixes parsing an Azure DevOps branch ref. +- Updated dependencies [26a3a6cf0] +- Updated dependencies [664dd08c9] +- Updated dependencies [6800da78d] +- Updated dependencies [9dd057662] + - @backstage/backend-common@0.5.1 + - @backstage/integration@0.3.1 + ## 0.5.1 ### Patch Changes diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index e118a1d7f9..57b62be2cd 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend", - "version": "0.5.1", + "version": "0.5.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,10 +29,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.5.0", + "@backstage/backend-common": "^0.5.1", "@backstage/catalog-model": "^0.7.0", "@backstage/config": "^0.1.2", - "@backstage/integration": "^0.3.0", + "@backstage/integration": "^0.3.1", "@gitbeaker/core": "^28.0.2", "@gitbeaker/node": "^28.0.2", "@octokit/rest": "^18.0.12", @@ -59,7 +59,7 @@ "yaml": "^1.10.0" }, "devDependencies": { - "@backstage/cli": "^0.4.7", + "@backstage/cli": "^0.5.0", "@backstage/test-utils": "^0.1.5", "@types/fs-extra": "^9.0.1", "@types/supertest": "^2.0.8", diff --git a/plugins/scaffolder/CHANGELOG.md b/plugins/scaffolder/CHANGELOG.md index 6d435a144c..11a9cfb75c 100644 --- a/plugins/scaffolder/CHANGELOG.md +++ b/plugins/scaffolder/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-scaffolder +## 0.4.1 + +### Patch Changes + +- 9dd057662: Upgrade [git-url-parse](https://www.npmjs.com/package/git-url-parse) to [v11.4.4](https://github.com/IonicaBizau/git-url-parse/pull/125) which fixes parsing an Azure DevOps branch ref. +- Updated dependencies [9dd057662] +- Updated dependencies [0b1182346] + - @backstage/plugin-catalog@0.2.14 + ## 0.4.0 ### Minor Changes diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 483dc548ea..d81d176232 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder", - "version": "0.4.0", + "version": "0.4.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,7 +32,7 @@ "dependencies": { "@backstage/catalog-model": "^0.7.0", "@backstage/core": "^0.5.0", - "@backstage/plugin-catalog": "^0.2.12", + "@backstage/plugin-catalog": "^0.2.14", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -51,7 +51,7 @@ "swr": "^0.3.0" }, "devDependencies": { - "@backstage/cli": "^0.4.7", + "@backstage/cli": "^0.5.0", "@backstage/dev-utils": "^0.1.8", "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/search/package.json b/plugins/search/package.json index 41e295aba4..9409a416a0 100644 --- a/plugins/search/package.json +++ b/plugins/search/package.json @@ -30,7 +30,7 @@ }, "dependencies": { "@backstage/core": "^0.5.0", - "@backstage/plugin-catalog": "^0.2.12", + "@backstage/plugin-catalog": "^0.2.14", "@backstage/catalog-model": "^0.7.0", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", @@ -43,7 +43,7 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.4.7", + "@backstage/cli": "^0.5.0", "@backstage/dev-utils": "^0.1.8", "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/sentry/package.json b/plugins/sentry/package.json index 0bcc49ce35..52343ad332 100644 --- a/plugins/sentry/package.json +++ b/plugins/sentry/package.json @@ -33,7 +33,7 @@ "dependencies": { "@backstage/catalog-model": "^0.7.0", "@backstage/core": "^0.5.0", - "@backstage/plugin-catalog": "^0.2.12", + "@backstage/plugin-catalog": "^0.2.14", "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -46,7 +46,7 @@ "timeago.js": "^4.0.2" }, "devDependencies": { - "@backstage/cli": "^0.4.7", + "@backstage/cli": "^0.5.0", "@backstage/dev-utils": "^0.1.8", "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/sonarqube/CHANGELOG.md b/plugins/sonarqube/CHANGELOG.md index 375b5a8312..c3be5e7415 100644 --- a/plugins/sonarqube/CHANGELOG.md +++ b/plugins/sonarqube/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/plugin-sonarqube +## 0.1.9 + +### Patch Changes + +- 49a67732f: Ask the SonarQube server for all support metrics prior to querying them for a project. + ## 0.1.8 ### Patch Changes diff --git a/plugins/sonarqube/package.json b/plugins/sonarqube/package.json index 0f7eca0f9a..b6701f9849 100644 --- a/plugins/sonarqube/package.json +++ b/plugins/sonarqube/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-sonarqube", - "version": "0.1.8", + "version": "0.1.9", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -46,7 +46,7 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.4.7", + "@backstage/cli": "^0.5.0", "@backstage/dev-utils": "^0.1.8", "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/tech-radar/package.json b/plugins/tech-radar/package.json index d9f627ac8c..9014d52931 100644 --- a/plugins/tech-radar/package.json +++ b/plugins/tech-radar/package.json @@ -43,7 +43,7 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.4.7", + "@backstage/cli": "^0.5.0", "@backstage/dev-utils": "^0.1.8", "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/techdocs-backend/package.json b/plugins/techdocs-backend/package.json index eb3d70dee7..e6af442d20 100644 --- a/plugins/techdocs-backend/package.json +++ b/plugins/techdocs-backend/package.json @@ -30,10 +30,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.5.0", + "@backstage/backend-common": "^0.5.1", "@backstage/catalog-model": "^0.7.0", "@backstage/config": "^0.1.2", - "@backstage/techdocs-common": "^0.3.5", + "@backstage/techdocs-common": "^0.3.6", "@types/dockerode": "^3.2.1", "@types/express": "^4.17.6", "cross-fetch": "^3.0.6", @@ -45,7 +45,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.4.7", + "@backstage/cli": "^0.5.0", "supertest": "^4.0.2" }, "files": [ diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index ac439b8623..2c1e0e5911 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -33,10 +33,10 @@ "dependencies": { "@backstage/catalog-model": "^0.7.0", "@backstage/core": "^0.5.0", - "@backstage/plugin-catalog": "^0.2.12", + "@backstage/plugin-catalog": "^0.2.14", "@backstage/test-utils": "^0.1.6", "@backstage/theme": "^0.2.2", - "@backstage/techdocs-common": "^0.3.5", + "@backstage/techdocs-common": "^0.3.6", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -49,7 +49,7 @@ "sanitize-html": "^1.27.0" }, "devDependencies": { - "@backstage/cli": "^0.4.7", + "@backstage/cli": "^0.5.0", "@backstage/dev-utils": "^0.1.8", "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/user-settings/package.json b/plugins/user-settings/package.json index 2e6f5e3e95..78790a891e 100644 --- a/plugins/user-settings/package.json +++ b/plugins/user-settings/package.json @@ -41,7 +41,7 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.4.7", + "@backstage/cli": "^0.5.0", "@backstage/dev-utils": "^0.1.8", "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/welcome/package.json b/plugins/welcome/package.json index 440e8c2d47..9482e34743 100644 --- a/plugins/welcome/package.json +++ b/plugins/welcome/package.json @@ -41,7 +41,7 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.4.7", + "@backstage/cli": "^0.5.0", "@backstage/dev-utils": "^0.1.8", "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", From 4237f8aa82ff1b5eab6a870f8e120610050c0fdd Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 28 Jan 2021 11:21:30 +0100 Subject: [PATCH 103/131] backend-common: add integration test for UrlReader --- .../src/reading/integration.test.ts | 155 ++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 packages/backend-common/src/reading/integration.test.ts diff --git a/packages/backend-common/src/reading/integration.test.ts b/packages/backend-common/src/reading/integration.test.ts new file mode 100644 index 0000000000..e8ebd1dd8d --- /dev/null +++ b/packages/backend-common/src/reading/integration.test.ts @@ -0,0 +1,155 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ConfigReader } from '@backstage/config'; +import { getVoidLogger } from '../logging'; +import { UrlReaders } from './UrlReaders'; + +const reader = UrlReaders.default({ + logger: getVoidLogger(), + config: new ConfigReader({ + // The tokens in this config provide read only access to the backstage-verification repos + integrations: { + github: [ + { + host: 'github.com', + token: `${86}af${617}d9c3c8bf958b37a${630691452765}bb0b0a`, + }, + ], + gitlab: [ + { + host: 'gitlab.com', + token: 'tveGtSHDBJM9ZRHZNRfm', + }, + ], + bitbucket: [ + { + host: 'bitbucket.org', + username: 'backstage-verification', + appPassword: 'H79MAAhtbZwCafkVTrrQ', + }, + ], + azure: [ + { + host: 'dev.azure.com', + // lasts until 2022-01-27 + token: 'bhs5cbukiuxrkc3ftuyt5h3eqewtkj37lmf3jx5aoajivq3f5jmq', + }, + ], + }, + }), +}); + +function withRetries(count: number, fn: () => Promise) { + return async () => { + let error; + for (let i = 0; i < count; i++) { + try { + await fn(); + return; + } catch (err) { + error = err; + } + } + throw error; + }; +} + +describe('UrlReaders', () => { + it( + 'should read data from azure', + withRetries(3, async () => { + const data = await reader.read( + 'https://dev.azure.com/backstage-verification/test-templates/_git/test-templates?path=%2Ftemplate.yaml', + ); + expect(data.toString()).toContain('test-template-azure'); + + const res = await reader.readTree( + 'https://dev.azure.com/backstage-verification/test-templates/_git/test-templates?path=%2F{{cookiecutter.name}}', + ); + const files = await res.files(); + expect(files).toEqual([ + { + path: 'catalog-info.yaml', + content: expect.any(Function), + }, + ]); + }), + ); + + it( + 'should read data from gitlab', + withRetries(3, async () => { + const data = await reader.read( + 'https://gitlab.com/backstage-verification/test-templates/-/blob/master/template.yaml', + ); + expect(data.toString()).toContain('test-template-gitlab'); + + const res = await reader.readTree( + 'https://gitlab.com/backstage-verification/test-templates/-/tree/master/{{cookiecutter.name}}', + ); + const files = await res.files(); + expect(files).toEqual([ + { + path: 'catalog-info.yaml', + content: expect.any(Function), + }, + ]); + }), + ); + + it( + 'should read data from bitbucket', + withRetries(3, async () => { + const data = await reader.read( + 'https://bitbucket.org/backstage-verification/test-template/src/master/template.yaml', + ); + expect(data.toString()).toContain('test-template-bitbucket'); + + const res = await reader.readTree( + 'https://bitbucket.org/backstage-verification/test-template/src/master/{{cookiecutter.name}}', + ); + const files = await res.files(); + expect(files).toEqual([ + { + path: 'catalog-info.yaml', + content: expect.any(Function), + }, + ]); + }), + ); + + it( + 'should read data from github', + withRetries(3, async () => { + const data = await reader.read( + 'https://github.com/backstage-verification/test-templates/blob/master/template.yaml', + ); + expect(data.toString()).toContain('test-template-github'); + + const res = await reader.readTree( + 'https://github.com/backstage-verification/test-templates/tree/master/{{cookiecutter.name}}', + ); + const files = await res.files(); + expect(files).toEqual([ + { + path: 'catalog-info.yaml', + content: expect.any(Function), + }, + ]); + }), + ); +}); From df26c76ab1f16f9f742de49284d8bc3255778138 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 28 Jan 2021 13:15:06 +0100 Subject: [PATCH 104/131] chore: bump just patch --- packages/create-app/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/create-app/package.json b/packages/create-app/package.json index 448b320346..475b61dde8 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": "1.0.0", + "version": "0.3.7", "private": false, "publishConfig": { "access": "public" From 5b06ca7171683ef7a408dec73f26bf2b6a039190 Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Thu, 28 Jan 2021 13:37:25 +0100 Subject: [PATCH 105/131] techdocs: Use URL Reader in example component --- packages/create-app/templates/default-app/catalog-info.yaml.hbs | 2 +- plugins/github-actions/examples/sample.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/create-app/templates/default-app/catalog-info.yaml.hbs b/packages/create-app/templates/default-app/catalog-info.yaml.hbs index 0f8e4f5aeb..d66da552b7 100644 --- a/packages/create-app/templates/default-app/catalog-info.yaml.hbs +++ b/packages/create-app/templates/default-app/catalog-info.yaml.hbs @@ -6,7 +6,7 @@ metadata: # Example for optional annotations # annotations: # github.com/project-slug: backstage/backstage - # backstage.io/techdocs-ref: github:https://github.com/backstage/backstage.git + # backstage.io/techdocs-ref: url:https://github.com/backstage/backstage spec: type: website owner: john@example.com diff --git a/plugins/github-actions/examples/sample.yaml b/plugins/github-actions/examples/sample.yaml index beda3c58ed..a765bd395d 100644 --- a/plugins/github-actions/examples/sample.yaml +++ b/plugins/github-actions/examples/sample.yaml @@ -5,7 +5,7 @@ metadata: description: backstage.io annotations: github.com/project-slug: 'backstage/backstage' - backstage.io/techdocs-ref: github:https://github.com/backstage/backstage.git + backstage.io/techdocs-ref: url:https://github.com/backstage/backstage spec: type: website lifecycle: production From c42cbd10c1252d0abe4d57b1c0ed42d0878215fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 28 Jan 2021 14:25:55 +0100 Subject: [PATCH 106/131] Update CHANGELOG.md --- packages/create-app/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/create-app/CHANGELOG.md b/packages/create-app/CHANGELOG.md index d2faa14876..6c449b33b5 100644 --- a/packages/create-app/CHANGELOG.md +++ b/packages/create-app/CHANGELOG.md @@ -1,6 +1,6 @@ # @backstage/create-app -## 1.0.0 +## 0.3.7 ### Patch Changes From 605629dc74f5198348f89405224a1384976ea27c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 28 Jan 2021 18:06:47 +0100 Subject: [PATCH 107/131] integration: forward errors from gitlab project lookups --- packages/integration/src/gitlab/core.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/integration/src/gitlab/core.ts b/packages/integration/src/gitlab/core.ts index 29dcc60bac..c1d43c3a46 100644 --- a/packages/integration/src/gitlab/core.ts +++ b/packages/integration/src/gitlab/core.ts @@ -147,10 +147,15 @@ export async function getProjectId( repoIDLookup.toString(), getGitLabRequestOptions(config), ); - const projectIDJson = await response.json(); - const projectID = Number(projectIDJson.id); + const data = await response.json(); - return projectID; + if (!response.ok) { + throw new Error( + `GitLab Error '${data.error}', ${data.error_description}`, + ); + } + + return Number(data.id); } catch (e) { throw new Error(`Could not get GitLab project ID for: ${target}, ${e}`); } From 064c513e1af9ef25ba559dbb3fb6d8e4ce9c3610 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 28 Jan 2021 18:10:12 +0100 Subject: [PATCH 108/131] add changeset --- .changeset/moody-buckets-visit.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/moody-buckets-visit.md diff --git a/.changeset/moody-buckets-visit.md b/.changeset/moody-buckets-visit.md new file mode 100644 index 0000000000..07a960620b --- /dev/null +++ b/.changeset/moody-buckets-visit.md @@ -0,0 +1,5 @@ +--- +'@backstage/integration': patch +--- + +Properly forward errors that occur when looking up GitLab project IDs. From 6e612ce252a62f421581da433f3de6c823f98ed9 Mon Sep 17 00:00:00 2001 From: Andrew Thauer <6507159+andrewthauer@users.noreply.github.com> Date: Wed, 27 Jan 2021 21:38:02 -0500 Subject: [PATCH 109/131] feat: add entity metadata links --- .changeset/fuzzy-pumpkins-tie.md | 5 + .../software-catalog/descriptor-format.md | 28 + packages/catalog-model/examples/acme/org.yaml | 3 + .../examples/apis/streetlights-api.yaml | 4 + .../examples/apis/swapi-graphql.yaml | 1173 +---------------- .../components/petstore-component.yaml | 4 + .../examples/domains/artists-domain.yaml | 3 + packages/catalog-model/src/entity/Entity.ts | 25 + .../policies/FieldFormatEntityPolicy.test.ts | 9 + .../policies/FieldFormatEntityPolicy.ts | 6 + 10 files changed, 93 insertions(+), 1167 deletions(-) create mode 100644 .changeset/fuzzy-pumpkins-tie.md diff --git a/.changeset/fuzzy-pumpkins-tie.md b/.changeset/fuzzy-pumpkins-tie.md new file mode 100644 index 0000000000..9b4a928c7c --- /dev/null +++ b/.changeset/fuzzy-pumpkins-tie.md @@ -0,0 +1,5 @@ +--- +'@backstage/catalog-model': minor +--- + +Adds a new optional `links` metadata field to the Entity class within the `catalog-model` package (as discussed in [[RFC] Entity Links](https://github.com/backstage/backstage/issues/3787)). This PR adds support for the entity links only. Follow up PR's will introduce the UI component to display them. diff --git a/docs/features/software-catalog/descriptor-format.md b/docs/features/software-catalog/descriptor-format.md index 2b173c4b3e..64d2638681 100644 --- a/docs/features/software-catalog/descriptor-format.md +++ b/docs/features/software-catalog/descriptor-format.md @@ -314,6 +314,34 @@ This field is optional, and currently has no special semantics. Each tag must be sequences of `[a-z0-9]` separated by `-`, at most 63 characters in total. +### `links` [optional] + +A list of external hyperlinks related to the entity. Links can provide +additional contextual information that may be located outside of backstage +itself. For example, an admin dashboard or external CMS page. + +Users may add links to descriptor YAML files to provide additional reference +information to external content & resources. Links are not intended to drive any +additional functionality within backstage, which is best left to `annotations` +and `labels`. It is recommended to use links only when an equivalent well-known +`annotation` does not cover a similar use case. + +Fields of a link are: + +| Field | Type | Description | +| ------- | ------ | ------------------------------------------------------------------------------------ | +| `url` | String | [Required] A `url` in a standard `uri` format (e.g. `https://example.com/some/page`) | +| `title` | String | [Optional] A user friendly display name for the link. | +| `icon` | String | [Optional] A key representing a visual icon to be displayed in the UI. | + +_NOTE_: The `icon` field value is meant to be a semantic key that will map to a +specific icon that may be provided by an icon library (e.g. `material-ui` +icons). These keys should a sequence of `[a-z0-9A-Z]` and possibly separated by +one of `[-_.]`. Backstage may support some basic icons out of the box, but the +backstage integrator will ultimately be left to provide the appropriate icon +component mappings. A generic fallback icon would be provided if a mapping +cannot be resolved. + ## Common to All Kinds: Relations The `relations` root field is a read-only list of relations, between the current diff --git a/packages/catalog-model/examples/acme/org.yaml b/packages/catalog-model/examples/acme/org.yaml index 05afc265c0..35dfa2633c 100644 --- a/packages/catalog-model/examples/acme/org.yaml +++ b/packages/catalog-model/examples/acme/org.yaml @@ -3,6 +3,9 @@ kind: Group metadata: name: acme-corp description: The acme-corp organization + links: + - url: http://www.acme.com/ + name: Website spec: type: organization profile: diff --git a/packages/catalog-model/examples/apis/streetlights-api.yaml b/packages/catalog-model/examples/apis/streetlights-api.yaml index 725811d0cc..4e0dbb1396 100644 --- a/packages/catalog-model/examples/apis/streetlights-api.yaml +++ b/packages/catalog-model/examples/apis/streetlights-api.yaml @@ -5,6 +5,10 @@ metadata: description: The Smartylighting Streetlights API allows you to remotely manage the city lights. tags: - mqtt + links: + - url: https://github.com/asyncapi/asyncapi/blob/master/examples/1.2.0/streetlights.yml + title: Source Code + icon: code spec: type: asyncapi lifecycle: production diff --git a/packages/catalog-model/examples/apis/swapi-graphql.yaml b/packages/catalog-model/examples/apis/swapi-graphql.yaml index 0c1f7af5a4..85b0bda3a3 100644 --- a/packages/catalog-model/examples/apis/swapi-graphql.yaml +++ b/packages/catalog-model/examples/apis/swapi-graphql.yaml @@ -3,1174 +3,13 @@ kind: API metadata: name: starwars-graphql description: SWAPI GraphQL Schema + links: + - url: https://github.com/graphql/swapi-graphql + title: GitHub Repo + icon: github spec: type: graphql lifecycle: production owner: team-b - definition: | - schema { - query: Root - } - - """A single film.""" - type Film implements Node { - """The title of this film.""" - title: String - - """The episode number of this film.""" - episodeID: Int - - """The opening paragraphs at the beginning of this film.""" - openingCrawl: String - - """The name of the director of this film.""" - director: String - - """The name(s) of the producer(s) of this film.""" - producers: [String] - - """The ISO 8601 date format of film release at original creator country.""" - releaseDate: String - speciesConnection(after: String, first: Int, before: String, last: Int): FilmSpeciesConnection - starshipConnection(after: String, first: Int, before: String, last: Int): FilmStarshipsConnection - vehicleConnection(after: String, first: Int, before: String, last: Int): FilmVehiclesConnection - characterConnection(after: String, first: Int, before: String, last: Int): FilmCharactersConnection - planetConnection(after: String, first: Int, before: String, last: Int): FilmPlanetsConnection - - """The ISO 8601 date format of the time that this resource was created.""" - created: String - - """The ISO 8601 date format of the time that this resource was edited.""" - edited: String - - """The ID of an object""" - id: ID! - } - - """A connection to a list of items.""" - type FilmCharactersConnection { - """Information to aid in pagination.""" - pageInfo: PageInfo! - - """A list of edges.""" - edges: [FilmCharactersEdge] - - """ - A count of the total number of objects in this connection, ignoring pagination. - This allows a client to fetch the first five objects by passing "5" as the - argument to "first", then fetch the total count so it could display "5 of 83", - for example. - """ - totalCount: Int - - """ - A list of all of the objects returned in the connection. This is a convenience - field provided for quickly exploring the API; rather than querying for - "{ edges { node } }" when no edge data is needed, this field can be be used - instead. Note that when clients like Relay need to fetch the "cursor" field on - the edge to enable efficient pagination, this shortcut cannot be used, and the - full "{ edges { node } }" version should be used instead. - """ - characters: [Person] - } - - """An edge in a connection.""" - type FilmCharactersEdge { - """The item at the end of the edge""" - node: Person - - """A cursor for use in pagination""" - cursor: String! - } - - """A connection to a list of items.""" - type FilmPlanetsConnection { - """Information to aid in pagination.""" - pageInfo: PageInfo! - - """A list of edges.""" - edges: [FilmPlanetsEdge] - - """ - A count of the total number of objects in this connection, ignoring pagination. - This allows a client to fetch the first five objects by passing "5" as the - argument to "first", then fetch the total count so it could display "5 of 83", - for example. - """ - totalCount: Int - - """ - A list of all of the objects returned in the connection. This is a convenience - field provided for quickly exploring the API; rather than querying for - "{ edges { node } }" when no edge data is needed, this field can be be used - instead. Note that when clients like Relay need to fetch the "cursor" field on - the edge to enable efficient pagination, this shortcut cannot be used, and the - full "{ edges { node } }" version should be used instead. - """ - planets: [Planet] - } - - """An edge in a connection.""" - type FilmPlanetsEdge { - """The item at the end of the edge""" - node: Planet - - """A cursor for use in pagination""" - cursor: String! - } - - """A connection to a list of items.""" - type FilmsConnection { - """Information to aid in pagination.""" - pageInfo: PageInfo! - - """A list of edges.""" - edges: [FilmsEdge] - - """ - A count of the total number of objects in this connection, ignoring pagination. - This allows a client to fetch the first five objects by passing "5" as the - argument to "first", then fetch the total count so it could display "5 of 83", - for example. - """ - totalCount: Int - - """ - A list of all of the objects returned in the connection. This is a convenience - field provided for quickly exploring the API; rather than querying for - "{ edges { node } }" when no edge data is needed, this field can be be used - instead. Note that when clients like Relay need to fetch the "cursor" field on - the edge to enable efficient pagination, this shortcut cannot be used, and the - full "{ edges { node } }" version should be used instead. - """ - films: [Film] - } - - """An edge in a connection.""" - type FilmsEdge { - """The item at the end of the edge""" - node: Film - - """A cursor for use in pagination""" - cursor: String! - } - - """A connection to a list of items.""" - type FilmSpeciesConnection { - """Information to aid in pagination.""" - pageInfo: PageInfo! - - """A list of edges.""" - edges: [FilmSpeciesEdge] - - """ - A count of the total number of objects in this connection, ignoring pagination. - This allows a client to fetch the first five objects by passing "5" as the - argument to "first", then fetch the total count so it could display "5 of 83", - for example. - """ - totalCount: Int - - """ - A list of all of the objects returned in the connection. This is a convenience - field provided for quickly exploring the API; rather than querying for - "{ edges { node } }" when no edge data is needed, this field can be be used - instead. Note that when clients like Relay need to fetch the "cursor" field on - the edge to enable efficient pagination, this shortcut cannot be used, and the - full "{ edges { node } }" version should be used instead. - """ - species: [Species] - } - - """An edge in a connection.""" - type FilmSpeciesEdge { - """The item at the end of the edge""" - node: Species - - """A cursor for use in pagination""" - cursor: String! - } - - """A connection to a list of items.""" - type FilmStarshipsConnection { - """Information to aid in pagination.""" - pageInfo: PageInfo! - - """A list of edges.""" - edges: [FilmStarshipsEdge] - - """ - A count of the total number of objects in this connection, ignoring pagination. - This allows a client to fetch the first five objects by passing "5" as the - argument to "first", then fetch the total count so it could display "5 of 83", - for example. - """ - totalCount: Int - - """ - A list of all of the objects returned in the connection. This is a convenience - field provided for quickly exploring the API; rather than querying for - "{ edges { node } }" when no edge data is needed, this field can be be used - instead. Note that when clients like Relay need to fetch the "cursor" field on - the edge to enable efficient pagination, this shortcut cannot be used, and the - full "{ edges { node } }" version should be used instead. - """ - starships: [Starship] - } - - """An edge in a connection.""" - type FilmStarshipsEdge { - """The item at the end of the edge""" - node: Starship - - """A cursor for use in pagination""" - cursor: String! - } - - """A connection to a list of items.""" - type FilmVehiclesConnection { - """Information to aid in pagination.""" - pageInfo: PageInfo! - - """A list of edges.""" - edges: [FilmVehiclesEdge] - - """ - A count of the total number of objects in this connection, ignoring pagination. - This allows a client to fetch the first five objects by passing "5" as the - argument to "first", then fetch the total count so it could display "5 of 83", - for example. - """ - totalCount: Int - - """ - A list of all of the objects returned in the connection. This is a convenience - field provided for quickly exploring the API; rather than querying for - "{ edges { node } }" when no edge data is needed, this field can be be used - instead. Note that when clients like Relay need to fetch the "cursor" field on - the edge to enable efficient pagination, this shortcut cannot be used, and the - full "{ edges { node } }" version should be used instead. - """ - vehicles: [Vehicle] - } - - """An edge in a connection.""" - type FilmVehiclesEdge { - """The item at the end of the edge""" - node: Vehicle - - """A cursor for use in pagination""" - cursor: String! - } - - """An object with an ID""" - interface Node { - """The id of the object.""" - id: ID! - } - - """Information about pagination in a connection.""" - type PageInfo { - """When paginating forwards, are there more items?""" - hasNextPage: Boolean! - - """When paginating backwards, are there more items?""" - hasPreviousPage: Boolean! - - """When paginating backwards, the cursor to continue.""" - startCursor: String - - """When paginating forwards, the cursor to continue.""" - endCursor: String - } - - """A connection to a list of items.""" - type PeopleConnection { - """Information to aid in pagination.""" - pageInfo: PageInfo! - - """A list of edges.""" - edges: [PeopleEdge] - - """ - A count of the total number of objects in this connection, ignoring pagination. - This allows a client to fetch the first five objects by passing "5" as the - argument to "first", then fetch the total count so it could display "5 of 83", - for example. - """ - totalCount: Int - - """ - A list of all of the objects returned in the connection. This is a convenience - field provided for quickly exploring the API; rather than querying for - "{ edges { node } }" when no edge data is needed, this field can be be used - instead. Note that when clients like Relay need to fetch the "cursor" field on - the edge to enable efficient pagination, this shortcut cannot be used, and the - full "{ edges { node } }" version should be used instead. - """ - people: [Person] - } - - """An edge in a connection.""" - type PeopleEdge { - """The item at the end of the edge""" - node: Person - - """A cursor for use in pagination""" - cursor: String! - } - - """An individual person or character within the Star Wars universe.""" - type Person implements Node { - """The name of this person.""" - name: String - - """ - The birth year of the person, using the in-universe standard of BBY or ABY - - Before the Battle of Yavin or After the Battle of Yavin. The Battle of Yavin is - a battle that occurs at the end of Star Wars episode IV: A New Hope. - """ - birthYear: String - - """ - The eye color of this person. Will be "unknown" if not known or "n/a" if the - person does not have an eye. - """ - eyeColor: String - - """ - The gender of this person. Either "Male", "Female" or "unknown", - "n/a" if the person does not have a gender. - """ - gender: String - - """ - The hair color of this person. Will be "unknown" if not known or "n/a" if the - person does not have hair. - """ - hairColor: String - - """The height of the person in centimeters.""" - height: Int - - """The mass of the person in kilograms.""" - mass: Float - - """The skin color of this person.""" - skinColor: String - - """A planet that this person was born on or inhabits.""" - homeworld: Planet - filmConnection(after: String, first: Int, before: String, last: Int): PersonFilmsConnection - - """The species that this person belongs to, or null if unknown.""" - species: Species - starshipConnection(after: String, first: Int, before: String, last: Int): PersonStarshipsConnection - vehicleConnection(after: String, first: Int, before: String, last: Int): PersonVehiclesConnection - - """The ISO 8601 date format of the time that this resource was created.""" - created: String - - """The ISO 8601 date format of the time that this resource was edited.""" - edited: String - - """The ID of an object""" - id: ID! - } - - """A connection to a list of items.""" - type PersonFilmsConnection { - """Information to aid in pagination.""" - pageInfo: PageInfo! - - """A list of edges.""" - edges: [PersonFilmsEdge] - - """ - A count of the total number of objects in this connection, ignoring pagination. - This allows a client to fetch the first five objects by passing "5" as the - argument to "first", then fetch the total count so it could display "5 of 83", - for example. - """ - totalCount: Int - - """ - A list of all of the objects returned in the connection. This is a convenience - field provided for quickly exploring the API; rather than querying for - "{ edges { node } }" when no edge data is needed, this field can be be used - instead. Note that when clients like Relay need to fetch the "cursor" field on - the edge to enable efficient pagination, this shortcut cannot be used, and the - full "{ edges { node } }" version should be used instead. - """ - films: [Film] - } - - """An edge in a connection.""" - type PersonFilmsEdge { - """The item at the end of the edge""" - node: Film - - """A cursor for use in pagination""" - cursor: String! - } - - """A connection to a list of items.""" - type PersonStarshipsConnection { - """Information to aid in pagination.""" - pageInfo: PageInfo! - - """A list of edges.""" - edges: [PersonStarshipsEdge] - - """ - A count of the total number of objects in this connection, ignoring pagination. - This allows a client to fetch the first five objects by passing "5" as the - argument to "first", then fetch the total count so it could display "5 of 83", - for example. - """ - totalCount: Int - - """ - A list of all of the objects returned in the connection. This is a convenience - field provided for quickly exploring the API; rather than querying for - "{ edges { node } }" when no edge data is needed, this field can be be used - instead. Note that when clients like Relay need to fetch the "cursor" field on - the edge to enable efficient pagination, this shortcut cannot be used, and the - full "{ edges { node } }" version should be used instead. - """ - starships: [Starship] - } - - """An edge in a connection.""" - type PersonStarshipsEdge { - """The item at the end of the edge""" - node: Starship - - """A cursor for use in pagination""" - cursor: String! - } - - """A connection to a list of items.""" - type PersonVehiclesConnection { - """Information to aid in pagination.""" - pageInfo: PageInfo! - - """A list of edges.""" - edges: [PersonVehiclesEdge] - - """ - A count of the total number of objects in this connection, ignoring pagination. - This allows a client to fetch the first five objects by passing "5" as the - argument to "first", then fetch the total count so it could display "5 of 83", - for example. - """ - totalCount: Int - - """ - A list of all of the objects returned in the connection. This is a convenience - field provided for quickly exploring the API; rather than querying for - "{ edges { node } }" when no edge data is needed, this field can be be used - instead. Note that when clients like Relay need to fetch the "cursor" field on - the edge to enable efficient pagination, this shortcut cannot be used, and the - full "{ edges { node } }" version should be used instead. - """ - vehicles: [Vehicle] - } - - """An edge in a connection.""" - type PersonVehiclesEdge { - """The item at the end of the edge""" - node: Vehicle - - """A cursor for use in pagination""" - cursor: String! - } - - """ - A large mass, planet or planetoid in the Star Wars Universe, at the time of - 0 ABY. - """ - type Planet implements Node { - """The name of this planet.""" - name: String - - """The diameter of this planet in kilometers.""" - diameter: Int - - """ - The number of standard hours it takes for this planet to complete a single - rotation on its axis. - """ - rotationPeriod: Int - - """ - The number of standard days it takes for this planet to complete a single orbit - of its local star. - """ - orbitalPeriod: Int - - """ - A number denoting the gravity of this planet, where "1" is normal or 1 standard - G. "2" is twice or 2 standard Gs. "0.5" is half or 0.5 standard Gs. - """ - gravity: String - - """The average population of sentient beings inhabiting this planet.""" - population: Float - - """The climates of this planet.""" - climates: [String] - - """The terrains of this planet.""" - terrains: [String] - - """ - The percentage of the planet surface that is naturally occuring water or bodies - of water. - """ - surfaceWater: Float - residentConnection(after: String, first: Int, before: String, last: Int): PlanetResidentsConnection - filmConnection(after: String, first: Int, before: String, last: Int): PlanetFilmsConnection - - """The ISO 8601 date format of the time that this resource was created.""" - created: String - - """The ISO 8601 date format of the time that this resource was edited.""" - edited: String - - """The ID of an object""" - id: ID! - } - - """A connection to a list of items.""" - type PlanetFilmsConnection { - """Information to aid in pagination.""" - pageInfo: PageInfo! - - """A list of edges.""" - edges: [PlanetFilmsEdge] - - """ - A count of the total number of objects in this connection, ignoring pagination. - This allows a client to fetch the first five objects by passing "5" as the - argument to "first", then fetch the total count so it could display "5 of 83", - for example. - """ - totalCount: Int - - """ - A list of all of the objects returned in the connection. This is a convenience - field provided for quickly exploring the API; rather than querying for - "{ edges { node } }" when no edge data is needed, this field can be be used - instead. Note that when clients like Relay need to fetch the "cursor" field on - the edge to enable efficient pagination, this shortcut cannot be used, and the - full "{ edges { node } }" version should be used instead. - """ - films: [Film] - } - - """An edge in a connection.""" - type PlanetFilmsEdge { - """The item at the end of the edge""" - node: Film - - """A cursor for use in pagination""" - cursor: String! - } - - """A connection to a list of items.""" - type PlanetResidentsConnection { - """Information to aid in pagination.""" - pageInfo: PageInfo! - - """A list of edges.""" - edges: [PlanetResidentsEdge] - - """ - A count of the total number of objects in this connection, ignoring pagination. - This allows a client to fetch the first five objects by passing "5" as the - argument to "first", then fetch the total count so it could display "5 of 83", - for example. - """ - totalCount: Int - - """ - A list of all of the objects returned in the connection. This is a convenience - field provided for quickly exploring the API; rather than querying for - "{ edges { node } }" when no edge data is needed, this field can be be used - instead. Note that when clients like Relay need to fetch the "cursor" field on - the edge to enable efficient pagination, this shortcut cannot be used, and the - full "{ edges { node } }" version should be used instead. - """ - residents: [Person] - } - - """An edge in a connection.""" - type PlanetResidentsEdge { - """The item at the end of the edge""" - node: Person - - """A cursor for use in pagination""" - cursor: String! - } - - """A connection to a list of items.""" - type PlanetsConnection { - """Information to aid in pagination.""" - pageInfo: PageInfo! - - """A list of edges.""" - edges: [PlanetsEdge] - - """ - A count of the total number of objects in this connection, ignoring pagination. - This allows a client to fetch the first five objects by passing "5" as the - argument to "first", then fetch the total count so it could display "5 of 83", - for example. - """ - totalCount: Int - - """ - A list of all of the objects returned in the connection. This is a convenience - field provided for quickly exploring the API; rather than querying for - "{ edges { node } }" when no edge data is needed, this field can be be used - instead. Note that when clients like Relay need to fetch the "cursor" field on - the edge to enable efficient pagination, this shortcut cannot be used, and the - full "{ edges { node } }" version should be used instead. - """ - planets: [Planet] - } - - """An edge in a connection.""" - type PlanetsEdge { - """The item at the end of the edge""" - node: Planet - - """A cursor for use in pagination""" - cursor: String! - } - - type Root { - allFilms(after: String, first: Int, before: String, last: Int): FilmsConnection - film(id: ID, filmID: ID): Film - allPeople(after: String, first: Int, before: String, last: Int): PeopleConnection - person(id: ID, personID: ID): Person - allPlanets(after: String, first: Int, before: String, last: Int): PlanetsConnection - planet(id: ID, planetID: ID): Planet - allSpecies(after: String, first: Int, before: String, last: Int): SpeciesConnection - species(id: ID, speciesID: ID): Species - allStarships(after: String, first: Int, before: String, last: Int): StarshipsConnection - starship(id: ID, starshipID: ID): Starship - allVehicles(after: String, first: Int, before: String, last: Int): VehiclesConnection - vehicle(id: ID, vehicleID: ID): Vehicle - - """Fetches an object given its ID""" - node( - """The ID of an object""" - id: ID! - ): Node - } - - """A type of person or character within the Star Wars Universe.""" - type Species implements Node { - """The name of this species.""" - name: String - - """The classification of this species, such as "mammal" or "reptile".""" - classification: String - - """The designation of this species, such as "sentient".""" - designation: String - - """The average height of this species in centimeters.""" - averageHeight: Float - - """The average lifespan of this species in years, null if unknown.""" - averageLifespan: Int - - """ - Common eye colors for this species, null if this species does not typically - have eyes. - """ - eyeColors: [String] - - """ - Common hair colors for this species, null if this species does not typically - have hair. - """ - hairColors: [String] - - """ - Common skin colors for this species, null if this species does not typically - have skin. - """ - skinColors: [String] - - """The language commonly spoken by this species.""" - language: String - - """A planet that this species originates from.""" - homeworld: Planet - personConnection(after: String, first: Int, before: String, last: Int): SpeciesPeopleConnection - filmConnection(after: String, first: Int, before: String, last: Int): SpeciesFilmsConnection - - """The ISO 8601 date format of the time that this resource was created.""" - created: String - - """The ISO 8601 date format of the time that this resource was edited.""" - edited: String - - """The ID of an object""" - id: ID! - } - - """A connection to a list of items.""" - type SpeciesConnection { - """Information to aid in pagination.""" - pageInfo: PageInfo! - - """A list of edges.""" - edges: [SpeciesEdge] - - """ - A count of the total number of objects in this connection, ignoring pagination. - This allows a client to fetch the first five objects by passing "5" as the - argument to "first", then fetch the total count so it could display "5 of 83", - for example. - """ - totalCount: Int - - """ - A list of all of the objects returned in the connection. This is a convenience - field provided for quickly exploring the API; rather than querying for - "{ edges { node } }" when no edge data is needed, this field can be be used - instead. Note that when clients like Relay need to fetch the "cursor" field on - the edge to enable efficient pagination, this shortcut cannot be used, and the - full "{ edges { node } }" version should be used instead. - """ - species: [Species] - } - - """An edge in a connection.""" - type SpeciesEdge { - """The item at the end of the edge""" - node: Species - - """A cursor for use in pagination""" - cursor: String! - } - - """A connection to a list of items.""" - type SpeciesFilmsConnection { - """Information to aid in pagination.""" - pageInfo: PageInfo! - - """A list of edges.""" - edges: [SpeciesFilmsEdge] - - """ - A count of the total number of objects in this connection, ignoring pagination. - This allows a client to fetch the first five objects by passing "5" as the - argument to "first", then fetch the total count so it could display "5 of 83", - for example. - """ - totalCount: Int - - """ - A list of all of the objects returned in the connection. This is a convenience - field provided for quickly exploring the API; rather than querying for - "{ edges { node } }" when no edge data is needed, this field can be be used - instead. Note that when clients like Relay need to fetch the "cursor" field on - the edge to enable efficient pagination, this shortcut cannot be used, and the - full "{ edges { node } }" version should be used instead. - """ - films: [Film] - } - - """An edge in a connection.""" - type SpeciesFilmsEdge { - """The item at the end of the edge""" - node: Film - - """A cursor for use in pagination""" - cursor: String! - } - - """A connection to a list of items.""" - type SpeciesPeopleConnection { - """Information to aid in pagination.""" - pageInfo: PageInfo! - - """A list of edges.""" - edges: [SpeciesPeopleEdge] - - """ - A count of the total number of objects in this connection, ignoring pagination. - This allows a client to fetch the first five objects by passing "5" as the - argument to "first", then fetch the total count so it could display "5 of 83", - for example. - """ - totalCount: Int - - """ - A list of all of the objects returned in the connection. This is a convenience - field provided for quickly exploring the API; rather than querying for - "{ edges { node } }" when no edge data is needed, this field can be be used - instead. Note that when clients like Relay need to fetch the "cursor" field on - the edge to enable efficient pagination, this shortcut cannot be used, and the - full "{ edges { node } }" version should be used instead. - """ - people: [Person] - } - - """An edge in a connection.""" - type SpeciesPeopleEdge { - """The item at the end of the edge""" - node: Person - - """A cursor for use in pagination""" - cursor: String! - } - - """A single transport craft that has hyperdrive capability.""" - type Starship implements Node { - """The name of this starship. The common name, such as "Death Star".""" - name: String - - """ - The model or official name of this starship. Such as "T-65 X-wing" or "DS-1 - Orbital Battle Station". - """ - model: String - - """ - The class of this starship, such as "Starfighter" or "Deep Space Mobile - Battlestation" - """ - starshipClass: String - - """The manufacturers of this starship.""" - manufacturers: [String] - - """The cost of this starship new, in galactic credits.""" - costInCredits: Float - - """The length of this starship in meters.""" - length: Float - - """The number of personnel needed to run or pilot this starship.""" - crew: String - - """The number of non-essential people this starship can transport.""" - passengers: String - - """ - The maximum speed of this starship in atmosphere. null if this starship is - incapable of atmosphering flight. - """ - maxAtmospheringSpeed: Int - - """The class of this starships hyperdrive.""" - hyperdriveRating: Float - - """ - The Maximum number of Megalights this starship can travel in a standard hour. - A "Megalight" is a standard unit of distance and has never been defined before - within the Star Wars universe. This figure is only really useful for measuring - the difference in speed of starships. We can assume it is similar to AU, the - distance between our Sun (Sol) and Earth. - """ - MGLT: Int - - """The maximum number of kilograms that this starship can transport.""" - cargoCapacity: Float - - """ - The maximum length of time that this starship can provide consumables for its - entire crew without having to resupply. - """ - consumables: String - pilotConnection(after: String, first: Int, before: String, last: Int): StarshipPilotsConnection - filmConnection(after: String, first: Int, before: String, last: Int): StarshipFilmsConnection - - """The ISO 8601 date format of the time that this resource was created.""" - created: String - - """The ISO 8601 date format of the time that this resource was edited.""" - edited: String - - """The ID of an object""" - id: ID! - } - - """A connection to a list of items.""" - type StarshipFilmsConnection { - """Information to aid in pagination.""" - pageInfo: PageInfo! - - """A list of edges.""" - edges: [StarshipFilmsEdge] - - """ - A count of the total number of objects in this connection, ignoring pagination. - This allows a client to fetch the first five objects by passing "5" as the - argument to "first", then fetch the total count so it could display "5 of 83", - for example. - """ - totalCount: Int - - """ - A list of all of the objects returned in the connection. This is a convenience - field provided for quickly exploring the API; rather than querying for - "{ edges { node } }" when no edge data is needed, this field can be be used - instead. Note that when clients like Relay need to fetch the "cursor" field on - the edge to enable efficient pagination, this shortcut cannot be used, and the - full "{ edges { node } }" version should be used instead. - """ - films: [Film] - } - - """An edge in a connection.""" - type StarshipFilmsEdge { - """The item at the end of the edge""" - node: Film - - """A cursor for use in pagination""" - cursor: String! - } - - """A connection to a list of items.""" - type StarshipPilotsConnection { - """Information to aid in pagination.""" - pageInfo: PageInfo! - - """A list of edges.""" - edges: [StarshipPilotsEdge] - - """ - A count of the total number of objects in this connection, ignoring pagination. - This allows a client to fetch the first five objects by passing "5" as the - argument to "first", then fetch the total count so it could display "5 of 83", - for example. - """ - totalCount: Int - - """ - A list of all of the objects returned in the connection. This is a convenience - field provided for quickly exploring the API; rather than querying for - "{ edges { node } }" when no edge data is needed, this field can be be used - instead. Note that when clients like Relay need to fetch the "cursor" field on - the edge to enable efficient pagination, this shortcut cannot be used, and the - full "{ edges { node } }" version should be used instead. - """ - pilots: [Person] - } - - """An edge in a connection.""" - type StarshipPilotsEdge { - """The item at the end of the edge""" - node: Person - - """A cursor for use in pagination""" - cursor: String! - } - - """A connection to a list of items.""" - type StarshipsConnection { - """Information to aid in pagination.""" - pageInfo: PageInfo! - - """A list of edges.""" - edges: [StarshipsEdge] - - """ - A count of the total number of objects in this connection, ignoring pagination. - This allows a client to fetch the first five objects by passing "5" as the - argument to "first", then fetch the total count so it could display "5 of 83", - for example. - """ - totalCount: Int - - """ - A list of all of the objects returned in the connection. This is a convenience - field provided for quickly exploring the API; rather than querying for - "{ edges { node } }" when no edge data is needed, this field can be be used - instead. Note that when clients like Relay need to fetch the "cursor" field on - the edge to enable efficient pagination, this shortcut cannot be used, and the - full "{ edges { node } }" version should be used instead. - """ - starships: [Starship] - } - - """An edge in a connection.""" - type StarshipsEdge { - """The item at the end of the edge""" - node: Starship - - """A cursor for use in pagination""" - cursor: String! - } - - """A single transport craft that does not have hyperdrive capability""" - type Vehicle implements Node { - """ - The name of this vehicle. The common name, such as "Sand Crawler" or "Speeder - bike". - """ - name: String - - """ - The model or official name of this vehicle. Such as "All-Terrain Attack - Transport". - """ - model: String - - """The class of this vehicle, such as "Wheeled" or "Repulsorcraft".""" - vehicleClass: String - - """The manufacturers of this vehicle.""" - manufacturers: [String] - - """The cost of this vehicle new, in Galactic Credits.""" - costInCredits: Float - - """The length of this vehicle in meters.""" - length: Float - - """The number of personnel needed to run or pilot this vehicle.""" - crew: String - - """The number of non-essential people this vehicle can transport.""" - passengers: String - - """The maximum speed of this vehicle in atmosphere.""" - maxAtmospheringSpeed: Int - - """The maximum number of kilograms that this vehicle can transport.""" - cargoCapacity: Float - - """ - The maximum length of time that this vehicle can provide consumables for its - entire crew without having to resupply. - """ - consumables: String - pilotConnection(after: String, first: Int, before: String, last: Int): VehiclePilotsConnection - filmConnection(after: String, first: Int, before: String, last: Int): VehicleFilmsConnection - - """The ISO 8601 date format of the time that this resource was created.""" - created: String - - """The ISO 8601 date format of the time that this resource was edited.""" - edited: String - - """The ID of an object""" - id: ID! - } - - """A connection to a list of items.""" - type VehicleFilmsConnection { - """Information to aid in pagination.""" - pageInfo: PageInfo! - - """A list of edges.""" - edges: [VehicleFilmsEdge] - - """ - A count of the total number of objects in this connection, ignoring pagination. - This allows a client to fetch the first five objects by passing "5" as the - argument to "first", then fetch the total count so it could display "5 of 83", - for example. - """ - totalCount: Int - - """ - A list of all of the objects returned in the connection. This is a convenience - field provided for quickly exploring the API; rather than querying for - "{ edges { node } }" when no edge data is needed, this field can be be used - instead. Note that when clients like Relay need to fetch the "cursor" field on - the edge to enable efficient pagination, this shortcut cannot be used, and the - full "{ edges { node } }" version should be used instead. - """ - films: [Film] - } - - """An edge in a connection.""" - type VehicleFilmsEdge { - """The item at the end of the edge""" - node: Film - - """A cursor for use in pagination""" - cursor: String! - } - - """A connection to a list of items.""" - type VehiclePilotsConnection { - """Information to aid in pagination.""" - pageInfo: PageInfo! - - """A list of edges.""" - edges: [VehiclePilotsEdge] - - """ - A count of the total number of objects in this connection, ignoring pagination. - This allows a client to fetch the first five objects by passing "5" as the - argument to "first", then fetch the total count so it could display "5 of 83", - for example. - """ - totalCount: Int - - """ - A list of all of the objects returned in the connection. This is a convenience - field provided for quickly exploring the API; rather than querying for - "{ edges { node } }" when no edge data is needed, this field can be be used - instead. Note that when clients like Relay need to fetch the "cursor" field on - the edge to enable efficient pagination, this shortcut cannot be used, and the - full "{ edges { node } }" version should be used instead. - """ - pilots: [Person] - } - - """An edge in a connection.""" - type VehiclePilotsEdge { - """The item at the end of the edge""" - node: Person - - """A cursor for use in pagination""" - cursor: String! - } - - """A connection to a list of items.""" - type VehiclesConnection { - """Information to aid in pagination.""" - pageInfo: PageInfo! - - """A list of edges.""" - edges: [VehiclesEdge] - - """ - A count of the total number of objects in this connection, ignoring pagination. - This allows a client to fetch the first five objects by passing "5" as the - argument to "first", then fetch the total count so it could display "5 of 83", - for example. - """ - totalCount: Int - - """ - A list of all of the objects returned in the connection. This is a convenience - field provided for quickly exploring the API; rather than querying for - "{ edges { node } }" when no edge data is needed, this field can be be used - instead. Note that when clients like Relay need to fetch the "cursor" field on - the edge to enable efficient pagination, this shortcut cannot be used, and the - full "{ edges { node } }" version should be used instead. - """ - vehicles: [Vehicle] - } - - """An edge in a connection.""" - type VehiclesEdge { - """The item at the end of the edge""" - node: Vehicle - - """A cursor for use in pagination""" - cursor: String! - } + definition: + $text: https://github.com/graphql/swapi-graphql/blob/master/schema.graphql diff --git a/packages/catalog-model/examples/components/petstore-component.yaml b/packages/catalog-model/examples/components/petstore-component.yaml index acbb2f82b0..48286f9b74 100644 --- a/packages/catalog-model/examples/components/petstore-component.yaml +++ b/packages/catalog-model/examples/components/petstore-component.yaml @@ -3,6 +3,10 @@ kind: Component metadata: name: petstore description: Petstore + links: + - url: https://github.com/swagger-api/swagger-petstore + title: GitHub Repo + icon: github spec: type: service lifecycle: experimental diff --git a/packages/catalog-model/examples/domains/artists-domain.yaml b/packages/catalog-model/examples/domains/artists-domain.yaml index 7bcc4329dd..7ac4ba66a1 100644 --- a/packages/catalog-model/examples/domains/artists-domain.yaml +++ b/packages/catalog-model/examples/domains/artists-domain.yaml @@ -3,5 +3,8 @@ kind: Domain metadata: name: artists description: Everything related to artists + links: + - url: http://example.com/domain/readme + title: Artists Domain Readme spec: owner: team-a diff --git a/packages/catalog-model/src/entity/Entity.ts b/packages/catalog-model/src/entity/Entity.ts index ac43137546..7abe1f9c0c 100644 --- a/packages/catalog-model/src/entity/Entity.ts +++ b/packages/catalog-model/src/entity/Entity.ts @@ -125,6 +125,11 @@ export type EntityMeta = JsonObject & { * various ways. */ tags?: string[]; + + /** + * A list of external hyperlinks related to the entity. + */ + links?: EntityLink[]; }; /** @@ -161,3 +166,23 @@ export type EntityRelationSpec = { */ target: EntityName; }; + +/** + * A link to external information that is related to the entity. + */ +export type EntityLink = { + /** + * The url to the external site, document, etc. + */ + url: string; + + /** + * An optional descriptive title for the link. + */ + title?: string; + + /** + * An optional semantic key that represents a visual icon. + */ + icon?: string; +}; diff --git a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.test.ts b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.test.ts index edf04a64cc..52a8207cc8 100644 --- a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.test.ts +++ b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.test.ts @@ -38,6 +38,10 @@ describe('FieldFormatEntityPolicy', () => { tags: - java - data-service + links: + - url: https://example.org + title: Website + icon: website spec: custom: stuff `); @@ -110,4 +114,9 @@ describe('FieldFormatEntityPolicy', () => { data.metadata.tags.push('Hello World'); await expect(policy.enforce(data)).rejects.toThrow(/tags.*"Hello World"/i); }); + + it('rejects bad link icon value', async () => { + data.metadata.links.push({ url: 'https://foo', icon: 'some icon' }); + await expect(policy.enforce(data)).rejects.toThrow(/links.*"some icon"/i); + }); }); diff --git a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts index 0aabbd9e48..09ccca6506 100644 --- a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts +++ b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts @@ -134,6 +134,12 @@ export class FieldFormatEntityPolicy implements EntityPolicy { require(`tags.${i}`, tags[i], this.validators.isValidTag); } + const links = entity.metadata.links ?? []; + + for (let i = 0; i < links.length; ++i) { + optional(`links.${i}`, links[i]?.icon, this.validators.isValidEntityName); + } + return entity; } } From b47136bd349dfdefe7b4d31295a5ac448b3787f3 Mon Sep 17 00:00:00 2001 From: Andrew Thauer <6507159+andrewthauer@users.noreply.github.com> Date: Thu, 28 Jan 2021 05:34:57 -0500 Subject: [PATCH 110/131] add entity links example json --- docs/features/software-catalog/descriptor-format.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/features/software-catalog/descriptor-format.md b/docs/features/software-catalog/descriptor-format.md index 64d2638681..1134fbb61a 100644 --- a/docs/features/software-catalog/descriptor-format.md +++ b/docs/features/software-catalog/descriptor-format.md @@ -54,6 +54,11 @@ software catalog API. "labels": { "system": "public-websites" }, + "links": [{ + "url": "https://admin.example-org.com", + "title": "Admin Dashboard", + "icon": "dashboard" + }], "tags": ["java"], "name": "artist-web", "uid": "2152f463-549d-4d8d-a94d-ce2b7676c6e2" From 83b94626572e324fadb9c3d1c434aeb35f1a3342 Mon Sep 17 00:00:00 2001 From: Andrew Thauer <6507159+andrewthauer@users.noreply.github.com> Date: Thu, 28 Jan 2021 05:43:14 -0500 Subject: [PATCH 111/131] update entity links catalog examples --- packages/catalog-model/examples/acme/org.yaml | 2 + .../examples/apis/petstore-api.yaml | 7 + .../examples/apis/swapi-graphql.yaml | 1169 ++++++++++++++++- .../components/artist-lookup-component.yaml | 6 + .../examples/domains/artists-domain.yaml | 7 +- 5 files changed, 1187 insertions(+), 4 deletions(-) diff --git a/packages/catalog-model/examples/acme/org.yaml b/packages/catalog-model/examples/acme/org.yaml index 35dfa2633c..f0a5003f7c 100644 --- a/packages/catalog-model/examples/acme/org.yaml +++ b/packages/catalog-model/examples/acme/org.yaml @@ -6,6 +6,8 @@ metadata: links: - url: http://www.acme.com/ name: Website + - url: https://meta.wikimedia.org/wiki/ + name: Intranet spec: type: organization profile: diff --git a/packages/catalog-model/examples/apis/petstore-api.yaml b/packages/catalog-model/examples/apis/petstore-api.yaml index a314b4e255..4c18b16a16 100644 --- a/packages/catalog-model/examples/apis/petstore-api.yaml +++ b/packages/catalog-model/examples/apis/petstore-api.yaml @@ -6,6 +6,13 @@ metadata: tags: - store - rest + links: + - url: https://github.com/swagger-api/swagger-petstore + title: GitHub Repo + icon: github + - url: https://github.com/OAI/OpenAPI-Specification/blob/master/examples/v3.0/petstore.yaml + title: API Spec + code: code spec: type: openapi lifecycle: experimental diff --git a/packages/catalog-model/examples/apis/swapi-graphql.yaml b/packages/catalog-model/examples/apis/swapi-graphql.yaml index 85b0bda3a3..f3d924fc53 100644 --- a/packages/catalog-model/examples/apis/swapi-graphql.yaml +++ b/packages/catalog-model/examples/apis/swapi-graphql.yaml @@ -11,5 +11,1170 @@ spec: type: graphql lifecycle: production owner: team-b - definition: - $text: https://github.com/graphql/swapi-graphql/blob/master/schema.graphql + definition: | + schema { + query: Root + } + + """A single film.""" + type Film implements Node { + """The title of this film.""" + title: String + + """The episode number of this film.""" + episodeID: Int + + """The opening paragraphs at the beginning of this film.""" + openingCrawl: String + + """The name of the director of this film.""" + director: String + + """The name(s) of the producer(s) of this film.""" + producers: [String] + + """The ISO 8601 date format of film release at original creator country.""" + releaseDate: String + speciesConnection(after: String, first: Int, before: String, last: Int): FilmSpeciesConnection + starshipConnection(after: String, first: Int, before: String, last: Int): FilmStarshipsConnection + vehicleConnection(after: String, first: Int, before: String, last: Int): FilmVehiclesConnection + characterConnection(after: String, first: Int, before: String, last: Int): FilmCharactersConnection + planetConnection(after: String, first: Int, before: String, last: Int): FilmPlanetsConnection + + """The ISO 8601 date format of the time that this resource was created.""" + created: String + + """The ISO 8601 date format of the time that this resource was edited.""" + edited: String + + """The ID of an object""" + id: ID! + } + + """A connection to a list of items.""" + type FilmCharactersConnection { + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """A list of edges.""" + edges: [FilmCharactersEdge] + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int + + """ + A list of all of the objects returned in the connection. This is a convenience + field provided for quickly exploring the API; rather than querying for + "{ edges { node } }" when no edge data is needed, this field can be be used + instead. Note that when clients like Relay need to fetch the "cursor" field on + the edge to enable efficient pagination, this shortcut cannot be used, and the + full "{ edges { node } }" version should be used instead. + """ + characters: [Person] + } + + """An edge in a connection.""" + type FilmCharactersEdge { + """The item at the end of the edge""" + node: Person + + """A cursor for use in pagination""" + cursor: String! + } + + """A connection to a list of items.""" + type FilmPlanetsConnection { + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """A list of edges.""" + edges: [FilmPlanetsEdge] + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int + + """ + A list of all of the objects returned in the connection. This is a convenience + field provided for quickly exploring the API; rather than querying for + "{ edges { node } }" when no edge data is needed, this field can be be used + instead. Note that when clients like Relay need to fetch the "cursor" field on + the edge to enable efficient pagination, this shortcut cannot be used, and the + full "{ edges { node } }" version should be used instead. + """ + planets: [Planet] + } + + """An edge in a connection.""" + type FilmPlanetsEdge { + """The item at the end of the edge""" + node: Planet + + """A cursor for use in pagination""" + cursor: String! + } + + """A connection to a list of items.""" + type FilmsConnection { + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """A list of edges.""" + edges: [FilmsEdge] + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int + + """ + A list of all of the objects returned in the connection. This is a convenience + field provided for quickly exploring the API; rather than querying for + "{ edges { node } }" when no edge data is needed, this field can be be used + instead. Note that when clients like Relay need to fetch the "cursor" field on + the edge to enable efficient pagination, this shortcut cannot be used, and the + full "{ edges { node } }" version should be used instead. + """ + films: [Film] + } + + """An edge in a connection.""" + type FilmsEdge { + """The item at the end of the edge""" + node: Film + + """A cursor for use in pagination""" + cursor: String! + } + + """A connection to a list of items.""" + type FilmSpeciesConnection { + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """A list of edges.""" + edges: [FilmSpeciesEdge] + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int + + """ + A list of all of the objects returned in the connection. This is a convenience + field provided for quickly exploring the API; rather than querying for + "{ edges { node } }" when no edge data is needed, this field can be be used + instead. Note that when clients like Relay need to fetch the "cursor" field on + the edge to enable efficient pagination, this shortcut cannot be used, and the + full "{ edges { node } }" version should be used instead. + """ + species: [Species] + } + + """An edge in a connection.""" + type FilmSpeciesEdge { + """The item at the end of the edge""" + node: Species + + """A cursor for use in pagination""" + cursor: String! + } + + """A connection to a list of items.""" + type FilmStarshipsConnection { + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """A list of edges.""" + edges: [FilmStarshipsEdge] + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int + + """ + A list of all of the objects returned in the connection. This is a convenience + field provided for quickly exploring the API; rather than querying for + "{ edges { node } }" when no edge data is needed, this field can be be used + instead. Note that when clients like Relay need to fetch the "cursor" field on + the edge to enable efficient pagination, this shortcut cannot be used, and the + full "{ edges { node } }" version should be used instead. + """ + starships: [Starship] + } + + """An edge in a connection.""" + type FilmStarshipsEdge { + """The item at the end of the edge""" + node: Starship + + """A cursor for use in pagination""" + cursor: String! + } + + """A connection to a list of items.""" + type FilmVehiclesConnection { + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """A list of edges.""" + edges: [FilmVehiclesEdge] + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int + + """ + A list of all of the objects returned in the connection. This is a convenience + field provided for quickly exploring the API; rather than querying for + "{ edges { node } }" when no edge data is needed, this field can be be used + instead. Note that when clients like Relay need to fetch the "cursor" field on + the edge to enable efficient pagination, this shortcut cannot be used, and the + full "{ edges { node } }" version should be used instead. + """ + vehicles: [Vehicle] + } + + """An edge in a connection.""" + type FilmVehiclesEdge { + """The item at the end of the edge""" + node: Vehicle + + """A cursor for use in pagination""" + cursor: String! + } + + """An object with an ID""" + interface Node { + """The id of the object.""" + id: ID! + } + + """Information about pagination in a connection.""" + type PageInfo { + """When paginating forwards, are there more items?""" + hasNextPage: Boolean! + + """When paginating backwards, are there more items?""" + hasPreviousPage: Boolean! + + """When paginating backwards, the cursor to continue.""" + startCursor: String + + """When paginating forwards, the cursor to continue.""" + endCursor: String + } + + """A connection to a list of items.""" + type PeopleConnection { + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """A list of edges.""" + edges: [PeopleEdge] + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int + + """ + A list of all of the objects returned in the connection. This is a convenience + field provided for quickly exploring the API; rather than querying for + "{ edges { node } }" when no edge data is needed, this field can be be used + instead. Note that when clients like Relay need to fetch the "cursor" field on + the edge to enable efficient pagination, this shortcut cannot be used, and the + full "{ edges { node } }" version should be used instead. + """ + people: [Person] + } + + """An edge in a connection.""" + type PeopleEdge { + """The item at the end of the edge""" + node: Person + + """A cursor for use in pagination""" + cursor: String! + } + + """An individual person or character within the Star Wars universe.""" + type Person implements Node { + """The name of this person.""" + name: String + + """ + The birth year of the person, using the in-universe standard of BBY or ABY - + Before the Battle of Yavin or After the Battle of Yavin. The Battle of Yavin is + a battle that occurs at the end of Star Wars episode IV: A New Hope. + """ + birthYear: String + + """ + The eye color of this person. Will be "unknown" if not known or "n/a" if the + person does not have an eye. + """ + eyeColor: String + + """ + The gender of this person. Either "Male", "Female" or "unknown", + "n/a" if the person does not have a gender. + """ + gender: String + + """ + The hair color of this person. Will be "unknown" if not known or "n/a" if the + person does not have hair. + """ + hairColor: String + + """The height of the person in centimeters.""" + height: Int + + """The mass of the person in kilograms.""" + mass: Float + + """The skin color of this person.""" + skinColor: String + + """A planet that this person was born on or inhabits.""" + homeworld: Planet + filmConnection(after: String, first: Int, before: String, last: Int): PersonFilmsConnection + + """The species that this person belongs to, or null if unknown.""" + species: Species + starshipConnection(after: String, first: Int, before: String, last: Int): PersonStarshipsConnection + vehicleConnection(after: String, first: Int, before: String, last: Int): PersonVehiclesConnection + + """The ISO 8601 date format of the time that this resource was created.""" + created: String + + """The ISO 8601 date format of the time that this resource was edited.""" + edited: String + + """The ID of an object""" + id: ID! + } + + """A connection to a list of items.""" + type PersonFilmsConnection { + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """A list of edges.""" + edges: [PersonFilmsEdge] + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int + + """ + A list of all of the objects returned in the connection. This is a convenience + field provided for quickly exploring the API; rather than querying for + "{ edges { node } }" when no edge data is needed, this field can be be used + instead. Note that when clients like Relay need to fetch the "cursor" field on + the edge to enable efficient pagination, this shortcut cannot be used, and the + full "{ edges { node } }" version should be used instead. + """ + films: [Film] + } + + """An edge in a connection.""" + type PersonFilmsEdge { + """The item at the end of the edge""" + node: Film + + """A cursor for use in pagination""" + cursor: String! + } + + """A connection to a list of items.""" + type PersonStarshipsConnection { + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """A list of edges.""" + edges: [PersonStarshipsEdge] + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int + + """ + A list of all of the objects returned in the connection. This is a convenience + field provided for quickly exploring the API; rather than querying for + "{ edges { node } }" when no edge data is needed, this field can be be used + instead. Note that when clients like Relay need to fetch the "cursor" field on + the edge to enable efficient pagination, this shortcut cannot be used, and the + full "{ edges { node } }" version should be used instead. + """ + starships: [Starship] + } + + """An edge in a connection.""" + type PersonStarshipsEdge { + """The item at the end of the edge""" + node: Starship + + """A cursor for use in pagination""" + cursor: String! + } + + """A connection to a list of items.""" + type PersonVehiclesConnection { + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """A list of edges.""" + edges: [PersonVehiclesEdge] + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int + + """ + A list of all of the objects returned in the connection. This is a convenience + field provided for quickly exploring the API; rather than querying for + "{ edges { node } }" when no edge data is needed, this field can be be used + instead. Note that when clients like Relay need to fetch the "cursor" field on + the edge to enable efficient pagination, this shortcut cannot be used, and the + full "{ edges { node } }" version should be used instead. + """ + vehicles: [Vehicle] + } + + """An edge in a connection.""" + type PersonVehiclesEdge { + """The item at the end of the edge""" + node: Vehicle + + """A cursor for use in pagination""" + cursor: String! + } + + """ + A large mass, planet or planetoid in the Star Wars Universe, at the time of + 0 ABY. + """ + type Planet implements Node { + """The name of this planet.""" + name: String + + """The diameter of this planet in kilometers.""" + diameter: Int + + """ + The number of standard hours it takes for this planet to complete a single + rotation on its axis. + """ + rotationPeriod: Int + + """ + The number of standard days it takes for this planet to complete a single orbit + of its local star. + """ + orbitalPeriod: Int + + """ + A number denoting the gravity of this planet, where "1" is normal or 1 standard + G. "2" is twice or 2 standard Gs. "0.5" is half or 0.5 standard Gs. + """ + gravity: String + + """The average population of sentient beings inhabiting this planet.""" + population: Float + + """The climates of this planet.""" + climates: [String] + + """The terrains of this planet.""" + terrains: [String] + + """ + The percentage of the planet surface that is naturally occuring water or bodies + of water. + """ + surfaceWater: Float + residentConnection(after: String, first: Int, before: String, last: Int): PlanetResidentsConnection + filmConnection(after: String, first: Int, before: String, last: Int): PlanetFilmsConnection + + """The ISO 8601 date format of the time that this resource was created.""" + created: String + + """The ISO 8601 date format of the time that this resource was edited.""" + edited: String + + """The ID of an object""" + id: ID! + } + + """A connection to a list of items.""" + type PlanetFilmsConnection { + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """A list of edges.""" + edges: [PlanetFilmsEdge] + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int + + """ + A list of all of the objects returned in the connection. This is a convenience + field provided for quickly exploring the API; rather than querying for + "{ edges { node } }" when no edge data is needed, this field can be be used + instead. Note that when clients like Relay need to fetch the "cursor" field on + the edge to enable efficient pagination, this shortcut cannot be used, and the + full "{ edges { node } }" version should be used instead. + """ + films: [Film] + } + + """An edge in a connection.""" + type PlanetFilmsEdge { + """The item at the end of the edge""" + node: Film + + """A cursor for use in pagination""" + cursor: String! + } + + """A connection to a list of items.""" + type PlanetResidentsConnection { + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """A list of edges.""" + edges: [PlanetResidentsEdge] + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int + + """ + A list of all of the objects returned in the connection. This is a convenience + field provided for quickly exploring the API; rather than querying for + "{ edges { node } }" when no edge data is needed, this field can be be used + instead. Note that when clients like Relay need to fetch the "cursor" field on + the edge to enable efficient pagination, this shortcut cannot be used, and the + full "{ edges { node } }" version should be used instead. + """ + residents: [Person] + } + + """An edge in a connection.""" + type PlanetResidentsEdge { + """The item at the end of the edge""" + node: Person + + """A cursor for use in pagination""" + cursor: String! + } + + """A connection to a list of items.""" + type PlanetsConnection { + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """A list of edges.""" + edges: [PlanetsEdge] + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int + + """ + A list of all of the objects returned in the connection. This is a convenience + field provided for quickly exploring the API; rather than querying for + "{ edges { node } }" when no edge data is needed, this field can be be used + instead. Note that when clients like Relay need to fetch the "cursor" field on + the edge to enable efficient pagination, this shortcut cannot be used, and the + full "{ edges { node } }" version should be used instead. + """ + planets: [Planet] + } + + """An edge in a connection.""" + type PlanetsEdge { + """The item at the end of the edge""" + node: Planet + + """A cursor for use in pagination""" + cursor: String! + } + + type Root { + allFilms(after: String, first: Int, before: String, last: Int): FilmsConnection + film(id: ID, filmID: ID): Film + allPeople(after: String, first: Int, before: String, last: Int): PeopleConnection + person(id: ID, personID: ID): Person + allPlanets(after: String, first: Int, before: String, last: Int): PlanetsConnection + planet(id: ID, planetID: ID): Planet + allSpecies(after: String, first: Int, before: String, last: Int): SpeciesConnection + species(id: ID, speciesID: ID): Species + allStarships(after: String, first: Int, before: String, last: Int): StarshipsConnection + starship(id: ID, starshipID: ID): Starship + allVehicles(after: String, first: Int, before: String, last: Int): VehiclesConnection + vehicle(id: ID, vehicleID: ID): Vehicle + + """Fetches an object given its ID""" + node( + """The ID of an object""" + id: ID! + ): Node + } + + """A type of person or character within the Star Wars Universe.""" + type Species implements Node { + """The name of this species.""" + name: String + + """The classification of this species, such as "mammal" or "reptile".""" + classification: String + + """The designation of this species, such as "sentient".""" + designation: String + + """The average height of this species in centimeters.""" + averageHeight: Float + + """The average lifespan of this species in years, null if unknown.""" + averageLifespan: Int + + """ + Common eye colors for this species, null if this species does not typically + have eyes. + """ + eyeColors: [String] + + """ + Common hair colors for this species, null if this species does not typically + have hair. + """ + hairColors: [String] + + """ + Common skin colors for this species, null if this species does not typically + have skin. + """ + skinColors: [String] + + """The language commonly spoken by this species.""" + language: String + + """A planet that this species originates from.""" + homeworld: Planet + personConnection(after: String, first: Int, before: String, last: Int): SpeciesPeopleConnection + filmConnection(after: String, first: Int, before: String, last: Int): SpeciesFilmsConnection + + """The ISO 8601 date format of the time that this resource was created.""" + created: String + + """The ISO 8601 date format of the time that this resource was edited.""" + edited: String + + """The ID of an object""" + id: ID! + } + + """A connection to a list of items.""" + type SpeciesConnection { + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """A list of edges.""" + edges: [SpeciesEdge] + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int + + """ + A list of all of the objects returned in the connection. This is a convenience + field provided for quickly exploring the API; rather than querying for + "{ edges { node } }" when no edge data is needed, this field can be be used + instead. Note that when clients like Relay need to fetch the "cursor" field on + the edge to enable efficient pagination, this shortcut cannot be used, and the + full "{ edges { node } }" version should be used instead. + """ + species: [Species] + } + + """An edge in a connection.""" + type SpeciesEdge { + """The item at the end of the edge""" + node: Species + + """A cursor for use in pagination""" + cursor: String! + } + + """A connection to a list of items.""" + type SpeciesFilmsConnection { + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """A list of edges.""" + edges: [SpeciesFilmsEdge] + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int + + """ + A list of all of the objects returned in the connection. This is a convenience + field provided for quickly exploring the API; rather than querying for + "{ edges { node } }" when no edge data is needed, this field can be be used + instead. Note that when clients like Relay need to fetch the "cursor" field on + the edge to enable efficient pagination, this shortcut cannot be used, and the + full "{ edges { node } }" version should be used instead. + """ + films: [Film] + } + + """An edge in a connection.""" + type SpeciesFilmsEdge { + """The item at the end of the edge""" + node: Film + + """A cursor for use in pagination""" + cursor: String! + } + + """A connection to a list of items.""" + type SpeciesPeopleConnection { + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """A list of edges.""" + edges: [SpeciesPeopleEdge] + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int + + """ + A list of all of the objects returned in the connection. This is a convenience + field provided for quickly exploring the API; rather than querying for + "{ edges { node } }" when no edge data is needed, this field can be be used + instead. Note that when clients like Relay need to fetch the "cursor" field on + the edge to enable efficient pagination, this shortcut cannot be used, and the + full "{ edges { node } }" version should be used instead. + """ + people: [Person] + } + + """An edge in a connection.""" + type SpeciesPeopleEdge { + """The item at the end of the edge""" + node: Person + + """A cursor for use in pagination""" + cursor: String! + } + + """A single transport craft that has hyperdrive capability.""" + type Starship implements Node { + """The name of this starship. The common name, such as "Death Star".""" + name: String + + """ + The model or official name of this starship. Such as "T-65 X-wing" or "DS-1 + Orbital Battle Station". + """ + model: String + + """ + The class of this starship, such as "Starfighter" or "Deep Space Mobile + Battlestation" + """ + starshipClass: String + + """The manufacturers of this starship.""" + manufacturers: [String] + + """The cost of this starship new, in galactic credits.""" + costInCredits: Float + + """The length of this starship in meters.""" + length: Float + + """The number of personnel needed to run or pilot this starship.""" + crew: String + + """The number of non-essential people this starship can transport.""" + passengers: String + + """ + The maximum speed of this starship in atmosphere. null if this starship is + incapable of atmosphering flight. + """ + maxAtmospheringSpeed: Int + + """The class of this starships hyperdrive.""" + hyperdriveRating: Float + + """ + The Maximum number of Megalights this starship can travel in a standard hour. + A "Megalight" is a standard unit of distance and has never been defined before + within the Star Wars universe. This figure is only really useful for measuring + the difference in speed of starships. We can assume it is similar to AU, the + distance between our Sun (Sol) and Earth. + """ + MGLT: Int + + """The maximum number of kilograms that this starship can transport.""" + cargoCapacity: Float + + """ + The maximum length of time that this starship can provide consumables for its + entire crew without having to resupply. + """ + consumables: String + pilotConnection(after: String, first: Int, before: String, last: Int): StarshipPilotsConnection + filmConnection(after: String, first: Int, before: String, last: Int): StarshipFilmsConnection + + """The ISO 8601 date format of the time that this resource was created.""" + created: String + + """The ISO 8601 date format of the time that this resource was edited.""" + edited: String + + """The ID of an object""" + id: ID! + } + + """A connection to a list of items.""" + type StarshipFilmsConnection { + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """A list of edges.""" + edges: [StarshipFilmsEdge] + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int + + """ + A list of all of the objects returned in the connection. This is a convenience + field provided for quickly exploring the API; rather than querying for + "{ edges { node } }" when no edge data is needed, this field can be be used + instead. Note that when clients like Relay need to fetch the "cursor" field on + the edge to enable efficient pagination, this shortcut cannot be used, and the + full "{ edges { node } }" version should be used instead. + """ + films: [Film] + } + + """An edge in a connection.""" + type StarshipFilmsEdge { + """The item at the end of the edge""" + node: Film + + """A cursor for use in pagination""" + cursor: String! + } + + """A connection to a list of items.""" + type StarshipPilotsConnection { + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """A list of edges.""" + edges: [StarshipPilotsEdge] + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int + + """ + A list of all of the objects returned in the connection. This is a convenience + field provided for quickly exploring the API; rather than querying for + "{ edges { node } }" when no edge data is needed, this field can be be used + instead. Note that when clients like Relay need to fetch the "cursor" field on + the edge to enable efficient pagination, this shortcut cannot be used, and the + full "{ edges { node } }" version should be used instead. + """ + pilots: [Person] + } + + """An edge in a connection.""" + type StarshipPilotsEdge { + """The item at the end of the edge""" + node: Person + + """A cursor for use in pagination""" + cursor: String! + } + + """A connection to a list of items.""" + type StarshipsConnection { + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """A list of edges.""" + edges: [StarshipsEdge] + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int + + """ + A list of all of the objects returned in the connection. This is a convenience + field provided for quickly exploring the API; rather than querying for + "{ edges { node } }" when no edge data is needed, this field can be be used + instead. Note that when clients like Relay need to fetch the "cursor" field on + the edge to enable efficient pagination, this shortcut cannot be used, and the + full "{ edges { node } }" version should be used instead. + """ + starships: [Starship] + } + + """An edge in a connection.""" + type StarshipsEdge { + """The item at the end of the edge""" + node: Starship + + """A cursor for use in pagination""" + cursor: String! + } + + """A single transport craft that does not have hyperdrive capability""" + type Vehicle implements Node { + """ + The name of this vehicle. The common name, such as "Sand Crawler" or "Speeder + bike". + """ + name: String + + """ + The model or official name of this vehicle. Such as "All-Terrain Attack + Transport". + """ + model: String + + """The class of this vehicle, such as "Wheeled" or "Repulsorcraft".""" + vehicleClass: String + + """The manufacturers of this vehicle.""" + manufacturers: [String] + + """The cost of this vehicle new, in Galactic Credits.""" + costInCredits: Float + + """The length of this vehicle in meters.""" + length: Float + + """The number of personnel needed to run or pilot this vehicle.""" + crew: String + + """The number of non-essential people this vehicle can transport.""" + passengers: String + + """The maximum speed of this vehicle in atmosphere.""" + maxAtmospheringSpeed: Int + + """The maximum number of kilograms that this vehicle can transport.""" + cargoCapacity: Float + + """ + The maximum length of time that this vehicle can provide consumables for its + entire crew without having to resupply. + """ + consumables: String + pilotConnection(after: String, first: Int, before: String, last: Int): VehiclePilotsConnection + filmConnection(after: String, first: Int, before: String, last: Int): VehicleFilmsConnection + + """The ISO 8601 date format of the time that this resource was created.""" + created: String + + """The ISO 8601 date format of the time that this resource was edited.""" + edited: String + + """The ID of an object""" + id: ID! + } + + """A connection to a list of items.""" + type VehicleFilmsConnection { + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """A list of edges.""" + edges: [VehicleFilmsEdge] + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int + + """ + A list of all of the objects returned in the connection. This is a convenience + field provided for quickly exploring the API; rather than querying for + "{ edges { node } }" when no edge data is needed, this field can be be used + instead. Note that when clients like Relay need to fetch the "cursor" field on + the edge to enable efficient pagination, this shortcut cannot be used, and the + full "{ edges { node } }" version should be used instead. + """ + films: [Film] + } + + """An edge in a connection.""" + type VehicleFilmsEdge { + """The item at the end of the edge""" + node: Film + + """A cursor for use in pagination""" + cursor: String! + } + + """A connection to a list of items.""" + type VehiclePilotsConnection { + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """A list of edges.""" + edges: [VehiclePilotsEdge] + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int + + """ + A list of all of the objects returned in the connection. This is a convenience + field provided for quickly exploring the API; rather than querying for + "{ edges { node } }" when no edge data is needed, this field can be be used + instead. Note that when clients like Relay need to fetch the "cursor" field on + the edge to enable efficient pagination, this shortcut cannot be used, and the + full "{ edges { node } }" version should be used instead. + """ + pilots: [Person] + } + + """An edge in a connection.""" + type VehiclePilotsEdge { + """The item at the end of the edge""" + node: Person + + """A cursor for use in pagination""" + cursor: String! + } + + """A connection to a list of items.""" + type VehiclesConnection { + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """A list of edges.""" + edges: [VehiclesEdge] + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int + + """ + A list of all of the objects returned in the connection. This is a convenience + field provided for quickly exploring the API; rather than querying for + "{ edges { node } }" when no edge data is needed, this field can be be used + instead. Note that when clients like Relay need to fetch the "cursor" field on + the edge to enable efficient pagination, this shortcut cannot be used, and the + full "{ edges { node } }" version should be used instead. + """ + vehicles: [Vehicle] + } + + """An edge in a connection.""" + type VehiclesEdge { + """The item at the end of the edge""" + node: Vehicle + + """A cursor for use in pagination""" + cursor: String! + } diff --git a/packages/catalog-model/examples/components/artist-lookup-component.yaml b/packages/catalog-model/examples/components/artist-lookup-component.yaml index 3fc516ece9..fbf28d7fcb 100644 --- a/packages/catalog-model/examples/components/artist-lookup-component.yaml +++ b/packages/catalog-model/examples/components/artist-lookup-component.yaml @@ -6,6 +6,12 @@ metadata: tags: - java - data + links: + - url: https://example.com/apm/artists-lookup + title: APM + icon: dashboard + - url: https://example.com/logs/artists-lookup + title: Logs spec: type: service lifecycle: experimental diff --git a/packages/catalog-model/examples/domains/artists-domain.yaml b/packages/catalog-model/examples/domains/artists-domain.yaml index 7ac4ba66a1..2a74425791 100644 --- a/packages/catalog-model/examples/domains/artists-domain.yaml +++ b/packages/catalog-model/examples/domains/artists-domain.yaml @@ -4,7 +4,10 @@ metadata: name: artists description: Everything related to artists links: - - url: http://example.com/domain/readme - title: Artists Domain Readme + - url: http://example.com/domain/artists/ + title: Domain Readme + - url: http://example.com/domains/artists/dashboard + title: Domain Metrics Dashboard + icon: dashboard spec: owner: team-a From fb5d20e65470266ded213821b30917b48603391c Mon Sep 17 00:00:00 2001 From: Andrew Thauer <6507159+andrewthauer@users.noreply.github.com> Date: Thu, 28 Jan 2021 05:48:20 -0500 Subject: [PATCH 112/131] export entity link type --- packages/catalog-model/src/entity/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/catalog-model/src/entity/index.ts b/packages/catalog-model/src/entity/index.ts index d450119f0a..4afb8f5c96 100644 --- a/packages/catalog-model/src/entity/index.ts +++ b/packages/catalog-model/src/entity/index.ts @@ -20,6 +20,7 @@ export { } from './constants'; export type { Entity, + EntityLink, EntityMeta, EntityRelation, EntityRelationSpec, From 91fc991a7930ae4b4fc458652b800a12324d6eab Mon Sep 17 00:00:00 2001 From: Andrew Thauer <6507159+andrewthauer@users.noreply.github.com> Date: Thu, 28 Jan 2021 06:07:02 -0500 Subject: [PATCH 113/131] add entity link schema validation policy --- .../policies/SchemaValidEntityPolicy.test.ts | 22 +++++++++++++++++++ .../policies/SchemaValidEntityPolicy.ts | 3 ++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.test.ts b/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.test.ts index d63ba49dbe..23289d83aa 100644 --- a/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.test.ts +++ b/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.test.ts @@ -39,6 +39,10 @@ describe('SchemaValidEntityPolicy', () => { tags: - java - data + links: + - url: https://example.com + title: Website + icon: website spec: custom: stuff `); @@ -198,6 +202,24 @@ describe('SchemaValidEntityPolicy', () => { await expect(policy.enforce(data)).rejects.toThrow(/tags/); }); + it('accepts missing links', async () => { + delete data.metadata.links; + await expect(policy.enforce(data)).resolves.toBe(data); + }); + + it('accepts empty links array', async () => { + data.metadata.links = []; + await expect(policy.enforce(data)).resolves.toBe(data); + }); + + it.each([['invalid type'], [123], [{}], [{ url: 'https://foo' }]])( + 'rejects bad links type %s', + async (val: any) => { + data.metadata.links = val; + await expect(policy.enforce(data)).rejects.toThrow(/links/); + }, + ); + // // spec // diff --git a/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts b/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts index 4ff73e9a49..ed49bea526 100644 --- a/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts +++ b/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts @@ -16,7 +16,7 @@ import * as yup from 'yup'; import { EntityPolicy } from '../../types'; -import { Entity } from '../Entity'; +import { Entity, EntityLink } from '../Entity'; const DEFAULT_ENTITY_SCHEMA = yup .object({ @@ -33,6 +33,7 @@ const DEFAULT_ENTITY_SCHEMA = yup labels: yup.object>().notRequired(), annotations: yup.object>().notRequired(), tags: yup.array().notRequired(), + links: yup.array().notRequired(), }) .required(), spec: yup.object({}).notRequired(), From a515fc1d1184f6178bb61ca75c9c0edd22a8917a Mon Sep 17 00:00:00 2001 From: Andrew Thauer <6507159+andrewthauer@users.noreply.github.com> Date: Thu, 28 Jan 2021 07:16:22 -0500 Subject: [PATCH 114/131] add entity link field validation & tests --- .../policies/FieldFormatEntityPolicy.test.ts | 96 ++++++++++++++++++- .../policies/FieldFormatEntityPolicy.ts | 10 +- .../policies/SchemaValidEntityPolicy.test.ts | 2 +- .../CommonValidatorFunctions.test.ts | 40 ++++++++ .../validation/CommonValidatorFunctions.ts | 31 ++++++ .../src/validation/makeValidator.ts | 3 + .../catalog-model/src/validation/types.ts | 3 + 7 files changed, 180 insertions(+), 5 deletions(-) diff --git a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.test.ts b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.test.ts index 52a8207cc8..e5a56764e3 100644 --- a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.test.ts +++ b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.test.ts @@ -115,8 +115,98 @@ describe('FieldFormatEntityPolicy', () => { await expect(policy.enforce(data)).rejects.toThrow(/tags.*"Hello World"/i); }); - it('rejects bad link icon value', async () => { - data.metadata.links.push({ url: 'https://foo', icon: 'some icon' }); - await expect(policy.enforce(data)).rejects.toThrow(/links.*"some icon"/i); + it('accepts missing links', async () => { + delete data.metadata.links; + await expect(policy.enforce(data)).resolves.toBe(data); + }); + + it('accepts empty links array', async () => { + data.metadata.links = []; + await expect(policy.enforce(data)).resolves.toBe(data); + }); + + it('accepts multiple links', async () => { + data.metadata.links = [{ url: 'http://foo' }, { url: 'https://bar' }]; + await expect(policy.enforce(data)).resolves.toBe(data); + }); + + it('rejects missing link url value', async () => { + data.metadata.links = [{}]; + await expect(policy.enforce(data)).rejects.toThrow(/links.0/); + }); + + it('rejects a single bad missing url value', async () => { + data.metadata.links = [{ url: 'http://good' }, { url: '' }]; + await expect(policy.enforce(data)).rejects.toThrow(/links.1/); + }); + + it('rejects empty link url value', async () => { + data.metadata.links = [{ url: '' }]; + await expect(policy.enforce(data)).rejects.toThrow(/links.0/); + }); + + it('rejects bad link url value', async () => { + data.metadata.links = [{ url: 'invalid' }]; + await expect(policy.enforce(data)).rejects.toThrow(/links.0/); + }); + + it('accepts missing link title', async () => { + data.metadata.links = [{ url: 'http://foo', icon: 'dashboard' }]; + await expect(policy.enforce(data)).resolves.toBe(data); + }); + + it('rejects empty link title', async () => { + data.metadata.links = [{ url: 'http://foo', title: '' }]; + await expect(policy.enforce(data)).rejects.toThrow(/links.0/); + }); + + it.each([[123], [{}], [[]]])( + 'rejects bad link title %s', + async (title: unknown) => { + data.metadata.links = [{ url: 'http://foo', title }]; + await expect(policy.enforce(data)).rejects.toThrow(/links.0/); + }, + ); + + it('rejects a single bad link title', async () => { + data.metadata.links = [ + { url: 'http://foo', title: 'good' }, + { url: 'http://foo', title: '' }, + ]; + await expect(policy.enforce(data)).rejects.toThrow(/links.1/); + }); + + it('accepts missing link icon', async () => { + data.metadata.links = [{ url: 'http://foo', title: 'foo' }]; + await expect(policy.enforce(data)).resolves.toBe(data); + }); + + it('rejects empty link icon', async () => { + data.metadata.links = [{ url: 'http://foo', icon: '' }]; + await expect(policy.enforce(data)).rejects.toThrow(/links.0/); + }); + + it.each([['dashboard'], ['admin-dashboard'], ['foo_dashboard']])( + 'accepts valid link icon', + async icon => { + data.metadata.links = [{ url: 'http://foo', icon }]; + await expect(policy.enforce(data)).resolves.toBe(data); + }, + ); + + it.each([[123], [{}], [[]], ['abc xyz']])( + 'rejects bad link icon value %s', + async (icon: unknown) => { + data.metadata.links = [{ url: 'http://foo', icon }]; + await expect(policy.enforce(data)).rejects.toThrow(/links.0/); + }, + ); + + it('rejects a single bad link icon value', async () => { + data.metadata.links = [ + { url: 'http://foo', icon: 'good' }, + { url: 'http://foo', icon: 'not good' }, + ]; + await expect(policy.enforce(data)).rejects.toThrow(/links.1/); }); }); diff --git a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts index 09ccca6506..d854a3d514 100644 --- a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts +++ b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts @@ -83,6 +83,12 @@ export class FieldFormatEntityPolicy implements EntityPolicy { expectation = 'a string that is a sequence of [a-zA-Z][a-z0-9A-Z], at most 63 characters in total'; break; + case 'isValidUrl': + expectation = 'a string that is a valid url'; + break; + case 'isValidString': + expectation = 'a non empty string'; + break; default: expectation = undefined; break; @@ -137,7 +143,9 @@ export class FieldFormatEntityPolicy implements EntityPolicy { const links = entity.metadata.links ?? []; for (let i = 0; i < links.length; ++i) { - optional(`links.${i}`, links[i]?.icon, this.validators.isValidEntityName); + require(`links.${i}`, links[i]?.url, this.validators.isValidLinkUrl); + optional(`links.${i}`, links[i]?.title, this.validators.isValidLinkTitle); + optional(`links.${i}`, links[i]?.icon, this.validators.isValidLinkIcon); } return entity; diff --git a/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.test.ts b/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.test.ts index 23289d83aa..c84bdbdd38 100644 --- a/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.test.ts +++ b/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.test.ts @@ -214,7 +214,7 @@ describe('SchemaValidEntityPolicy', () => { it.each([['invalid type'], [123], [{}], [{ url: 'https://foo' }]])( 'rejects bad links type %s', - async (val: any) => { + async (val: unknown) => { data.metadata.links = val; await expect(policy.enforce(data)).rejects.toThrow(/links/); }, diff --git a/packages/catalog-model/src/validation/CommonValidatorFunctions.test.ts b/packages/catalog-model/src/validation/CommonValidatorFunctions.test.ts index 129fdd2b62..bef997d54a 100644 --- a/packages/catalog-model/src/validation/CommonValidatorFunctions.test.ts +++ b/packages/catalog-model/src/validation/CommonValidatorFunctions.test.ts @@ -161,4 +161,44 @@ describe('CommonValidatorFunctions', () => { ])(`isValidDnsLabel %p ? %p`, (value, result) => { expect(CommonValidatorFunctions.isValidDnsLabel(value)).toBe(result); }); + + it.each([ + [null, false], + [7, false], + ['', false], + ['abc', false], + [' abc', false], + ['', false], + [{}, false], + ['http://foo', true], + ['https://www.foo.com/', true], + ['https://foo.com:8080', true], + ['https://foo:8080/page', true], + ['https://foo:8080/sub/page', true], + ['https://foo:8080/sub/page?query', true], + ['https://foo:8080/sub/page/?query=value', true], + ['https://foo:8080/sub/page/?query=value&', true], + ['https://foo:8080/sub/page/?query=value&another=val', true], + ['https://foo.com/page#fragment', true], + ['ftp://ftp.some.domain.com/path', true], + ['xyz://custom-protocol:4444/path', true], + ])(`isValidUrl %p ? %p`, (value, result) => { + expect(CommonValidatorFunctions.isValidUrl(value)).toBe(result); + }); + + it.each([ + [null, false], + [true, false], + [7, false], + [{}, false], + ['', false], + [' ', false], + [' ', false], + ['abc', true], + [' abc ', true], + ['abc xyz', true], + ['abc xyz abc.', true], + ])(`isValidString %p ? %p`, (value, result) => { + expect(CommonValidatorFunctions.isValidString(value)).toBe(result); + }); }); diff --git a/packages/catalog-model/src/validation/CommonValidatorFunctions.ts b/packages/catalog-model/src/validation/CommonValidatorFunctions.ts index 90b4e1a611..a53a102c00 100644 --- a/packages/catalog-model/src/validation/CommonValidatorFunctions.ts +++ b/packages/catalog-model/src/validation/CommonValidatorFunctions.ts @@ -92,4 +92,35 @@ export class CommonValidatorFunctions { /^[a-z0-9]+(\-[a-z0-9]+)*$/.test(value) ); } + + /** + * Checks that the value is a valid URL. + * + * @param value The value to check + */ + static isValidUrl(value: unknown): boolean { + if (typeof value !== 'string') { + return false; + } + + try { + const url = new URL(value); + return !!url; + } catch (_) { + return false; + } + } + + /** + * Checks that the value is a non empty string value. + * + * @param value The value to check + */ + static isValidString(value: unknown): boolean { + return ( + typeof value === 'string' && + value.length >= 1 && + /^(?!\s*$).+/.test(value) + ); + } } diff --git a/packages/catalog-model/src/validation/makeValidator.ts b/packages/catalog-model/src/validation/makeValidator.ts index 63341f5c33..e910b25f2a 100644 --- a/packages/catalog-model/src/validation/makeValidator.ts +++ b/packages/catalog-model/src/validation/makeValidator.ts @@ -28,6 +28,9 @@ const defaultValidators: Validators = { isValidAnnotationKey: KubernetesValidatorFunctions.isValidAnnotationKey, isValidAnnotationValue: KubernetesValidatorFunctions.isValidAnnotationValue, isValidTag: CommonValidatorFunctions.isValidDnsLabel, + isValidLinkUrl: CommonValidatorFunctions.isValidUrl, + isValidLinkTitle: CommonValidatorFunctions.isValidString, + isValidLinkIcon: KubernetesValidatorFunctions.isValidObjectName, }; export function makeValidator(overrides: Partial = {}): Validators { diff --git a/packages/catalog-model/src/validation/types.ts b/packages/catalog-model/src/validation/types.ts index a4475c4809..9c7c173ea3 100644 --- a/packages/catalog-model/src/validation/types.ts +++ b/packages/catalog-model/src/validation/types.ts @@ -24,4 +24,7 @@ export type Validators = { isValidAnnotationKey(value: unknown): boolean; isValidAnnotationValue(value: unknown): boolean; isValidTag(value: unknown): boolean; + isValidLinkUrl(value: unknown): boolean; + isValidLinkTitle(value: unknown): boolean; + isValidLinkIcon(value: unknown): boolean; }; From 232f8a58ea81731d620cb723b7580817bc842b11 Mon Sep 17 00:00:00 2001 From: Andrew Thauer <6507159+andrewthauer@users.noreply.github.com> Date: Thu, 28 Jan 2021 07:28:25 -0500 Subject: [PATCH 115/131] add entity links example yaml --- docs/features/software-catalog/descriptor-format.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/features/software-catalog/descriptor-format.md b/docs/features/software-catalog/descriptor-format.md index 1134fbb61a..469085a4c5 100644 --- a/docs/features/software-catalog/descriptor-format.md +++ b/docs/features/software-catalog/descriptor-format.md @@ -86,6 +86,10 @@ metadata: circleci.com/project-slug: github/example-org/artist-website tags: - java + links: + url: https://admin.example-org.com + title: Admin Dashboard + icon: dashboard spec: type: website lifecycle: production From f69f62738f08f59d27a87169e6804c7791a5be33 Mon Sep 17 00:00:00 2001 From: Andrew Thauer <6507159+andrewthauer@users.noreply.github.com> Date: Thu, 28 Jan 2021 07:28:52 -0500 Subject: [PATCH 116/131] update changeset for entity links --- .changeset/fuzzy-pumpkins-tie.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/fuzzy-pumpkins-tie.md b/.changeset/fuzzy-pumpkins-tie.md index 9b4a928c7c..1e87972ce1 100644 --- a/.changeset/fuzzy-pumpkins-tie.md +++ b/.changeset/fuzzy-pumpkins-tie.md @@ -1,5 +1,5 @@ --- -'@backstage/catalog-model': minor +'@backstage/catalog-model': patch --- Adds a new optional `links` metadata field to the Entity class within the `catalog-model` package (as discussed in [[RFC] Entity Links](https://github.com/backstage/backstage/issues/3787)). This PR adds support for the entity links only. Follow up PR's will introduce the UI component to display them. From 87411c4036d599321d1a5afd15f46ecc6e13850f Mon Sep 17 00:00:00 2001 From: Andrew Thauer <6507159+andrewthauer@users.noreply.github.com> Date: Thu, 28 Jan 2021 10:14:18 -0500 Subject: [PATCH 117/131] Update docs/features/software-catalog/descriptor-format.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw --- docs/features/software-catalog/descriptor-format.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/software-catalog/descriptor-format.md b/docs/features/software-catalog/descriptor-format.md index 469085a4c5..70376ca2d7 100644 --- a/docs/features/software-catalog/descriptor-format.md +++ b/docs/features/software-catalog/descriptor-format.md @@ -326,7 +326,7 @@ in total. ### `links` [optional] A list of external hyperlinks related to the entity. Links can provide -additional contextual information that may be located outside of backstage +additional contextual information that may be located outside of Backstage itself. For example, an admin dashboard or external CMS page. Users may add links to descriptor YAML files to provide additional reference From 18c6261ddec75b9106e170e4d81104eb6787fb4c Mon Sep 17 00:00:00 2001 From: Andrew Thauer <6507159+andrewthauer@users.noreply.github.com> Date: Thu, 28 Jan 2021 10:14:28 -0500 Subject: [PATCH 118/131] Update docs/features/software-catalog/descriptor-format.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw --- docs/features/software-catalog/descriptor-format.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/software-catalog/descriptor-format.md b/docs/features/software-catalog/descriptor-format.md index 70376ca2d7..2357d62836 100644 --- a/docs/features/software-catalog/descriptor-format.md +++ b/docs/features/software-catalog/descriptor-format.md @@ -331,7 +331,7 @@ itself. For example, an admin dashboard or external CMS page. Users may add links to descriptor YAML files to provide additional reference information to external content & resources. Links are not intended to drive any -additional functionality within backstage, which is best left to `annotations` +additional functionality within Backstage, which is best left to `annotations` and `labels`. It is recommended to use links only when an equivalent well-known `annotation` does not cover a similar use case. From 34f76899bdf25db994146aaca3c9736efaa02316 Mon Sep 17 00:00:00 2001 From: Andrew Thauer <6507159+andrewthauer@users.noreply.github.com> Date: Thu, 28 Jan 2021 10:14:37 -0500 Subject: [PATCH 119/131] Update docs/features/software-catalog/descriptor-format.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw --- docs/features/software-catalog/descriptor-format.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/software-catalog/descriptor-format.md b/docs/features/software-catalog/descriptor-format.md index 2357d62836..a6bd4cd0a6 100644 --- a/docs/features/software-catalog/descriptor-format.md +++ b/docs/features/software-catalog/descriptor-format.md @@ -345,7 +345,7 @@ Fields of a link are: _NOTE_: The `icon` field value is meant to be a semantic key that will map to a specific icon that may be provided by an icon library (e.g. `material-ui` -icons). These keys should a sequence of `[a-z0-9A-Z]` and possibly separated by +icons). These keys should be a sequence of `[a-z0-9A-Z]`, possibly separated by one of `[-_.]`. Backstage may support some basic icons out of the box, but the backstage integrator will ultimately be left to provide the appropriate icon component mappings. A generic fallback icon would be provided if a mapping From 97e8e6936850223155172de5391ce63be9fea260 Mon Sep 17 00:00:00 2001 From: Andrew Thauer <6507159+andrewthauer@users.noreply.github.com> Date: Thu, 28 Jan 2021 10:14:45 -0500 Subject: [PATCH 120/131] Update docs/features/software-catalog/descriptor-format.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw --- docs/features/software-catalog/descriptor-format.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/software-catalog/descriptor-format.md b/docs/features/software-catalog/descriptor-format.md index a6bd4cd0a6..6e3ac37657 100644 --- a/docs/features/software-catalog/descriptor-format.md +++ b/docs/features/software-catalog/descriptor-format.md @@ -347,7 +347,7 @@ _NOTE_: The `icon` field value is meant to be a semantic key that will map to a specific icon that may be provided by an icon library (e.g. `material-ui` icons). These keys should be a sequence of `[a-z0-9A-Z]`, possibly separated by one of `[-_.]`. Backstage may support some basic icons out of the box, but the -backstage integrator will ultimately be left to provide the appropriate icon +Backstage integrator will ultimately be left to provide the appropriate icon component mappings. A generic fallback icon would be provided if a mapping cannot be resolved. From 1362709953d11c064f38e7cc71df5599a99aa48a Mon Sep 17 00:00:00 2001 From: Andrew Thauer <6507159+andrewthauer@users.noreply.github.com> Date: Thu, 28 Jan 2021 10:21:10 -0500 Subject: [PATCH 121/131] fix entity link examples --- docs/features/software-catalog/descriptor-format.md | 6 +++--- packages/catalog-model/examples/acme/org.yaml | 4 ++-- packages/catalog-model/examples/apis/petstore-api.yaml | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/features/software-catalog/descriptor-format.md b/docs/features/software-catalog/descriptor-format.md index 6e3ac37657..1aab1268cd 100644 --- a/docs/features/software-catalog/descriptor-format.md +++ b/docs/features/software-catalog/descriptor-format.md @@ -87,9 +87,9 @@ metadata: tags: - java links: - url: https://admin.example-org.com - title: Admin Dashboard - icon: dashboard + - url: https://admin.example-org.com + title: Admin Dashboard + icon: dashboard spec: type: website lifecycle: production diff --git a/packages/catalog-model/examples/acme/org.yaml b/packages/catalog-model/examples/acme/org.yaml index f0a5003f7c..9a0d690b57 100644 --- a/packages/catalog-model/examples/acme/org.yaml +++ b/packages/catalog-model/examples/acme/org.yaml @@ -5,9 +5,9 @@ metadata: description: The acme-corp organization links: - url: http://www.acme.com/ - name: Website + title: Website - url: https://meta.wikimedia.org/wiki/ - name: Intranet + title: Intranet spec: type: organization profile: diff --git a/packages/catalog-model/examples/apis/petstore-api.yaml b/packages/catalog-model/examples/apis/petstore-api.yaml index 4c18b16a16..5b6878f7d3 100644 --- a/packages/catalog-model/examples/apis/petstore-api.yaml +++ b/packages/catalog-model/examples/apis/petstore-api.yaml @@ -12,7 +12,7 @@ metadata: icon: github - url: https://github.com/OAI/OpenAPI-Specification/blob/master/examples/v3.0/petstore.yaml title: API Spec - code: code + icon: code spec: type: openapi lifecycle: experimental From 13c59b82b37fb52a76ecb1cd1983e67447a0d106 Mon Sep 17 00:00:00 2001 From: Andrew Thauer <6507159+andrewthauer@users.noreply.github.com> Date: Thu, 28 Jan 2021 10:27:22 -0500 Subject: [PATCH 122/131] make isValidString use trim instead of regex --- .../src/validation/CommonValidatorFunctions.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/catalog-model/src/validation/CommonValidatorFunctions.ts b/packages/catalog-model/src/validation/CommonValidatorFunctions.ts index a53a102c00..84e6222457 100644 --- a/packages/catalog-model/src/validation/CommonValidatorFunctions.ts +++ b/packages/catalog-model/src/validation/CommonValidatorFunctions.ts @@ -106,7 +106,7 @@ export class CommonValidatorFunctions { try { const url = new URL(value); return !!url; - } catch (_) { + } catch { return false; } } @@ -117,10 +117,6 @@ export class CommonValidatorFunctions { * @param value The value to check */ static isValidString(value: unknown): boolean { - return ( - typeof value === 'string' && - value.length >= 1 && - /^(?!\s*$).+/.test(value) - ); + return typeof value === 'string' && value?.trim()?.length >= 1; } } From 059720015cf0a0371c070a9714ba2401ddb0777e Mon Sep 17 00:00:00 2001 From: Andrew Thauer <6507159+andrewthauer@users.noreply.github.com> Date: Thu, 28 Jan 2021 11:28:07 -0500 Subject: [PATCH 123/131] replace link validators with common functions --- .../policies/FieldFormatEntityPolicy.test.ts | 33 ++++++++++++------- .../policies/FieldFormatEntityPolicy.ts | 15 +++++++-- .../src/validation/makeValidator.ts | 3 -- .../catalog-model/src/validation/types.ts | 3 -- 4 files changed, 34 insertions(+), 20 deletions(-) diff --git a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.test.ts b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.test.ts index e5a56764e3..33ee045c1e 100644 --- a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.test.ts +++ b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.test.ts @@ -132,22 +132,26 @@ describe('FieldFormatEntityPolicy', () => { it('rejects missing link url value', async () => { data.metadata.links = [{}]; - await expect(policy.enforce(data)).rejects.toThrow(/links.0/); + await expect(policy.enforce(data)).rejects.toThrow(/links.0.url/i); }); - it('rejects a single bad missing url value', async () => { + it('rejects a single bad missing link url value', async () => { data.metadata.links = [{ url: 'http://good' }, { url: '' }]; - await expect(policy.enforce(data)).rejects.toThrow(/links.1/); + await expect(policy.enforce(data)).rejects.toThrow( + /links.1.url.*valid url/i, + ); }); it('rejects empty link url value', async () => { data.metadata.links = [{ url: '' }]; - await expect(policy.enforce(data)).rejects.toThrow(/links.0/); + await expect(policy.enforce(data)).rejects.toThrow(/links.0.url.*/i); }); it('rejects bad link url value', async () => { data.metadata.links = [{ url: 'invalid' }]; - await expect(policy.enforce(data)).rejects.toThrow(/links.0/); + await expect(policy.enforce(data)).rejects.toThrow( + /links.0.url.*"invalid"/i, + ); }); it('accepts missing link title', async () => { @@ -157,14 +161,19 @@ describe('FieldFormatEntityPolicy', () => { it('rejects empty link title', async () => { data.metadata.links = [{ url: 'http://foo', title: '' }]; - await expect(policy.enforce(data)).rejects.toThrow(/links.0/); + await expect(policy.enforce(data)).rejects.toThrow(/links.0.title.*""/i); + }); + + it('rejects bad link title', async () => { + data.metadata.links = [{ url: 'http://foo', title: 123 }]; + await expect(policy.enforce(data)).rejects.toThrow(/links.0.title.*"123"/i); }); it.each([[123], [{}], [[]]])( 'rejects bad link title %s', async (title: unknown) => { data.metadata.links = [{ url: 'http://foo', title }]; - await expect(policy.enforce(data)).rejects.toThrow(/links.0/); + await expect(policy.enforce(data)).rejects.toThrow(/links.0.title.*/i); }, ); @@ -173,7 +182,7 @@ describe('FieldFormatEntityPolicy', () => { { url: 'http://foo', title: 'good' }, { url: 'http://foo', title: '' }, ]; - await expect(policy.enforce(data)).rejects.toThrow(/links.1/); + await expect(policy.enforce(data)).rejects.toThrow(/links.1.title.*""/i); }); it('accepts missing link icon', async () => { @@ -183,7 +192,7 @@ describe('FieldFormatEntityPolicy', () => { it('rejects empty link icon', async () => { data.metadata.links = [{ url: 'http://foo', icon: '' }]; - await expect(policy.enforce(data)).rejects.toThrow(/links.0/); + await expect(policy.enforce(data)).rejects.toThrow(/links.0.icon.*""/i); }); it.each([['dashboard'], ['admin-dashboard'], ['foo_dashboard']])( @@ -198,7 +207,7 @@ describe('FieldFormatEntityPolicy', () => { 'rejects bad link icon value %s', async (icon: unknown) => { data.metadata.links = [{ url: 'http://foo', icon }]; - await expect(policy.enforce(data)).rejects.toThrow(/links.0/); + await expect(policy.enforce(data)).rejects.toThrow(/links.0.icon.*/i); }, ); @@ -207,6 +216,8 @@ describe('FieldFormatEntityPolicy', () => { { url: 'http://foo', icon: 'good' }, { url: 'http://foo', icon: 'not good' }, ]; - await expect(policy.enforce(data)).rejects.toThrow(/links.1/); + await expect(policy.enforce(data)).rejects.toThrow( + /links.1.icon.*"not good"/i, + ); }); }); diff --git a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts index d854a3d514..00add2f63c 100644 --- a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts +++ b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts @@ -143,9 +143,18 @@ export class FieldFormatEntityPolicy implements EntityPolicy { const links = entity.metadata.links ?? []; for (let i = 0; i < links.length; ++i) { - require(`links.${i}`, links[i]?.url, this.validators.isValidLinkUrl); - optional(`links.${i}`, links[i]?.title, this.validators.isValidLinkTitle); - optional(`links.${i}`, links[i]?.icon, this.validators.isValidLinkIcon); + require(`links.${i}.url`, links[i] + ?.url, CommonValidatorFunctions.isValidUrl); + optional( + `links.${i}.title`, + links[i]?.title, + CommonValidatorFunctions.isValidString, + ); + optional( + `links.${i}.icon`, + links[i]?.icon, + KubernetesValidatorFunctions.isValidObjectName, + ); } return entity; diff --git a/packages/catalog-model/src/validation/makeValidator.ts b/packages/catalog-model/src/validation/makeValidator.ts index e910b25f2a..63341f5c33 100644 --- a/packages/catalog-model/src/validation/makeValidator.ts +++ b/packages/catalog-model/src/validation/makeValidator.ts @@ -28,9 +28,6 @@ const defaultValidators: Validators = { isValidAnnotationKey: KubernetesValidatorFunctions.isValidAnnotationKey, isValidAnnotationValue: KubernetesValidatorFunctions.isValidAnnotationValue, isValidTag: CommonValidatorFunctions.isValidDnsLabel, - isValidLinkUrl: CommonValidatorFunctions.isValidUrl, - isValidLinkTitle: CommonValidatorFunctions.isValidString, - isValidLinkIcon: KubernetesValidatorFunctions.isValidObjectName, }; export function makeValidator(overrides: Partial = {}): Validators { diff --git a/packages/catalog-model/src/validation/types.ts b/packages/catalog-model/src/validation/types.ts index 9c7c173ea3..a4475c4809 100644 --- a/packages/catalog-model/src/validation/types.ts +++ b/packages/catalog-model/src/validation/types.ts @@ -24,7 +24,4 @@ export type Validators = { isValidAnnotationKey(value: unknown): boolean; isValidAnnotationValue(value: unknown): boolean; isValidTag(value: unknown): boolean; - isValidLinkUrl(value: unknown): boolean; - isValidLinkTitle(value: unknown): boolean; - isValidLinkIcon(value: unknown): boolean; }; From f11c9060786001e4ee5f1ee6eac0cbd75f4dda9f Mon Sep 17 00:00:00 2001 From: Andrew Thauer <6507159+andrewthauer@users.noreply.github.com> Date: Thu, 28 Jan 2021 12:21:54 -0500 Subject: [PATCH 124/131] update isvalidurl --- .../catalog-model/src/validation/CommonValidatorFunctions.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/catalog-model/src/validation/CommonValidatorFunctions.ts b/packages/catalog-model/src/validation/CommonValidatorFunctions.ts index 84e6222457..87b6ad3838 100644 --- a/packages/catalog-model/src/validation/CommonValidatorFunctions.ts +++ b/packages/catalog-model/src/validation/CommonValidatorFunctions.ts @@ -104,8 +104,9 @@ export class CommonValidatorFunctions { } try { - const url = new URL(value); - return !!url; + // eslint-disable-next-line no-new + new URL(value); + return true; } catch { return false; } From 062df71db1c5878866c2ee3a193f6bca516e9ce9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 27 Jan 2021 20:57:35 +0100 Subject: [PATCH 125/131] config-loader: Bump to ajv 7, to enable v7 feature use elsewhere --- .changeset/metal-pans-leave.md | 5 +++ packages/cli/package.json | 4 ++- packages/config-loader/package.json | 2 +- .../src/lib/schema/compile.test.ts | 12 +++---- .../config-loader/src/lib/schema/compile.ts | 12 ++++--- .../src/lib/schema/filtering.test.ts | 36 +++++++++---------- .../config-loader/src/lib/schema/filtering.ts | 4 +-- .../config-loader/src/lib/schema/load.test.ts | 2 +- 8 files changed, 43 insertions(+), 34 deletions(-) create mode 100644 .changeset/metal-pans-leave.md diff --git a/.changeset/metal-pans-leave.md b/.changeset/metal-pans-leave.md new file mode 100644 index 0000000000..249ffe7aec --- /dev/null +++ b/.changeset/metal-pans-leave.md @@ -0,0 +1,5 @@ +--- +'@backstage/config-loader': patch +--- + +Bump config-loader to ajv 7, to enable v7 feature use elsewhere diff --git a/packages/cli/package.json b/packages/cli/package.json index ad45a33d93..35ad049741 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -173,7 +173,9 @@ "type": "string", "visibility": "frontend", "description": "Tracking ID for Google Analytics", - "example": "UA-000000-0" + "examples": [ + "UA-000000-0" + ] }, "listen": { "type": "object", diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json index 45ceb36e19..3387477231 100644 --- a/packages/config-loader/package.json +++ b/packages/config-loader/package.json @@ -32,7 +32,7 @@ "dependencies": { "@backstage/cli-common": "^0.1.1", "@backstage/config": "^0.1.1", - "ajv": "^6.12.5", + "ajv": "^7.0.3", "fs-extra": "^9.0.0", "json-schema": "^0.2.5", "json-schema-merge-allof": "^0.7.0", diff --git a/packages/config-loader/src/lib/schema/compile.test.ts b/packages/config-loader/src/lib/schema/compile.test.ts index f03d7e6d10..e1d8ee5999 100644 --- a/packages/config-loader/src/lib/schema/compile.test.ts +++ b/packages/config-loader/src/lib/schema/compile.test.ts @@ -29,11 +29,11 @@ describe('compileConfigSchemas', () => { }, ]); expect(validate([{ data: { a: 1 }, context: 'test' }])).toEqual({ - errors: ['Config should be string { type=string } at .a'], + errors: ['Config should be string { type=string } at /a'], visibilityByPath: new Map(), }); expect(validate([{ data: { b: 'b' }, context: 'test' }])).toEqual({ - errors: ['Config should be number { type=number } at .b'], + errors: ['Config should be number { type=number } at /b'], visibilityByPath: new Map(), }); }); @@ -80,10 +80,10 @@ describe('compileConfigSchemas', () => { ).toEqual({ visibilityByPath: new Map( Object.entries({ - '.a': 'frontend', - '.b': 'secret', - '.d': 'secret', - '.d.0': 'frontend', + '/a': 'frontend', + '/b': 'secret', + '/d': 'secret', + '/d/0': 'frontend', }), ), }); diff --git a/packages/config-loader/src/lib/schema/compile.ts b/packages/config-loader/src/lib/schema/compile.ts index f01a4f640a..2607d51f80 100644 --- a/packages/config-loader/src/lib/schema/compile.ts +++ b/packages/config-loader/src/lib/schema/compile.ts @@ -42,23 +42,25 @@ export function compileConfigSchemas( const ajv = new Ajv({ allErrors: true, + allowUnionTypes: true, schemas: { 'https://backstage.io/schema/config-v1': true, }, - }).addKeyword('visibility', { + }).addKeyword({ + keyword: 'visibility', metaSchema: { type: 'string', enum: CONFIG_VISIBILITIES, }, compile(visibility: ConfigVisibility) { - return (_data, dataPath) => { - if (!dataPath) { + return (_data, context) => { + if (context?.dataPath === undefined) { return false; } if (visibility && visibility !== 'backend') { - const normalizedPath = dataPath.replace( + const normalizedPath = context.dataPath.replace( /\['?(.*?)'?\]/g, - (_, segment) => `.${segment}`, + (_, segment) => `/${segment}`, ); visibilityByPath.set(normalizedPath, visibility); } diff --git a/packages/config-loader/src/lib/schema/filtering.test.ts b/packages/config-loader/src/lib/schema/filtering.test.ts index f6926e3560..feb31ebab0 100644 --- a/packages/config-loader/src/lib/schema/filtering.test.ts +++ b/packages/config-loader/src/lib/schema/filtering.test.ts @@ -40,24 +40,24 @@ const data = { const visibility = new Map( Object.entries({ - '.arr.0': 'frontend', - '.arr.1': 'backend', - '.arr.2': 'secret', - '.obj.f': 'frontend', - '.obj.b': 'backend', - '.obj.b.s': 'secret', - '.objArr.0.f': 'frontend', - '.objArr.0.b': 'backend', - '.objArr.0.s': 'secret', - '.objArr.1.f': 'frontend', - '.objArr.1.b': 'backend', - '.objArr.1.s': 'secret', - '.arrF': 'frontend', - '.arrB': 'backend', - '.arrS': 'secret', - '.objF': 'frontend', - '.objB': 'backend', - '.objS': 'secret', + '/arr/0': 'frontend', + '/arr/1': 'backend', + '/arr/2': 'secret', + '/obj/f': 'frontend', + '/obj/b': 'backend', + '/obj/b/s': 'secret', + '/objArr/0/f': 'frontend', + '/objArr/0/b': 'backend', + '/objArr/0/s': 'secret', + '/objArr/1/f': 'frontend', + '/objArr/1/b': 'backend', + '/objArr/1/s': 'secret', + '/arrF': 'frontend', + '/arrB': 'backend', + '/arrS': 'secret', + '/objF': 'frontend', + '/objB': 'backend', + '/objS': 'secret', }), ); diff --git a/packages/config-loader/src/lib/schema/filtering.ts b/packages/config-loader/src/lib/schema/filtering.ts index 10a97f9a7f..93e899c9d8 100644 --- a/packages/config-loader/src/lib/schema/filtering.ts +++ b/packages/config-loader/src/lib/schema/filtering.ts @@ -49,7 +49,7 @@ export function filterByVisibility( const arr = new Array(); for (const [index, value] of jsonVal.entries()) { - const out = transform(value, `${path}.${index}`); + const out = transform(value, `${path}/${index}`); if (out !== undefined) { arr.push(out); } @@ -68,7 +68,7 @@ export function filterByVisibility( if (value === undefined) { continue; } - const out = transform(value, `${path}.${key}`); + const out = transform(value, `${path}/${key}`); if (out !== undefined) { outObj[key] = out; hasOutput = true; diff --git a/packages/config-loader/src/lib/schema/load.test.ts b/packages/config-loader/src/lib/schema/load.test.ts index baf63525bb..7d9f7cc803 100644 --- a/packages/config-loader/src/lib/schema/load.test.ts +++ b/packages/config-loader/src/lib/schema/load.test.ts @@ -85,7 +85,7 @@ describe('loadConfigSchema', () => { expect(() => schema2.process([...configs, { data: { key1: 3 }, context: 'test2' }]), ).toThrow( - 'Config validation failed, Config should be string { type=string } at .key1', + 'Config validation failed, Config should be string { type=string } at /key1', ); await expect( From 7881f21174cafb91e69bf17d71066f34bc295c76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 27 Jan 2021 21:04:28 +0100 Subject: [PATCH 126/131] catalog-model: introduce jsonschema variants of the yup validation schemas --- .changeset/six-ravens-heal.md | 5 + packages/catalog-model/package.json | 3 +- .../catalog-model/schema/Entity.schema.json | 67 +++++++++++++ .../schema/EntityMeta.schema.json | 89 +++++++++++++++++ .../schema/kinds/API.v1alpha1.schema.json | 79 +++++++++++++++ .../kinds/Component.v1alpha1.schema copy.json | 93 ++++++++++++++++++ .../schema/kinds/Domain.v1alpha1.schema.json | 47 +++++++++ .../schema/kinds/Group.v1alpha1.schema.json | 95 +++++++++++++++++++ .../kinds/Location.v1alpha1.schema.json | 68 +++++++++++++ .../kinds/Resource.v1alpha1.schema.json | 60 ++++++++++++ .../schema/kinds/System.v1alpha1.schema.json | 54 +++++++++++ .../kinds/Template.v1alpha1.schema.json | 94 ++++++++++++++++++ .../schema/kinds/User.v1alpha1.schema.json | 80 ++++++++++++++++ .../schema/shared/common.schema.json | 50 ++++++++++ 14 files changed, 883 insertions(+), 1 deletion(-) create mode 100644 .changeset/six-ravens-heal.md create mode 100644 packages/catalog-model/schema/Entity.schema.json create mode 100644 packages/catalog-model/schema/EntityMeta.schema.json create mode 100644 packages/catalog-model/schema/kinds/API.v1alpha1.schema.json create mode 100644 packages/catalog-model/schema/kinds/Component.v1alpha1.schema copy.json create mode 100644 packages/catalog-model/schema/kinds/Domain.v1alpha1.schema.json create mode 100644 packages/catalog-model/schema/kinds/Group.v1alpha1.schema.json create mode 100644 packages/catalog-model/schema/kinds/Location.v1alpha1.schema.json create mode 100644 packages/catalog-model/schema/kinds/Resource.v1alpha1.schema.json create mode 100644 packages/catalog-model/schema/kinds/System.v1alpha1.schema.json create mode 100644 packages/catalog-model/schema/kinds/Template.v1alpha1.schema.json create mode 100644 packages/catalog-model/schema/kinds/User.v1alpha1.schema.json create mode 100644 packages/catalog-model/schema/shared/common.schema.json diff --git a/.changeset/six-ravens-heal.md b/.changeset/six-ravens-heal.md new file mode 100644 index 0000000000..91ed92c89c --- /dev/null +++ b/.changeset/six-ravens-heal.md @@ -0,0 +1,5 @@ +--- +'@backstage/catalog-model': patch +--- + +Introduce jsonschema variants of the yup vlidation schemas diff --git a/packages/catalog-model/package.json b/packages/catalog-model/package.json index 0b05f489a1..56d167d390 100644 --- a/packages/catalog-model/package.json +++ b/packages/catalog-model/package.json @@ -45,6 +45,7 @@ "yaml": "^1.9.2" }, "files": [ - "dist" + "dist", + "schema" ] } diff --git a/packages/catalog-model/schema/Entity.schema.json b/packages/catalog-model/schema/Entity.schema.json new file mode 100644 index 0000000000..803e025796 --- /dev/null +++ b/packages/catalog-model/schema/Entity.schema.json @@ -0,0 +1,67 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "Entity", + "description": "The format envelope that's common to all versions/kinds of entity.", + "examples": [ + { + "apiVersion": "backstage.io/v1alpha1", + "kind": "Component", + "metadata": { + "name": "LoremService", + "description": "Creates Lorems like a pro.", + "labels": { + "product_name": "Random value Generator" + }, + "annotations": { + "docs": "https://github.com/..../tree/develop/doc" + } + }, + "spec": { + "type": "service", + "lifecycle": "production", + "owner": "tools" + } + } + ], + "type": "object", + "required": ["apiVersion", "kind", "metadata"], + "additionalProperties": false, + "properties": { + "apiVersion": { + "type": "string", + "description": "The version of specification format for this particular entity that this is written against.", + "minLength": 1, + "examples": ["backstage.io/v1alpha1", "my-company.net/v1", "1.0"] + }, + "kind": { + "type": "string", + "description": "The high level entity type being described.", + "minLength": 1, + "examples": [ + "API", + "Component", + "Domain", + "Group", + "Location", + "Resource", + "System", + "Template", + "User" + ] + }, + "metadata": { + "$ref": "EntityMeta" + }, + "spec": { + "type": "object", + "description": "The specification data describing the entity itself." + }, + "relations": { + "type": "array", + "description": "The relations that this entity has with other entities.", + "items": { + "$ref": "common#relation" + } + } + } +} diff --git a/packages/catalog-model/schema/EntityMeta.schema.json b/packages/catalog-model/schema/EntityMeta.schema.json new file mode 100644 index 0000000000..d44ca84bf4 --- /dev/null +++ b/packages/catalog-model/schema/EntityMeta.schema.json @@ -0,0 +1,89 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "EntityMeta", + "description": "Metadata fields common to all versions/kinds of entity.", + "examples": [ + { + "uid": "e01199ab-08cc-44c2-8e19-5c29ded82521", + "etag": "lsndfkjsndfkjnsdfkjnsd==", + "generation": 13, + "name": "my-component-yay", + "namespace": "the-namespace", + "labels": { + "backstage.io/custom": "ValueStuff" + }, + "annotations": { + "example.com/bindings": "are-secret" + }, + "tags": ["java", "data"] + } + ], + "type": "object", + "required": ["name"], + "additionalProperties": true, + "properties": { + "uid": { + "type": "string", + "description": "A globally unique ID for the entity. This field can not be set by the user at creation time, and the server will reject an attempt to do so. The field will be populated in read operations. The field can (optionally) be specified when performing update or delete operations, but the server is free to reject requests that do so in such a way that it breaks semantics.", + "examples": ["e01199ab-08cc-44c2-8e19-5c29ded82521"], + "minLength": 1 + }, + "etag": { + "type": "string", + "description": "An opaque string that changes for each update operation to any part of the entity, including metadata. This field can not be set by the user at creation time, and the server will reject an attempt to do so. The field will be populated in read operations. The field can (optionally) be specified when performing update or delete operations, and the server will then reject the operation if it does not match the current stored value.", + "examples": ["lsndfkjsndfkjnsdfkjnsd=="], + "minLength": 1 + }, + "generation": { + "type": "integer", + "description": "A positive nonzero number that indicates the current generation of data for this entity; the value is incremented each time the spec changes. This field can not be set by the user at creation time, and the server will reject an attempt to do so. The field will be populated in read operations.", + "examples": [1], + "minimum": 1 + }, + "name": { + "type": "string", + "description": "The name of the entity. Must be unique within the catalog at any given point in time, for any given namespace + kind pair.", + "examples": ["metadata-proxy"], + "minLength": 1 + }, + "namespace": { + "type": "string", + "description": "The namespace that the entity belongs to.", + "default": "default", + "examples": ["default", "admin"], + "minLength": 1 + }, + "description": { + "type": "string", + "description": "A short (typically relatively few words, on one line) description of the entity." + }, + "labels": { + "type": "object", + "description": "Key/value pairs of identifying information attached to the entity.", + "additionalProperties": true, + "patternProperties": { + "^.+$": { + "type": "string" + } + } + }, + "annotations": { + "type": "object", + "description": "Key/value pairs of non-identifying auxiliary information attached to the entity.", + "additionalProperties": true, + "patternProperties": { + "^.+$": { + "type": "string" + } + } + }, + "tags": { + "type": "array", + "description": "A list of single-valued strings, to for example classify catalog entities in various ways.", + "items": { + "type": "string", + "minLength": 1 + } + } + } +} diff --git a/packages/catalog-model/schema/kinds/API.v1alpha1.schema.json b/packages/catalog-model/schema/kinds/API.v1alpha1.schema.json new file mode 100644 index 0000000000..fae1225764 --- /dev/null +++ b/packages/catalog-model/schema/kinds/API.v1alpha1.schema.json @@ -0,0 +1,79 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "ApiV1alpha1", + "description": "An API describes an interface that can be exposed by a component. The API can be defined in different formats, like OpenAPI, AsyncAPI, GraphQL, gRPC, or other formats.", + "examples": [ + { + "apiVersion": "backstage.io/v1alpha1", + "kind": "API", + "metadata": { + "name": "artist-api", + "description": "Retrieve artist details", + "labels": { + "product_name": "Random value Generator" + }, + "annotations": { + "docs": "https://github.com/..../tree/develop/doc" + } + }, + "spec": { + "type": "openapi", + "lifecycle": "production", + "owner": "artist-relations-team", + "system": "artist-engagement-portal", + "definition": "openapi: \"3.0.0\"\ninfo:..." + } + } + ], + "allOf": [ + { + "$ref": "Entity" + }, + { + "type": "object", + "required": ["spec"], + "properties": { + "apiVersion": { + "enum": ["backstage.io/v1alpha1", "backstage.io/v1beta1"] + }, + "kind": { + "enum": ["API"] + }, + "spec": { + "type": "object", + "required": ["type", "lifecycle", "owner", "definition"], + "properties": { + "type": { + "type": "string", + "description": "The type of the API definition.", + "examples": ["openapi", "asyncapi", "graphql", "grpc"], + "minLength": 1 + }, + "lifecycle": { + "type": "string", + "description": "The lifecycle state of the API.", + "examples": ["experimental", "production", "deprecated"], + "minLength": 1 + }, + "owner": { + "type": "string", + "description": "An entity reference to the owner of the API.", + "examples": ["artist-relations-team", "user:john.johnson"], + "minLength": 1 + }, + "system": { + "type": "string", + "description": "An entity reference to the system that the API belongs to.", + "minLength": 1 + }, + "definition": { + "type": "string", + "description": "The definition of the API, based on the format defined by the type.", + "minLength": 1 + } + } + } + } + } + ] +} diff --git a/packages/catalog-model/schema/kinds/Component.v1alpha1.schema copy.json b/packages/catalog-model/schema/kinds/Component.v1alpha1.schema copy.json new file mode 100644 index 0000000000..822059a0ea --- /dev/null +++ b/packages/catalog-model/schema/kinds/Component.v1alpha1.schema copy.json @@ -0,0 +1,93 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "ComponentV1alpha1", + "description": "A Component describes a software component. It is typically intimately linked to the source code that constitutes the component, and should be what a developer may regard a \"unit of software\", usually with a distinct deployable or linkable artifact.", + "examples": [ + { + "apiVersion": "backstage.io/v1alpha1", + "kind": "Component", + "metadata": { + "name": "LoremService", + "description": "Creates Lorems like a pro.", + "labels": { + "product_name": "Random value Generator" + }, + "annotations": { + "docs": "https://github.com/..../tree/develop/doc" + } + }, + "spec": { + "type": "service", + "lifecycle": "production", + "owner": "tools" + } + } + ], + "allOf": [ + { + "$ref": "Entity" + }, + { + "type": "object", + "required": ["spec"], + "properties": { + "apiVersion": { + "enum": ["backstage.io/v1alpha1", "backstage.io/v1beta1"] + }, + "kind": { + "enum": ["Component"] + }, + "spec": { + "type": "object", + "required": ["type", "lifecycle", "owner"], + "properties": { + "type": { + "type": "string", + "description": "The type of component.", + "examples": ["service", "website", "library"], + "minLength": 1 + }, + "lifecycle": { + "type": "string", + "description": "The lifecycle state of the component.", + "examples": ["experimental", "production", "deprecated"], + "minLength": 1 + }, + "owner": { + "type": "string", + "description": "An entity reference to the owner of the component.", + "examples": ["artist-relations-team", "user:john.johnson"], + "minLength": 1 + }, + "system": { + "type": "string", + "description": "An entity reference to the system that the component belongs to.", + "minLength": 1 + }, + "subcomponentOf": { + "type": "string", + "description": "An entity reference to another component of which the component is a part.", + "minLength": 1 + }, + "providesApis": { + "type": "array", + "description": "An array of entity references to the APIs that are provided by the component.", + "items": { + "type": "string", + "minLength": 1 + } + }, + "consumesApis": { + "type": "array", + "description": "An array of entity references to the APIs that are consumed by the component.", + "items": { + "type": "string", + "minLength": 1 + } + } + } + } + } + } + ] +} diff --git a/packages/catalog-model/schema/kinds/Domain.v1alpha1.schema.json b/packages/catalog-model/schema/kinds/Domain.v1alpha1.schema.json new file mode 100644 index 0000000000..a1a09b1213 --- /dev/null +++ b/packages/catalog-model/schema/kinds/Domain.v1alpha1.schema.json @@ -0,0 +1,47 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "DomainV1alpha1", + "description": "A Domain groups a collection of systems that share terminology, domain models, business purpose, or documentation, i.e. form a bounded context.", + "examples": [ + { + "apiVersion": "backstage.io/v1alpha1", + "kind": "Domain", + "metadata": { + "name": "artists", + "description": "Everything about artists" + }, + "spec": { + "owner": "artist-relations-team" + } + } + ], + "allOf": [ + { + "$ref": "Entity" + }, + { + "type": "object", + "required": ["spec"], + "properties": { + "apiVersion": { + "enum": ["backstage.io/v1alpha1", "backstage.io/v1beta1"] + }, + "kind": { + "enum": ["Domain"] + }, + "spec": { + "type": "object", + "required": ["owner"], + "properties": { + "owner": { + "type": "string", + "description": "An entity reference to the owner of the component.", + "examples": ["artist-relations-team", "user:john.johnson"], + "minLength": 1 + } + } + } + } + } + ] +} diff --git a/packages/catalog-model/schema/kinds/Group.v1alpha1.schema.json b/packages/catalog-model/schema/kinds/Group.v1alpha1.schema.json new file mode 100644 index 0000000000..fc19d98301 --- /dev/null +++ b/packages/catalog-model/schema/kinds/Group.v1alpha1.schema.json @@ -0,0 +1,95 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "GroupV1alpha1", + "description": "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.", + "examples": [ + { + "apiVersion": "backstage.io/v1alpha1", + "kind": "Group", + "metadata": { + "name": "infrastructure", + "description": "The infra business unit" + }, + "spec": { + "type": "business-unit", + "profile": { + "displayName": "Infrastructure", + "email": "infrastructure@example.com", + "picture": "https://example.com/groups/bu-infrastructure.jpeg" + }, + "parent": "ops", + "children": ["backstage", "other"] + } + } + ], + "allOf": [ + { + "$ref": "Entity" + }, + { + "type": "object", + "required": ["spec"], + "properties": { + "apiVersion": { + "enum": ["backstage.io/v1alpha1", "backstage.io/v1beta1"] + }, + "kind": { + "enum": ["Group"] + }, + "spec": { + "type": "object", + "required": ["type", "children"], + "properties": { + "type": { + "type": "string", + "description": "The type of group. 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.", + "examples": ["team", "business-unit", "product-area", "root"], + "minLength": 1 + }, + "profile": { + "type": "object", + "description": "Optional profile information about the group, mainly for display purposes. All fields of this structure are also optional. The email would be a group email of some form, that the group 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 group, and that a browser could fetch and render on a group page or similar.", + "properties": { + "displayName": { + "type": "string", + "description": "A simple display name to present to users.", + "examples": ["Infrastructure"], + "minLength": 1 + }, + "email": { + "type": "string", + "description": "An email where this entity can be reached.", + "examples": ["infrastructure@example.com"], + "minLength": 1 + }, + "picture": { + "type": "string", + "description": "The URL of an image that represents this entity.", + "examples": [ + "https://example.com/groups/bu-infrastructure.jpeg" + ], + "minLength": 1 + } + } + }, + "parent": { + "type": "string", + "description": "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.", + "examples": ["ops"], + "minLength": 1 + }, + "children": { + "type": "array", + "description": "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.", + "items": { + "type": "string", + "examples": ["backstage", "other"], + "minLength": 1 + } + } + } + } + } + } + ] +} diff --git a/packages/catalog-model/schema/kinds/Location.v1alpha1.schema.json b/packages/catalog-model/schema/kinds/Location.v1alpha1.schema.json new file mode 100644 index 0000000000..d633d30229 --- /dev/null +++ b/packages/catalog-model/schema/kinds/Location.v1alpha1.schema.json @@ -0,0 +1,68 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "LocationV1alpha1", + "description": "A location is a marker that references other places to look for catalog data.", + "examples": [ + { + "apiVersion": "backstage.io/v1alpha1", + "kind": "Location", + "metadata": { + "name": "org-data" + }, + "spec": { + "type": "url", + "targets": [ + "http://github.com/myorg/myproject/org-data-dump/catalog-info-staff.yaml", + "http://github.com/myorg/myproject/org-data-dump/catalog-info-consultants.yaml" + ] + } + } + ], + "allOf": [ + { + "$ref": "Entity" + }, + { + "type": "object", + "required": ["spec"], + "properties": { + "apiVersion": { + "enum": ["backstage.io/v1alpha1", "backstage.io/v1beta1"] + }, + "kind": { + "enum": ["Location"] + }, + "spec": { + "type": "object", + "required": [], + "properties": { + "type": { + "type": "string", + "description": "The single location type, that's common to the targets specified in the spec. If it is left out, it is inherited from the location type that originally read the entity data.", + "examples": ["url"], + "minLength": 1 + }, + "target": { + "type": "string", + "description": "A single target as a string. Can be either an absolute path/URL (depending on the type), or a relative path such as ./details/catalog-info.yaml which is resolved relative to the location of this Location entity itself.", + "examples": ["./details/catalog-info.yaml"], + "minLength": 1 + }, + "targets": { + "type": "array", + "description": "A list of targets as strings. They can all be either absolute paths/URLs (depending on the type), or relative paths such as ./details/catalog-info.yaml which are resolved relative to the location of this Location entity itself.", + "items": { + "type": "string", + "examples": [ + "./details/catalog-info.yaml", + "http://github.com/myorg/myproject/org-data-dump/catalog-info-staff.yaml" + ], + "minLength": 1 + } + } + } + } + } + } + ] +} diff --git a/packages/catalog-model/schema/kinds/Resource.v1alpha1.schema.json b/packages/catalog-model/schema/kinds/Resource.v1alpha1.schema.json new file mode 100644 index 0000000000..b426dd7f94 --- /dev/null +++ b/packages/catalog-model/schema/kinds/Resource.v1alpha1.schema.json @@ -0,0 +1,60 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "ResourceV1alpha1", + "description": "A resource describes the infrastructure a system needs to operate, like BigTable databases, Pub/Sub topics, S3 buckets or CDNs. Modelling them together with components and systems allows to visualize resource footprint, and create tooling around them.", + "examples": [ + { + "apiVersion": "backstage.io/v1alpha1", + "kind": "Resource", + "metadata": { + "name": "artists-db", + "description": "Stores artist details" + }, + "spec": { + "type": "database", + "owner": "artist-relations-team", + "system": "artist-engagement-portal" + } + } + ], + "allOf": [ + { + "$ref": "Entity" + }, + { + "type": "object", + "required": ["spec"], + "properties": { + "apiVersion": { + "enum": ["backstage.io/v1alpha1", "backstage.io/v1beta1"] + }, + "kind": { + "enum": ["Resource"] + }, + "spec": { + "type": "object", + "required": ["type", "owner"], + "properties": { + "type": { + "type": "string", + "description": "The type of resource.", + "examples": ["database", "s3-bucket", "cluster"], + "minLength": 1 + }, + "owner": { + "type": "string", + "description": "An entity reference to the owner of the resource.", + "examples": ["artist-relations-team", "user:john.johnson"], + "minLength": 1 + }, + "system": { + "type": "string", + "description": "An entity reference to the system that the resource belongs to.", + "minLength": 1 + } + } + } + } + } + ] +} diff --git a/packages/catalog-model/schema/kinds/System.v1alpha1.schema.json b/packages/catalog-model/schema/kinds/System.v1alpha1.schema.json new file mode 100644 index 0000000000..2cdbc37076 --- /dev/null +++ b/packages/catalog-model/schema/kinds/System.v1alpha1.schema.json @@ -0,0 +1,54 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "SystemV1alpha1", + "description": "A system is a collection of resources and components. The system may expose or consume one or several APIs. It is viewed as abstraction level that provides potential consumers insights into exposed features without needing a too detailed view into the details of all components. This also gives the owning team the possibility to decide about published artifacts and APIs.", + "examples": [ + { + "apiVersion": "backstage.io/v1alpha1", + "kind": "System", + "metadata": { + "name": "artist-engagement-portal", + "description": "Handy tools to keep artists in the loop" + }, + "spec": { + "owner": "artist-relations-team", + "domain": "artists" + } + } + ], + "allOf": [ + { + "$ref": "Entity" + }, + { + "type": "object", + "required": ["spec"], + "properties": { + "apiVersion": { + "enum": ["backstage.io/v1alpha1", "backstage.io/v1beta1"] + }, + "kind": { + "enum": ["System"] + }, + "spec": { + "type": "object", + "required": ["owner"], + "properties": { + "owner": { + "type": "string", + "description": "An entity reference to the owner of the component.", + "examples": ["artist-relations-team", "user:john.johnson"], + "minLength": 1 + }, + "domain": { + "type": "string", + "description": "An entity reference to the domain that the system belongs to.", + "examples": ["artists"], + "minLength": 1 + } + } + } + } + } + ] +} diff --git a/packages/catalog-model/schema/kinds/Template.v1alpha1.schema.json b/packages/catalog-model/schema/kinds/Template.v1alpha1.schema.json new file mode 100644 index 0000000000..5bb83d116b --- /dev/null +++ b/packages/catalog-model/schema/kinds/Template.v1alpha1.schema.json @@ -0,0 +1,94 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "TemplateV1alpha1", + "description": "A Template describes a skeleton for use with the Scaffolder. It is used for describing what templating library is supported, and also for documenting the variables that the template requires using JSON Forms Schema.", + "examples": [ + { + "apiVersion": "backstage.io/v1alpha1", + "kind": "Template", + "metadata": { + "name": "react-ssr-template", + "title": "React SSR Template", + "description": "Next.js application skeleton for creating isomorphic web applications.", + "tags": ["recommended", "react"] + }, + "spec": { + "owner": "artist-relations-team", + "templater": "cookiecutter", + "type": "website", + "path": ".", + "schema": { + "required": ["component-id", "description"], + "properties": { + "component_id": { + "title": "Name", + "type": "string", + "description": "Unique name of the component" + }, + "description": { + "title": "Description", + "type": "string", + "description": "Description of the component" + } + } + } + } + } + ], + "allOf": [ + { + "$ref": "Entity" + }, + { + "type": "object", + "required": ["spec"], + "properties": { + "apiVersion": { + "enum": ["backstage.io/v1alpha1", "backstage.io/v1beta1"] + }, + "kind": { + "enum": ["Template"] + }, + "metadata": { + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "The nice display name for the template. This field is required as is used to reference the template to the user instead of the metadata.name field.", + "examples": ["React SSR Template"], + "minLength": 1 + } + } + }, + "spec": { + "type": "object", + "required": ["type", "templater", "schema"], + "properties": { + "type": { + "type": "string", + "description": "The type of component. This field is optional but recommended. The software catalog accepts any type value, but an organization should take great care to establish a proper taxonomy for these. Tools including Backstage itself may read this field and behave differently depending on its value. For example, a website type component may present tooling in the Backstage interface that is specific to just websites.", + "examples": ["service", "website", "library"], + "minLength": 1 + }, + "templater": { + "type": "string", + "description": "The templating library that is supported by the template skeleton.", + "examples": ["cookiecutter"], + "minLength": 1 + }, + "path": { + "type": "string", + "description": "The string location where the templater should be run if it is not on the same level as the template.yaml definition.", + "examples": ["./cookiecutter/skeleton"], + "minLength": 1 + }, + "schema": { + "type": "object", + "description": "The JSONSchema describing the inputs for the template." + } + } + } + } + } + ] +} diff --git a/packages/catalog-model/schema/kinds/User.v1alpha1.schema.json b/packages/catalog-model/schema/kinds/User.v1alpha1.schema.json new file mode 100644 index 0000000000..a71409d5ba --- /dev/null +++ b/packages/catalog-model/schema/kinds/User.v1alpha1.schema.json @@ -0,0 +1,80 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "UserV1alpha1", + "description": "A user describes a person, such as an employee, a contractor, or similar. Users belong to Group entities in the catalog. These catalog user entries are connected to the way that authentication within the Backstage ecosystem works. See the auth section of the docs for a discussion of these concepts.", + "examples": [ + { + "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"] + } + } + ], + "allOf": [ + { + "$ref": "Entity" + }, + { + "type": "object", + "required": ["spec"], + "properties": { + "apiVersion": { + "enum": ["backstage.io/v1alpha1", "backstage.io/v1beta1"] + }, + "kind": { + "enum": ["User"] + }, + "spec": { + "type": "object", + "required": ["memberOf"], + "properties": { + "profile": { + "type": "object", + "description": "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.", + "properties": { + "displayName": { + "type": "string", + "description": "A simple display name to present to users.", + "examples": ["Jenny Doe"], + "minLength": 1 + }, + "email": { + "type": "string", + "description": "An email where this user can be reached.", + "examples": ["jenny-doe@example.com"], + "minLength": 1 + }, + "picture": { + "type": "string", + "description": "The URL of an image that represents this user.", + "examples": [ + "https://example.com/staff/jenny-with-party-hat.jpeg" + ], + "minLength": 1 + } + } + }, + "memberOf": { + "type": "array", + "description": "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.", + "items": { + "type": "string", + "examples": ["team-b", "employees"], + "minLength": 1 + } + } + } + } + } + } + ] +} diff --git a/packages/catalog-model/schema/shared/common.schema.json b/packages/catalog-model/schema/shared/common.schema.json new file mode 100644 index 0000000000..84ae8a4147 --- /dev/null +++ b/packages/catalog-model/schema/shared/common.schema.json @@ -0,0 +1,50 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "common", + "type": "object", + "description": "Common definitions to import from other schemas", + "definitions": { + "reference": { + "$id": "#reference", + "type": "object", + "description": "A reference by name to another entity.", + "required": ["kind", "namespace", "name"], + "additionalProperties": false, + "properties": { + "kind": { + "type": "string", + "description": "The kind field of the entity." + }, + "namespace": { + "type": "string", + "description": "The metadata.namespace field of the entity." + }, + "name": { + "type": "string", + "description": "The metadata.name field of the entity." + } + } + }, + "relation": { + "$id": "#relation", + "type": "object", + "description": "A directed relation from one entity to another.", + "required": ["type", "source", "target"], + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "minLength": 1, + "pattern": "^\\w+$", + "description": "The type of relation." + }, + "source": { + "$ref": "#reference" + }, + "target": { + "$ref": "#reference" + } + } + } + } +} From d822077dd69271c450c01b399af0f42413c69733 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 27 Jan 2021 21:13:02 +0100 Subject: [PATCH 127/131] catalog-model: unbreak EntityPolicy import cycle --- .../catalog-model/src/EntityPolicies.test.ts | 3 +- packages/catalog-model/src/EntityPolicies.ts | 3 +- .../policies/DefaultNamespaceEntityPolicy.ts | 2 +- .../policies/FieldFormatEntityPolicy.ts | 2 +- .../NoForeignRootFieldsEntityPolicy.ts | 2 +- .../policies/SchemaValidEntityPolicy.ts | 2 +- .../src/entity/policies/index.ts | 1 + .../src/entity/policies/types.ts | 33 +++++++++++++++++++ packages/catalog-model/src/index.ts | 2 +- packages/catalog-model/src/types.ts | 17 ---------- 10 files changed, 41 insertions(+), 26 deletions(-) create mode 100644 packages/catalog-model/src/entity/policies/types.ts diff --git a/packages/catalog-model/src/EntityPolicies.test.ts b/packages/catalog-model/src/EntityPolicies.test.ts index cd869f798c..b67180241e 100644 --- a/packages/catalog-model/src/EntityPolicies.test.ts +++ b/packages/catalog-model/src/EntityPolicies.test.ts @@ -1,4 +1,3 @@ -import { Entity } from './entity'; /* * Copyright 2020 Spotify AB * @@ -15,8 +14,8 @@ import { Entity } from './entity'; * limitations under the License. */ +import { Entity, EntityPolicy } from './entity'; import { EntityPolicies } from './EntityPolicies'; -import { EntityPolicy } from './types'; describe('EntityPolicies', () => { const p1: jest.Mocked = { enforce: jest.fn() }; diff --git a/packages/catalog-model/src/EntityPolicies.ts b/packages/catalog-model/src/EntityPolicies.ts index 1f304f8f27..2d576b1670 100644 --- a/packages/catalog-model/src/EntityPolicies.ts +++ b/packages/catalog-model/src/EntityPolicies.ts @@ -14,8 +14,7 @@ * limitations under the License. */ -import { Entity } from './entity'; -import { EntityPolicy } from './types'; +import { Entity, EntityPolicy } from './entity'; // Helper that requires that all of a set of policies can be successfully // applied diff --git a/packages/catalog-model/src/entity/policies/DefaultNamespaceEntityPolicy.ts b/packages/catalog-model/src/entity/policies/DefaultNamespaceEntityPolicy.ts index 3119164454..4f5bbe04f4 100644 --- a/packages/catalog-model/src/entity/policies/DefaultNamespaceEntityPolicy.ts +++ b/packages/catalog-model/src/entity/policies/DefaultNamespaceEntityPolicy.ts @@ -15,7 +15,7 @@ */ import lodash from 'lodash'; -import { EntityPolicy } from '../../types'; +import { EntityPolicy } from './types'; import { ENTITY_DEFAULT_NAMESPACE } from '../constants'; import { Entity } from '../Entity'; diff --git a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts index 00add2f63c..1ed13c972c 100644 --- a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts +++ b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { EntityPolicy } from '../../types'; +import { EntityPolicy } from './types'; import { CommonValidatorFunctions, KubernetesValidatorFunctions, diff --git a/packages/catalog-model/src/entity/policies/NoForeignRootFieldsEntityPolicy.ts b/packages/catalog-model/src/entity/policies/NoForeignRootFieldsEntityPolicy.ts index 9d1851bc02..7d401542ba 100644 --- a/packages/catalog-model/src/entity/policies/NoForeignRootFieldsEntityPolicy.ts +++ b/packages/catalog-model/src/entity/policies/NoForeignRootFieldsEntityPolicy.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { EntityPolicy } from '../../types'; +import { EntityPolicy } from './types'; import { Entity } from '../Entity'; const defaultKnownFields = ['apiVersion', 'kind', 'metadata', 'spec']; diff --git a/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts b/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts index ed49bea526..2796826b20 100644 --- a/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts +++ b/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts @@ -15,8 +15,8 @@ */ import * as yup from 'yup'; -import { EntityPolicy } from '../../types'; import { Entity, EntityLink } from '../Entity'; +import { EntityPolicy } from './types'; const DEFAULT_ENTITY_SCHEMA = yup .object({ diff --git a/packages/catalog-model/src/entity/policies/index.ts b/packages/catalog-model/src/entity/policies/index.ts index c381d29a23..5d75ef4d84 100644 --- a/packages/catalog-model/src/entity/policies/index.ts +++ b/packages/catalog-model/src/entity/policies/index.ts @@ -18,3 +18,4 @@ export { DefaultNamespaceEntityPolicy } from './DefaultNamespaceEntityPolicy'; export { FieldFormatEntityPolicy } from './FieldFormatEntityPolicy'; export { NoForeignRootFieldsEntityPolicy } from './NoForeignRootFieldsEntityPolicy'; export { SchemaValidEntityPolicy } from './SchemaValidEntityPolicy'; +export type { EntityPolicy } from './types'; diff --git a/packages/catalog-model/src/entity/policies/types.ts b/packages/catalog-model/src/entity/policies/types.ts new file mode 100644 index 0000000000..415c98bbd2 --- /dev/null +++ b/packages/catalog-model/src/entity/policies/types.ts @@ -0,0 +1,33 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { Entity } from '../Entity'; + +/** + * A policy for validation or mutation to be applied to entities as they are + * entering the system. + */ +export type EntityPolicy = { + /** + * Applies validation or mutation on an entity. + * + * @param entity The entity, as validated/mutated so far in the policy tree + * @returns The incoming entity, or a mutated version of the same, or + * undefined if this processor could not handle the entity + * @throws An error if the entity should be rejected + */ + enforce(entity: Entity): Promise; +}; diff --git a/packages/catalog-model/src/index.ts b/packages/catalog-model/src/index.ts index f93a4001a5..976b5f6148 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 { EntityName, EntityPolicy, EntityRef, JSONSchema } from './types'; +export type { EntityName, EntityRef, JSONSchema } from './types'; export * from './validation'; diff --git a/packages/catalog-model/src/types.ts b/packages/catalog-model/src/types.ts index aa96e46102..edac03466d 100644 --- a/packages/catalog-model/src/types.ts +++ b/packages/catalog-model/src/types.ts @@ -16,23 +16,6 @@ import { JsonValue } from '@backstage/config'; import { JSONSchema7 } from 'json-schema'; -import type { Entity } from './entity/Entity'; - -/** - * A policy for validation or mutation to be applied to entities as they are - * entering the system. - */ -export type EntityPolicy = { - /** - * Applies validation or mutation on an entity. - * - * @param entity The entity, as validated/mutated so far in the policy tree - * @returns The incoming entity, or a mutated version of the same, or - * undefined if this processor could not handle the entity - * @throws An error if the entity should be rejected - */ - enforce(entity: Entity): Promise; -}; export type JSONSchema = JSONSchema7 & { [key in string]?: JsonValue }; From 12ece98cdb7ae98e354b59eb9c2847dc85d7a6c3 Mon Sep 17 00:00:00 2001 From: vitorgrenzel Date: Thu, 28 Jan 2021 17:39:44 -0300 Subject: [PATCH 128/131] Add className to the SidebarItem --- .changeset/dirty-carrots-invent.md | 5 +++++ packages/core/package.json | 5 +++-- packages/core/src/layout/Sidebar/Items.test.tsx | 17 +++++++++++++++++ packages/core/src/layout/Sidebar/Items.tsx | 3 +++ 4 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 .changeset/dirty-carrots-invent.md diff --git a/.changeset/dirty-carrots-invent.md b/.changeset/dirty-carrots-invent.md new file mode 100644 index 0000000000..ceb4e4d2bb --- /dev/null +++ b/.changeset/dirty-carrots-invent.md @@ -0,0 +1,5 @@ +--- +'@backstage/core': patch +--- + +Add className to the SidebarItem diff --git a/packages/core/package.json b/packages/core/package.json index 325689e179..d4592d48ac 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -35,21 +35,22 @@ "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", + "@testing-library/react-hooks": "^3.4.2", "@types/dagre": "^0.7.44", + "@types/prop-types": "^15.7.3", "@types/react": "^16.9", "@types/react-sparklines": "^1.7.0", - "@types/prop-types": "^15.7.3", "classnames": "^2.2.6", "clsx": "^1.1.0", "d3-selection": "^2.0.0", "d3-shape": "^2.0.0", "d3-zoom": "^2.0.0", "dagre": "^0.8.5", - "qs": "^6.9.4", "immer": "^8.0.1", "lodash": "^4.17.15", "material-table": "^1.69.1", "prop-types": "^15.7.2", + "qs": "^6.9.4", "rc-progress": "^3.0.0", "react": "^16.12.0", "react-dom": "^16.12.0", diff --git a/packages/core/src/layout/Sidebar/Items.test.tsx b/packages/core/src/layout/Sidebar/Items.test.tsx index 099a3477d3..fb0ee174f8 100644 --- a/packages/core/src/layout/Sidebar/Items.test.tsx +++ b/packages/core/src/layout/Sidebar/Items.test.tsx @@ -22,8 +22,18 @@ import HomeIcon from '@material-ui/icons/Home'; import CreateComponentIcon from '@material-ui/icons/AddCircleOutline'; import { Sidebar } from './Bar'; import { SidebarItem } from './Items'; +import { renderHook } from '@testing-library/react-hooks'; +import { hexToRgb, makeStyles } from '@material-ui/core'; + +const useStyles = makeStyles({ + spotlight: { + backgroundColor: '#2b2a2a', + }, +}); async function renderSidebar() { + const { result } = renderHook(() => useStyles()); + await renderInTestApp( @@ -31,6 +41,7 @@ async function renderSidebar() { icon={CreateComponentIcon} onClick={() => {}} text="Create..." + className={result.current.spotlight} /> , ); @@ -54,5 +65,11 @@ describe('Items', () => { await screen.findByRole('button', { name: /create/i }), ).toBeInTheDocument(); }); + + it('should render a button with custom style', async () => { + expect( + await screen.findByRole('button', { name: /create/i }), + ).toHaveStyle(`background-color: ${hexToRgb('2b2a2a')}`); + }); }); }); diff --git a/packages/core/src/layout/Sidebar/Items.tsx b/packages/core/src/layout/Sidebar/Items.tsx index e954dcb4f8..57cf435651 100644 --- a/packages/core/src/layout/Sidebar/Items.tsx +++ b/packages/core/src/layout/Sidebar/Items.tsx @@ -128,6 +128,7 @@ type SidebarItemBaseProps = { text?: string; hasNotifications?: boolean; children?: ReactNode; + className?: string; }; type SidebarItemButtonProps = SidebarItemBaseProps & { @@ -154,6 +155,7 @@ export const SidebarItem = forwardRef((props, ref) => { hasNotifications = false, onClick, children, + className, } = props; const classes = useStyles(); // XXX (@koroeskohr): unsure this is optimal. But I just really didn't want to have the item component @@ -193,6 +195,7 @@ export const SidebarItem = forwardRef((props, ref) => { const childProps = { onClick, className: clsx( + className, classes.root, isOpen ? classes.open : classes.closed, isButtonItem(props) && classes.buttonItem, From 025e122c3e5147c91eb9e5b963170c797c0d5383 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 27 Jan 2021 21:32:39 +0100 Subject: [PATCH 129/131] catalog-model: replace yup with ajv, for validation of catalog entities --- .changeset/little-pets-cross.md | 5 ++ .changeset/metal-pans-leave.md | 2 +- .changeset/six-ravens-heal.md | 2 +- packages/catalog-model/package.json | 4 +- .../policies/SchemaValidEntityPolicy.ts | 58 ++++++++----------- .../src/kinds/ApiEntityV1alpha1.ts | 24 +++----- .../src/kinds/ComponentEntityV1alpha1.ts | 26 +++------ .../src/kinds/DomainEntityV1alpha1.ts | 20 +++---- .../src/kinds/GroupEntityV1alpha1.ts | 37 +++--------- .../src/kinds/LocationEntityV1alpha1.ts | 22 +++---- .../src/kinds/ResourceEntityV1alpha1.ts | 22 +++---- .../src/kinds/SystemEntityV1alpha1.ts | 21 +++---- .../src/kinds/TemplateEntityV1alpha1.ts | 23 +++----- .../src/kinds/UserEntityV1alpha1.ts | 35 +++-------- packages/catalog-model/src/kinds/util.ts | 44 ++++++++++++-- .../{ => src}/schema/Entity.schema.json | 0 .../{ => src}/schema/EntityMeta.schema.json | 28 +++++++++ .../schema/kinds/API.v1alpha1.schema.json | 0 .../kinds/Component.v1alpha1.schema.json} | 0 .../schema/kinds/Domain.v1alpha1.schema.json | 0 .../schema/kinds/Group.v1alpha1.schema.json | 0 .../kinds/Location.v1alpha1.schema.json | 0 .../kinds/Resource.v1alpha1.schema.json | 0 .../schema/kinds/System.v1alpha1.schema.json | 0 .../kinds/Template.v1alpha1.schema.json | 0 .../schema/kinds/User.v1alpha1.schema.json | 0 .../schema/shared/common.schema.json | 0 yarn.lock | 22 +++++++ 28 files changed, 187 insertions(+), 208 deletions(-) create mode 100644 .changeset/little-pets-cross.md rename packages/catalog-model/{ => src}/schema/Entity.schema.json (100%) rename packages/catalog-model/{ => src}/schema/EntityMeta.schema.json (77%) rename packages/catalog-model/{ => src}/schema/kinds/API.v1alpha1.schema.json (100%) rename packages/catalog-model/{schema/kinds/Component.v1alpha1.schema copy.json => src/schema/kinds/Component.v1alpha1.schema.json} (100%) rename packages/catalog-model/{ => src}/schema/kinds/Domain.v1alpha1.schema.json (100%) rename packages/catalog-model/{ => src}/schema/kinds/Group.v1alpha1.schema.json (100%) rename packages/catalog-model/{ => src}/schema/kinds/Location.v1alpha1.schema.json (100%) rename packages/catalog-model/{ => src}/schema/kinds/Resource.v1alpha1.schema.json (100%) rename packages/catalog-model/{ => src}/schema/kinds/System.v1alpha1.schema.json (100%) rename packages/catalog-model/{ => src}/schema/kinds/Template.v1alpha1.schema.json (100%) rename packages/catalog-model/{ => src}/schema/kinds/User.v1alpha1.schema.json (100%) rename packages/catalog-model/{ => src}/schema/shared/common.schema.json (100%) diff --git a/.changeset/little-pets-cross.md b/.changeset/little-pets-cross.md new file mode 100644 index 0000000000..0f4e2605cd --- /dev/null +++ b/.changeset/little-pets-cross.md @@ -0,0 +1,5 @@ +--- +'@backstage/catalog-model': patch +--- + +Replace `yup` with `ajv`, for validation of catalog entities. diff --git a/.changeset/metal-pans-leave.md b/.changeset/metal-pans-leave.md index 249ffe7aec..16d5478e43 100644 --- a/.changeset/metal-pans-leave.md +++ b/.changeset/metal-pans-leave.md @@ -2,4 +2,4 @@ '@backstage/config-loader': patch --- -Bump config-loader to ajv 7, to enable v7 feature use elsewhere +Bump `config-loader` to `ajv` 7, to enable v7 feature use elsewhere diff --git a/.changeset/six-ravens-heal.md b/.changeset/six-ravens-heal.md index 91ed92c89c..960c482be9 100644 --- a/.changeset/six-ravens-heal.md +++ b/.changeset/six-ravens-heal.md @@ -2,4 +2,4 @@ '@backstage/catalog-model': patch --- -Introduce jsonschema variants of the yup vlidation schemas +Introduce json schema variants of the `yup` validation schemas diff --git a/packages/catalog-model/package.json b/packages/catalog-model/package.json index 56d167d390..e81bc3a9b6 100644 --- a/packages/catalog-model/package.json +++ b/packages/catalog-model/package.json @@ -32,6 +32,7 @@ "@backstage/config": "^0.1.2", "@types/json-schema": "^7.0.5", "@types/yup": "^0.29.8", + "ajv": "^7.0.3", "json-schema": "^0.2.5", "lodash": "^4.17.15", "uuid": "^8.0.0", @@ -45,7 +46,6 @@ "yaml": "^1.9.2" }, "files": [ - "dist", - "schema" + "dist" ] } diff --git a/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts b/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts index 2796826b20..7e0a8df268 100644 --- a/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts +++ b/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts @@ -14,32 +14,13 @@ * limitations under the License. */ -import * as yup from 'yup'; -import { Entity, EntityLink } from '../Entity'; +import Ajv, { ValidateFunction } from 'ajv'; +import entitySchema from '../../schema/Entity.schema.json'; +import entityMetaSchema from '../../schema/EntityMeta.schema.json'; +import commonSchema from '../../schema/shared/common.schema.json'; +import { Entity } from '../Entity'; import { EntityPolicy } from './types'; -const DEFAULT_ENTITY_SCHEMA = yup - .object({ - apiVersion: yup.string().required(), - kind: yup.string().required(), - metadata: yup - .object({ - uid: yup.string().notRequired().min(1), - etag: yup.string().notRequired().min(1), - generation: yup.number().notRequired().integer().min(1), - name: yup.string().required(), - namespace: yup.string().notRequired(), - description: yup.string().notRequired(), - labels: yup.object>().notRequired(), - annotations: yup.object>().notRequired(), - tags: yup.array().notRequired(), - links: yup.array().notRequired(), - }) - .required(), - spec: yup.object({}).notRequired(), - }) - .required(); - /** * Ensures that the entity spec is valid according to a schema. * @@ -48,17 +29,28 @@ const DEFAULT_ENTITY_SCHEMA = yup * typescript type. */ export class SchemaValidEntityPolicy implements EntityPolicy { - private readonly schema: yup.Schema; - - constructor(schema: yup.Schema = DEFAULT_ENTITY_SCHEMA) { - this.schema = schema; - } + private validate: ValidateFunction | undefined; async enforce(entity: Entity): Promise { - try { - return await this.schema.validate(entity, { strict: true }); - } catch (e) { - throw new Error(`Malformed envelope, ${e}`); + if (!this.validate) { + const ajv = new Ajv({ allowUnionTypes: true }); + this.validate = ajv + .addSchema([commonSchema, entityMetaSchema], undefined, undefined, true) + .compile(entitySchema); } + + const result = this.validate(entity); + if (result === true) { + return entity; + } + + const [error] = this.validate.errors || []; + if (!error) { + throw new Error(`Malformed envelope, Unknown error`); + } + + throw new Error( + `Malformed envelope, ${error.dataPath || ''} ${error.message}`, + ); } } diff --git a/packages/catalog-model/src/kinds/ApiEntityV1alpha1.ts b/packages/catalog-model/src/kinds/ApiEntityV1alpha1.ts index 2c634ff091..5432cafdeb 100644 --- a/packages/catalog-model/src/kinds/ApiEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/ApiEntityV1alpha1.ts @@ -14,27 +14,16 @@ * limitations under the License. */ -import * as yup from 'yup'; import type { Entity } from '../entity/Entity'; -import { schemaValidator } from './util'; +import schema from '../schema/kinds/API.v1alpha1.schema.json'; +import entitySchema from '../schema/Entity.schema.json'; +import entityMetaSchema from '../schema/EntityMeta.schema.json'; +import commonSchema from '../schema/shared/common.schema.json'; +import { ajvCompiledJsonSchemaValidator } from './util'; const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const; const KIND = 'API' as const; -const schema = yup.object>({ - apiVersion: yup.string().required().oneOf(API_VERSION), - kind: yup.string().required().equals([KIND]), - spec: yup - .object({ - type: yup.string().required().min(1), - lifecycle: yup.string().required().min(1), - owner: yup.string().required().min(1), - definition: yup.string().required().min(1), - system: yup.string().notRequired().min(1), - }) - .required(), -}); - export interface ApiEntityV1alpha1 extends Entity { apiVersion: typeof API_VERSION[number]; kind: typeof KIND; @@ -47,8 +36,9 @@ export interface ApiEntityV1alpha1 extends Entity { }; } -export const apiEntityV1alpha1Validator = schemaValidator( +export const apiEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator( KIND, API_VERSION, schema, + [commonSchema, entityMetaSchema, entitySchema], ); diff --git a/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.ts b/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.ts index c55c48055a..3006a4288c 100644 --- a/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.ts @@ -14,29 +14,16 @@ * limitations under the License. */ -import * as yup from 'yup'; import type { Entity } from '../entity/Entity'; -import { schemaValidator } from './util'; +import schema from '../schema/kinds/Component.v1alpha1.schema.json'; +import entitySchema from '../schema/Entity.schema.json'; +import entityMetaSchema from '../schema/EntityMeta.schema.json'; +import commonSchema from '../schema/shared/common.schema.json'; +import { ajvCompiledJsonSchemaValidator } from './util'; const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const; const KIND = 'Component' as const; -const schema = yup.object>({ - apiVersion: yup.string().required().oneOf(API_VERSION), - kind: yup.string().required().equals([KIND]), - spec: yup - .object({ - type: yup.string().required().min(1), - lifecycle: yup.string().required().min(1), - owner: yup.string().required().min(1), - subcomponentOf: yup.string().notRequired().min(1), - providesApis: yup.array(yup.string().required()).notRequired(), - consumesApis: yup.array(yup.string().required()).notRequired(), - system: yup.string().notRequired().min(1), - }) - .required(), -}); - export interface ComponentEntityV1alpha1 extends Entity { apiVersion: typeof API_VERSION[number]; kind: typeof KIND; @@ -51,8 +38,9 @@ export interface ComponentEntityV1alpha1 extends Entity { }; } -export const componentEntityV1alpha1Validator = schemaValidator( +export const componentEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator( KIND, API_VERSION, schema, + [commonSchema, entityMetaSchema, entitySchema], ); diff --git a/packages/catalog-model/src/kinds/DomainEntityV1alpha1.ts b/packages/catalog-model/src/kinds/DomainEntityV1alpha1.ts index 60b11aa124..7aab35e367 100644 --- a/packages/catalog-model/src/kinds/DomainEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/DomainEntityV1alpha1.ts @@ -14,23 +14,16 @@ * limitations under the License. */ -import * as yup from 'yup'; import type { Entity } from '../entity/Entity'; -import { schemaValidator } from './util'; +import schema from '../schema/kinds/Domain.v1alpha1.schema.json'; +import entitySchema from '../schema/Entity.schema.json'; +import entityMetaSchema from '../schema/EntityMeta.schema.json'; +import commonSchema from '../schema/shared/common.schema.json'; +import { ajvCompiledJsonSchemaValidator } from './util'; const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const; const KIND = 'Domain' as const; -const schema = yup.object>({ - apiVersion: yup.string().required().oneOf(API_VERSION), - kind: yup.string().required().equals([KIND]), - spec: yup - .object({ - owner: yup.string().required().min(1), - }) - .required(), -}); - export interface DomainEntityV1alpha1 extends Entity { apiVersion: typeof API_VERSION[number]; kind: typeof KIND; @@ -39,8 +32,9 @@ export interface DomainEntityV1alpha1 extends Entity { }; } -export const domainEntityV1alpha1Validator = schemaValidator( +export const domainEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator( KIND, API_VERSION, schema, + [commonSchema, entityMetaSchema, entitySchema], ); diff --git a/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts b/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts index 6f2664af77..d039fdaba8 100644 --- a/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts @@ -14,40 +14,16 @@ * limitations under the License. */ -import * as yup from 'yup'; import type { Entity } from '../entity/Entity'; -import { schemaValidator } from './util'; +import schema from '../schema/kinds/Group.v1alpha1.schema.json'; +import entitySchema from '../schema/Entity.schema.json'; +import entityMetaSchema from '../schema/EntityMeta.schema.json'; +import commonSchema from '../schema/shared/common.schema.json'; +import { ajvCompiledJsonSchemaValidator } from './util'; const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const; const KIND = 'Group' as const; -const schema = yup.object>({ - apiVersion: yup.string().required().oneOf(API_VERSION), - kind: yup.string().required().equals([KIND]), - spec: yup - .object({ - type: yup.string().required().min(1), - profile: yup - .object({ - displayName: yup.string().min(1).notRequired(), - email: yup.string().min(1).notRequired(), - picture: yup.string().min(1).notRequired(), - }) - .notRequired(), - parent: yup.string().notRequired().min(1), - // Use these manual tests because yup .required() requires at least - // one element and there is no simple workaround -_- - // the cast is there to convince typescript that the array itself is - // required without using .required() - children: yup.array(yup.string().required()).test({ - name: 'isDefined', - message: 'children must be defined', - test: v => Boolean(v), - }) as yup.ArraySchema, - }) - .required(), -}); - export interface GroupEntityV1alpha1 extends Entity { apiVersion: typeof API_VERSION[number]; kind: typeof KIND; @@ -63,8 +39,9 @@ export interface GroupEntityV1alpha1 extends Entity { }; } -export const groupEntityV1alpha1Validator = schemaValidator( +export const groupEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator( KIND, API_VERSION, schema, + [commonSchema, entityMetaSchema, entitySchema], ); diff --git a/packages/catalog-model/src/kinds/LocationEntityV1alpha1.ts b/packages/catalog-model/src/kinds/LocationEntityV1alpha1.ts index 9cd767de94..fb452b6ac7 100644 --- a/packages/catalog-model/src/kinds/LocationEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/LocationEntityV1alpha1.ts @@ -14,25 +14,16 @@ * limitations under the License. */ -import * as yup from 'yup'; import type { Entity } from '../entity/Entity'; -import { schemaValidator } from './util'; +import schema from '../schema/kinds/Location.v1alpha1.schema.json'; +import entitySchema from '../schema/Entity.schema.json'; +import entityMetaSchema from '../schema/EntityMeta.schema.json'; +import commonSchema from '../schema/shared/common.schema.json'; +import { ajvCompiledJsonSchemaValidator } from './util'; const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const; const KIND = 'Location' as const; -const schema = yup.object>({ - apiVersion: yup.string().required().oneOf(API_VERSION), - kind: yup.string().required().equals([KIND]), - spec: yup - .object({ - type: yup.string().notRequired().min(1), - target: yup.string().notRequired().min(1), - targets: yup.array(yup.string().required()).notRequired(), - }) - .required(), -}); - export interface LocationEntityV1alpha1 extends Entity { apiVersion: typeof API_VERSION[number]; kind: typeof KIND; @@ -43,8 +34,9 @@ export interface LocationEntityV1alpha1 extends Entity { }; } -export const locationEntityV1alpha1Validator = schemaValidator( +export const locationEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator( KIND, API_VERSION, schema, + [commonSchema, entityMetaSchema, entitySchema], ); diff --git a/packages/catalog-model/src/kinds/ResourceEntityV1alpha1.ts b/packages/catalog-model/src/kinds/ResourceEntityV1alpha1.ts index 12df7f6664..520a39f02c 100644 --- a/packages/catalog-model/src/kinds/ResourceEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/ResourceEntityV1alpha1.ts @@ -14,25 +14,16 @@ * limitations under the License. */ -import * as yup from 'yup'; import type { Entity } from '../entity/Entity'; -import { schemaValidator } from './util'; +import schema from '../schema/kinds/Resource.v1alpha1.schema.json'; +import entitySchema from '../schema/Entity.schema.json'; +import entityMetaSchema from '../schema/EntityMeta.schema.json'; +import commonSchema from '../schema/shared/common.schema.json'; +import { ajvCompiledJsonSchemaValidator } from './util'; const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const; const KIND = 'Resource' as const; -const schema = yup.object>({ - apiVersion: yup.string().required().oneOf(API_VERSION), - kind: yup.string().required().equals([KIND]), - spec: yup - .object({ - type: yup.string().required().min(1), - owner: yup.string().required().min(1), - system: yup.string().notRequired().min(1), - }) - .required(), -}); - export interface ResourceEntityV1alpha1 extends Entity { apiVersion: typeof API_VERSION[number]; kind: typeof KIND; @@ -43,8 +34,9 @@ export interface ResourceEntityV1alpha1 extends Entity { }; } -export const resourceEntityV1alpha1Validator = schemaValidator( +export const resourceEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator( KIND, API_VERSION, schema, + [commonSchema, entityMetaSchema, entitySchema], ); diff --git a/packages/catalog-model/src/kinds/SystemEntityV1alpha1.ts b/packages/catalog-model/src/kinds/SystemEntityV1alpha1.ts index 764514efdd..1ee19466f0 100644 --- a/packages/catalog-model/src/kinds/SystemEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/SystemEntityV1alpha1.ts @@ -14,24 +14,16 @@ * limitations under the License. */ -import * as yup from 'yup'; import type { Entity } from '../entity/Entity'; -import { schemaValidator } from './util'; +import schema from '../schema/kinds/System.v1alpha1.schema.json'; +import entitySchema from '../schema/Entity.schema.json'; +import entityMetaSchema from '../schema/EntityMeta.schema.json'; +import commonSchema from '../schema/shared/common.schema.json'; +import { ajvCompiledJsonSchemaValidator } from './util'; const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const; const KIND = 'System' as const; -const schema = yup.object>({ - apiVersion: yup.string().required().oneOf(API_VERSION), - kind: yup.string().required().equals([KIND]), - spec: yup - .object({ - owner: yup.string().required().min(1), - domain: yup.string().notRequired().min(1), - }) - .required(), -}); - export interface SystemEntityV1alpha1 extends Entity { apiVersion: typeof API_VERSION[number]; kind: typeof KIND; @@ -41,8 +33,9 @@ export interface SystemEntityV1alpha1 extends Entity { }; } -export const systemEntityV1alpha1Validator = schemaValidator( +export const systemEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator( KIND, API_VERSION, schema, + [commonSchema, entityMetaSchema, entitySchema], ); diff --git a/packages/catalog-model/src/kinds/TemplateEntityV1alpha1.ts b/packages/catalog-model/src/kinds/TemplateEntityV1alpha1.ts index b16fcdc7e4..72479c7efa 100644 --- a/packages/catalog-model/src/kinds/TemplateEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/TemplateEntityV1alpha1.ts @@ -14,27 +14,17 @@ * limitations under the License. */ -import * as yup from 'yup'; import type { Entity } from '../entity/Entity'; +import schema from '../schema/kinds/Template.v1alpha1.schema.json'; +import entitySchema from '../schema/Entity.schema.json'; +import entityMetaSchema from '../schema/EntityMeta.schema.json'; +import commonSchema from '../schema/shared/common.schema.json'; import type { JSONSchema } from '../types'; -import { schemaValidator } from './util'; +import { ajvCompiledJsonSchemaValidator } from './util'; const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const; const KIND = 'Template' as const; -const schema = yup.object>({ - apiVersion: yup.string().required().oneOf(API_VERSION), - kind: yup.string().required().equals([KIND]), - spec: yup - .object({ - type: yup.string().required().min(1), - path: yup.string(), - schema: yup.object().required(), - templater: yup.string().required(), - }) - .required(), -}); - export interface TemplateEntityV1alpha1 extends Entity { apiVersion: typeof API_VERSION[number]; kind: typeof KIND; @@ -46,8 +36,9 @@ export interface TemplateEntityV1alpha1 extends Entity { }; } -export const templateEntityV1alpha1Validator = schemaValidator( +export const templateEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator( KIND, API_VERSION, schema, + [commonSchema, entityMetaSchema, entitySchema], ); diff --git a/packages/catalog-model/src/kinds/UserEntityV1alpha1.ts b/packages/catalog-model/src/kinds/UserEntityV1alpha1.ts index 16a5a86e05..a8700a496e 100644 --- a/packages/catalog-model/src/kinds/UserEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/UserEntityV1alpha1.ts @@ -14,38 +14,16 @@ * limitations under the License. */ -import * as yup from 'yup'; import type { Entity } from '../entity/Entity'; -import { schemaValidator } from './util'; +import schema from '../schema/kinds/User.v1alpha1.schema.json'; +import entitySchema from '../schema/Entity.schema.json'; +import entityMetaSchema from '../schema/EntityMeta.schema.json'; +import commonSchema from '../schema/shared/common.schema.json'; +import { ajvCompiledJsonSchemaValidator } from './util'; const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const; const KIND = 'User' as const; -const 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(), - // Use this manual test because yup .required() requires at least one - // element and there is no simple workaround -_- - // the cast is there to convince typescript that the array itself is - // required without using .required() - memberOf: yup.array(yup.string().required()).test({ - name: 'isDefined', - message: 'memberOf must be defined', - test: v => Boolean(v), - }) as yup.ArraySchema, - }) - .required(), -}); - export interface UserEntityV1alpha1 extends Entity { apiVersion: typeof API_VERSION[number]; kind: typeof KIND; @@ -59,8 +37,9 @@ export interface UserEntityV1alpha1 extends Entity { }; } -export const userEntityV1alpha1Validator = schemaValidator( +export const userEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator( KIND, API_VERSION, schema, + [commonSchema, entityMetaSchema, entitySchema], ); diff --git a/packages/catalog-model/src/kinds/util.ts b/packages/catalog-model/src/kinds/util.ts index f1b02cfba1..4002f2806c 100644 --- a/packages/catalog-model/src/kinds/util.ts +++ b/packages/catalog-model/src/kinds/util.ts @@ -14,9 +14,13 @@ * limitations under the License. */ +import Ajv, { AnySchema } from 'ajv'; import * as yup from 'yup'; import { KindValidator } from './types'; +/** + * @deprecated We no longer use yup for the catalog model. This utility method will be removed. + */ export function schemaValidator( kind: string, apiVersion: readonly string[], @@ -24,10 +28,7 @@ export function schemaValidator( ): KindValidator { return { async check(envelope) { - if ( - kind !== envelope.kind || - !apiVersion.includes(envelope.apiVersion as any) - ) { + if (kind !== envelope.kind || !apiVersion.includes(envelope.apiVersion)) { return false; } await schema.validate(envelope, { strict: true }); @@ -35,3 +36,38 @@ export function schemaValidator( }, }; } + +export function ajvCompiledJsonSchemaValidator( + kind: string, + apiVersion: readonly string[], + schema: AnySchema, + extraSchemas?: AnySchema[], +): KindValidator { + const ajv = new Ajv({ allowUnionTypes: true }); + if (extraSchemas) { + ajv.addSchema(extraSchemas, undefined, undefined, true); + } + const validate = ajv.compile(schema); + + return { + async check(envelope) { + if (kind !== envelope.kind || !apiVersion.includes(envelope.apiVersion)) { + return false; + } + + const result = validate(envelope); + if (result === true) { + return true; + } + + const [error] = validate.errors || []; + if (!error) { + throw new TypeError(`Malformed ${kind}, Unknown error`); + } + + throw new TypeError( + `Malformed ${kind}, ${error.dataPath || ''} ${error.message}`, + ); + }, + }; +} diff --git a/packages/catalog-model/schema/Entity.schema.json b/packages/catalog-model/src/schema/Entity.schema.json similarity index 100% rename from packages/catalog-model/schema/Entity.schema.json rename to packages/catalog-model/src/schema/Entity.schema.json diff --git a/packages/catalog-model/schema/EntityMeta.schema.json b/packages/catalog-model/src/schema/EntityMeta.schema.json similarity index 77% rename from packages/catalog-model/schema/EntityMeta.schema.json rename to packages/catalog-model/src/schema/EntityMeta.schema.json index d44ca84bf4..ff0f9c84a8 100644 --- a/packages/catalog-model/schema/EntityMeta.schema.json +++ b/packages/catalog-model/src/schema/EntityMeta.schema.json @@ -84,6 +84,34 @@ "type": "string", "minLength": 1 } + }, + "links": { + "type": "array", + "description": "A list of external hyperlinks related to the entity. Links can provide additional contextual information that may be located outside of Backstage itself. For example, an admin dashboard or external CMS page.", + "items": { + "type": "object", + "required": ["url"], + "properties": { + "url": { + "type": "string", + "description": "A url in a standard uri format.", + "examples": ["https://admin.example-org.com"], + "minLength": 1 + }, + "title": { + "type": "string", + "description": "A user friendly display name for the link.", + "examples": ["Admin Dashboard"], + "minLength": 1 + }, + "icon": { + "type": "string", + "description": "A key representing a visual icon to be displayed in the UI.", + "examples": ["dashboard"], + "minLength": 1 + } + } + } } } } diff --git a/packages/catalog-model/schema/kinds/API.v1alpha1.schema.json b/packages/catalog-model/src/schema/kinds/API.v1alpha1.schema.json similarity index 100% rename from packages/catalog-model/schema/kinds/API.v1alpha1.schema.json rename to packages/catalog-model/src/schema/kinds/API.v1alpha1.schema.json diff --git a/packages/catalog-model/schema/kinds/Component.v1alpha1.schema copy.json b/packages/catalog-model/src/schema/kinds/Component.v1alpha1.schema.json similarity index 100% rename from packages/catalog-model/schema/kinds/Component.v1alpha1.schema copy.json rename to packages/catalog-model/src/schema/kinds/Component.v1alpha1.schema.json diff --git a/packages/catalog-model/schema/kinds/Domain.v1alpha1.schema.json b/packages/catalog-model/src/schema/kinds/Domain.v1alpha1.schema.json similarity index 100% rename from packages/catalog-model/schema/kinds/Domain.v1alpha1.schema.json rename to packages/catalog-model/src/schema/kinds/Domain.v1alpha1.schema.json diff --git a/packages/catalog-model/schema/kinds/Group.v1alpha1.schema.json b/packages/catalog-model/src/schema/kinds/Group.v1alpha1.schema.json similarity index 100% rename from packages/catalog-model/schema/kinds/Group.v1alpha1.schema.json rename to packages/catalog-model/src/schema/kinds/Group.v1alpha1.schema.json diff --git a/packages/catalog-model/schema/kinds/Location.v1alpha1.schema.json b/packages/catalog-model/src/schema/kinds/Location.v1alpha1.schema.json similarity index 100% rename from packages/catalog-model/schema/kinds/Location.v1alpha1.schema.json rename to packages/catalog-model/src/schema/kinds/Location.v1alpha1.schema.json diff --git a/packages/catalog-model/schema/kinds/Resource.v1alpha1.schema.json b/packages/catalog-model/src/schema/kinds/Resource.v1alpha1.schema.json similarity index 100% rename from packages/catalog-model/schema/kinds/Resource.v1alpha1.schema.json rename to packages/catalog-model/src/schema/kinds/Resource.v1alpha1.schema.json diff --git a/packages/catalog-model/schema/kinds/System.v1alpha1.schema.json b/packages/catalog-model/src/schema/kinds/System.v1alpha1.schema.json similarity index 100% rename from packages/catalog-model/schema/kinds/System.v1alpha1.schema.json rename to packages/catalog-model/src/schema/kinds/System.v1alpha1.schema.json diff --git a/packages/catalog-model/schema/kinds/Template.v1alpha1.schema.json b/packages/catalog-model/src/schema/kinds/Template.v1alpha1.schema.json similarity index 100% rename from packages/catalog-model/schema/kinds/Template.v1alpha1.schema.json rename to packages/catalog-model/src/schema/kinds/Template.v1alpha1.schema.json diff --git a/packages/catalog-model/schema/kinds/User.v1alpha1.schema.json b/packages/catalog-model/src/schema/kinds/User.v1alpha1.schema.json similarity index 100% rename from packages/catalog-model/schema/kinds/User.v1alpha1.schema.json rename to packages/catalog-model/src/schema/kinds/User.v1alpha1.schema.json diff --git a/packages/catalog-model/schema/shared/common.schema.json b/packages/catalog-model/src/schema/shared/common.schema.json similarity index 100% rename from packages/catalog-model/schema/shared/common.schema.json rename to packages/catalog-model/src/schema/shared/common.schema.json diff --git a/yarn.lock b/yarn.lock index 43bcf73364..f553207d23 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2478,6 +2478,7 @@ "@backstage/config" "^0.1.2" "@types/json-schema" "^7.0.5" "@types/yup" "^0.29.8" + ajv "^7.0.3" json-schema "^0.2.5" lodash "^4.17.15" uuid "^8.0.0" @@ -2489,6 +2490,7 @@ "@backstage/config" "^0.1.2" "@types/json-schema" "^7.0.5" "@types/yup" "^0.29.8" + ajv "^7.0.3" json-schema "^0.2.5" lodash "^4.17.15" uuid "^8.0.0" @@ -7970,6 +7972,16 @@ ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.1, ajv@^6.10.2, ajv@^6.12.4, ajv@^6.12.5, ajv json-schema-traverse "^0.4.1" uri-js "^4.2.2" +ajv@^7.0.3: + version "7.0.3" + resolved "https://registry.npmjs.org/ajv/-/ajv-7.0.3.tgz#13ae747eff125cafb230ac504b2406cf371eece2" + integrity sha512-R50QRlXSxqXcQP5SvKUrw8VZeypvo12i2IX0EeR5PiZ7bEKeHWgzgo264LDadUsCU42lTJVhFikTqJwNeH34gQ== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" + alphanum-sort@^1.0.0: version "1.0.2" resolved "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3" @@ -16954,6 +16966,11 @@ json-schema-traverse@^0.4.1: resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + json-schema@0.2.3: version "0.2.3" resolved "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" @@ -22693,6 +22710,11 @@ require-directory@^2.1.1: resolved "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + require-main-filename@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" From c7c69571073ad0c4d4d0c1312691351ff3a4aa87 Mon Sep 17 00:00:00 2001 From: vitorgrenzel Date: Thu, 28 Jan 2021 17:46:39 -0300 Subject: [PATCH 130/131] fix(core): update packages --- packages/core/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/package.json b/packages/core/package.json index d4592d48ac..49090e1362 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -37,20 +37,20 @@ "@material-ui/lab": "4.0.0-alpha.45", "@testing-library/react-hooks": "^3.4.2", "@types/dagre": "^0.7.44", - "@types/prop-types": "^15.7.3", "@types/react": "^16.9", "@types/react-sparklines": "^1.7.0", + "@types/prop-types": "^15.7.3", "classnames": "^2.2.6", "clsx": "^1.1.0", "d3-selection": "^2.0.0", "d3-shape": "^2.0.0", "d3-zoom": "^2.0.0", "dagre": "^0.8.5", + "qs": "^6.9.4", "immer": "^8.0.1", "lodash": "^4.17.15", "material-table": "^1.69.1", "prop-types": "^15.7.2", - "qs": "^6.9.4", "rc-progress": "^3.0.0", "react": "^16.12.0", "react-dom": "^16.12.0", From b379276f1038138a149ccab89ab98f7c23b833f9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 29 Jan 2021 04:57:47 +0000 Subject: [PATCH 131/131] chore(deps-dev): bump nodemon from 2.0.6 to 2.0.7 Bumps [nodemon](https://github.com/remy/nodemon) from 2.0.6 to 2.0.7. - [Release notes](https://github.com/remy/nodemon/releases) - [Commits](https://github.com/remy/nodemon/compare/v2.0.6...v2.0.7) Signed-off-by: dependabot[bot] --- yarn.lock | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/yarn.lock b/yarn.lock index f553207d23..c40b1ab675 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2505,6 +2505,7 @@ "@material-ui/core" "^4.11.0" "@material-ui/icons" "^4.9.1" "@material-ui/lab" "4.0.0-alpha.45" + "@testing-library/react-hooks" "^3.4.2" "@types/dagre" "^0.7.44" "@types/prop-types" "^15.7.3" "@types/react" "^16.9" @@ -14199,13 +14200,6 @@ git-url-parse@^11.4.4: dependencies: git-up "^4.0.0" -git-url-parse@^11.4.4: - version "11.4.4" - resolved "https://registry.npmjs.org/git-url-parse/-/git-url-parse-11.4.4.tgz#5d747debc2469c17bc385719f7d0427802d83d77" - integrity sha512-Y4o9o7vQngQDIU9IjyCmRJBin5iYjI5u9ZITnddRZpD7dcCFQj2sL2XuMNbLRE4b4B/4ENPsp2Q8P44fjAZ0Pw== - dependencies: - git-up "^4.0.0" - gitconfiglocal@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/gitconfiglocal/-/gitconfiglocal-1.0.0.tgz#41d045f3851a5ea88f03f24ca1c6178114464b9b" @@ -19226,9 +19220,9 @@ node-request-interceptor@^0.5.1: headers-utils "^1.2.0" nodemon@^2.0.2: - version "2.0.6" - resolved "https://registry.npmjs.org/nodemon/-/nodemon-2.0.6.tgz#1abe1937b463aaf62f0d52e2b7eaadf28cc2240d" - integrity sha512-4I3YDSKXg6ltYpcnZeHompqac4E6JeAMpGm8tJnB9Y3T0ehasLa4139dJOcCrB93HHrUMsCrKtoAlXTqT5n4AQ== + version "2.0.7" + resolved "https://registry.npmjs.org/nodemon/-/nodemon-2.0.7.tgz#6f030a0a0ebe3ea1ba2a38f71bf9bab4841ced32" + integrity sha512-XHzK69Awgnec9UzHr1kc8EomQh4sjTQ8oRf8TsGrSmHDx9/UmiGG9E/mM3BuTfNeFwdNBvrqQq/RHL0xIeyFOA== dependencies: chokidar "^3.2.2" debug "^3.2.6"