Merge branch 'plugin-azure-functions' of https://github.com/wesley-pattison/backstage into plugin-azure-functions

This commit is contained in:
Wesley Pattison
2022-10-09 19:08:45 +02:00
16 changed files with 321 additions and 15 deletions
+37
View File
@@ -0,0 +1,37 @@
---
'@backstage/plugin-catalog': minor
---
Added new column `Label` to `CatalogTable.columns`, this new column allows you make use of labels from metadata.
For example: category and visibility are type of labels associated with API entity illustrated below.
YAML code snippet for API entity
```yaml
apiVersion: backstage.io/v1alpha1
kind: API
metadata:
name: sample-api
description: API for sample
links:
- url: http://localhost:8080/swagger-ui.html
title: Swagger UI
tags:
- http
labels:
category: legacy
visibility: protected
```
Consumers can customise columns to include label column and show in api-docs list
```typescript
const columns = [
CatalogTable.columns.createNameColumn({ defaultKind: 'API' }),
CatalogTable.columns.createLabelColumn('category', { title: 'Category' }),
CatalogTable.columns.createLabelColumn('visibility', {
title: 'Visibility',
defaultValue: 'public',
}),
];
```
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-kubernetes-backend': patch
---
kubernetes service locator now take request context parameters
+5 -1
View File
@@ -53,6 +53,10 @@ jobs:
github-token: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }}
# TODO(Rugvip): Remove the create-app dispatch once we've been on the release version for a while
script: |
const releaseVersion = require('./backstage/package.json').version;
if(releaseVersion.includes('next')) {
return;
}
console.log('Dispatching upgrade helper sync');
await github.rest.actions.createWorkflowDispatch({
owner: 'backstage',
@@ -61,6 +65,6 @@ jobs:
ref: 'master',
inputs: {
version: require('./backstage/packages/create-app/package.json').version,
releaseVersion: require('./backstage/package.json').version
releaseVersion
},
});
+1 -1
View File
@@ -91,7 +91,7 @@ _You can do this by using the [Adopter form](https://form.typeform.com/to/zcOaKi
| [HBO Max](https://hbomax.com) | [@mdb](https://github.com/mdb), [@nesta219](https://github.com/nesta219), [@nmische](https://github.com/nmische), [@hbomark](https://github.com/hbomark) | Developer portal hosting service catalog and API documentation, as well as cloud infrastructure details, operational visibility tools, and a custom plugin for browsing notable platform change events, such as deployments and configuration updates. |
| [RCHLO](https://www.riachuelo.com.br) & [MIDWAY](https://www.midway.com.br) | [@marcosborges](https://github.com/marcosborges), [@defaultbr](https://github.com/defaultbr) | Self-Service Platform |
| [HP Inc](https://www.hp.com) | [Damon Kaswell](https://github.com/dekoding) | DevEx engagement hub (dev portal: docs, standards, Q&A) and extensive assets catalog (APIs, services, code, data, etc.) for the pan-HP internal developer community. |
| [VMware](https://www.vmware.com) | [@mpriamo](https://github.com/mpriamo), [@krisapplegate](https://github.com/krisapplegate) | Part of [Tanzu Application Platform](https://docs.vmware.com/en/VMware-Tanzu-Application-Platform/index.html) offering; internal developer portal |
| [VMware](https://www.vmware.com) | [Waldir Montoya](https://github.com/waldirmontoya25), [Kris Applegate](https://github.com/krisapplegate), [Jamie Klassen](https://github.com/jamieklassen) | Part of [Tanzu Application Platform](https://docs.vmware.com/en/VMware-Tanzu-Application-Platform/index.html) offering; internal developer portal |
| [Ualá](https://www.uala.com.ar/) | [Santiago Bernal](https://github.com/sabernal) | Initial work being done to centralize documentation for all our microservices and APIs, as well as scaffolding new services and tracking code quality |
| [IKEA IT AB](https://www.ingka.com) | [@bjornramberg](https://github.com/bjornramberg), [@supriyachitale](https://github.com/supriyachitale) | Supporting engineers at scale with self serve access and connecting the dots of our engineering platform and services, enabling product teams to move faster and go further, and unleashing innovation, reuse and co-creation across the organisation. |
| [Invitae](https://www.invitae.com/en) | [@ryan-hanchett](https://github.com/ryan-hanchett), [@gmandler42](https://github.com/gmandler42) | Centralized Developer Experience portal, putting all of our tooling behind a single pane of glass and creating a living service catalog. |
+6
View File
@@ -96,6 +96,10 @@ catalog:
topic:
include: ['backstage-include'] # optional array of strings
exclude: ['experiments'] # optional array of strings
enterpriseProviderId:
host: ghe.example.net
organization: 'backstage' # string
catalogPath: '/catalog-info.yaml' # string
```
This provider supports multiple organizations via unique provider IDs.
@@ -125,6 +129,8 @@ This provider supports multiple organizations via unique provider IDs.
- **organization**:
Name of your organization account/workspace.
If you want to add multiple organizations, you need to add one provider config each.
- **host** _(optional)_:
The hostname of your GitHub Enterprise instance. It must match a host defined in [integrations.github](locations.md).
## GitHub API Rate Limits
+1
View File
@@ -6,6 +6,7 @@ description: Support and Community Details and Links
- [Discord chatroom](https://discord.gg/MUpMjP2) - Get support or discuss the
project.
- [Stack Overflow](https://stackoverflow.com/questions/tagged/backstage) - Browse or ask questions on Stack Overflow.
- [Good First Issues](https://github.com/backstage/backstage/contribute) - Start
here if you want to contribute.
- [RFCs](https://github.com/backstage/backstage/labels/rfc) - Help shape the
+139 -5
View File
@@ -34,7 +34,10 @@ import { ScmIntegrations } from '@backstage/integration';
import { DefaultCatalogRulesEnforcer } from './ingestion/CatalogRules';
import { Stitcher } from './stitching/Stitcher';
import { DefaultEntitiesCatalog } from './service/DefaultEntitiesCatalog';
import { DefaultCatalogProcessingEngine } from './processing/DefaultCatalogProcessingEngine';
import {
DefaultCatalogProcessingEngine,
ProgressTracker,
} from './processing/DefaultCatalogProcessingEngine';
import { createHash } from 'crypto';
import { DefaultRefreshService } from './service/DefaultRefreshService';
import { connectEntityProviders } from './processing/connectEntityProviders';
@@ -69,10 +72,6 @@ class TestProvider implements EntityProvider {
}
}
type ProgressTracker = NonNullable<
ConstructorParameters<typeof DefaultCatalogProcessingEngine>[7]
>;
class ProxyProgressTracker implements ProgressTracker {
#inner: ProgressTracker;
@@ -540,4 +539,139 @@ describe('Catalog Backend Integration', () => {
await expect(harness.getOutputEntities()).resolves.toEqual(outputEntities);
});
// NOTE(freben): This test documents existing behavior, but it would be more correct to mark the cycle as orphans
it('leaves behind orphaned cycles without orphan markers', async () => {
function mkEntity(name: string) {
return {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
name,
annotations: {
'backstage.io/managed-by-location': 'url:.',
'backstage.io/managed-by-origin-location': 'url:.',
},
},
};
}
const harness = await TestHarness.create({
async processEntity(
entity: Entity,
location: LocationSpec,
emit: CatalogProcessorEmit,
) {
if (entity.spec?.noEmit) {
return entity;
}
switch (entity.metadata.name) {
case 'a':
emit(processingResult.entity(location, mkEntity('b')));
break;
case 'b':
emit(processingResult.entity(location, mkEntity('c')));
break;
case 'c':
emit(processingResult.entity(location, mkEntity('d')));
break;
case 'd':
emit(processingResult.entity(location, mkEntity('b')));
break;
default:
}
return entity;
},
});
await harness.setInputEntities([
{
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
name: 'a',
annotations: {
'backstage.io/managed-by-location': 'url:.',
'backstage.io/managed-by-origin-location': 'url:.',
},
},
},
]);
await expect(harness.getOutputEntities()).resolves.toEqual({});
await expect(harness.process()).resolves.toEqual({});
await expect(harness.getOutputEntities()).resolves.toEqual({
'component:default/a': expect.objectContaining({
metadata: expect.objectContaining({ name: 'a' }),
}),
'component:default/b': expect.objectContaining({
metadata: expect.objectContaining({ name: 'b' }),
}),
'component:default/c': expect.objectContaining({
metadata: expect.objectContaining({ name: 'c' }),
}),
'component:default/d': expect.objectContaining({
metadata: expect.objectContaining({ name: 'd' }),
}),
});
// NOTE(freben): Avoid .toHaveProperty here, since it treats dots as path separators
expect(
(await harness.getOutputEntities())['component:default/b'].metadata
.annotations!['backstage.io/orphan'],
).toBeUndefined();
expect(
(await harness.getOutputEntities())['component:default/c'].metadata
.annotations!['backstage.io/orphan'],
).toBeUndefined();
expect(
(await harness.getOutputEntities())['component:default/d'].metadata
.annotations!['backstage.io/orphan'],
).toBeUndefined();
await harness.setInputEntities([
{
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
name: 'a',
annotations: {
'backstage.io/managed-by-location': 'url:.',
'backstage.io/managed-by-origin-location': 'url:.',
},
},
spec: { noEmit: true },
},
]);
await expect(harness.process()).resolves.toEqual({});
await expect(harness.getOutputEntities()).resolves.toEqual({
'component:default/a': expect.objectContaining({
metadata: expect.objectContaining({ name: 'a' }),
}),
'component:default/b': expect.objectContaining({
metadata: expect.objectContaining({ name: 'b' }),
}),
'component:default/c': expect.objectContaining({
metadata: expect.objectContaining({ name: 'c' }),
}),
'component:default/d': expect.objectContaining({
metadata: expect.objectContaining({ name: 'd' }),
}),
});
// TODO(freben): Ideally these should be orphaned now
expect(
(await harness.getOutputEntities())['component:default/b'].metadata
.annotations!['backstage.io/orphan'],
).toBeUndefined();
expect(
(await harness.getOutputEntities())['component:default/c'].metadata
.annotations!['backstage.io/orphan'],
).toBeUndefined();
expect(
(await harness.getOutputEntities())['component:default/d'].metadata
.annotations!['backstage.io/orphan'],
).toBeUndefined();
});
});
@@ -31,6 +31,8 @@ import { startTaskPipeline } from './TaskPipeline';
const CACHE_TTL = 5;
export type ProgressTracker = ReturnType<typeof progressTracker>;
export class DefaultCatalogProcessingEngine implements CatalogProcessingEngine {
private stopFunc?: () => void;
@@ -45,7 +47,7 @@ export class DefaultCatalogProcessingEngine implements CatalogProcessingEngine {
unprocessedEntity: Entity;
errors: Error[];
}) => Promise<void> | void,
private readonly tracker = progressTracker(),
private readonly tracker: ProgressTracker = progressTracker(),
) {}
async start() {
+9
View File
@@ -148,6 +148,15 @@ export const CatalogTable: {
}
| undefined,
): TableColumn<CatalogTableRow>;
createLabelColumn(
key: string,
options?:
| {
title?: string | undefined;
defaultValue?: string | undefined;
}
| undefined,
): TableColumn<CatalogTableRow>;
}>;
};
@@ -331,4 +331,42 @@ describe('CatalogTable component', () => {
expect(getByText('Should be rendered')).toBeInTheDocument();
});
it('should render the label column with customised title and value as specified', async () => {
const columns = [
CatalogTable.columns.createNameColumn({ defaultKind: 'API' }),
CatalogTable.columns.createLabelColumn('category', { title: 'Category' }),
];
const entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'API',
metadata: {
name: 'APIWithLabel',
labels: { category: 'generic' },
},
};
const expectedColumns = ['Name', 'Category', 'Actions'];
const { getAllByRole, getByText } = await renderInTestApp(
<ApiProvider apis={mockApis}>
<MockEntityListContextProvider value={{ entities: [entity] }}>
<CatalogTable columns={columns} />
</MockEntityListContextProvider>
</ApiProvider>,
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
},
},
);
const columnHeader = getAllByRole('button').filter(
c => c.tagName === 'SPAN',
);
const columnHeaderLabels = columnHeader.map(c => c.textContent);
expect(columnHeaderLabels).toEqual(expectedColumns);
const labelCellValue = getByText('generic');
expect(labelCellValue).toBeInTheDocument();
});
});
@@ -162,4 +162,35 @@ export const columnFactories = Object.freeze({
searchable: true,
};
},
createLabelColumn(
key: string,
options?: { title?: string; defaultValue?: string },
): TableColumn<CatalogTableRow> {
return {
title: options?.title || 'Label',
field: 'entity.metadata.labels',
cellStyle: {
padding: '0px 16px 0px 20px',
},
render: ({ entity }: { entity: Entity }) => {
const labels: Record<string, string> | undefined =
entity.metadata?.labels;
const specifiedLabelValue =
(labels && labels[key]) || options?.defaultValue;
return (
<>
{specifiedLabelValue && (
<Chip
key={specifiedLabelValue}
label={specifiedLabelValue}
size="small"
variant="outlined"
/>
)}
</>
);
},
width: 'auto',
};
},
});
+12 -1
View File
@@ -326,7 +326,10 @@ export type KubernetesObjectTypes =
// @alpha
export interface KubernetesServiceLocator {
// (undocumented)
getClustersByEntity(entity: Entity): Promise<{
getClustersByEntity(
entity: Entity,
requestContext: ServiceLocatorRequestContext,
): Promise<{
clusters: ClusterDetails[];
}>;
}
@@ -403,6 +406,14 @@ export interface ServiceAccountClusterDetails extends ClusterDetails {}
// @alpha (undocumented)
export type ServiceLocatorMethod = 'multiTenant' | 'http';
// @alpha (undocumented)
export interface ServiceLocatorRequestContext {
// (undocumented)
customResources: CustomResourceMatcher[];
// (undocumented)
objectTypesToFetch: Set<ObjectToFetch>;
}
// @alpha (undocumented)
export type SigningCreds = {
accessKeyId: string | undefined;
@@ -16,6 +16,7 @@
import '@backstage/backend-common';
import { Entity } from '@backstage/catalog-model';
import { ServiceLocatorRequestContext } from '../types/types';
import { MultiTenantServiceLocator } from './MultiTenantServiceLocator';
describe('MultiTenantConfigClusterLocator', () => {
@@ -24,7 +25,10 @@ describe('MultiTenantConfigClusterLocator', () => {
getClusters: async () => [],
});
const result = await sut.getClustersByEntity({} as Entity);
const result = await sut.getClustersByEntity(
{} as Entity,
{} as ServiceLocatorRequestContext,
);
expect(result).toStrictEqual({ clusters: [] });
});
@@ -43,7 +47,10 @@ describe('MultiTenantConfigClusterLocator', () => {
},
});
const result = await sut.getClustersByEntity({} as Entity);
const result = await sut.getClustersByEntity(
{} as Entity,
{} as ServiceLocatorRequestContext,
);
expect(result).toStrictEqual({
clusters: [
@@ -76,7 +83,10 @@ describe('MultiTenantConfigClusterLocator', () => {
},
});
const result = await sut.getClustersByEntity({} as Entity);
const result = await sut.getClustersByEntity(
{} as Entity,
{} as ServiceLocatorRequestContext,
);
expect(result).toStrictEqual({
clusters: [
@@ -19,6 +19,7 @@ import {
ClusterDetails,
KubernetesClustersSupplier,
KubernetesServiceLocator,
ServiceLocatorRequestContext,
} from '../types/types';
// This locator assumes that every service is located on every cluster
@@ -33,6 +34,7 @@ export class MultiTenantServiceLocator implements KubernetesServiceLocator {
// As this implementation always returns all clusters serviceId is ignored here
getClustersByEntity(
_entity: Entity,
_requestContext: ServiceLocatorRequestContext,
): Promise<{ clusters: ClusterDetails[] }> {
return this.clusterSupplier.getClusters().then(clusters => ({ clusters }));
}
@@ -27,6 +27,7 @@ import {
CustomResource,
CustomResourcesByEntity,
KubernetesObjectsByEntity,
ServiceLocatorRequestContext,
} from '../types/types';
import { KubernetesAuthTranslator } from '../kubernetes-auth-translator/types';
import { KubernetesAuthTranslatorGenerator } from '../kubernetes-auth-translator/KubernetesAuthTranslatorGenerator';
@@ -231,7 +232,10 @@ export class KubernetesFanOutHandler {
entity.metadata?.name;
const clusterDetailsDecoratedForAuth: ClusterDetails[] =
await this.decorateClusterDetailsWithAuth(entity, auth);
await this.decorateClusterDetailsWithAuth(entity, auth, {
objectTypesToFetch: objectTypesToFetch,
customResources: customResources ?? [],
});
this.logger.info(
`entity.metadata.name=${entityName} clusterDetails=[${clusterDetailsDecoratedForAuth
@@ -274,9 +278,10 @@ export class KubernetesFanOutHandler {
private async decorateClusterDetailsWithAuth(
entity: Entity,
auth: KubernetesRequestAuth,
requestContext: ServiceLocatorRequestContext,
) {
const clusterDetails: ClusterDetails[] = await (
await this.serviceLocator.getClustersByEntity(entity)
await this.serviceLocator.getClustersByEntity(entity, requestContext)
).clusters;
// Execute all of these async actions simultaneously/without blocking sequentially as no common object is modified by them
+12 -1
View File
@@ -120,12 +120,23 @@ export interface KubernetesClustersSupplier {
getClusters(): Promise<ClusterDetails[]>;
}
/**
* @alpha
*/
export interface ServiceLocatorRequestContext {
objectTypesToFetch: Set<ObjectToFetch>;
customResources: CustomResourceMatcher[];
}
/**
* Used to locate which cluster(s) a service is running on
* @alpha
*/
export interface KubernetesServiceLocator {
getClustersByEntity(entity: Entity): Promise<{ clusters: ClusterDetails[] }>;
getClustersByEntity(
entity: Entity,
requestContext: ServiceLocatorRequestContext,
): Promise<{ clusters: ClusterDetails[] }>;
}
/**