Merge pull request #3351 from mfrinnstrom/scaffolder-support-add-azure-components

[Scaffolder] support adding scaffolded components in Azure DevOps to catalog
This commit is contained in:
Ben Lambert
2020-11-20 11:04:18 +01:00
committed by GitHub
10 changed files with 78 additions and 54 deletions
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/plugin-scaffolder': patch
'@backstage/plugin-scaffolder-backend': patch
---
Move constructing the catalog-info.yaml URL for scaffolded components to the publishers
@@ -41,7 +41,7 @@ describe('Azure Publisher', () => {
describe('publish: createRemoteInAzure', () => {
it('should use azure-devops-node-api to create a repo in the given project', async () => {
mockGitApi.createRepository.mockResolvedValue({
remoteUrl: 'mockclone',
remoteUrl: 'https://dev.azure.com/organization/project/_git/repo',
} as { remoteUrl: string });
const result = await publisher.publish({
@@ -52,7 +52,11 @@ describe('Azure Publisher', () => {
directory: '/tmp/test',
});
expect(result).toEqual({ remoteUrl: 'mockclone' });
expect(result).toEqual({
remoteUrl: 'https://dev.azure.com/organization/project/_git/repo',
catalogInfoUrl:
'https://dev.azure.com/organization/project/_git/repo?path=%2Fcatalog-info.yaml',
});
expect(mockGitApi.createRepository).toHaveBeenCalledWith(
{
name: 'repo',
@@ -61,7 +65,7 @@ describe('Azure Publisher', () => {
);
expect(pushToRemoteUserPass).toHaveBeenCalledWith(
'/tmp/test',
'mockclone',
'https://dev.azure.com/organization/project/_git/repo',
'notempty',
'fake-token',
);
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { PublisherBase } from './types';
import { PublisherBase, PublisherOptions, PublisherResult } from './types';
import { GitApi } from 'azure-devops-node-api/GitApi';
import { GitRepositoryCreateOptions } from 'azure-devops-node-api/interfaces/GitInterfaces';
import { pushToRemoteUserPass } from './helpers';
@@ -33,14 +33,12 @@ export class AzurePublisher implements PublisherBase {
async publish({
values,
directory,
}: {
values: RequiredTemplateValues & Record<string, JsonValue>;
directory: string;
}): Promise<{ remoteUrl: string }> {
}: PublisherOptions): Promise<PublisherResult> {
const remoteUrl = await this.createRemote(values);
await pushToRemoteUserPass(directory, remoteUrl, 'notempty', this.token);
const catalogInfoUrl = `${remoteUrl}?path=%2Fcatalog-info.yaml`;
return { remoteUrl };
return { remoteUrl, catalogInfoUrl };
}
private async createRemote(
@@ -53,7 +53,7 @@ describe('GitHub Publisher', () => {
it('should use octokit to create a repo in an organisation if the organisation property is set', async () => {
mockGithubClient.repos.createInOrg.mockResolvedValue({
data: {
clone_url: 'mockclone',
clone_url: 'https://github.com/backstage/backstage.git',
},
} as OctokitResponse<ReposCreateInOrgResponseData>);
mockGithubClient.users.getByUsername.mockResolvedValue({
@@ -71,7 +71,11 @@ describe('GitHub Publisher', () => {
directory: '/tmp/test',
});
expect(result).toEqual({ remoteUrl: 'mockclone' });
expect(result).toEqual({
remoteUrl: 'https://github.com/backstage/backstage.git',
catalogInfoUrl:
'https://github.com/backstage/backstage/blob/master/catalog-info.yaml',
});
expect(mockGithubClient.repos.createInOrg).toHaveBeenCalledWith({
org: 'blam',
name: 'test',
@@ -89,7 +93,7 @@ describe('GitHub Publisher', () => {
});
expect(pushToRemoteUserPass).toHaveBeenCalledWith(
'/tmp/test',
'mockclone',
'https://github.com/backstage/backstage.git',
'abc',
'x-oauth-basic',
);
@@ -98,7 +102,7 @@ describe('GitHub Publisher', () => {
it('should use octokit to create a repo in the authed user if the organisation property is not set', async () => {
mockGithubClient.repos.createForAuthenticatedUser.mockResolvedValue({
data: {
clone_url: 'mockclone',
clone_url: 'https://github.com/backstage/backstage.git',
},
} as OctokitResponse<ReposCreateInOrgResponseData>);
mockGithubClient.users.getByUsername.mockResolvedValue({
@@ -116,7 +120,11 @@ describe('GitHub Publisher', () => {
directory: '/tmp/test',
});
expect(result).toEqual({ remoteUrl: 'mockclone' });
expect(result).toEqual({
remoteUrl: 'https://github.com/backstage/backstage.git',
catalogInfoUrl:
'https://github.com/backstage/backstage/blob/master/catalog-info.yaml',
});
expect(
mockGithubClient.repos.createForAuthenticatedUser,
).toHaveBeenCalledWith({
@@ -126,7 +134,7 @@ describe('GitHub Publisher', () => {
expect(mockGithubClient.repos.addCollaborator).not.toHaveBeenCalled();
expect(pushToRemoteUserPass).toHaveBeenCalledWith(
'/tmp/test',
'mockclone',
'https://github.com/backstage/backstage.git',
'abc',
'x-oauth-basic',
);
@@ -136,7 +144,7 @@ describe('GitHub Publisher', () => {
it('should invite other user in the authed user', async () => {
mockGithubClient.repos.createForAuthenticatedUser.mockResolvedValue({
data: {
clone_url: 'mockclone',
clone_url: 'https://github.com/backstage/backstage.git',
},
} as OctokitResponse<ReposCreateInOrgResponseData>);
mockGithubClient.users.getByUsername.mockResolvedValue({
@@ -155,7 +163,11 @@ describe('GitHub Publisher', () => {
directory: '/tmp/test',
});
expect(result).toEqual({ remoteUrl: 'mockclone' });
expect(result).toEqual({
remoteUrl: 'https://github.com/backstage/backstage.git',
catalogInfoUrl:
'https://github.com/backstage/backstage/blob/master/catalog-info.yaml',
});
expect(
mockGithubClient.repos.createForAuthenticatedUser,
).toHaveBeenCalledWith({
@@ -171,7 +183,7 @@ describe('GitHub Publisher', () => {
});
expect(pushToRemoteUserPass).toHaveBeenCalledWith(
'/tmp/test',
'mockclone',
'https://github.com/backstage/backstage.git',
'abc',
'x-oauth-basic',
);
@@ -188,7 +200,7 @@ describe('GitHub Publisher', () => {
it('creates a private repository in the organization with visibility set to internal', async () => {
mockGithubClient.repos.createInOrg.mockResolvedValue({
data: {
clone_url: 'mockclone',
clone_url: 'https://github.com/backstage/backstage.git',
},
} as OctokitResponse<ReposCreateInOrgResponseData>);
mockGithubClient.users.getByUsername.mockResolvedValue({
@@ -206,7 +218,11 @@ describe('GitHub Publisher', () => {
directory: '/tmp/test',
});
expect(result).toEqual({ remoteUrl: 'mockclone' });
expect(result).toEqual({
remoteUrl: 'https://github.com/backstage/backstage.git',
catalogInfoUrl:
'https://github.com/backstage/backstage/blob/master/catalog-info.yaml',
});
expect(mockGithubClient.repos.createInOrg).toHaveBeenCalledWith({
org: 'blam',
name: 'test',
@@ -215,7 +231,7 @@ describe('GitHub Publisher', () => {
});
expect(pushToRemoteUserPass).toHaveBeenCalledWith(
'/tmp/test',
'mockclone',
'https://github.com/backstage/backstage.git',
'abc',
'x-oauth-basic',
);
@@ -232,7 +248,7 @@ describe('GitHub Publisher', () => {
it('creates a private repository', async () => {
mockGithubClient.repos.createForAuthenticatedUser.mockResolvedValue({
data: {
clone_url: 'mockclone',
clone_url: 'https://github.com/backstage/backstage.git',
},
} as OctokitResponse<ReposCreateInOrgResponseData>);
mockGithubClient.users.getByUsername.mockResolvedValue({
@@ -249,7 +265,11 @@ describe('GitHub Publisher', () => {
directory: '/tmp/test',
});
expect(result).toEqual({ remoteUrl: 'mockclone' });
expect(result).toEqual({
remoteUrl: 'https://github.com/backstage/backstage.git',
catalogInfoUrl:
'https://github.com/backstage/backstage/blob/master/catalog-info.yaml',
});
expect(
mockGithubClient.repos.createForAuthenticatedUser,
).toHaveBeenCalledWith({
@@ -258,7 +278,7 @@ describe('GitHub Publisher', () => {
});
expect(pushToRemoteUserPass).toHaveBeenCalledWith(
'/tmp/test',
'mockclone',
'https://github.com/backstage/backstage.git',
'abc',
'x-oauth-basic',
);
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { PublisherBase } from './types';
import { PublisherBase, PublisherOptions, PublisherResult } from './types';
import { Octokit } from '@octokit/rest';
import { pushToRemoteUserPass } from './helpers';
import { JsonValue } from '@backstage/config';
@@ -46,10 +46,7 @@ export class GithubPublisher implements PublisherBase {
async publish({
values,
directory,
}: {
values: RequiredTemplateValues & Record<string, JsonValue>;
directory: string;
}): Promise<{ remoteUrl: string }> {
}: PublisherOptions): Promise<PublisherResult> {
const remoteUrl = await this.createRemote(values);
await pushToRemoteUserPass(
directory,
@@ -57,8 +54,12 @@ export class GithubPublisher implements PublisherBase {
this.token,
'x-oauth-basic',
);
const catalogInfoUrl = remoteUrl.replace(
/\.git$/,
'/blob/master/catalog-info.yaml',
);
return { remoteUrl };
return { remoteUrl, catalogInfoUrl };
}
private async createRemote(
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { PublisherBase } from './types';
import { PublisherBase, PublisherOptions, PublisherResult } from './types';
import { Gitlab } from '@gitbeaker/core';
import { pushToRemoteUserPass } from './helpers';
import { JsonValue } from '@backstage/config';
@@ -32,10 +32,7 @@ export class GitlabPublisher implements PublisherBase {
async publish({
values,
directory,
}: {
values: RequiredTemplateValues & Record<string, JsonValue>;
directory: string;
}): Promise<{ remoteUrl: string }> {
}: PublisherOptions): Promise<PublisherResult> {
const remoteUrl = await this.createRemote(values);
await pushToRemoteUserPass(directory, remoteUrl, 'oauth2', this.token);
@@ -29,11 +29,17 @@ export type PublisherBase = {
* catalog, plus the values from the form and the directory that has
* been templated
*/
publish(opts: {
entity: TemplateEntityV1alpha1;
values: RequiredTemplateValues & Record<string, JsonValue>;
directory: string;
}): Promise<{ remoteUrl: string }>;
publish(opts: PublisherOptions): Promise<PublisherResult>;
};
export type PublisherOptions = {
values: RequiredTemplateValues & Record<string, JsonValue>;
directory: string;
};
export type PublisherResult = {
remoteUrl: string;
catalogInfoUrl?: string;
};
export type PublisherBuilder = {
@@ -156,12 +156,11 @@ export async function createRouter(
handler: async (ctx: StageContext<{ resultDir: string }>) => {
const publisher = publishers.get(ctx.entity);
ctx.logger.info('Will now store the template');
const { remoteUrl } = await publisher.publish({
entity: ctx.entity,
const result = await publisher.publish({
values: ctx.values,
directory: ctx.resultDir,
});
return { remoteUrl };
return result;
},
},
],
@@ -106,18 +106,10 @@ export const TemplatePage = () => {
);
const handleCreateComplete = async (job: Job) => {
const target = job.metadata.remoteUrl?.replace(
/\.git$/,
// TODO(Rugvip): This is not the location we want. As part of scaffolder v2 we
// want this to be more flexible, but before that we might want
// to update all templates to use catalog-info.yaml instead.
'/blob/master/component-info.yaml',
);
if (!target) {
if (!job.metadata.catalogInfoUrl) {
errorApi.post(
new Error(
`Failed to find component-info.yaml file in ${job.metadata.remoteUrl}.`,
`Failed to find catalog-info.yaml file in ${job.metadata.remoteUrl}.`,
),
);
return;
@@ -125,7 +117,7 @@ export const TemplatePage = () => {
const {
entities: [createdEntity],
} = await catalogApi.addLocation({ target });
} = await catalogApi.addLocation({ target: job.metadata.catalogInfoUrl });
setEntity((createdEntity as any) as TemplateEntityV1alpha1);
};
+1
View File
@@ -19,6 +19,7 @@ export type Job = {
entity: any;
values: any;
remoteUrl?: string;
catalogInfoUrl?: string;
};
status: 'PENDING' | 'STARTED' | 'COMPLETED' | 'FAILED';
stages: Stage[];