Skip to content

Commit

Permalink
feat: Add support for IAM Identity Center shortcut links (#129)
Browse files Browse the repository at this point in the history
Co-authored-by: Niall Thomson <[email protected]>
  • Loading branch information
benjidotsh and niallthomson authored May 31, 2024
1 parent 4a4a3af commit 2e54c29
Show file tree
Hide file tree
Showing 13 changed files with 323 additions and 10 deletions.
10 changes: 10 additions & 0 deletions plugins/codebuild/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,3 +165,13 @@ spec:
type: service
# ...
```
## IAM Identity Center shortcut links
As a user of [IAM Identity Center](https://docs.aws.amazon.com/singlesignon/latest/userguide/what-is.html), you can make use of [shortcut links](https://docs.aws.amazon.com/singlesignon/latest/userguide/createshortcutlink.html) by adding your AWS access portal subdomain to your `app-config.yaml`:
```yaml
aws:
sso:
subdomain: d-xxxxxxxxxx
```
10 changes: 10 additions & 0 deletions plugins/codebuild/frontend/config.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export interface Config {
aws?: {
sso?: {
/**
* @visibility frontend
*/
subdomain: string;
};
};
}
6 changes: 4 additions & 2 deletions plugins/codebuild/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@
"msw": "^1.0.0"
},
"files": [
"dist"
]
"dist",
"config.d.ts"
],
"configSchema": "config.d.ts"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import React from 'react';
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
import { CodeBuildProjectCard } from '.';
import { AwsCodeBuildApi, awsCodeBuildApiRef } from '../../api';
import { ConfigApi, configApiRef } from '@backstage/core-plugin-api';
import { ConfigReader } from '@backstage/core-app-api';
import { EntityProvider } from '@backstage/plugin-catalog-react';
import {
mockCodeBuildProject,
mockCodeBuildProjectBuild,
mockEntityWithTags,
} from '@aws/aws-codebuild-plugin-for-backstage-common';

const configApi: ConfigApi = new ConfigReader({
aws: {
sso: {
subdomain: 'd-123456',
},
},
});

const codeBuildApiSingle: Partial<AwsCodeBuildApi> = {
getProjectsByEntity: () =>
Promise.resolve({
projects: [
{
projectAccountId: '1234567890',
projectName: 'project1',
projectRegion: 'us-west-2',
project: mockCodeBuildProject('project1'),
builds: [mockCodeBuildProjectBuild('project1', 'test')],
},
],
}),
};

const PROJECT_URL = `https://us-west-2.console.aws.amazon.com/codesuite/codebuild/1234567890/projects/project1/?region=us-west-2`;
const SSO_PROJECT_URL = `https://d-123456.awsapps.com/start/#/console?account_id=1234567890&destination=${encodeURIComponent(
PROJECT_URL,
)}`;

describe('<CodeBuildProjectCard />', () => {
describe('for a single project', () => {
it('should show project status', async () => {
const rendered = await renderInTestApp(
<TestApiProvider apis={[[awsCodeBuildApiRef, codeBuildApiSingle]]}>
<EntityProvider entity={mockEntityWithTags}>
<CodeBuildProjectCard />
</EntityProvider>
</TestApiProvider>,
);

expect((await rendered.findByText('project1')).getAttribute('href')).toBe(
PROJECT_URL,
);
});

it('should use sso domain', async () => {
const rendered = await renderInTestApp(
<TestApiProvider
apis={[
[awsCodeBuildApiRef, codeBuildApiSingle],
[configApiRef, configApi],
]}
>
<EntityProvider entity={mockEntityWithTags}>
<CodeBuildProjectCard />
</EntityProvider>
</TestApiProvider>,
);

expect((await rendered.findByText('project1')).getAttribute('href')).toBe(
SSO_PROJECT_URL,
);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -38,20 +38,29 @@ import { Build } from '@aws-sdk/client-codebuild';
import { BuildStatus } from '../BuildStatus';
import { useProjects } from '../../hooks';
import { formatTime, getDurationFromStringDates } from '../../util';
import { useApi, configApiRef } from '@backstage/core-plugin-api';
import { generateShortcutLink } from '@aws/aws-core-plugin-for-backstage-common';

const generatedColumns = (
region: string,
project: string,
accountId: string,
ssoSubdomain?: string,
) => {
return [
{
title: 'Build run',
field: 'id',
render: (row: Partial<Build>) => {
const projectUrl = `https://${region}.console.aws.amazon.com/codesuite/codebuild/${accountId}/projects/${project}/build/${row.id}/?region=${region}`;

return (
<Link
href={`https://${region}.console.aws.amazon.com/codesuite/codebuild/${accountId}/projects/${project}/build/${row.id}/?region=${region}`}
href={
ssoSubdomain
? generateShortcutLink(ssoSubdomain, accountId, projectUrl)
: projectUrl
}
target="_blank"
>
#{row.buildNumber}
Expand Down Expand Up @@ -115,14 +124,28 @@ export const ProjectWidgetContent = ({
}: {
project: ProjectResponse;
}) => {
const configApi = useApi(configApiRef);
const ssoSubdomain = configApi.getOptionalString('aws.sso.subdomain');

const projectUrl = `https://${project.projectRegion}.console.aws.amazon.com/codesuite/codebuild/${project.projectAccountId}/projects/${project.projectName}/?region=${project.projectRegion}`;

return (
<div>
<Box sx={{ m: 2 }}>
<Grid container>
<AboutField label="Project Name" gridSizes={{ md: 12 }}>
<Link href={projectUrl} target="_blank">
<Link
href={
ssoSubdomain
? generateShortcutLink(
ssoSubdomain,
project.projectAccountId,
projectUrl,
)
: projectUrl
}
target="_blank"
>
{project.projectName}
</Link>
</AboutField>
Expand Down Expand Up @@ -155,6 +178,7 @@ export const ProjectWidgetContent = ({
project.projectRegion,
project.project.name!,
project.projectAccountId,
ssoSubdomain,
)}
/>
</div>
Expand Down
10 changes: 10 additions & 0 deletions plugins/codepipeline/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,3 +183,13 @@ spec:
type: service
# ...
```
## IAM Identity Center shortcut links
As a user of [IAM Identity Center](https://docs.aws.amazon.com/singlesignon/latest/userguide/what-is.html), you can make use of [shortcut links](https://docs.aws.amazon.com/singlesignon/latest/userguide/createshortcutlink.html) by adding your AWS access portal subdomain to your `app-config.yaml`:
```yaml
aws:
sso:
subdomain: d-xxxxxxxxxx
```
10 changes: 10 additions & 0 deletions plugins/codepipeline/frontend/config.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export interface Config {
aws?: {
sso?: {
/**
* @visibility frontend
*/
subdomain: string;
};
};
}
7 changes: 5 additions & 2 deletions plugins/codepipeline/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
},
"dependencies": {
"@aws-sdk/client-codepipeline": "^3.511.0",
"@aws-sdk/util-arn-parser": "^3.568.0",
"@aws/aws-codepipeline-plugin-for-backstage-common": "workspace:^",
"@aws/aws-core-plugin-for-backstage-common": "workspace:^",
"@aws/aws-core-plugin-for-backstage-react": "workspace:^",
Expand Down Expand Up @@ -61,6 +62,8 @@
"msw": "^1.0.0"
},
"files": [
"dist"
]
"dist",
"config.d.ts"
],
"configSchema": "config.d.ts"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import React from 'react';
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
import { CodePipelineExecutions } from '.';
import { AwsCodePipelineApi, awsCodePipelineApiRef } from '../../api';
import {
mockCodePipelineExecutions,
mockEntityWithTags,
} from '@aws/aws-codepipeline-plugin-for-backstage-common';
import { ConfigApi, configApiRef } from '@backstage/core-plugin-api';
import { ConfigReader } from '@backstage/core-app-api';

const configApi: ConfigApi = new ConfigReader({
aws: {
sso: {
subdomain: 'd-123456',
},
},
});

const codePipelineApiSingle: Partial<AwsCodePipelineApi> = {
getPipelineExecutionsByEntity: () =>
Promise.resolve({
pipelineExecutions: [
{
pipelineName: 'pipeline1',
pipelineRegion: 'us-west-2',
pipelineArn: 'arn:aws:codepipeline:us-west-2:1234567890:pipeline1',
pipelineExecutions: mockCodePipelineExecutions(),
},
],
}),
};

const codePipelineApiMultiple: Partial<AwsCodePipelineApi> = {
getPipelineExecutionsByEntity: () =>
Promise.resolve({
pipelineExecutions: [
{
pipelineName: 'pipeline1',
pipelineRegion: 'us-west-2',
pipelineArn: 'arn:aws:codepipeline:us-west-2:1234567890:pipeline1',
pipelineExecutions: mockCodePipelineExecutions(),
},
{
pipelineName: 'pipeline2',
pipelineRegion: 'us-west-2',
pipelineArn: 'arn:aws:codepipeline:us-west-2:1234567890:pipeline2',
pipelineExecutions: mockCodePipelineExecutions(),
},
],
}),
};

const EXECUTION_ID = 'e6c91a02-d844-4663-ad62-b719608f8fc5';

const CONSOLE_URL = `https://us-west-2.console.aws.amazon.com/codesuite/codepipeline/pipelines/pipeline1/executions/${EXECUTION_ID}/timeline?region=us-west-2`;
const SSO_CONSOLE_URL = `https://d-123456.awsapps.com/start/#/console?account_id=1234567890&destination=${encodeURIComponent(
CONSOLE_URL,
)}`;

describe('<CodePipelineExecutions />', () => {
describe('for a single pipeline', () => {
it('should show latest executions', async () => {
const rendered = await renderInTestApp(
<TestApiProvider
apis={[[awsCodePipelineApiRef, codePipelineApiSingle]]}
>
<CodePipelineExecutions entity={mockEntityWithTags} />
</TestApiProvider>,
);

expect(await rendered.findByText(EXECUTION_ID)).toBeInTheDocument();
expect(
await rendered.queryByTestId('select-pipeline'),
).not.toBeInTheDocument();
expect(
(await rendered.findByText(EXECUTION_ID)).getAttribute('href'),
).toBe(CONSOLE_URL);
});

it('should use sso domain', async () => {
const rendered = await renderInTestApp(
<TestApiProvider
apis={[
[awsCodePipelineApiRef, codePipelineApiSingle],
[configApiRef, configApi],
]}
>
<CodePipelineExecutions entity={mockEntityWithTags} />
</TestApiProvider>,
);

expect(await rendered.findByText(EXECUTION_ID)).toBeInTheDocument();
expect(
await rendered.queryByTestId('select-pipeline'),
).not.toBeInTheDocument();
expect(
(await rendered.findByText(EXECUTION_ID)).getAttribute('href'),
).toBe(SSO_CONSOLE_URL);
});
});

describe('for multiple pipelines', () => {
it('should show latest executions', async () => {
const rendered = await renderInTestApp(
<TestApiProvider
apis={[[awsCodePipelineApiRef, codePipelineApiMultiple]]}
>
<CodePipelineExecutions entity={mockEntityWithTags} />
</TestApiProvider>,
);

expect(await rendered.findByText(EXECUTION_ID)).toBeInTheDocument();
expect(
await rendered.queryByTestId('select-pipeline'),
).toBeInTheDocument();
});
});
});
Loading

0 comments on commit 2e54c29

Please sign in to comment.