Merge branch 'plugin-azure-functions' of https://github.com/wesley-pattison/backstage into plugin-azure-functions
This commit is contained in:
@@ -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',
|
||||
}),
|
||||
];
|
||||
```
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-kubernetes-backend': patch
|
||||
---
|
||||
|
||||
kubernetes service locator now take request context parameters
|
||||
@@ -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
@@ -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. |
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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[] }>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user