feat(container): add image build validation steps
This commit is contained in:
parent
e670fb8bf6
commit
8e1b257144
|
@ -1,12 +1,20 @@
|
||||||
String buildAndPublishImage(Map options = [:]) {
|
String buildAndPublishImage(Map options = [:]) {
|
||||||
def dockerfile = options.get("dockerfile", "./Dockerfile")
|
String dockerfile = options.get('dockerfile', './Dockerfile')
|
||||||
def contextDir = options.get("contextDir", ".")
|
String contextDir = options.get('contextDir', '.')
|
||||||
def imageName = options.get("imageName", "")
|
String imageName = options.get('imageName', '')
|
||||||
def gitRef = sh(returnStdout: true, script: 'git describe --always').trim()
|
String gitRef = sh(returnStdout: true, script: 'git describe --always').trim()
|
||||||
def imageTag = options.get("imageTag", gitRef)
|
String imageTag = options.get('imageTag', gitRef)
|
||||||
def gitCredentialsId = options.get("gitCredentialsId", "forge-jenkins")
|
String gitCredentialsId = options.get('gitCredentialsId', 'forge-jenkins')
|
||||||
def dockerRepository = options.get("dockerRepository", "docker.io")
|
String dockerRepository = options.get('dockerRepository', 'docker.io')
|
||||||
def dockerRepositoryCredentialsId = options.get("dockerRepositoryCredentialsId", "cadoles-docker-hub")
|
String dockerRepositoryCredentialsId = options.get('dockerRepositoryCredentialsId', 'cadoles-docker-hub')
|
||||||
|
Boolean dryRun = options.get('dryRun', true)
|
||||||
|
Boolean skipVerifications = options.get('skipVerification', false)
|
||||||
|
|
||||||
|
String projectRepository = env.JOB_NAME
|
||||||
|
if (env.BRANCH_NAME ==~ /^PR-.*$/) {
|
||||||
|
projectRepository = env.JOB_NAME - "/${env.JOB_BASE_NAME}"
|
||||||
|
}
|
||||||
|
projectRepository = options.get('projectRepository', projectRepository)
|
||||||
|
|
||||||
withCredentials([
|
withCredentials([
|
||||||
usernamePassword([
|
usernamePassword([
|
||||||
|
@ -17,20 +25,33 @@ String buildAndPublishImage(Map options = [:]) {
|
||||||
]) {
|
]) {
|
||||||
String fullImageName = "${dockerRepository}/${env.HUB_USERNAME}/${imageName}"
|
String fullImageName = "${dockerRepository}/${env.HUB_USERNAME}/${imageName}"
|
||||||
|
|
||||||
|
stage('Validate Dockerfile with Hadolint') {
|
||||||
|
utils.when(!skipVerifications) {
|
||||||
|
runHadolintCheck(dockerfile, projectRepository)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
stage("Build image '${imageName}:${imageTag}'") {
|
stage("Build image '${imageName}:${imageTag}'") {
|
||||||
git.withHTTPCredentials(gitCredentialsId) {
|
git.withHTTPCredentials(gitCredentialsId) {
|
||||||
sh """
|
sh """
|
||||||
docker build \
|
docker build \
|
||||||
--build-arg="GIT_USERNAME=${env.GIT_USERNAME}" \
|
--build-arg="GIT_USERNAME=${env.GIT_USERNAME}" \
|
||||||
--build-arg="GIT_PASSWORD=${env.GIT_PASSWORD}" \
|
--build-arg="GIT_PASSWORD=${env.GIT_PASSWORD}" \
|
||||||
-t ${fullImageName}:${imageTag} \
|
-t '${fullImageName}:${imageTag}' \
|
||||||
-f '${dockerfile}' \
|
-f '${dockerfile}' \
|
||||||
'${contextDir}'
|
'${contextDir}'
|
||||||
"""
|
"""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stage('Validate image with Trivy') {
|
||||||
|
utils.when(!skipVerifications) {
|
||||||
|
runTrivyCheck("${fullImageName}:${imageTag}", projectRepository)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
stage("Publish image '${fullImageName}:${imageTag}'") {
|
stage("Publish image '${fullImageName}:${imageTag}'") {
|
||||||
|
utils.when(!dryRun) {
|
||||||
retry(2) {
|
retry(2) {
|
||||||
sh """
|
sh """
|
||||||
echo ${env.HUB_PASSWORD} | docker login -u '${env.HUB_USERNAME}' --password-stdin
|
echo ${env.HUB_PASSWORD} | docker login -u '${env.HUB_USERNAME}' --password-stdin
|
||||||
|
@ -41,27 +62,146 @@ String buildAndPublishImage(Map options = [:]) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def createPodmanPackage(Map options = [:]) {
|
|
||||||
String gomplateBin = getOrInstallGomplate()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String getOrInstallGomplate() {
|
void runHadolintCheck(String dockerfile, String projectRepository) {
|
||||||
String installDir = options.get("installDir", "/usr/local/bin")
|
String reportFile = ".hadolint-report-${currentBuild.startTimeInMillis}.txt"
|
||||||
String version = options.get("version", "3.10.0")
|
|
||||||
String forceDownload = options.get("forceDownload", false)
|
|
||||||
String downloadUrl = options.get("downloadUrl", "https://github.com/hairyhenderson/gomplate/releases/download/v${version}/gomplate_linux-amd64")
|
|
||||||
|
|
||||||
String gomplateBin = sh(returnStdout: true, script: 'which gomplate').trim("")
|
try {
|
||||||
if (gomplateBin == "" || forceDownload) {
|
validateDockerfileWithHadolint(dockerfile, ['reportFile': reportFile])
|
||||||
sh("""
|
} catch (err) {
|
||||||
mkdir -p '${installDir}'
|
unstable("Dockerfile '${dockerfile}' failed linting !")
|
||||||
curl -o tools/gomplate/bin/gomplate -sSL '${forceDownload}'
|
} finally {
|
||||||
chmod +x '${installDir}/gomplate'
|
String lintReport = ''
|
||||||
|
|
||||||
|
if (fileExists(reportFile)) {
|
||||||
|
lintReport = """${lintReport}
|
||||||
|
|
|
||||||
|
|```
|
||||||
|
|${readFile(reportFile)}
|
||||||
|
|```"""
|
||||||
|
} else {
|
||||||
|
lintReport = """${lintReport}
|
||||||
|
|
|
||||||
|
|_Vérification échouée mais aucun rapport trouvé !?_ :thinking:"""
|
||||||
|
}
|
||||||
|
|
||||||
|
String defaultReport = '_Rien à signaler !_ :thumbsup:'
|
||||||
|
String report = """## Validation du Dockerfile `${dockerfile}`
|
||||||
|
|
|
||||||
|
|${lintReport ?: defaultReport}
|
||||||
|
""".stripMargin()
|
||||||
|
|
||||||
|
print report
|
||||||
|
|
||||||
|
if (env.CHANGE_ID) {
|
||||||
|
gitea.commentPullRequest(projectRepository, env.CHANGE_ID, report)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String validateDockerfileWithHadolint(String dockerfile, Map options = [:]) {
|
||||||
|
String hadolintBin = getOrInstallHadolint(options)
|
||||||
|
String hadolintArgs = options.get('hadolintArgs', '--no-color')
|
||||||
|
String reportFile = options.get('reportFile', ".hadolint-report-${currentBuild.startTimeInMillis}.txt")
|
||||||
|
|
||||||
|
sh("""#!/bin/bash
|
||||||
|
set -eo pipefail
|
||||||
|
'${hadolintBin}' '${dockerfile}' ${hadolintArgs} | tee '${reportFile}'
|
||||||
""")
|
""")
|
||||||
|
|
||||||
gomplateBin = "${installDir}/gomplate"
|
return reportFile
|
||||||
}
|
}
|
||||||
|
|
||||||
return gomplateBin
|
void runTrivyCheck(String imageName, String projectRepository, Map options = [:]) {
|
||||||
|
String reportFile = ".trivy-report-${currentBuild.startTimeInMillis}.txt"
|
||||||
|
|
||||||
|
try {
|
||||||
|
validateImageWithTrivy(imageName, ['reportFile': reportFile])
|
||||||
|
} catch (err) {
|
||||||
|
unstable("Image '${imageName}' failed validation !")
|
||||||
|
} finally {
|
||||||
|
String lintReport = ''
|
||||||
|
|
||||||
|
if (fileExists(reportFile)) {
|
||||||
|
lintReport = """${lintReport}
|
||||||
|
|
|
||||||
|
|```
|
||||||
|
|${readFile(reportFile)}
|
||||||
|
|```"""
|
||||||
|
} else {
|
||||||
|
lintReport = """${lintReport}
|
||||||
|
|
|
||||||
|
|_Vérification échouée mais aucun rapport trouvé !?_ :thinking:"""
|
||||||
|
}
|
||||||
|
|
||||||
|
String defaultReport = '_Rien à signaler !_ :thumbsup:'
|
||||||
|
String report = """## Validation de l'image `${imageName}`
|
||||||
|
|
|
||||||
|
|${lintReport ?: defaultReport}
|
||||||
|
""".stripMargin()
|
||||||
|
|
||||||
|
print report
|
||||||
|
|
||||||
|
if (env.CHANGE_ID) {
|
||||||
|
gitea.commentPullRequest(projectRepository, env.CHANGE_ID, report)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String validateImageWithTrivy(String imageName, Map options = [:]) {
|
||||||
|
String trivyBin = getOrInstallTrivy(options)
|
||||||
|
String trivyArgs = options.get('trivyArgs', '--exit-code 1')
|
||||||
|
String cacheDirectory = options.get('cacheDirectory', '.trivy/.cache')
|
||||||
|
String cacheDefaultBranch = options.get('cacheDefaultBranch', 'develop')
|
||||||
|
Integer cacheMaxSize = options.get('cacheMaxSize', 250)
|
||||||
|
String reportFile = options.get('reportFile', ".trivy-report-${currentBuild.startTimeInMillis}.txt")
|
||||||
|
|
||||||
|
cache(maxCacheSize: cacheMaxSize, defaultBranch: cacheDefaultBranch, caches: [
|
||||||
|
[$class: 'ArbitraryFileCache', path: cacheDirectory, compressionMethod: 'TARGZ']
|
||||||
|
]) {
|
||||||
|
sh("'${trivyBin}' --cache-dir '${cacheDirectory}' image -o '${reportFile}' ${trivyArgs} '${imageName}'")
|
||||||
|
}
|
||||||
|
|
||||||
|
return reportFile
|
||||||
|
}
|
||||||
|
|
||||||
|
String getOrInstallHadolint(Map options = [:]) {
|
||||||
|
String installDir = options.get('installDir', '/usr/local/bin')
|
||||||
|
String version = options.get('version', '2.10.0')
|
||||||
|
String forceDownload = options.get('forceDownload', false)
|
||||||
|
String downloadUrl = options.get('downloadUrl', "https://github.com/hadolint/hadolint/releases/download/v${version}/hadolint-Linux-x86_64")
|
||||||
|
|
||||||
|
String hadolintBin = sh(returnStdout: true, script: 'which hadolint || exit 0').trim()
|
||||||
|
if (hadolintBin == '' || forceDownload) {
|
||||||
|
sh("""
|
||||||
|
mkdir -p '${installDir}'
|
||||||
|
curl -o '${installDir}/hadolint' -sSL '${downloadUrl}'
|
||||||
|
chmod +x '${installDir}/hadolint'
|
||||||
|
""")
|
||||||
|
|
||||||
|
hadolintBin = "${installDir}/hadolint"
|
||||||
|
}
|
||||||
|
|
||||||
|
return hadolintBin
|
||||||
|
}
|
||||||
|
|
||||||
|
String getOrInstallTrivy(Map options = [:]) {
|
||||||
|
String installDir = options.get('installDir', '/usr/local/bin')
|
||||||
|
String version = options.get('version', '0.27.1')
|
||||||
|
String forceDownload = options.get('forceDownload', false)
|
||||||
|
String installScriptDownloadUrl = options.get('downloadUrl', 'https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh')
|
||||||
|
|
||||||
|
String trivyBin = sh(returnStdout: true, script: 'which trivy || exit 0').trim()
|
||||||
|
if (trivyBin == '' || forceDownload) {
|
||||||
|
sh("""
|
||||||
|
mkdir -p '${installDir}'
|
||||||
|
curl -sfL '${installScriptDownloadUrl}' | sh -s -- -b '${installDir}' v${version}
|
||||||
|
chmod +x '${installDir}/trivy'
|
||||||
|
""")
|
||||||
|
|
||||||
|
trivyBin = "${installDir}/trivy"
|
||||||
|
}
|
||||||
|
|
||||||
|
return trivyBin
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
def commentPullRequest(String repo, String issueId, String comment, Integer commentIndex = 0) {
|
def commentPullRequest(String repo, String issueId, String comment, Integer commentIndex = -1) {
|
||||||
comment = comment.replaceAll('"', '\\"')
|
comment = comment.replaceAll('"', '\\"')
|
||||||
withCredentials([
|
withCredentials([
|
||||||
string(credentialsId: 'GITEA_JENKINS_PERSONAL_TOKEN', variable: 'GITEA_TOKEN'),
|
string(credentialsId: 'GITEA_JENKINS_PERSONAL_TOKEN', variable: 'GITEA_TOKEN'),
|
||||||
|
@ -7,6 +7,9 @@ def commentPullRequest(String repo, String issueId, String comment, Integer comm
|
||||||
sh """#!/bin/bash
|
sh """#!/bin/bash
|
||||||
set -xeo pipefail
|
set -xeo pipefail
|
||||||
|
|
||||||
|
previous_comment_id=null
|
||||||
|
|
||||||
|
if [ "${commentIndex}" != "-1" ]; then
|
||||||
# Récupération si il existe du commentaire existant
|
# Récupération si il existe du commentaire existant
|
||||||
previous_comment_id=\$(curl -v --fail \
|
previous_comment_id=\$(curl -v --fail \
|
||||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
@ -14,6 +17,7 @@ def commentPullRequest(String repo, String issueId, String comment, Integer comm
|
||||||
https://forge.cadoles.com/api/v1/repos/${repo}/issues/${issueId}/comments \
|
https://forge.cadoles.com/api/v1/repos/${repo}/issues/${issueId}/comments \
|
||||||
| jq -c '[ .[] | select(.user.login=="jenkins") ] | .[${commentIndex}] | .id' \
|
| jq -c '[ .[] | select(.user.login=="jenkins") ] | .[${commentIndex}] | .id' \
|
||||||
)
|
)
|
||||||
|
fi
|
||||||
|
|
||||||
# Génération du payload pour l'API Gitea
|
# Génération du payload pour l'API Gitea
|
||||||
echo '{}' | jq -c --rawfile body .prComment '.body = \$body' > payload.json
|
echo '{}' | jq -c --rawfile body .prComment '.body = \$body' > payload.json
|
||||||
|
|
|
@ -1,7 +1,13 @@
|
||||||
import org.jenkinsci.plugins.pipeline.modeldefinition.Utils
|
import org.jenkinsci.plugins.pipeline.modeldefinition.Utils
|
||||||
|
|
||||||
def call(String baseImage = 'ubuntu:22.04') {
|
def call(String baseImage = 'ubuntu:22.04', Map options = [:]) {
|
||||||
|
Map hooks = options.get('hooks', [:])
|
||||||
|
String jobHistory = options.get('jobHistory', '10')
|
||||||
|
|
||||||
node {
|
node {
|
||||||
|
properties([
|
||||||
|
buildDiscarder(logRotator(daysToKeepStr: jobHistory, numToKeepStr: jobHistory)),
|
||||||
|
])
|
||||||
stage('Cancel older jobs') {
|
stage('Cancel older jobs') {
|
||||||
def buildNumber = env.BUILD_NUMBER as int
|
def buildNumber = env.BUILD_NUMBER as int
|
||||||
if (buildNumber > 1) milestone(buildNumber - 1)
|
if (buildNumber > 1) milestone(buildNumber - 1)
|
||||||
|
@ -11,10 +17,10 @@ def call(String baseImage = 'ubuntu:22.04') {
|
||||||
checkout(scm)
|
checkout(scm)
|
||||||
}
|
}
|
||||||
stage('Run pre hooks') {
|
stage('Run pre hooks') {
|
||||||
hook('pre-symfony-app')
|
runHook(hooks, 'preSymfonyAppPipeline')
|
||||||
}
|
}
|
||||||
stage('Run in Symfony image') {
|
stage('Run in Symfony image') {
|
||||||
def symfonyImage = buildDockerImage(baseImage)
|
def symfonyImage = buildDockerImage(baseImage, hooks)
|
||||||
symfonyImage.inside() {
|
symfonyImage.inside() {
|
||||||
def repo = env.JOB_NAME
|
def repo = env.JOB_NAME
|
||||||
if (env.BRANCH_NAME ==~ /^PR-.*$/) {
|
if (env.BRANCH_NAME ==~ /^PR-.*$/) {
|
||||||
|
@ -34,7 +40,7 @@ def call(String baseImage = 'ubuntu:22.04') {
|
||||||
def auditReport = sh(script: 'local-php-security-checker --format=markdown || true', returnStdout: true)
|
def auditReport = sh(script: 'local-php-security-checker --format=markdown || true', returnStdout: true)
|
||||||
if (auditReport.trim() != '') {
|
if (auditReport.trim() != '') {
|
||||||
if (env.CHANGE_ID) {
|
if (env.CHANGE_ID) {
|
||||||
gitea.commentPullRequest(repo, env.CHANGE_ID, auditReport, 0)
|
gitea.commentPullRequest(repo, env.CHANGE_ID, auditReport)
|
||||||
} else {
|
} else {
|
||||||
print auditReport
|
print auditReport
|
||||||
}
|
}
|
||||||
|
@ -60,7 +66,7 @@ def call(String baseImage = 'ubuntu:22.04') {
|
||||||
'''
|
'''
|
||||||
def report = sh(script: 'junit2md php-cs-fixer.xml', returnStdout: true)
|
def report = sh(script: 'junit2md php-cs-fixer.xml', returnStdout: true)
|
||||||
if (env.CHANGE_ID) {
|
if (env.CHANGE_ID) {
|
||||||
gitea.commentPullRequest(repo, env.CHANGE_ID, report, 1)
|
gitea.commentPullRequest(repo, env.CHANGE_ID, report)
|
||||||
} else {
|
} else {
|
||||||
print report
|
print report
|
||||||
}
|
}
|
||||||
|
@ -81,7 +87,7 @@ def call(String baseImage = 'ubuntu:22.04') {
|
||||||
report = '## Rapport PHPStan\n\n```\n' + report
|
report = '## Rapport PHPStan\n\n```\n' + report
|
||||||
report = report + '\n```\n'
|
report = report + '\n```\n'
|
||||||
if (env.CHANGE_ID) {
|
if (env.CHANGE_ID) {
|
||||||
gitea.commentPullRequest(repo, env.CHANGE_ID, report, 2)
|
gitea.commentPullRequest(repo, env.CHANGE_ID, report)
|
||||||
} else {
|
} else {
|
||||||
print report
|
print report
|
||||||
}
|
}
|
||||||
|
@ -92,12 +98,12 @@ def call(String baseImage = 'ubuntu:22.04') {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
stage('Run post hooks') {
|
stage('Run post hooks') {
|
||||||
hook('post-symfony-app')
|
runHook(hooks, 'postSymfonyAppPipeline')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def buildDockerImage(String baseImage) {
|
void buildDockerImage(String baseImage, Map hooks) {
|
||||||
def imageName = 'cadoles-symfony-ci'
|
def imageName = 'cadoles-symfony-ci'
|
||||||
dir(".${imageName}") {
|
dir(".${imageName}") {
|
||||||
def dockerfile = libraryResource 'com/cadoles/symfony/Dockerfile'
|
def dockerfile = libraryResource 'com/cadoles/symfony/Dockerfile'
|
||||||
|
@ -106,7 +112,7 @@ def buildDockerImage(String baseImage) {
|
||||||
def addLetsEncryptCA = libraryResource 'com/cadoles/common/add-letsencrypt-ca.sh'
|
def addLetsEncryptCA = libraryResource 'com/cadoles/common/add-letsencrypt-ca.sh'
|
||||||
writeFile file:'add-letsencrypt-ca.sh', text:addLetsEncryptCA
|
writeFile file:'add-letsencrypt-ca.sh', text:addLetsEncryptCA
|
||||||
|
|
||||||
hook('build-symfony-image')
|
runHook(hooks, 'buildSymfonyImage')
|
||||||
|
|
||||||
def safeJobName = URLDecoder.decode(env.JOB_NAME).toLowerCase().replace('/', '-').replace(' ', '-')
|
def safeJobName = URLDecoder.decode(env.JOB_NAME).toLowerCase().replace('/', '-').replace(' ', '-')
|
||||||
def imageTag = "${safeJobName}-${env.BUILD_ID}"
|
def imageTag = "${safeJobName}-${env.BUILD_ID}"
|
||||||
|
@ -114,14 +120,15 @@ def buildDockerImage(String baseImage) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def when(boolean condition, body) {
|
void runHook(Map hooks, String name) {
|
||||||
def config = [:]
|
if (!hooks[name]) {
|
||||||
body.resolveStrategy = Closure.OWNER_FIRST
|
println("No hook '${name}' defined. Skipping.")
|
||||||
body.delegate = config
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (condition) {
|
if (hooks[name] instanceof Closure) {
|
||||||
body()
|
hooks[name]()
|
||||||
} else {
|
} else {
|
||||||
Utils.markStageSkippedForConditional(STAGE_NAME)
|
error("Hook '${name}' seems to be defined but is not a closure !")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
import org.jenkinsci.plugins.pipeline.modeldefinition.Utils
|
||||||
|
|
||||||
|
void when(Boolean condition, body) {
|
||||||
|
Map config = [:]
|
||||||
|
body.resolveStrategy = Closure.OWNER_FIRST
|
||||||
|
body.delegate = config
|
||||||
|
|
||||||
|
if (condition) {
|
||||||
|
body()
|
||||||
|
} else {
|
||||||
|
Utils.markStageSkippedForConditional(STAGE_NAME)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue