├── .editorconfig ├── .gitignore ├── .mvn └── wrapper │ └── maven-wrapper.properties ├── CHANGELOG.md ├── Jenkinsfile ├── LICENSE ├── README.md ├── docs └── development │ ├── development_en.md │ └── releasing_en.md ├── mvnw ├── mvnw.cmd ├── pom.xml ├── src └── com │ └── cloudogu │ └── ces │ └── cesbuildlib │ ├── Bats.groovy │ ├── Changelog.groovy │ ├── Docker.groovy │ ├── Dockerfile.groovy │ ├── DoguRegistry.groovy │ ├── Git.groovy │ ├── GitFlow.groovy │ ├── GitHub.groovy │ ├── Gpg.groovy │ ├── Gradle.groovy │ ├── GradleInDockerBase.groovy │ ├── GradleWrapperInDocker.groovy │ ├── HttpClient.groovy │ ├── K3d.groovy │ ├── K3dRegistry.groovy │ ├── Makefile.groovy │ ├── Markdown.groovy │ ├── Maven.groovy │ ├── MavenInDocker.groovy │ ├── MavenInDockerBase.groovy │ ├── MavenLocal.groovy │ ├── MavenWrapper.groovy │ ├── MavenWrapperInDocker.groovy │ ├── ReleaseNotes.groovy │ ├── SCMManager.groovy │ ├── Sh.groovy │ ├── SonarCloud.groovy │ ├── SonarQube.groovy │ ├── Trivy.groovy │ ├── TrivyScanException.groovy │ ├── TrivyScanFormat.groovy │ ├── TrivyScanStrategy.groovy │ └── TrivySeverityLevel.groovy ├── test └── com │ └── cloudogu │ └── ces │ └── cesbuildlib │ ├── BatsTest.groovy │ ├── ChangelogTest.groovy │ ├── DockerMock.groovy │ ├── DockerTest.groovy │ ├── DoguRegistryTest.groovy │ ├── GitFlowTest.groovy │ ├── GitHubTest.groovy │ ├── GitTest.groovy │ ├── GpgTest.groovy │ ├── GradleInDockerBaseTest.groovy │ ├── GradleMock.groovy │ ├── GradleTest.groovy │ ├── GradleWrapperInDockerTest.groovy │ ├── HttpClientTest.groovy │ ├── K3dTest.groovy │ ├── MakefileTest.groovy │ ├── MarkdownTest.groovy │ ├── MavenInDockerBaseTest.groovy │ ├── MavenInDockerTest.groovy │ ├── MavenLocalTest.groovy │ ├── MavenMock.groovy │ ├── MavenTest.groovy │ ├── MavenWrapperInDockerTest.groovy │ ├── MavenWrapperTest.groovy │ ├── SCMManagerTest.groovy │ ├── ScriptMock.groovy │ ├── ShTest.groovy │ ├── SonarCloudTest.groovy │ ├── SonarQubeTest.groovy │ ├── TrivyExecutor.groovy │ ├── TrivyTest.groovy │ ├── findEmailRecipientsTest.groovy │ ├── findHostnameTest.groovy │ ├── findVulnerabilitiesWithTrivyTest.groovy │ ├── isPullRequestTest.groovy │ └── mailIfStatusChangedTest.groovy └── vars ├── checkChangelog.groovy ├── checkReleaseNotes.groovy ├── findEmailRecipients.groovy ├── findEmailRecipients.txt ├── findHostName.groovy ├── findHostName.txt ├── findVulnerabilitiesWithTrivy.groovy ├── findVulnerabilitiesWithTrivy.txt ├── isBuildSuccessful.groovy ├── isBuildSuccessful.txt ├── isPullRequest.groovy ├── isPullRequest.txt ├── lintDockerfile.groovy ├── mailIfStatusChanged.groovy ├── mailIfStatusChanged.txt └── shellCheck.groovy /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | # Unix-style newlines with a newline ending every file 7 | end_of_line = lf 8 | charset = utf-8 9 | insert_final_newline = true 10 | indent_size = 4 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | /target/ 3 | /*.iml 4 | /.idea/ 5 | .mvn/** 6 | **/trivyReport.json 7 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | wrapperVersion=3.3.2 18 | distributionType=only-script 19 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip 20 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | #!groovy 2 | 3 | node('docker') { 4 | 5 | properties([ 6 | // Keep only the last 10 build to preserve space 7 | buildDiscarder(logRotator(numToKeepStr: '10')), 8 | // Don't run concurrent builds for a branch, because they use the same workspace directory 9 | disableConcurrentBuilds() 10 | ]) 11 | 12 | def cesBuildLib = libraryFromLocalRepo().com.cloudogu.ces.cesbuildlib 13 | 14 | def mvn = cesBuildLib.MavenWrapperInDocker.new(this, 'eclipse-temurin:11.0.25_9-jdk-alpine') 15 | mvn.useLocalRepoFromJenkins = true 16 | def git = cesBuildLib.Git.new(this) 17 | 18 | if ("master".equals(env.BRANCH_NAME)) { 19 | mvn.additionalArgs = "-DperformRelease" 20 | currentBuild.description = mvn.getVersion() 21 | } 22 | 23 | String emailRecipients = env.EMAIL_RECIPIENTS 24 | 25 | catchError { 26 | stage('Checkout') { 27 | checkout scm 28 | /* Don't remove folders starting in "." like 29 | * .m2 (maven) 30 | * .npm 31 | * .cache, .local (bower) 32 | */ 33 | git.clean('".*/"') 34 | } 35 | 36 | stage('Build') { 37 | // Run the maven build 38 | mvn 'clean install -DskipTests' 39 | archive 'target/*.jar' 40 | } 41 | 42 | stage('Unit Test') { 43 | mvn 'test' 44 | // Archive Unit and integration test results, if any 45 | junit allowEmptyResults: true, testResults: '**/target/failsafe-reports/TEST-*.xml,**/target/surefire-reports/TEST-*.xml' 46 | } 47 | 48 | stage('SonarQube') { 49 | generateCoverageReportForSonarQube(mvn) 50 | def sonarQube = cesBuildLib.SonarQube.new(this, 'ces-sonar') 51 | sonarQube.updateAnalysisResultOfPullRequestsToGitHub('sonarqube-gh-token') 52 | 53 | // SonarQube >= v25.01 needs JDK 17 54 | def mvnWithJdk17 = cesBuildLib.MavenWrapperInDocker.new(this, 'eclipse-temurin:17.0.14_7-jdk-alpine') 55 | mvnWithJdk17.useLocalRepoFromJenkins = true 56 | sonarQube.analyzeWith(mvnWithJdk17) 57 | 58 | if (!sonarQube.waitForQualityGateWebhookToBeCalled()) { 59 | unstable("Pipeline unstable due to SonarQube quality gate failure") 60 | } 61 | } 62 | } 63 | 64 | mailIfStatusChanged(findEmailRecipients(emailRecipients)) 65 | } 66 | 67 | static void generateCoverageReportForSonarQube(def mvn) { 68 | mvn 'org.jacoco:jacoco-maven-plugin:0.8.5:report' 69 | } 70 | 71 | def libraryFromLocalRepo() { 72 | // Workaround for loading the current repo as shared build lib. 73 | // Checks out to workspace local folder named like the identifier. 74 | // We have to pass an identifier with version (which is ignored). Otherwise the build fails. 75 | library(identifier: 'ces-build-lib@snapshot', retriever: legacySCM(scm)) 76 | } 77 | -------------------------------------------------------------------------------- /docs/development/development_en.md: -------------------------------------------------------------------------------- 1 | # Developing the ces-build-lib 2 | 3 | You need a working Java-11 SDK 4 | 5 | Run tests with 6 | 7 | ```bash 8 | ./mvnw test 9 | ``` 10 | 11 | If you want to run tests within IntelliJ you need to use Java 8. 12 | 13 | # Running tests in IntelliJ 14 | 15 | Open Project Structure and set Java 8 as SDK. 16 | 17 | Run 18 | 19 | ```bash 20 | ./mvnw install 21 | ``` 22 | 23 | Then right-click tests in IntelliJ and run. 24 | 25 | # Update Maven Version 26 | 27 | Use this line to update the mvnw command with your desired version: 28 | 29 | ```bash 30 | ./mvnw -N wrapper:wrapper -Dmaven=3.9.9 31 | ``` 32 | 33 | This will change the mvnw-File and the mvnw.cmd-File. 34 | -------------------------------------------------------------------------------- /docs/development/releasing_en.md: -------------------------------------------------------------------------------- 1 | # Releasing ces-build-lib 2 | 3 | The ces-build-lib is released via git-flow: 4 | - Review and merge your PR into the develop branch 5 | - Start git-flow release on develop branch, e.g. `git flow release start 1.51.0` 6 | - Update CHANGELOG.md and pom.xml; commit 7 | - Finish git-flow release, e.g. `git flow release finish -s 1.51.0` 8 | - Push branches to remote, e.g. `git push origin develop --tags` and `git push origin main` 9 | - Add new release on Github: https://github.com/cloudogu/ces-build-lib/releases -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | <# : batch portion 2 | @REM ---------------------------------------------------------------------------- 3 | @REM Licensed to the Apache Software Foundation (ASF) under one 4 | @REM or more contributor license agreements. See the NOTICE file 5 | @REM distributed with this work for additional information 6 | @REM regarding copyright ownership. The ASF licenses this file 7 | @REM to you under the Apache License, Version 2.0 (the 8 | @REM "License"); you may not use this file except in compliance 9 | @REM with the License. You may obtain a copy of the License at 10 | @REM 11 | @REM http://www.apache.org/licenses/LICENSE-2.0 12 | @REM 13 | @REM Unless required by applicable law or agreed to in writing, 14 | @REM software distributed under the License is distributed on an 15 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | @REM KIND, either express or implied. See the License for the 17 | @REM specific language governing permissions and limitations 18 | @REM under the License. 19 | @REM ---------------------------------------------------------------------------- 20 | 21 | @REM ---------------------------------------------------------------------------- 22 | @REM Apache Maven Wrapper startup batch script, version 3.3.2 23 | @REM 24 | @REM Optional ENV vars 25 | @REM MVNW_REPOURL - repo url base for downloading maven distribution 26 | @REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven 27 | @REM MVNW_VERBOSE - true: enable verbose log; others: silence the output 28 | @REM ---------------------------------------------------------------------------- 29 | 30 | @IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) 31 | @SET __MVNW_CMD__= 32 | @SET __MVNW_ERROR__= 33 | @SET __MVNW_PSMODULEP_SAVE=%PSModulePath% 34 | @SET PSModulePath= 35 | @FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( 36 | IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) 37 | ) 38 | @SET PSModulePath=%__MVNW_PSMODULEP_SAVE% 39 | @SET __MVNW_PSMODULEP_SAVE= 40 | @SET __MVNW_ARG0_NAME__= 41 | @SET MVNW_USERNAME= 42 | @SET MVNW_PASSWORD= 43 | @IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) 44 | @echo Cannot start maven from wrapper >&2 && exit /b 1 45 | @GOTO :EOF 46 | : end batch / begin powershell #> 47 | 48 | $ErrorActionPreference = "Stop" 49 | if ($env:MVNW_VERBOSE -eq "true") { 50 | $VerbosePreference = "Continue" 51 | } 52 | 53 | # calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties 54 | $distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl 55 | if (!$distributionUrl) { 56 | Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" 57 | } 58 | 59 | switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { 60 | "maven-mvnd-*" { 61 | $USE_MVND = $true 62 | $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" 63 | $MVN_CMD = "mvnd.cmd" 64 | break 65 | } 66 | default { 67 | $USE_MVND = $false 68 | $MVN_CMD = $script -replace '^mvnw','mvn' 69 | break 70 | } 71 | } 72 | 73 | # apply MVNW_REPOURL and calculate MAVEN_HOME 74 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ 75 | if ($env:MVNW_REPOURL) { 76 | $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } 77 | $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" 78 | } 79 | $distributionUrlName = $distributionUrl -replace '^.*/','' 80 | $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' 81 | $MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" 82 | if ($env:MAVEN_USER_HOME) { 83 | $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" 84 | } 85 | $MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' 86 | $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" 87 | 88 | if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { 89 | Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" 90 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 91 | exit $? 92 | } 93 | 94 | if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { 95 | Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" 96 | } 97 | 98 | # prepare tmp dir 99 | $TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile 100 | $TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" 101 | $TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null 102 | trap { 103 | if ($TMP_DOWNLOAD_DIR.Exists) { 104 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 105 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 106 | } 107 | } 108 | 109 | New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null 110 | 111 | # Download and Install Apache Maven 112 | Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." 113 | Write-Verbose "Downloading from: $distributionUrl" 114 | Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" 115 | 116 | $webclient = New-Object System.Net.WebClient 117 | if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { 118 | $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) 119 | } 120 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 121 | $webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null 122 | 123 | # If specified, validate the SHA-256 sum of the Maven distribution zip file 124 | $distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum 125 | if ($distributionSha256Sum) { 126 | if ($USE_MVND) { 127 | Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." 128 | } 129 | Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash 130 | if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { 131 | Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." 132 | } 133 | } 134 | 135 | # unzip and move 136 | Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null 137 | Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null 138 | try { 139 | Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null 140 | } catch { 141 | if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { 142 | Write-Error "fail to move MAVEN_HOME" 143 | } 144 | } finally { 145 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 146 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 147 | } 148 | 149 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 150 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 4.0.0 7 | 8 | 9 | jenkins-ci-releases 10 | https://repo.jenkins-ci.org/releases/ 11 | 12 | 13 | maven-central 14 | https://repo1.maven.org/maven2 15 | 16 | 17 | 18 | 19 | com.cloudogu.ces 20 | ces-build-lib 21 | ces-build-lib 22 | 4.2.0 23 | 24 | 25 | 26 | UTF-8 27 | 0.8.12 28 | 11 29 | 11 30 | 31 | 32 | 33 | 34 | groovy-plugins-release 35 | https://groovy.jfrog.io/artifactory/plugins-release 36 | 37 | 38 | 39 | 40 | 41 | 42 | org.codehaus.groovy 43 | groovy-all 44 | 2.5.23 45 | pom 46 | 47 | 48 | org.codehaus.groovy 49 | groovy-test 50 | 51 | 52 | 53 | 54 | 55 | org.jenkins-ci.plugins.workflow 56 | workflow-cps 57 | 3996.va_f5c1799f978 58 | 59 | 60 | 61 | org.codehaus.groovy 62 | groovy-test-junit5 63 | 2.5.23 64 | test 65 | 66 | 67 | 68 | org.mockito 69 | mockito-junit-jupiter 70 | 3.6.28 71 | test 72 | 73 | 74 | 75 | org.mockito 76 | mockito-core 77 | 3.6.28 78 | test 79 | 80 | 81 | 82 | org.hamcrest 83 | hamcrest 84 | 3.0 85 | test 86 | 87 | 88 | 89 | org.junit.jupiter 90 | junit-jupiter 91 | 5.4.2 92 | test 93 | 94 | 95 | 96 | org.junit.vintage 97 | junit-vintage-engine 98 | 5.4.2 99 | test 100 | 101 | 102 | 103 | com.lesfurets 104 | jenkins-pipeline-unit 105 | 1.23 106 | test 107 | 108 | 109 | 110 | 111 | commons-io 112 | commons-io 113 | 2.18.0 114 | test 115 | 116 | 117 | org.apache.commons 118 | commons-compress 119 | 1.27.1 120 | test 121 | 122 | 123 | 124 | 125 | src 126 | test 127 | 128 | 129 | resources 130 | 131 | 132 | 133 | 134 | 135 | org.apache.maven.plugins 136 | maven-compiler-plugin 137 | 3.13.0 138 | 139 | groovy-eclipse-compiler 140 | ${maven.compiler.target} 141 | 142 | 143 | 144 | org.codehaus.groovy 145 | groovy-eclipse-compiler 146 | 3.9.0 147 | 148 | 149 | org.codehaus.groovy 150 | groovy-eclipse-batch 151 | 2.5.17-02 152 | 153 | 154 | 155 | 156 | 157 | org.apache.maven.plugins 158 | maven-surefire-plugin 159 | 3.5.2 160 | 161 | 162 | 163 | 164 | org.apache.maven.plugins 165 | maven-source-plugin 166 | 3.0.1 167 | 168 | 169 | attach-sources 170 | 171 | jar 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | jenkins 183 | 184 | 185 | 186 | env.BUILD_URL 187 | 188 | 189 | 190 | 191 | 192 | 193 | org.jacoco 194 | jacoco-maven-plugin 195 | ${jacoco.version} 196 | 197 | 198 | initialize 199 | 200 | prepare-agent 201 | 202 | 203 | 204 | report 205 | 206 | report 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | -------------------------------------------------------------------------------- /src/com/cloudogu/ces/cesbuildlib/Bats.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | 3 | /** 4 | * Bats provides functions to easily execute bats tests (bash scripting tests) 5 | */ 6 | class Bats { 7 | private script 8 | private docker 9 | 10 | private static String bats_base_image = "bats_base_image" 11 | private static String bats_custom_image = "bats_custom_image" 12 | private static String bats_tag = "bats_tag" 13 | def defaultSetupConfig = [ 14 | bats_base_image : "bats/bats", 15 | bats_custom_image: "cloudogu/bats", 16 | bats_tag : "1.2.1" 17 | ] 18 | 19 | Bats(script, docker) { 20 | this.script = script 21 | this.docker = docker 22 | } 23 | 24 | void checkAndExecuteTests(config = [:]) { 25 | // Merge default config with the one passed as parameter 26 | config = defaultSetupConfig << config 27 | 28 | script.echo "Executing bats tests with config:" 29 | script.echo "${config}" 30 | def batsImage = docker.build("${config[bats_custom_image]}:${config[bats_tag]}", "--build-arg=BATS_BASE_IMAGE=${config[bats_base_image]} --build-arg=BATS_TAG=${config[bats_tag]} ./build/make/bats") 31 | try { 32 | script.sh "mkdir -p target" 33 | script.sh "mkdir -p testdir" 34 | 35 | batsImage.inside("--entrypoint='' -v ${script.env.WORKSPACE}:/workspace -v ${script.env.WORKSPACE}/testdir:/usr/share/webapps") { 36 | script.sh "make unit-test-shell-ci" 37 | } 38 | } finally { 39 | script.junit allowEmptyResults: true, testResults: 'target/shell_test_reports/*.xml' 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/com/cloudogu/ces/cesbuildlib/Changelog.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | 3 | /** 4 | * Provides the functionality to read changes of a specific version in a changelog that is 5 | * based on the changelog format on https://keepachangelog.com/. 6 | */ 7 | class Changelog implements Serializable { 8 | private script 9 | private String changelogFileName 10 | 11 | Changelog(script) { 12 | this(script, 'CHANGELOG.md') 13 | } 14 | 15 | Changelog(script, changelogFileName) { 16 | this.script = script 17 | this.changelogFileName = changelogFileName 18 | } 19 | 20 | /** 21 | * @return Returns the content of the given changelog. 22 | */ 23 | private String readChangelog(){ 24 | script.readFile changelogFileName 25 | } 26 | 27 | /** 28 | * Extracts the changes for a given version out of the changelog. 29 | * 30 | * @param releaseVersion The version to get the changes for. 31 | * @return Returns the changes as String. 32 | */ 33 | String changesForVersion(String releaseVersion) { 34 | def changelog = readChangelog() 35 | def start = changesStartIndex(changelog, releaseVersion) 36 | def end = changesEndIndex(changelog, start) 37 | return escapeForJson(changelog.substring(start, end).trim()) 38 | } 39 | 40 | /** 41 | * Removes characters from a string that could break the json struct when passing the string as json value. 42 | * 43 | * @param string The string to format. 44 | * @return Returns the formatted string. 45 | */ 46 | private static String escapeForJson(String string) { 47 | return string 48 | .replace("\"", "") 49 | .replace("'", "") 50 | .replace("\\", "") 51 | .replace("\n", "\\n") 52 | } 53 | 54 | /** 55 | * Returns the start index of changes of a specific release version in the changelog. 56 | * 57 | * @param releaseVersion The version to get the changes for. 58 | * @return Returns the index in the changelog string where the changes start. 59 | */ 60 | private static int changesStartIndex(String changelog, String releaseVersion) { 61 | def index = changelog.indexOf("## [${releaseVersion}]") 62 | if (index == -1){ 63 | throw new IllegalArgumentException("The desired version '${releaseVersion}' could not be found in the changelog.") 64 | } 65 | def offset = changelog.substring(index).indexOf("\n") 66 | return index + offset 67 | } 68 | 69 | /** 70 | * Returns the end index of changes of a specific release version in the changelog. 71 | * 72 | * @param start The start index of the changes for this version. 73 | * @return Returns the index in the changelog string where the changes end. 74 | */ 75 | private static int changesEndIndex(String changelog, int start) { 76 | def changelogAfterStartIndex = changelog.substring(start) 77 | def index = changelogAfterStartIndex.indexOf("\n## [") 78 | if (index == -1) { 79 | return changelog.length() 80 | } 81 | return index + start 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/com/cloudogu/ces/cesbuildlib/Dockerfile.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | 3 | class Dockerfile { 4 | private script 5 | 6 | Dockerfile(script) { 7 | this.script = script 8 | } 9 | 10 | /** 11 | * Lints the Dockerfile with hadolint using a configuration file 12 | * 13 | * To configure hadelint, add a ".hadolint.yaml" file to your working directory 14 | * See https://github.com/hadolint/hadolint#configure 15 | * 16 | * @param dockerfile Path to the Dockerfile that should be linted 17 | * @param configuration Path to the hadolint configuration file 18 | * @param hadolintVersion Version of the hadolint/hadolint container image 19 | */ 20 | void lintWithConfig(String dockerfile = "Dockerfile", String configuration = ".hadolint.yaml", hadolintVersion = "latest-debian"){ 21 | script.docker.image("hadolint/hadolint:${hadolintVersion}").inside(){ 22 | script.sh "hadolint --no-color -c ${configuration} ${dockerfile}" 23 | } 24 | } 25 | 26 | /** 27 | * Lints the Dockerfile with the latest version of hadolint 28 | * Only fails on errors, ignores warnings etc. 29 | * Trusts registries docker.io, gcr.io and registry.cloudogu.com 30 | * 31 | * @param dockerfile Path to the Dockerfile that should be linted 32 | * @param hadolintVersion Version of the hadolint/hadolint container image 33 | */ 34 | void lint(String dockerfile = "Dockerfile", hadolintVersion = "latest-debian"){ 35 | script.docker.image("hadolint/hadolint:${hadolintVersion}").inside(){ 36 | script.sh "hadolint -t error --no-color --trusted-registry docker.io --trusted-registry gcr.io --trusted-registry registry.cloudogu.com ${dockerfile}" 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/com/cloudogu/ces/cesbuildlib/DoguRegistry.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | 3 | /** 4 | * This class contain methods and workflows to upload dogus (json) or k8s components (yaml) to a specified dogu registry. 5 | */ 6 | class DoguRegistry { 7 | public Sh sh 8 | public HttpClient doguRegistryHttpClient 9 | 10 | private script 11 | private String backendCredentialsID 12 | private String doguRegistryURL 13 | 14 | private static String DOGU_POST_ENDPOINT = "api/v2/dogus" 15 | private static String K8S_POST_ENDPOINT = "api/v1/k8s" 16 | 17 | /** 18 | * Create an object to upload dogus or k8s components to a specified registry. 19 | * 20 | * @param script The Jenkins script you are coming from (aka "this"). 21 | * @param doguRegistryURL Url to the actual dogu registry. Default: 'https://dogu.cloudogu.com'. 22 | * @param backendCredentialsID Identifier of credentials used to log into the backend. Default: cesmarvin-setup. 23 | */ 24 | DoguRegistry(script, String doguRegistryURL = "https://dogu.cloudogu.com", String backendCredentialsID = "cesmarvin-setup") { 25 | this.script = script 26 | this.backendCredentialsID = backendCredentialsID 27 | this.doguRegistryURL = doguRegistryURL 28 | this.doguRegistryHttpClient = new HttpClient(script, backendCredentialsID) 29 | this.sh = new Sh(script) 30 | } 31 | 32 | /** 33 | * Pushes a dogu to the dogu registry. 34 | * 35 | * @param pathToDoguJson path to the dogu.json file. The path should be relative to the workspace. 36 | */ 37 | void pushDogu(String pathToDoguJson = "dogu.json") { 38 | def doguJson = script.readJSON file: pathToDoguJson 39 | def doguVersion = doguJson.Version 40 | def doguNameWithNamespace = doguJson.Name 41 | script.sh "echo 'Push Dogu:\n-Namespace/Name: ${doguNameWithNamespace}\n-Version: ${doguVersion}'" 42 | 43 | def doguString = this.sh.returnStdOut("cat ${pathToDoguJson}") 44 | def trimmedUrl = trimSuffix(doguRegistryURL, '/') 45 | def result = doguRegistryHttpClient.put("${trimmedUrl}/${DOGU_POST_ENDPOINT}/${doguNameWithNamespace}", "application/json", doguString) 46 | checkStatus(result, pathToDoguJson) 47 | } 48 | 49 | /** 50 | * Pushes a yaml tapestry to the dogu registry for k8s components. 51 | * 52 | * @param pathToYaml Path to the yaml containing the k8s component. 53 | * @param k8sName Name of the k8s component. 54 | * @param k8sNamespace Namespace of the k8s component. 55 | * @param versionWithoutVPrefix The version of the component without the version prefix. 56 | */ 57 | void pushK8sYaml(String pathToYaml, String k8sName, String k8sNamespace, String versionWithoutVPrefix) { 58 | script.sh "echo 'Push Yaml:\n-Name: ${k8sName}\n-Namespace: ${k8sNamespace}\n-Version: ${versionWithoutVPrefix}'" 59 | 60 | def trimmedUrl = trimSuffix(doguRegistryURL, '/') 61 | def result = doguRegistryHttpClient.putFile("${trimmedUrl}/${K8S_POST_ENDPOINT}/${k8sNamespace}/${k8sName}/${versionWithoutVPrefix}", "application/yaml", pathToYaml) 62 | checkStatus(result, pathToYaml) 63 | } 64 | 65 | private static String trimSuffix(String original, String suffix) { 66 | if(original.endsWith(suffix)) { 67 | return original.substring(0, original.length() - suffix.length()) 68 | } 69 | return original 70 | } 71 | 72 | private void checkStatus(LinkedHashMap result, String fileName) { 73 | def status = result["httpCode"] 74 | def body = result["body"] 75 | 76 | if ((status as Integer) >= 400) { 77 | script.sh "echo 'Error pushing ${fileName}'" 78 | script.sh "echo '${body}'" 79 | script.sh "exit 1" 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/com/cloudogu/ces/cesbuildlib/GitFlow.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | 3 | class GitFlow implements Serializable { 4 | private def script 5 | private Git git 6 | Sh sh 7 | 8 | GitFlow(script, Git git) { 9 | this.script = script 10 | this.git = git 11 | this.sh = new Sh(script) 12 | } 13 | 14 | /** 15 | * @return if this branch is a release branch according to git flow 16 | */ 17 | boolean isReleaseBranch() { 18 | return git.getBranchName().startsWith('release/') 19 | } 20 | 21 | /** 22 | * @return if this branch is the develop branch and therefor ready for pre-release according to git flow 23 | */ 24 | boolean isPreReleaseBranch() { 25 | return git.getSimpleBranchName().equals("develop") 26 | } 27 | 28 | /** 29 | * Finishes a git flow release and pushes all merged branches to remote 30 | * 31 | * Only execute this function if you are already on a release branch 32 | * 33 | * @param releaseVersion the version that is going to be released 34 | */ 35 | void finishRelease(String releaseVersion, String productionBranch = "master") { 36 | String branchName = git.getBranchName() 37 | 38 | // Stop the build here if there is already a tag for this version on remote. 39 | // Do not stop the build when the tag only exists locally 40 | // because this could mean the build has failed and was restarted. 41 | if (git.originTagExists("${releaseVersion}")) { 42 | script.error('You cannot build this version, because it already exists.') 43 | } 44 | 45 | // Make sure all branches are fetched 46 | git.fetch() 47 | 48 | // Stop the build if there are new changes on develop that are not merged into this feature branch. 49 | if (git.originBranchesHaveDiverged(branchName, 'develop')) { 50 | script.error('There are changes on develop branch that are not merged into release. Please merge and restart process.') 51 | } 52 | 53 | // Make sure any branch we need exists locally 54 | git.checkoutLatest(branchName) 55 | // Remember latest committer on develop to use as author of release commits 56 | String releaseBranchAuthor = git.commitAuthorName 57 | String releaseBranchEmail = git.commitAuthorEmail 58 | 59 | git.checkoutLatest('develop') 60 | git.checkoutLatest(productionBranch) 61 | 62 | // Merge release branch into productionBranch 63 | git.mergeNoFastForward(branchName, releaseBranchAuthor, releaseBranchEmail) 64 | 65 | // Create tag. Use -f because the created tag will persist when build has failed. 66 | git.setTag(releaseVersion, "release version ${releaseVersion}", true) 67 | // Merge release branch into develop 68 | git.checkout('develop') 69 | // Set author of release Branch as author of merge commit 70 | // Otherwise the author of the last commit on develop would author the commit, which is unexpected 71 | git.mergeNoFastForward(branchName, releaseBranchAuthor, releaseBranchEmail) 72 | 73 | // Delete release branch 74 | git.deleteLocalBranch(branchName) 75 | 76 | // Checkout tag 77 | git.checkout(releaseVersion) 78 | 79 | // Push changes and tags 80 | git.push("origin ${productionBranch} develop ${releaseVersion}") 81 | git.deleteOriginBranch(branchName) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/com/cloudogu/ces/cesbuildlib/GitHub.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | 3 | import groovy.json.JsonSlurper 4 | 5 | class GitHub implements Serializable { 6 | private script 7 | private git 8 | Sh sh 9 | 10 | GitHub(script, git) { 11 | this.script = script 12 | this.git = git 13 | this.sh = new Sh(script) 14 | } 15 | 16 | /** 17 | * Uploads a file and adds it to the release assets of a release. 18 | * @param releaseId The API-ID of the release. Can be obtained as return value of 'createReleaseWithChangelog' or 'createRelease'. 19 | * @param filePath The path to the file which should be uploaded. 20 | */ 21 | void addReleaseAsset(String releaseId, String filePath) { 22 | def repositoryName = git.getRepositoryName() 23 | 24 | try { 25 | if (!git.credentials) { 26 | throw new IllegalArgumentException('Unable to create Github release without credentials.') 27 | } 28 | 29 | script.withCredentials([script.usernamePassword( 30 | credentialsId: git.credentials, usernameVariable: 'GIT_AUTH_USR', passwordVariable: 'GIT_AUTH_PSW')]) { 31 | 32 | def apiUrl = "https://uploads.github.com/repos/${repositoryName}/releases/${releaseId}/assets?name=\$(basename ${filePath})" 33 | def flags = """--header "Content-Type: multipart/form-data" --data-binary @${filePath}""" 34 | def username = '\$GIT_AUTH_USR' 35 | def password = '\$GIT_AUTH_PSW' 36 | script.sh "curl -u ${username}:${password} ${flags} ${apiUrl}" 37 | } 38 | } catch (Exception e) { 39 | script.unstable("Asset upload failed due to error: ${e}") 40 | script.echo 'Please manually upload asset.' 41 | } 42 | } 43 | 44 | /** 45 | * Creates a new release on Github and adds changelog info to it 46 | * 47 | * @param releaseVersion the version for the github release 48 | * @param changelog the changelog object to extract the release information from 49 | */ 50 | String createReleaseWithChangelog(String releaseVersion, Changelog changelog, String productionBranch = "master") { 51 | try { 52 | def changelogText = changelog.changesForVersion(releaseVersion) 53 | script.echo "The description of github release will be: >>>${changelogText}<<<" 54 | createRelease(releaseVersion, changelogText, productionBranch) 55 | } catch (IllegalArgumentException e) { 56 | script.unstable("Release failed due to error: ${e}") 57 | script.echo 'Please manually update github release.' 58 | } 59 | } 60 | 61 | /** 62 | * Creates a release on Github and fills it with the changes provided 63 | */ 64 | String createRelease(String releaseVersion, String changes, String productionBranch = "master") { 65 | def repositoryName = git.getRepositoryName() 66 | if (!git.credentials) { 67 | throw new IllegalArgumentException('Unable to create Github release without credentials.') 68 | } 69 | script.withCredentials([script.usernamePassword( 70 | credentialsId: git.credentials, usernameVariable: 'GIT_AUTH_USR', passwordVariable: 'GIT_AUTH_PSW')]) { 71 | 72 | def body = 73 | """{"tag_name": "${releaseVersion}", "target_commitish": "${productionBranch}", "name": "${releaseVersion}", "body":"${changes}"}""" 74 | def apiUrl = "https://api.github.com/repos/${repositoryName}/releases" 75 | def flags = """--request POST --data '${body.trim()}' --header "Content-Type: application/json" """ 76 | def username = '\$GIT_AUTH_USR' 77 | def password = '\$GIT_AUTH_PSW' 78 | def jsonResponse = this.sh.returnStdOut("curl -u ${username}:${password} ${flags} ${apiUrl}") 79 | def jsonSlurper = new JsonSlurper() 80 | return jsonSlurper.parseText(jsonResponse).id 81 | } 82 | } 83 | 84 | /** 85 | * Commits and pushes a folder to the gh-pages branch of the current repo. 86 | * Can be used to conveniently deliver websites. See https://pages.github.com/ 87 | * 88 | * Uses the name and email of the last committer as author and committer. 89 | * 90 | * Note that the branch is temporarily checked out to the .gh-pages folder. 91 | * 92 | * @param workspaceFolder 93 | * @param commitMessage 94 | */ 95 | void pushPagesBranch(String workspaceFolder, String commitMessage, String subFolder = '.') { 96 | def ghPagesTempDir = '.gh-pages' 97 | try { 98 | script.dir(ghPagesTempDir) { 99 | this.git.git url: this.git.repositoryUrl, branch: 'gh-pages', changelog: false, poll: false 100 | 101 | script.sh "mkdir -p ${subFolder}" 102 | script.sh "cp -rf ../${workspaceFolder}/* ${subFolder}" 103 | this.git.add '.' 104 | this.git.commit commitMessage 105 | this.git.push 'gh-pages' 106 | } 107 | } finally { 108 | script.sh "rm -rf ${ghPagesTempDir}" 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/com/cloudogu/ces/cesbuildlib/Gpg.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | 3 | class Gpg { 4 | private script 5 | private docker 6 | 7 | Gpg(script, docker) { 8 | this.script = script 9 | this.docker = docker 10 | } 11 | 12 | /** 13 | * Executes the 'make signature-ci' command to create the digital signature for a build. 14 | */ 15 | void createSignature() { 16 | this.withPrivateKey { 17 | // The passphrase variable is provided in `withPrivateKey` 18 | script.sh("make passphrase=\$passphrase signature-ci") 19 | } 20 | } 21 | 22 | /** 23 | * Calls a closure inside of a docker container which is able to execute 'gpg' commands as well as 'make' commands. 24 | * @param closure The closure which is called inside of the docker container. 25 | */ 26 | private void withGpg(Closure closure) { 27 | this.buildGpgDockerImage() 28 | String dockerArgs = "-v ${script.env.WORKSPACE}:/tmp/workspace" 29 | dockerArgs <<= " --entrypoint=''" 30 | dockerArgs <<= " -v ${script.env.pwd}/.gnupg:/root/.gnupg" 31 | docker 32 | .image('cloudogu/gpg:1.0') 33 | .mountJenkinsUser() 34 | .inside(dockerArgs) { 35 | script.sh "cd /tmp/workspace" 36 | closure.call() 37 | } 38 | } 39 | 40 | /** 41 | * Creates, builds and then removes a Dockerfile which is able to execute 'gpg' commands as well as 'make' commands. 42 | */ 43 | private void buildGpgDockerImage() { 44 | def dockerfile = """ 45 | FROM debian:stable-slim 46 | LABEL maintainer="hello@cloudogu.com" 47 | 48 | RUN apt update && apt install -y gnupg2 make git 49 | 50 | ENTRYPOINT ["/usr/bin/gpg"] 51 | """ 52 | try { 53 | script.writeFile encoding: 'UTF-8', file: 'Dockerfile.gpgbuild', text: dockerfile.trim() 54 | docker.build("cloudogu/gpg:1.0", "-f Dockerfile.gpgbuild .") 55 | } catch (e) { 56 | script.echo "${e}" 57 | throw e 58 | } finally { 59 | script.sh "rm -f Dockerfile.gpgbuild" 60 | } 61 | } 62 | 63 | /** 64 | * Calls a closure in an environment which contains a gpg signing key and is able to execute 'gpg' commands and 'make' commands. 65 | * @param closure The closure to call. 66 | */ 67 | private void withPrivateKey(Closure closure) { 68 | script.withCredentials([script.string(credentialsId: 'jenkins_gpg_private_key_passphrase', variable: 'passphrase')]) { 69 | script.withCredentials([script.file(credentialsId: 'jenkins_gpg_private_key_for_ces_tool_release_signing', variable: 'pkey')]) { 70 | try { 71 | withGpg { 72 | script.sh "gpg --yes --always-trust --pinentry-mode loopback --passphrase=\"\$passphrase\" --import \$pkey" 73 | closure.call() 74 | } 75 | } catch (e) { 76 | script.echo "${e}" 77 | throw e 78 | } 79 | finally { 80 | script.sh "rm -f \$pkey" 81 | script.sh "rm -rf .gnupg" 82 | } 83 | } 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/com/cloudogu/ces/cesbuildlib/Gradle.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | 3 | abstract class Gradle implements Serializable { 4 | protected script 5 | 6 | Gradle(script) { 7 | this.script = script 8 | } 9 | 10 | def call(String args, boolean printStdOut = true) { 11 | gradle(args, printStdOut) 12 | } 13 | 14 | /** 15 | * @param printStdOut - returns output of gradle as String instead of printing to console 16 | */ 17 | protected abstract def gradle(String args, boolean printStdOut = true) 18 | 19 | def gradlew(String args, boolean printStdOut) { 20 | sh("./gradlew "+ args, printStdOut) 21 | } 22 | 23 | void sh(String command, boolean printStdOut) { 24 | script.echo "executing sh: ${command}, return Stdout: ${printStdOut}" 25 | if (printStdOut) { 26 | script.sh "${command}" 27 | } else { 28 | new Sh(script).returnStdOut command 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/com/cloudogu/ces/cesbuildlib/GradleInDockerBase.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | /** 3 | * Common tools for all GradleInDocker classes 4 | */ 5 | abstract class GradleInDockerBase extends Gradle { 6 | 7 | /** Setting this to {@code true} allows the Gradle build to access the docker host, i.e. to start other containers.*/ 8 | boolean enableDockerHost = false 9 | protected String credentialsId = null 10 | 11 | Docker docker 12 | 13 | GradleInDockerBase(script, String credentialsId = null) { 14 | super(script) 15 | this.credentialsId = credentialsId 16 | this.docker = new Docker(script) 17 | } 18 | 19 | @Override 20 | def gradle(String args, boolean printStdOut = true) { 21 | call({ args }, printStdOut) 22 | } 23 | 24 | abstract def call(Closure closure, boolean printStdOut); 25 | 26 | protected void inDocker(String imageId, Closure closure) { 27 | if (this.credentialsId) { 28 | docker.withRegistry("https://${imageId}", this.credentialsId) { 29 | dockerImageBuilder(imageId, closure) 30 | } 31 | } else { 32 | dockerImageBuilder(imageId, closure) 33 | } 34 | } 35 | 36 | protected void dockerImageBuilder(String imageId , closure) { 37 | docker.image(imageId) 38 | // Mount user and set HOME, which results in the workspace being user.home. Otherwise '?' might be the user.home. 39 | .mountJenkinsUser(true) 40 | .mountDockerSocket(enableDockerHost) 41 | .inside("") { 42 | closure.call() 43 | } 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/com/cloudogu/ces/cesbuildlib/GradleWrapperInDocker.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | 3 | /** 4 | * Run gradle using a Gradle Wrapper from the local repository within a Docker Container. 5 | * The image of the container can be used to specified a JDK. 6 | */ 7 | class GradleWrapperInDocker extends GradleInDockerBase { 8 | /** The docker image to use, e.g. {@code adoptopenjdk/openjdk11:jdk-11.0.1.13-alpine} **/ 9 | private String imageId 10 | 11 | GradleWrapperInDocker(script, String imageId, String credentialsId = null) { 12 | super(script, credentialsId) 13 | this.imageId = imageId 14 | } 15 | 16 | @Override 17 | def call(Closure closure, boolean printStdOut) { 18 | inDocker(imageId) { 19 | gradlew(closure.call(), printStdOut) 20 | } 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/com/cloudogu/ces/cesbuildlib/HttpClient.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | 3 | /** 4 | * An HTTP client that calls curl on the shell. 5 | * 6 | * Returns a map of 7 | * * httpCode (String) 8 | * * headers (Map) 9 | * * body (String) 10 | */ 11 | class HttpClient implements Serializable { 12 | private script 13 | private credentials 14 | private Sh sh 15 | 16 | HttpClient(script, credentials = '') { 17 | this.script = script 18 | this.credentials = credentials 19 | this.sh = new Sh(script) 20 | } 21 | 22 | Map get(String url, String contentType = '', def data = '') { 23 | return httpRequest('GET', url, contentType, data) 24 | } 25 | 26 | Map put(String url, String contentType = '', def data = '') { 27 | return httpRequest('PUT', url, contentType, data) 28 | } 29 | 30 | Map putFile(String url, String contentType = '', String filePath) { 31 | String command 32 | executeWithCredentials { 33 | command = getUploadFileCurlCommand('PUT', url, contentType, filePath) 34 | } 35 | return httpRequest('PUT', url, contentType, filePath, command) 36 | } 37 | 38 | Map post(String url, String contentType = '', def data = '') { 39 | return httpRequest('POST', url, contentType, data) 40 | } 41 | 42 | protected String executeWithCredentials(Closure closure) { 43 | if (credentials) { 44 | script.withCredentials([script.usernamePassword(credentialsId: credentials, 45 | passwordVariable: 'CURL_PASSWORD', usernameVariable: 'CURL_USER')]) { 46 | closure.call(true) 47 | } 48 | } else { 49 | closure.call(false) 50 | } 51 | } 52 | 53 | private static String escapeSingleQuotes(String toEscape) { 54 | return toEscape.replaceAll("'", "'\"'\"'") 55 | } 56 | 57 | protected String getCurlAuthParam() { 58 | "-u '" + escapeSingleQuotes(script.env.CURL_USER) + ":" + escapeSingleQuotes(script.env.CURL_PASSWORD) + "' " 59 | } 60 | 61 | private String getCurlCommand(String httpMethod, String url, String contentType, String data) { 62 | return "curl -i -X '" + escapeSingleQuotes(httpMethod) + "' " + 63 | (credentials ? getCurlAuthParam() : '') + 64 | (contentType ? "-H 'Content-Type: " + escapeSingleQuotes(contentType) + "' " : '') + 65 | (data ? "-d '" + escapeSingleQuotes(data) + "' " : '') + 66 | "'" + escapeSingleQuotes(url) + "'" 67 | } 68 | 69 | private String getUploadFileCurlCommand(String httpMethod, String url, String contentType, String filePath) { 70 | return "curl -i -X '" + escapeSingleQuotes(httpMethod) + "' " + 71 | (credentials ? getCurlAuthParam() : '') + 72 | (contentType ? "-H 'Content-Type: " + escapeSingleQuotes(contentType) + "' " : '') + 73 | (filePath ? "-T '" + escapeSingleQuotes(filePath) + "' " : '') + 74 | "'" + escapeSingleQuotes(url) + "'" 75 | } 76 | 77 | protected Map httpRequest(String httpMethod, String url, String contentType, def data, String customCommand = '') { 78 | String httpResponse 79 | def rawHeaders 80 | def body 81 | 82 | executeWithCredentials { 83 | String curlCommand 84 | if (customCommand.isEmpty()) { 85 | curlCommand = getCurlCommand(httpMethod, url, contentType, data.toString()) 86 | } else { 87 | curlCommand = customCommand 88 | } 89 | 90 | // Command must be run inside this closure, otherwise the credentials will not be masked (using '*') in the console 91 | httpResponse = sh.returnStdOut curlCommand 92 | } 93 | 94 | String[] responseLines = httpResponse.split("\n") 95 | 96 | // e.g. HTTP/2 301 97 | String httpCode = responseLines[0].split(" ")[1] 98 | def separatingLine = responseLines.findIndexOf { it.trim().isEmpty() } 99 | 100 | if (separatingLine > 0) { 101 | rawHeaders = responseLines[1..(separatingLine - 1)] 102 | body = responseLines[separatingLine + 1..-1].join('\n') 103 | } else { 104 | // No body returned 105 | rawHeaders = responseLines[1..-1] 106 | body = '' 107 | } 108 | 109 | def headers = [:] 110 | for (String line : rawHeaders) { 111 | // e.g. cache-control: no-cache 112 | def splitLine = line.split(':', 2) 113 | headers[splitLine[0].trim()] = splitLine[1].trim() 114 | } 115 | return [httpCode: httpCode, headers: headers, body: body] 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/com/cloudogu/ces/cesbuildlib/K3dRegistry.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | 3 | class K3dRegistry { 4 | private final String registryName 5 | private final String localRegistryPort 6 | private String imageRegistryInternalHandle 7 | private String imageRegistryExternalHandle 8 | private Sh sh 9 | private script 10 | 11 | /** 12 | * creates a new K3dRegistry object 13 | * @param script the jenkins script 14 | * @param registryName the name of the local image registry under which images are made available 15 | * @param port the local registry's TCP port under which images are made available 16 | */ 17 | K3dRegistry(def script, String registryName, String port) { 18 | this.localRegistryPort = port 19 | this.registryName = registryName 20 | this.script = script 21 | this.sh = new Sh(script) 22 | } 23 | 24 | /** 25 | * installs a local registry avoiding double resource occupation for TCP ports and registry name. 26 | * Note that k3d prefixes its internal registry with "k3d-". 27 | */ 28 | protected void installLocalRegistry() { 29 | script.sh "k3d registry create ${this.registryName} --port ${localRegistryPort}" 30 | 31 | def sillyK3dRegistryPrefix="k3d-" 32 | this.imageRegistryInternalHandle = "${sillyK3dRegistryPrefix}${this.registryName}:${localRegistryPort}" 33 | this.imageRegistryExternalHandle = "localhost:${localRegistryPort}" 34 | } 35 | 36 | def getImageRegistryInternalWithPort() { 37 | return this.imageRegistryInternalHandle 38 | } 39 | 40 | /** 41 | * builds an image with the given image name and image tag and pushes it to the local image registry 42 | * @param imageName the image name 43 | * @param tag the image tag 44 | * @return the image repository name of the built image relative to the internal image registry, f. i. localRegistyName:randomPort/my/image:tag 45 | */ 46 | def buildAndPushToLocalRegistry(def imageName, def tag) { 47 | def internalHandle="${imageName}:${tag}" 48 | def externalRegistry="${this.imageRegistryExternalHandle}" 49 | 50 | def dockerImage = script.docker.build("${internalHandle}") 51 | 52 | script.docker.withRegistry("http://${externalRegistry}/") { 53 | dockerImage.push("${tag}") 54 | } 55 | 56 | return "${this.imageRegistryInternalHandle}/${internalHandle}" 57 | } 58 | 59 | /** 60 | * deletes the local K3d registry 61 | */ 62 | def delete() { 63 | try { 64 | script.sh "k3d registry delete ${this.registryName}" 65 | } finally {} 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/com/cloudogu/ces/cesbuildlib/Makefile.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | 3 | class Makefile { 4 | private script 5 | private Sh sh 6 | 7 | /** 8 | * Creates an object to conveniently read from a Makefile in the current directory. 9 | * 10 | * @param script The Jenkins script you are coming from (aka "this") 11 | */ 12 | Makefile(script) { 13 | this.script = script 14 | this.sh = new Sh(script) 15 | } 16 | 17 | /** 18 | * Retrieves the value of the VERSION Variable defined in the Makefile. 19 | */ 20 | String getVersion() { 21 | return sh.returnStdOut('grep -e "^VERSION=" Makefile | sed "s/VERSION=//g"') 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/com/cloudogu/ces/cesbuildlib/Markdown.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | 3 | class Markdown implements Serializable{ 4 | Sh sh 5 | private script 6 | Docker docker 7 | private String tag 8 | 9 | Markdown(script, String tag = "stable") { 10 | this.script = script 11 | this.sh = new Sh(script) 12 | this.docker = new Docker(script) 13 | this.tag = tag 14 | } 15 | 16 | def check(){ 17 | this.docker.image("ghcr.io/tcort/markdown-link-check:${this.tag}") 18 | .mountJenkinsUser() 19 | .inside("--entrypoint=\"\" -v ${this.script.env.WORKSPACE}/docs:/docs") { 20 | this.script.sh 'find /docs -name \\*.md -print0 | xargs -0 -n1 markdown-link-check -v' 21 | } 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/com/cloudogu/ces/cesbuildlib/MavenInDocker.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | /** 3 | * Run maven in a docker container. 4 | * 5 | * This can be helpful, 6 | * * when constant ports are bound during the build that cause port conflicts in concurrent builds. 7 | * For example, when running integration tests, unit tests that use infrastructure that binds to ports or 8 | * * when one maven repo per builds is required 9 | * For example when concurrent builds of multi module project install the same snapshot versions. 10 | * 11 | * The build are run inside the official maven containers from https://hub.docker.com/_/maven/ 12 | */ 13 | class MavenInDocker extends MavenInDockerBase { 14 | 15 | /** The version of the maven docker image to use, e.g. {@code maven:3.5.0-jdk-8} **/ 16 | String mavenImage 17 | 18 | /** 19 | * @param script the Jenkinsfile instance ({@code this} in Jenkinsfile) 20 | * @param mavenImage the version of the maven docker image to use, e.g. {@code 3.5.0-jdk-8} 21 | */ 22 | MavenInDocker(script, String mavenImage, String credentialsId = null) { 23 | super(script) 24 | this.mavenImage = mavenImage 25 | this.credentialsId = credentialsId 26 | } 27 | 28 | @Override 29 | def call(Closure closure, boolean printStdOut) { 30 | inDocker(getMavenImage()) { 31 | sh("mvn ${createCommandLineArgs(closure.call())}", printStdOut) 32 | } 33 | } 34 | 35 | //allowing downward compatibility for the old workflow only specifying the tag 36 | def getMavenImage() { 37 | return mavenImage.contains(':') ? mavenImage : "maven:${mavenImage}" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/com/cloudogu/ces/cesbuildlib/MavenInDockerBase.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | /** 3 | * Common tools for all MavenInDocker classes 4 | */ 5 | abstract class MavenInDockerBase extends Maven { 6 | 7 | public String credentialsId = null 8 | 9 | /** Setting this to {@code true} allows the maven build to access the docker host, i.e. to start other containers.*/ 10 | boolean enableDockerHost = false 11 | 12 | /** Setting this to {@code true} makes Maven use Jenkin's local maven repo instead of one in the build's workspace 13 | * Using the Jenkins speeds up the first build and uses less memory. However, concurrent builds of multi module 14 | * projects building the same version (e.g. a SNAPSHOT), might overwrite their dependencies, causing 15 | * non-deterministic build failures.*/ 16 | boolean useLocalRepoFromJenkins = false 17 | 18 | Docker docker 19 | 20 | MavenInDockerBase(script) { 21 | super(script) 22 | this.docker = new Docker(script) 23 | } 24 | 25 | @Override 26 | def mvn(String args, boolean printStdOut = true) { 27 | call({ args }, printStdOut) 28 | } 29 | 30 | abstract def call(Closure closure, boolean printStdOut); 31 | 32 | String createDockerRunArgs() { 33 | String runArgs = "" 34 | 35 | if (useLocalRepoFromJenkins) { 36 | // If Jenkin's local maven repo does not exist, make sure it is created by the user that runs the build. 37 | // Otherwise, if not existing, this folder is create as root, which denies permission to jenkins 38 | script.sh returnStatus: true, script: 'mkdir -p $HOME/.m2' 39 | 40 | // Mount Jenkin's local maven repo as local maven repo within the container 41 | runArgs += " -v ${script.env.HOME}/.m2:${script.pwd()}/.m2" 42 | } 43 | 44 | return runArgs 45 | } 46 | 47 | protected void inDocker(String imageId, Closure closure) { 48 | if (this.credentialsId) { 49 | docker.withRegistry("https://${imageId}", this.credentialsId) { 50 | dockerImageBuilder(imageId, closure) 51 | } 52 | } else { 53 | dockerImageBuilder(imageId, closure) 54 | } 55 | } 56 | 57 | protected void dockerImageBuilder(String imageId , closure) { 58 | docker.image(imageId) 59 | // Mount user and set HOME, which results in the workspace being user.home. Otherwise '?' might be the user.home. 60 | .mountJenkinsUser(true) 61 | .mountDockerSocket(enableDockerHost) 62 | .inside(createDockerRunArgs()) { 63 | closure.call() 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/com/cloudogu/ces/cesbuildlib/MavenLocal.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | 3 | /** 4 | * Run maven from a local tool installation on Jenkins. 5 | */ 6 | class MavenLocal extends Maven { 7 | private mvnHome 8 | private javaHome 9 | 10 | MavenLocal(script, mvnHome, javaHome) { 11 | super(script) 12 | this.mvnHome = mvnHome 13 | this.javaHome = javaHome 14 | } 15 | 16 | @Override 17 | def mvn(String args, boolean printStdOut = true) { 18 | // Advice: don't define M2_HOME in general. Maven will autodetect its root fine. 19 | // PATH+something prepends to PATH 20 | warnIfToolsNotInstalled() 21 | script.withEnv(["JAVA_HOME=${javaHome}", "PATH+MAVEN=${mvnHome}/bin:${script.env.JAVA_HOME}/bin"]) { 22 | sh ("${mvnHome}/bin/mvn ${createCommandLineArgs(args)}", printStdOut) 23 | } 24 | } 25 | 26 | void warnIfToolsNotInstalled() { 27 | /* Unfortunately, Jenkins seems to silently return null when calling "tool 'toolID'" for an existing tool that 28 | does not support auto installation. */ 29 | if (!mvnHome) { 30 | script.echo 'WARNING: mvnHome is empty. Did you check "Install automatically"?' 31 | } 32 | if (!javaHome) { 33 | script.echo 'WARNING: javaHome is empty. Did you check "Install automatically"?' 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/com/cloudogu/ces/cesbuildlib/MavenWrapper.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | 3 | /** 4 | * Run maven using a Maven Wrapper from the local repository. 5 | * 6 | * See https://github.com/takari/maven-wrapper 7 | */ 8 | class MavenWrapper extends Maven { 9 | 10 | private javaHome 11 | 12 | /** 13 | * @deprecated 14 | * Using no explicit Java tool results in using the one that happens to be in the PATH of the build agent. 15 | * Experience tells us that this is absolutely non-deterministic and will result in unpredictable behavior. 16 | * So: Better set an explicit Java tool to be used, or use MavenWrapperInDocker. 17 | * 18 | */ 19 | @Deprecated 20 | MavenWrapper(script) { 21 | this(script, '') 22 | } 23 | 24 | MavenWrapper(script, javaHome) { 25 | super(script) 26 | this.javaHome = javaHome 27 | } 28 | 29 | @Override 30 | def mvn(String args, boolean printStdOut = true) { 31 | 32 | if (javaHome) { 33 | // PATH+something prepends to PATH 34 | script.withEnv(["JAVA_HOME=${javaHome}", "PATH+JDK=${script.env.JAVA_HOME}/bin"]) { 35 | mvnw(args, printStdOut) 36 | } 37 | } else { 38 | mvnw(args, printStdOut) 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /src/com/cloudogu/ces/cesbuildlib/MavenWrapperInDocker.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | 3 | /** 4 | * Run maven using a Maven Wrapper from the local repository within a Docker Container. 5 | * The image of the container can be used to specified a JDK. 6 | * 7 | * See https://github.com/takari/maven-wrapper 8 | */ 9 | class MavenWrapperInDocker extends MavenInDockerBase { 10 | 11 | /** The docker image to use, e.g. {@code adoptopenjdk/openjdk11:jdk-11.0.1.13-alpine} **/ 12 | private String imageId 13 | 14 | MavenWrapperInDocker(script, String imageId, String credentialsId = null ) { 15 | super(script) 16 | this.imageId = imageId 17 | this.credentialsId = credentialsId 18 | } 19 | 20 | @Override 21 | def call(Closure closure, boolean printStdOut) { 22 | inDocker(imageId) { 23 | mvnw(closure.call(), printStdOut) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/com/cloudogu/ces/cesbuildlib/ReleaseNotes.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | 3 | /** 4 | * Enables interaction with release notes files 5 | */ 6 | class ReleaseNotes implements Serializable { 7 | private script 8 | private String releaseNotesFileNameDE 9 | private String releaseNotesFileNameEN 10 | 11 | ReleaseNotes(script) { 12 | this(script, 'docs/gui/release_notes_de.md', 'docs/gui/release_notes_en.md') 13 | } 14 | 15 | ReleaseNotes(script, releaseNotesFileNameDE, releaseNotesFileNameEN) { 16 | this.script = script 17 | this.releaseNotesFileNameDE = releaseNotesFileNameDE 18 | this.releaseNotesFileNameEN = releaseNotesFileNameEN 19 | } 20 | 21 | /** 22 | * @return Returns the content of german release notes file. 23 | */ 24 | private String readReleaseNotesDE(){ 25 | script.readFile releaseNotesFileNameDE 26 | } 27 | 28 | /** 29 | * @return Returns the content of english release notes file. 30 | */ 31 | private String readReleaseNotesEN(){ 32 | script.readFile releaseNotesFileNameEN 33 | } 34 | 35 | /** 36 | * Extracts the changes for a given version out of the german release notes. 37 | * 38 | * @param releaseVersion The version to get the changes for. 39 | * @return Returns the changes as String. 40 | */ 41 | String changesForDEVersion(String releaseVersion) { 42 | def releaseNotesDE = readReleaseNotesDE() 43 | def start = changesStartIndex(releaseNotesDE, releaseVersion) 44 | def end = changesEndIndex(releaseNotesDE, start) 45 | return escapeForJson(releaseNotesDE.substring(start, end).trim()) 46 | } 47 | 48 | /** 49 | * Extracts the changes for a given version out of the english release notes. 50 | * 51 | * @param releaseVersion The version to get the changes for. 52 | * @return Returns the changes as String. 53 | */ 54 | String changesForENVersion(String releaseVersion) { 55 | def releaseNotesEN = readReleaseNotesEN() 56 | def start = changesStartIndex(releaseNotesEN, releaseVersion) 57 | def end = changesEndIndex(releaseNotesEN, start) 58 | return escapeForJson(releaseNotesEN.substring(start, end).trim()) 59 | } 60 | 61 | /** 62 | * Removes characters from a string that could break the json struct when passing the string as json value. 63 | * 64 | * @param string The string to format. 65 | * @return Returns the formatted string. 66 | */ 67 | private static String escapeForJson(String string) { 68 | return string 69 | .replace("\"", "") 70 | .replace("'", "") 71 | .replace("\\", "") 72 | .replace("\n", "\\n") 73 | } 74 | 75 | /** 76 | * Returns the start index of changes of a specific release version in the release notes. 77 | * 78 | * @param releaseVersion The version to get the changes for. 79 | * @return Returns the index in the release notes string where the changes start. 80 | */ 81 | private static int changesStartIndex(String releaseNotes, String releaseVersion) { 82 | def index = releaseNotes.indexOf("## [${releaseVersion}]") 83 | if (index == -1){ 84 | throw new IllegalArgumentException("The desired version '${releaseVersion}' could not be found in the release notes.") 85 | } 86 | def offset = releaseNotes.substring(index).indexOf("\n") 87 | return index + offset 88 | } 89 | 90 | /** 91 | * Returns the end index of changes of a specific release version in the release notes. 92 | * 93 | * @param start The start index of the changes for this version. 94 | * @return Returns the index in the release notes string where the changes end. 95 | */ 96 | private static int changesEndIndex(String releaseNotes, int start) { 97 | def releaseNotesAfterStartIndex = releaseNotes.substring(start) 98 | def index = releaseNotesAfterStartIndex.indexOf("\n## [") 99 | if (index == -1) { 100 | return releaseNotes.length() 101 | } 102 | return index + start 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/com/cloudogu/ces/cesbuildlib/SCMManager.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | 3 | import groovy.json.JsonOutput 4 | 5 | class SCMManager implements Serializable { 6 | 7 | private script 8 | protected HttpClient http 9 | protected String baseUrl 10 | 11 | SCMManager(script, String baseUrl, String credentials) { 12 | this.script = script 13 | this.baseUrl = baseUrl 14 | this.http = new HttpClient(script, credentials) 15 | } 16 | 17 | String searchPullRequestIdByTitle(String repository, String title) { 18 | def pullRequest 19 | for (Map pr : getPullRequests(repository)) { 20 | if (pr.title == title) { 21 | pullRequest = pr 22 | } 23 | } 24 | 25 | if (pullRequest) { 26 | return pullRequest.id.toString() 27 | } else { 28 | return '' 29 | } 30 | } 31 | 32 | String createPullRequest(String repository, String source, String target, String title, String description) { 33 | def dataJson = JsonOutput.toJson([ 34 | title : title, 35 | description: description, 36 | source : source, 37 | target : target 38 | ]) 39 | def httpResponse = http.post(pullRequestEndpoint(repository), 'application/vnd.scmm-pullRequest+json;v=2', dataJson) 40 | 41 | script.echo "Creating pull request yields httpCode: ${httpResponse.httpCode}" 42 | if (httpResponse.httpCode != "201") { 43 | script.echo 'WARNING: Http status code indicates, that pull request was not created' 44 | return '' 45 | } 46 | 47 | // example: "location: https://some/pr/42" - extract id 48 | // in some cases the location key might be upper case so we check for that 49 | if (httpResponse.headers.containsKey("location")) { 50 | return httpResponse.headers.location.split("/")[-1] 51 | } else { 52 | return httpResponse.headers.Location.split("/")[-1] 53 | } 54 | } 55 | 56 | boolean updatePullRequest(String repository, String pullRequestId, String title, String description) { 57 | // In order to update the description put in also the title. Otherwise the title is overwritten with an empty string. 58 | def dataJson = JsonOutput.toJson([ 59 | title : title, 60 | description: description 61 | ]) 62 | 63 | def httpResponse = http.put("${pullRequestEndpoint(repository)}/${pullRequestId}", 'application/vnd.scmm-pullRequest+json;v=2', dataJson) 64 | 65 | script.echo "Pull request update yields http_code: ${httpResponse.httpCode}" 66 | if (httpResponse.httpCode != "204") { 67 | script.echo 'WARNING: Http status code indicates, that the pull request was not updated' 68 | return false 69 | } 70 | return true 71 | } 72 | 73 | String createOrUpdatePullRequest(String repository, String source, String target, String title, String description) { 74 | 75 | def pullRequestId = searchPullRequestIdByTitle(repository, title) 76 | 77 | if(pullRequestId.isEmpty()) { 78 | return createPullRequest(repository, source, target, title, description) 79 | } else { 80 | if(updatePullRequest(repository, pullRequestId, title, description)) { 81 | return pullRequestId 82 | } else { 83 | return '' 84 | } 85 | } 86 | } 87 | 88 | boolean addComment(String repository, String pullRequestId, String comment) { 89 | def dataJson = JsonOutput.toJson([ 90 | comment: comment 91 | ]) 92 | def httpResponse = http.post("${pullRequestEndpoint(repository)}/${pullRequestId}/comments", 'application/json', dataJson) 93 | 94 | script.echo "Adding comment yields http_code: ${httpResponse.httpCode}" 95 | if (httpResponse.httpCode != "201") { 96 | script.echo 'WARNING: Http status code indicates, that the comment was not added' 97 | return false 98 | } 99 | return true 100 | } 101 | 102 | protected String pullRequestEndpoint(String repository) { 103 | "${this.baseUrl}/api/v2/pull-requests/${repository}" 104 | } 105 | 106 | /** 107 | * @return SCM-Manager's representation of PRs. Basically a list of PR objects. 108 | * properties (as of SCM-Manager 2.12.0) 109 | * * id 110 | * * author 111 | * * id 112 | * * displayName 113 | * * mail 114 | * * source - the source branch 115 | * * target - the target branch 116 | * * title 117 | * * description (branch) 118 | * * creationDate: (e.g. "2020-10-09T15:08:11.459Z") 119 | * * lastModified" 120 | * * status, e.g. "OPEN" 121 | * * reviewer (list) 122 | * * tasks 123 | * * todo (number) 124 | * * done (number 125 | * * tasks sourceRevision 126 | * * targetRevision 127 | * * targetRevision 128 | * * markedAsReviewed (list) 129 | * * emergencyMerged 130 | * * ignoredMergeObstacles 131 | */ 132 | protected List getPullRequests(String repository) { 133 | def httpResponse = http.get(pullRequestEndpoint(repository), 'application/vnd.scmm-pullRequestCollection+json;v=2') 134 | 135 | script.echo "Getting all pull requests yields httpCode: ${httpResponse.httpCode}" 136 | if (httpResponse.httpCode != "200") { 137 | script.echo 'WARNING: Http status code indicates, that the pull requests could not be retrieved' 138 | return [] 139 | } 140 | 141 | def prsAsJson = script.readJSON text: httpResponse.body 142 | return prsAsJson._embedded.pullRequests 143 | } 144 | 145 | } 146 | -------------------------------------------------------------------------------- /src/com/cloudogu/ces/cesbuildlib/Sh.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | 3 | /** 4 | * An abstraction for the {@code sh} step 5 | */ 6 | class Sh implements Serializable { 7 | private script 8 | 9 | Sh(script) { 10 | this.script = script 11 | } 12 | 13 | /** 14 | * @return the trimmed stdout of the shell call. Most likely never {@code null} 15 | */ 16 | String returnStdOut(args) { 17 | return script.sh(returnStdout: true, script: args) 18 | // Trim to remove trailing line breaks, which result in unwanted behavior in Jenkinsfiles: 19 | // E.g. when using output in other sh() calls leading to executing the sh command after the line breaks, 20 | // possibly discarding additional arguments 21 | ?.trim() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/com/cloudogu/ces/cesbuildlib/SonarCloud.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | 3 | /** 4 | * Abstraction for SonarCloud. More or less a special SonarQube instance. 5 | * 6 | * The integration into GitHub, BitBucket and such is done via 3rd party integrations into those tools. 7 | * Normal SonarQube uses e.g. the GitHub plugin. 8 | */ 9 | class SonarCloud extends SonarQube { 10 | 11 | SonarCloud(script, Map config) { 12 | super(script, config) 13 | 14 | this.isUsingBranchPlugin = true 15 | } 16 | 17 | void initMavenForPullRequest(Maven mvn) { 18 | script.echo "SonarQube analyzing PullRequest ${script.env.CHANGE_ID}." 19 | 20 | def git = new Git(script) 21 | String repoUrl = git.repositoryUrl 22 | String repoName = git.repositoryName 23 | 24 | mvn.additionalArgs += 25 | // an additional space is required in the case of pre existing additionalArgs 26 | " " + 27 | "-Dsonar.pullrequest.base=${script.env.CHANGE_TARGET} " + 28 | "-Dsonar.pullrequest.branch=${script.env.CHANGE_BRANCH} " + 29 | "-Dsonar.pullrequest.key=${script.env.CHANGE_ID} " 30 | 31 | if (repoUrl.contains('github.com')) { 32 | // See https://sonarcloud.io/documentation/integrations/github 33 | mvn.additionalArgs += 34 | "-Dsonar.pullrequest.provider=GitHub " + 35 | "-Dsonar.pullrequest.github.repository=${repoName} " 36 | } else if (repoUrl.contains('bitbucket.org')) { 37 | String owner = repoName.split('/')[0] 38 | String plainRepoName = repoName.split('/')[1] 39 | 40 | mvn.additionalArgs += 41 | "-Dsonar.pullrequest.provider=bitbucketcloud " + 42 | "-Dsonar.pullrequest.bitbucketcloud.owner=${owner} " + 43 | "-Dsonar.pullrequest.bitbucketcloud.repository=${plainRepoName} " 44 | } else { 45 | script.error "Unknown sonar.pullrequest.provider. None matching for repo URL: ${repoUrl}" 46 | } 47 | } 48 | 49 | @Override 50 | protected void initMaven(Maven mvn) { 51 | if (script.isPullRequest()) { 52 | initMavenForPullRequest(mvn) 53 | } else { 54 | initMavenForRegularAnalysis(mvn) 55 | } 56 | 57 | if (config['sonarOrganization']) { 58 | mvn.additionalArgs += " -Dsonar.organization=${config['sonarOrganization']} " 59 | } 60 | } 61 | 62 | @Override 63 | protected void validateMandatoryFieldsWithoutSonarQubeEnv() { 64 | super.validateMandatoryFieldsWithoutSonarQubeEnv() 65 | // When using SonarQube env, the sonarOrganization might be set there as SONAR_EXTRA_PROPS 66 | validateFieldPresent(config, 'sonarOrganization') 67 | 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/com/cloudogu/ces/cesbuildlib/SonarQube.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | 3 | /** 4 | * Abstraction for SonarQube. Use in conjunction with the SonarQube plugin for Jenkins: 5 | * 6 | * https://wiki.jenkins.io/display/JENKINS/SonarQube+plugin and 7 | * https://docs.sonarqube.org/display/SCAN/Analyzing+with+SonarQube+Scanner+for+Jenkins#AnalyzingwithSonarQubeScannerforJenkins-AnalyzinginaJenkinspipeline 8 | */ 9 | class SonarQube implements Serializable { 10 | protected script 11 | 12 | boolean isIgnoringBranches = false 13 | int timeoutInMinutes = 2 14 | // If enabled uses the branch plugin, available for developer edition and above 15 | protected boolean isUsingBranchPlugin = false 16 | protected Map config 17 | 18 | @Deprecated 19 | SonarQube(script, String sonarQubeEnv) { 20 | this(script, [sonarQubeEnv: sonarQubeEnv]) 21 | } 22 | 23 | SonarQube(script, Map config) { 24 | this.script = script 25 | this.config = config 26 | } 27 | 28 | /** 29 | * Executes a SonarQube analysis using maven. 30 | * 31 | * The current branch name is added to the SonarQube project name. Paid versions of GitHub offer the branch plugin. 32 | * If available set {@link #isUsingBranchPlugin} to {@code true}. 33 | * 34 | */ 35 | void analyzeWith(Maven mvn) { 36 | initMaven(mvn) 37 | determineAnalysisStrategy().executeWith(mvn) 38 | } 39 | 40 | /** 41 | * Blocks until a webhook is called on Jenkins that signalizes finished SonarQube QualityGate evaluation. 42 | * 43 | * It's good practice to execute this outside of a build executor/node, in order not to block it while waiting. 44 | * However, if the webhook is set in most cases the result will be returned in a couple of seconds. 45 | * 46 | * If there is no webhook or SonarQube does not respond within 2 minutes, the build fails. 47 | * So make sure to set up a webhook in SonarQube global administration or per project to 48 | * {@code /sonarqube-webhook/}. 49 | * See https://docs.sonarqube.org/display/SCAN/Analyzing+with+SonarQube+Scanner+for+Jenkins 50 | * 51 | * If this build is a Pull Request, this method will not wait, because usually PRs are analyzed locally. 52 | * See https://docs.sonarqube.org/display/PLUG/GitHub+Plugin 53 | * 54 | * Will only work after {@link #analyzeWith(com.cloudogu.ces.cesbuildlib.Maven)} is called. This is how the 55 | * SonarQube plugin for Jenkins works... 56 | * 57 | * @return {@code true} if the result of the quality is 'OK' or if a Pull Request is built. Otherwise {@code false}. 58 | */ 59 | boolean waitForQualityGateWebhookToBeCalled() { 60 | if (!config['sonarQubeEnv']) { 61 | script.error "waitForQualityGate will only work when using the SonarQube Plugin for Jenkins, via the 'sonarQubeEnv' parameter" 62 | } 63 | 64 | return doWaitForQualityGateWebhookToBeCalled() 65 | } 66 | 67 | protected boolean doWaitForQualityGateWebhookToBeCalled() { 68 | script.timeout(time: timeoutInMinutes, unit: 'MINUTES') { // Needed when there is no webhook for example 69 | def qGate = script.waitForQualityGate() 70 | script.echo "SonarQube Quality Gate status: ${qGate.status}" 71 | if (qGate.status != 'OK') { 72 | return false 73 | } 74 | return true 75 | } 76 | } 77 | 78 | /** 79 | * SonarQube can update the Build status of a commit within a PullRequest at GitHub. 80 | * 81 | * To do so, it needs a GitHub access token, which should be passed via a Jenkins credential. 82 | * 83 | * See https://docs.sonarqube.org/display/PLUG/GitHub+Plugin 84 | */ 85 | @Deprecated 86 | void updateAnalysisResultOfPullRequestsToGitHub(String gitHubCredentials) { 87 | script.echo "WARNING: Decorating PRs was deprecated in SonarQube. See https://docs.sonarqube.org/display/PLUG/GitHub+Plugin" 88 | // As this is a deprecated method, the signature can't be changed. Still, the parameter is not needed. Ignore warnings. 89 | // @SuppressWarnings(["grvy:org.codenarc.rule.unused.UnusedMethodParameterRule"]) does not seem to wirk 90 | gitHubCredentials 91 | } 92 | 93 | protected void initMaven(Maven mvn) { 94 | initMavenForRegularAnalysis(mvn) 95 | } 96 | 97 | protected void initMavenForRegularAnalysis(Maven mvn) { 98 | script.echo "SonarQube analyzing branch ${script.env.BRANCH_NAME}" 99 | 100 | if (isIgnoringBranches) { 101 | return 102 | } 103 | def artifactId = mvn.artifactId.trim() 104 | if (isUsingBranchPlugin) { 105 | mvn.additionalArgs += " -Dsonar.branch.name=${script.env.BRANCH_NAME} " 106 | 107 | String integrationBranch = determineIntegrationBranch() 108 | if (!integrationBranch.equals(script.env.BRANCH_NAME)) { 109 | String targetBranch = script.env.CHANGE_TARGET ? script.env.CHANGE_TARGET : integrationBranch 110 | // Avoid exception "The main branch must not have a target" on master branch 111 | mvn.additionalArgs += " -Dsonar.branch.target=${targetBranch} " 112 | } 113 | // Use -Dsonar.branch.name with following plugin: 114 | // https://github.com/mc1arke/sonarqube-community-branch-plugin 115 | // Some examples for Env Vars when building PRs. 116 | // BRANCH_NAME=PR-26 117 | // CHANGE_BRANCH=feature/simplify_git_push 118 | // CHANGE_TARGET=develop 119 | } else if (script.env.CHANGE_TARGET) { 120 | mvn.additionalArgs += "-Dsonar.projectKey=${replaceCharactersNotAllowedInProjectKey(artifactId)} " + 121 | " -Dsonar.projectName=${artifactId} " + 122 | " -Dsonar.pullrequest.key=${script.env.CHANGE_ID} " + 123 | " -Dsonar.pullrequest.branch=${script.env.CHANGE_BRANCH} " + 124 | " -Dsonar.pullrequest.base=${script.env.CHANGE_TARGET} " 125 | } else if (script.env.BRANCH_NAME) { 126 | mvn.additionalArgs += "-Dsonar.projectKey=${replaceCharactersNotAllowedInProjectKey(artifactId)} " + 127 | " -Dsonar.projectName=${artifactId} " + 128 | " -Dsonar.branch.name=${script.env.BRANCH_NAME} " 129 | } 130 | } 131 | 132 | protected static String replaceCharactersNotAllowedInProjectKey(String potentialProjectKey) { 133 | return potentialProjectKey.replaceAll("[^a-zA-Z0-9-_.:]", "_") 134 | } 135 | 136 | protected String determineIntegrationBranch() { 137 | if (config['integrationBranch']) { 138 | return config['integrationBranch'] 139 | } else { 140 | return 'master' 141 | } 142 | } 143 | 144 | protected void validateFieldPresent(Map config, String fieldKey) { 145 | if (!config[fieldKey]) { 146 | script.error "Missing required '${fieldKey}' parameter." 147 | } 148 | } 149 | 150 | protected AnalysisStrategy determineAnalysisStrategy() { 151 | // If private may fail for SonarCloud with: 152 | // No signature of method: com.cloudogu.ces.cesbuildlib.SonarCloud.determineAnalysisStrategy() is applicable for argument types: () values: [] 153 | 154 | if (config['sonarQubeEnv']) { 155 | return new EnvAnalysisStrategy(script, config['sonarQubeEnv']) 156 | 157 | } else if (config['token']) { 158 | validateMandatoryFieldsWithoutSonarQubeEnv() 159 | return new TokenAnalysisStrategy(script, config['token'], config['sonarHostUrl']) 160 | 161 | } else if (config['usernamePassword']) { 162 | validateMandatoryFieldsWithoutSonarQubeEnv() 163 | return new UsernamePasswordAnalysisStrategy(script, config['usernamePassword'], config['sonarHostUrl']) 164 | 165 | } else { 166 | script.error "Requires either 'sonarQubeEnv', 'token' or 'usernamePassword' parameter." 167 | } 168 | } 169 | 170 | protected void validateMandatoryFieldsWithoutSonarQubeEnv() { 171 | validateFieldPresent(config, 'sonarHostUrl') 172 | } 173 | 174 | private static abstract class AnalysisStrategy { 175 | 176 | def script 177 | 178 | AnalysisStrategy(script) { 179 | this.script = script 180 | } 181 | 182 | abstract executeWith(Maven mvn) 183 | 184 | protected analyzeWith(Maven mvn, String sonarMavenGoal, String sonarHostUrl, String sonarLogin, 185 | String sonarExtraProps = '') { 186 | 187 | mvn "${sonarMavenGoal} -Dsonar.host.url=${sonarHostUrl} -Dsonar.login=${sonarLogin} ${sonarExtraProps}" 188 | } 189 | } 190 | 191 | private static class EnvAnalysisStrategy extends AnalysisStrategy { 192 | 193 | String sonarQubeEnv 194 | 195 | EnvAnalysisStrategy(script, String sonarQubeEnv) { 196 | super(script) 197 | this.sonarQubeEnv = sonarQubeEnv 198 | } 199 | 200 | def executeWith(Maven mvn) { 201 | script.withSonarQubeEnv(sonarQubeEnv) { 202 | String sonarExtraProps = script.env.SONAR_EXTRA_PROPS 203 | if (sonarExtraProps == null) { 204 | sonarExtraProps = "" 205 | } 206 | 207 | analyzeWith(mvn, script.env.SONAR_MAVEN_GOAL, script.env.SONAR_HOST_URL, script.env.SONAR_AUTH_TOKEN, 208 | sonarExtraProps) 209 | } 210 | } 211 | } 212 | 213 | private static class TokenAnalysisStrategy extends AnalysisStrategy { 214 | 215 | String token 216 | String host 217 | 218 | TokenAnalysisStrategy(script, String tokenCredential, String host) { 219 | super(script) 220 | this.token = tokenCredential 221 | this.host = host 222 | } 223 | 224 | def executeWith(Maven mvn) { 225 | script.withCredentials([script.string(credentialsId: token, variable: 'SONAR_AUTH_TOKEN')]) { 226 | analyzeWith(mvn, 'sonar:sonar', host, script.env.SONAR_AUTH_TOKEN) 227 | } 228 | } 229 | } 230 | 231 | private static class UsernamePasswordAnalysisStrategy extends AnalysisStrategy { 232 | 233 | String usernameAndPasswordCredential 234 | String host 235 | 236 | UsernamePasswordAnalysisStrategy(script, String usernameAndPasswordCredential, String host) { 237 | super(script) 238 | this.usernameAndPasswordCredential = usernameAndPasswordCredential 239 | this.host = host 240 | } 241 | 242 | def executeWith(Maven mvn) { 243 | script.withCredentials([script.usernamePassword(credentialsId: usernameAndPasswordCredential, 244 | passwordVariable: 'PASSWORD', usernameVariable: 'USERNAME')]) { 245 | analyzeWith(mvn, 'sonar:sonar', host, script.env.USERNAME, 246 | "-Dsonar.password=${script.env.PASSWORD} ") 247 | } 248 | } 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /src/com/cloudogu/ces/cesbuildlib/Trivy.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | 3 | import com.cloudbees.groovy.cps.NonCPS 4 | 5 | class Trivy implements Serializable { 6 | static final String DEFAULT_TRIVY_VERSION = "0.57.1" 7 | static final String DEFAULT_TRIVY_IMAGE = "aquasec/trivy" 8 | private script 9 | private Docker docker 10 | private String trivyVersion 11 | private String trivyImage 12 | private String trivyDirectory = "trivy" 13 | 14 | // Do not use DEFAULT_TRIVY_VERSION or DEFAULT_TRIVY_IMAGE here, as it will lead to java.lang.VerifyError 15 | Trivy(script, String trivyVersion = "0.57.1", String trivyImage = "aquasec/trivy", Docker docker = new Docker(script)) { 16 | this.script = script 17 | this.trivyVersion = trivyVersion 18 | this.trivyImage = trivyImage 19 | this.docker = docker 20 | } 21 | 22 | /** 23 | * Scans an image for vulnerabilities. 24 | * Notes: 25 | * - Use a .trivyignore file for allowed CVEs 26 | * - This function will generate a JSON formatted report file which can be converted to other formats via saveFormattedTrivyReport() 27 | * 28 | * @param imageName The name of the image to be scanned; may include a version tag 29 | * @param severityLevel The vulnerability level to scan. Can be a member of TrivySeverityLevel or a custom String (e.g. 'CRITICAL,LOW') 30 | * @param strategy The strategy to follow after the scan. Should the build become unstable or failed? Or Should any vulnerability be ignored? (@see TrivyScanStrategy) 31 | * @param additionalFlags Additional Trivy command flags 32 | * @param trivyReportFile Location of Trivy report file. Should be set individually when scanning multiple images in the same pipeline 33 | * @return Returns true if the scan was ok (no vulnerability found); returns false if any vulnerability was found 34 | */ 35 | boolean scanImage( 36 | String imageName, 37 | String severityLevel = TrivySeverityLevel.CRITICAL, 38 | String strategy = TrivyScanStrategy.UNSTABLE, 39 | // Avoid rate limits of default Trivy database source 40 | String additionalFlags = "--db-repository public.ecr.aws/aquasecurity/trivy-db --java-db-repository public.ecr.aws/aquasecurity/trivy-java-db", 41 | String trivyReportFile = "trivy/trivyReport.json" 42 | ) { 43 | Integer exitCode = docker.image("${trivyImage}:${trivyVersion}") 44 | .mountJenkinsUser() 45 | .mountDockerSocket() 46 | .inside("-v ${script.env.WORKSPACE}/.trivy/.cache:/root/.cache/") { 47 | // Write result to $trivyReportFile in json format (--format json), which can be converted in the saveFormattedTrivyReport function 48 | // Exit with exit code 10 if vulnerabilities are found or OS is so old that Trivy has no records for it anymore 49 | script.sh("mkdir -p " + trivyDirectory) 50 | script.sh(script: "trivy image --exit-code 10 --exit-on-eol 10 --format ${TrivyScanFormat.JSON} -o ${trivyReportFile} --severity ${severityLevel} ${additionalFlags} ${imageName}", returnStatus: true) 51 | } 52 | switch (exitCode) { 53 | case 0: 54 | // Everything all right, no vulnerabilities 55 | return true 56 | case 10: 57 | // Found vulnerabilities 58 | // Set build status according to strategy 59 | switch (strategy) { 60 | case TrivyScanStrategy.IGNORE: 61 | break 62 | case TrivyScanStrategy.UNSTABLE: 63 | script.archiveArtifacts artifacts: "${trivyReportFile}", allowEmptyArchive: true 64 | script.unstable("Trivy has found vulnerabilities in image " + imageName + ". See " + trivyReportFile) 65 | break 66 | case TrivyScanStrategy.FAIL: 67 | script.archiveArtifacts artifacts: "${trivyReportFile}", allowEmptyArchive: true 68 | script.error("Trivy has found vulnerabilities in image " + imageName + ". See " + trivyReportFile) 69 | break 70 | } 71 | return false 72 | default: 73 | script.error("Error during trivy scan; exit code: " + exitCode) 74 | } 75 | } 76 | 77 | /** 78 | * Scans a dogu image for vulnerabilities. 79 | * Notes: 80 | * - Use a .trivyignore file for allowed CVEs 81 | * - This function will generate a JSON formatted report file which can be converted to other formats via saveFormattedTrivyReport() 82 | * 83 | * @param doguDir The directory the dogu code (dogu.json) is located 84 | * @param severityLevel The vulnerability level to scan. Can be a member of TrivySeverityLevel or a custom String (e.g. 'CRITICAL,LOW') 85 | * @param strategy The strategy to follow after the scan. Should the build become unstable or failed? Or Should any vulnerability be ignored? (@see TrivyScanStrategy) 86 | * @param additionalFlags Additional Trivy command flags 87 | * @param trivyReportFile Location of Trivy report file. Should be set individually when scanning multiple images in the same pipeline 88 | * @return Returns true if the scan was ok (no vulnerability found); returns false if any vulnerability was found 89 | */ 90 | boolean scanDogu( 91 | String doguDir = ".", 92 | String severityLevel = TrivySeverityLevel.CRITICAL, 93 | String strategy = TrivyScanStrategy.UNSTABLE, 94 | // Avoid rate limits of default Trivy database source 95 | String additionalFlags = "--db-repository public.ecr.aws/aquasecurity/trivy-db --java-db-repository public.ecr.aws/aquasecurity/trivy-java-db", 96 | String trivyReportFile = "trivy/trivyReport.json" 97 | ) { 98 | String image = script.sh(script: "jq .Image ${doguDir}/dogu.json", returnStdout: true).trim() 99 | String version = script.sh(script: "jq .Version ${doguDir}/dogu.json", returnStdout: true).trim() 100 | return scanImage(image + ":" + version, severityLevel, strategy, additionalFlags, trivyReportFile) 101 | } 102 | 103 | /** 104 | * Save the Trivy scan results as a file with a specific format 105 | * 106 | * @param format The format of the output file {@link TrivyScanFormat}. 107 | * You may enter supported formats (sarif, cyclonedx, spdx, spdx-json, github, cosign-vuln, table or json) 108 | * or your own template ("template --template @FILENAME"). 109 | * If you want to convert to a format that requires a list of packages, such as SBOM, you need to add 110 | * the `--list-all-pkgs` flag to the {@link Trivy#scanImage} call, when outputting in JSON 111 | * (See trivy docs). 112 | * @param severity Severities of security issues to be added (taken from UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL) 113 | * @param formattedTrivyReportFilename The file name your report files should get, with file extension. E.g. "ubuntu24report.html" 114 | * @param trivyReportFile The "trivyReportFile" parameter you used in the "scanImage" function, if it was set 115 | */ 116 | void saveFormattedTrivyReport(String format = TrivyScanFormat.HTML, 117 | String severity = TrivySeverityLevel.ALL, 118 | String formattedTrivyReportFilename = null, 119 | String trivyReportFile = "trivy/trivyReport.json") { 120 | 121 | // set default report filename depending on the chosen format 122 | if (formattedTrivyReportFilename == null) { 123 | formattedTrivyReportFilename = "formattedTrivyReport" + getFileExtension(format) 124 | } 125 | 126 | String formatString 127 | switch (format) { 128 | // TrivyScanFormat.JSON and TrivyScanFormat.TABLE are handled by the default case, too 129 | case TrivyScanFormat.HTML: 130 | formatString = "template --template \"@/contrib/html.tpl\"" 131 | break 132 | default: 133 | // You may enter supported formats (sarif, cyclonedx, spdx, spdx-json, github, cosign-vuln, table or json) 134 | // or your own template ("template --template @FILENAME") 135 | List trivyFormats = ['sarif', 'cyclonedx', 'spdx', 'spdx-json', 'github', 'cosign-vuln', 'table', 'json'] 136 | // Check if "format" is a custom template from a file 137 | boolean isTemplateFormat = format ==~ /^template --template @\S+$/ 138 | // Check if "format" is one of the trivyFormats or a template 139 | if (trivyFormats.any { (format == it) } || isTemplateFormat) { 140 | formatString = format 141 | break 142 | } else { 143 | script.error("This format did not match the supported formats: " + format) 144 | return 145 | } 146 | } 147 | // Validate severity input parameter to prevent injection of additional parameters 148 | if (!severity.split(',').every { it.trim() in ["UNKNOWN", "LOW", "MEDIUM", "HIGH", "CRITICAL"] }) { 149 | script.error("The severity levels provided ($severity) do not match the applicable levels (UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL).") 150 | } 151 | 152 | docker.image("${trivyImage}:${trivyVersion}") 153 | .inside("-v ${script.env.WORKSPACE}/.trivy/.cache:/root/.cache/") { 154 | script.sh(script: "trivy convert --format ${formatString} --severity ${severity} --output ${trivyDirectory}/${formattedTrivyReportFilename} ${trivyReportFile}") 155 | } 156 | script.archiveArtifacts artifacts: "${trivyDirectory}/*", allowEmptyArchive: true 157 | } 158 | 159 | private static String getFileExtension(String format) { 160 | return TrivyScanFormat.isStandardScanFormat(format) ? "." + format : ".txt" 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/com/cloudogu/ces/cesbuildlib/TrivyScanException.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | 3 | /** 4 | * This exception is thrown whenever a vulnerability was found. 5 | */ 6 | class TrivyScanException extends RuntimeException { 7 | TrivyScanException(String error) { 8 | super(error) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/com/cloudogu/ces/cesbuildlib/TrivyScanFormat.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | 3 | /** 4 | * Defines the output format for the trivy report. 5 | */ 6 | class TrivyScanFormat { 7 | /** 8 | * Output as HTML file. 9 | */ 10 | static String HTML = "html" 11 | 12 | /** 13 | * Output as JSON file. 14 | */ 15 | static String JSON = "json" 16 | 17 | /** 18 | * Output as table. 19 | */ 20 | static String TABLE = "table" 21 | 22 | static boolean isStandardScanFormat(String format) { 23 | return format == HTML || format == JSON || format == TABLE 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/com/cloudogu/ces/cesbuildlib/TrivyScanStrategy.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | 3 | class TrivyScanStrategy { 4 | /** 5 | * Strategy: Fail if any vulnerability was found. 6 | */ 7 | static String FAIL = "fail" 8 | 9 | /** 10 | * Strategy: Make build unstable if any vulnerability was found. 11 | */ 12 | static String UNSTABLE = "unstable" 13 | 14 | /** 15 | * Strategy: Ignore any found vulnerability. 16 | */ 17 | static String IGNORE = "ignore" 18 | } 19 | -------------------------------------------------------------------------------- /src/com/cloudogu/ces/cesbuildlib/TrivySeverityLevel.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | 3 | /** 4 | * Defines aggregated vulnerability levels 5 | */ 6 | class TrivySeverityLevel { 7 | /** 8 | * Only critical vulnerabilities. 9 | */ 10 | static String CRITICAL = "CRITICAL" 11 | 12 | /** 13 | * High or critical vulnerabilities. 14 | */ 15 | static String HIGH_AND_ABOVE = "CRITICAL,HIGH" 16 | 17 | /** 18 | * Medium or higher vulnerabilities. 19 | */ 20 | static String MEDIUM_AND_ABOVE = "CRITICAL,HIGH,MEDIUM" 21 | 22 | /** 23 | * All vulnerabilities. 24 | */ 25 | static String ALL = "UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL" 26 | } 27 | -------------------------------------------------------------------------------- /test/com/cloudogu/ces/cesbuildlib/BatsTest.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | 3 | import org.junit.jupiter.api.Test 4 | import org.mockito.invocation.InvocationOnMock 5 | import org.mockito.stubbing.Answer 6 | 7 | import static org.assertj.core.api.Assertions.assertThat 8 | import static org.mockito.ArgumentMatchers.any 9 | import static org.mockito.ArgumentMatchers.anyString 10 | import static org.mockito.ArgumentMatchers.eq 11 | import static org.mockito.Mockito.mock 12 | import static org.mockito.Mockito.verify 13 | import static org.mockito.Mockito.when 14 | 15 | import static org.junit.jupiter.api.Assertions.* 16 | 17 | class BatsTest { 18 | 19 | ScriptMock scriptMock = new ScriptMock() 20 | 21 | @Test 22 | void test_Constructor() { 23 | // given 24 | Docker dockerMock = mock(Docker.class) 25 | Docker.Image imageMock = mock(Docker.Image.class) 26 | when(dockerMock.build(anyString(), anyString())).thenReturn(imageMock) 27 | 28 | // when 29 | Bats bats = new Bats(scriptMock, dockerMock) 30 | 31 | // then 32 | assertNotNull(bats) 33 | } 34 | 35 | @Test 36 | void test_checkAndExecuteTests() { 37 | // given 38 | Docker dockerMock = mock(Docker.class) 39 | Docker.Image imageMock = mock(Docker.Image.class) 40 | when(dockerMock.build("cloudogu/bats:1.2.1", "--build-arg=BATS_BASE_IMAGE=bats/bats --build-arg=BATS_TAG=1.2.1 ./build/make/bats")).thenReturn(imageMock) 41 | when(imageMock.inside(anyString(), any())).thenAnswer(new Answer() { 42 | @Override 43 | Object answer(InvocationOnMock invocation) throws Throwable { 44 | Closure closure = invocation.getArgument(1) 45 | closure.call() 46 | } 47 | }) 48 | 49 | Bats bats = new Bats(scriptMock, dockerMock) 50 | 51 | // when 52 | bats.checkAndExecuteTests() 53 | 54 | // then 55 | assertThat(scriptMock.actualEcho[0].trim()).contains("Executing bats tests with config:") 56 | assertThat(scriptMock.actualEcho[1].trim()).contains("[bats_base_image:bats/bats, bats_custom_image:cloudogu/bats, bats_tag:1.2.1]") 57 | 58 | verify(dockerMock).build("cloudogu/bats:1.2.1", "--build-arg=BATS_BASE_IMAGE=bats/bats --build-arg=BATS_TAG=1.2.1 ./build/make/bats") 59 | verify(imageMock).inside(eq("--entrypoint='' -v :/workspace -v /testdir:/usr/share/webapps"), any()) 60 | 61 | assertEquals("true", scriptMock.actualJUnitFlags["allowEmptyResults"].toString()) 62 | assertEquals("target/shell_test_reports/*.xml", scriptMock.actualJUnitFlags["testResults"].toString()) 63 | } 64 | 65 | @Test 66 | void test_checkAndExecuteTests_with_custom_config() { 67 | // given 68 | def defaultSetupConfig = [ 69 | bats_custom_image: "myimage/bats", 70 | bats_tag : "1.4.1" 71 | ] 72 | 73 | Docker dockerMock = mock(Docker.class) 74 | Docker.Image imageMock = mock(Docker.Image.class) 75 | when(dockerMock.build("myimage/bats:1.4.1", "--build-arg=BATS_BASE_IMAGE=bats/bats --build-arg=BATS_TAG=1.4.1 ./build/make/bats")).thenReturn(imageMock) 76 | when(imageMock.inside(anyString(), any())).thenAnswer(new Answer() { 77 | @Override 78 | Object answer(InvocationOnMock invocation) throws Throwable { 79 | Closure closure = invocation.getArgument(1) 80 | closure.call() 81 | } 82 | }) 83 | 84 | Bats bats = new Bats(scriptMock, dockerMock) 85 | 86 | // when 87 | bats.checkAndExecuteTests(defaultSetupConfig) 88 | 89 | // then 90 | assertThat(scriptMock.actualEcho[0].trim()).contains("Executing bats tests with config:") 91 | assertThat(scriptMock.actualEcho[1].trim()).contains("[bats_base_image:bats/bats, bats_custom_image:myimage/bats, bats_tag:1.4.1]") 92 | 93 | verify(dockerMock).build("myimage/bats:1.4.1", "--build-arg=BATS_BASE_IMAGE=bats/bats --build-arg=BATS_TAG=1.4.1 ./build/make/bats") 94 | verify(imageMock).inside(eq("--entrypoint='' -v :/workspace -v /testdir:/usr/share/webapps"), any()) 95 | 96 | assertEquals("true", scriptMock.actualJUnitFlags["allowEmptyResults"].toString()) 97 | assertEquals("target/shell_test_reports/*.xml", scriptMock.actualJUnitFlags["testResults"].toString()) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /test/com/cloudogu/ces/cesbuildlib/ChangelogTest.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | 3 | import org.junit.jupiter.api.Test 4 | import static org.junit.jupiter.api.Assertions.* 5 | import static groovy.test.GroovyAssert.shouldFail 6 | 7 | class ChangelogTest { 8 | def validChangelog = 9 | ''' 10 | ## [Unreleased] 11 | ### Changed 12 | - Some other things 13 | 14 | ## [v2.0.0] - 2020-01-01 15 | ### Changed 16 | - Everything! 17 | 18 | ## [v1.0.0] - 2020-01-01 19 | ### Changed 20 | - Something 21 | 22 | ## [v0.9.9] - 2020-01-01 23 | ### Added 24 | - Anything 25 | 26 | ''' 27 | def newChangelog = 28 | ''' 29 | ## [Unreleased] 30 | 31 | ## [v0.0.1] - 2020-01-01 32 | ### Added 33 | - Nothing yet 34 | 35 | ''' 36 | 37 | ScriptMock scriptMock = new ScriptMock() 38 | 39 | @Test 40 | void testGetCorrectVersion() { 41 | scriptMock.files.put('CHANGELOG.md', validChangelog) 42 | Changelog changelog = new Changelog(scriptMock) 43 | 44 | def changes1 = changelog.changesForVersion("v1.0.0") 45 | assertEquals("### Changed\\n- Something", changes1) 46 | 47 | def changes2 = changelog.changesForVersion("v0.9.9") 48 | assertEquals("### Added\\n- Anything", changes2) 49 | 50 | def changes3 = changelog.changesForVersion("v2.0.0") 51 | assertEquals("### Changed\\n- Everything!", changes3) 52 | } 53 | 54 | @Test 55 | void testWillWorkWithNewChangelog() { 56 | scriptMock.files.put('CHANGELOG.md', newChangelog) 57 | Changelog changelog = new Changelog(scriptMock) 58 | def changes = changelog.changesForVersion("v0.0.1") 59 | assertEquals("### Added\\n- Nothing yet", changes) 60 | } 61 | 62 | @Test 63 | void testReplaceInvalidCharactersCorrect() { 64 | scriptMock.files.put('CHANGELOG.md', validChangelog) 65 | Changelog changelog = new Changelog(scriptMock) 66 | 67 | assertEquals("", changelog.escapeForJson("\"")) 68 | assertEquals("", changelog.escapeForJson("'")) 69 | assertEquals("", changelog.escapeForJson("''")) 70 | assertEquals("", changelog.escapeForJson("\\")) 71 | assertEquals("\\n", changelog.escapeForJson("\n")) 72 | assertEquals("\\n", changelog.escapeForJson("\n\"\"''\\\\")) 73 | } 74 | 75 | @Test 76 | void testThrowsErrorOnVersionNotFound() { 77 | scriptMock.files.put('CHANGELOG.md', validChangelog) 78 | Changelog changelog = new Changelog(scriptMock) 79 | def exception = shouldFail { 80 | changelog.changesForVersion("not existing version") 81 | } 82 | assertEquals("The desired version 'not existing version' could not be found in the changelog.", exception.getMessage()) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /test/com/cloudogu/ces/cesbuildlib/DockerMock.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | 3 | import org.mockito.invocation.InvocationOnMock 4 | import org.mockito.stubbing.Answer 5 | 6 | import static org.mockito.ArgumentMatchers.any 7 | import static org.mockito.ArgumentMatchers.anyBoolean 8 | import static org.mockito.ArgumentMatchers.anyString 9 | import static org.mockito.Mockito.mock 10 | import static org.mockito.Mockito.when 11 | 12 | class DockerMock { 13 | Docker mock 14 | Docker.Image imageMock 15 | 16 | DockerMock(String imageTag = "") { 17 | mock = mock(Docker.class) 18 | imageMock = mock(Docker.Image.class) 19 | if (imageTag == "") { 20 | when(mock.image(anyString())).thenReturn(imageMock) 21 | } else { 22 | when(mock.image(imageTag)).thenReturn(imageMock) 23 | } 24 | when(imageMock.mountJenkinsUser()).thenReturn(imageMock) 25 | when(imageMock.mountJenkinsUser(anyBoolean())).thenReturn(imageMock) 26 | when(imageMock.mountDockerSocket()).thenReturn(imageMock) 27 | when(imageMock.mountDockerSocket(anyBoolean())).thenReturn(imageMock) 28 | when(imageMock.inside(any(), any())).thenAnswer(new Answer() { 29 | @Override 30 | Object answer(InvocationOnMock invocation) throws Throwable { 31 | Closure closure = invocation.getArgument(1) 32 | closure.call() 33 | } 34 | }) 35 | when(mock.withRegistry(any(), any(), any())).thenAnswer(new Answer() { 36 | @Override 37 | Object answer(InvocationOnMock invocation) throws Throwable { 38 | Closure closure = invocation.getArgument(2) 39 | closure.call() 40 | } 41 | }) 42 | } 43 | 44 | static Docker create(String imageTag = "") { 45 | return new DockerMock(imageTag).mock 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /test/com/cloudogu/ces/cesbuildlib/DoguRegistryTest.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | 3 | import groovy.json.JsonSlurper 4 | import org.junit.jupiter.api.Test 5 | import static org.junit.jupiter.api.Assertions.* 6 | 7 | import static org.mockito.Mockito.mock 8 | import static org.mockito.Mockito.when 9 | 10 | class DoguRegistryTest { 11 | 12 | @Test 13 | void testCreateRegistryObjectWithDefaults() { 14 | // when 15 | DoguRegistry sut = new DoguRegistry("script") 16 | 17 | // then 18 | assertTrue(sut != null) 19 | } 20 | 21 | @Test 22 | void testPushDogu() { 23 | // given 24 | String doguPath = "dogu.json" 25 | String doguStr = '{"Name": "testing/ldap", "Version": "2.4.48-4"}' 26 | ScriptMock scriptMock = new ScriptMock() 27 | scriptMock.jsonFiles.put(doguPath, new JsonSlurper().parseText(doguStr)) 28 | scriptMock.expectedShRetValueForScript.put("cat ${doguPath}".toString(), doguStr) 29 | 30 | def httpMock = mock(HttpClient.class) 31 | when(httpMock.put('http://url.de/api/v2/dogus/testing/ldap', 'application/json', doguStr)).then({ invocation -> 32 | return [ 33 | httpCode: '200', 34 | body : 'td' 35 | ] 36 | }) 37 | 38 | DoguRegistry sut = new DoguRegistry(scriptMock, "http://url.de") 39 | sut.doguRegistryHttpClient = httpMock 40 | 41 | // when 42 | sut.pushDogu(doguPath) 43 | 44 | // then 45 | assertEquals("echo 'Push Dogu:\n-Namespace/Name: testing/ldap\n-Version: 2.4.48-4'", scriptMock.allActualArgs.get(0)) 46 | assertEquals("cat dogu.json", scriptMock.allActualArgs.get(1)) 47 | } 48 | 49 | @Test 50 | void testExitOnHttpErrorJson() { 51 | // given 52 | String doguPath = "dogu.json" 53 | String doguStr = '{"Name": "testing/ldap", "Version": "2.4.48-4"}' 54 | ScriptMock scriptMock = new ScriptMock() 55 | scriptMock.jsonFiles.put(doguPath, new JsonSlurper().parseText(doguStr)) 56 | scriptMock.expectedShRetValueForScript.put("cat ${doguPath}".toString(), doguStr) 57 | 58 | def httpMock = mock(HttpClient.class) 59 | when(httpMock.put('http://url.de/api/v2/dogus/testing/ldap', 'application/json', doguStr)).then({ invocation -> 60 | return [ 61 | httpCode: '500', 62 | body : 'body' 63 | ] 64 | }) 65 | 66 | DoguRegistry sut = new DoguRegistry(scriptMock, "http://url.de") 67 | sut.doguRegistryHttpClient = httpMock 68 | 69 | // when 70 | sut.pushDogu(doguPath) 71 | 72 | // then 73 | assertEquals("echo 'Push Dogu:\n-Namespace/Name: testing/ldap\n-Version: 2.4.48-4'", scriptMock.allActualArgs.get(0)) 74 | assertEquals("cat dogu.json", scriptMock.allActualArgs.get(1)) 75 | assertEquals("echo 'Error pushing ${doguPath}'".toString(), scriptMock.allActualArgs.get(2)) 76 | assertEquals("echo 'body'", scriptMock.allActualArgs.get(3)) 77 | assertEquals("exit 1", scriptMock.allActualArgs.get(4)) 78 | } 79 | 80 | @Test 81 | void testPushYaml() { 82 | // given 83 | String yamlPath = "path.yaml" 84 | String k8sName = "dogu-operator" 85 | String namespace = "testing" 86 | String version = "1.0.0" 87 | ScriptMock scriptMock = new ScriptMock() 88 | 89 | def httpMock = mock(HttpClient.class) 90 | when(httpMock.putFile('http://url.de/api/v1/k8s/testing/dogu-operator/1.0.0', 'application/yaml', yamlPath)).then({ invocation -> 91 | return [ 92 | "httpCode": '200', 93 | "body" : 'td' 94 | ] 95 | }) 96 | 97 | DoguRegistry sut = new DoguRegistry(scriptMock, "http://url.de") 98 | sut.doguRegistryHttpClient = httpMock 99 | 100 | // when 101 | sut.pushK8sYaml(yamlPath, k8sName, namespace, version) 102 | 103 | // then 104 | assertEquals("echo 'Push Yaml:\n-Name: ${k8sName}\n-Namespace: ${namespace}\n-Version: ${version}'".toString(), scriptMock.allActualArgs.get(0)) 105 | } 106 | 107 | @Test 108 | void testExitOnHttpErrorYaml() { 109 | // given 110 | String yamlPath = "path.yaml" 111 | String k8sName = "dogu-operator" 112 | String namespace = "testing" 113 | String version = "1.0.0" 114 | ScriptMock scriptMock = new ScriptMock() 115 | 116 | def httpMock = mock(HttpClient.class) 117 | when(httpMock.putFile('http://url.de/api/v1/k8s/testing/dogu-operator/1.0.0', 'application/yaml', yamlPath)).then({ invocation -> 118 | return [ 119 | "httpCode": '491', 120 | "body" : 'body' 121 | ] 122 | }) 123 | 124 | DoguRegistry sut = new DoguRegistry(scriptMock, "http://url.de") 125 | sut.doguRegistryHttpClient = httpMock 126 | 127 | // when 128 | sut.pushK8sYaml(yamlPath, k8sName, namespace, version) 129 | 130 | // then 131 | assertEquals("echo 'Push Yaml:\n-Name: ${k8sName}\n-Namespace: ${namespace}\n-Version: ${version}'".toString(), scriptMock.allActualArgs.get(0)) 132 | assertEquals("echo 'Error pushing ${yamlPath}'".toString(), scriptMock.allActualArgs.get(1)) 133 | assertEquals("echo 'body'", scriptMock.allActualArgs.get(2)) 134 | assertEquals("exit 1", scriptMock.allActualArgs.get(3)) 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /test/com/cloudogu/ces/cesbuildlib/GitFlowTest.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | 3 | import org.junit.jupiter.api.Test 4 | import static org.junit.jupiter.api.Assertions.* 5 | import static groovy.test.GroovyAssert.shouldFail 6 | 7 | class GitFlowTest { 8 | def scriptMock = new ScriptMock() 9 | 10 | @Test 11 | void testIsReleaseBranch() { 12 | String branchPrefixRelease = "release" 13 | String branchPrefixFeature = "feature" 14 | 15 | def scriptMock1 = new ScriptMock() 16 | scriptMock1.env = new Object() { 17 | String BRANCH_NAME = "$branchPrefixRelease/something" 18 | } 19 | Git git1 = new Git(scriptMock1) 20 | GitFlow gitflow1 = new GitFlow(scriptMock1, git1) 21 | 22 | def scriptMock2 = new ScriptMock() 23 | scriptMock2.env = new Object() { 24 | String BRANCH_NAME = "$branchPrefixFeature/something" 25 | } 26 | Git git2 = new Git(scriptMock2) 27 | GitFlow gitflow2 = new GitFlow(scriptMock2, git2) 28 | 29 | assertTrue(gitflow1.isReleaseBranch()) 30 | assertFalse(gitflow2.isReleaseBranch()) 31 | } 32 | 33 | @Test 34 | void testFinishRelease() { 35 | String releaseBranchAuthorName = 'release' 36 | String releaseBranchEmail = 'rele@s.e' 37 | String releaseBranchAuthor = createGitAuthorString(releaseBranchAuthorName, releaseBranchEmail) 38 | String developBranchAuthorName = 'develop' 39 | String developBranchEmail = 'develop@a.a' 40 | String developBranchAuthor = createGitAuthorString(developBranchAuthorName, developBranchEmail) 41 | scriptMock.expectedShRetValueForScript.put('git --no-pager show -s --format=\'%an <%ae>\' HEAD', 42 | [releaseBranchAuthor, releaseBranchAuthor, releaseBranchAuthor, releaseBranchAuthor, 43 | // these two are the ones where the release branch author is stored: 44 | releaseBranchAuthor, releaseBranchAuthor, 45 | developBranchAuthor, developBranchAuthor 46 | ]) 47 | scriptMock.expectedShRetValueForScript.put('git push origin master develop myVersion', 0) 48 | 49 | scriptMock.expectedDefaultShRetValue = "" 50 | scriptMock.env.BRANCH_NAME = "myReleaseBranch" 51 | Git git = new Git(scriptMock) 52 | GitFlow gitflow = new GitFlow(scriptMock, git) 53 | gitflow.finishRelease("myVersion") 54 | 55 | scriptMock.allActualArgs.removeAll("echo ") 56 | scriptMock.allActualArgs.removeAll("git --no-pager show -s --format='%an <%ae>' HEAD") 57 | int i = 0 58 | assertEquals("git ls-remote origin refs/tags/myVersion", scriptMock.allActualArgs[i++]) 59 | assertEquals("git config 'remote.origin.fetch' '+refs/heads/*:refs/remotes/origin/*'", scriptMock.allActualArgs[i++]) 60 | assertEquals("git fetch --all", scriptMock.allActualArgs[i++]) 61 | assertEquals("git log origin/myReleaseBranch..origin/develop --oneline", scriptMock.allActualArgs[i++]) 62 | assertEquals("git checkout myReleaseBranch", scriptMock.allActualArgs[i++]) 63 | assertEquals("git reset --hard origin/myReleaseBranch", scriptMock.allActualArgs[i++]) 64 | assertEquals("git checkout develop", scriptMock.allActualArgs[i++]) 65 | assertEquals("git reset --hard origin/develop", scriptMock.allActualArgs[i++]) 66 | assertEquals("git checkout master", scriptMock.allActualArgs[i++]) 67 | assertEquals("git reset --hard origin/master", scriptMock.allActualArgs[i++]) 68 | 69 | // Author & Email 1 (calls 'git --no-pager...' twice) 70 | assertEquals("git merge --no-ff myReleaseBranch", scriptMock.allActualArgs[i++]) 71 | assertAuthor(0, releaseBranchAuthorName, releaseBranchEmail) 72 | 73 | // Author & Email 2 (calls 'git --no-pager...' twice) 74 | assertEquals("git tag -f -m \"release version myVersion\" myVersion", scriptMock.allActualArgs[i++]) 75 | assertAuthor(1, releaseBranchAuthorName, releaseBranchEmail) 76 | 77 | assertEquals("git checkout develop", scriptMock.allActualArgs[i++]) 78 | // Author & Email 3 (calls 'git --no-pager...' twice) 79 | assertEquals("git merge --no-ff myReleaseBranch", scriptMock.allActualArgs[i++]) 80 | assertAuthor(2, releaseBranchAuthorName, releaseBranchEmail) 81 | 82 | assertEquals("git branch -d myReleaseBranch", scriptMock.allActualArgs[i++]) 83 | assertEquals("git checkout myVersion", scriptMock.allActualArgs[i++]) 84 | assertEquals("git push origin master develop myVersion", scriptMock.allActualArgs[i++]) 85 | assertEquals("git push --delete origin myReleaseBranch", scriptMock.allActualArgs[i++]) 86 | } 87 | 88 | @Test 89 | void testFinishReleaseWithMainBranch() { 90 | String releaseBranchAuthorName = 'release' 91 | String releaseBranchEmail = 'rele@s.e' 92 | String releaseBranchAuthor = createGitAuthorString(releaseBranchAuthorName, releaseBranchEmail) 93 | String developBranchAuthorName = 'develop' 94 | String developBranchEmail = 'develop@a.a' 95 | String developBranchAuthor = createGitAuthorString(developBranchAuthorName, developBranchEmail) 96 | scriptMock.expectedShRetValueForScript.put('git --no-pager show -s --format=\'%an <%ae>\' HEAD', 97 | [releaseBranchAuthor, releaseBranchAuthor, releaseBranchAuthor, releaseBranchAuthor, 98 | // these two are the ones where the release branch author is stored: 99 | releaseBranchAuthor, releaseBranchAuthor, 100 | developBranchAuthor, developBranchAuthor 101 | ]) 102 | scriptMock.expectedShRetValueForScript.put('git push origin main develop myVersion', 0) 103 | 104 | scriptMock.expectedDefaultShRetValue = "" 105 | scriptMock.env.BRANCH_NAME = "myReleaseBranch" 106 | Git git = new Git(scriptMock) 107 | GitFlow gitflow = new GitFlow(scriptMock, git) 108 | gitflow.finishRelease("myVersion", "main") 109 | 110 | scriptMock.allActualArgs.removeAll("echo ") 111 | scriptMock.allActualArgs.removeAll("git --no-pager show -s --format='%an <%ae>' HEAD") 112 | int i = 0 113 | assertEquals("git ls-remote origin refs/tags/myVersion", scriptMock.allActualArgs[i++]) 114 | assertEquals("git config 'remote.origin.fetch' '+refs/heads/*:refs/remotes/origin/*'", scriptMock.allActualArgs[i++]) 115 | assertEquals("git fetch --all", scriptMock.allActualArgs[i++]) 116 | assertEquals("git log origin/myReleaseBranch..origin/develop --oneline", scriptMock.allActualArgs[i++]) 117 | assertEquals("git checkout myReleaseBranch", scriptMock.allActualArgs[i++]) 118 | assertEquals("git reset --hard origin/myReleaseBranch", scriptMock.allActualArgs[i++]) 119 | assertEquals("git checkout develop", scriptMock.allActualArgs[i++]) 120 | assertEquals("git reset --hard origin/develop", scriptMock.allActualArgs[i++]) 121 | assertEquals("git checkout main", scriptMock.allActualArgs[i++]) 122 | assertEquals("git reset --hard origin/main", scriptMock.allActualArgs[i++]) 123 | 124 | // Author & Email 1 (calls 'git --no-pager...' twice) 125 | assertEquals("git merge --no-ff myReleaseBranch", scriptMock.allActualArgs[i++]) 126 | assertAuthor(0, releaseBranchAuthorName, releaseBranchEmail) 127 | 128 | // Author & Email 2 (calls 'git --no-pager...' twice) 129 | assertEquals("git tag -f -m \"release version myVersion\" myVersion", scriptMock.allActualArgs[i++]) 130 | assertAuthor(1, releaseBranchAuthorName, releaseBranchEmail) 131 | 132 | assertEquals("git checkout develop", scriptMock.allActualArgs[i++]) 133 | // Author & Email 3 (calls 'git --no-pager...' twice) 134 | assertEquals("git merge --no-ff myReleaseBranch", scriptMock.allActualArgs[i++]) 135 | assertAuthor(2, releaseBranchAuthorName, releaseBranchEmail) 136 | 137 | assertEquals("git branch -d myReleaseBranch", scriptMock.allActualArgs[i++]) 138 | assertEquals("git checkout myVersion", scriptMock.allActualArgs[i++]) 139 | assertEquals("git push origin main develop myVersion", scriptMock.allActualArgs[i++]) 140 | assertEquals("git push --delete origin myReleaseBranch", scriptMock.allActualArgs[i++]) 141 | } 142 | 143 | @Test 144 | void testThrowsErrorWhenTagAlreadyExists() { 145 | scriptMock.expectedShRetValueForScript.put('git ls-remote origin refs/tags/myVersion', 'thisIsATag') 146 | Git git = new Git(scriptMock) 147 | GitFlow gitflow = new GitFlow(scriptMock, git) 148 | def err = shouldFail(Exception.class) { 149 | gitflow.finishRelease("myVersion") 150 | } 151 | assertEquals("You cannot build this version, because it already exists.", err.getMessage()) 152 | } 153 | 154 | @Test 155 | void testThrowsErrorWhenDevelopHasChanged() { 156 | scriptMock.env.BRANCH_NAME = "branch" 157 | scriptMock.expectedShRetValueForScript.put("git log origin/branch..origin/develop --oneline", "some changes") 158 | Git git = new Git(scriptMock) 159 | GitFlow gitflow = new GitFlow(scriptMock, git) 160 | def err = shouldFail(Exception.class) { 161 | gitflow.finishRelease("myVersion") 162 | } 163 | assertEquals("There are changes on develop branch that are not merged into release. Please merge and restart process.", err.getMessage()) 164 | } 165 | 166 | void assertAuthor(int withEnvInvocationIndex, String author, String email) { 167 | def withEnvMap = scriptMock.actualWithEnvAsMap(withEnvInvocationIndex) 168 | assert withEnvMap['GIT_AUTHOR_NAME'] == author 169 | assert withEnvMap['GIT_COMMITTER_NAME'] == author 170 | assert withEnvMap['GIT_AUTHOR_EMAIL'] == email 171 | assert withEnvMap['GIT_COMMITTER_EMAIL'] == email 172 | } 173 | 174 | String createGitAuthorString(String author, String email) { 175 | "${author} <${email}>" 176 | } 177 | 178 | } 179 | -------------------------------------------------------------------------------- /test/com/cloudogu/ces/cesbuildlib/GitHubTest.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | 3 | import org.junit.jupiter.api.Test 4 | import static org.junit.jupiter.api.Assertions.* 5 | import static groovy.test.GroovyAssert.shouldFail 6 | 7 | class GitHubTest { 8 | def testChangelog = 9 | ''' 10 | ## [Unreleased] 11 | 12 | ## [v1.0.0] 13 | ### Added 14 | - everything 15 | 16 | ''' 17 | 18 | ScriptMock scriptMock = new ScriptMock() 19 | 20 | @Test 21 | void testPushPagesBranch() { 22 | Git git = new Git(scriptMock) 23 | GitHub github = new GitHub(scriptMock, git) 24 | scriptMock.expectedShRetValueForScript.put('git push origin gh-pages', 0) 25 | scriptMock.expectedShRetValueForScript.put("git --no-pager show -s --format='%an <%ae>' HEAD", "User Name ") 26 | scriptMock.expectedShRetValueForScript.put('git remote get-url origin', "https://repo.url") 27 | 28 | github.pushPagesBranch('website', 'Deploys new version of website') 29 | 30 | assertPagesBranchToSubFolder('.', scriptMock) 31 | } 32 | 33 | @Test 34 | void testPushPagesBranchToSubFolder() { 35 | Git git = new Git(scriptMock) 36 | GitHub github = new GitHub(scriptMock, git) 37 | scriptMock.expectedShRetValueForScript.put('git push origin gh-pages', 0) 38 | scriptMock.expectedShRetValueForScript.put("git --no-pager show -s --format='%an <%ae>' HEAD", "User Name ") 39 | scriptMock.expectedShRetValueForScript.put('git remote get-url origin', "https://repo.url") 40 | 41 | github.pushPagesBranch('website', 'Deploys new version of website', 'some-folder') 42 | 43 | assertPagesBranchToSubFolder('some-folder', scriptMock) 44 | } 45 | 46 | private void assertPagesBranchToSubFolder(String subFolder, ScriptMock scriptMock) { 47 | assert scriptMock.actualGitArgs.url == "https://repo.url" 48 | assert scriptMock.actualGitArgs.branch == "gh-pages" 49 | 50 | assert scriptMock.actualDir == '.gh-pages' 51 | assert scriptMock.allActualArgs.contains("cp -rf ../website/* ${subFolder}".toString()) 52 | assert scriptMock.allActualArgs.contains("mkdir -p ${subFolder}".toString()) 53 | assert scriptMock.allActualArgs.contains('git add .') 54 | assert scriptMock.allActualArgs.contains('git commit -m "Deploys new version of website"') 55 | assert scriptMock.actualWithEnv.contains("${'GIT_AUTHOR_NAME=User Name'}") 56 | assert scriptMock.actualWithEnv.contains("${'GIT_COMMITTER_NAME=User Name'}") 57 | assert scriptMock.actualWithEnv.contains("${'GIT_AUTHOR_EMAIL=user.name@doma.in'}") 58 | assert scriptMock.actualWithEnv.contains("${'GIT_COMMITTER_EMAIL=user.name@doma.in'}") 59 | assert scriptMock.allActualArgs.contains('git push origin gh-pages') 60 | assert scriptMock.allActualArgs.last == 'rm -rf .gh-pages' 61 | } 62 | 63 | @Test 64 | void testCreateReleaseByChangelog() { 65 | scriptMock.files.put('CHANGELOG.md', testChangelog) 66 | scriptMock.expectedShRetValueForScript.put("git remote get-url origin", "myRepoName") 67 | scriptMock.expectedShRetValueForScript.put("curl -u \$GIT_AUTH_USR:\$GIT_AUTH_PSW --request POST --data '{\"tag_name\": \"v1.0.0\", \"target_commitish\": \"master\", \"name\": \"v1.0.0\", \"body\":\"### Added\\n- everything\"}' --header \"Content-Type: application/json\" https://api.github.com/repos/myRepoName/releases", "{\"id\": 12345}") 68 | Git git = new Git(scriptMock, "credentials") 69 | GitHub github = new GitHub(scriptMock, git) 70 | Changelog changelog = new Changelog(scriptMock) 71 | 72 | String response=github.createReleaseWithChangelog("v1.0.0", changelog) 73 | assertEquals(response, "12345") 74 | 75 | assertEquals(2, scriptMock.allActualArgs.size()) 76 | int i = 0; 77 | assertEquals("git remote get-url origin", scriptMock.allActualArgs[i++]) 78 | 79 | String expectedData = """--data '{"tag_name": "v1.0.0", "target_commitish": "master", """ + 80 | """"name": "v1.0.0", "body":"### Added\\n- everything"}'""" 81 | String expectedHeader = """--header "Content-Type: application/json" https://api.github.com/repos/myRepoName/releases""" 82 | 83 | assertEquals("curl -u \$GIT_AUTH_USR:\$GIT_AUTH_PSW --request POST ${expectedData} ${expectedHeader}".toString(), scriptMock.allActualArgs[i++]) 84 | } 85 | 86 | @Test 87 | void testCreateReleaseByChangelogOnMainBranch() { 88 | String expectedProductionBranch = "main" 89 | 90 | scriptMock.files.put('CHANGELOG.md', testChangelog) 91 | scriptMock.expectedShRetValueForScript.put("git remote get-url origin", "myRepoName") 92 | scriptMock.expectedShRetValueForScript.put("curl -u \$GIT_AUTH_USR:\$GIT_AUTH_PSW --request POST --data '{\"tag_name\": \"v1.0.0\", \"target_commitish\": \"main\", \"name\": \"v1.0.0\", \"body\":\"### Added\\n- everything\"}' --header \"Content-Type: application/json\" https://api.github.com/repos/myRepoName/releases", "{\"id\": 12345}") 93 | 94 | Git git = new Git(scriptMock, "credentials") 95 | GitHub github = new GitHub(scriptMock, git) 96 | Changelog changelog = new Changelog(scriptMock) 97 | 98 | String response=github.createReleaseWithChangelog("v1.0.0", changelog, expectedProductionBranch) 99 | assertEquals(response, "12345") 100 | 101 | assertEquals(2, scriptMock.allActualArgs.size()) 102 | int i = 0; 103 | assertEquals("git remote get-url origin", scriptMock.allActualArgs[i++]) 104 | 105 | String expectedData = """--data '{"tag_name": "v1.0.0", "target_commitish": "${expectedProductionBranch}", """ + 106 | """"name": "v1.0.0", "body":"### Added\\n- everything"}'""" 107 | String expectedHeader = """--header "Content-Type: application/json" https://api.github.com/repos/myRepoName/releases""" 108 | 109 | assertEquals("curl -u \$GIT_AUTH_USR:\$GIT_AUTH_PSW --request POST ${expectedData} ${expectedHeader}".toString(), scriptMock.allActualArgs[i++]) 110 | } 111 | 112 | @Test 113 | void testReleaseFailsWithoutCredentials() { 114 | scriptMock.files.put('CHANGELOG.md', testChangelog) 115 | scriptMock.expectedShRetValueForScript.put("git remote get-url origin", "myRepoName") 116 | Git git = new Git(scriptMock) 117 | GitHub github = new GitHub(scriptMock, git) 118 | Changelog changelog = new Changelog(scriptMock) 119 | 120 | def exception = shouldFail { 121 | github.createRelease("v1.0.0", "changes") 122 | } 123 | assert exception.getMessage().contains("Unable to create Github release without credentials") 124 | 125 | assertFalse(scriptMock.unstable) 126 | github.createReleaseWithChangelog("v1.0.0", changelog) 127 | assertTrue(scriptMock.unstable) 128 | } 129 | 130 | @Test 131 | void testAddReleaseAssetFailsWithoutCredentials() { 132 | scriptMock.files.put('tool.sha256sum.asc', "") 133 | scriptMock.expectedShRetValueForScript.put("git remote get-url origin", "myRepoName") 134 | Git git = new Git(scriptMock) 135 | GitHub github = new GitHub(scriptMock, git) 136 | 137 | assertFalse(scriptMock.unstable) 138 | github.addReleaseAsset("12345", "tool.sha256sum.asc") 139 | assertTrue(scriptMock.unstable) 140 | } 141 | 142 | @Test 143 | void testAddReleaseAssetWithoutError() { 144 | scriptMock.files.put('tool.sha256sum.asc', "") 145 | scriptMock.expectedShRetValueForScript.put("git remote get-url origin", "myRepoName") 146 | Git git = new Git(scriptMock, "credentials") 147 | scriptMock.usernamePassword(["username": "password"]) 148 | GitHub github = new GitHub(scriptMock, git) 149 | 150 | assertFalse(scriptMock.unstable) 151 | github.addReleaseAsset("12345", "tool.sha256sum.asc") 152 | assertFalse(scriptMock.unstable) 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /test/com/cloudogu/ces/cesbuildlib/GpgTest.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | 3 | import org.junit.jupiter.api.Test 4 | import org.mockito.ArgumentMatchers 5 | import org.mockito.invocation.InvocationOnMock 6 | import org.mockito.stubbing.Answer 7 | 8 | import static groovy.test.GroovyAssert.shouldFail 9 | import static org.mockito.ArgumentMatchers.any 10 | import static org.mockito.ArgumentMatchers.anyString 11 | import static org.mockito.Mockito.mock 12 | import static org.mockito.Mockito.when 13 | 14 | class GpgTest { 15 | 16 | @Test 17 | void gpgCreateSignatureWithoutErrors() { 18 | 19 | Docker dockerMock = mock(Docker.class) 20 | Docker.Image imageMock = mock(Docker.Image.class) 21 | ScriptMock scriptMock = new ScriptMock(dockerMock) 22 | scriptMock.expectedShRetValueForScript.put("whoami", "jenkins") 23 | scriptMock.expectedShRetValueForScript.put("cat /etc/passwd | grep jenkins", "jenkins:x:1000:1000:jenkins,,,:/home/jenkins:/bin/bash") 24 | 25 | when(dockerMock.build(anyString(), anyString())).thenAnswer(new Answer() { 26 | @Override 27 | Object answer(InvocationOnMock invocation) throws Throwable { 28 | scriptMock.sh("docker build call") 29 | } 30 | }) 31 | when(dockerMock.image("cloudogu/gpg:1.0")).thenReturn(imageMock) 32 | when(imageMock.mountJenkinsUser()).thenReturn(imageMock) 33 | when(imageMock.inside(ArgumentMatchers.eq("-v :/tmp/workspace --entrypoint='' -v null/.gnupg:/root/.gnupg"), any())).thenAnswer(new Answer() { 34 | @Override 35 | Object answer(InvocationOnMock invocation) throws Throwable { 36 | scriptMock.sh("docker run call") 37 | Closure closure = invocation.getArgument(1) 38 | closure.call() 39 | } 40 | }) 41 | 42 | Gpg gpg = new Gpg(scriptMock, scriptMock.docker) 43 | gpg.createSignature() 44 | assert scriptMock.allActualArgs.size() == 8 45 | assert scriptMock.allActualArgs[0] == "docker build call" 46 | assert scriptMock.allActualArgs[1] == "rm -f Dockerfile.gpgbuild" 47 | assert scriptMock.allActualArgs[2] == "docker run call" 48 | assert scriptMock.allActualArgs[3] == "cd /tmp/workspace" 49 | assert scriptMock.allActualArgs[4] == "gpg --yes --always-trust --pinentry-mode loopback --passphrase=\"\$passphrase\" --import \$pkey" 50 | assert scriptMock.allActualArgs[5] == "make passphrase=\$passphrase signature-ci" 51 | assert scriptMock.allActualArgs[6] == "rm -f \$pkey" 52 | assert scriptMock.allActualArgs[7] == "rm -rf .gnupg" 53 | assert scriptMock.actualEcho.size() == 0 54 | } 55 | 56 | @Test 57 | void gpgCreateSignatureErrorOnImportKey() { 58 | Docker dockerMock = mock(Docker.class) 59 | Docker.Image imageMock = mock(Docker.Image.class) 60 | ScriptMock scriptMock = new ScriptMock(dockerMock) 61 | scriptMock.expectedShRetValueForScript.put("whoami", "jenkins") 62 | scriptMock.expectedShRetValueForScript.put("cat /etc/passwd | grep jenkins", "jenkins:x:1000:1000:jenkins,,,:/home/jenkins:/bin/bash") 63 | scriptMock.expectedShCommandToThrow.put("gpg --yes --always-trust --pinentry-mode loopback --passphrase=\"\$passphrase\" --import \$pkey", new Exception("test-error")) 64 | 65 | when(dockerMock.build(anyString(), anyString())).thenAnswer(new Answer() { 66 | @Override 67 | Object answer(InvocationOnMock invocation) throws Throwable { 68 | scriptMock.sh("docker build call") 69 | } 70 | }) 71 | when(dockerMock.image("cloudogu/gpg:1.0")).thenReturn(imageMock) 72 | when(imageMock.mountJenkinsUser()).thenReturn(imageMock) 73 | when(imageMock.inside(ArgumentMatchers.eq("-v :/tmp/workspace --entrypoint='' -v null/.gnupg:/root/.gnupg"), any())).thenAnswer(new Answer() { 74 | @Override 75 | Object answer(InvocationOnMock invocation) throws Throwable { 76 | scriptMock.sh("docker run call") 77 | Closure closure = invocation.getArgument(1) 78 | closure.call() 79 | } 80 | }) 81 | 82 | Gpg gpg = new Gpg(scriptMock, scriptMock.docker) 83 | 84 | def exception = shouldFail { 85 | gpg.createSignature() 86 | } 87 | 88 | assert 'test-error' == exception.getMessage() 89 | 90 | assert scriptMock.allActualArgs.size() == 7 91 | println scriptMock.allActualArgs 92 | assert scriptMock.allActualArgs[0] == "docker build call" 93 | assert scriptMock.allActualArgs[1] == "rm -f Dockerfile.gpgbuild" 94 | assert scriptMock.allActualArgs[2] == "docker run call" 95 | assert scriptMock.allActualArgs[3] == "cd /tmp/workspace" 96 | assert scriptMock.allActualArgs[4] == "gpg --yes --always-trust --pinentry-mode loopback --passphrase=\"\$passphrase\" --import \$pkey" 97 | assert scriptMock.allActualArgs[5] == "rm -f \$pkey" 98 | assert scriptMock.allActualArgs[6] == "rm -rf .gnupg" 99 | assert scriptMock.actualEcho.size() == 1 100 | assert scriptMock.actualEcho[0] == "java.lang.Exception: test-error" 101 | } 102 | 103 | @Test 104 | void gpgCreateSignatureErrorOnDockerfileBuild() { 105 | Docker dockerMock = DockerMock.create() 106 | ScriptMock scriptMock = new ScriptMock(dockerMock) 107 | 108 | scriptMock.expectedShRetValueForScript.put("whoami", "jenkins") 109 | scriptMock.expectedShRetValueForScript.put("cat /etc/passwd | grep jenkins", "jenkins:x:1000:1000:jenkins,,,:/home/jenkins:/bin/bash") 110 | Docker.Image imageMock = mock(Docker.Image.class) 111 | when(dockerMock.build(anyString(), anyString())).thenAnswer(new Answer() { 112 | @Override 113 | Object answer(InvocationOnMock invocation) throws Throwable { 114 | scriptMock.sh("docker build call") 115 | throw new Exception("test-error") 116 | } 117 | }) 118 | 119 | Gpg gpg = new Gpg(scriptMock, scriptMock.docker) 120 | 121 | def exception = shouldFail { 122 | gpg.createSignature() 123 | } 124 | 125 | assert 'test-error' == exception.getMessage() 126 | 127 | assert scriptMock.allActualArgs.size() == 4 128 | assert scriptMock.allActualArgs[0] == "docker build call" 129 | assert scriptMock.allActualArgs[1] == "rm -f Dockerfile.gpgbuild" 130 | assert scriptMock.allActualArgs[2] == "rm -f \$pkey" 131 | assert scriptMock.allActualArgs[3] == "rm -rf .gnupg" 132 | 133 | assert scriptMock.actualEcho.size() == 2 134 | assert scriptMock.actualEcho[0] == "java.lang.Exception: test-error" 135 | assert scriptMock.actualEcho[1] == "java.lang.Exception: test-error" 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /test/com/cloudogu/ces/cesbuildlib/GradleInDockerBaseTest.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | 3 | import org.junit.jupiter.api.Test 4 | 5 | import static org.assertj.core.api.Assertions.assertThat 6 | import static org.mockito.ArgumentMatchers.any 7 | import static org.mockito.ArgumentMatchers.eq 8 | import static org.mockito.Mockito.verify 9 | 10 | class GradleInDockerBaseTest { 11 | 12 | 13 | static final IMAGE_ID = 'adoptopenjdk/openjdk11:jdk-11.0.1.13-alpine' 14 | def scriptMock = new GradleWrapperInDockerBaseScriptMock() 15 | DockerMock docker = new DockerMock(IMAGE_ID) 16 | 17 | static final ORIGINAL_USER_HOME = "/home/jenkins" 18 | static final String EXPECTED_JENKINS_USER_FROM_ETC_PASSWD = 19 | "jenkins:x:1000:1000:Jenkins,,,:" + ORIGINAL_USER_HOME + ":/bin/bash" 20 | static final EXPECTED_GROUP_ID = "999" 21 | static final EXPECTED_GROUP_FROM_ETC_GROUP = "docker:x:$EXPECTED_GROUP_ID:jenkins" 22 | // Expected output of pwd, print working directory 23 | static final EXPECTED_PWD = "/home/jenkins/workspaces/NAME" 24 | static final SOME_WHITESPACES = " \n " 25 | 26 | @Test 27 | void inDockerWithRegistry() { 28 | def gradle = new GradleInDockerBaseForTest(scriptMock, 'myCreds') 29 | gradle.docker = docker.mock 30 | scriptMock.expectedDefaultShRetValue = '' 31 | boolean closureCalled = false 32 | 33 | gradle.inDocker(IMAGE_ID, { 34 | closureCalled = true 35 | }) 36 | 37 | assertThat(closureCalled).isTrue() 38 | verify(docker.mock).withRegistry(eq("https://$IMAGE_ID".toString()), eq('myCreds'), any()) 39 | } 40 | 41 | class GradleWrapperInDockerBaseScriptMock extends ScriptMock { 42 | GradleWrapperInDockerBaseScriptMock() { 43 | expectedPwd = EXPECTED_PWD 44 | } 45 | 46 | @Override 47 | String sh(Map params) { 48 | super.sh(params) 49 | // Add some whitespaces 50 | String script = params.get("script") 51 | if (script == "cat /etc/passwd | grep jenkins") { 52 | return EXPECTED_JENKINS_USER_FROM_ETC_PASSWD + SOME_WHITESPACES 53 | } else if (script == "cat /etc/group | grep docker") { 54 | return EXPECTED_GROUP_FROM_ETC_GROUP + SOME_WHITESPACES 55 | } else if (script.contains(EXPECTED_GROUP_FROM_ETC_GROUP)) { 56 | return EXPECTED_GROUP_ID 57 | } 58 | "" 59 | } 60 | } 61 | 62 | class GradleInDockerBaseForTest extends GradleInDockerBase { 63 | 64 | GradleInDockerBaseForTest(script, String credentialsId = null) { 65 | super(script, credentialsId) 66 | } 67 | 68 | def call(Closure closure, boolean printStdOut) { 69 | inDocker(IMAGE_ID) { 70 | closure.call() 71 | } 72 | } 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /test/com/cloudogu/ces/cesbuildlib/GradleMock.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | 3 | class GradleMock extends Gradle { 4 | String args 5 | 6 | GradleMock(scriptMock) { 7 | super(scriptMock) 8 | } 9 | 10 | def gradle(String args, boolean printStdOut) { 11 | this.args = args 12 | return args 13 | } 14 | 15 | static Docker setupDockerMock(GradleInDockerBase gradle) { 16 | gradle.docker = DockerMock.create() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/com/cloudogu/ces/cesbuildlib/GradleTest.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | 3 | 4 | import org.junit.jupiter.api.AfterEach 5 | import org.junit.jupiter.api.Test 6 | 7 | import static org.junit.jupiter.api.Assertions.assertEquals 8 | 9 | class GradleTest { 10 | def scriptMock = new ScriptMock() 11 | def gradle = new GradleMock(scriptMock) 12 | 13 | @AfterEach 14 | void tearDown() throws Exception { 15 | // always reset metaClass after messing with it to prevent changes from leaking to other tests 16 | Gradle.metaClass = null 17 | } 18 | 19 | @Test 20 | void testCall() throws Exception { 21 | def result = gradle "test" 22 | assertEquals("test", result) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test/com/cloudogu/ces/cesbuildlib/GradleWrapperInDockerTest.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | 3 | import org.junit.jupiter.api.Test 4 | 5 | import static org.mockito.Mockito.verify 6 | 7 | class GradleWrapperInDockerTest { 8 | def scriptMock = new ScriptMock() 9 | 10 | @Test 11 | void gradleInDocker() { 12 | def gradle = new GradleWrapperInDocker(scriptMock, 'adoptopenjdk/openjdk11:jdk-11.0.1.13-alpine') 13 | Docker docker = GradleMock.setupDockerMock(gradle) 14 | gradle 'clean install' 15 | 16 | assert scriptMock.actualShStringArgs[0].trim().contains('clean install') 17 | verify(docker).image('adoptopenjdk/openjdk11:jdk-11.0.1.13-alpine') 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/com/cloudogu/ces/cesbuildlib/HttpClientTest.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | 3 | import groovy.json.JsonOutput 4 | import org.junit.jupiter.api.Test 5 | 6 | import static org.assertj.core.api.Assertions.assertThat 7 | 8 | class HttpClientTest { 9 | 10 | ScriptMock scriptMock = new ScriptMock() 11 | HttpClient http = new HttpClient(scriptMock) 12 | 13 | @Test 14 | void "simple request"() { 15 | def expectedResponse = 'HTTP/2 201\n' + 16 | 'location: https://some:/url' 17 | scriptMock.expectedDefaultShRetValue = expectedResponse 18 | 19 | def actualResponse = http.get('http://url') 20 | 21 | assertThat(actualResponse.httpCode).isEqualTo('201') 22 | assertThat(actualResponse.headers['location']).isEqualTo('https://some:/url') 23 | assertThat(actualResponse.body).isEqualTo('') 24 | assertThat(scriptMock.actualShMapArgs[0]).isEqualTo("curl -i -X 'GET' 'http://url'") 25 | } 26 | 27 | @Test 28 | void "request with body"() { 29 | def expectedResponse = 'HTTP/1.1 203\n' + 30 | 'cache-control: no-cache\n' + 31 | 'content-type: output' 32 | scriptMock.expectedDefaultShRetValue = expectedResponse 33 | 34 | def dataJson = JsonOutput.toJson([ 35 | title : 't', 36 | description: 'd' 37 | ]) 38 | 39 | def actualResponse = http.post('http://some-url', 'input', dataJson) 40 | 41 | assertThat(actualResponse.httpCode).isEqualTo('203') 42 | assertThat(actualResponse.headers['content-type']).isEqualTo('output') 43 | assertThat(actualResponse.body).isEqualTo('') 44 | 45 | assertThat(scriptMock.actualShMapArgs[0]) 46 | .isEqualTo("curl -i -X 'POST' -H 'Content-Type: input' -d '{\"title\":\"t\",\"description\":\"d\"}' 'http://some-url'" ) 47 | } 48 | 49 | @Test 50 | void "request with file upload"() { 51 | def expectedResponse = 'HTTP/1.1 203\n' + 52 | 'cache-control: no-cache\n' + 53 | 'content-type: output' 54 | scriptMock.expectedDefaultShRetValue = expectedResponse 55 | 56 | def actualResponse = http.putFile('http://some-url', 'input', "/path/to/file") 57 | 58 | assertThat(actualResponse.httpCode).isEqualTo('203') 59 | assertThat(actualResponse.headers['content-type']).isEqualTo('output') 60 | assertThat(actualResponse.body).isEqualTo('') 61 | 62 | assertThat(scriptMock.actualShMapArgs[0]) 63 | .isEqualTo("curl -i -X 'PUT' -H 'Content-Type: input' -T '/path/to/file' 'http://some-url'") 64 | } 65 | 66 | @Test 67 | void "response with body"() { 68 | String expectedBody1 = '{"some":"body"}\n' 69 | String expectedBody2 = 'second line' 70 | def expectedResponse = 'HTTP/1.1 203\n' + 71 | 'cache-control: no-cache\n' + 72 | 'content-type: output\n' + 73 | '\n' + 74 | expectedBody1 + 75 | expectedBody2 76 | scriptMock.expectedDefaultShRetValue = expectedResponse 77 | 78 | def actualResponse = http.post('http://some-url') 79 | 80 | assertThat(actualResponse.httpCode).isEqualTo('203') 81 | assertThat(actualResponse.headers['content-type']).isEqualTo('output') 82 | assertThat(actualResponse.body).isEqualTo(expectedBody1 + expectedBody2) 83 | 84 | assertThat(scriptMock.actualShMapArgs[0]) 85 | .isEqualTo("curl -i -X 'POST' 'http://some-url'") 86 | } 87 | 88 | @Test 89 | void "request with credentials"() { 90 | http = new HttpClient(scriptMock, "credentialsID") 91 | scriptMock.env.put("CURL_USER", "user") 92 | scriptMock.env.put("CURL_PASSWORD", "pw") 93 | 94 | def expectedResponse = 'HTTP/2 201\n' + 95 | 'location: https://some:/url' 96 | scriptMock.expectedDefaultShRetValue = expectedResponse 97 | 98 | http.get('http://url') 99 | 100 | assertThat(scriptMock.actualShMapArgs[0]).isEqualTo("curl -i -X 'GET' -u 'user:pw' 'http://url'") 101 | } 102 | 103 | @Test 104 | void "put request with single quotes"() { 105 | http = new HttpClient(scriptMock, "credentialsID") 106 | def expectedResponse = 'HTTP/1.1 203\n' + 107 | 'cache-control: no-cache\n' + 108 | 'content-type: output' 109 | scriptMock.env.put("CURL_USER", "us'er") 110 | scriptMock.env.put("CURL_PASSWORD", "p'w") 111 | scriptMock.expectedDefaultShRetValue = expectedResponse 112 | 113 | def actualResponse = http.putFile('http://so\'me-url', 'in\'put', "/path/t\'o/file") 114 | 115 | assertThat(actualResponse.httpCode).isEqualTo('203') 116 | assertThat(actualResponse.headers['content-type']).isEqualTo('output') 117 | assertThat(actualResponse.body).isEqualTo('') 118 | 119 | assertThat(scriptMock.actualShMapArgs[0]) 120 | .isEqualTo("curl -i -X 'PUT' -u 'us'\"'\"'er:p'\"'\"'w' -H 'Content-Type: in'\"'\"'put' -T '/path/t'\"'\"'o/file' 'http://so'\"'\"'me-url'" ) 121 | } 122 | 123 | @Test 124 | void "post request with single quotes"() { 125 | def expectedResponse = 'HTTP/1.1 203\n' + 126 | 'cache-control: no-cache\n' + 127 | 'content-type: output' 128 | scriptMock.expectedDefaultShRetValue = expectedResponse 129 | 130 | def dataJson = JsonOutput.toJson([ 131 | title : 't', 132 | description: 'd\'d' 133 | ]) 134 | 135 | def actualResponse = http.post('http://some-url', 'input', dataJson) 136 | 137 | assertThat(actualResponse.httpCode).isEqualTo('203') 138 | assertThat(actualResponse.headers['content-type']).isEqualTo('output') 139 | assertThat(actualResponse.body).isEqualTo('') 140 | 141 | assertThat(scriptMock.actualShMapArgs[0]) 142 | .isEqualTo("curl -i -X 'POST' -H 'Content-Type: input' -d '{\"title\":\"t\",\"description\":\"d'\"'\"'d\"}' 'http://some-url'" ) 143 | } 144 | } 145 | 146 | -------------------------------------------------------------------------------- /test/com/cloudogu/ces/cesbuildlib/MakefileTest.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | 3 | import static org.assertj.core.api.Assertions.assertThat 4 | 5 | class MakefileTest { 6 | 7 | void testGetVersion() { 8 | def scriptMock = new ScriptMock() 9 | scriptMock.expectedShRetValueForScript.put('grep -e "^VERSION=" Makefile | sed "s/VERSION=//g"'.toString(), "4.2.2".toString()) 10 | 11 | Makefile makefile = new Makefile(scriptMock) 12 | 13 | makefile.getVersion() 14 | 15 | assertThat(scriptMock.allActualArgs[0]).isEqualTo('grep -e "^VERSION=" Makefile | sed "s/VERSION=//g"'.toString()) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/com/cloudogu/ces/cesbuildlib/MarkdownTest.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | 3 | import org.junit.jupiter.api.Test 4 | 5 | import static org.mockito.Mockito.times 6 | import static org.mockito.Mockito.verify 7 | 8 | class MarkdownTest { 9 | 10 | @Test 11 | void testIfDockerContainerCommandIsCalledWithCorrectArgs() { 12 | Docker dockerMock = DockerMock.create("ghcr.io/tcort/markdown-link-check:stable") 13 | ScriptMock scriptMock = new ScriptMock(dockerMock) 14 | Markdown markdown = new Markdown(scriptMock) 15 | 16 | markdown.docker = dockerMock 17 | 18 | markdown.check() 19 | 20 | assert scriptMock.allActualArgs.size() == 1 21 | assert scriptMock.allActualArgs[0] == "find /docs -name \\*.md -print0 | xargs -0 -n1 markdown-link-check -v" 22 | 23 | verify(dockerMock.image("ghcr.io/tcort/markdown-link-check:stable"), times(1)).mountJenkinsUser() 24 | } 25 | 26 | @Test 27 | void testIfDockerContainerCommandIsCalledWithCorrectArgsWithMarkDownTag() { 28 | Docker dockerMock = DockerMock.create("ghcr.io/tcort/markdown-link-check:3.11.0") 29 | ScriptMock scriptMock = new ScriptMock(dockerMock) 30 | Markdown markdown = new Markdown(scriptMock, "3.11.0") 31 | 32 | markdown.docker = dockerMock 33 | 34 | markdown.check() 35 | 36 | assert scriptMock.allActualArgs.size() == 1 37 | assert scriptMock.allActualArgs[0] == "find /docs -name \\*.md -print0 | xargs -0 -n1 markdown-link-check -v" 38 | 39 | verify(dockerMock.image("ghcr.io/tcort/markdown-link-check:3.11.0"), times(1)).mountJenkinsUser() 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test/com/cloudogu/ces/cesbuildlib/MavenInDockerBaseTest.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | 3 | import org.junit.jupiter.api.BeforeEach 4 | import org.junit.jupiter.api.Test 5 | 6 | import static org.assertj.core.api.Assertions.assertThat 7 | import static org.junit.jupiter.api.Assertions.assertEquals 8 | import static org.mockito.ArgumentMatchers.any 9 | import static org.mockito.ArgumentMatchers.eq 10 | import static org.mockito.Mockito.verify 11 | 12 | class MavenInDockerBaseTest { 13 | 14 | static final ORIGINAL_USER_HOME = "/home/jenkins" 15 | static final String EXPECTED_JENKINS_USER_FROM_ETC_PASSWD = 16 | "jenkins:x:1000:1000:Jenkins,,,:" + ORIGINAL_USER_HOME + ":/bin/bash" 17 | static final EXPECTED_GROUP_ID = "999" 18 | static final EXPECTED_GROUP_FROM_ETC_GROUP = "docker:x:$EXPECTED_GROUP_ID:jenkins" 19 | // Expected output of pwd, print working directory 20 | static final EXPECTED_PWD = "/home/jenkins/workspaces/NAME" 21 | static final SOME_WHITESPACES = " \n " 22 | static final IMAGE_ID = 'maven:3.5.0-jdk8' 23 | 24 | def scriptMock = new MavenInDockerScriptMock() 25 | DockerMock docker = new DockerMock(IMAGE_ID) 26 | 27 | def mvn = new MavenInDockerForTest(scriptMock) 28 | 29 | @BeforeEach 30 | void setup() { 31 | mvn.docker = docker.mock 32 | } 33 | 34 | @Test 35 | void testCreateDockerRunArgsDefault() { 36 | assertEquals("", mvn.createDockerRunArgs()) 37 | } 38 | 39 | @Test 40 | void testDockerHostEnabled() { 41 | mvn.enableDockerHost = true 42 | 43 | mvn 'test' 44 | 45 | verify(docker.imageMock).mountDockerSocket(true) 46 | verify(docker.imageMock).mountJenkinsUser(true) 47 | } 48 | 49 | @Test 50 | void testCreateDockerRunArgsUseLocalRepoFromJenkins() { 51 | scriptMock.env.HOME = "/home/jenkins" 52 | mvn.useLocalRepoFromJenkins = true 53 | 54 | def expectedMavenRunArgs = " -v /home/jenkins/.m2:$EXPECTED_PWD/.m2" 55 | 56 | assert mvn.createDockerRunArgs().contains(expectedMavenRunArgs) 57 | assert scriptMock.actualShMapArgs.size() == 1 58 | assert scriptMock.actualShMapArgs.get(0) == 'mkdir -p $HOME/.m2' 59 | } 60 | 61 | @Test 62 | void inDockerWithRegistry() { 63 | 64 | mvn.credentialsId = 'myCreds' 65 | boolean closureCalled = false 66 | 67 | mvn.inDocker(IMAGE_ID, { 68 | closureCalled = true 69 | }) 70 | 71 | assertThat(closureCalled).isTrue() 72 | verify(docker.mock).withRegistry(eq("https://$IMAGE_ID".toString()), eq('myCreds'), any()) 73 | } 74 | 75 | class MavenInDockerScriptMock extends ScriptMock { 76 | MavenInDockerScriptMock() { 77 | expectedPwd = EXPECTED_PWD 78 | } 79 | 80 | @Override 81 | String sh(Map params) { 82 | super.sh(params) 83 | // Add some whitespaces 84 | String script = params.get("script") 85 | if (script == "cat /etc/passwd | grep jenkins") { 86 | return EXPECTED_JENKINS_USER_FROM_ETC_PASSWD + SOME_WHITESPACES 87 | } else if (script == "cat /etc/group | grep docker") { 88 | return EXPECTED_GROUP_FROM_ETC_GROUP + SOME_WHITESPACES 89 | } else if (script.contains(EXPECTED_GROUP_FROM_ETC_GROUP)) { 90 | return EXPECTED_GROUP_ID 91 | } 92 | "" 93 | } 94 | } 95 | 96 | class MavenInDockerForTest extends MavenInDockerBase { 97 | 98 | MavenInDockerForTest(Object script) { 99 | super(script) 100 | } 101 | 102 | def call(Closure closure, boolean printStdOut) { 103 | inDocker(IMAGE_ID) { 104 | closure.call() 105 | } 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /test/com/cloudogu/ces/cesbuildlib/MavenInDockerTest.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | 3 | import org.junit.jupiter.api.Test 4 | 5 | import static org.mockito.Mockito.verify 6 | import static com.cloudogu.ces.cesbuildlib.MavenMock.setupDockerMock 7 | 8 | class MavenInDockerTest { 9 | def scriptMock = new ScriptMock() 10 | 11 | @Test 12 | void mavenInDocker() { 13 | def mvn = new MavenInDocker(scriptMock, '3.5.0-jdk8') 14 | Docker docker = setupDockerMock(mvn) 15 | mvn 'clean install' 16 | 17 | assert scriptMock.actualShStringArgs[0].trim().contains('clean install') 18 | verify(docker).image('maven:3.5.0-jdk8') 19 | } 20 | 21 | @Test 22 | void customMavenImageTest() { 23 | def mvn = new MavenInDocker(scriptMock, 'maven:latest') 24 | Docker docker = setupDockerMock(mvn) 25 | mvn 'clean install' 26 | 27 | verify(docker).image('maven:latest') 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /test/com/cloudogu/ces/cesbuildlib/MavenLocalTest.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | 3 | import org.junit.jupiter.api.Test 4 | 5 | class MavenLocalTest { 6 | 7 | def scriptMock = new ScriptMock() 8 | 9 | @Test 10 | void testMvnHomeNull() throws Exception { 11 | def mvn = new MavenLocal(scriptMock, null, "javaHome") 12 | mvn "test" 13 | 14 | assert scriptMock.actualEcho[0] == 'WARNING: mvnHome is empty. Did you check "Install automatically"?' 15 | } 16 | 17 | @Test 18 | void testMvnHomeEmpty() throws Exception { 19 | def mvn = new MavenLocal(scriptMock, '', "javaHome") 20 | mvn "test" 21 | 22 | assert scriptMock.actualEcho[0] == 'WARNING: mvnHome is empty. Did you check "Install automatically"?' 23 | } 24 | 25 | @Test 26 | void testJavaHomeNull() throws Exception { 27 | def mvn = new MavenLocal(scriptMock, 'mvnHome', null) 28 | mvn "test" 29 | 30 | assert scriptMock.actualEcho[0] == 'WARNING: javaHome is empty. Did you check "Install automatically"?' 31 | } 32 | 33 | @Test 34 | void testJavaHomeEmpty() throws Exception { 35 | def mvn = new MavenLocal(scriptMock, 'mvnHome', '') 36 | mvn "test" 37 | 38 | assert scriptMock.actualEcho[0] == 'WARNING: javaHome is empty. Did you check "Install automatically"?' 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/com/cloudogu/ces/cesbuildlib/MavenMock.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | 3 | class MavenMock extends Maven { 4 | String args 5 | String mockedGroupId = "" 6 | String mockedArtifactId = "" 7 | String mockedName = "" 8 | 9 | MavenMock(scriptMock) { 10 | super(scriptMock) 11 | } 12 | 13 | def mvn(String args, boolean printStdOut) { 14 | this.args = args 15 | } 16 | 17 | static Docker setupDockerMock(MavenInDockerBase mvn) { 18 | mvn.docker = DockerMock.create() 19 | } 20 | 21 | @Override 22 | String getArtifactId() { mockedArtifactId } 23 | 24 | @Override 25 | String getGroupId() { mockedGroupId } 26 | 27 | @Override 28 | String getName() { mockedName } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /test/com/cloudogu/ces/cesbuildlib/MavenWrapperInDockerTest.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | 3 | import org.junit.jupiter.api.Test 4 | 5 | import static com.cloudogu.ces.cesbuildlib.MavenMock.setupDockerMock 6 | import static org.mockito.Mockito.verify 7 | 8 | class MavenWrapperInDockerTest { 9 | def scriptMock = new ScriptMock() 10 | 11 | @Test 12 | void mavenInDocker() { 13 | def mvn = new MavenWrapperInDocker(scriptMock, 'adoptopenjdk/openjdk11:jdk-11.0.1.13-alpine') 14 | Docker docker = setupDockerMock(mvn) 15 | mvn 'clean install' 16 | 17 | assert scriptMock.actualShStringArgs[0].trim().contains('/.m2') 18 | assert scriptMock.actualShStringArgs[1].trim().contains('clean install') 19 | verify(docker).image('adoptopenjdk/openjdk11:jdk-11.0.1.13-alpine') 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/com/cloudogu/ces/cesbuildlib/MavenWrapperTest.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | 3 | import org.junit.jupiter.api.Test 4 | 5 | class MavenWrapperTest { 6 | 7 | def scriptMock = new ScriptMock() 8 | 9 | 10 | 11 | @Test 12 | void testCallWithoutJavaHome() throws Exception { 13 | def mvn = new MavenWrapper(scriptMock) 14 | mvn 'ourGoal' 15 | assert scriptMock.getActualShStringArgs().size() == 2 16 | assert scriptMock.getActualShStringArgs().get(0).startsWith('MAVEN_USER_HOME=') 17 | assert scriptMock.getActualShStringArgs().get(0).contains('/.m2') 18 | assert scriptMock.getActualShStringArgs().get(1).startsWith('MVNW_VERBOSE=true ./mvnw') 19 | assert scriptMock.getActualShStringArgs().get(1).contains('ourGoal') 20 | assert scriptMock.actualWithEnv == null 21 | } 22 | 23 | @Test 24 | void testCallWithJavaHome() throws Exception { 25 | def mvn = new MavenWrapper(scriptMock, '/java') 26 | scriptMock.env.JAVA_HOME = '/env/java' 27 | mvn 'ourGoal' 28 | 29 | assert scriptMock.getActualShStringArgs().size() == 2 30 | assert scriptMock.getActualShStringArgs().get(0).startsWith('MAVEN_USER_HOME=') 31 | assert scriptMock.getActualShStringArgs().get(0).contains('/.m2') 32 | assert scriptMock.getActualShStringArgs().get(1).startsWith('MVNW_VERBOSE=true ./mvnw') 33 | assert scriptMock.getActualShStringArgs().get(1).contains('ourGoal') 34 | assert scriptMock.actualWithEnv == ["JAVA_HOME=/java", "PATH+JDK=/env/java/bin"] 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/com/cloudogu/ces/cesbuildlib/SCMManagerTest.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | 3 | import groovy.json.JsonOutput 4 | import groovy.json.JsonSlurper 5 | import org.junit.jupiter.api.BeforeEach 6 | import org.junit.jupiter.api.Test 7 | 8 | import static org.assertj.core.api.Assertions.assertThat 9 | import static org.mockito.ArgumentMatchers.any 10 | import static org.mockito.Mockito.mock 11 | import static org.mockito.Mockito.when 12 | 13 | class SCMManagerTest { 14 | 15 | ScriptMock scriptMock = new ScriptMock() 16 | String repo = 'scm/repo' 17 | String baseUrl = 'http://ho.st/scm' 18 | SCMManager scmm = new SCMManager(scriptMock, baseUrl, "credentialsID") 19 | HttpClient httpMock 20 | 21 | def slurper = new JsonSlurper() 22 | 23 | def jsonTwoPrs = JsonOutput.toJson([ 24 | _embedded: [ 25 | pullRequests: [ 26 | [ 27 | title: 'one', 28 | id : '1' 29 | ], 30 | [ 31 | title: 'two', 32 | id : '2' 33 | ] 34 | ] 35 | ] 36 | ]) 37 | 38 | @BeforeEach 39 | void init() { 40 | httpMock = mock(HttpClient.class) 41 | scmm.http = httpMock 42 | } 43 | 44 | @Test 45 | void "find pull request by title"() { 46 | when(httpMock.get(any(), any())).then({ invocation -> 47 | assert invocation.getArguments()[0] == 'http://ho.st/scm/api/v2/pull-requests/scm/repo' 48 | assert invocation.getArguments()[1] == 'application/vnd.scmm-pullRequestCollection+json;v=2' 49 | 50 | return [ 51 | httpCode: '200', 52 | body : jsonTwoPrs.toString() 53 | ] 54 | }) 55 | 56 | def prs = scmm.searchPullRequestIdByTitle(repo, "one") 57 | assertThat(prs).isEqualTo('1') 58 | } 59 | 60 | @Test 61 | void "did not find pull request by title"() { 62 | when(httpMock.get(any(), any())).thenReturn([ 63 | httpCode: '200', 64 | body : jsonTwoPrs.toString() 65 | ]) 66 | def prs = scmm.searchPullRequestIdByTitle(repo, "3") 67 | assertThat(prs).isEqualTo("") 68 | } 69 | 70 | @Test 71 | void "returns empty string when no pr is found"() { 72 | when(httpMock.get(any(), any())).thenReturn([ 73 | httpCode: '200', 74 | body : JsonOutput.toJson([ 75 | _embedded: [ 76 | pullRequests: [] 77 | ] 78 | ]) 79 | ]) 80 | 81 | def prs = scmm.searchPullRequestIdByTitle(repo, "just something") 82 | assertThat(prs).isEqualTo("") 83 | } 84 | 85 | @Test 86 | void "successfully creating a pull request yields the created prs id"() { 87 | def expected = [ 88 | title : 'ti', 89 | description: 'd', 90 | source : 's', 91 | target : 'ta' 92 | ] 93 | 94 | when(httpMock.post(any(), any(), any())).then({ invocation -> 95 | assert invocation.getArguments()[0] == 'http://ho.st/scm/api/v2/pull-requests/scm/repo' 96 | assert invocation.getArguments()[1] == 'application/vnd.scmm-pullRequest+json;v=2' 97 | assert invocation.getArguments()[2] == JsonOutput.toJson(expected) 98 | 99 | return [ 100 | httpCode: '201', 101 | headers: [ Location: 'https://a/long/url/with/id/id/12' ] 102 | ] 103 | }) 104 | 105 | def id = scmm.createPullRequest(repo, expected.source, expected.target, expected.title, expected.description) 106 | assertThat(id.toString()).isEqualTo('12') 107 | } 108 | 109 | @Test 110 | void "error on pull request creation makes build unstable"() { 111 | when(httpMock.post(any(), any(), any())).thenReturn([ 112 | httpCode: '500', 113 | headers: [ Location: 'https://a/long/url/with/id/id/12' ] 114 | ]) 115 | 116 | def id = scmm.createPullRequest(repo, 'source', 'target', 'title', 'description') 117 | assertThat(id.toString()).isEqualTo("") 118 | } 119 | 120 | @Test 121 | void "successful description update yields to a successful build"() { 122 | def expectedTitle = 'title' 123 | def expectedDescription = 'description' 124 | 125 | when(httpMock.put(any(), any(), any())).then({ invocation -> 126 | assert invocation.getArguments()[0] == 'http://ho.st/scm/api/v2/pull-requests/scm/repo/123' 127 | assert invocation.getArguments()[1] == 'application/vnd.scmm-pullRequest+json;v=2' 128 | def body = slurper.parseText(invocation.getArguments()[2]) 129 | assert body.title == expectedTitle 130 | assert body.description == expectedDescription 131 | 132 | return [ httpCode: '204' ] 133 | }) 134 | boolean response = scmm.updatePullRequest(repo, '123', expectedTitle, expectedDescription) 135 | assertThat(response).isTrue() 136 | } 137 | 138 | @Test 139 | void "error on description update yields to an unstable build"() { 140 | when(httpMock.post(any(), any(), any())).then({ invocation -> 141 | return [ httpCode: '500' ] 142 | }) 143 | 144 | boolean response = scmm.updatePullRequest(repo, '123', 'title', 'description') 145 | assertThat(response).isFalse() 146 | } 147 | 148 | @Test 149 | void "successful comment update yields to a successful build"() { 150 | String expectedComment = 'com123' 151 | when(httpMock.post(any(), any(), any())).then({ invocation -> 152 | assert invocation.getArguments()[0] == 'http://ho.st/scm/api/v2/pull-requests/scm/repo/123/comments' 153 | assert invocation.getArguments()[1] == 'application/json' 154 | assert slurper.parseText(invocation.getArguments()[2]).comment == expectedComment 155 | return [ 156 | httpCode: '201' 157 | ] 158 | }) 159 | 160 | boolean response = scmm.addComment(repo,'123', expectedComment) 161 | assertThat(response).isTrue() 162 | } 163 | 164 | @Test 165 | void "error on comment update yields to an unstable build"() { 166 | when(httpMock.post(any(), any(), any())).thenReturn([ 167 | httpCode: '500' 168 | ]) 169 | 170 | boolean response = scmm.addComment(repo,'123', 'comment') 171 | assertThat(response).isFalse() 172 | } 173 | 174 | @Test 175 | void "returns pr id when creating pr with createOrUpdate"() { 176 | def expected = [ 177 | title : 'ti', 178 | description: 'd', 179 | source : 's', 180 | target : 'ta' 181 | ] 182 | 183 | when(httpMock.get(any(), any())).thenReturn([ 184 | httpCode: '200', 185 | body : jsonTwoPrs.toString() 186 | ]) 187 | 188 | when(httpMock.post(any(), any(), any())).then({ invocation -> 189 | assert invocation.getArguments()[0] == 'http://ho.st/scm/api/v2/pull-requests/scm/repo' 190 | assert invocation.getArguments()[1] == 'application/vnd.scmm-pullRequest+json;v=2' 191 | assert invocation.getArguments()[2] == JsonOutput.toJson(expected) 192 | 193 | return [ 194 | httpCode: '201', 195 | headers: [ Location: 'https://a/long/url/with/id/id/12' ] 196 | ] 197 | }) 198 | 199 | def id = scmm.createOrUpdatePullRequest(repo, expected.source, expected.target, expected.title, expected.description) 200 | assertThat(id.toString()).isEqualTo('12') 201 | } 202 | 203 | @Test 204 | void "returns pr id when updating pr with createOrUpdate"() { 205 | def expected = [ 206 | title : 'one', 207 | description: 'd', 208 | source : 's', 209 | target : 'ta' 210 | ] 211 | 212 | when(httpMock.get(any(), any())).thenReturn([ 213 | httpCode: '200', 214 | body : jsonTwoPrs.toString() 215 | ]) 216 | 217 | when(httpMock.put(any(), any(), any())).then({ invocation -> 218 | assert invocation.getArguments()[0] == 'http://ho.st/scm/api/v2/pull-requests/scm/repo/1' 219 | assert invocation.getArguments()[1] == 'application/vnd.scmm-pullRequest+json;v=2' 220 | def body = slurper.parseText(invocation.getArguments()[2]) 221 | assert body.title == expected.title 222 | assert body.description == expected.description 223 | 224 | return [ httpCode: '204' ] 225 | }) 226 | 227 | def id = scmm.createOrUpdatePullRequest(repo, expected.source, expected.target, expected.title, expected.description) 228 | assertThat(id.toString()).isEqualTo('1') 229 | } 230 | 231 | @Test 232 | void "returns empty string when updating pr with createOrUpdate and fails"() { 233 | def expected = [ 234 | title : 'one', 235 | description: 'd', 236 | source : 's', 237 | target : 'ta' 238 | ] 239 | 240 | when(httpMock.get(any(), any())).thenReturn([ 241 | httpCode: '200', 242 | body : jsonTwoPrs.toString() 243 | ]) 244 | 245 | when(httpMock.put(any(), any(), any())).then({ invocation -> 246 | return [ httpCode: '500' ] 247 | }) 248 | 249 | def id = scmm.createOrUpdatePullRequest(repo, expected.source, expected.target, expected.title, expected.description) 250 | assertThat(id.toString()).isEqualTo('') 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /test/com/cloudogu/ces/cesbuildlib/ScriptMock.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | 3 | import groovy.json.JsonSlurper 4 | 5 | class ScriptMock { 6 | def env = [WORKSPACE: "", HOME: ""] 7 | 8 | boolean expectedIsPullRequest = false 9 | boolean unstable = false 10 | String unstableMsg = "" 11 | def expectedQGate 12 | def expectedPwd 13 | 14 | /** Used when no value in expectedShRetValueForScript matches **/ 15 | def expectedDefaultShRetValue 16 | def expectedShRetValueForScript = [:] 17 | Map expectedShCommandToThrow = [:] 18 | 19 | String actualSonarQubeEnv 20 | 21 | Map actualTimeoutParams = [:] 22 | 23 | List actualUsernamePasswordArgs = [] 24 | List actualShStringArgs = new LinkedList<>() 25 | List allActualArgs = new LinkedList<>() 26 | List actualEcho = new LinkedList<>() 27 | 28 | LinkedHashMap actualJUnitFlags = new LinkedHashMap<>() 29 | 30 | List actualShMapArgs = new LinkedList<>() 31 | 32 | List> writeFileParams = new LinkedList<>() 33 | List> zipParams = new LinkedList<>() 34 | List> archivedArtifacts = new LinkedList<>() 35 | Map actualFileArgs 36 | Map actualStringArgs 37 | Map files = new HashMap() 38 | List> actualWithEnv = [] 39 | 40 | Map jsonFiles = new HashMap<>() 41 | 42 | String actualDir 43 | def actualGitArgs 44 | private ignoreOutputFile 45 | Docker docker 46 | 47 | ScriptMock(Docker docker){ 48 | this.docker = docker 49 | } 50 | 51 | ScriptMock(){ 52 | this(DockerMock.create()) 53 | } 54 | 55 | String sh(String args) { 56 | actualShStringArgs.add(args.toString()) 57 | allActualArgs.add(args.toString()) 58 | return getReturnValueFor(args) 59 | } 60 | 61 | void junit(LinkedHashMap map = [:]) { 62 | actualJUnitFlags = map 63 | } 64 | 65 | void deleteDir() { 66 | allActualArgs.add("called deleteDir()") 67 | } 68 | 69 | String sh(Map args) { 70 | actualShMapArgs.add(args.script.toString()) 71 | allActualArgs.add(args.script.toString()) 72 | 73 | return getReturnValueFor(args.get('script')) 74 | } 75 | 76 | private Object getReturnValueFor(Object arg) { 77 | // toString() to make Map also match GStrings 78 | def error = expectedShCommandToThrow.get(arg.toString().trim()) 79 | if (error != null){ 80 | throw error 81 | } 82 | 83 | def value = expectedShRetValueForScript.get(arg.toString().trim()) 84 | if (value == null) { 85 | return expectedDefaultShRetValue 86 | } 87 | if (value instanceof List) { 88 | // If an exception is thrown here that means that less list items have been passed to 89 | // expectedShRetValueForScript.put('shell command', List) than actual calls to 'shell command'. 90 | // That is, you have to add more items! 91 | return ((List) value).removeAt(0) 92 | } else { 93 | return value 94 | } 95 | } 96 | 97 | boolean isPullRequest() { 98 | return expectedIsPullRequest 99 | } 100 | 101 | def timeout(Map params, closure) { 102 | actualTimeoutParams = params 103 | return closure.call() 104 | } 105 | 106 | def waitForQualityGate() { 107 | return expectedQGate 108 | } 109 | 110 | def unstable(String msg) { 111 | unstable = true 112 | } 113 | 114 | void withSonarQubeEnv(String sonarQubeEnv, Closure closure) { 115 | actualSonarQubeEnv = sonarQubeEnv 116 | closure.call() 117 | } 118 | 119 | void withEnv(List env, Closure closure) { 120 | actualWithEnv.add(env) 121 | closure.call() 122 | } 123 | 124 | def withCredentials(List args, Closure closure) { 125 | closure.call() 126 | } 127 | 128 | void usernamePassword(Map args) { 129 | actualUsernamePasswordArgs.add(args) 130 | } 131 | 132 | void file(Map args) { 133 | actualFileArgs = args 134 | } 135 | 136 | void string(Map args) { 137 | actualStringArgs = args 138 | } 139 | 140 | void error(String args) { 141 | throw new RuntimeException(args) 142 | } 143 | 144 | void echo(String msg) { 145 | actualEcho.add(msg) 146 | } 147 | 148 | String pwd() { expectedPwd } 149 | 150 | def git(def args) { 151 | actualGitArgs = args 152 | return args 153 | } 154 | 155 | void writeFile(Map params) { 156 | writeFileParams.add(params) 157 | } 158 | 159 | void zip(Map params) { 160 | zipParams.add(params) 161 | } 162 | 163 | void archiveArtifacts(Map params) { 164 | archivedArtifacts.add(params) 165 | } 166 | 167 | String readFile(String file) { 168 | return files.get(file) 169 | } 170 | 171 | boolean fileExists(String file) { 172 | return files.containsKey(file) 173 | } 174 | 175 | Object readJSON(Map args) { 176 | String text = args.get('text') 177 | if (text != null) { 178 | def slurper = new JsonSlurper() 179 | return slurper.parseText(text) 180 | } 181 | 182 | String path = args.get("file") 183 | if (path != null) { 184 | return jsonFiles.get(path) 185 | } 186 | 187 | throw new InputMismatchException() 188 | } 189 | 190 | void dir(String dir, Closure closure) { 191 | actualDir = dir 192 | closure.call() 193 | } 194 | 195 | def getActualWithEnv() { 196 | actualWithEnv.isEmpty() ? null : actualWithEnv[actualWithEnv.size() - 1] 197 | } 198 | 199 | Map actualWithEnvAsMap(int index = actualWithEnv.size() - 1) { 200 | if (index < 0) { 201 | return null 202 | } 203 | actualWithEnv[index].collectEntries { [it.split('=')[0], it.split('=')[1]] } 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /test/com/cloudogu/ces/cesbuildlib/ShTest.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | 3 | import org.junit.jupiter.api.Test 4 | 5 | import static groovy.util.GroovyTestCase.assertEquals 6 | 7 | class ShTest { 8 | 9 | @Test 10 | void testReturnStdOut() throws Exception { 11 | Sh sh = new Sh( [ sh: { Map args -> return args['script'] } ]) 12 | 13 | def result = sh.returnStdOut 'echo abc \n ' 14 | assertEquals('echo abc', result) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/com/cloudogu/ces/cesbuildlib/SonarCloudTest.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | 3 | import org.junit.jupiter.api.AfterEach 4 | import org.junit.jupiter.api.Test 5 | 6 | import static groovy.test.GroovyAssert.shouldFail 7 | import static org.junit.jupiter.api.Assertions.assertEquals 8 | 9 | class SonarCloudTest { 10 | 11 | def scriptMock = new ScriptMock() 12 | def mavenMock = new MavenMock(scriptMock) 13 | 14 | def sonarCloud = new SonarCloud(scriptMock, [sonarQubeEnv: 'sonarcloud.io']) 15 | 16 | @AfterEach 17 | void tearDown() throws Exception { 18 | // always reset metaClass after messing with it to prevent changes from leaking to other tests 19 | SonarQube.metaClass = null 20 | } 21 | 22 | @Test 23 | void regularAnalysis() { 24 | 25 | doRegularAnalysis() 26 | 27 | } 28 | 29 | private void doRegularAnalysis() { 30 | scriptMock.env = [ 31 | SONAR_MAVEN_GOAL : 'sonar:sonar', 32 | SONAR_HOST_URL : 'host', 33 | SONAR_AUTH_TOKEN : 'auth', 34 | SONAR_EXTRA_PROPS: '-DextraKey=extraValue', 35 | BRANCH_NAME : 'develop' 36 | ] 37 | 38 | sonarCloud.analyzeWith(mavenMock) 39 | 40 | assert sonarCloud.isUsingBranchPlugin 41 | assert mavenMock.args == 42 | 'sonar:sonar -Dsonar.host.url=host -Dsonar.login=auth -DextraKey=extraValue' 43 | assert scriptMock.actualSonarQubeEnv == 'sonarcloud.io' 44 | } 45 | 46 | @Test 47 | void pullRequestAnalysis() { 48 | scriptMock.expectedIsPullRequest = true 49 | scriptMock.env = [ 50 | CHANGE_ID : 'PR-42', 51 | CHANGE_TARGET : 'develop', 52 | CHANGE_BRANCH : 'feature/something' 53 | ] 54 | scriptMock.expectedDefaultShRetValue = 'github.com/owner/repo' 55 | 56 | sonarCloud.analyzeWith(mavenMock) 57 | 58 | assertEquals( 59 | ' -Dsonar.pullrequest.base=develop -Dsonar.pullrequest.branch=feature/something ' + 60 | '-Dsonar.pullrequest.key=PR-42 -Dsonar.pullrequest.provider=GitHub ' + 61 | '-Dsonar.pullrequest.github.repository=owner/repo ', 62 | mavenMock.additionalArgs) 63 | } 64 | 65 | @Test 66 | void pullRequestAnalysisWithExistingAdditionalArgs() { 67 | scriptMock.expectedIsPullRequest = true 68 | scriptMock.env = [ 69 | CHANGE_ID : 'PR-42', 70 | CHANGE_TARGET : 'develop', 71 | CHANGE_BRANCH : 'feature/something' 72 | ] 73 | scriptMock.expectedDefaultShRetValue = 'github.com/owner/repo' 74 | 75 | mavenMock.additionalArgs = "-Pci" 76 | sonarCloud.analyzeWith(mavenMock) 77 | 78 | assertEquals( 79 | '-Pci ' + 80 | '-Dsonar.pullrequest.base=develop -Dsonar.pullrequest.branch=feature/something ' + 81 | '-Dsonar.pullrequest.key=PR-42 -Dsonar.pullrequest.provider=GitHub ' + 82 | '-Dsonar.pullrequest.github.repository=owner/repo ', 83 | mavenMock.additionalArgs) 84 | } 85 | 86 | @Test 87 | void waitForQualityGatePullRequest() throws Exception { 88 | scriptMock.expectedQGate = [status: 'OK'] 89 | scriptMock.expectedIsPullRequest = true 90 | def qualityGate = sonarCloud.waitForQualityGateWebhookToBeCalled() 91 | assert qualityGate 92 | } 93 | 94 | @Test 95 | void sonarOrganizationMandatoryForUsernameAndPassword() { 96 | def exception = shouldFail { 97 | new SonarCloud(scriptMock, [usernamePassword: 'secretTextCred', sonarHostUrl: 'http://ces/sonar']).analyzeWith(mavenMock) 98 | } 99 | 100 | assert exception.message == "Missing required 'sonarOrganization' parameter." 101 | } 102 | 103 | @Test 104 | void sonarOrganizationMandatoryForToken() { 105 | def exception = shouldFail { 106 | new SonarCloud(scriptMock, [token: 'secretTextCred', sonarHostUrl: 'http://ces/sonar']).analyzeWith(mavenMock) 107 | } 108 | 109 | assert exception.message == "Missing required 'sonarOrganization' parameter." 110 | } 111 | 112 | @Test 113 | void setSonarOrganizationIfPresent() { 114 | sonarCloud = new SonarCloud(scriptMock, [sonarQubeEnv: 'sonarcloud.io', sonarOrganization: 'org']) 115 | 116 | doRegularAnalysis() 117 | 118 | assert mavenMock.additionalArgs.endsWith(' -Dsonar.organization=org ') 119 | } 120 | 121 | 122 | @Test 123 | void pullRequestAnalysisBitBucket() { 124 | scriptMock.expectedIsPullRequest = true 125 | scriptMock.env = [ 126 | CHANGE_ID : 'PR-42', 127 | CHANGE_TARGET : 'develop', 128 | CHANGE_BRANCH : 'feature/something' 129 | ] 130 | scriptMock.expectedDefaultShRetValue = 'bitbucket.org/orga/repo' 131 | 132 | sonarCloud.analyzeWith(mavenMock) 133 | 134 | assertEquals( 135 | ' -Dsonar.pullrequest.base=develop -Dsonar.pullrequest.branch=feature/something ' + 136 | '-Dsonar.pullrequest.key=PR-42 -Dsonar.pullrequest.provider=bitbucketcloud ' + 137 | '-Dsonar.pullrequest.bitbucketcloud.owner=orga ' + 138 | '-Dsonar.pullrequest.bitbucketcloud.repository=repo ', 139 | mavenMock.additionalArgs) 140 | } 141 | 142 | @Test 143 | void pullRequestAnalysisUnknown() { 144 | scriptMock.expectedIsPullRequest = true 145 | scriptMock.expectedDefaultShRetValue = 'UnameIt.org/orga/repo' 146 | 147 | def exception = shouldFail { 148 | sonarCloud.analyzeWith(mavenMock) 149 | } 150 | assert exception.message == "Unknown sonar.pullrequest.provider. None matching for repo URL: UnameIt.org/orga/repo" 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /test/com/cloudogu/ces/cesbuildlib/TrivyExecutor.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | 3 | import org.apache.commons.compress.archivers.ArchiveEntry 4 | import org.apache.commons.compress.archivers.tar.TarArchiveInputStream 5 | import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream 6 | 7 | import java.nio.channels.Channels 8 | import java.nio.channels.FileChannel 9 | import java.nio.channels.ReadableByteChannel 10 | import java.nio.file.Files 11 | import java.nio.file.Path 12 | import java.nio.file.Paths 13 | import java.util.logging.Logger 14 | 15 | class TrivyExecutor { 16 | 17 | private static final Logger logger = Logger.getLogger(TrivyExecutor.class.getName()) 18 | private Path installDir 19 | 20 | TrivyExecutor(Path installDir = Paths.get("trivyInstallation")) { 21 | this.installDir = installDir 22 | } 23 | 24 | Process exec(String version, String argumentstring, Path workDir) { 25 | Path trivyPath = installTrivy(version) 26 | if (workDir.getParent() != null) { 27 | Files.createDirectories(workDir.getParent()) 28 | } 29 | 30 | List arguments = new ArrayList() 31 | arguments.add(trivyPath.toAbsolutePath().toString()) 32 | arguments.addAll(argumentstring.split(" ")) 33 | logger.info("start trivy: ${arguments.join(" ")}") 34 | return new ProcessBuilder(arguments) 35 | .directory(workDir.toAbsolutePath().toFile()) 36 | .inheritIO() 37 | .start() 38 | } 39 | 40 | /** 41 | * downloads, extracts and installs trivy as an executable file. 42 | * Trivy is not downloaded again if the given version is already present. 43 | * Each trivy version is installed into its own subdirectory to distinguish them. 44 | * @param version trivy version 45 | * @return the path to the trivy executable 46 | */ 47 | private Path installTrivy(String version) { 48 | Path pathToExtractedArchive = installDir.resolve("v${version}") 49 | Path pathToTrivyExecutable = pathToExtractedArchive.resolve("trivy") 50 | if (!pathToExtractedArchive.toFile().exists()) { 51 | installDir.toFile().mkdirs() 52 | File archive = downloadTrivy(version, installDir) 53 | untar(archive, pathToExtractedArchive) 54 | logger.info("delete trivy download archive $pathToExtractedArchive") 55 | if (!archive.delete()) { 56 | throw new RuntimeException("cannot delete trivy download archive: $pathToExtractedArchive") 57 | } 58 | 59 | logger.fine("make $pathToTrivyExecutable an executable") 60 | if (pathToTrivyExecutable.toFile().setExecutable(true)) { 61 | return pathToTrivyExecutable 62 | } else { 63 | throw new RuntimeException("cannot make trivy executable: ${pathToTrivyExecutable}") 64 | } 65 | } else { 66 | logger.info("trivy v${version} already installed") 67 | } 68 | 69 | return pathToTrivyExecutable 70 | } 71 | 72 | private static File downloadTrivy(String version, Path downloadDir) { 73 | URL url = new URL("https://github.com/aquasecurity/trivy/releases/download/v${version}/trivy_${version}_Linux-64bit.tar.gz") 74 | File archive = downloadDir.resolve("trivy.tar.gz").toFile() 75 | archive.createNewFile() 76 | logger.info("download trivy v${version} from $url to $archive") 77 | 78 | ReadableByteChannel readableByteChannel = Channels.newChannel(url.openStream()) 79 | FileOutputStream fileOutputStream = new FileOutputStream(archive) 80 | FileChannel fileChannel = fileOutputStream.getChannel() 81 | fileChannel.transferFrom(readableByteChannel, 0, Long.MAX_VALUE) 82 | return archive 83 | } 84 | 85 | private static void untar(File archive, Path destination) throws IOException { 86 | BufferedInputStream inputStream = new BufferedInputStream(archive.newInputStream()) 87 | TarArchiveInputStream tar = new TarArchiveInputStream(new GzipCompressorInputStream(inputStream)) 88 | logger.info("untar $archive to $destination") 89 | try { 90 | ArchiveEntry entry 91 | while ((entry = tar.getNextEntry()) != null) { 92 | Path extractTo = entry.resolveIn(destination) 93 | logger.info("untar: extract entry to ${extractTo}") 94 | Files.createDirectories(extractTo.getParent()) 95 | Files.copy(tar, extractTo) 96 | } 97 | } finally { 98 | inputStream.close() 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /test/com/cloudogu/ces/cesbuildlib/TrivyTest.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | 3 | import org.junit.jupiter.api.Test 4 | import org.mockito.invocation.InvocationOnMock 5 | import org.mockito.stubbing.Answer 6 | import org.opentest4j.AssertionFailedError 7 | 8 | import java.nio.file.Files 9 | import java.nio.file.Path 10 | import java.nio.file.Paths 11 | import java.util.concurrent.TimeUnit 12 | 13 | import static org.junit.jupiter.api.Assertions.* 14 | import static org.mockito.ArgumentMatchers.any 15 | import static org.mockito.ArgumentMatchers.matches 16 | import static org.mockito.Mockito.mock 17 | import static org.mockito.Mockito.when 18 | 19 | class TrivyTest { 20 | 21 | String additionalFlags = "--db-repository public.ecr.aws/aquasecurity/trivy-db --java-db-repository public.ecr.aws/aquasecurity/trivy-java-db" 22 | Path installDir = Paths.get("target/trivyInstalls") 23 | Path workDir = Paths.get("") 24 | TrivyExecutor trivyExec = new TrivyExecutor(installDir) 25 | String trivyImage = "aquasec/trivy:" + Trivy.DEFAULT_TRIVY_VERSION 26 | 27 | 28 | ScriptMock doTestScan(String imageName, String severityLevel, String strategy, int expectedStatusCode) { 29 | File trivyReportFile = new File("trivy/trivyReport.json") 30 | Path trivyDir = Paths.get(trivyReportFile.getParent()) 31 | String trivyArguments = "image --exit-code 10 --exit-on-eol 10 --format ${TrivyScanFormat.JSON} -o ${trivyReportFile} --severity ${severityLevel} ${additionalFlags} ${imageName}" 32 | String expectedTrivyCommand = "trivy $trivyArguments" 33 | 34 | def scriptMock = new ScriptMock() 35 | scriptMock.env.WORKSPACE = "/test" 36 | Docker dockerMock = mock(Docker.class) 37 | Docker.Image imageMock = mock(Docker.Image.class) 38 | when(dockerMock.image(trivyImage)).thenReturn(imageMock) 39 | when(imageMock.mountJenkinsUser()).thenReturn(imageMock) 40 | when(imageMock.mountDockerSocket()).thenReturn(imageMock) 41 | when(imageMock.inside(matches("-v /test/.trivy/.cache:/root/.cache/"), any())).thenAnswer(new Answer() { 42 | @Override 43 | Integer answer(InvocationOnMock invocation) throws Throwable { 44 | // mock "sh trivy" so that it returns the expected status code and check trivy arguments 45 | Closure closure = invocation.getArgument(1) 46 | scriptMock.expectedShRetValueForScript.put(expectedTrivyCommand, expectedStatusCode) 47 | Integer statusCode = closure.call() as Integer 48 | assertEquals(expectedStatusCode, statusCode) 49 | assertEquals(expectedTrivyCommand, scriptMock.getActualShMapArgs().getLast()) 50 | 51 | // emulate trivy call with local trivy installation and check that it has the same behavior 52 | Files.createDirectories(trivyDir) 53 | Process process = trivyExec.exec(Trivy.DEFAULT_TRIVY_VERSION, trivyArguments, workDir) 54 | if (process.waitFor(2, TimeUnit.MINUTES)) { 55 | assertEquals(expectedStatusCode, process.exitValue()) 56 | } else { 57 | process.destroyForcibly() 58 | fail("terminate trivy due to timeout") 59 | } 60 | 61 | return expectedStatusCode 62 | } 63 | }) 64 | Trivy trivy = new Trivy(scriptMock, Trivy.DEFAULT_TRIVY_VERSION, Trivy.DEFAULT_TRIVY_IMAGE, dockerMock) 65 | 66 | trivy.scanImage(imageName, severityLevel, strategy) 67 | 68 | return scriptMock 69 | } 70 | 71 | @Test 72 | void testScanImage_successfulTrivyExecution() { 73 | // with hopes that this image will never have CVEs 74 | String imageName = "hello-world" 75 | String severityLevel = TrivySeverityLevel.CRITICAL 76 | 77 | def scriptMock = doTestScan(imageName, severityLevel, TrivyScanStrategy.UNSTABLE, 0) 78 | 79 | assertEquals(false, scriptMock.getUnstable()) 80 | } 81 | 82 | @Test 83 | void testScanImage_unstableBecauseOfCVEs() { 84 | // with hopes that this image will always have CVEs 85 | String imageName = "alpine:3.18.7" 86 | String severityLevel = TrivySeverityLevel.ALL 87 | 88 | def scriptMock = doTestScan(imageName, severityLevel, TrivyScanStrategy.UNSTABLE, 10) 89 | 90 | assertEquals(true, scriptMock.getUnstable()) 91 | } 92 | 93 | @Test 94 | void testScanImage_failBecauseOfCVEs() { 95 | // with hopes that this image will always have CVEs 96 | String imageName = "alpine:3.18.7" 97 | String severityLevel = TrivySeverityLevel.ALL 98 | 99 | def gotException = false 100 | try { 101 | doTestScan(imageName, severityLevel, TrivyScanStrategy.FAIL, 10) 102 | } catch (AssertionFailedError e) { 103 | // exception could also be a junit assertion exception. This means a previous assertion failed 104 | throw e 105 | } catch (Exception e) { 106 | assertTrue(e.getMessage().contains("Trivy has found vulnerabilities in image"), "exception is: ${e.getMessage()}") 107 | gotException = true 108 | } 109 | assertTrue(gotException) 110 | } 111 | 112 | @Test 113 | void testScanImage_unsuccessfulTrivyExecution() { 114 | String imageName = "inval!d:::///1.1...1.1." 115 | String severityLevel = TrivySeverityLevel.ALL 116 | 117 | def gotException = false 118 | try { 119 | doTestScan(imageName, severityLevel, TrivyScanStrategy.FAIL, 1) 120 | } catch (AssertionFailedError e) { 121 | // exception could also be a junit assertion exception. This means a previous assertion failed 122 | throw e 123 | } catch (Exception e) { 124 | assertTrue(e.getMessage().contains("Error during trivy scan; exit code: 1"), "exception is: ${e.getMessage()}") 125 | gotException = true 126 | } 127 | assertTrue(gotException) 128 | } 129 | 130 | @Test 131 | void testSaveFormattedTrivyReport_HtmlAllSeverities() { 132 | Trivy trivy = mockTrivy( 133 | "template --template \"@/contrib/html.tpl\"", 134 | "UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL", 135 | "trivy/formattedTrivyReport.html") 136 | trivy.saveFormattedTrivyReport() 137 | } 138 | 139 | @Test 140 | void testSaveFormattedTrivyReport_JsonCriticalSeverity() { 141 | Trivy trivy = mockTrivy( 142 | "json", 143 | "CRITICAL", 144 | "trivy/formattedTrivyReport.json") 145 | trivy.saveFormattedTrivyReport(TrivyScanFormat.JSON, TrivySeverityLevel.CRITICAL) 146 | } 147 | 148 | @Test 149 | void testSaveFormattedTrivyReport_TableHighAndUpSeverity() { 150 | Trivy trivy = mockTrivy( 151 | "table", 152 | "CRITICAL,HIGH", 153 | "trivy/formattedTrivyReport.table") 154 | trivy.saveFormattedTrivyReport(TrivyScanFormat.TABLE, TrivySeverityLevel.HIGH_AND_ABOVE) 155 | } 156 | 157 | @Test 158 | void testSaveFormattedTrivyReport_MediumAndUpSeverity() { 159 | Trivy trivy = mockTrivy( 160 | "sarif", 161 | "CRITICAL,HIGH,MEDIUM", 162 | "trivy/formattedTrivyReport.txt") 163 | trivy.saveFormattedTrivyReport("sarif", TrivySeverityLevel.MEDIUM_AND_ABOVE) 164 | } 165 | 166 | @Test 167 | void testSaveFormattedTrivyReport_CustomFilename() { 168 | Trivy trivy = mockTrivy( 169 | "json", 170 | "CRITICAL,HIGH,MEDIUM", 171 | "trivy/myOutput.custom") 172 | trivy.saveFormattedTrivyReport(TrivyScanFormat.JSON, TrivySeverityLevel.MEDIUM_AND_ABOVE, "myOutput.custom") 173 | } 174 | 175 | @Test 176 | void testSaveFormattedTrivyReport_UnsupportedFormat() { 177 | def scriptMock = new ScriptMock() 178 | scriptMock.env.WORKSPACE = "/test" 179 | Trivy trivy = new Trivy(scriptMock, Trivy.DEFAULT_TRIVY_VERSION, Trivy.DEFAULT_TRIVY_IMAGE, mock(Docker.class)) 180 | def gotException = false 181 | try { 182 | trivy.saveFormattedTrivyReport("UnsupportedFormat", TrivySeverityLevel.MEDIUM_AND_ABOVE) 183 | } catch (AssertionFailedError e) { 184 | // exception could also be a junit assertion exception. This means a previous assertion failed 185 | throw e 186 | } catch (Exception e) { 187 | assertTrue(e.getMessage().contains("This format did not match the supported formats"), "exception is: ${e.getMessage()}") 188 | gotException = true 189 | } 190 | assertTrue(gotException) 191 | } 192 | 193 | @Test 194 | void testSaveFormattedTrivyReport_UnsupportedSeverity() { 195 | def scriptMock = new ScriptMock() 196 | scriptMock.env.WORKSPACE = "/test" 197 | Trivy trivy = new Trivy(scriptMock, Trivy.DEFAULT_TRIVY_VERSION, Trivy.DEFAULT_TRIVY_IMAGE, mock(Docker.class)) 198 | def gotException = false 199 | try { 200 | trivy.saveFormattedTrivyReport(TrivyScanFormat.JSON, "UnsupportedSeverity") 201 | } catch (AssertionFailedError e) { 202 | // exception could also be a junit assertion exception. This means a previous assertion failed 203 | throw e 204 | } catch (Exception e) { 205 | assertTrue(e.getMessage().contains("The severity levels provided (UnsupportedSeverity) do not match the " + 206 | "applicable levels (UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL)."), "exception is: ${e.getMessage()}") 207 | gotException = true 208 | } 209 | assertTrue(gotException) 210 | } 211 | 212 | Trivy mockTrivy(String expectedFormat, String expectedSeverity, String expectedOutput) { 213 | String trivyArguments = "convert --format ${expectedFormat} --severity ${expectedSeverity} --output ${expectedOutput} trivy/trivyReport.json" 214 | String expectedTrivyCommand = "trivy $trivyArguments" 215 | 216 | def scriptMock = new ScriptMock() 217 | scriptMock.env.WORKSPACE = "/test" 218 | Docker dockerMock = mock(Docker.class) 219 | Docker.Image imageMock = mock(Docker.Image.class) 220 | when(dockerMock.image(trivyImage)).thenReturn(imageMock) 221 | when(imageMock.inside(matches("-v /test/.trivy/.cache:/root/.cache/"), any())).thenAnswer(new Answer() { 222 | @Override 223 | Integer answer(InvocationOnMock invocation) throws Throwable { 224 | // mock "sh trivy" so that it returns the expected status code and check trivy arguments 225 | Closure closure = invocation.getArgument(1) 226 | scriptMock.expectedShRetValueForScript.put(expectedTrivyCommand, 0) 227 | closure.call() 228 | assertEquals(expectedTrivyCommand, scriptMock.getActualShMapArgs().getLast()) 229 | return 0 230 | } 231 | }) 232 | Trivy trivy = new Trivy(scriptMock, Trivy.DEFAULT_TRIVY_VERSION, Trivy.DEFAULT_TRIVY_IMAGE, dockerMock) 233 | return trivy 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /test/com/cloudogu/ces/cesbuildlib/findEmailRecipientsTest.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | 3 | import com.lesfurets.jenkins.unit.BasePipelineTest 4 | import org.junit.jupiter.api.BeforeEach 5 | import org.junit.jupiter.api.Test 6 | 7 | class FindEmailRecipientsTest extends BasePipelineTest { 8 | 9 | String expectedDefaultRecipients = 'a@b.c,d@e.f' 10 | String expectedCommitAuthor = 'q@q.q' 11 | def script 12 | 13 | @Override 14 | @BeforeEach 15 | void setUp() throws Exception { 16 | super.setUp() 17 | script = loadScript('vars/findEmailRecipients.groovy') 18 | } 19 | 20 | @Test 21 | void developBranch() { 22 | script.env = [BRANCH_NAME: 'develop'] 23 | setCommitAuthor(expectedCommitAuthor) 24 | 25 | def actualRecipients = script.call(expectedDefaultRecipients) 26 | 27 | assert actualRecipients == "$expectedDefaultRecipients,$expectedCommitAuthor" 28 | } 29 | 30 | @Test 31 | void masterBranch() { 32 | script.env = [BRANCH_NAME: 'master'] 33 | setCommitAuthor(expectedCommitAuthor) 34 | 35 | def actualRecipients = script.call(expectedDefaultRecipients) 36 | 37 | assert actualRecipients == "$expectedDefaultRecipients,$expectedCommitAuthor" 38 | } 39 | 40 | @Test 41 | void unstableBranch() { 42 | script.env = [BRANCH_NAME: 'someBranch'] 43 | setCommitAuthor(expectedCommitAuthor) 44 | 45 | def actualRecipients = script.call(expectedDefaultRecipients) 46 | 47 | assert actualRecipients == expectedCommitAuthor 48 | } 49 | 50 | @Test 51 | void stableBranchCommitAuthorContainedInDefaultRecipients() { 52 | script.env = [BRANCH_NAME: 'master'] 53 | setCommitAuthor('d@e.f') 54 | 55 | def actualRecipients = script.call('a@b.c,d@e.f') 56 | 57 | assert actualRecipients == 'a@b.c,d@e.f' 58 | } 59 | 60 | @Test 61 | void commitAuthorEmpty() { 62 | script.env = [BRANCH_NAME: 'someBranch'] 63 | setCommitAuthor('') 64 | 65 | def actualRecipients = script.call(expectedDefaultRecipients) 66 | 67 | assert actualRecipients == expectedDefaultRecipients 68 | } 69 | 70 | def setCommitAuthor(String commitAuthor) { 71 | helper.registerAllowedMethod("sh", [Map.class], { paramMap -> "<$commitAuthor>" }) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /test/com/cloudogu/ces/cesbuildlib/findHostnameTest.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | 3 | import com.lesfurets.jenkins.unit.BasePipelineTest 4 | import org.junit.jupiter.api.BeforeEach 5 | import org.junit.jupiter.api.Test 6 | 7 | import static groovy.test.GroovyAssert.shouldFail 8 | 9 | class FindHostnameTest extends BasePipelineTest { 10 | 11 | def script 12 | String errorParam = "" 13 | 14 | @Override 15 | @BeforeEach 16 | void setUp() throws Exception { 17 | super.setUp() 18 | script = loadScript('vars/findHostName.groovy') 19 | helper.registerAllowedMethod("error", [String.class], { arg -> 20 | errorParam = arg 21 | throw new RuntimeException("Mocked error") 22 | }) 23 | } 24 | 25 | @Test 26 | void developBranchHttp() { 27 | script.env = [JENKINS_URL: 'http://jenkins.url:123/jenkins'] 28 | 29 | def actualHostname = script.call() 30 | 31 | assert actualHostname == 'jenkins.url' 32 | } 33 | 34 | @Test 35 | void developBranchHttps() { 36 | script.env = [JENKINS_URL: 'https://jenkins.url:123/jenkins'] 37 | 38 | def actualHostname = script.call() 39 | 40 | assert actualHostname == 'jenkins.url' 41 | } 42 | 43 | @Test 44 | void developBranchInvalid() { 45 | script.env = [JENKINS_URL: 'df://jenkins.url:123/jenkins'] 46 | 47 | def exception = shouldFail { 48 | script.call() 49 | } 50 | 51 | assert 'Mocked error' == exception.getMessage() 52 | errorParam = 'Unable to determine hostname from env.JENKINS_URL. Expecting http(s)://server:port/jenkins' 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /test/com/cloudogu/ces/cesbuildlib/isPullRequestTest.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | 3 | import com.lesfurets.jenkins.unit.BasePipelineTest 4 | import org.junit.jupiter.api.BeforeEach 5 | import org.junit.jupiter.api.Test 6 | 7 | class IsPullRequestTest extends BasePipelineTest { 8 | 9 | @Override 10 | @BeforeEach 11 | void setUp() throws Exception { 12 | super.setUp() 13 | } 14 | 15 | @Test 16 | void isPullRequest() { 17 | 18 | def script = loadScript('vars/isPullRequest.groovy') 19 | script.env = [CHANGE_ID: 'PR-42'] 20 | 21 | def isPullRequest = script.call() 22 | 23 | assert isPullRequest == true 24 | } 25 | 26 | @Test 27 | void isNotPullRequestChangeIdNull() { 28 | 29 | def script = loadScript('vars/isPullRequest.groovy') 30 | script.env = [CHANGE_ID: null] 31 | 32 | def isPullRequest = script.call() 33 | 34 | assert isPullRequest == false 35 | } 36 | 37 | @Test 38 | void isNotPullRequestChangeIdEmpty() { 39 | 40 | def script = loadScript('vars/isPullRequest.groovy') 41 | script.env = [CHANGE_ID: ''] 42 | 43 | def isPullRequest = script.call() 44 | 45 | assert isPullRequest == false 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /test/com/cloudogu/ces/cesbuildlib/mailIfStatusChangedTest.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | 3 | import com.lesfurets.jenkins.unit.BasePipelineTest 4 | import org.junit.jupiter.api.BeforeEach 5 | import org.junit.jupiter.api.Test 6 | 7 | class MailIfStatusChangedTest extends BasePipelineTest { 8 | 9 | @Override 10 | @BeforeEach 11 | void setUp() throws Exception { 12 | super.setUp() 13 | } 14 | 15 | @Test 16 | void mailIfStatusChangedTest() { 17 | def stepParams = [:] 18 | helper.registerAllowedMethod("step", [Map.class], {paramMap -> stepParams = paramMap}) 19 | binding.getVariable('currentBuild').currentResult = 'Not SUCCESS' 20 | def expectedResult = 'Do not change this' 21 | binding.getVariable('currentBuild').result = expectedResult 22 | 23 | def script = loadScript('vars/mailIfStatusChanged.groovy') 24 | script.call('a@b.cd') 25 | assert stepParams.recipients == 'a@b.cd' 26 | assert stepParams.$class == 'Mailer' 27 | assert binding.getVariable('currentBuild').result == expectedResult 28 | } 29 | 30 | @Test 31 | void mailIfStatusChangedIfStatusIsBackToNormal() { 32 | def stepParams = [:] 33 | helper.registerAllowedMethod("step", [Map.class], {paramMap -> stepParams = paramMap}) 34 | binding.getVariable('currentBuild').result = null 35 | binding.getVariable('currentBuild').currentResult = 'SUCCESS' 36 | 37 | def script = loadScript('vars/mailIfStatusChanged.groovy') 38 | script.call('a@b.cd') 39 | assert stepParams.recipients == 'a@b.cd' 40 | assert stepParams.$class == 'Mailer' 41 | assert binding.getVariable('currentBuild').result == 'SUCCESS' 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /vars/checkChangelog.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | 3 | def call(Changelog changelog = new Changelog(this)) { 4 | // Checking if this is associated with a pull request 5 | if (env.CHANGE_TARGET) { 6 | echo "Checking changelog..." 7 | String newChanges = changelog.changesForVersion('Unreleased') 8 | if (!newChanges || newChanges.allWhitespace) { 9 | unstable('CHANGELOG.md should contain new change entries in the `[Unreleased]` section but none were found.') 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /vars/checkReleaseNotes.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | 3 | def call(ReleaseNotes releaseNotes = new ReleaseNotes(this)) { 4 | // Checking if this is associated with a pull request 5 | if (env.CHANGE_TARGET) { 6 | echo "Checking release notes..." 7 | String newChangesReleaseNotesDE = releaseNotes.changesForDEVersion("Unreleased") 8 | if (!newChangesReleaseNotesDE || newChangesReleaseNotesDE.allWhitespace) { 9 | unstable('Release Notes should contain new change entries in the `[Unreleased]` section but none were found in the german version') 10 | } 11 | String newChangesReleaseNotesEN = releaseNotes.changesForENVersion("Unreleased") 12 | if (!newChangesReleaseNotesEN || newChangesReleaseNotesEN.allWhitespace) { 13 | unstable('Release Notes should contain new change entries in the `[Unreleased]` section but none were found in the english version') 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /vars/findEmailRecipients.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | 3 | def call(String defaultRecipients) { 4 | def isStableBranch = env.BRANCH_NAME in ['master', 'develop'] 5 | String commitAuthorEmail = new Git(this).commitAuthorEmail 6 | 7 | if (commitAuthorEmail.isEmpty()) { 8 | return defaultRecipients 9 | } 10 | 11 | if (isStableBranch) { 12 | 13 | if (!defaultRecipients.contains(commitAuthorEmail)) { 14 | defaultRecipients += ",$commitAuthorEmail" 15 | } 16 | 17 | return defaultRecipients 18 | 19 | } else { 20 | return commitAuthorEmail 21 | } 22 | } -------------------------------------------------------------------------------- /vars/findEmailRecipients.txt: -------------------------------------------------------------------------------- 1 | Determines the email recipients: 2 | For branches that are considered unstable (all except for 'master' and 'develop') only the Git author is returned 3 | (if present). 4 | Otherwise, the default recipients (passed as parameter) and git author are returned. 5 | 6 | Git author can be contained in default recipients. 7 | 8 | Example: 9 | 10 | findEmailRecipients('a@b.cd,123@xy.z') 11 | 12 | Returns a comma-separated list of email addresses, depending on the branch: the git author (if present) for unstable 13 | branches. Otherwise the default recipients passed as parameter. -------------------------------------------------------------------------------- /vars/findHostName.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | 3 | def call() { 4 | String regexMatchesHostName = 'https?://([^:/]*)' 5 | 6 | // Storing matcher in a variable might lead to java.io.NotSerializableException: java.util.regex.Matcher 7 | if (!(env.JENKINS_URL =~ regexMatchesHostName)) { 8 | error 'Unable to determine hostname from env.JENKINS_URL. Expecting http(s)://server:port/jenkins' 9 | } 10 | return (env.JENKINS_URL =~ regexMatchesHostName)[0][1] 11 | } -------------------------------------------------------------------------------- /vars/findHostName.txt: -------------------------------------------------------------------------------- 1 | Returns the hostname of the current Jenkins instance. 2 | For example, ff running on "http(s)://server:port/jenkins", "server" is returned. -------------------------------------------------------------------------------- /vars/findVulnerabilitiesWithTrivy.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | 3 | //findVulnerabilitiesWithTrivy([ imageName: 'nginx', severity=[ 'HIGH, CRITICAL' ], additionalFlags: '--ignore-unfixed' ,trivyVersion: '0.41.0' ]) 4 | // Use a .trivyignore file for allowed CVEs 5 | // If no vulnerabilities are found or no imageName was passed an empty List is returned 6 | // Otherwise the list with all vulnerabilities (excluding the ones in the .trivyignore if one was passed) 7 | ArrayList call (Map args) { 8 | //imageName is mandatory 9 | if(validateArgs(args)) { 10 | if(args.containsKey('allowList')) 11 | error "Arg allowList is deprecated, please use .trivyignore file" 12 | def imageName = args.imageName 13 | def trivyVersion = args.trivyVersion ? args.trivyVersion : '0.57.1' 14 | def severityFlag = args.severity ? "${args.severity.join(',')}" : '' 15 | def additionalFlags = args.additionalFlags ? args.additionalFlags : '' 16 | println(severityFlag) 17 | 18 | 19 | sh "mkdir -p .trivy/.cache" 20 | 21 | return getVulnerabilities(trivyVersion as String, severityFlag as String, additionalFlags as String, imageName as String) 22 | } else { 23 | error "There was no imageName to be processed. An imageName is mandatory to check for vulnerabilities." 24 | return [] 25 | } 26 | } 27 | 28 | ArrayList getVulnerabilities(String trivyVersion, String severityFlag, String additionalFlags,String imageName) { 29 | // this runs trivy and creates an output file with found vulnerabilities 30 | Trivy trivy = new Trivy(this, trivyVersion) 31 | trivy.scanImage(imageName, severityFlag, TrivyScanStrategy.UNSTABLE, additionalFlags, "${env.WORKSPACE}/.trivy/trivyOutput.json") 32 | 33 | def trivyOutput = readJSON file: "${env.WORKSPACE}/.trivy/trivyOutput.json" 34 | 35 | def vulnerabilities = [] 36 | for (int i = 0; i < trivyOutput.Results.size(); i++) { 37 | 38 | if(trivyOutput.Results[i].Vulnerabilities != null ) { 39 | vulnerabilities += trivyOutput.Results[i].Vulnerabilities 40 | } 41 | } 42 | return vulnerabilities 43 | 44 | } 45 | 46 | static boolean validateArgs(Map args) { 47 | return !(args == null || args.imageName == null || args.imageName == '') 48 | } 49 | -------------------------------------------------------------------------------- /vars/findVulnerabilitiesWithTrivy.txt: -------------------------------------------------------------------------------- 1 | Returns a list of vulnerabilities or an empty list if there are no vulnerabilities for the given severity. -------------------------------------------------------------------------------- /vars/isBuildSuccessful.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | 3 | 4 | boolean call() { 5 | currentBuild.currentResult == 'SUCCESS' && 6 | // Build result == SUCCESS seems not to set be during pipeline execution. 7 | (currentBuild.result == null || currentBuild.result == 'SUCCESS') 8 | } 9 | -------------------------------------------------------------------------------- /vars/isBuildSuccessful.txt: -------------------------------------------------------------------------------- 1 | Returns true if the build is successful, i.e. not failed or unstable (yet). -------------------------------------------------------------------------------- /vars/isPullRequest.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | 3 | def call() { 4 | // CHANGE_ID == pull request id 5 | // http://stackoverflow.com/questions/41695530/how-to-get-pull-request-id-from-jenkins-pipeline 6 | env.CHANGE_ID != null && env.CHANGE_ID.length() > 0 7 | } -------------------------------------------------------------------------------- /vars/isPullRequest.txt: -------------------------------------------------------------------------------- 1 | Returns true if the current build is a pull request (when the CHANGE_IDenvironment variable is set). 2 | Tested with GitHub. -------------------------------------------------------------------------------- /vars/lintDockerfile.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | 3 | @Deprecated 4 | def call(String dockerfile = "Dockerfile") { 5 | docker.image('hadolint/hadolint:latest-debian').inside(){ 6 | sh "hadolint --no-color -t error " + 7 | "--trusted-registry docker.io --trusted-registry gcr.io --trusted-registry registry.cloudogu.com " + 8 | "${WORKSPACE}/${dockerfile}" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /vars/mailIfStatusChanged.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.ces.cesbuildlib 2 | 3 | 4 | def call(String recipients) { 5 | 6 | // Also send "back to normal" emails and . Mailer seems to check build result, but SUCCESS is not set during pipeline execution. 7 | if (!currentBuild.result) { 8 | currentBuild.result = currentBuild.currentResult 9 | } 10 | 11 | step([$class: 'Mailer', recipients: recipients, 12 | sendToIndividuals: true, 13 | // Necessary for "still unstable" emails 14 | notifyEveryUnstableBuild: true]) 15 | } 16 | -------------------------------------------------------------------------------- /vars/mailIfStatusChanged.txt: -------------------------------------------------------------------------------- 1 | Provides the functionality of the Jenkins Post-build Action "E-mail Notification" known from freestyle projects. 2 | That is, it sends an email when the build status is `FAILED`, and (after failed builds) when the build is back to normal. 3 | 4 | Example: 5 | mailIfStatusChanged('a@b.cd,123@xy.z') 6 | 7 | Sends the email (if necessary) to the string passed. Expects a comma-separated list of email addresses. -------------------------------------------------------------------------------- /vars/shellCheck.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * shellcheck for jenkins-pipelines https://github.com/koalaman/shellcheck 3 | * 4 | */ 5 | package com.cloudogu.ces.cesbuildlib 6 | 7 | /** 8 | * run shellcheck with a custom fileList 9 | * sample input "test1.sh", "test2.sh" 10 | * 11 | */ 12 | def call(fileList) { 13 | executeWithDocker(fileList) 14 | } 15 | 16 | /** 17 | * run shellcheck on every .sh file inside the project folder 18 | * note: it ignores ./ecosystem folder for usage with ecosystem instances 19 | * 20 | */ 21 | def call() { 22 | def fileList = sh (script: 'find . -path ./ecosystem -prune -o -type f -regex .*\\.sh -print', returnStdout: true) 23 | fileList='"' + fileList.trim().replaceAll('\n','" "') + '"' 24 | executeWithDocker(fileList) 25 | } 26 | 27 | /* 28 | * run the alpine based shellcheck image 29 | * note: we encountered some problems while using the minified docker image 30 | */ 31 | private def executeWithDocker(fileList){ 32 | docker.image('koalaman/shellcheck-alpine:stable').inside(){ 33 | sh "/bin/shellcheck ${fileList}" 34 | } 35 | } 36 | --------------------------------------------------------------------------------