dd-trace
Version:
Datadog APM tracing client for JavaScript
318 lines (275 loc) • 9.17 kB
JavaScript
const fs = require('fs')
const path = require('path')
const FormData = require('../../../exporters/common/form-data')
const request = require('../../../exporters/common/request')
const { getEnvironmentVariable } = require('../../../config-helper')
const log = require('../../../log')
const { isFalse } = require('../../../util')
const {
getLatestCommits,
getRepositoryUrl,
generatePackFilesForCommits,
getCommitsRevList,
isShallowRepository,
unshallowRepository,
isGitAvailable
} = require('../../../plugins/util/git')
const {
incrementCountMetric,
distributionMetric,
TELEMETRY_GIT_REQUESTS_SEARCH_COMMITS,
TELEMETRY_GIT_REQUESTS_SEARCH_COMMITS_MS,
TELEMETRY_GIT_REQUESTS_SEARCH_COMMITS_ERRORS,
TELEMETRY_GIT_REQUESTS_OBJECT_PACKFILES_NUM,
TELEMETRY_GIT_REQUESTS_OBJECT_PACKFILES,
TELEMETRY_GIT_REQUESTS_OBJECT_PACKFILES_MS,
TELEMETRY_GIT_REQUESTS_OBJECT_PACKFILES_ERRORS,
TELEMETRY_GIT_REQUESTS_OBJECT_PACKFILES_BYTES
} = require('../../../ci-visibility/telemetry')
const isValidSha1 = (sha) => /^[0-9a-f]{40}$/.test(sha)
const isValidSha256 = (sha) => /^[0-9a-f]{64}$/.test(sha)
function validateCommits (commits) {
return commits.map(({ id: commitSha, type }) => {
if (type !== 'commit') {
throw new Error('Invalid commit type response')
}
if (isValidSha1(commitSha) || isValidSha256(commitSha)) {
return commitSha.replaceAll(/[^0-9a-f]+/g, '')
}
throw new Error('Invalid commit format')
})
}
function getCommonRequestOptions (url) {
return {
method: 'POST',
headers: {
'dd-api-key': getEnvironmentVariable('DD_API_KEY')
},
timeout: 15_000,
url
}
}
/**
* This function posts the SHAs of the commits of the last month
* The response are the commits for which the backend already has information
* This response is used to know which commits can be ignored from there on
*/
function getCommitsToUpload ({ url, repositoryUrl, latestCommits, isEvpProxy, evpProxyPrefix }, callback) {
const commonOptions = getCommonRequestOptions(url)
const options = {
...commonOptions,
headers: {
...commonOptions.headers,
'Content-Type': 'application/json'
},
path: '/api/v2/git/repository/search_commits'
}
if (isEvpProxy) {
options.path = `${evpProxyPrefix}/api/v2/git/repository/search_commits`
options.headers['X-Datadog-EVP-Subdomain'] = 'api'
delete options.headers['dd-api-key']
}
const localCommitData = JSON.stringify({
meta: {
repository_url: repositoryUrl
},
data: latestCommits.map(commit => ({
id: commit,
type: 'commit'
}))
})
incrementCountMetric(TELEMETRY_GIT_REQUESTS_SEARCH_COMMITS)
const startTime = Date.now()
request(localCommitData, options, (err, response, statusCode) => {
distributionMetric(TELEMETRY_GIT_REQUESTS_SEARCH_COMMITS_MS, {}, Date.now() - startTime)
if (err) {
incrementCountMetric(TELEMETRY_GIT_REQUESTS_SEARCH_COMMITS_ERRORS, { statusCode })
const error = new Error(`Error fetching commits to exclude: ${err.message}`)
return callback(error)
}
let alreadySeenCommits
try {
alreadySeenCommits = validateCommits(JSON.parse(response).data)
} catch (e) {
incrementCountMetric(TELEMETRY_GIT_REQUESTS_SEARCH_COMMITS_ERRORS, { errorType: 'network' })
return callback(new Error(`Can't parse commits to exclude response: ${e.message}`))
}
log.debug('There are %s commits to exclude.', alreadySeenCommits.length)
const commitsToInclude = latestCommits.filter((commit) => !alreadySeenCommits.includes(commit))
log.debug('There are %s commits to include.', commitsToInclude.length)
if (!commitsToInclude.length) {
return callback(null, [])
}
const commitsToUpload = getCommitsRevList(alreadySeenCommits, commitsToInclude)
if (commitsToUpload === null) {
return callback(new Error('git rev-list failed'))
}
callback(null, commitsToUpload)
})
}
/**
* This function uploads a git packfile
*/
function uploadPackFile ({ url, isEvpProxy, evpProxyPrefix, packFileToUpload, repositoryUrl, headCommit }, callback) {
const form = new FormData()
const pushedSha = JSON.stringify({
data: {
id: headCommit,
type: 'commit'
},
meta: {
repository_url: repositoryUrl
}
})
form.append('pushedSha', pushedSha, { contentType: 'application/json' })
try {
const packFileContent = fs.readFileSync(packFileToUpload)
// The original filename includes a random prefix, so we remove it here
const [, filename] = path.basename(packFileToUpload).split('-')
form.append('packfile', packFileContent, {
filename,
contentType: 'application/octet-stream'
})
} catch {
callback(new Error(`Could not read "${packFileToUpload}"`))
return
}
const commonOptions = getCommonRequestOptions(url)
const options = {
...commonOptions,
path: '/api/v2/git/repository/packfile',
headers: {
...commonOptions.headers,
...form.getHeaders()
}
}
if (isEvpProxy) {
options.path = `${evpProxyPrefix}/api/v2/git/repository/packfile`
options.headers['X-Datadog-EVP-Subdomain'] = 'api'
delete options.headers['dd-api-key']
}
incrementCountMetric(TELEMETRY_GIT_REQUESTS_OBJECT_PACKFILES)
const uploadSize = form.size()
const startTime = Date.now()
request(form, options, (err, _, statusCode) => {
distributionMetric(TELEMETRY_GIT_REQUESTS_OBJECT_PACKFILES_MS, {}, Date.now() - startTime)
if (err) {
incrementCountMetric(TELEMETRY_GIT_REQUESTS_OBJECT_PACKFILES_ERRORS, { statusCode })
const error = new Error(`Could not upload packfiles: status code ${statusCode}: ${err.message}`)
return callback(error, uploadSize)
}
callback(null, uploadSize)
})
}
function generateAndUploadPackFiles ({
url,
isEvpProxy,
evpProxyPrefix,
commitsToUpload,
repositoryUrl,
headCommit
}, callback) {
log.debug('There are %s commits to upload', commitsToUpload.length)
const packFilesToUpload = generatePackFilesForCommits(commitsToUpload)
log.debug('Uploading %s packfiles.', packFilesToUpload.length)
if (!packFilesToUpload.length) {
return callback(new Error('Failed to generate packfiles'))
}
distributionMetric(TELEMETRY_GIT_REQUESTS_OBJECT_PACKFILES_NUM, {}, packFilesToUpload.length)
let packFileIndex = 0
let totalUploadedBytes = 0
// This uploads packfiles sequentially
const uploadPackFileCallback = (err, byteLength) => {
totalUploadedBytes += byteLength
if (err || packFileIndex === packFilesToUpload.length) {
distributionMetric(TELEMETRY_GIT_REQUESTS_OBJECT_PACKFILES_BYTES, {}, totalUploadedBytes)
return callback(err)
}
return uploadPackFile(
{
packFileToUpload: packFilesToUpload[packFileIndex++],
url,
isEvpProxy,
evpProxyPrefix,
repositoryUrl,
headCommit
},
uploadPackFileCallback
)
}
uploadPackFile(
{
packFileToUpload: packFilesToUpload[packFileIndex++],
url,
isEvpProxy,
evpProxyPrefix,
repositoryUrl,
headCommit
},
uploadPackFileCallback
)
}
/**
* This function uploads git metadata to CI Visibility's backend.
*/
function sendGitMetadata (url, { isEvpProxy, evpProxyPrefix }, configRepositoryUrl, callback) {
if (!isGitAvailable()) {
return callback(new Error('Git is not available'))
}
let repositoryUrl = configRepositoryUrl
if (!repositoryUrl) {
repositoryUrl = getRepositoryUrl()
}
log.debug('Uploading git history for repository', repositoryUrl)
if (!repositoryUrl) {
return callback(new Error('Repository URL is empty'))
}
let latestCommits = getLatestCommits()
log.debug('There were %s commits since last month.', latestCommits.length)
const getOnFinishGetCommitsToUpload = (hasCheckedShallow) => (err, commitsToUpload) => {
if (err) {
return callback(err)
}
if (!commitsToUpload.length) {
log.debug('No commits to upload')
return callback(null)
}
// If it has already unshallowed or the clone is not shallow, we move on
if (hasCheckedShallow || !isShallowRepository()) {
const [headCommit] = latestCommits
return generateAndUploadPackFiles({
url,
isEvpProxy,
evpProxyPrefix,
commitsToUpload,
repositoryUrl,
headCommit
}, callback)
}
// Otherwise we unshallow and get commits to upload again
log.debug('It is shallow clone, unshallowing...')
if (!isFalse(getEnvironmentVariable('DD_CIVISIBILITY_GIT_UNSHALLOW_ENABLED'))) {
unshallowRepository(false)
}
// The latest commits change after unshallowing
latestCommits = getLatestCommits()
getCommitsToUpload({
url,
repositoryUrl,
latestCommits,
isEvpProxy,
evpProxyPrefix
}, getOnFinishGetCommitsToUpload(true))
}
getCommitsToUpload({
url,
repositoryUrl,
latestCommits,
isEvpProxy,
evpProxyPrefix
}, getOnFinishGetCommitsToUpload(false))
}
module.exports = {
sendGitMetadata
}