Skip to content

Commit b9ac0df

Browse files
Bitbucket runner support (#798)
* Bitbucket runner support * docker * remove docker.sh * remove redundant * DUMMY * restore register * fix test * revert package-lock * revert correctly package-lock * gentle poll * warn workdir and verbose docker command * single * Add `bitbucket` to `cml runner --driver` values Co-authored-by: Helio Machado <[email protected]>
1 parent cbc4ba6 commit b9ac0df

File tree

8 files changed

+160
-37
lines changed

8 files changed

+160
-37
lines changed

bin/cml/runner.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,10 @@ const runLocal = async (opts) => {
229229
RUNNER_JOBS_RUNNING = RUNNER_JOBS_RUNNING.filter(
230230
(job) => job.id !== jobId
231231
);
232+
233+
if (single && cml.driver === 'bitbucket') {
234+
await shutdown({ ...opts, reason: 'single job' });
235+
}
232236
}
233237
};
234238

@@ -396,7 +400,10 @@ const run = async (opts) => {
396400
try {
397401
winston.info(`Preparing workdir ${workdir}...`);
398402
await fs.mkdir(workdir, { recursive: true });
399-
} catch (err) {}
403+
await fs.chmod(workdir, '766');
404+
} catch (err) {
405+
winston.warn(err.message);
406+
}
400407

401408
if (cloud) await runCloud(opts);
402409
else await runLocal(opts);
@@ -470,7 +477,7 @@ exports.builder = (yargs) =>
470477
},
471478
driver: {
472479
type: 'string',
473-
choices: ['github', 'gitlab'],
480+
choices: ['github', 'gitlab', 'bitbucket'],
474481
description:
475482
'Platform where the repository is hosted. If not specified, it will be inferred from the environment'
476483
},

