Consistently use job-full-name not name or slug

Signed-off-by: Andrew Shirley <andrew.shirley@sainsburys.co.uk>
Signed-off-by: blam <ben@blam.sh>
This commit is contained in:
Andrew Shirley
2021-07-11 14:14:58 +01:00
committed by blam
parent 91cc4913ab
commit 8e38300d7c
15 changed files with 97 additions and 86 deletions
+9 -7
View File
@@ -71,10 +71,10 @@ kind: Component
metadata:
name: artist-lookup
annotations:
'jenkins.io/job-slug': teamA/artistLookup-build
'jenkins.io/job-full-name': teamA/artistLookup-build
```
The old annotation name of `jenkins.io/github-folder` is equivalent to `jenkins.io/job-slug`
The old annotation name of `jenkins.io/github-folder` is equivalent to `jenkins.io/job-full-name`
#### Example - Multiple global instances
@@ -103,7 +103,7 @@ kind: Component
metadata:
name: artist-lookup
annotations:
'jenkins.io/job-slug': departmentFoo:teamA/artistLookup-build
'jenkins.io/job-full-name': departmentFoo:teamA/artistLookup-build
```
If the `departmentFoo:` part is omitted, the default instance will be assumed.
@@ -124,7 +124,7 @@ jenkins:
### Custom JenkinsInfoProvider
An example of a bespoke JenkinsInfoProvider which uses an organisation specific annotation to look up the Jenkins info (including jobName):
An example of a bespoke JenkinsInfoProvider which uses an organisation specific annotation to look up the Jenkins info (including jobFullName):
```typescript
class AcmeJenkinsInfoProvider implements JenkinsInfoProvider {
@@ -132,7 +132,7 @@ class AcmeJenkinsInfoProvider implements JenkinsInfoProvider {
async getInstance(opt: {
entityRef: EntityName;
jobName?: string;
jobFullName?: string;
}): Promise<JenkinsInfo> {
const PAAS_ANNOTATION = 'acme.example.com/paas-project-name';
@@ -157,7 +157,7 @@ class AcmeJenkinsInfoProvider implements JenkinsInfoProvider {
const { team, dept } = this.lookupPaasInfo(paasProjectName);
const baseUrl = `https://jenkins-${dept}.example.com/`;
const jobName = `${team}/${paasProjectName}`;
const jobFullName = `${team}/${paasProjectName}`;
const username = 'backstage-bot';
const apiKey = this.getJenkinsApiKey(paasProjectName);
const creds = btoa(`${username}:${apiKey}`);
@@ -167,7 +167,7 @@ class AcmeJenkinsInfoProvider implements JenkinsInfoProvider {
headers: {
Authorization: `Basic ${creds}`,
},
jobName,
jobFullName,
};
}
@@ -205,3 +205,5 @@ The domain model for Jenkins is not particularly clear but for the purposes of t
Jenkins contains a tree of *job*s which have children of either; other *job*s (making it a _folder_) or *build*s (making it a _project_).
Concepts like _pipeline_ and *view*s are meaningless (pipelines are just jobs for our purposes, views are (as the name suggests) just views of subsets of jobs)
A _job full name_ is a slash separated list of the names of the job, and the folders which contain it. For example `teamA/artistLookupService/develop`, and the same way that a filesystem path has folders and file names.
+4 -4
View File
@@ -23,10 +23,10 @@ export class DefaultJenkinsInfoProvider implements JenkinsInfoProvider {
// (undocumented)
getInstance(opt: {
entityRef: EntityName;
jobName?: string;
jobFullName?: string;
}): Promise<JenkinsInfo>;
// (undocumented)
static readonly NEW_JENKINS_ANNOTATION = "jenkins.io/job-slug";
static readonly NEW_JENKINS_ANNOTATION = "jenkins.io/job-full-name";
// (undocumented)
static readonly OLD_JENKINS_ANNOTATION = "jenkins.io/github-folder";
}
@@ -38,7 +38,7 @@ export interface JenkinsInfo {
// (undocumented)
headers?: Record<string, string | string[]>;
// (undocumented)
jobName: string;
jobFullName: string;
}
// @public (undocumented)
@@ -46,7 +46,7 @@ export interface JenkinsInfoProvider {
// (undocumented)
getInstance(options: {
entityRef: EntityName;
jobName?: string;
jobFullName?: string;
}): Promise<JenkinsInfo>;
}
@@ -31,12 +31,12 @@ const mockedJenkinsClient = {
const mockedJenkins = jenkins as jest.Mocked<any>;
mockedJenkins.mockReturnValue(mockedJenkinsClient);
const jobName = 'example-jobName/foo';
const jobFullName = 'example-jobName/foo';
const buildNumber = 19;
const jenkinsInfo: JenkinsInfo = {
baseUrl: 'https://jenkins.example.com',
headers: { headerName: 'headerValue' },
jobName: 'example-jobName',
jobFullName: 'example-jobName',
};
describe('JenkinsApi', () => {
@@ -74,7 +74,7 @@ describe('JenkinsApi', () => {
promisify: true,
});
expect(mockedJenkinsClient.job.get).toBeCalledWith({
name: jenkinsInfo.jobName,
name: jenkinsInfo.jobFullName,
tree: expect.anything(),
});
expect(result).toHaveLength(1);
@@ -117,7 +117,7 @@ describe('JenkinsApi', () => {
promisify: true,
});
expect(mockedJenkinsClient.job.get).toBeCalledWith({
name: `${jenkinsInfo.jobName}/testBranchName`,
name: `${jenkinsInfo.jobFullName}/testBranchName`,
tree: expect.anything(),
});
expect(result).toHaveLength(1);
@@ -368,7 +368,7 @@ describe('JenkinsApi', () => {
mockedJenkinsClient.job.get.mockResolvedValueOnce(project);
mockedJenkinsClient.build.get.mockResolvedValueOnce(build);
await jenkinsApi.getBuild(jenkinsInfo, jobName, buildNumber);
await jenkinsApi.getBuild(jenkinsInfo, jobFullName, buildNumber);
expect(mockedJenkins).toHaveBeenCalledWith({
baseUrl: jenkinsInfo.baseUrl,
@@ -376,19 +376,22 @@ describe('JenkinsApi', () => {
promisify: true,
});
expect(mockedJenkinsClient.job.get).toBeCalledWith({
name: jobName,
name: jobFullName,
depth: 1,
});
expect(mockedJenkinsClient.build.get).toBeCalledWith(jobName, buildNumber);
expect(mockedJenkinsClient.build.get).toBeCalledWith(
jobFullName,
buildNumber,
);
});
it('buildProject', async () => {
await jenkinsApi.buildProject(jenkinsInfo, jobName);
await jenkinsApi.buildProject(jenkinsInfo, jobFullName);
expect(mockedJenkins).toHaveBeenCalledWith({
baseUrl: jenkinsInfo.baseUrl,
headers: jenkinsInfo.headers,
promisify: true,
});
expect(mockedJenkinsClient.job.build).toBeCalledWith(jobName);
expect(mockedJenkinsClient.job.build).toBeCalledWith(jobFullName);
});
});
@@ -67,18 +67,18 @@ export class JenkinsApiImpl {
if (branch) {
// we have been asked to filter to a single branch.
// Assume jenkinsInfo.jobName is a folder which contains one job per branch.
// Assume jenkinsInfo.jobFullName is a folder which contains one job per branch.
// TODO: extract a strategy interface for this
const job = await client.job.get({
name: `${jenkinsInfo.jobName}/${branch}`,
name: `${jenkinsInfo.jobFullName}/${branch}`,
tree: JenkinsApiImpl.jobTreeSpec.replace(/\s/g, ''),
});
projects.push(this.augmentProject(job));
} else {
// We aren't filtering
// Assume jenkinsInfo.jobName is a folder which contains one job per branch.
// Assume jenkinsInfo.jobFullName is a folder which contains one job per branch.
const folder = await client.job.get({
name: jenkinsInfo.jobName,
name: jenkinsInfo.jobFullName,
// Filter only be the information we need, instead of loading all fields.
// Limit to only show the latest build for each job and only load 50 jobs
// at all.
@@ -107,17 +107,17 @@ export class JenkinsApiImpl {
*/
async getBuild(
jenkinsInfo: JenkinsInfo,
jobName: string,
jobFullName: string,
buildNumber: number,
) {
const client = await JenkinsApiImpl.getClient(jenkinsInfo);
const project = await client.job.get({
name: jobName,
name: jobFullName,
depth: 1,
});
const build = await client.build.get(jobName, buildNumber);
const build = await client.build.get(jobFullName, buildNumber);
const jobScmInfo = JenkinsApiImpl.extractScmDetailsFromJob(project);
return this.augmentBuild(build, jobScmInfo);
@@ -127,7 +127,7 @@ export class JenkinsApiImpl {
* Trigger a build of a project
* @see ../../../jenkins/src/api/JenkinsApi.ts#retry
*/
async buildProject(jenkinsInfo: JenkinsInfo, jobName: string) {
async buildProject(jenkinsInfo: JenkinsInfo, jobFullName: string) {
const client = await JenkinsApiImpl.getClient(jenkinsInfo);
// looks like the current SDK only supports triggering a new build
@@ -135,7 +135,7 @@ export class JenkinsApiImpl {
// Note Jenkins itself has concepts of rebuild and replay on a job.
// The latter should be possible to trigger with a POST to /replay/rebuild
await client.job.build(jobName);
await client.job.build(jobFullName);
}
// private helper methods
@@ -61,7 +61,7 @@ describe('DefaultJenkinsInfoProvider', () => {
{
metadata: {
annotations: {
'jenkins.io/job-slug': 'teamA/artistLookup-build',
'jenkins.io/job-full-name': 'teamA/artistLookup-build',
},
},
},
@@ -75,7 +75,7 @@ describe('DefaultJenkinsInfoProvider', () => {
Authorization:
'Basic YmFja3N0YWdlIC0gYm90OjEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNlZGYwMTI=',
},
jobName: 'teamA/artistLookup-build',
jobFullName: 'teamA/artistLookup-build',
});
});
@@ -96,7 +96,7 @@ describe('DefaultJenkinsInfoProvider', () => {
{
metadata: {
annotations: {
'jenkins.io/job-slug': 'teamA/artistLookup-build',
'jenkins.io/job-full-name': 'teamA/artistLookup-build',
},
},
},
@@ -106,7 +106,7 @@ describe('DefaultJenkinsInfoProvider', () => {
expect(mockCatalog.getEntityByName).toBeCalledWith(entityRef);
expect(info).toMatchObject({
baseUrl: 'https://jenkins.example.com',
jobName: 'teamA/artistLookup-build',
jobFullName: 'teamA/artistLookup-build',
});
});
@@ -133,7 +133,7 @@ describe('DefaultJenkinsInfoProvider', () => {
{
metadata: {
annotations: {
'jenkins.io/job-slug': 'teamA/artistLookup-build',
'jenkins.io/job-full-name': 'teamA/artistLookup-build',
},
},
},
@@ -143,7 +143,7 @@ describe('DefaultJenkinsInfoProvider', () => {
expect(mockCatalog.getEntityByName).toBeCalledWith(entityRef);
expect(info).toMatchObject({
baseUrl: 'https://jenkins.example.com',
jobName: 'teamA/artistLookup-build',
jobFullName: 'teamA/artistLookup-build',
});
});
@@ -170,7 +170,7 @@ describe('DefaultJenkinsInfoProvider', () => {
{
metadata: {
annotations: {
'jenkins.io/job-slug': 'other:teamA/artistLookup-build',
'jenkins.io/job-full-name': 'other:teamA/artistLookup-build',
},
},
},
@@ -180,7 +180,7 @@ describe('DefaultJenkinsInfoProvider', () => {
expect(mockCatalog.getEntityByName).toBeCalledWith(entityRef);
expect(info).toMatchObject({
baseUrl: 'https://jenkins-other.example.com',
jobName: 'teamA/artistLookup-build',
jobFullName: 'teamA/artistLookup-build',
});
});
@@ -196,7 +196,7 @@ describe('DefaultJenkinsInfoProvider', () => {
{
metadata: {
annotations: {
'jenkins.io/job-slug': 'default:teamA/artistLookup-build',
'jenkins.io/job-full-name': 'default:teamA/artistLookup-build',
},
},
},
@@ -206,7 +206,7 @@ describe('DefaultJenkinsInfoProvider', () => {
expect(mockCatalog.getEntityByName).toBeCalledWith(entityRef);
expect(info).toMatchObject({
baseUrl: 'https://jenkins.example.com',
jobName: 'teamA/artistLookup-build',
jobFullName: 'teamA/artistLookup-build',
});
});
@@ -232,7 +232,7 @@ describe('DefaultJenkinsInfoProvider', () => {
expect(mockCatalog.getEntityByName).toBeCalledWith(entityRef);
expect(info).toMatchObject({
baseUrl: 'https://jenkins.example.com',
jobName: 'teamA/artistLookup-build',
jobFullName: 'teamA/artistLookup-build',
});
});
@@ -253,7 +253,7 @@ describe('DefaultJenkinsInfoProvider', () => {
{
metadata: {
annotations: {
'jenkins.io/job-slug': 'other:teamA/artistLookup-build',
'jenkins.io/job-full-name': 'other:teamA/artistLookup-build',
},
},
},
@@ -263,7 +263,7 @@ describe('DefaultJenkinsInfoProvider', () => {
expect(mockCatalog.getEntityByName).toBeCalledWith(entityRef);
expect(info).toMatchObject({
baseUrl: 'https://jenkins-other.example.com',
jobName: 'teamA/artistLookup-build',
jobFullName: 'teamA/artistLookup-build',
});
});
});
@@ -30,14 +30,14 @@ export interface JenkinsInfoProvider {
/**
* A specific job to get. This is only passed in when we know about a job name we are interested in.
*/
jobName?: string;
jobFullName?: string;
}): Promise<JenkinsInfo>;
}
export interface JenkinsInfo {
baseUrl: string;
headers?: Record<string, string | string[]>;
jobName: string; // TODO: make this an array
jobFullName: string; // TODO: make this an array
}
/**
@@ -47,7 +47,7 @@ export interface JenkinsInfo {
*/
export class DefaultJenkinsInfoProvider implements JenkinsInfoProvider {
static readonly OLD_JENKINS_ANNOTATION = 'jenkins.io/github-folder';
static readonly NEW_JENKINS_ANNOTATION = 'jenkins.io/job-slug';
static readonly NEW_JENKINS_ANNOTATION = 'jenkins.io/job-full-name';
private constructor(
private readonly config: {
@@ -71,7 +71,7 @@ export class DefaultJenkinsInfoProvider implements JenkinsInfoProvider {
async getInstance(opt: {
entityRef: EntityName;
jobName?: string;
jobFullName?: string;
}): Promise<JenkinsInfo> {
// load entity
const entity = await this.catalog.getEntityByName(opt.entityRef);
@@ -81,7 +81,7 @@ export class DefaultJenkinsInfoProvider implements JenkinsInfoProvider {
);
}
// lookup `[jenkinsName#]jobName` from entity annotation
// lookup `[jenkinsName#]jobFullName` from entity annotation
const jenkinsAndJobName = DefaultJenkinsInfoProvider.getEntityAnnotationValue(
entity,
);
@@ -93,16 +93,16 @@ export class DefaultJenkinsInfoProvider implements JenkinsInfoProvider {
);
}
let jobName;
let jobFullName;
let jenkinsName: string | undefined;
const splitIndex = jenkinsAndJobName.indexOf(':');
if (splitIndex === -1) {
// no jenkinsName specified, use default
jobName = jenkinsAndJobName;
jobFullName = jenkinsAndJobName;
} else {
// There is a jenkinsName specified
jenkinsName = jenkinsAndJobName.substring(0, splitIndex);
jobName = jenkinsAndJobName.substring(
jobFullName = jenkinsAndJobName.substring(
splitIndex + 1,
jenkinsAndJobName.length,
);
@@ -124,7 +124,7 @@ export class DefaultJenkinsInfoProvider implements JenkinsInfoProvider {
headers: {
Authorization: `Basic ${creds}`,
},
jobName,
jobFullName,
};
}
+14 -8
View File
@@ -73,9 +73,15 @@ export async function createRouter(
);
router.get(
'/v1/entity/:namespace/:kind/:name/job/:jobName/:buildNumber',
'/v1/entity/:namespace/:kind/:name/job/:jobFullName/:buildNumber',
async (request, response) => {
const { namespace, kind, name, jobName, buildNumber } = request.params;
const {
namespace,
kind,
name,
jobFullName,
buildNumber,
} = request.params;
const jenkinsInfo = await jenkinsInfoProvider.getInstance({
entityRef: {
@@ -83,12 +89,12 @@ export async function createRouter(
namespace,
name,
},
jobName,
jobFullName,
});
const build = await jenkinsApi.getBuild(
jenkinsInfo,
jobName,
jobFullName,
parseInt(buildNumber, 10),
);
@@ -99,9 +105,9 @@ export async function createRouter(
);
router.post(
'/v1/entity/:namespace/:kind/:name/job/:jobName/:buildNumber::rebuild',
'/v1/entity/:namespace/:kind/:name/job/:jobFullName/:buildNumber::rebuild',
async (request, response) => {
const { namespace, kind, name, jobName } = request.params;
const { namespace, kind, name, jobFullName } = request.params;
const jenkinsInfo = await jenkinsInfoProvider.getInstance({
entityRef: {
@@ -109,10 +115,10 @@ export async function createRouter(
namespace,
name,
},
jobName,
jobFullName,
});
await jenkinsApi.buildProject(jenkinsInfo, jobName);
await jenkinsApi.buildProject(jenkinsInfo, jobFullName);
response.json({});
},
);
@@ -36,7 +36,7 @@ export async function startStandaloneServer(
logger,
jenkinsInfoProvider: {
async getInstance(_: { entityRef: EntityRef }): Promise<JenkinsInfo> {
return { baseUrl: 'https://example.com/', jobName: 'build-foo' };
return { baseUrl: 'https://example.com/', jobFullName: 'build-foo' };
},
},
});
+11 -11
View File
@@ -91,20 +91,20 @@ export interface JenkinsApi {
*
* This takes an entity to support selecting between multiple jenkins instances.
*
* TODO: abstract jobName (so we could support differentiating between the same named job on multiple instances).
* TODO: abstract jobFullName (so we could support differentiating between the same named job on multiple instances).
* @param options.entity
* @param options.jobName
* @param options.jobFullName
* @param options.buildNumber
*/
getBuild(options: {
entity: EntityName;
jobName: string;
jobFullName: string;
buildNumber: string;
}): Promise<Build>;
retry(options: {
entity: EntityName;
jobName: string;
jobFullName: string;
buildNumber: string;
}): Promise<void>;
}
@@ -154,7 +154,7 @@ export class JenkinsClient implements JenkinsApi {
onRestartClick: async () => {
await this.retry({
entity,
jobName: p.fullName,
jobFullName: p.fullName,
buildNumber: String(p.lastBuild.number),
});
},
@@ -164,11 +164,11 @@ export class JenkinsClient implements JenkinsApi {
async getBuild({
entity,
jobName,
jobFullName,
buildNumber,
}: {
entity: EntityName;
jobName: string;
jobFullName: string;
buildNumber: string;
}): Promise<Build> {
const url = `${await this.discoveryApi.getBaseUrl(
@@ -176,7 +176,7 @@ export class JenkinsClient implements JenkinsApi {
)}/v1/entity/${encodeURIComponent(entity.namespace)}/${encodeURIComponent(
entity.kind,
)}/${encodeURIComponent(entity.name)}/job/${encodeURIComponent(
jobName,
jobFullName,
)}/${encodeURIComponent(buildNumber)}`;
const idToken = await this.identityApi.getIdToken();
@@ -192,11 +192,11 @@ export class JenkinsClient implements JenkinsApi {
async retry({
entity,
jobName,
jobFullName,
buildNumber,
}: {
entity: EntityName;
jobName: string;
jobFullName: string;
buildNumber: string;
}): Promise<void> {
const url = `${await this.discoveryApi.getBaseUrl(
@@ -204,7 +204,7 @@ export class JenkinsClient implements JenkinsApi {
)}/v1/entity/${encodeURIComponent(entity.namespace)}/${encodeURIComponent(
entity.kind,
)}/${encodeURIComponent(entity.name)}/job/${encodeURIComponent(
jobName,
jobFullName,
)}/${encodeURIComponent(buildNumber)}:rebuild`;
const idToken = await this.identityApi.getIdToken();
@@ -49,10 +49,10 @@ const useStyles = makeStyles(theme => ({
const BuildWithStepsView = () => {
// TODO: Add a test that react-router decodes this (even though `generatePath` doesn't encode it for you!)
const { jobName, buildNumber } = useRouteRefParams(buildRouteRef);
const { jobFullName, buildNumber } = useRouteRefParams(buildRouteRef);
const classes = useStyles();
const [{ value }] = useBuildWithSteps({ jobName, buildNumber });
const [{ value }] = useBuildWithSteps({ jobFullName, buildNumber });
return (
<div className={classes.root}>
@@ -100,7 +100,7 @@ const generatedColumns: TableColumn[] = [
<Link
component={RouterLink}
to={generatePath(buildRouteRef.path, {
jobName: encodeURIComponent(row.fullName),
jobFullName: encodeURIComponent(row.fullName),
buildNumber: String(row.lastBuild?.number),
})}
>
@@ -25,14 +25,14 @@ const INTERVAL_AMOUNT = 1500;
/**
* Hook to expose a specific build.
* @param jobName the full name of the project (job with builds, not a folder). e.g. "department-A/team-1/project-foo/master"
* @param jobFullName the full name of the project (job with builds, not a folder). e.g. "department-A/team-1/project-foo/master"
* @param buildNumber the number of the build. e.g. "13"
*/
export function useBuildWithSteps({
jobName,
jobFullName,
buildNumber,
}: {
jobName: string;
jobFullName: string;
buildNumber: string;
}) {
const api = useApi(jenkinsApiRef);
@@ -42,12 +42,12 @@ export function useBuildWithSteps({
const getBuildWithSteps = useCallback(async () => {
try {
const entityName = await getEntityName(entity);
return api.getBuild({ entity: entityName, jobName, buildNumber });
return api.getBuild({ entity: entityName, jobFullName, buildNumber });
} catch (e) {
errorApi.post(e);
return Promise.reject(e);
}
}, [buildNumber, jobName, entity, api, errorApi]);
}, [buildNumber, jobFullName, entity, api, errorApi]);
const { loading, value, retry } = useAsyncRetry(() => getBuildWithSteps(), [
getBuildWithSteps,
+2 -2
View File
@@ -45,9 +45,9 @@ export function useBuilds({ branch }: { branch?: string } = {}) {
errorType: ErrorType;
}>();
const restartBuild = async (jobName: string, buildNumber: string) => {
const restartBuild = async (jobFullName: string, buildNumber: string) => {
try {
await api.retry({ entity: entityName, jobName, buildNumber });
await api.retry({ entity: entityName, jobFullName, buildNumber });
} catch (e) {
errorApi.post(e);
}
+2 -2
View File
@@ -31,8 +31,8 @@ export const rootRouteRef = createRouteRef({
});
export const buildRouteRef = createRouteRef({
path: 'build/:jobName/:buildNumber',
params: ['jobName', 'buildNumber'],
path: 'build/:jobFullName/:buildNumber',
params: ['jobFullName', 'buildNumber'],
title: 'Jenkins build',
});