abstruse
Version:
Abstruse CI
772 lines (722 loc) • 24.8 kB
text/typescript
import { startBuildProcess } from './process';
import { Observable, Subject, BehaviorSubject, Subscription, from, timer } from 'rxjs';
import { filter, mergeMap, share, map } from 'rxjs/operators';
import {
insertBuild,
updateBuild,
getBuild,
getBuildStatus,
getLastRunId,
getDepracatedBuilds,
getLastBuild
} from './db/build';
import { insertBuildRun, updateBuildRun } from './db/build-run';
import * as dbJob from './db/job';
import * as dbJobRuns from './db/job-run';
import { getRepositoryOnly, getRepositoryByBuildId } from './db/repository';
import { getRemoteParsedConfig, JobsAndEnv, CommandType } from './config';
import { killContainer } from './docker';
import { logger, LogMessageType } from './logger';
import { getHttpJsonResponse, getBitBucketAccessToken } from './utils';
import { getConfig } from './setup';
import { sendFailureStatus, sendPendingStatus, sendSuccessStatus } from './commit-status';
import { decrypt } from './security';
import * as envVars from './env-variables';
export interface BuildMessage {
type: string;
jobMessage: JobMessage;
}
export interface JobMessage {
build_id?: number;
job_id?: number;
image_name?: string;
type: string;
data: string | number;
}
export interface JobProcess {
build_id?: number;
job_id?: number;
status?: 'queued' | 'running' | 'cancelled' | 'errored' | 'success';
image_name?: string;
log?: string;
requestData: any;
commands?: { command: string, type: CommandType }[];
cache?: string[];
repo_name?: string;
branch?: string;
env?: string[];
job?: Observable<any>;
exposed_ports?: string;
debug?: boolean;
}
export interface JobProcessEvent {
build_id?: number;
job_id?: number;
repository_id?: number;
type?: string;
data?: string;
additionalData?: any;
}
let config: any = getConfig();
export let jobProcesses: Subject<JobProcess> = new Subject();
export let jobEvents: BehaviorSubject<JobProcessEvent> = new BehaviorSubject({});
export let terminalEvents: Subject<JobProcessEvent> = new Subject();
export let buildSub: { [id: number]: Subscription } = {};
export let processes: JobProcess[] = [];
jobEvents
.pipe(
filter(event => !!event.build_id && !!event.job_id),
share()
)
.subscribe(event => {
let msg: LogMessageType = {
message: `[build]: build: ${event.build_id} job: ${event.job_id} => ${event.data}`,
type: 'info',
notify: false
};
logger.next(msg);
});
// main scheduler
let concurrency = config.concurrency || 10;
jobProcesses
.pipe(
mergeMap(process => execJob(process), concurrency)
)
.subscribe();
function execJob(proc: JobProcess): Observable<any> {
let index = processes.findIndex(process => process.job_id === proc.job_id);
if (index !== -1) {
processes[index] = proc;
} else {
processes.push(proc);
}
let buildProcesses = processes.filter(p => p.build_id === proc.build_id);
let testProcesses = buildProcesses.filter(process => {
return process.env.findIndex(e => e === 'DEPLOY') === -1;
});
let queuedOrRunning = testProcesses.filter(p => {
return p.status === 'queued' || p.status === 'running';
});
let succeded = testProcesses.filter(p => p.status === 'success');
let isDeploy = proc.env.findIndex(e => e === 'DEPLOY') !== -1 ? true : false;
if (!isDeploy || succeded.length === testProcesses.length) {
return startJobProcess(proc);
} else if (queuedOrRunning.length) {
// give some time (5s) to check again other processes
return timer(5000).pipe(map(() => jobProcesses.next(proc)));
} else {
return from(stopJob(proc.job_id));
}
}
export function startJobProcess(proc: JobProcess): Observable<{}> {
return new Observable(observer => {
getRepositoryByBuildId(proc.build_id)
.then(repository => {
let envs = envVars.generate(proc);
let secureVarirables = false;
repository.variables.forEach(v => {
if (!!v.encrypted) {
secureVarirables = true;
envVars.set(envs, v.name, decrypt(v.value), true);
} else {
envVars.set(envs, v.name, v.value);
}
});
envVars.set(envs, 'ABSTRUSE_SECURE_ENV_VARS', secureVarirables);
let jobTimeout = config.jobTimeout ? config.jobTimeout * 1000 : 3600000;
let idleTimeout = config.idleTimeout ? config.idleTimeout * 1000 : 3600000;
buildSub[proc.job_id] =
startBuildProcess(proc, envs, jobTimeout, idleTimeout)
.subscribe(event => {
let msg: JobProcessEvent = {
build_id: proc.build_id,
job_id: proc.job_id,
type: event.type,
data: event.data
};
terminalEvents.next(msg);
if (event.data && event.type === 'data') {
proc.log += event.data;
} else if (event.data && event.type === 'exposed ports') {
proc.exposed_ports = event.data;
} else if (event.type === 'container') {
let ev: JobProcessEvent = {
type: 'process',
build_id: proc.build_id,
job_id: proc.job_id,
data: event.data
};
jobEvents.next(ev);
} else if (event.type === 'exit') {
proc.log += event.data;
observer.complete();
}
}, err => {
let msg: LogMessageType = {
message: typeof err === 'object' ? `[error]: ${JSON.stringify(err)}` : `[error]: ${err}`, type: 'error', notify: false
};
jobFailed(proc, msg)
.then(() => observer.complete());
}, () => {
jobSucceded(proc)
.then(() => observer.complete());
});
})
.then(() => dbJob.getLastRunId(proc.job_id))
.then(runId => {
let time = new Date();
let data = { id: runId, start_time: time, end_time: null, status: 'running', log: '' };
return dbJobRuns.updateJobRun(data)
.then(() => {
let jobRunData = {
type: 'process',
build_id: proc.build_id,
job_id: proc.job_id,
data: 'job started',
additionalData: time.getTime()
};
jobEvents.next(jobRunData);
});
})
.catch(err => {
let msg: LogMessageType = {
message: typeof err === 'object' ? `[error]: ${JSON.stringify(err)}` : `[error]: ${err}`, type: 'error', notify: false
};
logger.next(msg);
});
});
}
export function restartJob(jobId: number): Promise<void> {
let time = new Date();
let job = null;
let process = processes.find(p => p.job_id === Number(jobId));
if (process && process.debug) {
process.debug = false;
jobEvents.next({
type: 'process',
build_id: process.build_id,
job_id: jobId,
data: 'job failed',
additionalData: time.getTime()
});
jobEvents.next({
type: 'debug',
build_id: process.build_id,
job_id: jobId,
data: 'false'
});
}
return stopJob(jobId)
.then(() => dbJob.getLastRun(jobId))
.then(lastRun => dbJobRuns.insertJobRun({
start_time: time,
end_time: null,
status: 'queued',
log: '',
build_run_id: lastRun.build_run_id,
job_id: jobId
}))
.then(() => queueJob(jobId))
.then(() => dbJob.getJob(jobId))
.then(j => job = j)
.then(() => {
jobEvents.next({
type: 'process',
build_id: job.builds_id,
job_id: job.id,
data: 'job restarted',
additionalData: time.getTime()
});
})
.then(() => getBuild(job.builds_id))
.then(build => sendPendingStatus(build, build.id))
.catch(err => {
let msg: LogMessageType = {
message: typeof err === 'object' ? `[error]: ${JSON.stringify(err)}` : `[error]: ${err}`, type: 'error', notify: false
};
logger.next(msg);
});
}
export function stopJob(jobId: number): Promise<void> {
let time = new Date();
let process = processes.find(p => p.job_id === Number(jobId));
if (process && process.debug) {
process.debug = false;
jobEvents.next({
type: 'process',
build_id: process.build_id,
job_id: jobId,
data: 'job failed',
additionalData: time.getTime()
});
jobEvents.next({
type: 'debug',
build_id: process.build_id,
job_id: jobId,
data: 'false'
});
}
return Promise.resolve()
.then(() => dbJob.getJob(jobId))
.then(job => {
return killContainer(`abstruse_${job.builds_id}_${jobId}`)
.then(() => getBuildStatus(job.builds_id))
.then(status => {
if (status === 'success') {
return getBuild(job.builds_id)
.then(build => {
return updateBuild({ id: build.id, end_time: time })
.then(() => getLastRunId(build.id))
.then(id => updateBuildRun({ id: id, end_time: time }))
.then(() => sendSuccessStatus(build, build.id))
.then(() => {
jobEvents.next({
type: 'process',
build_id: build.id,
data: 'build succeeded',
additionalData: time.getTime()
});
})
.catch(err => {
let msg: LogMessageType = {
message: typeof err === 'object' ? `[error]: ${JSON.stringify(err)}` : `[error]: ${err}`,
type: 'error',
notify: false
};
logger.next(msg);
});
}).catch(err => {
let msg: LogMessageType = {
message: typeof err === 'object' ? `[error]: ${JSON.stringify(err)}` : `[error]: ${err}`,
type: 'error',
notify: false
};
logger.next(msg);
});
} else if (status === 'failed') {
return getBuild(job.builds_id)
.then(build => {
return updateBuild({ id: build.id, end_time: time })
.then(() => getLastRunId(build.id))
.then(id => updateBuildRun({ id: id, end_time: time }))
.then(() => sendFailureStatus(build, build.id))
.then(() => {
jobEvents.next({
type: 'process',
build_id: build.id,
data: 'build failed',
additionalData: time.getTime()
});
})
.catch(err => {
let msg: LogMessageType = {
message: typeof err === 'object' ? `[error]: ${JSON.stringify(err)}` : `[error]: ${err}`,
type: 'error',
notify: false
};
logger.next(msg);
});
}).catch(err => {
let msg: LogMessageType = {
message: typeof err === 'object' ? `[error]: ${JSON.stringify(err)}` : `[error]: ${err}`,
type: 'error',
notify: false
};
logger.next(msg);
});
}
}).catch(err => {
let msg: LogMessageType = {
message: typeof err === 'object' ? `[error]: ${JSON.stringify(err)}` : `[error]: ${err}`, type: 'error', notify: false
};
logger.next(msg);
});
})
.then(() => dbJob.getLastRunId(jobId))
.then(runId => dbJobRuns.getRun(runId))
.then(jobRun => {
if (!jobRun.end_time) {
return dbJobRuns.updateJobRun({ id: jobRun.id, end_time: time, status: 'failed' });
}
})
.then(() => dbJob.getJob(jobId))
.then(job => {
let data = {
type: 'process',
build_id: job.builds_id,
job_id: job.id,
data: 'job stopped',
additionalData: time.getTime()
};
jobEvents.next(data);
}).then(() => {
if (buildSub[jobId]) {
buildSub[jobId].unsubscribe();
delete buildSub[jobId];
}
}).catch(err => {
let msg: LogMessageType = {
message: typeof err === 'object' ? `[error]: ${JSON.stringify(err)}` : `[error]: ${err}`, type: 'error', notify: false
};
logger.next(msg);
});
}
export function debugJob(jobId: number, debug: boolean): Promise<void> {
return new Promise((resolve, reject) => {
let time = new Date();
let process = processes.find(p => p.job_id === Number(jobId));
process.debug = debug;
if (debug) {
buildSub[jobId].unsubscribe();
delete buildSub[jobId];
let msg: JobProcessEvent = {
build_id: process.build_id,
job_id: Number(jobId),
type: 'data',
data: `[exectime]: stopped`
};
terminalEvents.next(msg);
process.log += `[exectime]: stopped`;
}
});
}
export function startBuild(data: any, buildConfig?: any): Promise<any> {
let cfg: JobsAndEnv[];
let repoId = data.repositories_id;
let pr = null;
let sha = null;
let branch = null;
let buildData = null;
return getRepositoryOnly(data.repositories_id)
.then(repository => {
let isGithub = repository.github_id ? true : false;
let isBitbucket = repository.bitbucket_id ? true : false;
let isGitlab = repository.gitlab_id ? true : false;
let isGogs = repository.gogs_id ? true : false;
if (isGithub) {
if (data.data.pull_request) {
pr = data.data.pull_request.number;
sha = data.data.pull_request.head.sha;
branch = data.data.pull_request.base.ref;
} else {
sha = data.data.after || data.data.sha;
if (data.data && data.data.ref) {
branch = data.data.ref.split('/').pop();
}
}
} else if (isBitbucket) {
if (data.data.push) {
let push = data.data.push.changes[data.data.push.changes.length - 1];
let commit = push.commits[push.commits.length - 1];
sha = commit.hash;
branch = push.new.type === 'branch' ? push.new.name : 'master';
} else if (data.data.pullrequest) {
pr = data.data.pullrequest.id;
sha = data.data.pullrequest.source.commit.hash;
branch = data.data.pullrequest.source.branch.name;
} else if (data.hash) {
sha = data.data.hash;
}
} else if (isGitlab) {
if (data.data.name) {
sha = data.data.commit.id;
branch = data.data.name;
}
}
branch = branch || repository.default_branch || 'master';
let repo = {
clone_url: repository.clone_url,
branch: branch,
pr: pr,
sha: sha,
access_token: repository.access_token || null,
type: repository.repository_provider
};
if (buildConfig) {
return Promise.resolve(buildConfig);
} else {
return getRemoteParsedConfig(repo);
}
})
.then(parsedConfig => cfg = parsedConfig)
.then(() => data.parsed_config = JSON.stringify(cfg))
.then(() => data = Object.assign(data, { branch: branch, pr: pr }))
.then(() => insertBuild(data))
.then(build => {
data = Object.assign(data, { build_id: build.id });
delete data.repositories_id;
delete data.pr;
delete data.parsed_config;
return insertBuildRun(data);
})
.then(() => getBuild(data.build_id))
.then(bdata => buildData = bdata)
.then(() => sendPendingStatus(buildData, buildData.id))
.then(() => {
return cfg.reduce((prev, c, i) => {
return prev.then(() => {
let dataJob = null;
return dbJob.insertJob({ data: JSON.stringify(c), builds_id: data.build_id })
.then(job => dataJob = job)
.then(() => getLastRunId(data.build_id))
.then(lastRunId => {
const jobRun = {
start_time: new Date,
status: 'queued',
build_run_id: lastRunId,
job_id: dataJob.id
};
return dbJobRuns.insertJobRun(jobRun);
})
.then(() => queueJob(dataJob.id));
});
}, Promise.resolve());
})
.then(lastBuild => {
jobEvents.next({
type: 'process',
build_id: data.build_id,
repository_id: repoId,
data: 'build added',
additionalData: null
});
})
.then(() => getDepracatedBuilds(buildData))
.then(builds => Promise.all(builds.map(build => stopBuild(build))))
.then(() => ({ buildId: buildData.id }))
.catch(err => {
let msg: LogMessageType = {
message: typeof err === 'object' ? `[error]: ${JSON.stringify(err)}` : `[error]: ${err}`, type: 'error', notify: false
};
logger.next(msg);
});
}
export function restartBuild(buildId: number): Promise<any> {
let time = new Date();
let buildData;
let accessToken;
return stopBuild(buildId)
.then(() => getBuild(buildId))
.then(build => buildData = build)
.then(() => accessToken = buildData.repository.access_token || null)
.then(() => {
let jobs = buildData.jobs;
buildData.start_time = time;
buildData.end_time = null;
return updateBuild(buildData)
.then(() => {
buildData.build_id = buildId;
return insertBuildRun(buildData);
})
.then(buildRun => {
return Promise.all(jobs.map(job => {
dbJobRuns.insertJobRun({
start_time: time,
end_time: null,
status: 'queued',
log: '',
build_run_id: buildRun.id,
job_id: job.id
});
}));
})
.then(() => {
return jobs.reduce((prev, curr) => {
return prev.then(() => {
return stopJob(curr.id).then(() => queueJob(curr.id));
});
}, Promise.resolve());
})
.then(() => getBuild(buildId))
.then(build => sendPendingStatus(build, build.id))
.then(() => {
jobEvents.next({
type: 'process',
build_id: buildId,
data: 'build restarted',
additionalData: time.getTime()
});
})
.catch(err => {
let msg: LogMessageType = {
message: typeof err === 'object' ? `[error]: ${JSON.stringify(err)}` : `[error]: ${err}`, type: 'error', notify: false
};
logger.next(msg);
});
}).catch(err => {
let msg: LogMessageType = {
message: typeof err === 'object' ? `[error]: ${JSON.stringify(err)}` : `[error]: ${err}`, type: 'error', notify: false
};
logger.next(msg);
});
}
export function stopBuild(buildId: number): Promise<any> {
return getBuild(buildId)
.then(build => {
return build.jobs.reduce((prev, current) => {
return prev.then(() => stopJob(current.id));
}, Promise.resolve());
})
.catch(err => {
let msg: LogMessageType = {
message: typeof err === 'object' ? `[error]: ${JSON.stringify(err)}` : `[error]: ${err}`, type: 'error', notify: false
};
logger.next(msg);
});
}
function queueJob(jobId: number): Promise<void> {
let job = null;
let requestData = null;
return dbJob.getJob(jobId)
.then(jobData => job = jobData)
.then(() => getBuild(job.builds_id))
.then(build => requestData = { branch: build.branch, pr: build.pr, data: build.data })
.then(() => {
let data = JSON.parse(job.data);
let jobProcess: JobProcess = {
build_id: job.builds_id,
job_id: jobId,
status: 'queued',
requestData: requestData,
commands: data.commands,
cache: data.cache || null,
repo_name: job.build.repository.full_name || null,
branch: job.build.branch || null,
env: data.env,
image_name: data.image,
exposed_ports: null,
log: '',
debug: false
};
jobProcesses.next(jobProcess);
jobEvents.next({
type: 'process',
build_id: job.builds_id,
job_id: job.id,
data: 'job queued'
});
});
}
function jobSucceded(proc: JobProcess): Promise<any> {
return Promise.resolve()
.then(() => {
proc.status = 'success';
let time = new Date();
return dbJob.getLastRunId(proc.job_id)
.then(runId => {
let data = {
id: runId,
end_time: time,
status: 'success',
log: proc.log
};
return dbJobRuns.updateJobRun(data);
})
.then(() => getBuildStatus(proc.build_id))
.then(status => {
if (status === 'success') {
return updateBuild({ id: proc.build_id, end_time: time })
.then(() => getLastRunId(proc.build_id))
.then(id => updateBuildRun({ id: id, end_time: time }))
.then(() => getBuild(proc.build_id))
.then(build => sendSuccessStatus(build, build.id))
.then(() => {
jobEvents.next({
type: 'process',
build_id: proc.build_id,
data: 'build succeeded',
additionalData: time.getTime()
});
});
} else if (status === 'failed') {
return getBuild(proc.build_id)
.then(build => updateBuild({ id: proc.build_id, end_time: time }))
.then(() => getLastRunId(proc.build_id))
.then(id => updateBuildRun({ id: id, end_time: time }))
.then(() => getBuild(proc.build_id))
.then(build => sendFailureStatus(build, build.id))
.then(() => {
jobEvents.next({
type: 'process',
build_id: proc.build_id,
data: 'build failed',
additionalData: time.getTime()
});
});
} else {
return Promise.resolve();
}
})
.then(() => {
jobEvents.next({
type: 'process',
build_id: proc.build_id,
job_id: proc.job_id,
data: 'job succeded',
additionalData: time.getTime()
});
})
.catch(err => {
let msg: LogMessageType = {
message: typeof err === 'object' ? `[error]: ${JSON.stringify(err)}` : `[error]: ${err}`, type: 'error', notify: false
};
logger.next(msg);
jobEvents.next({
type: 'process',
build_id: proc.build_id,
job_id: proc.job_id,
data: 'job failed',
additionalData: time.getTime()
});
return getLastRunId(proc.build_id)
.then(id => updateBuildRun({ id: id, end_time: time }));
});
});
}
function jobFailed(proc: JobProcess, msg?: LogMessageType): Promise<any> {
return Promise.resolve()
.then(() => {
proc.status = 'errored';
let time = new Date();
if (msg) {
logger.next(msg);
}
return dbJob.getLastRunId(proc.job_id)
.then(runId => {
let data = {
id: runId,
end_time: time,
status: 'failed',
log: proc.log
};
return dbJobRuns.updateJobRun(data);
})
.then(build => updateBuild({ id: proc.build_id, end_time: time }))
.then(id => updateBuildRun({ id: id, end_time: time }))
.then(() => getBuild(proc.build_id))
.then(build => sendFailureStatus(build, build.id))
.then(() => {
jobEvents.next({
type: 'process',
build_id: proc.build_id,
data: 'build failed',
additionalData: time.getTime()
});
})
.then(() => {
jobEvents.next({
type: 'process',
build_id: proc.build_id,
job_id: proc.job_id,
data: 'job failed',
additionalData: time.getTime()
});
})
.catch(err => {
let logMessage: LogMessageType = {
message: typeof err === 'object' ? `[error]: ${JSON.stringify(err)}` : `[error]: ${err}`, type: 'error', notify: false
};
logger.next(logMessage);
});
});
}