bin/cml/runner.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ describe('CML e2e', () => {
8787
--driver Platform where the repository is
8888
hosted. If not specified, it will be
8989
inferred from the environment
90-
[string] [choices: \\"github\\", \\"gitlab\\"]
90+
[string] [choices: \\"github\\", \\"gitlab\\", \\"bitbucket\\"]
9191
--repo Repository to be used for
9292
registering the runner. If not
9393
specified, it will be inferred from

src/cml.js

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,6 @@ class CML {
281281
log.job = id;
282282
log.status = 'job_ended';
283283
log.success = data.includes('Succeeded');
284-
log.level = log.success ? 'info' : 'error';
285284
} else if (data.includes('Listening for Jobs')) {
286285
log.status = 'ready';
287286
}
@@ -299,12 +298,30 @@ class CML {
299298
} else if (duration) {
300299
log.status = 'job_ended';
301300
log.success = msg.includes('Job succeeded');
302-
log.level = log.success ? 'info' : 'error';
303301
} else if (msg.includes('Starting runner for')) {
304302
log.status = 'ready';
305303
}
306304
return log;
307305
}
306+
307+
if (this.driver === BB) {
308+
const id = 'bb';
309+
if (data.includes('Getting step StepId{accountUuid={')) {
310+
log.job = id;
311+
log.status = 'job_started';
312+
} else if (
313+
data.includes('Completing step with result Result{status=')
314+
) {
315+
log.job = id;
316+
log.status = 'job_ended';
317+
log.success = data.includes('status=PASSED');
318+
} else if (data.includes('Updating runner status to "ONLINE"')) {
319+
log.status = 'ready';
320+
}
321+
322+
log.level = log.success ? 'info' : 'error';
323+
return log.status ? log : null;
324+
}
308325
} catch (err) {
309326
winston.warn(`Failed parsing log: ${err.message}`);
310327
winston.warn(

src/drivers/bitbucket_cloud.js

Lines changed: 111 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ const crypto = require('crypto');
22
const fetch = require('node-fetch');
33
const winston = require('winston');
44
const { URL } = require('url');
5+
const { spawn } = require('child_process');
56
const FormData = require('form-data');
67
const ProxyAgent = require('proxy-agent');
78

8-
const { fetchUploadData } = require('../utils');
9+
const { fetchUploadData, exec, gpuPresent, sleep } = require('../utils');
910

1011
const { BITBUCKET_COMMIT, BITBUCKET_BRANCH, BITBUCKET_PIPELINE_UUID } =
1112
process.env;
@@ -118,19 +119,112 @@ class BitbucketCloud {
118119
}
119120

120121
async runnerToken() {
121-
throw new Error('Bitbucket Cloud does not support runnerToken!');
122+
return 'DUMMY';
123+
}
124+
125+
async startRunner(opts) {
126+
const { projectPath } = this;
127+
const { workdir, name, labels } = opts;
128+
129+
winston.warn(
130+
`Bitbucket runner is working under /tmp folder and not under ${workdir} as expected`
131+
);
132+
133+
try {
134+
const { uuid: accountId } = await this.request({ endpoint: `/user` });
135+
const { uuid: repoId } = await this.request({
136+
endpoint: `/repositories/${projectPath}`
137+
});
138+
const {
139+
uuid,
140+
oauth_client: { id, secret }
141+
} = await this.registerRunner({ name, labels });
142+
143+
const gpu = await gpuPresent();
144+
const command = `docker container run -t -a stderr -a stdout --rm \
145+
-v /var/run/docker.sock:/var/run/docker.sock \
146+
-v /var/lib/docker/containers:/var/lib/docker/containers:ro \
147+
-v /tmp:/tmp \
148+
-e ACCOUNT_UUID=${accountId} \
149+
-e REPOSITORY_UUID=${repoId} \
150+
-e RUNNER_UUID=${uuid} \
151+
-e OAUTH_CLIENT_ID=${id} \
152+
-e OAUTH_CLIENT_SECRET=${secret} \
153+
-e WORKING_DIRECTORY=/tmp \
154+
--name ${name} \
155+
${gpu ? '--runtime=nvidia -e NVIDIA_VISIBLE_DEVICES=all' : ''} \
156+
docker-public.packages.atlassian.com/sox/atlassian/bitbucket-pipelines-runner:1`;
157+
158+
return spawn(command, { shell: true });
159+
} catch (err) {
160+
throw new Error(`Failed preparing runner: ${err.message}`);
161+
}
122162
}
123163

124164
async registerRunner(opts = {}) {
125-
throw new Error('Bitbucket Cloud does not support registerRunner!');
165+
const { projectPath } = this;
166+
const { name, labels } = opts;
167+
168+
const endpoint = `/repositories/${projectPath}/pipelines-config/runners`;
169+
170+
const request = await this.request({
171+
api: 'https://api.bitbucket.org/internal',
172+
endpoint,
173+
method: 'POST',
174+
body: JSON.stringify({
175+
labels: ['self.hosted'].concat(labels.split(',')),
176+
name
177+
})
178+
});
179+
180+
let registered = false;
181+
while (!registered) {
182+
await sleep(1);
183+
const runner = (await this.runners()).find(
184+
(runner) => runner.name === name
185+
);
186+
if (runner) registered = true;
187+
}
188+
189+
return request;
126190
}
127191

128192
async unregisterRunner(opts = {}) {
129-
throw new Error('Bitbucket Cloud does not support unregisterRunner!');
193+
const { projectPath } = this;
194+
const { runnerId, name } = opts;
195+
const endpoint = `/repositories/${projectPath}/pipelines-config/runners/${runnerId}`;
196+
197+
try {
198+
await this.request({
199+
api: 'https://api.bitbucket.org/internal',
200+
endpoint,
201+
method: 'DELETE'
202+
});
203+
} catch (err) {
204+
if (!err.message.includes('invalid json response body')) {
205+
throw err;
206+
}
207+
} finally {
208+
await exec(`docker stop ${name}`);
209+
}
130210
}
131211

132212
async runners(opts = {}) {
133-
throw new Error('Bitbucket Cloud does not support runners!');
213+
const { projectPath } = this;
214+
215+
const endpoint = `/repositories/${projectPath}/pipelines-config/runners`;
216+
const runners = await this.paginatedRequest({
217+
api: 'https://api.bitbucket.org/internal',
218+
endpoint
219+
});
220+
221+
return runners.map(({ uuid: id, name, labels, state: { status } }) => ({
222+
id,
223+
name,
224+
labels,
225+
online: status === 'ONLINE',
226+
busy: status === 'ONLINE'
227+
}));
134228
}
135229

136230
async runnerById(opts = {}) {
@@ -269,6 +363,16 @@ class BitbucketCloud {
269363
}
270364
}
271365

366+
async pipelineRestart(opts = {}) {
367+
winston.warn('BitBucket Cloud does not support workflowRestart yet!');
368+
}
369+
370+
async pipelineJobs(opts = {}) {
371+
winston.warn('BitBucket Cloud does not support pipelineJobs yet!');
372+
373+
return [];
374+
}
375+
272376
async pipelineRerun(opts = {}) {
273377
const { projectPath } = this;
274378
const { id = BITBUCKET_PIPELINE_UUID } = opts;
@@ -290,14 +394,6 @@ class BitbucketCloud {
290394
});
291395
}
292396

293-
async pipelineRestart(opts = {}) {
294-
throw new Error('BitBucket Cloud does not support workflowRestart!');
295-
}
296-
297-
async pipelineJobs(opts = {}) {
298-
throw new Error('Not implemented');
299-
}
300-
301397
async updateGitConfig({ userName, userEmail } = {}) {
302398
const [user, password] = Buffer.from(this.token, 'base64')
303399
.toString('utf-8')
@@ -351,8 +447,8 @@ class BitbucketCloud {
351447
}
352448

353449
async request(opts = {}) {
354-
const { token, api } = this;
355-
const { url, endpoint, method = 'GET', body } = opts;
450+
const { token } = this;
451+
const { url, endpoint, method = 'GET', body, api = this.api } = opts;
356452

357453
if (!(url || endpoint))
358454
throw new Error('Bitbucket Cloud API endpoint not found');
@@ -362,8 +458,6 @@ class BitbucketCloud {
362458
headers['Content-Type'] = 'application/json';
363459

364460
const requestUrl = url || `${api}${endpoint}`;
365-
winston.debug(`${method} ${requestUrl}`);
366-
367461
const response = await fetch(requestUrl, {
368462
method,
369463
headers,

src/drivers/bitbucket_cloud.test.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,8 @@ describe('Non Enviromental tests', () => {
3535
});
3636

3737
test('Runner token', async () => {
38-
await expect(client.runnerToken()).rejects.toThrow(
39-
'Bitbucket Cloud does not support runnerToken!'
40-
);
38+
const token = await client.runnerToken();
39+
await expect(token).toBe('DUMMY');
4140
});
4241

4342
test('updateGitConfig', async () => {

src/drivers/github.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,6 @@ class Github {
263263
)}.tar.gz`;
264264
await download({ url, path: destination });
265265
await tar.extract({ file: destination, cwd: workdir });
266-
await exec(`chmod -R 777 ${workdir}`);
267266
}
268267

269268
await exec(

src/drivers/gitlab.js

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const ProxyAgent = require('proxy-agent');
99
const { backOff } = require('exponential-backoff');
1010
const winston = require('winston');
1111

12-
const { fetchUploadData, download, exec } = require('../utils');
12+
const { fetchUploadData, download, gpuPresent } = require('../utils');
1313

1414
const { IN_DOCKER, CI_PIPELINE_ID } = process.env;
1515
const API_VER = 'v4';
@@ -173,16 +173,7 @@ class Gitlab {
173173
dockerVolumes = []
174174
} = opts;
175175

176-
let gpu = true;
177-
try {
178-
await exec('nvidia-smi');
179-
} catch (err) {
180-
try {
181-
await exec('cuda-smi');
182-
} catch (err) {
183-
gpu = false;
184-
}
185-
}
176+
const gpu = await gpuPresent();
186177

187178
try {
188179
const bin = resolve(workdir, 'gitlab-runner');

src/utils.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,21 @@ const sshConnection = async (opts) => {
164164
return ssh;
165165
};
166166

167+
const gpuPresent = async () => {
168+
let gpu = true;
169+
try {
170+
await exec('nvidia-smi');
171+
} catch (err) {
172+
try {
173+
await exec('cuda-smi');
174+
} catch (err) {
175+
gpu = false;
176+
}
177+
}
178+
179+
return gpu;
180+
};
181+
167182
exports.exec = exec;
168183
exports.fetchUploadData = fetchUploadData;
169184
exports.upload = upload;
@@ -173,3 +188,4 @@ exports.isProcRunning = isProcRunning;
173188
exports.watermarkUri = watermarkUri;
174189
exports.download = download;
175190
exports.sshConnection = sshConnection;
191+
exports.gpuPresent = gpuPresent;

0 commit comments

Comments
 (0)