Merge branch 'master' into kp/fix-misleading-status-code-in-error-page

This commit is contained in:
Karthikeyan Perumal
2024-01-20 17:12:46 -06:00
55 changed files with 714 additions and 79 deletions
+14
View File
@@ -0,0 +1,14 @@
---
'@backstage/plugin-explore-backend': patch
---
Add support for the new backend system.
A new backend plugin for the explore backend
was added and exported as `default`.
You can use it with the new backend system like
```ts title="packages/backend/src/index.ts"
backend.add(import('@backstage/plugin-explore-backend'));
```
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/app-defaults': patch
'@backstage/plugin-catalog': patch
---
Change default icon for `kind:resource` to the storage icon.
+25
View File
@@ -0,0 +1,25 @@
---
'@backstage/frontend-app-api': minor
---
**BREAKING**: The `app.routes.bindings` app-config mapping has been simplified. You now only need to specify the plugin ID and route ID on both sides of the mapping.
Old form:
```yaml
app:
routes:
bindings:
plugin.catalog.externalRoutes.viewTechDoc: plugin.techdocs.routes.docRoot
plugin.catalog.externalRoutes.createComponent: plugin.catalog-import.routes.importPage
```
New form:
```yaml
app:
routes:
bindings:
catalog.viewTechDoc: techdocs.docRoot
catalog.createComponent: catalog-import.importPage
```
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/frontend-app-api': patch
---
Improved the error message when data input/output shapes do not match
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
Updated dependencies in frontend plugin templates
+12
View File
@@ -0,0 +1,12 @@
---
'@backstage/plugin-cloudbuild': minor
---
Changed build list view to automatically filter builds based on repository name matching component-info's metadata.name.
Added optional `google.com/cloudbuild-repo-name` annotation which allows you to specify a different repository to filter on.
Added optional `google.com/cloudbuild-trigger-name` annotation which allows you to filter based on a trigger name instead of a repo name.
Updated the ReadMe with information about the filtering and some other minor verbiage updates.
Changed `substitutions.BRANCH_NAME` to `substitutions.REF_NAME` so that the Ref field is populated properly.
Added optional `google.com/cloudbuild-location` annotation which allows you to specify the Cloud Build location of your builds. Default is global scope.
Changed build list view to show builds in a specific location if the location annotation is used.
Updated ReadMe with information about the use of the location filtering.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
Updated the backend module template to make the module instance the package default export.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-azure-devops': patch
---
Prefer `dev.azure.com/build-definition` annotation when it is provided, as it is more specific than `dev.azure.com/project-repo`. This can also be used as a filter for mono-repos.
@@ -1 +1 @@
This terraform file should create a S3 bucket and setup IAM with a user with an inline policy which gives the user access to the bucket. After you have created the bucket, user and policy you should go to the user in the AWS console and create an access key. This access key should be used as the env variables in step 3a [here](https://backstage.io/docs/features/techdocs/using-cloud-storage#configuring-aws-s3-bucket-with-techdocs).
This Terraform file should create an S3 bucket and set up IAM with a user and an inline policy that authorises access to the bucket. After you've set up the bucket, user, and policy, go to the user's AWS dashboard and create an access key. This access key should be used to configure the environment variables in step 3a [here](https://backstage.io/docs/features/techdocs/using-cloud-storage#configuring-aws-s3-bucket-with-techdocs).
@@ -241,10 +241,10 @@ app:
bindings:
# point to the Scaffolder create component page when the Catalog create component ref is used
# highlight-next-line
plugin.catalog.externalRoutes.createComponent: plugin.scaffolder.routes.index
catalog.createComponent: scaffolder.index
# point to the Catalog details page when the Scaffolder component details ref is used
# highlight-next-line
plugin.scaffolder.externalRoutes.componentDetails: plugin.catalog.routes.details
scaffolder.componentDetails: catalog.details
```
We also have the ability to express this in code as an option to `createApp`, but you of course only need to use one of these two methods:
@@ -1,10 +1,10 @@
---
title: AWS App Developer Tools
title: OPA on AWS
author: Amazon Web Services
authorUrl: https://aws.amazon.com/
category: Infrastructure
description: Create and manage AWS Apps within Backstage
documentation: https://github.com/awslabs/app-development-for-backstage-io-on-aws#readme
iconUrl: https://github.com/awslabs/app-development-for-backstage-io-on-aws/blob/main/docs/images/AWS_logo.png?raw=true
description: Orchestrate Platforms and Applications (OPA) allows customers to build and manage AWS Apps & Environments within Backstage
documentation: https://opaonaws.io
iconUrl: https://github.com/awslabs/app-development-for-backstage-io-on-aws/blob/main/website/static/img/white_OPA_text02.png?raw=true
npmPackageName: '@aws/plugin-aws-apps-for-backstage'
addedDate: '2023-05-10'
+10
View File
@@ -0,0 +1,10 @@
---
title: Backchat GenAI
author: benwilcock
authorUrl: https://github.com/benwilcock
category: Services
description: Access your favorite open source GenAI GUIs privately from Backstage. Chat wth large language models in your portal. Choose from hundreds of LLMs. Run inferencing wherever you like - local or remote, CPU or GPU - it's up to you!
documentation: https://github.com/benwilcock/backstage-plugin-backchat
iconUrl: /img/backchat-logo.png
npmPackageName: '@benbravo73/backstage-plugin-backchat'
addedDate: '2024-01-12'
+10
View File
@@ -0,0 +1,10 @@
---
title: End of life
author: dweber019
authorUrl: https://github.com/dweber019
category: Quality
description: Display end of life data for entities from endoflife.data
documentation: https://github.com/dweber019/backstage-plugin-endoflife
iconUrl: https://raw.githubusercontent.com/dweber019/backstage-plugin-endoflife/main/plugins/endoflife/docs/pluginIcon.png
npmPackageName: '@dweber019/backstage-plugin-endoflife'
addedDate: '2024-01-18'
@@ -0,0 +1,15 @@
---
title: GitHub Codespaces
author: Aditya Singhal
authorUrl: https://github.com/adityasinghal26
category: Development
description: Integrates GitHub Codespaces for a Backstage component with the Authenticated User.
documentation: https://github.com/adityasinghal26/backstage-plugins/tree/main/plugins/github-codespaces
iconUrl: https://github.com/adityasinghal26/backstage-plugins/blob/00c9c00ba9acc3135014d6454ccf04f573195eef/plugins/github-codespaces/images/GitHubLogo.png
npmPackageName: '@adityasinghal26/plugin-github-codespaces'
tags:
- github
- codespaces
- development
- devcontainers
addedDate: '2023-12-30'
Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

+2 -2
View File
@@ -34,7 +34,7 @@ import MuiMenuBookIcon from '@material-ui/icons/MenuBook';
import MuiPeopleIcon from '@material-ui/icons/People';
import MuiPersonIcon from '@material-ui/icons/Person';
import MuiWarningIcon from '@material-ui/icons/Warning';
import MuiWorkIcon from '@material-ui/icons/Work';
import MuiStorageIcon from '@material-ui/icons/Storage';
import MuiFeaturedPlayListIcon from '@material-ui/icons/FeaturedPlayList';
export const icons = {
@@ -58,7 +58,7 @@ export const icons = {
'kind:location': MuiLocationOnIcon as IconComponent,
'kind:system': MuiCategoryIcon as IconComponent,
'kind:user': MuiPersonIcon as IconComponent,
'kind:resource': MuiWorkIcon as IconComponent,
'kind:resource': MuiStorageIcon as IconComponent,
'kind:template': MuiFeaturedPlayListIcon as IconComponent,
user: MuiPersonIcon as IconComponent,
warning: MuiWarningIcon as IconComponent,
+3 -3
View File
@@ -3,9 +3,9 @@ app:
packages: 'all' # ✨
routes:
bindings:
plugin.pages.externalRoutes.pageX: plugin.pages.routes.pageX
plugin.catalog.externalRoutes.viewTechDoc: plugin.techdocs.routes.docRoot
plugin.catalog.externalRoutes.createComponent: plugin.catalog-import.routes.importPage
pages.pageX: pages.pageX
catalog.viewTechDoc: techdocs.docRoot
catalog.createComponent: catalog-import.importPage
extensions:
# - apis.plugin.graphiql.browse.gitlab: true
@@ -5,4 +5,4 @@
* @packageDocumentation
*/
export { {{moduleVar}} } from './module';
export { {{moduleVar}} as default } from './module';
@@ -45,8 +45,8 @@
"@backstage/core-app-api": "{{versionQuery '@backstage/core-app-api'}}",
"@backstage/dev-utils": "{{versionQuery '@backstage/dev-utils'}}",
"@backstage/test-utils": "{{versionQuery '@backstage/test-utils'}}",
"@testing-library/jest-dom": "{{versionQuery '@testing-library/jest-dom' '5.10.1'}}",
"@testing-library/react": "{{versionQuery '@testing-library/react' '12.1.3'}}",
"@testing-library/jest-dom": "{{versionQuery '@testing-library/jest-dom' '6.0.0'}}",
"@testing-library/react": "{{versionQuery '@testing-library/react' '14.0.0'}}",
"@testing-library/user-event": "{{versionQuery '@testing-library/user-event' '14.0.0'}}",
"msw": "{{versionQuery 'msw' '1.0.0'}}"
},
@@ -39,8 +39,8 @@
"devDependencies": {
"@backstage/cli": "{{versionQuery '@backstage/cli'}}",
"@backstage/test-utils": "{{versionQuery '@backstage/test-utils'}}",
"@testing-library/jest-dom": "{{versionQuery '@testing-library/jest-dom' '5.10.1'}}",
"@testing-library/react": "{{versionQuery '@testing-library/react' '12.1.3'}}"
"@testing-library/jest-dom": "{{versionQuery '@testing-library/jest-dom' '6.0.0'}}",
"@testing-library/react": "{{versionQuery '@testing-library/react' '14.0.0'}}"
},
"files": [
"dist"
@@ -30,7 +30,7 @@
},
"devDependencies": {
"@backstage/cli": "{{versionQuery '@backstage/cli'}}",
"@testing-library/jest-dom": "{{versionQuery '@testing-library/jest-dom' '5.10.1'}}"
"@testing-library/jest-dom": "{{versionQuery '@testing-library/jest-dom' '6.0.0'}}"
},
"files": [
"dist"
@@ -37,15 +37,13 @@ describe('collectRouteIds', () => {
createPlugin({ id: 'test', routes: { ref }, externalRoutes: { extRef } }),
]);
expect(Object.fromEntries(collected.routes)).toEqual({
'plugin.test.routes.ref': ref,
'test.ref': ref,
});
expect(Object.fromEntries(collected.externalRoutes)).toEqual({
'plugin.test.externalRoutes.extRef': extRef,
'test.extRef': extRef,
});
expect(String(ref)).toBe('RouteRef{plugin.test.routes.ref}');
expect(String(extRef)).toBe(
'ExternalRouteRef{plugin.test.externalRoutes.extRef}',
);
expect(String(ref)).toBe('RouteRef{test.ref}');
expect(String(extRef)).toBe('ExternalRouteRef{test.extRef}');
});
});
@@ -47,7 +47,7 @@ export function collectRouteIds(features: FrontendFeature[]): RouteRefsById {
}
for (const [name, ref] of Object.entries(feature.routes)) {
const refId = `plugin.${feature.id}.routes.${name}`;
const refId = `${feature.id}.${name}`;
if (routesById.has(refId)) {
throw new Error(`Unexpected duplicate route '${refId}'`);
}
@@ -62,7 +62,7 @@ export function collectRouteIds(features: FrontendFeature[]): RouteRefsById {
}
}
for (const [name, ref] of Object.entries(feature.externalRoutes)) {
const refId = `plugin.${feature.id}.externalRoutes.${name}`;
const refId = `${feature.id}.${name}`;
if (externalRoutesById.has(refId)) {
throw new Error(`Unexpected duplicate external route '${refId}'`);
}
@@ -615,8 +615,8 @@ describe('createAppNodeInstance', () => {
),
),
}),
).toThrow(
"Failed to instantiate extension 'app/test', input 'singleton' did not receive required extension data 'other' from extension 'app/test'",
).toThrowErrorMatchingInlineSnapshot(
`"Failed to instantiate extension 'app/test', extension 'app/test' could not be attached because its output data ('test', 'other') does not match what the input 'singleton' requires ('other')"`,
);
});
});
@@ -37,8 +37,17 @@ function resolveInputData(
return mapValues(dataMap, ref => {
const value = attachment.instance?.getData(ref);
if (value === undefined && !ref.config.optional) {
const expected = Object.values(dataMap)
.filter(r => !r.config.optional)
.map(r => `'${r.id}'`)
.join(', ');
const provided = [...(attachment.instance?.getDataRefs() ?? [])]
.map(r => `'${r.id}'`)
.join(', ');
throw new Error(
`input '${inputName}' did not receive required extension data '${ref.id}' from extension '${attachment.spec.id}'`,
`extension '${attachment.spec.id}' could not be attached because its output data (${provided}) does not match what the input '${inputName}' requires (${expected})`,
);
}
return value;
@@ -63,8 +63,8 @@ describe('createExtensionTester', () => {
factory: () => ({ path: '/foo' }),
});
const tester = createExtensionTester(extension);
expect(() => tester.render()).toThrow(
"Failed to instantiate extension 'app/routes', input 'routes' did not receive required extension data 'core.reactElement' from extension 'test'",
expect(() => tester.render()).toThrowErrorMatchingInlineSnapshot(
`"Failed to instantiate extension 'app/routes', extension 'test' could not be attached because its output data ('core.routing.path') does not match what the input 'routes' requires ('core.routing.path', 'core.reactElement')"`,
);
});
+19
View File
@@ -61,6 +61,25 @@ spec:
# ...
```
#### Mono repos
If you have multiple entities within a single repo, you will need to specify which pipelines belong to each entity.
```yaml
dev.azure.com/project-repo: <my-project>/<my-repo>
dev.azure.com/build-definition: <build-definition-name>
```
#### Pipeline in different project to repo
If your pipeline is in a different project to the source code, you will need to specify this in the project annotation.
```yaml
dev.azure.com/project-repo: <project-with-source-code>/<my-repo>
dev.azure.com/build-definition: <build-definition-name>
dev.azure.com/project: <project-with-build-code>
```
#### Azure Pipelines Only
If you are only using Azure Pipelines along with a different SCM tool then you can use the following two annotations to see Builds:
@@ -99,7 +99,7 @@ describe('useBuildRuns', () => {
renderHook(() => useBuildRuns(entity), {
wrapper: Wrapper,
}),
).toThrow('Value for annotation "dev.azure.com/project" was not found');
).toThrow('Expected "dev.azure.com" annotations were not found');
});
it('should return throw when annotation invalid', async () => {
@@ -103,7 +103,7 @@ describe('useGitTags', () => {
renderHook(() => useGitTags(entity), {
wrapper: Wrapper,
}),
).toThrow('Value for annotation "dev.azure.com/project" was not found');
).toThrow('Expected "dev.azure.com" annotations were not found');
});
it('should return throw when annotation invalid', async () => {
@@ -102,7 +102,7 @@ describe('usePullRequests', () => {
renderHook(() => usePullRequests(entity), {
wrapper: Wrapper,
}),
).toThrow('Value for annotation "dev.azure.com/project" was not found');
).toThrow('Expected "dev.azure.com" annotations were not found');
});
it('should return throw when annotation invalid', async () => {
@@ -81,7 +81,7 @@ describe('useReadme', () => {
renderHook(() => useReadme(entity), {
wrapper: Wrapper,
}),
).toThrow('Value for annotation "dev.azure.com/project" was not found');
).toThrow('Expected "dev.azure.com" annotations were not found');
});
it('should return throw when annotation invalid', async () => {
+93 -1
View File
@@ -13,10 +13,102 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { azureDevOpsPlugin } from './plugin';
import { Entity } from '@backstage/catalog-model';
import { azureDevOpsPlugin, isAzurePipelinesAvailable } from './plugin';
describe('azure-devops', () => {
it('should export plugin', () => {
expect(azureDevOpsPlugin).toBeDefined();
});
describe('isAzurePipelinesAvailable', () => {
it('should be true when project-repo annotation is present', () => {
const entity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
namespace: 'default',
name: 'sample',
annotations: {
'dev.azure.com/project-repo': 'projectName/repoName',
},
},
};
expect(isAzurePipelinesAvailable(entity)).toBe(true);
});
it('should be true when project and build-definition annotation is present', () => {
const entity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
namespace: 'default',
name: 'sample',
annotations: {
'dev.azure.com/project': 'projectName',
'dev.azure.com/build-definition': 'buildDefinitionName',
},
},
};
expect(isAzurePipelinesAvailable(entity)).toBe(true);
});
it('should be true when project-repo and build-definition annotation is present', () => {
const entity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
namespace: 'default',
name: 'sample',
annotations: {
'dev.azure.com/project-repo': 'projectName/repoName',
'dev.azure.com/build-definition': 'buildDefinitionName',
},
},
};
expect(isAzurePipelinesAvailable(entity)).toBe(true);
});
it('should be false when no annotations are present', () => {
const entity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
namespace: 'default',
name: 'sample',
},
};
expect(isAzurePipelinesAvailable(entity)).toBe(false);
});
it('should be false when only project annotation is present', () => {
const entity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
namespace: 'default',
name: 'sample',
annotations: {
'dev.azure.com/project': 'projectName',
},
},
};
expect(isAzurePipelinesAvailable(entity)).toBe(false);
});
it('should be false when only build-definition annotation is present', () => {
const entity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
namespace: 'default',
name: 'sample',
annotations: {
'dev.azure.com/build-definition': 'buildDefinitionName',
},
},
};
expect(isAzurePipelinesAvailable(entity)).toBe(false);
});
});
});
@@ -18,6 +18,22 @@ import { Entity } from '@backstage/catalog-model';
import { getAnnotationValuesFromEntity } from './getAnnotationValuesFromEntity';
describe('getAnnotationValuesFromEntity', () => {
describe('without any annotations', () => {
it('should throw annotations not found', () => {
const entity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
namespace: 'default',
name: 'project-repo',
},
};
expect(() => getAnnotationValuesFromEntity(entity)).toThrow(
'Expected "dev.azure.com" annotations were not found',
);
});
});
describe('with valid project-repo annotation', () => {
it('should return project and repo', () => {
const entity: Entity = {
@@ -140,7 +156,7 @@ describe('getAnnotationValuesFromEntity', () => {
});
describe('with only project annotation', () => {
it('should should throw annotation not found error', () => {
it('should throw annotation not found error', () => {
const entity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
@@ -163,7 +179,7 @@ describe('getAnnotationValuesFromEntity', () => {
});
describe('with only build-definition annotation', () => {
it('should should throw annotation not found error', () => {
it('should throw annotation not found error', () => {
const entity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
@@ -381,4 +397,55 @@ describe('getAnnotationValuesFromEntity', () => {
);
});
});
describe('projectRepo and buildDefinition are provided', () => {
it('should return project, repo and buildDefinition', () => {
const entity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
namespace: 'default',
name: 'project-repo',
annotations: {
'dev.azure.com/build-definition': 'buildDefinitionName',
'dev.azure.com/project-repo': 'projectName/repoName',
},
},
};
const values = getAnnotationValuesFromEntity(entity);
expect(values).toEqual({
project: 'projectName',
repo: 'repoName',
definition: 'buildDefinitionName',
host: undefined,
org: undefined,
});
});
});
describe('project, projectRepo and buildDefinition are provided', () => {
it('should prefer project over project-repo.project and return no repo', () => {
const entity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
namespace: 'default',
name: 'project-repo',
annotations: {
'dev.azure.com/project': 'projectName',
'dev.azure.com/build-definition': 'buildDefinitionName',
'dev.azure.com/project-repo': 'ignoredProject/repoName',
},
},
};
const values = getAnnotationValuesFromEntity(entity);
expect(values).toEqual({
project: 'projectName',
repo: undefined,
definition: 'buildDefinitionName',
host: undefined,
org: undefined,
});
});
});
});
@@ -29,34 +29,49 @@ export function getAnnotationValuesFromEntity(entity: Entity): {
host?: string;
org?: string;
} {
const { host, org } = getHostOrg(entity.metadata.annotations);
const projectRepoValues = getProjectRepo(entity.metadata.annotations);
if (projectRepoValues.project && projectRepoValues.repo) {
return {
project: projectRepoValues.project,
repo: projectRepoValues.repo,
host,
org,
};
}
const hostOrg = getHostOrg(entity.metadata.annotations);
const projectRepo = getProjectRepo(entity.metadata.annotations);
const project =
entity.metadata.annotations?.[AZURE_DEVOPS_PROJECT_ANNOTATION];
if (!project) {
const definition =
entity.metadata.annotations?.[AZURE_DEVOPS_BUILD_DEFINITION_ANNOTATION];
if (definition) {
if (project) {
return {
project,
definition,
...hostOrg,
};
}
if (projectRepo.project) {
return {
project: projectRepo.project,
repo: projectRepo.repo,
definition,
...hostOrg,
};
}
throw new Error(
`Value for annotation "${AZURE_DEVOPS_PROJECT_ANNOTATION}" was not found`,
);
} else {
if (projectRepo.project) {
return {
project: projectRepo.project,
repo: projectRepo.repo,
...hostOrg,
};
}
if (project) {
throw new Error(
`Value for annotation "${AZURE_DEVOPS_BUILD_DEFINITION_ANNOTATION}" was not found`,
);
}
}
const definition =
entity.metadata.annotations?.[AZURE_DEVOPS_BUILD_DEFINITION_ANNOTATION];
if (!definition) {
throw new Error(
`Value for annotation "${AZURE_DEVOPS_BUILD_DEFINITION_ANNOTATION}" was not found`,
);
}
return { project, definition, host, org };
throw new Error('Expected "dev.azure.com" annotations were not found');
}
function getProjectRepo(annotations?: Record<string, string>): {
@@ -26,7 +26,7 @@ import LocationOnIcon from '@material-ui/icons/LocationOn';
import MemoryIcon from '@material-ui/icons/Memory';
import PeopleIcon from '@material-ui/icons/People';
import PersonIcon from '@material-ui/icons/Person';
import WorkIcon from '@material-ui/icons/Work';
import StorageIcon from '@material-ui/icons/Storage';
import { DefaultEntityPresentationApiRenderer } from './DefaultEntityPresentationApi';
export const DEFAULT_CACHE_TTL: HumanDuration = { seconds: 10 };
@@ -39,7 +39,7 @@ export const DEFAULT_ICONS: Record<string, IconComponent> = {
api: ExtensionIcon,
component: MemoryIcon,
system: CategoryIcon,
resource: WorkIcon,
resource: StorageIcon,
domain: ApartmentIcon,
location: LocationOnIcon,
user: PersonIcon,
+63 -4
View File
@@ -1,4 +1,4 @@
# Google Cloud Build
# Google Cloud Build Plugin
### Welcome to the Google Cloud Build plugin!
@@ -47,7 +47,7 @@ const cicdContent = (
##### OPTIONAL
If you don't use GitHub Actions, or don't want to show it on your CI/CD page, then you can remove the switch case for it
If you don't use GitHub Actions, or don't want to show it on your CI/CD page, then you can remove the switch case for it:
```diff
// packages/app/src/components/catalog/EntityPage.tsx
@@ -62,9 +62,9 @@ const cicdContent = (
- </EntitySwitch.Case>
```
### Add annotation to your component-info.yaml file.
### Add annotation(s) to your component-info.yaml file
Any component, that you would like the Cloud Build Plugin to populate for, should include the following annotation:
Any component, that you would like the Cloud Build Plugin to populate for, should include the following `cloudbuild-project-slug` annotation. This annotation sets the GCP project name to be used for pulling the Cloud Build details from.
```diff
// component-info.yaml
@@ -79,3 +79,62 @@ spec:
type: website
lifecycle: development
```
By default, the cloud build results list is filtered by repository name equal to the name you have set in your component-info.yaml file. This is `metadata.name`. So if your metadata.name is `backstage` then it will only show builds matching the backstage repo name.
Additionally, build results are pulled from the `global` region by default.
#### Change Filtering
If you need the page to be filtered on a different repository name, then you can use the following annotation:
```diff
// component-info.yaml
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage
description: Backstage application.
annotations:
google.com/cloudbuild-project-slug: your-project-name
+ google.com/cloudbuild-repo-name: my-backstage
spec:
type: website
lifecycle: development
```
You can also automatically filter the results based on trigger name instead of repository name. To do so, use the following annotation:
```diff
// component-info.yaml
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage
description: Backstage application.
annotations:
google.com/cloudbuild-project-slug: your-project-name
+ google.com/cloudbuild-trigger-name: my-trigger-name
spec:
type: website
lifecycle: development
```
`Note:` The `cloudbuild-repo-name` annotation takes precedence over the `cloudbuild-trigger-name` annotation. So if you happen to use both annotations, cloudbuild-repo-name will be used. It is recommended to use one or the other if required.
If you need to pull Cloud Build results from a location or region other than the global scope, then use the following annotation:
```diff
// component-info.yaml
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage
description: Backstage application.
annotations:
google.com/cloudbuild-project-slug: your-project-name
+ google.com/cloudbuild-location: us-central1
spec:
type: website
lifecycle: development
```
+16 -3
View File
@@ -58,17 +58,22 @@ export const CLOUDBUILD_ANNOTATION = 'google.com/cloudbuild-project-slug';
export type CloudbuildApi = {
listWorkflowRuns: (options: {
projectId: string;
location: string;
cloudBuildFilter: string;
}) => Promise<ActionsListWorkflowRunsForRepoResponseData>;
getWorkflow: (options: {
projectId: string;
location: string;
id: string;
}) => Promise<ActionsGetWorkflowResponseData>;
getWorkflowRun: (options: {
projectId: string;
location: string;
id: string;
}) => Promise<ActionsGetWorkflowResponseData>;
reRunWorkflow: (options: {
projectId: string;
location: string;
runId: string;
}) => Promise<any>;
};
@@ -84,19 +89,27 @@ export class CloudbuildClient implements CloudbuildApi {
// (undocumented)
getWorkflow(options: {
projectId: string;
location: string;
id: string;
}): Promise<ActionsGetWorkflowResponseData>;
// (undocumented)
getWorkflowRun(options: {
projectId: string;
location: string;
id: string;
}): Promise<ActionsGetWorkflowResponseData>;
// (undocumented)
listWorkflowRuns(options: {
projectId: string;
location: string;
cloudBuildFilter: string;
}): Promise<ActionsListWorkflowRunsForRepoResponseData>;
// (undocumented)
reRunWorkflow(options: { projectId: string; runId: string }): Promise<void>;
reRunWorkflow(options: {
projectId: string;
location: string;
runId: string;
}): Promise<void>;
}
// @public (undocumented)
@@ -234,11 +247,11 @@ export interface StorageSource {
// @public (undocumented)
export interface Substitutions {
// (undocumented)
BRANCH_NAME: string;
// (undocumented)
COMMIT_SHA: string;
// (undocumented)
REF_NAME: string;
// (undocumented)
REPO_NAME: string;
// (undocumented)
REVISION_ID: string;
@@ -29,17 +29,22 @@ export const cloudbuildApiRef = createApiRef<CloudbuildApi>({
export type CloudbuildApi = {
listWorkflowRuns: (options: {
projectId: string;
location: string;
cloudBuildFilter: string;
}) => Promise<ActionsListWorkflowRunsForRepoResponseData>;
getWorkflow: (options: {
projectId: string;
location: string;
id: string;
}) => Promise<ActionsGetWorkflowResponseData>;
getWorkflowRun: (options: {
projectId: string;
location: string;
id: string;
}) => Promise<ActionsGetWorkflowResponseData>;
reRunWorkflow: (options: {
projectId: string;
location: string;
runId: string;
}) => Promise<any>;
};
+14 -1
View File
@@ -27,11 +27,14 @@ export class CloudbuildClient implements CloudbuildApi {
async reRunWorkflow(options: {
projectId: string;
location: string;
runId: string;
}): Promise<void> {
await fetch(
`https://cloudbuild.googleapis.com/v1/projects/${encodeURIComponent(
options.projectId,
)}/locations/${encodeURIComponent(
options.location,
)}/builds/${encodeURIComponent(options.runId)}:retry`,
{
method: 'POST',
@@ -45,11 +48,15 @@ export class CloudbuildClient implements CloudbuildApi {
async listWorkflowRuns(options: {
projectId: string;
location: string;
cloudBuildFilter: string;
}): Promise<ActionsListWorkflowRunsForRepoResponseData> {
const workflowRuns = await fetch(
`https://cloudbuild.googleapis.com/v1/projects/${encodeURIComponent(
options.projectId,
)}/builds`,
)}/locations/${encodeURIComponent(
options.location,
)}/builds?filter=${encodeURIComponent(options.cloudBuildFilter)}`,
{
headers: new Headers({
Accept: '*/*',
@@ -66,11 +73,14 @@ export class CloudbuildClient implements CloudbuildApi {
async getWorkflow(options: {
projectId: string;
location: string;
id: string;
}): Promise<ActionsGetWorkflowResponseData> {
const workflow = await fetch(
`https://cloudbuild.googleapis.com/v1/projects/${encodeURIComponent(
options.projectId,
)}/locations/${encodeURIComponent(
options.location,
)}/builds/${encodeURIComponent(options.id)}`,
{
headers: new Headers({
@@ -87,11 +97,14 @@ export class CloudbuildClient implements CloudbuildApi {
async getWorkflowRun(options: {
projectId: string;
location: string;
id: string;
}): Promise<ActionsGetWorkflowResponseData> {
const workflow = await fetch(
`https://cloudbuild.googleapis.com/v1/projects/${encodeURIComponent(
options.projectId,
)}/locations/${encodeURIComponent(
options.location,
)}/builds/${encodeURIComponent(options.id)}`,
{
headers: new Headers({
+1 -1
View File
@@ -81,7 +81,7 @@ export interface Options {
export interface Substitutions {
COMMIT_SHA: string;
SHORT_SHA: string;
BRANCH_NAME: string;
REF_NAME: string;
REPO_NAME: string;
REVISION_ID: string;
}
@@ -22,6 +22,8 @@ import { WorkflowRunStatus } from '../WorkflowRunStatus';
import { makeStyles, LinearProgress } from '@material-ui/core';
import ExternalLinkIcon from '@material-ui/icons/Launch';
import { CLOUDBUILD_ANNOTATION } from '../useProjectName';
import { getLocation } from '../useLocation';
import { getCloudbuildFilter } from '../useCloudBuildFilter';
import {
InfoCard,
@@ -79,9 +81,13 @@ export const LatestWorkflowRunCard = (props: { branch: string }) => {
const { entity } = useEntity();
const errorApi = useApi(errorApiRef);
const projectId = entity?.metadata.annotations?.[CLOUDBUILD_ANNOTATION] || '';
const location = getLocation(entity);
const cloudBuildFilter = getCloudbuildFilter(entity);
const [{ runs, loading, error }] = useWorkflowRuns({
projectId,
location,
cloudBuildFilter,
});
const lastRun = runs?.[0] ?? ({} as WorkflowRun);
useEffect(() => {
@@ -34,6 +34,7 @@ import { useProjectName } from '../useProjectName';
import { WorkflowRunStatus } from '../WorkflowRunStatus';
import { useWorkflowRunsDetails } from './useWorkflowRunsDetails';
import { Breadcrumbs, Link, WarningPanel } from '@backstage/core-components';
import { getLocation } from '../useLocation';
const useStyles = makeStyles(theme => ({
root: {
@@ -63,8 +64,9 @@ const useStyles = makeStyles(theme => ({
export const WorkflowRunDetails = (props: { entity: Entity }) => {
const { value: projectName, loading, error } = useProjectName(props.entity);
const [projectId] = (projectName ?? '/').split('/');
const location = getLocation(props.entity);
const details = useWorkflowRunsDetails(projectId);
const details = useWorkflowRunsDetails(projectId, location);
const classes = useStyles();
if (error) {
@@ -96,9 +98,9 @@ export const WorkflowRunDetails = (props: { entity: Entity }) => {
<TableBody>
<TableRow>
<TableCell>
<Typography noWrap>Branch</Typography>
<Typography noWrap>Ref</Typography>
</TableCell>
<TableCell>{details.value?.substitutions.BRANCH_NAME}</TableCell>
<TableCell>{details.value?.substitutions.REF_NAME}</TableCell>
</TableRow>
<TableRow>
<TableCell>
@@ -18,13 +18,14 @@ import { cloudbuildApiRef } from '../../api';
import { useApi, useRouteRefParams } from '@backstage/core-plugin-api';
import { buildRouteRef } from '../../routes';
export const useWorkflowRunsDetails = (projectId: string) => {
export const useWorkflowRunsDetails = (projectId: string, location: string) => {
const api = useApi(cloudbuildApiRef);
const { id } = useRouteRefParams(buildRouteRef);
const details = useAsync(async () => {
return projectId
? api.getWorkflowRun({
projectId,
location,
id: id,
})
: Promise.reject('No projectId provided');
@@ -35,7 +35,7 @@ describe('<WorkflowRunsTableView />', () => {
substitutions: {
COMMIT_SHA: 'e3adasd2e3adasd2e3adasd2',
SHORT_SHA: 'f12j1231',
BRANCH_NAME: 'main',
REF_NAME: 'main',
REPO_NAME: 'backstage',
REVISION_ID: 'g123123',
},
@@ -26,6 +26,8 @@ import { buildRouteRef } from '../../routes';
import { DateTime } from 'luxon';
import { Table, TableColumn, Link } from '@backstage/core-components';
import { useRouteRef } from '@backstage/core-plugin-api';
import { getLocation } from '../useLocation';
import { getCloudbuildFilter } from '../useCloudBuildFilter';
const generatedColumns: TableColumn[] = [
{
@@ -71,7 +73,7 @@ const generatedColumns: TableColumn[] = [
title: 'Ref',
render: (row: Partial<WorkflowRun>) => (
<Typography variant="body2" noWrap>
{row.substitutions?.BRANCH_NAME}
{row.substitutions?.REF_NAME}
</Typography>
),
},
@@ -162,9 +164,12 @@ export const WorkflowRunsTableView = ({
export const WorkflowRunsTable = (props: { entity: Entity }) => {
const { value: projectName, loading } = useProjectName(props.entity);
const [projectId] = (projectName ?? '/').split('/');
const location = getLocation(props.entity);
const cloudBuildFilter = getCloudbuildFilter(props.entity);
const [tableProps, { retry, setPage, setPageSize }] = useWorkflowRuns({
projectId,
location,
cloudBuildFilter,
});
return (
@@ -0,0 +1,43 @@
/*
* Copyright 2020 The Backstage Authors
*
* 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 } from '@backstage/catalog-model';
const CLOUDBUILD_FILTER_REPO_STRING = 'substitutions.REPO_NAME=';
const CLOUDBUILD_FILTER_REPO_ANNOTATION = 'google.com/cloudbuild-repo-name';
const CLOUDBUILD_FILTER_TRIGGER_STRING = 'substitutions.TRIGGER_NAME=';
const CLOUDBUILD_FILTER_TRIGGER_ANNOTATION =
'google.com/cloudbuild-trigger-name';
/** @public */
export const getCloudbuildFilter = (entity: Entity) => {
const repoAnnotation =
entity?.metadata.annotations?.[CLOUDBUILD_FILTER_REPO_ANNOTATION] ?? '';
const triggerAnnotation =
entity?.metadata.annotations?.[CLOUDBUILD_FILTER_TRIGGER_ANNOTATION] ?? '';
if (repoAnnotation) {
const cloudbuildFilter = CLOUDBUILD_FILTER_REPO_STRING + repoAnnotation;
return cloudbuildFilter;
} else if (triggerAnnotation) {
const cloudbuildFilter =
CLOUDBUILD_FILTER_TRIGGER_STRING + triggerAnnotation;
return cloudbuildFilter;
}
const entityName = entity?.metadata.name ?? '';
const cloudbuildFilter = CLOUDBUILD_FILTER_REPO_STRING + entityName;
return cloudbuildFilter;
};
@@ -0,0 +1,30 @@
/*
* Copyright 2020 The Backstage Authors
*
* 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 } from '@backstage/catalog-model';
const CLOUDBUILD_LOCATION_ANNOTATION = 'google.com/cloudbuild-location';
/** @public */
export const getLocation = (entity: Entity) => {
const locationAnnotation =
entity?.metadata.annotations?.[CLOUDBUILD_LOCATION_ANNOTATION] ?? '';
if (locationAnnotation) {
return locationAnnotation;
}
return 'global';
};
@@ -33,8 +33,12 @@ export type WorkflowRun = {
rerun: () => void;
};
export function useWorkflowRuns(options: { projectId: string }) {
const { projectId } = options;
export function useWorkflowRuns(options: {
projectId: string;
location: string;
cloudBuildFilter: string;
}) {
const { projectId, location, cloudBuildFilter } = options;
const api = useApi(cloudbuildApiRef);
const errorApi = useApi(errorApiRef);
@@ -51,6 +55,8 @@ export function useWorkflowRuns(options: { projectId: string }) {
return api
.listWorkflowRuns({
projectId,
location,
cloudBuildFilter,
})
.then(
(
@@ -65,6 +71,7 @@ export function useWorkflowRuns(options: { projectId: string }) {
try {
await api.reRunWorkflow({
projectId,
location,
runId: run.id,
});
} catch (e) {
+30
View File
@@ -9,6 +9,36 @@ for these tools.
### Adding the plugin to your `packages/backend`
Install dependencies
```bash
# From your Backstage root directory
yarn add --cwd packages/backend @backstage/plugin-explore-backend
```
Add feature
```ts title="packages/backend/src/index.ts"
backend.add(import('@backstage/plugin-explore-backend'));
```
Config:
```yaml
explore:
tools:
- title: New Relic
description: new relic plugin
url: /newrelic
image: https://i.imgur.com/L37ikrX.jpg
tags:
- newrelic
- proxy
- nerdGraph
```
### Adding the plugin to your `packages/backend` (old)
#### Tools as Config
Install dependencies
+5
View File
@@ -3,6 +3,7 @@
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { BackendFeature } from '@backstage/backend-plugin-api';
import { Config } from '@backstage/config';
import { ExploreTool } from '@backstage/plugin-explore-common';
import express from 'express';
@@ -16,6 +17,10 @@ import type { ToolDocumentCollatorFactoryOptions as ToolDocumentCollatorFactoryO
// @public (undocumented)
export function createRouter(options: RouterOptions): Promise<express.Router>;
// @public
const explorePlugin: () => BackendFeature;
export default explorePlugin;
// @public (undocumented)
export interface ExploreToolProvider {
getTools(request: GetExploreToolsRequest): Promise<GetExploreToolsResponse>;
+2
View File
@@ -29,6 +29,7 @@
},
"dependencies": {
"@backstage/backend-common": "workspace:^",
"@backstage/backend-plugin-api": "workspace:^",
"@backstage/config": "workspace:^",
"@backstage/plugin-explore-common": "workspace:^",
"@backstage/plugin-search-backend-module-explore": "workspace:^",
@@ -42,6 +43,7 @@
"yn": "^4.0.0"
},
"devDependencies": {
"@backstage/backend-test-utils": "workspace:^",
"@backstage/cli": "workspace:^",
"@types/supertest": "^2.0.8",
"supertest": "^6.2.4"
+1
View File
@@ -20,6 +20,7 @@
* @packageDocumentation
*/
export { explorePlugin as default } from './plugin';
export * from './service';
export * from './tools';
@@ -0,0 +1,50 @@
/*
* Copyright 2024 The Backstage Authors
*
* 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 { mockServices, startTestBackend } from '@backstage/backend-test-utils';
import { ExploreTool } from '@backstage/plugin-explore-common';
import { explorePlugin } from './plugin';
describe('explorePlugin', () => {
it('should register explore plugin and its router', async () => {
const tool: ExploreTool = {
title: 'Tool Title',
image: 'https://example.com/image.png',
url: 'https://example.com',
lifecycle: 'production',
tags: ['tag1', 'tag2'],
};
const httpRouterMock = mockServices.httpRouter.mock();
await startTestBackend({
extensionPoints: [],
features: [
explorePlugin(),
httpRouterMock.factory,
mockServices.rootConfig.factory({
data: {
explore: {
tools: [tool],
},
},
}),
],
});
expect(httpRouterMock.use).toHaveBeenCalledTimes(1);
});
});
+49
View File
@@ -0,0 +1,49 @@
/*
* Copyright 2024 The Backstage Authors
*
* 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 { loggerToWinstonLogger } from '@backstage/backend-common';
import {
coreServices,
createBackendPlugin,
} from '@backstage/backend-plugin-api';
import { createRouter } from './service';
import { StaticExploreToolProvider } from './tools';
/**
* The explore backend plugin.
*
* @public
*/
export const explorePlugin = createBackendPlugin({
pluginId: 'explore',
register(env) {
env.registerInit({
deps: {
config: coreServices.rootConfig,
httpRouter: coreServices.httpRouter,
logger: coreServices.logger,
},
async init({ config, httpRouter, logger }) {
httpRouter.use(
await createRouter({
logger: loggerToWinstonLogger(logger),
toolProvider: StaticExploreToolProvider.fromConfig(config),
}),
);
},
});
},
});
+2
View File
@@ -6415,6 +6415,8 @@ __metadata:
resolution: "@backstage/plugin-explore-backend@workspace:plugins/explore-backend"
dependencies:
"@backstage/backend-common": "workspace:^"
"@backstage/backend-plugin-api": "workspace:^"
"@backstage/backend-test-utils": "workspace:^"
"@backstage/cli": "workspace:^"
"@backstage/config": "workspace:^"
"@backstage/plugin-explore-common": "workspace:^"