@codefresh-io/cf-git-providers
Version:
An NPM module/CLI for interacting with various git providers
677 lines • 25.7 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const lodash_1 = require("lodash");
const querystring_1 = __importDefault(require("querystring"));
const types_1 = require("./types");
const https_1 = require("https");
const helpers_1 = require("../helpers");
const url_1 = require("url");
const request_retry_1 = require("../helpers/request-retry");
// eslint-disable-next-line @typescript-eslint/no-var-requires
const CFError = require('cf-errors');
const logger = (0, helpers_1.createNewLogger)('codefresh:infra:git-providers:gitlab');
const MAX_PER_PAGE = 100;
const MAX_CONCURRENT_REQUESTS = 4; // requests X pages each time
const MAX_RESULTS = 10000;
const DEFAULT_GROUP = 'GitLab Instance';
const ApiVersions = {
v4: 'api/v4/',
};
const statesMap = {
pending: 'pending',
running: 'running',
success: 'success',
failure: 'failed',
error: 'failed',
};
const _extractErrorFromResponse = (res) => {
const message = (0, lodash_1.get)(res, 'body.error', '') || _extractMessageFromObject(res.body?.message);
return new types_1.HttpError(res.statusCode, message);
};
const _extractMessageFromObject = (message) => {
if (typeof message !== 'object') {
return message;
}
let finalMessage = '';
Object.keys(message).map((key) => finalMessage += message[key]);
return finalMessage;
};
const _toOrderBy = (orderBy) => {
switch (orderBy) {
case 'name': return 'name';
case 'created': return 'created_at';
case 'pushed': return 'last_activity_at';
default: return 'last_activity_at';
}
};
const _encodePath = (path) => {
return encodeURIComponent(path).replace(/\./g, '%2E');
};
const _encodeProjectPath = (group, project) => {
return _encodePath(`${group}/${project}`);
};
const _toBranch = (rawBranch) => {
return {
name: rawBranch.name,
id: rawBranch.name,
commit: {
sha: rawBranch.commit.id,
commiter_name: rawBranch.commit.author_name,
message: rawBranch.commit.message,
url: rawBranch.commit.web_url,
}
};
};
const _toUser = (user, email) => {
return {
login: user.username,
email: email || user.email,
avatar_url: user.avatar_url,
web_url: user.web_url,
};
};
const _toRepo = (rawRepo) => {
const isGroup = (0, lodash_1.get)(rawRepo, 'namespace.kind') === 'group'; // is it a group/org repository
return {
id: rawRepo.id.toString(),
provider: 'gitlab',
name: rawRepo.name,
full_name: rawRepo.path_with_namespace,
private: rawRepo.visibility === 'private',
pushed_at: rawRepo.last_activity_at,
open_issues: rawRepo.open_issues_count,
clone_url: rawRepo.http_url_to_repo,
ssh_url: rawRepo.ssh_url_to_repo,
owner: {
login: (0, lodash_1.get)(rawRepo, isGroup ? 'namespace.full_path' : 'owner.username', ''),
avatar_url: (0, lodash_1.get)(rawRepo, isGroup ? 'namespace.avatar_url' : 'owner.avatar_url', '') || '',
creator: (0, lodash_1.get)(rawRepo, isGroup ? 'namespace.name' : 'owner.name', null),
},
org: isGroup ? (0, lodash_1.get)(rawRepo, 'namespace.name') : null,
default_branch: rawRepo.default_branch,
permissions: {
admin: (0, lodash_1.get)(rawRepo, `permissions.${isGroup ? 'group_access' : 'project_access'}.access_level`, 0) >= 40,
},
webUrl: rawRepo.web_url || '',
};
};
const _toWebhook = (owner, repo, rawWebhook) => {
return {
id: rawWebhook.id,
name: String(rawWebhook.id),
endpoint: rawWebhook.url,
repo: `${owner}/${repo}`,
events: Object.keys({
...(rawWebhook.push_events && { push_events: true }),
...(rawWebhook.tag_push_events && { tag_push_events: true }),
...(rawWebhook.merge_requests_events && { merge_requests_events: true }),
...(rawWebhook.issues_events && { issues_events: true }),
...(rawWebhook.releases_events && { releases_events: true }),
}),
};
};
class Gitlab {
baseUrl;
authenticationHeader;
timeout;
agent;
auth;
refreshTokenHandler;
retryConfig;
constructor(opts) {
const url = new url_1.URL(opts.apiURL || opts.apiUrl || 'https://gitlab.com/');
if (url.pathname === '/') {
url.pathname = ApiVersions.v4;
}
this.baseUrl = url.href;
if (!this.baseUrl.endsWith('/')) {
this.baseUrl = `${this.baseUrl}/`;
}
this.timeout = opts.timeout || 10000;
this.refreshTokenHandler = opts.refreshTokenHandler;
// works for both oauth2 and private token authentication methods
// eslint-disable-next-line @typescript-eslint/naming-convention
this.authenticationHeader = opts.password ? { Authorization: `Bearer ${opts.password}` } : {};
this.auth = {
username: 'oauth2',
password: opts.password,
refreshToken: opts.refreshToken,
};
if (this.baseUrl.startsWith('https') && (process.env.NODE_TLS_REJECT_UNAUTHORIZED === '0' || opts.insecure)) {
this.agent = new https_1.Agent({ rejectUnauthorized: false });
}
this.retryConfig = opts.retryConfig;
}
async createRepository(opt) {
let namespace_id;
try {
namespace_id = await this.getGroupId({ groupName: opt.owner });
}
catch {
namespace_id = await this.getGroupId({ groupName: DEFAULT_GROUP });
}
const res = await this.performAPICall({
api: `projects`,
json: true,
method: 'POST',
body: {
name: opt.repo,
namespace_id
}
});
if (res.statusCode >= 400) {
throw new CFError({
message: `Failed to get repository: ${JSON.stringify(opt)}`,
cause: _extractErrorFromResponse(res),
});
}
return _toRepo(res.body);
}
async shouldRefreshToken(requestOptions) {
const validateTokenRequestOptions = (0, lodash_1.cloneDeep)(requestOptions);
validateTokenRequestOptions.qs = {};
validateTokenRequestOptions.url = `${this.baseUrl}user`;
const response = await request_retry_1.RpRetry.rpRetry(validateTokenRequestOptions, logger);
switch (true) {
case response.statusCode === 401: return true;
case response.statusCode < 400: return false;
default: throw new Error(`Refresh token error. StatusCode: ${response.statusCode}. Message: ${response.statusMessage}`);
}
}
updateAuth(auth) {
this.auth.password = auth.accessToken;
this.auth.refreshToken = auth.refreshToken;
// eslint-disable-next-line @typescript-eslint/naming-convention
this.authenticationHeader = auth.accessToken ? { Authorization: `Bearer ${auth.accessToken}` } : {};
}
async performAPICall(opts) {
const method = opts.method || 'GET';
const requestHeaders = (0, lodash_1.merge)(opts.noAuth ? {} : this.authenticationHeader, opts.headers || {});
const requestOptions = {
method,
url: `${this.baseUrl}${opts.api}`,
qs: opts.qs || {},
headers: requestHeaders,
json: opts.json,
timeout: this.timeout,
form: opts.data,
resolveWithFullResponse: true,
agent: this.agent,
simple: false,
body: opts.body,
retryConfig: this.retryConfig,
};
let requestStr = `${method} ${requestOptions.url}`;
if (Object.keys(requestOptions.qs).length) {
requestStr += `?${querystring_1.default.stringify(requestOptions.qs)}`;
}
logger.debug(`request: ${requestStr}`);
return request_retry_1.RpRetry.rpRetry(requestOptions, logger)
.then(async (res) => {
if (res.statusCode === 401 && this.refreshTokenHandler && this.auth.refreshToken) {
try {
// eslint-disable-next-line unicorn/no-lonely-if
if (await this.shouldRefreshToken(requestOptions)) {
const auth = await this.refreshTokenHandler(this.auth.refreshToken);
if (!auth) {
throw new Error(`Refresh token error`);
}
this.updateAuth(auth);
requestOptions.headers = (0, lodash_1.merge)(opts.noAuth ? {} : this.authenticationHeader, opts.headers || {});
return request_retry_1.RpRetry.rpRetry(requestOptions, logger);
}
}
catch (error) {
logger.error(error.message);
return Promise.reject(error);
}
}
return Promise.resolve(res);
})
.then((res) => {
logger.debug(`request: ${requestStr} status: ${res.statusCode}`);
return res;
});
}
async getPaginatedResults(apiFunc, args, limit = MAX_RESULTS, page = 1) {
const finalResults = [];
let startingPage = page;
let shouldStop = false;
while (!shouldStop) {
const neededResults = limit - finalResults.length;
// make up to MAX_PAGES_PER_CYCLE requests in parallel
const neededPages = Math.min((0, lodash_1.ceil)(neededResults / MAX_PER_PAGE), MAX_CONCURRENT_REQUESTS);
const pagePromises = (0, lodash_1.range)(startingPage, startingPage + neededPages).map((pageNumber) => apiFunc((0, lodash_1.merge)(args, {
qs: {
page: pageNumber,
per_page: MAX_PER_PAGE,
}
})));
// harvest results
const pagesResults = await Promise.all(pagePromises);
for (const res of pagesResults) {
if (res.statusCode >= 400) {
return res;
}
const currentNeededResults = limit - finalResults.length;
const data = res.body;
finalResults.push(...data.slice(0, currentNeededResults));
if (limit === finalResults.length) {
shouldStop = true;
break; // got all the results we need, no reason to process other requests
}
if (data.length < MAX_PER_PAGE) {
shouldStop = true;
break; // last page, no reason to process other requests
}
}
// next batch
startingPage += neededPages;
}
return {
data: finalResults,
statusCode: 200,
};
}
getName() {
return 'gitlab';
}
async fetchRawFile(opt) {
const res = await this.performAPICall({
api: `projects/${_encodeProjectPath(opt.owner, opt.repo)}/repository/files/${(0, helpers_1.cleanEncodedFilePath)(opt.path)}`,
qs: { ref: opt.ref },
json: true,
});
if (res.statusCode >= 400) {
throw new CFError({
message: `Failed to retrieve file: ${JSON.stringify(opt)}`,
cause: _extractErrorFromResponse(res),
});
}
const base64Content = res.body.content.replace(/\n/gi, '');
return Buffer.from(base64Content, 'base64').toString();
}
async getBranch(opt) {
const res = await this.performAPICall({
api: `projects/${_encodeProjectPath(opt.owner, opt.repo)}/repository/branches/${opt.branch}`,
json: true
});
if (res.statusCode >= 400) {
throw new CFError({
message: `Failed to retrieve branch: ${JSON.stringify(opt)}`,
cause: _extractErrorFromResponse(res),
});
}
return _toBranch(res.body);
}
async getRepository(opt) {
const res = await this.performAPICall({
api: `projects/${_encodeProjectPath(opt.owner, opt.repo)}`,
json: true,
});
if (res.statusCode >= 400) {
throw new CFError({
message: `Failed to get repository: ${JSON.stringify(opt)}`,
cause: _extractErrorFromResponse(res)
});
}
return _toRepo(res.body);
}
async listBranches(opt) {
const res = await this.performAPICall({
api: `projects/${_encodeProjectPath(opt.owner, opt.repo)}/repository/branches`,
json: true,
});
if (res.statusCode >= 400) {
throw new CFError({
message: `Failed to list repository branches: ${JSON.stringify(opt)}`,
cause: _extractErrorFromResponse(res),
});
}
return res.body.map(_toBranch);
}
async createBranch() {
throw new Error('Method createBranch not implemented.');
}
async listRepositoriesForOwner(opt) {
const { owner, sort, direction = 'desc', limit = 20, page = 1, } = opt;
const res = await this.performAPICall({
api: `users/${owner}/projects`,
qs: {
order_by: _toOrderBy(sort),
sort: direction,
page: String(page),
per_page: String(limit),
},
json: true,
});
if (res.statusCode >= 400) {
throw new CFError({
message: `Failed to list repositories for owner: ${JSON.stringify(opt)}`,
cause: _extractErrorFromResponse(res),
});
}
return res.body.map(_toRepo);
}
async listWebhooks(opt) {
const { owner, repo } = opt;
const res = await this.performAPICall({
api: `projects/${_encodeProjectPath(owner, repo)}/hooks`,
json: true,
});
if (res.statusCode >= 400) {
throw new CFError({
message: `Failed to list repository webhooks: ${JSON.stringify(opt)}`,
cause: _extractErrorFromResponse(res),
});
}
return res.body.map(_toWebhook.bind(undefined, owner, repo));
}
async createRepositoryWebhook(opt) {
const { owner, repo, endpoint, secret } = opt;
const res = await this.performAPICall({
api: `projects/${_encodeProjectPath(owner, repo)}/hooks`,
method: 'POST',
json: true,
body: {
url: endpoint,
token: secret,
push_events: true,
merge_requests_events: true,
tag_push_events: true,
description: 'Codefresh',
}
});
if (res.statusCode >= 400) {
throw new CFError({
message: `Failed to create repository webhook: ${JSON.stringify(opt)}`,
cause: _extractErrorFromResponse(res),
});
}
return _toWebhook(owner, repo, res.body);
}
async deleteRepositoryWebhook(opt) {
const { owner, repo, hookId } = opt;
const res = await this.performAPICall({
api: `projects/${_encodeProjectPath(owner, repo)}/hooks/${hookId}`,
method: 'DELETE',
});
if (res.statusCode >= 400) {
throw new CFError({
message: `Failed to delete repository webhook: ${JSON.stringify(opt)}`,
cause: _extractErrorFromResponse(res),
});
}
}
async listRepositoriesWithAffiliation(opt) {
const { affiliation, sort, direction, limit, page } = opt;
const [err, res] = await (0, helpers_1.to)(this.getPaginatedResults(this.performAPICall.bind(this), {
api: 'projects/',
json: true,
qs: {
membership: true,
order_by: _toOrderBy(sort),
simple: false,
...(direction && { sort: direction }),
...(affiliation === 'owner' && { owned: true })
},
}, limit, page));
if (err) {
throw new CFError({
message: `Failed to list repositories: ${JSON.stringify(opt)}`,
cause: err,
});
}
if (res.statusCode >= 400) {
throw new CFError({
message: `Failed to list repositories: ${JSON.stringify(opt)}`,
cause: _extractErrorFromResponse(res),
});
}
return res.data.map(_toRepo);
}
async listOrganizations(opt) {
const { limit = 20, page = 1 } = opt;
const res = await this.performAPICall({
api: `groups`,
qs: {
all_available: 'true',
page: String(page),
perPage: String(limit),
},
json: true,
});
if (res.statusCode >= 400) {
throw new CFError({
message: `Failed to get repository: ${JSON.stringify(opt)}`,
cause: _extractErrorFromResponse(res),
});
}
return res.body.map((group) => group.name);
}
async listRepositoriesForOrganization(opt) {
const { organization, sort, direction = 'desc', limit = 20, page = 1, } = opt;
const res = await this.performAPICall({
api: `groups/${_encodePath(organization)}/projects`,
qs: {
order_by: _toOrderBy(sort),
sort: direction,
page: String(page),
per_page: String(limit),
},
json: true,
});
if (res.statusCode >= 400) {
throw new CFError({
message: `Failed to list repositores for organization: ${JSON.stringify(opt)}`,
cause: _extractErrorFromResponse(res),
});
}
return res.body.map(_toRepo);
}
async createCommitStatus(opt) {
const res = await this.performAPICall({
api: `projects/${_encodeProjectPath(opt.owner, opt.repo)}/statuses/${opt.sha}`,
method: 'POST',
json: true,
body: {
context: opt.context,
description: opt.description,
target_url: opt.targetUrl,
state: statesMap[opt.state],
}
});
const error = _extractErrorFromResponse(res);
if (res.statusCode === 400 && error.message.includes('running') && error.message.includes('Cannot transition status')) {
/* for fixing pipelines running forever,
avoid retry mechanism in case of error that cause by change commit status from running to running
https://codefresh-io.atlassian.net/browse/CR-5024?atlOrigin=eyJpIjoiNmNkODA2NzdhNmVlNGVhNWFiYjljZjE3OTc3ZmNlZDkiLCJwIjoiaiJ9 */
return;
}
if (res.statusCode >= 400) {
throw new CFError({
message: `Failed to create commit status: ${JSON.stringify(opt)}`,
cause: error,
});
}
}
async getUser(opt) {
const user = opt?.username ? opt.username : 'authenticated user';
const [err, res] = await (0, helpers_1.to)(this.performAPICall({
api: opt?.username ? 'users' : 'user',
json: true,
qs: opt?.username ? { username: opt.username } : {}
}));
if (err) {
throw new CFError({
message: `Failed to get user "${user}"`,
cause: err,
});
}
else if (res.statusCode >= 400) {
throw new CFError({
message: `Failed to get user "${user}"`,
cause: _extractErrorFromResponse(res),
});
}
const userObj = Array.isArray(res.body) ? res.body[0] : res.body;
return _toUser(userObj);
}
async getUserByEmail(email) {
const [err, res] = await (0, helpers_1.to)(this.performAPICall({
api: 'users',
json: true,
qs: { search: email }
}));
if (err) {
throw new CFError({
message: `Failed to get user with email "${email}"`,
cause: err,
});
}
else if (res.statusCode >= 400) {
throw new CFError({
message: `Failed to get user with email "${email}"`,
cause: _extractErrorFromResponse(res),
});
}
if (res.body.length === 0) {
throw new CFError(`No user with email: ${email} was found on ${this.getName()}`);
}
return _toUser(res.body[0], email);
}
async getPullRequestFiles() {
throw new Error('Method getPullRequestFiles not implemented.');
}
async getPullRequest() {
throw new Error('Method getPullRequest not implemented.');
}
async searchMergedPullRequestByCommitSha() {
throw new Error('Method searchMergedPullRequestByCommitSha not implemented.');
}
async createPullRequest() {
throw new Error('Method createPullRequest not implemented.');
}
// check read permissions for both user and token and write permissions for user only
async getRepositoryPermissions(opt) {
const repoPermissions = {
read: false,
write: false,
};
const res = await this.performAPICall({
api: `projects/${_encodeProjectPath(opt.owner, opt.repo)}`,
json: true,
});
if (res.statusCode == 200) {
const repo = res.body;
const isGroup = (0, lodash_1.get)(repo, 'namespace.kind') === 'group';
const accessLevel = (0, lodash_1.get)(repo, `permissions.${isGroup ? 'group_access' : 'project_access'}.access_level`, 0);
repoPermissions.write = accessLevel >= 30;
repoPermissions.read = true;
}
return repoPermissions;
}
async assertApiScopes(opt) {
if (opt.scopes.includes('admin_repo_hook')) {
await this.assertAdminScope();
return;
}
if ((opt.scopes.includes('repo_write') || opt.scopes.includes('repo_create')) && opt.repoUrl) {
await this.assertWriteScope(opt.repoUrl);
return;
}
if (opt.scopes.includes('repo_read')) {
await this.assertReadScope();
}
}
async validateToken() {
const res = await this.performAPICall({
api: `user`,
json: true,
});
if (res.statusCode == 401) {
throw new CFError(`ValidationError: token is invalid`);
}
}
skipPermissionsValidation() {
return { skip: false };
}
async assertReadScope() {
const projectsRes = await this.performAPICall({
api: `projects`,
json: true,
});
if (projectsRes.statusCode >= 400) {
throw new CFError({
message: `ValidationError: check your token permissions, failed assert read scopes`,
cause: _extractErrorFromResponse(projectsRes),
});
}
}
async assertWriteScope(repoUrl) {
// there is no existing endpoint to validate write scopes permissions
try {
await (0, helpers_1.assertPatWritePermission)(repoUrl, this.getAuth());
}
catch (error) {
throw new CFError({
message: `ValidationError: check your token permissions, failed assert write scopes`,
cause: error,
});
}
}
async assertAdminScope() {
const res = await this.performAPICall({
api: `projects`,
method: 'POST',
json: true,
});
if (res.statusCode !== 400) {
throw new CFError({
message: `ValidationError: failed to get project, check your token permissions, expected Project Admin`,
cause: _extractErrorFromResponse(res),
});
}
}
async getGroupId(opt) {
const groupRes = await this.performAPICall({
api: `groups/${_encodePath(opt.groupName)}`,
json: true,
method: 'GET'
});
if (groupRes.statusCode == 200) {
return groupRes.body.id;
}
throw new CFError({
message: `Failed to get group id of group "${opt.groupName}"`,
cause: _extractErrorFromResponse(groupRes),
});
}
toOwnerRepo(fullRepoName) {
const repoName = fullRepoName.split('/');
const repo = repoName.pop();
// groups is part of the owner
const owner = repoName.join('/');
if (!repo) {
throw new CFError(`Failed to get repo from repository name ${fullRepoName}`);
}
return [owner, repo];
}
getAuth() {
return (0, lodash_1.omit)(this.auth, 'refreshToken');
}
isTokenMutable() {
return false;
}
requiresRepoToCheckTokenScopes() {
return true;
}
useAdminForUserPermission() {
return false;
}
}
exports.default = Gitlab;
//# sourceMappingURL=gitlab.js.map