/** * Construit, valide et publie (optionnellement) une image Docker sur le registre Cadoles (par défaut) * * Options disponibles: * * - dockerfile - String - Chemin vers le fichier Dockerfile à utiliser pour construire l'image, par défaut "./Dockerfile" * - contextDir - String - Répertoire servant de "contexte" pour la construction de l'image, par défault "./" * - imageName - String - Nom de l'image à construire, par défaut "" * - imageTags - String - Tag(s) apposé(s) sur l'image après construction, par défaut tags générés par la méthode utils.getProjectVersionTags() * - gitCredentialsId - String - Identifiant des "credentials" Jenkins utilisés pour cloner le dépôt Git, par défaut "forge-jenkins" * - dockerRepository - String - Nom d'hôte du registre Docker sur lequel publier l'image, par défaut "reg.cadoles.com" * - dockerRepositoryCredentialsId - String - Identifiant des "credentials" Jenkins utilisés pour déployer l'image sur le registre Docker, par défault "reg.cadoles.com-jenkins" * - dryRun - Boolean - Désactiver/activer la publication de l'image sur le registre Docker, par défaut "true" * - skipVerifications - Boolean - Désactiver/activer les étapes de vérifications de qualité/sécurité de l'image Docker, par défaut "false" */ String buildAndPublishImage(Map options = [:]) { String dockerfile = options.get('dockerfile', './Dockerfile') String contextDir = options.get('contextDir', '.') String imageName = options.get('imageName', '') String gitRef = sh(returnStdout: true, script: 'git describe --always').trim() List imageTags = options.get('imageTags', utils.getProjectVersionTags()) // Handle legacy imageTag parameter if (options.containsKey('imageTag')) { imageTags = [ options.get("imageTag", gitRef) ] } String gitCredentialsId = options.get('gitCredentialsId', 'forge-jenkins') String dockerRepository = options.get('dockerRepository', 'reg.cadoles.com') String dockerRepositoryCredentialsId = options.get('dockerRepositoryCredentialsId', 'reg.cadoles.com-jenkins') 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([ usernamePassword([ credentialsId: dockerRepositoryCredentialsId, usernameVariable: 'HUB_USERNAME', passwordVariable: 'HUB_PASSWORD' ]), ]) { stage('Validate Dockerfile with Hadolint') { utils.when(!skipVerifications) { runHadolintCheck(dockerfile, projectRepository) } } String primaryImageTag = imageTags[0] stage("Build image '${imageName}:${primaryImageTag}'") { git.withHTTPCredentials(gitCredentialsId) { sh """ docker build \ --build-arg="GIT_USERNAME=${env.GIT_USERNAME}" \ --build-arg="GIT_PASSWORD=${env.GIT_PASSWORD}" \ -t '${imageName}:${primaryImageTag}' \ -f '${dockerfile}' \ '${contextDir}' """ } } stage('Validate image with Trivy') { utils.when(!skipVerifications) { runTrivyCheck("${imageName}:${primaryImageTag}", projectRepository) } } stage("Login with image repository") { utils.when(!dryRun) { sh """ echo ${env.HUB_PASSWORD} | docker login -u '${env.HUB_USERNAME}' --password-stdin '${dockerRepository}' """ } } imageTags.each { imageTag -> stage("Publish image '${imageName}:${imageTag}'") { utils.when(!dryRun) { sh """ docker tag "${imageName}:${primaryImageTag}" "${imageName}:${imageTag}" """ retry(2) { sh """ docker push '${imageName}:${imageTag}' """ } } } } } } void runHadolintCheck(String dockerfile, String projectRepository) { String reportFile = ".hadolint-report-${currentBuild.startTimeInMillis}.txt" try { validateDockerfileWithHadolint(dockerfile, ['reportFile': reportFile]) } catch (err) { unstable("Dockerfile '${dockerfile}' failed linting !") } 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 = """## Rapport d'analyse du fichier `${dockerfile}` avec [Hadolint](https://github.com/hadolint/hadolint) | |${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}' """) return reportFile } 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 = """## Rapport d'analyse de l'image avec [Trivy](https://github.com/aquasecurity/trivy) | |${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") String markdownTemplate = libraryResource 'com/cadoles/trivy/templates/markdown.tpl' writeFile file:'.trivy-markdown.tpl', text: markdownTemplate cache(maxCacheSize: cacheMaxSize, defaultBranch: cacheDefaultBranch, caches: [ [$class: 'ArbitraryFileCache', path: cacheDirectory, compressionMethod: 'TARGZ'] ]) { sh("'${trivyBin}' --cache-dir '${cacheDirectory}' image --format template --template '@.trivy-markdown.tpl' -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 }