├── .editorconfig ├── .github └── workflows │ └── create-release.yml ├── .gitignore ├── .mvn └── wrapper │ └── maven-wrapper.properties ├── CHANGELOG.md ├── Jenkinsfile ├── LICENSE ├── README.md ├── gitops-build-lib-use-case.drawio.svg ├── mvnw ├── mvnw.cmd ├── pom.xml ├── src └── com │ └── cloudogu │ └── gitopsbuildlib │ ├── GitRepo.groovy │ ├── deployment │ ├── Deployment.groovy │ ├── FolderStructureStrategy.groovy │ ├── GitopsTool.groovy │ ├── SourceType.groovy │ ├── helm │ │ ├── Helm.groovy │ │ ├── helmrelease │ │ │ ├── ArgoCDRelease.groovy │ │ │ ├── FluxV1Release.groovy │ │ │ └── HelmRelease.groovy │ │ └── repotype │ │ │ ├── GitRepo.groovy │ │ │ ├── HelmRepo.groovy │ │ │ ├── LocalRepo.groovy │ │ │ └── RepoType.groovy │ └── plain │ │ └── Plain.groovy │ ├── docker │ └── DockerWrapper.groovy │ ├── scm │ ├── SCMManager.groovy │ └── SCMProvider.groovy │ └── validation │ ├── Deployment.groovy │ ├── HelmKubeval.groovy │ ├── Kubeval.groovy │ ├── Validator.groovy │ ├── Yamllint.groovy │ └── utils │ └── KubevalArgsParser.groovy ├── test └── com │ └── cloudogu │ └── gitopsbuildlib │ ├── DeployViaGitopsTest.groovy │ ├── DockerMock.groovy │ ├── ScriptMock.groovy │ ├── deployment │ ├── DeploymentTest.groovy │ ├── HelmTest.groovy │ ├── PlainTest.groovy │ └── helm │ │ ├── helmrelease │ │ ├── ArgoCDReleaseTest.groovy │ │ ├── FluxV1ReleaseTest.groovy │ │ └── HelmReleaseTest.groovy │ │ └── repotype │ │ ├── GitRepoTest.groovy │ │ └── HelmRepoTest.groovy │ ├── docker │ └── DockerWrapperTest.groovy │ └── validation │ ├── HelmKubevalTest.groovy │ ├── KubevalTest.groovy │ ├── ValidatorTest.groovy │ ├── YamllintTest.groovy │ └── utils │ └── KubevalArgsParserTest.groovy └── vars └── deployViaGitops.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 | -------------------------------------------------------------------------------- /.github/workflows/create-release.yml: -------------------------------------------------------------------------------- 1 | name: Create release 2 | 3 | on: 4 | push: 5 | tags: [ '*' ] 6 | 7 | permissions: 8 | contents: write 9 | 10 | jobs: 11 | release: 12 | name: Release pushed tag 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Create release 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | tag: ${{ github.ref_name }} 19 | run: | 20 | gh release create "$tag" \ 21 | --repo="$GITHUB_REPOSITORY" \ 22 | --title="${tag}" \ 23 | --generate-notes 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | maven-wrapper.jar 4 | target/ 5 | .classpath 6 | .factorypath 7 | .project 8 | .settings/ 9 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ### Added 11 | - Add additional docker runs args support 12 | - Add welcome message and print configs at application start 13 | 14 | ### Changed 15 | - Disable yamllint image due to image is not longer maintained 16 | - Update k8s, helm and yamllint 17 | 18 | ## [0.6.0](https://github.com/cloudogu/gitops-build-lib/releases/tag/0.6.0) - 2024-08-26 19 | 20 | ### Changed 21 | - Change License to agpl3.0 22 | 23 | ## [0.5.0](https://github.com/cloudogu/gitops-build-lib/releases/tag/0.5.0) - 2023-05-03 24 | 25 | ### Added 26 | - Make `deployments.plain` work with `CronJob` 27 | 28 | ## [0.4.0](https://github.com/cloudogu/gitops-build-lib/releases/tag/0.4.0) - 2023-03-21 29 | 30 | ### Added 31 | 32 | - Helm Repo Type `LOCAL` 33 | 34 | ## [0.3.0](https://github.com/cloudogu/gitops-build-lib/releases/tag/0.3.0) - 2023-03-21 35 | 36 | ### Added 37 | - Add `k8sVersion` parameter. 38 | - Specifies `--kube-version ` for `helm template` (ArgoCD) and 39 | - `kubectl` image (if no explicit `buildImages.kubectl` parameter is used) 40 | - ⚠️ `k8sVersion` has a default value, so this is no breaking change per se. However, depending on the helm chart used, 41 | the rendered result might look different from before where no `--kube-version` parameter was used. 42 | We recommend setting the `k8sVersion`. Double-check the commits in your GitOps repo. 43 | 44 | ### Changed 45 | - Changed kubectl image from `lachlanevenson/k8s-kubectl` to `bitnami/kubectl`, because it is available for every k8s version out there 46 | 47 | ### Fixed 48 | - Don't fail when no `values-shared.yaml` is provided 49 | 50 | ## [0.2.0](https://github.com/cloudogu/gitops-build-lib/releases/tag/0.2.0) - 2023-03-10 51 | 52 | ### Added 53 | - Support different base path in destination gitops repository with `destinationRootPath` #26 54 | - Support different folder strategies with `folderStructureStrategy` #26 55 | - Optional `credentialsId` for build images #19 56 | - Add option for other mainbranches in helm git repositories #19 57 | 58 | ### Changed 59 | 60 | - Bump default cesBuildLib version to 1.62.0 #26 61 | - Bump default kubectl image to 'lachlanevenson/k8s-kubectl:v1.24.8' #26 62 | - Bump default helm image to 'ghcr.io/cloudogu/helm:3.11.1-2' #26 63 | 64 | ### Removed 65 | 66 | - Disable kubeval and helm kubeval in default config, because they are deprecated (we will introduce another linting tool later) #26 67 | 68 | ### Fixed 69 | - Add namespace to argo helm release #19 70 | 71 | ## 0.0.1 - 0.1.3 72 | 73 | No change log provided. See GitHub release pages for details, e.g. 74 | https://github.com/cloudogu/gitops-build-lib/releases/tag/0.1.3 75 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | #!groovy 2 | @Library('github.com/cloudogu/ces-build-lib@1.62.0') 3 | import com.cloudogu.ces.cesbuildlib.* 4 | 5 | node('docker') { 6 | 7 | Maven mvn = new MavenWrapperInDocker(this, 'azul/zulu-openjdk-alpine:11.0.10') 8 | 9 | catchError { 10 | 11 | stage('Checkout') { 12 | checkout scm 13 | } 14 | 15 | mvn.useRepositoryCredentials([id: 'ecosystem.cloudogu.com', credentialsId: 'jenkins']) 16 | String versionName = mvn.version 17 | 18 | stage('Build') { 19 | mvn 'clean package -DskipTests' 20 | } 21 | 22 | stage('Test') { 23 | mvn 'test -Dmaven.test.failure.ignore=true' 24 | // Archive test results. Makes build unstable on failed tests. 25 | junit testResults: '**/target/surefire-reports/TEST-*.xml' 26 | } 27 | 28 | // We should enable this next time we're working on this class. For now build and tests are already a huge step forward! 29 | /* stage('Static Code Analysis') { 30 | generateCoverageReportForSonarQube(mvn) 31 | def sonarQube = new SonarQube(this, [sonarQubeEnv: 'ces-sonar']) 32 | 33 | sonarQube.analyzeWith(mvn) 34 | 35 | if (!sonarQube.waitForQualityGateWebhookToBeCalled()) { 36 | unstable("Pipeline unstable due to SonarQube quality gate failure") 37 | } 38 | }*/ 39 | 40 | } 41 | 42 | // Find maven warnings and visualize in job 43 | publishIssues issues: [scanForIssues(tool: mavenConsole())] 44 | 45 | mailIfStatusChanged(new Git(this).commitAuthorEmail) 46 | } 47 | 48 | void generateCoverageReportForSonarQube(Maven mvn) { 49 | mvn 'org.jacoco:jacoco-maven-plugin:0.8.5:report' 50 | } 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published 637 | by the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . 662 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /etc/mavenrc ] ; then 40 | . /etc/mavenrc 41 | fi 42 | 43 | if [ -f "$HOME/.mavenrc" ] ; then 44 | . "$HOME/.mavenrc" 45 | fi 46 | 47 | fi 48 | 49 | # OS specific support. $var _must_ be set to either true or false. 50 | cygwin=false; 51 | darwin=false; 52 | mingw=false 53 | case "`uname`" in 54 | CYGWIN*) cygwin=true ;; 55 | MINGW*) mingw=true;; 56 | Darwin*) darwin=true 57 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 58 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 59 | if [ -z "$JAVA_HOME" ]; then 60 | if [ -x "/usr/libexec/java_home" ]; then 61 | export JAVA_HOME="`/usr/libexec/java_home`" 62 | else 63 | export JAVA_HOME="/Library/Java/Home" 64 | fi 65 | fi 66 | ;; 67 | esac 68 | 69 | if [ -z "$JAVA_HOME" ] ; then 70 | if [ -r /etc/gentoo-release ] ; then 71 | JAVA_HOME=`java-config --jre-home` 72 | fi 73 | fi 74 | 75 | if [ -z "$M2_HOME" ] ; then 76 | ## resolve links - $0 may be a link to maven's home 77 | PRG="$0" 78 | 79 | # need this for relative symlinks 80 | while [ -h "$PRG" ] ; do 81 | ls=`ls -ld "$PRG"` 82 | link=`expr "$ls" : '.*-> \(.*\)$'` 83 | if expr "$link" : '/.*' > /dev/null; then 84 | PRG="$link" 85 | else 86 | PRG="`dirname "$PRG"`/$link" 87 | fi 88 | done 89 | 90 | saveddir=`pwd` 91 | 92 | M2_HOME=`dirname "$PRG"`/.. 93 | 94 | # make it fully qualified 95 | M2_HOME=`cd "$M2_HOME" && pwd` 96 | 97 | cd "$saveddir" 98 | # echo Using m2 at $M2_HOME 99 | fi 100 | 101 | # For Cygwin, ensure paths are in UNIX format before anything is touched 102 | if $cygwin ; then 103 | [ -n "$M2_HOME" ] && 104 | M2_HOME=`cygpath --unix "$M2_HOME"` 105 | [ -n "$JAVA_HOME" ] && 106 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 107 | [ -n "$CLASSPATH" ] && 108 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 109 | fi 110 | 111 | # For Mingw, ensure paths are in UNIX format before anything is touched 112 | if $mingw ; then 113 | [ -n "$M2_HOME" ] && 114 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 115 | [ -n "$JAVA_HOME" ] && 116 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 117 | fi 118 | 119 | if [ -z "$JAVA_HOME" ]; then 120 | javaExecutable="`which javac`" 121 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 122 | # readlink(1) is not available as standard on Solaris 10. 123 | readLink=`which readlink` 124 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 125 | if $darwin ; then 126 | javaHome="`dirname \"$javaExecutable\"`" 127 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 128 | else 129 | javaExecutable="`readlink -f \"$javaExecutable\"`" 130 | fi 131 | javaHome="`dirname \"$javaExecutable\"`" 132 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 133 | JAVA_HOME="$javaHome" 134 | export JAVA_HOME 135 | fi 136 | fi 137 | fi 138 | 139 | if [ -z "$JAVACMD" ] ; then 140 | if [ -n "$JAVA_HOME" ] ; then 141 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 142 | # IBM's JDK on AIX uses strange locations for the executables 143 | JAVACMD="$JAVA_HOME/jre/sh/java" 144 | else 145 | JAVACMD="$JAVA_HOME/bin/java" 146 | fi 147 | else 148 | JAVACMD="`which java`" 149 | fi 150 | fi 151 | 152 | if [ ! -x "$JAVACMD" ] ; then 153 | echo "Error: JAVA_HOME is not defined correctly." >&2 154 | echo " We cannot execute $JAVACMD" >&2 155 | exit 1 156 | fi 157 | 158 | if [ -z "$JAVA_HOME" ] ; then 159 | echo "Warning: JAVA_HOME environment variable is not set." 160 | fi 161 | 162 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 163 | 164 | # traverses directory structure from process work directory to filesystem root 165 | # first directory with .mvn subdirectory is considered project base directory 166 | find_maven_basedir() { 167 | 168 | if [ -z "$1" ] 169 | then 170 | echo "Path not specified to find_maven_basedir" 171 | return 1 172 | fi 173 | 174 | basedir="$1" 175 | wdir="$1" 176 | while [ "$wdir" != '/' ] ; do 177 | if [ -d "$wdir"/.mvn ] ; then 178 | basedir=$wdir 179 | break 180 | fi 181 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 182 | if [ -d "${wdir}" ]; then 183 | wdir=`cd "$wdir/.."; pwd` 184 | fi 185 | # end of workaround 186 | done 187 | echo "${basedir}" 188 | } 189 | 190 | # concatenates all lines of a file 191 | concat_lines() { 192 | if [ -f "$1" ]; then 193 | echo "$(tr -s '\n' ' ' < "$1")" 194 | fi 195 | } 196 | 197 | BASE_DIR=`find_maven_basedir "$(pwd)"` 198 | if [ -z "$BASE_DIR" ]; then 199 | exit 1; 200 | fi 201 | 202 | ########################################################################################## 203 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 204 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 205 | ########################################################################################## 206 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 207 | if [ "$MVNW_VERBOSE" = true ]; then 208 | echo "Found .mvn/wrapper/maven-wrapper.jar" 209 | fi 210 | else 211 | if [ "$MVNW_VERBOSE" = true ]; then 212 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 213 | fi 214 | if [ -n "$MVNW_REPOURL" ]; then 215 | jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 216 | else 217 | jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 218 | fi 219 | while IFS="=" read key value; do 220 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;; 221 | esac 222 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 223 | if [ "$MVNW_VERBOSE" = true ]; then 224 | echo "Downloading from: $jarUrl" 225 | fi 226 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 227 | if $cygwin; then 228 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` 229 | fi 230 | 231 | if command -v wget > /dev/null; then 232 | if [ "$MVNW_VERBOSE" = true ]; then 233 | echo "Found wget ... using wget" 234 | fi 235 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 236 | wget "$jarUrl" -O "$wrapperJarPath" 237 | else 238 | wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" 239 | fi 240 | elif command -v curl > /dev/null; then 241 | if [ "$MVNW_VERBOSE" = true ]; then 242 | echo "Found curl ... using curl" 243 | fi 244 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 245 | curl -o "$wrapperJarPath" "$jarUrl" -f 246 | else 247 | curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f 248 | fi 249 | 250 | else 251 | if [ "$MVNW_VERBOSE" = true ]; then 252 | echo "Falling back to using Java to download" 253 | fi 254 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 255 | # For Cygwin, switch paths to Windows format before running javac 256 | if $cygwin; then 257 | javaClass=`cygpath --path --windows "$javaClass"` 258 | fi 259 | if [ -e "$javaClass" ]; then 260 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 261 | if [ "$MVNW_VERBOSE" = true ]; then 262 | echo " - Compiling MavenWrapperDownloader.java ..." 263 | fi 264 | # Compiling the Java class 265 | ("$JAVA_HOME/bin/javac" "$javaClass") 266 | fi 267 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 268 | # Running the downloader 269 | if [ "$MVNW_VERBOSE" = true ]; then 270 | echo " - Running MavenWrapperDownloader.java ..." 271 | fi 272 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 273 | fi 274 | fi 275 | fi 276 | fi 277 | ########################################################################################## 278 | # End of extension 279 | ########################################################################################## 280 | 281 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 282 | if [ "$MVNW_VERBOSE" = true ]; then 283 | echo $MAVEN_PROJECTBASEDIR 284 | fi 285 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 286 | 287 | # For Cygwin, switch paths to Windows format before running java 288 | if $cygwin; then 289 | [ -n "$M2_HOME" ] && 290 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 291 | [ -n "$JAVA_HOME" ] && 292 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 293 | [ -n "$CLASSPATH" ] && 294 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 295 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 296 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 297 | fi 298 | 299 | # Provide a "standardized" way to retrieve the CLI args that will 300 | # work with both Windows and non-Windows executions. 301 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 302 | export MAVEN_CMD_LINE_ARGS 303 | 304 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 305 | 306 | exec "$JAVACMD" \ 307 | $MAVEN_OPTS \ 308 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 309 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 310 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 311 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM http://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM set title of command window 39 | title %0 40 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 42 | 43 | @REM set %HOME% to equivalent of $HOME 44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 45 | 46 | @REM Execute a user defined script before this one 47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 49 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 50 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 51 | :skipRcPre 52 | 53 | @setlocal 54 | 55 | set ERROR_CODE=0 56 | 57 | @REM To isolate internal variables from possible post scripts, we use another setlocal 58 | @setlocal 59 | 60 | @REM ==== START VALIDATION ==== 61 | if not "%JAVA_HOME%" == "" goto OkJHome 62 | 63 | echo. 64 | echo Error: JAVA_HOME not found in your environment. >&2 65 | echo Please set the JAVA_HOME variable in your environment to match the >&2 66 | echo location of your Java installation. >&2 67 | echo. 68 | goto error 69 | 70 | :OkJHome 71 | if exist "%JAVA_HOME%\bin\java.exe" goto init 72 | 73 | echo. 74 | echo Error: JAVA_HOME is set to an invalid directory. >&2 75 | echo JAVA_HOME = "%JAVA_HOME%" >&2 76 | echo Please set the JAVA_HOME variable in your environment to match the >&2 77 | echo location of your Java installation. >&2 78 | echo. 79 | goto error 80 | 81 | @REM ==== END VALIDATION ==== 82 | 83 | :init 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 122 | 123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 124 | 125 | FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 127 | ) 128 | 129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 131 | if exist %WRAPPER_JAR% ( 132 | if "%MVNW_VERBOSE%" == "true" ( 133 | echo Found %WRAPPER_JAR% 134 | ) 135 | ) else ( 136 | if not "%MVNW_REPOURL%" == "" ( 137 | SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 138 | ) 139 | if "%MVNW_VERBOSE%" == "true" ( 140 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 141 | echo Downloading from: %DOWNLOAD_URL% 142 | ) 143 | 144 | powershell -Command "&{"^ 145 | "$webclient = new-object System.Net.WebClient;"^ 146 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 147 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 148 | "}"^ 149 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ 150 | "}" 151 | if "%MVNW_VERBOSE%" == "true" ( 152 | echo Finished downloading %WRAPPER_JAR% 153 | ) 154 | ) 155 | @REM End of extension 156 | 157 | @REM Provide a "standardized" way to retrieve the CLI args that will 158 | @REM work with both Windows and non-Windows executions. 159 | set MAVEN_CMD_LINE_ARGS=%* 160 | 161 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 162 | if ERRORLEVEL 1 goto error 163 | goto end 164 | 165 | :error 166 | set ERROR_CODE=1 167 | 168 | :end 169 | @endlocal & set ERROR_CODE=%ERROR_CODE% 170 | 171 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 172 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 173 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 174 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 175 | :skipRcPost 176 | 177 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 178 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 179 | 180 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 181 | 182 | exit /B %ERROR_CODE% 183 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 4.0.0 7 | 8 | com.cloudogu 9 | gitops-build-lib 10 | gitops-build-lib 11 | -SNAPSHOT 12 | 13 | 14 | UTF-8 15 | 0.8.8 16 | 11 17 | 11 18 | 19 | 20 | 21 | 22 | org.codehaus.groovy 23 | groovy-all 24 | 2.4.21 25 | 26 | 27 | 28 | 29 | org.codehaus.groovy 30 | groovy-yaml 31 | 3.0.15 32 | test 33 | 34 | 35 | 36 | 37 | org.junit.jupiter 38 | junit-jupiter 39 | 5.9.2 40 | test 41 | 42 | 43 | 44 | org.mockito 45 | mockito-junit-jupiter 46 | 5.1.1 47 | test 48 | 49 | 50 | 51 | org.assertj 52 | assertj-core 53 | 3.24.2 54 | test 55 | 56 | 57 | 58 | com.lesfurets 59 | jenkins-pipeline-unit 60 | 1.17 61 | test 62 | 63 | 64 | 65 | org.spockframework 66 | spock-core 67 | 1.3-groovy-2.4 68 | test 69 | 70 | 71 | 72 | 73 | com.github.cloudogu 74 | ces-build-lib 75 | 76 | 1.62.0 77 | 78 | true 79 | 80 | test 81 | 82 | 83 | 84 | 85 | 86 | 87 | jitpack.io 88 | https://jitpack.io 89 | 90 | 91 | 92 | 93 | jenkins-ci-releases 94 | https://repo.jenkins-ci.org/releases/ 95 | 96 | 97 | 98 | 99 | src 100 | vars 101 | test 102 | 103 | 104 | resources 105 | 106 | 107 | 108 | 109 | 110 | maven-compiler-plugin 111 | 3.11.0 112 | 113 | groovy-eclipse-compiler 114 | 115 | 116 | 117 | org.codehaus.groovy 118 | groovy-eclipse-compiler 119 | 3.7.0 120 | 121 | 122 | org.codehaus.groovy 123 | groovy-eclipse-batch 124 | 2.5.14-02 125 | 126 | 127 | 128 | 129 | 130 | org.apache.maven.plugins 131 | maven-surefire-plugin 132 | 2.22.2 133 | 139 | 140 | 141 | 142 | org.apache.maven.plugins 143 | maven-source-plugin 144 | 3.2.1 145 | 146 | 147 | attach-sources 148 | 149 | jar 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | jenkins 161 | 162 | 163 | 164 | env.BUILD_URL 165 | 166 | 167 | 168 | 169 | 170 | 171 | org.jacoco 172 | jacoco-maven-plugin 173 | ${jacoco.version} 174 | 175 | 176 | initialize 177 | 178 | prepare-agent 179 | 180 | 181 | 182 | report 183 | 184 | report 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | -------------------------------------------------------------------------------- /src/com/cloudogu/gitopsbuildlib/GitRepo.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.gitopsbuildlib 2 | 3 | /** Queries and stores info about current repo and HEAD commit */ 4 | class GitRepo { 5 | 6 | static GitRepo create(git) { 7 | // Constructors can't be used in Jenkins pipelines due to CPS 8 | // https://www.jenkins.io/doc/book/pipeline/cps-method-mismatches/#constructors 9 | return new GitRepo(git.commitAuthorName, git.commitAuthorEmail, git.commitHashShort, git.commitMessage, git.repositoryUrl) 10 | } 11 | 12 | GitRepo(String authorName, String authorEmail, String commitHash, String commitMessage, String repositoryUrl) { 13 | this.authorName = authorName 14 | this.authorEmail = authorEmail 15 | this.commitHash = commitHash 16 | this.commitMessage = commitMessage 17 | this.repositoryUrl = repositoryUrl 18 | } 19 | 20 | final String authorName 21 | final String authorEmail 22 | final String commitHash 23 | final String commitMessage 24 | final String repositoryUrl 25 | } 26 | -------------------------------------------------------------------------------- /src/com/cloudogu/gitopsbuildlib/deployment/Deployment.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.gitopsbuildlib.deployment 2 | 3 | import com.cloudogu.gitopsbuildlib.docker.DockerWrapper 4 | 5 | abstract class Deployment { 6 | 7 | protected String extraResourcesFolder = "" 8 | 9 | static String getConfigDir() { '.config' } 10 | 11 | protected script 12 | protected Map gitopsConfig 13 | 14 | protected DockerWrapper dockerWrapper 15 | 16 | Deployment(def script, def gitopsConfig) { 17 | this.script = script 18 | this.gitopsConfig = gitopsConfig 19 | dockerWrapper = new DockerWrapper(this.script) 20 | } 21 | 22 | def create(String stage) { 23 | createFoldersAndCopyK8sResources(stage) 24 | createFileConfigmaps(stage) 25 | preValidation(stage) 26 | validate(stage) 27 | postValidation(stage) 28 | } 29 | 30 | abstract preValidation(String stage) 31 | abstract postValidation(String stage) 32 | abstract validate(String stage) 33 | 34 | def createFoldersAndCopyK8sResources(String stage) { 35 | def sourcePath = gitopsConfig.deployments.sourcePath 36 | def destinationPath = getDestinationFolder(getFolderStructureStrategy(), stage) 37 | 38 | script.sh "mkdir -p ${destinationPath}/${extraResourcesFolder}" 39 | script.sh "mkdir -p ${configDir}/" 40 | // copy extra resources like sealed secrets 41 | script.echo "Copying k8s payload from application repo to gitOps Repo: '${sourcePath}/${stage}/*' to '${destinationPath}/${extraResourcesFolder}'" 42 | script.sh "cp -r ${script.env.WORKSPACE}/${sourcePath}/${stage}/* ${destinationPath}/${extraResourcesFolder} || true" 43 | script.sh "cp ${script.env.WORKSPACE}/*.yamllint.yaml ${configDir}/ || true" 44 | } 45 | 46 | void createFileConfigmaps(String stage) { 47 | def destinationPath = getDestinationFolder(getFolderStructureStrategy(), stage) 48 | 49 | gitopsConfig.fileConfigmaps.each { 50 | if (stage in it['stage']) { 51 | String key = it['sourceFilePath'].split('/').last() 52 | script.writeFile file: "${destinationPath}/generatedResources/${it['name']}.yaml", text: createConfigMap(key, "${script.env.WORKSPACE}/${gitopsConfig.deployments.sourcePath}/${it['sourceFilePath']}", it['name'], getNamespace(stage)) 53 | } 54 | } 55 | } 56 | 57 | String createConfigMap(String key, String filePath, String name, String namespace) { 58 | String configMap = "" 59 | withDockerImage(gitopsConfig.buildImages.kubectl) { 60 | String kubeScript = "kubectl create configmap ${name} " + 61 | "--from-file=${key}=${filePath} " + 62 | "--dry-run=client -o yaml -n ${namespace}" 63 | 64 | configMap = script.sh returnStdout: true, script: kubeScript 65 | } 66 | return configMap 67 | } 68 | 69 | void withDockerImage(def imageConfig, Closure body) { 70 | dockerWrapper.withDockerImage(imageConfig, body) 71 | } 72 | 73 | String getNamespace(String stage) { 74 | def namespace 75 | if (gitopsConfig.stages."${stage}".containsKey('namespace')) { 76 | namespace = gitopsConfig.stages."${stage}".namespace 77 | } else { 78 | namespace = stage 79 | } 80 | return namespace 81 | } 82 | 83 | String getDestinationFolder(FolderStructureStrategy folderStructureStrategy, String stage) { 84 | 85 | def destinationRootPath = gitopsConfig.deployments.destinationRootPath 86 | 87 | if (destinationRootPath == ".") { 88 | destinationRootPath = "" 89 | } else { 90 | if (!destinationRootPath.endsWith("/")) { 91 | destinationRootPath = destinationRootPath + "/" 92 | } 93 | } 94 | 95 | switch (folderStructureStrategy) { 96 | case FolderStructureStrategy.GLOBAL_ENV: 97 | return "${destinationRootPath}${stage}/${gitopsConfig.application}" 98 | case FolderStructureStrategy.ENV_PER_APP: 99 | return "${destinationRootPath}${gitopsConfig.application}/${stage}" 100 | default: 101 | return null 102 | } 103 | } 104 | 105 | protected GitopsTool getGitopsTool() { 106 | // Already asserted in deployViaGitOps 107 | GitopsTool.get(gitopsConfig.gitopsTool) 108 | } 109 | 110 | protected FolderStructureStrategy getFolderStructureStrategy() { 111 | // Already asserted in deployViaGitOps 112 | FolderStructureStrategy.get(gitopsConfig.folderStructureStrategy) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/com/cloudogu/gitopsbuildlib/deployment/FolderStructureStrategy.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.gitopsbuildlib.deployment 2 | 3 | /** 4 | * Determines which folder structure strategy shall be used. 5 | * Read more about this topic here: https://github.com/cloudogu/gitops-patterns#release-promotion 6 | */ 7 | enum FolderStructureStrategy { 8 | /** 9 | * Uses subfolders for each stage at the root path, with all application-folders located in one of these. 10 | *
11 | *
12 | * Example: 13 | *
    14 | *
  • $ROOTPATH/staging/myapp/
  • 15 | *
  • $ROOTPATH/production/myapp/
  • 16 | *
17 | */ 18 | GLOBAL_ENV, 19 | 20 | /** 21 | * Uses subfolders for each application at the root path, with all subfolders per stage in each of them. 22 | *
23 | *
24 | * Example: 25 | *
    26 | *
  • $ROOTPATH/myapp/staging/
  • 27 | *
  • $ROOTPATH/myapp/production/
  • 28 | *
29 | */ 30 | ENV_PER_APP 31 | 32 | // Creating enums without constructor results in Exception on Jenkins: 33 | // "RejectedAccessException: Scripts not permitted to use new java.util.LinkedHashMap" 🙄 34 | FolderStructureStrategy() {} 35 | 36 | // valueOf() does not work on Jenkins, so create our own 37 | static FolderStructureStrategy get(String potentialStrategy) { 38 | return values().find { it.name() == potentialStrategy } 39 | } 40 | } 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/com/cloudogu/gitopsbuildlib/deployment/GitopsTool.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.gitopsbuildlib.deployment 2 | 3 | enum GitopsTool { 4 | FLUX, ARGO 5 | 6 | // Creating enums without constructor results in Exception on Jenkins: 7 | // "RejectedAccessException: Scripts not permitted to use new java.util.LinkedHashMap" 🙄 8 | GitopsTool() {} 9 | 10 | // valueOf() does not work on Jenkins, so create our own 11 | static GitopsTool get(String potentialTool) { 12 | return values().find { it.name() == potentialTool } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/com/cloudogu/gitopsbuildlib/deployment/SourceType.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.gitopsbuildlib.deployment 2 | 3 | enum SourceType { 4 | HELM, PLAIN 5 | 6 | // Creating enums without constructor results in Exception on Jenkins: 7 | // "RejectedAccessException: Scripts not permitted to use new java.util.LinkedHashMap" 🙄 8 | SourceType() {} 9 | } 10 | -------------------------------------------------------------------------------- /src/com/cloudogu/gitopsbuildlib/deployment/helm/Helm.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.gitopsbuildlib.deployment.helm 2 | 3 | import com.cloudogu.gitopsbuildlib.deployment.Deployment 4 | import com.cloudogu.gitopsbuildlib.deployment.SourceType 5 | import com.cloudogu.gitopsbuildlib.deployment.helm.helmrelease.ArgoCDRelease 6 | import com.cloudogu.gitopsbuildlib.deployment.helm.helmrelease.FluxV1Release 7 | import com.cloudogu.gitopsbuildlib.deployment.helm.helmrelease.HelmRelease 8 | import com.cloudogu.gitopsbuildlib.deployment.helm.repotype.RepoType 9 | 10 | class Helm extends Deployment { 11 | 12 | protected HelmRelease helmRelease 13 | 14 | public static final String HELM_CHART_TEMP_DIR = ".helmChartTempDir" 15 | public static final String CHART_ROOT_DIR = "chart" 16 | 17 | Helm(def script, def gitopsConfig) { 18 | super(script, gitopsConfig) 19 | this.extraResourcesFolder = "extraResources" 20 | 21 | if(gitopsConfig.gitopsTool == 'FLUX') { 22 | helmRelease = new FluxV1Release(script) 23 | } else if(gitopsConfig.gitopsTool == 'ARGO') { 24 | helmRelease = new ArgoCDRelease(script) 25 | } 26 | } 27 | 28 | @Override 29 | def preValidation(String stage) { 30 | def sourcePath = gitopsConfig.deployments.sourcePath 31 | def destinationPath = getDestinationFolder(getFolderStructureStrategy(), stage) 32 | 33 | RepoType repoType = RepoType.create(gitopsConfig.deployments.helm.repoType, script) 34 | repoType.prepareRepo(gitopsConfig, HELM_CHART_TEMP_DIR, CHART_ROOT_DIR) 35 | 36 | // writing the merged-values.yaml via writeYaml into a file has the advantage, that it gets formatted as valid yaml 37 | // This makes it easier to read in and indent for the inline use in the helmRelease. 38 | // It enables us to reuse the `fileToInlineYaml` function, without writing a complex formatting logic. 39 | 40 | def valueFiles = ["${script.env.WORKSPACE}/${sourcePath}/values-${stage}.yaml"] 41 | // only add values-shared.yaml, if it exists 42 | if (script.fileExists("${script.env.WORKSPACE}/${sourcePath}/values-shared.yaml")) { 43 | valueFiles.add("${script.env.WORKSPACE}/${sourcePath}/values-shared.yaml") 44 | } 45 | script.writeFile file: "${script.env.WORKSPACE}/${HELM_CHART_TEMP_DIR}/mergedValues.yaml", text: mergeValuesFiles(gitopsConfig, valueFiles as String[], repoType) 46 | 47 | updateYamlValue("${script.env.WORKSPACE}/${HELM_CHART_TEMP_DIR}/mergedValues.yaml", gitopsConfig) 48 | 49 | script.writeFile file: "${destinationPath}/applicationRelease.yaml", text: helmRelease.create(gitopsConfig, getNamespace(stage), "${script.env.WORKSPACE}/${HELM_CHART_TEMP_DIR}/mergedValues.yaml") 50 | } 51 | 52 | @Override 53 | def postValidation(String stage) { 54 | // clean the helm chart folder since the validation on this helm chart is done 55 | script.sh "rm -rf ${script.env.WORKSPACE}/${HELM_CHART_TEMP_DIR} || true" 56 | } 57 | 58 | @Override 59 | def validate(String stage) { 60 | def destinationPath = getDestinationFolder(getFolderStructureStrategy(), stage) 61 | 62 | gitopsConfig.validators.each { validator -> 63 | validator.value.validator.validate(validator.value.enabled, getGitopsTool(), SourceType.PLAIN, "${destinationPath}", validator.value.config, gitopsConfig) 64 | validator.value.validator.validate(validator.value.enabled, getGitopsTool(), SourceType.HELM, "${script.env.WORKSPACE}/${HELM_CHART_TEMP_DIR}",validator.value.config, gitopsConfig) 65 | } 66 | } 67 | 68 | private void updateYamlValue(String yamlFilePath, Map gitopsConfig) { 69 | def helmConfig = gitopsConfig.deployments.helm 70 | 71 | def data = script.readYaml file: yamlFilePath 72 | helmConfig.updateValues.each { 73 | String[] paths = it["fieldPath"].split("\\.") 74 | def _tmp = data 75 | paths.eachWithIndex { String p, int i -> 76 | def tmp = _tmp.get(p) 77 | if (i == paths.length - 1 && tmp != null) { 78 | _tmp.put(p, it["newValue"]) 79 | } 80 | _tmp = tmp 81 | } 82 | } 83 | script.writeYaml file: yamlFilePath, data: data, overwrite: true 84 | } 85 | 86 | private String mergeValuesFiles(Map gitopsConfig, String[] valuesFiles, RepoType repoType) { 87 | 88 | String mergedValuesFile = "" 89 | 90 | def chartPath = repoType.getChartPath(gitopsConfig, HELM_CHART_TEMP_DIR, CHART_ROOT_DIR) 91 | 92 | withDockerImage(gitopsConfig.buildImages.helm) { 93 | String helmScript = "helm values ${chartPath} ${valuesFilesWithParameter(valuesFiles)}" 94 | mergedValuesFile = script.sh returnStdout: true, script: helmScript 95 | } 96 | return mergedValuesFile 97 | } 98 | 99 | private String valuesFilesWithParameter(String[] valuesFiles) { 100 | String valuesFilesWithParameter = "" 101 | valuesFiles.each { 102 | valuesFilesWithParameter += "-f $it " 103 | } 104 | return valuesFilesWithParameter 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/com/cloudogu/gitopsbuildlib/deployment/helm/helmrelease/ArgoCDRelease.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.gitopsbuildlib.deployment.helm.helmrelease 2 | 3 | import com.cloudogu.gitopsbuildlib.deployment.helm.Helm 4 | import com.cloudogu.gitopsbuildlib.deployment.helm.repotype.RepoType 5 | import com.cloudogu.gitopsbuildlib.docker.DockerWrapper 6 | 7 | class ArgoCDRelease extends HelmRelease { 8 | 9 | protected DockerWrapper dockerWrapper 10 | 11 | ArgoCDRelease(def script) { 12 | super(script) 13 | dockerWrapper = new DockerWrapper(script) 14 | } 15 | 16 | @Override 17 | String create(Map gitopsConfig, String namespace, String mergedValuesFile) { 18 | Map helmConfig = gitopsConfig.deployments.helm 19 | String application = gitopsConfig.application 20 | 21 | if (helmConfig.repoType == 'GIT') { 22 | return createResourcesFromGitRepo(gitopsConfig, application, namespace, mergedValuesFile) 23 | } else if (helmConfig.repoType == 'HELM') { 24 | return createResourcesFromHelmRepo(gitopsConfig, application, namespace, mergedValuesFile) 25 | } else if (helmConfig.repoType == 'LOCAL') { 26 | return createResourcesFromLocalRepo(gitopsConfig, application, namespace, mergedValuesFile) 27 | } 28 | return null // Validated in deployViaGitOps.groovy 29 | } 30 | 31 | private String createResourcesFromGitRepo(Map gitopsConfig, String application, String namespace, String mergedValuesFile) { 32 | return createHelmRelease(gitopsConfig, application, namespace, gitopsConfig.buildImages.helm, mergedValuesFile) 33 | } 34 | 35 | private String createResourcesFromHelmRepo(Map gitopsConfig, String application, String namespace, String mergedValuesFile) { 36 | return createHelmRelease(gitopsConfig, application, namespace, gitopsConfig.buildImages.helm, mergedValuesFile) 37 | } 38 | 39 | private String createResourcesFromLocalRepo(Map gitopsConfig, String application, String namespace, String mergedValuesFile) { 40 | return createHelmRelease(gitopsConfig, application, namespace, gitopsConfig.buildImages.helm, mergedValuesFile) 41 | } 42 | 43 | private String createHelmRelease(Map gitopsConfig, String application, String namespace, def helmImageConfig, String mergedValuesFile) { 44 | String helmRelease = "" 45 | def chartPath = RepoType.create(gitopsConfig.deployments.helm.repoType, script).getChartPath(gitopsConfig, Helm.HELM_CHART_TEMP_DIR, Helm.CHART_ROOT_DIR) 46 | dockerWrapper.withDockerImage(helmImageConfig) { 47 | String templateScript = "helm template ${application} ${chartPath} -n ${namespace} --kube-version ${gitopsConfig.k8sVersion} -f ${mergedValuesFile}" 48 | helmRelease = script.sh returnStdout: true, script: templateScript 49 | } 50 | 51 | // this line removes all empty lines since helm template creates some and the helm validator will throw an error if there are emtpy lines present 52 | helmRelease = helmRelease.replaceAll("(?m)^[ \t]*\r?\n", "") 53 | return helmRelease 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/com/cloudogu/gitopsbuildlib/deployment/helm/helmrelease/FluxV1Release.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.gitopsbuildlib.deployment.helm.helmrelease 2 | 3 | class FluxV1Release extends HelmRelease { 4 | 5 | FluxV1Release(def script) { 6 | super(script) 7 | } 8 | 9 | @Override 10 | String create(Map gitopsConfig, String namespace, String mergedValuesFile) { 11 | Map helmConfig = gitopsConfig.deployments.helm 12 | String application = gitopsConfig.application 13 | 14 | def values = fileToInlineYaml(mergedValuesFile) 15 | def chart = getChart(helmConfig) 16 | return """apiVersion: helm.fluxcd.io/v1 17 | kind: HelmRelease 18 | metadata: 19 | name: ${application} 20 | namespace: ${namespace} 21 | annotations: 22 | fluxcd.io/automated: "false" 23 | spec: 24 | releaseName: ${application} 25 | chart:${chart} 26 | values: 27 | ${values} 28 | """ 29 | } 30 | 31 | private String gitRepoChart(Map helmConfig) { 32 | def chartPath = "." 33 | if (helmConfig.containsKey('chartPath') && helmConfig.chartPath) { 34 | chartPath = helmConfig.chartPath 35 | } 36 | 37 | return """ 38 | git: ${helmConfig.repoUrl} 39 | ref: ${helmConfig.version} 40 | path: ${chartPath}""" 41 | } 42 | 43 | private String helmRepoChart(Map helmConfig) { 44 | return """ 45 | repository: ${helmConfig.repoUrl} 46 | name: ${helmConfig.chartName} 47 | version: ${helmConfig.version}""" 48 | } 49 | 50 | private String getChart(Map helmConfig) { 51 | if (helmConfig.repoType == 'GIT') { 52 | return gitRepoChart(helmConfig) 53 | } else if (helmConfig.repoType == 'HELM') { 54 | return helmRepoChart(helmConfig) 55 | } else if (helmConfig.repoType == 'LOCAL') { 56 | return script.error("Helm repoType LOCAL not supported for fluxv1") 57 | } 58 | return null // Validated in deployViaGitOps.groovy 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/com/cloudogu/gitopsbuildlib/deployment/helm/helmrelease/HelmRelease.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.gitopsbuildlib.deployment.helm.helmrelease 2 | 3 | abstract class HelmRelease { 4 | 5 | protected script 6 | 7 | HelmRelease(def script) { 8 | this.script = script 9 | } 10 | 11 | abstract String create(Map gitopsConfig, String namespace, String mergedValuesFile) 12 | 13 | String fileToInlineYaml(String fileContents) { 14 | String values = "" 15 | String indent = " " 16 | String fileContent = script.readFile fileContents 17 | fileContent.split("\n").each { line -> 18 | if(line.size() > 0) { 19 | values += indent + line + "\n" 20 | } else { 21 | values += line + "\n" 22 | } 23 | } 24 | // remove unnecessary last blank line 25 | return values.substring(0, values.lastIndexOf('\n')) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/com/cloudogu/gitopsbuildlib/deployment/helm/repotype/GitRepo.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.gitopsbuildlib.deployment.helm.repotype 2 | 3 | class GitRepo extends RepoType { 4 | 5 | GitRepo(def script) { 6 | super(script) 7 | } 8 | 9 | @Override 10 | String getChartPath(Map gitopsConfig, String helmChartTempDir, String chartRootDir) { 11 | def helmConfig = gitopsConfig.deployments.helm 12 | def chartPath = '' 13 | if (helmConfig.containsKey('chartPath') && helmConfig.chartPath) { 14 | chartPath = helmConfig.chartPath 15 | } 16 | 17 | return "${script.env.WORKSPACE}/${helmChartTempDir}/${chartRootDir}/${chartPath}" 18 | } 19 | 20 | @Override 21 | void prepareRepo(Map gitopsConfig, String helmChartTempDir, String chartRootDir) { 22 | def helmConfig = gitopsConfig.deployments.helm 23 | 24 | getHelmChartFromGitRepo(helmConfig, helmChartTempDir, chartRootDir) 25 | 26 | def chartPath = '' 27 | if (helmConfig.containsKey('chartPath')) { 28 | chartPath = helmConfig.chartPath 29 | } 30 | 31 | withDockerImage(gitopsConfig.buildImages.helm) { 32 | script.sh "helm dep update ${script.env.WORKSPACE}/.helmChartTempDir/${chartRootDir}/${chartPath}" 33 | } 34 | } 35 | 36 | private getHelmChartFromGitRepo(Map helmConfig, String helmChartTempDir, String chartRootDir) { 37 | def git 38 | 39 | script.dir("${script.env.WORKSPACE}/${helmChartTempDir}/${chartRootDir}/") { 40 | 41 | if (helmConfig.containsKey('credentialsId')) { 42 | git = script.cesBuildLib.Git.new(script, helmConfig.credentialsId) 43 | } else { 44 | git = script.cesBuildLib.Git.new(script) 45 | } 46 | 47 | if (helmConfig.containsKey('mainBranch') && helmConfig.mainBranch) { 48 | git url: helmConfig.repoUrl, branch: helmConfig.mainBranch, changelog: false, poll: false 49 | } else { 50 | git url: helmConfig.repoUrl, branch: 'main', changelog: false, poll: false 51 | } 52 | 53 | if(helmConfig.containsKey('version') && helmConfig.version) { 54 | git.fetch() 55 | git.checkout(helmConfig.version) 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/com/cloudogu/gitopsbuildlib/deployment/helm/repotype/HelmRepo.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.gitopsbuildlib.deployment.helm.repotype 2 | 3 | class HelmRepo extends RepoType { 4 | 5 | HelmRepo(def script) { 6 | super(script) 7 | } 8 | 9 | @Override 10 | String getChartPath(Map gitopsConfig, String helmChartTempDir, String chartRootDir) { 11 | return "${script.env.WORKSPACE}/${helmChartTempDir}/${chartRootDir}/${gitopsConfig.deployments.helm.chartName}" 12 | } 13 | 14 | @Override 15 | void prepareRepo(Map gitopsConfig, String helmChartTempDir, String chartRootDir) { 16 | def helmConfig = gitopsConfig.deployments.helm 17 | 18 | if (helmConfig.containsKey('credentialsId') && helmConfig.credentialsId) { 19 | script.withCredentials([ 20 | script.usernamePassword( 21 | credentialsId: helmConfig.credentialsId, 22 | usernameVariable: 'USERNAME', 23 | passwordVariable: 'PASSWORD') 24 | ]) { 25 | String credentialArgs = " --username ${script.USERNAME} --password ${script.PASSWORD}" 26 | addAndPullRepo(gitopsConfig, helmChartTempDir, chartRootDir, credentialArgs) 27 | } 28 | } else { 29 | addAndPullRepo(gitopsConfig, helmChartTempDir, chartRootDir) 30 | } 31 | } 32 | 33 | private void addAndPullRepo(Map gitopsConfig, String helmChartTempDir, String chartRootDir, String credentialArgs = "") { 34 | def helmConfig = gitopsConfig.deployments.helm 35 | withDockerImage(gitopsConfig.buildImages.helm) { 36 | script.sh "helm repo add chartRepo ${helmConfig.repoUrl}${credentialArgs}" 37 | script.sh "helm repo update" 38 | // helm pull also executes helm dependency so we don't need to do it in this step 39 | script.sh "helm pull chartRepo/${helmConfig.chartName} --version=${helmConfig.version} --untar --untardir=${script.env.WORKSPACE}/${helmChartTempDir}/${chartRootDir}" 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/com/cloudogu/gitopsbuildlib/deployment/helm/repotype/LocalRepo.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.gitopsbuildlib.deployment.helm.repotype 2 | 3 | class LocalRepo extends RepoType { 4 | 5 | LocalRepo(def script) { 6 | super(script) 7 | } 8 | 9 | @Override 10 | void prepareRepo(Map gitopsConfig, String helmChartTempDir, String chartRootDir) { 11 | // Nothing to prepare for a local chart 12 | } 13 | 14 | @Override 15 | String getChartPath(Map gitopsConfig, String helmChartTempDir, String chartRootDir) { 16 | def chartPath = gitopsConfig.deployments.helm.chartPath 17 | // Use absolute paths, so e.g. helm values works in .configRepoTempDir 18 | if (!chartRootDir.contains(script.env.WORKSPACE)) { 19 | chartPath = "${script.env.WORKSPACE}/${chartPath}" 20 | } 21 | return "${chartPath}" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/com/cloudogu/gitopsbuildlib/deployment/helm/repotype/RepoType.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.gitopsbuildlib.deployment.helm.repotype 2 | 3 | import com.cloudogu.gitopsbuildlib.docker.DockerWrapper 4 | 5 | abstract class RepoType { 6 | 7 | protected script 8 | protected DockerWrapper dockerWrapper 9 | 10 | RepoType(def script) { 11 | this.script = script 12 | dockerWrapper = new DockerWrapper(script) 13 | } 14 | 15 | abstract void prepareRepo(Map gitopsConfig, String helmChartTempDir, String chartRootDir) 16 | abstract String getChartPath(Map gitopsConfig, String helmChartTempDir, String chartRootDir) 17 | 18 | void withDockerImage(def imageConfig, Closure body) { 19 | dockerWrapper.withDockerImage(imageConfig, body) 20 | } 21 | 22 | static RepoType create(String potentialRepoType, def script) { 23 | RepoType repoType = null 24 | if (potentialRepoType == 'GIT') { 25 | repoType = new GitRepo(script) 26 | } else if (potentialRepoType == 'HELM') { 27 | repoType = new HelmRepo(script) 28 | } else if (potentialRepoType == 'LOCAL') { 29 | repoType = new LocalRepo(script) 30 | } 31 | return repoType 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/com/cloudogu/gitopsbuildlib/deployment/plain/Plain.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.gitopsbuildlib.deployment.plain 2 | 3 | import com.cloudogu.gitopsbuildlib.deployment.Deployment 4 | import com.cloudogu.gitopsbuildlib.deployment.SourceType 5 | 6 | class Plain extends Deployment { 7 | 8 | Plain(def script, def gitopsConfig) { 9 | super(script, gitopsConfig) 10 | } 11 | 12 | @Override 13 | def preValidation(String stage) { 14 | updateImage(stage) 15 | } 16 | 17 | @Override 18 | def postValidation(String stage) { 19 | } 20 | 21 | @Override 22 | def validate(String stage) { 23 | def destinationPath = getDestinationFolder(getFolderStructureStrategy(), stage) 24 | 25 | gitopsConfig.validators.each { validator -> 26 | validator.value.validator.validate(validator.value.enabled, getGitopsTool(), SourceType.PLAIN, "${destinationPath}", validator.value.config, gitopsConfig) 27 | } 28 | } 29 | 30 | private updateImage(String stage) { 31 | def destinationPath = getDestinationFolder(getFolderStructureStrategy(), stage) 32 | 33 | script.echo "About Updating images in plain deployment: ${gitopsConfig.deployments.plain.updateImages}" 34 | gitopsConfig.deployments.plain.updateImages.each { 35 | script.echo "Replacing image '${it['imageName']}' in file: ${it['filename']}" 36 | def deploymentFilePath = "${destinationPath}/${it['filename']}" 37 | def data = script.readYaml file: deploymentFilePath 38 | String kind = data.kind 39 | def containers = findContainers(data, kind) 40 | script.echo "Found containers '${containers}' in YAML: ${it['filename']}" 41 | def containerName = it['containerName'] 42 | def updateContainer = containers.find { it.name == containerName } 43 | updateContainer.image = it['imageName'] 44 | script.writeYaml file: deploymentFilePath, data: data, overwrite: true 45 | script.echo "Wrote file ${deploymentFilePath} with yaml:\n${data}" 46 | } 47 | } 48 | 49 | def findContainers(def data, String kind) { 50 | //noinspection GroovyFallthrough 51 | switch (kind) { 52 | case 'Deployment': 53 | // Falling through because Deployment and StatefulSet's paths are the same 54 | case 'StatefulSet': 55 | return data.spec.template.spec.containers 56 | case 'CronJob': 57 | return data.spec.jobTemplate.spec.template.spec.containers 58 | default: 59 | script.echo "Warning: Kind '$kind' is unknown, using best effort to find 'containers' in YAML" 60 | // Best effort: Try the same as for Deployment and StatefulSet 61 | return data.spec.template.spec.containers 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/com/cloudogu/gitopsbuildlib/docker/DockerWrapper.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.gitopsbuildlib.docker 2 | 3 | class DockerWrapper { 4 | protected def script 5 | 6 | DockerWrapper(def script) { 7 | this.script = script 8 | } 9 | 10 | void withDockerImage(def imageConfig, Closure body) { 11 | // imageConfig can either be a Map or a String, depending on the old or the new format if this field 12 | // The old format was a String containing an image url. The new one is a map with an image url and optional credentials 13 | if (imageConfig instanceof Map) { 14 | if (imageConfig.containsKey('credentialsId') && imageConfig.credentialsId) { 15 | def registryUrl = getRegistryUrlFromImage(imageConfig.image) 16 | script.docker.withRegistry("https://${registryUrl}", imageConfig.credentialsId) { 17 | runDockerImage(imageConfig.image, body) 18 | } 19 | } else { 20 | runDockerImage(imageConfig.image, body) 21 | } 22 | } else { 23 | // When imageConfig is a String 24 | runDockerImage(imageConfig, body) 25 | } 26 | } 27 | 28 | private String getRegistryUrlFromImage(String image) { 29 | int i = image.lastIndexOf('/') 30 | return image.substring(0, i) 31 | } 32 | 33 | private void runDockerImage(String image, Closure body) { 34 | script.docker.image(image).inside( 35 | // Allow accessing WORKSPACE even when we are in a child dir (using "dir() {}") 36 | "${script.pwd().equals(script.env.WORKSPACE) ? '' : "-v ${script.env.WORKSPACE}:${script.env.WORKSPACE} "}" + 37 | "${script.env.ADDITIONAL_DOCKER_RUN_ARGS ? " ${script.env.ADDITIONAL_DOCKER_RUN_ARGS} " : ''}" + 38 | // Avoid: "ERROR: The container started but didn't run the expected command" 39 | '--entrypoint=""' 40 | ) { 41 | body() 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/com/cloudogu/gitopsbuildlib/scm/SCMManager.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.gitopsbuildlib.scm 2 | 3 | class SCMManager extends SCMProvider { 4 | protected String credentials 5 | protected String baseUrl 6 | protected String repositoryUrl 7 | 8 | 9 | SCMManager(def script) { 10 | super(script) 11 | } 12 | 13 | @Override 14 | void setBaseUrl(String baseUrl) { this.baseUrl = baseUrl } 15 | 16 | @Override 17 | void setRepositoryUrl(String repositoryUrl) { this.repositoryUrl = repositoryUrl } 18 | 19 | @Override 20 | void setCredentials(String credentialsId) { this.credentials = credentialsId } 21 | 22 | @Override 23 | String getRepositoryUrl() { 24 | return "${this.baseUrl}/repo/${this.repositoryUrl}" 25 | } 26 | 27 | @Override 28 | void createOrUpdatePullRequest(String stageBranch, String mainBranch, String title, String description) { 29 | script.cesBuildLib.SCMManager.new(script, this.baseUrl, this.credentials).createOrUpdatePullRequest(this.repositoryUrl, stageBranch, mainBranch, title, description) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/com/cloudogu/gitopsbuildlib/scm/SCMProvider.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.gitopsbuildlib.scm 2 | 3 | abstract class SCMProvider { 4 | 5 | protected script 6 | 7 | SCMProvider(def script) { 8 | this.script = script 9 | } 10 | 11 | abstract void setBaseUrl(String baseUrl) 12 | 13 | abstract void setRepositoryUrl(String repositoryUrl) 14 | 15 | abstract void setCredentials(String credentialsId) 16 | 17 | abstract String getRepositoryUrl() 18 | 19 | abstract void createOrUpdatePullRequest(String stageBranch, String mainBranch, String title, String description) 20 | } 21 | -------------------------------------------------------------------------------- /src/com/cloudogu/gitopsbuildlib/validation/Deployment.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.gitopsbuildlib.validation 2 | 3 | enum Deployment { 4 | HELM('helm'), PLAIN('plain') 5 | 6 | private final String name 7 | 8 | Deployment(String name) { 9 | this.name = name 10 | } 11 | 12 | String getNameValue() { 13 | return name 14 | } 15 | 16 | String toString() { 17 | return name() + " = " + getNameValue() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/com/cloudogu/gitopsbuildlib/validation/HelmKubeval.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.gitopsbuildlib.validation 2 | 3 | import com.cloudogu.gitopsbuildlib.deployment.GitopsTool 4 | import com.cloudogu.gitopsbuildlib.deployment.SourceType 5 | import com.cloudogu.gitopsbuildlib.validation.utils.KubevalArgsParser 6 | 7 | 8 | class HelmKubeval extends Validator { 9 | 10 | KubevalArgsParser argsParser 11 | 12 | HelmKubeval(def script) { 13 | super(script) 14 | argsParser = new KubevalArgsParser() 15 | } 16 | 17 | @Override 18 | void validate(String targetDirectory, Map validatorConfig, Map gitopsConfig) { 19 | Map deployments = gitopsConfig.deployments as Map 20 | String args = argsParser.parse(validatorConfig) 21 | script.sh "helm kubeval ${targetDirectory}/chart/${getChartDir(deployments)} -f ${targetDirectory}/mergedValues.yaml -v ${validatorConfig.k8sSchemaVersion}${args}" 22 | } 23 | 24 | private String getChartDir(Map deployments) { 25 | def chartDir = '' 26 | if (deployments.helm.containsKey('chartPath')) { 27 | chartDir = deployments.helm.chartPath 28 | } else if ( deployments.helm.containsKey('chartName')) { 29 | chartDir = deployments.helm.chartName 30 | } 31 | return chartDir 32 | } 33 | 34 | @Override 35 | SourceType[] getSupportedSourceTypes() { 36 | return [SourceType.HELM] 37 | } 38 | 39 | @Override 40 | GitopsTool[] getSupportedGitopsTools() { 41 | return [GitopsTool.FLUX] 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/com/cloudogu/gitopsbuildlib/validation/Kubeval.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.gitopsbuildlib.validation 2 | 3 | import com.cloudogu.gitopsbuildlib.deployment.GitopsTool 4 | import com.cloudogu.gitopsbuildlib.deployment.SourceType 5 | import com.cloudogu.gitopsbuildlib.validation.utils.KubevalArgsParser 6 | 7 | /** 8 | * Validates all yaml-resources within the target-directory against the specs of the given k8s version 9 | */ 10 | class Kubeval extends Validator { 11 | 12 | KubevalArgsParser argsParser 13 | 14 | Kubeval(def script) { 15 | super(script) 16 | argsParser = new KubevalArgsParser() 17 | } 18 | 19 | @Override 20 | void validate(String targetDirectory, Map validatorConfig, Map gitopsConfig) { 21 | String args = argsParser.parse(validatorConfig) 22 | script.sh "kubeval -d ${targetDirectory} -v ${validatorConfig.k8sSchemaVersion}${args}" 23 | } 24 | 25 | @Override 26 | SourceType[] getSupportedSourceTypes() { 27 | return [SourceType.PLAIN] 28 | } 29 | 30 | @Override 31 | GitopsTool[] getSupportedGitopsTools() { 32 | return [GitopsTool.ARGO, GitopsTool.FLUX] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/com/cloudogu/gitopsbuildlib/validation/Validator.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.gitopsbuildlib.validation 2 | 3 | import com.cloudogu.gitopsbuildlib.deployment.GitopsTool 4 | import com.cloudogu.gitopsbuildlib.deployment.SourceType 5 | import com.cloudogu.gitopsbuildlib.docker.DockerWrapper 6 | import org.codehaus.groovy.runtime.NullObject 7 | 8 | abstract class Validator { 9 | 10 | protected script 11 | protected DockerWrapper dockerWrapper 12 | 13 | Validator(def script) { 14 | this.script = script 15 | dockerWrapper = new DockerWrapper(script) 16 | } 17 | 18 | void validate(boolean enabled, GitopsTool gitopsTool, SourceType sourceType, String targetDirectory, Map validatorConfig, Map gitopsConfig) { 19 | if (enabled && getSupportedGitopsTools().contains(gitopsTool) && getSupportedSourceTypes().contains(sourceType)) { 20 | script.echo "Starting validator ${this.getClass().getSimpleName()} for ${gitopsTool.name()} in ${sourceType.name()} resources" 21 | withDockerImage(getImageConfig(gitopsConfig, validatorConfig)) { 22 | validate(targetDirectory, validatorConfig, gitopsConfig) 23 | } 24 | } else { 25 | script.echo "Skipping validator ${this.getClass().getSimpleName()} because it is configured as enabled=false or doesn't support the given gitopsTool=${gitopsTool.name()} or sourceType=${sourceType.name()}" 26 | } 27 | } 28 | 29 | abstract protected void validate(String targetDirectory, Map validatorConfig, Map gitopsConfig) 30 | abstract SourceType[] getSupportedSourceTypes() 31 | abstract GitopsTool[] getSupportedGitopsTools() 32 | 33 | protected void withDockerImage(def imageConfig, Closure body) { 34 | dockerWrapper.withDockerImage(imageConfig, body) 35 | } 36 | 37 | protected getImageConfig(Map gitopsConfig, Map validatorConfig) { 38 | if (validatorConfig.containsKey('image')) { 39 | return [ image: validatorConfig.image ] 40 | } else if (validatorConfig.containsKey('imageRef') && gitopsConfig.buildImages.containsKey(validatorConfig.imageRef)) { 41 | return gitopsConfig.buildImages[validatorConfig.imageRef] 42 | } else { 43 | return null 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/com/cloudogu/gitopsbuildlib/validation/Yamllint.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.gitopsbuildlib.validation 2 | 3 | import com.cloudogu.gitopsbuildlib.deployment.GitopsTool 4 | import com.cloudogu.gitopsbuildlib.deployment.SourceType 5 | 6 | /** 7 | * Checks for correct YAML syntax using yamllint 8 | * 9 | * If you want to use your own yamllint configuration your best bet is the following: 10 | * ".yamllint, .yamllint.yaml or .yamllint.yml in the current working directory" 11 | * See: https://yamllint.readthedocs.io/en/stable/configuration.html 12 | */ 13 | class Yamllint extends Validator { 14 | 15 | Yamllint(def script) { 16 | super(script) 17 | } 18 | 19 | @Override 20 | void validate(String targetDirectory, Map validatorConfig, Map gitopsConfig) { 21 | script.sh "yamllint " + 22 | "${validatorConfig.profile ? "-d ${validatorConfig.profile} " : ''}" + 23 | '-f standard ' + // non-colored for CI-server 24 | "${targetDirectory}" 25 | } 26 | 27 | @Override 28 | SourceType[] getSupportedSourceTypes() { 29 | return [SourceType.PLAIN] 30 | } 31 | 32 | @Override 33 | GitopsTool[] getSupportedGitopsTools() { 34 | return [GitopsTool.FLUX, GitopsTool.ARGO] 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/com/cloudogu/gitopsbuildlib/validation/utils/KubevalArgsParser.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.gitopsbuildlib.validation.utils 2 | 3 | class KubevalArgsParser { 4 | 5 | private boolean strict = true 6 | private boolean ignoreMissingSchemas = true 7 | private List skipKinds = [] 8 | 9 | String parse(Map validatorConfig) { 10 | 11 | String args = '' 12 | 13 | args += parseStrict(validatorConfig) 14 | args += parseIgnoreMissingSchemas(validatorConfig) 15 | args += parseSkipKinds(validatorConfig) 16 | 17 | return args 18 | } 19 | 20 | private String parseStrict(Map validatorConfig) { 21 | String strictArgs = '' 22 | if(validatorConfig.containsKey('strict')) { 23 | strict = validatorConfig.strict 24 | } 25 | if(strict) { 26 | strictArgs += " --strict" 27 | } 28 | return strictArgs 29 | } 30 | 31 | private String parseIgnoreMissingSchemas(Map validatorConfig) { 32 | String ignoreMissingSchemasArgs = '' 33 | if(validatorConfig.containsKey('ignoreMissingSchemas')) { 34 | ignoreMissingSchemas = validatorConfig.ignoreMissingSchemas 35 | } 36 | if(ignoreMissingSchemas) { 37 | ignoreMissingSchemasArgs += " --ignore-missing-schemas" 38 | } 39 | return ignoreMissingSchemasArgs 40 | } 41 | 42 | private String parseSkipKinds(Map validatorConfig) { 43 | String skipKindsArgs = '' 44 | if(validatorConfig.containsKey('skipKinds') && validatorConfig.skipKinds) { 45 | skipKinds = validatorConfig.skipKinds 46 | } 47 | if(!skipKinds.isEmpty()) { 48 | skipKindsArgs += " --skip-kinds ".concat(skipKinds.join(",")) 49 | } 50 | return skipKindsArgs 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /test/com/cloudogu/gitopsbuildlib/DeployViaGitopsTest.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.gitopsbuildlib 2 | 3 | import com.cloudogu.ces.cesbuildlib.Git 4 | import com.cloudogu.gitopsbuildlib.validation.Kubeval 5 | import com.cloudogu.gitopsbuildlib.validation.Yamllint 6 | import com.lesfurets.jenkins.unit.BasePipelineTest 7 | import groovy.mock.interceptor.StubFor 8 | import groovy.yaml.YamlSlurper 9 | import org.junit.jupiter.api.AfterEach 10 | import org.junit.jupiter.api.BeforeEach 11 | import org.junit.jupiter.api.Test 12 | import org.junit.jupiter.api.TestInstance 13 | import org.mockito.ArgumentCaptor 14 | 15 | import static com.lesfurets.jenkins.unit.MethodCall.callArgsToString 16 | import static groovy.test.GroovyAssert.shouldFail 17 | import static org.assertj.core.api.Assertions.assertThat 18 | import static org.mockito.ArgumentMatchers.anyString 19 | import static org.mockito.ArgumentMatchers.eq 20 | import static org.mockito.Mockito.* 21 | 22 | // Lifecycle.PER_METHOD is slower than PER_CLASS but we had issues regarding shared state (by executing all tests there was different behaviour than executing single tests in isolation). 23 | @TestInstance(TestInstance.Lifecycle.PER_METHOD) 24 | class DeployViaGitopsTest extends BasePipelineTest { 25 | 26 | class CesBuildLibMock { 27 | def Git = [:] 28 | def Docker = [:] 29 | def SCMManager = [:] 30 | } 31 | 32 | def git 33 | def docker 34 | def scmm 35 | 36 | def gitRepo 37 | 38 | def cesBuildLibMock 39 | def deployViaGitops 40 | 41 | static final String EXPECTED_APPLICATION = 'app' 42 | 43 | 44 | Map gitopsConfig(Map stages, Map deployments) { 45 | return [ 46 | scm : [ 47 | provider : 'SCMManager', 48 | credentialsId: 'scmManagerCredentials', 49 | baseUrl : 'http://scmm-scm-manager/scm', 50 | repositoryUrl : 'fluxv1/gitops', 51 | ], 52 | cesBuildLibRepo : 'cesBuildLibRepo', 53 | cesBuildLibVersion : 'cesBuildLibVersion', 54 | cesBuildLibCredentialsId: 'cesBuildLibCredentialsId', 55 | application : 'application', 56 | mainBranch : 'main', 57 | gitopsTool : 'FLUX', 58 | deployments : deployments, 59 | validators : [ 60 | kubeval : [ 61 | validator: new Kubeval(deployViaGitops), 62 | enabled : false, 63 | config : [ 64 | // We use the helm image (that also contains kubeval plugin) to speed up builds by allowing to reuse image 65 | image : 'ghcr.io/cloudogu/helm:3.4.1-1', 66 | k8sSchemaVersion: '1.18.1' 67 | ] 68 | ], 69 | yamllint: [ 70 | validator: new Yamllint(deployViaGitops), 71 | enabled : false, 72 | config : [ 73 | image : 'cytopia/yamllint:1.25-0.9', 74 | // Default to relaxed profile because it's feasible for mere mortalYAML programmers. 75 | // It still fails on syntax errors. 76 | profile: 'relaxed' 77 | ] 78 | ] 79 | ], 80 | stages : stages, 81 | folderStructureStrategy: 'GLOBAL_ENV' 82 | ] 83 | } 84 | 85 | def plainDeployment = [ 86 | destinationRootPath: '.', 87 | plain : [ 88 | updateImages: [ 89 | [deploymentFilename: "deployment.yaml", 90 | containerName : 'application', 91 | imageName : 'newImageName'] 92 | ] 93 | ] 94 | ] 95 | 96 | def singleStages = [ 97 | staging: [deployDirectly: true] 98 | ] 99 | 100 | def multipleStages = [ 101 | staging : [deployDirectly: true], 102 | production: [deployDirectly: false], 103 | qa : [deployDirectly: false] 104 | ] 105 | 106 | @BeforeEach 107 | void init() { 108 | super.setUp() 109 | 110 | scriptRoots += 'vars' 111 | 112 | deployViaGitops = loadScript('vars/deployViaGitops.groovy') 113 | binding.getVariable('currentBuild').result = 'SUCCESS' 114 | setupGlobals(deployViaGitops) 115 | 116 | cesBuildLibMock = new CesBuildLibMock() 117 | git = mock(Git.class) 118 | docker = new DockerMock().createMock() 119 | scmm = mock(com.cloudogu.ces.cesbuildlib.SCMManager.class) 120 | 121 | cesBuildLibMock.Docker.new = { 122 | return docker 123 | } 124 | 125 | cesBuildLibMock.Git.new = { def script, String credentials -> 126 | return git 127 | } 128 | 129 | cesBuildLibMock.SCMManager.new = { def script, String prBaseUrl, String scmmCredentialsId -> 130 | return scmm 131 | } 132 | 133 | def configYaml = '''\ 134 | --- 135 | spec: 136 | template: 137 | spec: 138 | containers: 139 | - name: 'application' 140 | image: 'oldImageName' 141 | ''' 142 | 143 | gitRepo = new StubFor(GitRepo) 144 | gitRepo.demand.with { 145 | create { new GitRepo('test', 'test', 'test', 'test', 'test') } 146 | // this needs to be defined three times for our three stages. 147 | // the authorName changes are only for verifying purposes within this testcase. 148 | // in normal usage the authorName is the same 149 | // for staging 150 | getCommitMessage { '#0001' } 151 | getRepositoryUrl { 'backend/k8s-gitops/' } 152 | getAuthorName { 'staging' } 153 | getAuthorEmail { 'authorName@email.de' } 154 | getCommitHash { '1234abcd' } 155 | 156 | // for production 157 | getCommitMessage { '#0001' } 158 | getRepositoryUrl { 'backend/k8s-gitops/' } 159 | getAuthorName { 'production' } 160 | getAuthorEmail { 'authorName@email.de' } 161 | getCommitHash { '1234abcd' } 162 | 163 | // for qa 164 | getCommitMessage { '#0001' } 165 | getRepositoryUrl { 'backend/k8s-gitops/' } 166 | getAuthorName { 'qa' } 167 | getAuthorEmail { 'authorName@email.de' } 168 | getCommitHash { '1234abcd' } 169 | } 170 | 171 | deployViaGitops.metaClass.initCesBuildLib = { String repo, String version, String credentialsId -> 172 | return cesBuildLibMock 173 | } 174 | 175 | deployViaGitops.metaClass.pwd = { 176 | return '/' 177 | } 178 | 179 | deployViaGitops.metaClass.readYaml = { 180 | return new YamlSlurper().parseText(configYaml) 181 | } 182 | 183 | deployViaGitops.metaClass.writeYaml = { LinkedHashMap args -> 184 | echo "filepath is: ${args.file}, data is: ${args.data}, overwrite is: ${args.overwrite}" 185 | } 186 | 187 | deployViaGitops.metaClass.error = { String message -> 188 | throw new JenkinsError(message) 189 | } 190 | 191 | when(git.commitHashShort).thenReturn('1234abcd') 192 | } 193 | 194 | @AfterEach 195 | void tearDown() throws Exception { 196 | // always reset metaClass after messing with it to prevent changes from leaking to other tests 197 | deployViaGitops.metaClass = null 198 | } 199 | 200 | @Test 201 | void 'default values are set'() { 202 | 203 | deployViaGitops.metaClass.validateConfig = { Map actualGitOpsConfig -> 204 | assertGitOpsConfigWithoutInstances(actualGitOpsConfig, deployViaGitops.createDefaultConfig()) 205 | } 206 | 207 | deployViaGitops([:]) 208 | } 209 | 210 | @Test 211 | void 'default values can be overwritten'() { 212 | 213 | deployViaGitops.metaClass.validateConfig = { Map actualGitOpsConfig -> 214 | assertThat(actualGitOpsConfig.cesBuildLibRepo).isEqualTo('abc') 215 | assertThat(actualGitOpsConfig.cesBuildLibCredentialsId).isEqualTo('testuser') 216 | } 217 | deployViaGitops.metaClass.deploy = {Map actualGitOpsConfig ->} // Stop after validation 218 | 219 | deployViaGitops([cesBuildLibRepo: 'abc', cesBuildLibCredentialsId: 'testuser']) 220 | } 221 | 222 | 223 | @Test 224 | void 'default validator can be enabled'() { 225 | 226 | deployViaGitops.metaClass.validateConfig = { Map actualGitOpsConfig -> 227 | assertThat(actualGitOpsConfig.validators.kubeval.enabled).isEqualTo(true) 228 | assertThat(actualGitOpsConfig.validators.kubeval.validator).isNotNull() 229 | assertThat(actualGitOpsConfig.validators.yamllint.enabled).isEqualTo(true) 230 | } 231 | deployViaGitops.metaClass.deploy = {Map actualGitOpsConfig ->} // Stop after validation 232 | 233 | deployViaGitops([ 234 | validators: [ 235 | kubeval: [ 236 | enabled: true 237 | ], 238 | yamllint: [ 239 | enabled: true 240 | ] 241 | ] 242 | ]) 243 | } 244 | 245 | @Test 246 | void 'custom validator can be added'() { 247 | 248 | deployViaGitops.metaClass.validateConfig = { Map actualGitOpsConfig -> 249 | assertThat(actualGitOpsConfig.validators.myVali.config.a).isEqualTo('b') 250 | assertThat(actualGitOpsConfig.validators.myVali.enabled).isEqualTo(true) 251 | } 252 | deployViaGitops.metaClass.deploy = {Map actualGitOpsConfig ->} // Stop after validation 253 | 254 | deployViaGitops([ 255 | validators: [ 256 | myVali: [ 257 | validator: {}, 258 | enabled : true, 259 | config : [ 260 | a: 'b' 261 | ] 262 | ] 263 | ] 264 | ]) 265 | } 266 | 267 | @Test 268 | void 'single stage deployment via gitops'() { 269 | 270 | when(git.areChangesStagedForCommit()).thenReturn(true) 271 | 272 | gitRepo.use { 273 | deployViaGitops.call(gitopsConfig(singleStages, plainDeployment)) 274 | } 275 | 276 | // testing deploy 277 | assertThat(helper.callStack.findAll { call -> call.methodName == "dir" }.any { call -> 278 | callArgsToString(call).contains(".configRepoTempDir") 279 | }).isTrue() 280 | 281 | ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Map.class) 282 | verify(git).call(argumentCaptor.capture()) 283 | assertThat(argumentCaptor.getValue().url).isEqualTo('http://scmm-scm-manager/scm/repo/fluxv1/gitops') 284 | assertThat(argumentCaptor.getValue().branch).isEqualTo('main') 285 | verify(git, times(1)).fetch() 286 | 287 | assertThat(helper.callStack.findAll { call -> call.methodName == "sh" }.any { call -> 288 | callArgsToString(call).equals("rm -rf .configRepoTempDir") 289 | }).isTrue() 290 | 291 | // testing syncGitopsRepoPerStage 292 | verify(git, times(1)).checkoutOrCreate('main') 293 | 294 | // testing commitAndPushToStage 295 | verify(git, times(1)).add('.') 296 | 297 | ArgumentCaptor argumentCaptor2 = ArgumentCaptor.forClass(String.class) 298 | verify(git).commit(argumentCaptor2.capture(), anyString(), anyString()) 299 | assertThat(argumentCaptor2.getValue()).isEqualTo('[staging] #0001 backend/k8s-gitops@1234abcd') 300 | 301 | argumentCaptor2 = ArgumentCaptor.forClass(String.class) 302 | verify(git).pushAndPullOnFailure(argumentCaptor2.capture()) 303 | assertThat(argumentCaptor2.getValue()).isEqualTo('origin main') 304 | 305 | 306 | assertThat(deployViaGitops.currentBuild.description).isEqualTo('GitOps commits: staging (1234abcd)\nImage: [newImageName]') 307 | } 308 | 309 | @Test 310 | void 'multi-stage deployment via GitOps'() { 311 | when(git.areChangesStagedForCommit()).thenReturn(true) 312 | 313 | gitRepo.use { 314 | deployViaGitops.call(gitopsConfig(multipleStages, plainDeployment)) 315 | } 316 | 317 | // testing deploy 318 | assertThat(helper.callStack.findAll { call -> call.methodName == "dir" }.any { call -> 319 | callArgsToString(call).contains(".configRepoTempDir") 320 | }).isTrue() 321 | 322 | ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Map.class) 323 | verify(git).call(argumentCaptor.capture()) 324 | assertThat(argumentCaptor.getValue().url).isEqualTo('http://scmm-scm-manager/scm/repo/fluxv1/gitops') 325 | assertThat(argumentCaptor.getValue().branch).isEqualTo('main') 326 | verify(git, times(1)).fetch() 327 | 328 | assertThat(helper.callStack.findAll { call -> call.methodName == "sh" }.any { call -> 329 | callArgsToString(call).equals("rm -rf .configRepoTempDir") 330 | }).isTrue() 331 | 332 | // testing syncGitopsRepoPerStage 333 | verify(git, times(3)).checkoutOrCreate('main') 334 | verify(git, times(1)).checkoutOrCreate('production_application') 335 | verify(git, times(1)).checkoutOrCreate('qa_application') 336 | 337 | 338 | // testing commitAndPushToStage 339 | verify(git, times(3)).add('.') 340 | 341 | ArgumentCaptor argumentCaptor2 = ArgumentCaptor.forClass(String.class) 342 | verify(git).commit(argumentCaptor2.capture(), eq('staging'), anyString()) 343 | assertThat(argumentCaptor2.getValue()).isEqualTo('[staging] #0001 backend/k8s-gitops@1234abcd') 344 | 345 | argumentCaptor2 = ArgumentCaptor.forClass(String.class) 346 | verify(git).commit(argumentCaptor2.capture(), eq('production'), anyString()) 347 | assertThat(argumentCaptor2.getValue()).isEqualTo('[production] #0001 backend/k8s-gitops@1234abcd') 348 | 349 | argumentCaptor2 = ArgumentCaptor.forClass(String.class) 350 | verify(git).commit(argumentCaptor2.capture(), eq('qa'), anyString()) 351 | assertThat(argumentCaptor2.getValue()).isEqualTo('[qa] #0001 backend/k8s-gitops@1234abcd') 352 | 353 | verify(git, times(3)).pushAndPullOnFailure(anyString()) 354 | 355 | assertThat(deployViaGitops.currentBuild.description).isEqualTo('GitOps commits: staging (1234abcd); production (1234abcd); qa (1234abcd)\nImage: [newImageName]') 356 | } 357 | 358 | @Test 359 | void 'no changes in git with multiple stages'() { 360 | when(git.areChangesStagedForCommit()).thenReturn(false) 361 | 362 | gitRepo.use { 363 | deployViaGitops.call(gitopsConfig(multipleStages, plainDeployment)) 364 | } 365 | 366 | List stringArgs = [] 367 | helper.callStack.findAll { call -> call.methodName == "echo" }.any { call -> 368 | stringArgs += callArgsToString(call) 369 | } 370 | assertThat(stringArgs.contains('No changes on gitOps repo for staging (branch: main)')) 371 | assertThat(stringArgs.contains('No changes on gitOps repo for staging (branch: production_application)')) 372 | assertThat(stringArgs.contains('No changes on gitOps repo for staging (branch: qa_application)')) 373 | } 374 | 375 | @Test 376 | void 'no changes in git with single stages'() { 377 | when(git.areChangesStagedForCommit()).thenReturn(false) 378 | 379 | gitRepo.use { 380 | deployViaGitops.call(gitopsConfig(singleStages, plainDeployment)) 381 | } 382 | 383 | assertThat( 384 | helper.callStack.findAll { call -> call.methodName == "echo" }.any { call -> 385 | callArgsToString(call).equals("No changes on gitOps repo for staging (branch: main). Not committing or pushing.") 386 | }).isTrue() 387 | } 388 | 389 | @Test 390 | void 'preparing gitRepo returns config'() { 391 | 392 | def stub = new StubFor(GitRepo) 393 | 394 | stub.demand.with { 395 | create { new GitRepo('test', 'test', 'test', 'test', 'test') } 396 | } 397 | 398 | stub.use { 399 | def output = deployViaGitops.prepareGitRepo(git) 400 | assert output.applicationRepo != null 401 | assert output.configRepoTempDir == '.configRepoTempDir' 402 | } 403 | } 404 | 405 | @Test 406 | void 'returns correct build description'() { 407 | def output = deployViaGitops.createBuildDescription('changes', 'imageName') 408 | assert output == 'GitOps commits: changes\nImage: imageName' 409 | } 410 | 411 | @Test 412 | void 'returns correct build description without imageName'() { 413 | def output = deployViaGitops.createBuildDescription('changes') 414 | assert output == 'GitOps commits: changes' 415 | } 416 | 417 | @Test 418 | void 'return No Changes if no changes are present'() { 419 | def output = deployViaGitops.createBuildDescription('', 'imageName') 420 | assert output == 'GitOps commits: No changes\nImage: imageName' 421 | } 422 | 423 | @Test 424 | void 'return No Changes if no changes are present without imageName'() { 425 | def output = deployViaGitops.createBuildDescription('') 426 | assert output == 'GitOps commits: No changes' 427 | } 428 | 429 | @Test 430 | void 'changes are being aggregated'() { 431 | def changes = ['1', '2', '3'] 432 | def output = deployViaGitops.aggregateChangesOnGitOpsRepo(changes) 433 | assert output == '1; 2; 3' 434 | } 435 | 436 | @Test 437 | void 'error on single missing mandatory field'() { 438 | 439 | def gitopsConfigMissingMandatoryField = [ 440 | scm: [ 441 | baseUrl : 'http://scmm-scm-manager/scm', 442 | repositoryUrl: 'fluxv1/gitops', 443 | ], 444 | application: 'app', 445 | gitopsTool: 'FLUX_V1', 446 | deployments: [ 447 | sourcePath: 'k8s', 448 | destinationRootPath: '.', 449 | plain: [ 450 | updateImages: [ 451 | [filename : "deployment.yaml", 452 | containerName: 'application', 453 | imageName : 'imageName'] 454 | ] 455 | ] 456 | ], 457 | stages: [ 458 | staging : [deployDirectly: true], 459 | production: [deployDirectly: false], 460 | qa : [] 461 | ] 462 | ] 463 | 464 | gitRepo.use { 465 | String message = shouldFail { 466 | deployViaGitops.call(gitopsConfigMissingMandatoryField) 467 | } 468 | assertThat(message).contains('[scm.provider]') 469 | } 470 | } 471 | 472 | @Test 473 | void 'error on single non valid mandatory field'() { 474 | 475 | def gitopsConfigMissingMandatoryField = [ 476 | scm : [ 477 | provider : 'SCMManagerr', 478 | baseUrl : 'http://scmm-scm-manager/scm', 479 | repositoryUrl: 'fluxv1/gitops', 480 | ], 481 | application : '', 482 | gitopsTool : 'FLUX_V1', 483 | deployments : [ 484 | sourcePath: 'k8s', 485 | destinationRootPath: '.', 486 | plain : [ 487 | updateImages: [ 488 | [filename : "deployment.yaml", 489 | containerName: 'application', 490 | imageName : 'imageName'] 491 | ] 492 | ] 493 | ], 494 | stages : [ 495 | staging : [deployDirectly: true], 496 | production: [deployDirectly: false], 497 | qa : [] 498 | ] 499 | ] 500 | 501 | gitRepo.use { 502 | String message = shouldFail { 503 | deployViaGitops.call(gitopsConfigMissingMandatoryField) 504 | } 505 | assertThat(message).contains('[application]') 506 | } 507 | } 508 | 509 | @Test 510 | void 'error on missing or non valid values on mandatory fields'() { 511 | 512 | def gitopsConfigMissingMandatoryField = [ 513 | scm : [ 514 | credentialsId: 'scmManagerCredentials', 515 | baseUrl : 'http://scmm-scm-manager/scm', 516 | repositoryUrl : '', 517 | ], 518 | application: '', 519 | gitopsTool : 'FLUX_V1', 520 | stages : [] 521 | ] 522 | 523 | gitRepo.use { 524 | String message = shouldFail { 525 | deployViaGitops.call(gitopsConfigMissingMandatoryField) 526 | } 527 | assertThat(message).contains('[scm.provider, scm.repositoryUrl, application, stages]') 528 | } 529 | } 530 | 531 | @Test 532 | void 'error on non available scm provider'() { 533 | 534 | def gitopsConfigMissingMandatoryField = [ 535 | scm: [ 536 | provider: 'nonValid', 537 | baseUrl : 'http://scmm-scm-manager/scm', 538 | repositoryUrl: 'fluxv1/gitops', 539 | ], 540 | application: 'app', 541 | gitopsTool: 'FLUX', 542 | deployments: [ 543 | sourcePath: 'k8s', 544 | destinationRootPath: '.', 545 | plain: [ 546 | updateImages: [ 547 | [filename : "deployment.yaml", 548 | containerName: 'application', 549 | imageName : 'imageName'] 550 | ] 551 | ] 552 | ], 553 | stages: [ 554 | staging : [deployDirectly: true], 555 | production: [deployDirectly: false], 556 | qa : [] 557 | ] 558 | ] 559 | 560 | gitRepo.use { 561 | String message = shouldFail { 562 | deployViaGitops.call(gitopsConfigMissingMandatoryField) 563 | } 564 | assertThat(message).contains('The given scm-provider seems to be invalid. Please choose one of the following: \'SCMManager\'.') 565 | } 566 | } 567 | 568 | @Test 569 | void 'error on invalid gitopsTool'() { 570 | gitRepo.use { 571 | String message = shouldFail { 572 | def gitOpsConfig = gitopsConfig(singleStages, plainDeployment) 573 | gitOpsConfig.gitopsTool = 'not very valid' 574 | deployViaGitops.call(gitOpsConfig) 575 | } 576 | assertThat(message).contains('The specified \'gitopsTool\' is invalid. Please choose one of the following: [FLUX, ARGO]') 577 | } 578 | } 579 | 580 | @Test 581 | void 'error on invalid folderStructureStrategy'() { 582 | gitRepo.use { 583 | String message = shouldFail { 584 | def gitOpsConfig = gitopsConfig(singleStages, plainDeployment) 585 | gitOpsConfig.folderStructureStrategy = 'not very valid' 586 | deployViaGitops.call(gitOpsConfig) 587 | } 588 | assertThat(message).contains('The specified \'folderStructureStrategy\' is invalid. Please choose one of the following: [GLOBAL_ENV, ENV_PER_APP]') 589 | } 590 | } 591 | 592 | @Test 593 | void 'error on missing tooling'() { 594 | def gitopsConfigMissingTooling = [ 595 | scm : [ 596 | provider : 'SCMManager', 597 | baseUrl : 'http://scmm-scm-manager/scm', 598 | repositoryUrl: 'fluxv1/gitops', 599 | ], 600 | application : 'application', 601 | gitopsTool : '', 602 | deployments : [ 603 | sourcePath: 'k8s', 604 | destinationRootPath: '.', 605 | plain : [ 606 | updateImages: [ 607 | [filename : "deployment.yaml", 608 | containerName: 'application', 609 | imageName : 'imageName'] 610 | ] 611 | ] 612 | ], 613 | stages : [ 614 | staging : [deployDirectly: true], 615 | production: [deployDirectly: false], 616 | qa : [] 617 | ] 618 | ] 619 | 620 | gitRepo.use { 621 | String message = shouldFail { 622 | deployViaGitops.call(gitopsConfigMissingTooling) 623 | } 624 | assertThat(message).contains('[gitopsTool]') 625 | } 626 | } 627 | 628 | private static void setupGlobals(Script script) { 629 | script.metaClass.getAppDockerRegistry = { EXPECTED_REGISTRY } 630 | script.application = EXPECTED_APPLICATION 631 | script.repoDigest = null 632 | script.forceDeployStaging = null 633 | script.reDeployImageVersion = null 634 | script.skipQualityAssurance = null 635 | script.configRepositoryPRUrl = "scm/repo" 636 | } 637 | 638 | void assertGitOpsConfigWithoutInstances(Map actualGitOpsConfig, Map expected) { 639 | // Remove Instance IDs, e.g. Yamllint@1234567 because they are generate on each getDefaultConfig() call. 640 | assertThat(actualGitOpsConfig.toString().replaceAll('@.*,', ',')) 641 | .isEqualTo(deployViaGitops.createDefaultConfig().toString().replaceAll('@.*,', ',')) 642 | } 643 | } 644 | 645 | class JenkinsError extends RuntimeException { 646 | JenkinsError(String message) { 647 | super(message) 648 | } 649 | } 650 | -------------------------------------------------------------------------------- /test/com/cloudogu/gitopsbuildlib/DockerMock.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.gitopsbuildlib 2 | 3 | import com.cloudogu.ces.cesbuildlib.Docker 4 | import org.mockito.invocation.InvocationOnMock 5 | import org.mockito.stubbing.Answer 6 | 7 | import static org.mockito.ArgumentMatchers.* 8 | import static org.mockito.Mockito.mock 9 | import static org.mockito.Mockito.when 10 | 11 | class DockerMock { 12 | 13 | List actualInsideArgs = new LinkedList<>() 14 | List actualRegistryArgs = new LinkedList<>() 15 | List actualImages = new LinkedList<>() 16 | 17 | Docker createMock() { 18 | Docker dockerMock = mock(Docker.class) 19 | Docker.Image imageMock = mock(Docker.Image.class) 20 | when(dockerMock.image(anyString())).thenAnswer(new Answer() { 21 | @Override 22 | Object answer(InvocationOnMock invocation) throws Throwable { 23 | actualImages += invocation.getArgument(0) 24 | return imageMock 25 | } 26 | }) 27 | 28 | when(dockerMock.withRegistry(anyString(), anyString(), any())).thenAnswer(new Answer() { 29 | @Override 30 | Object answer(InvocationOnMock invocationOnMock) throws Throwable { 31 | actualRegistryArgs += invocationOnMock.getArgument(0) 32 | actualRegistryArgs += invocationOnMock.getArgument(1) 33 | Closure closure = invocationOnMock.getArgument(2) 34 | closure.call() 35 | } 36 | }) 37 | when(imageMock.mountJenkinsUser()).thenReturn(imageMock) 38 | when(imageMock.mountJenkinsUser(anyBoolean())).thenReturn(imageMock) 39 | when(imageMock.mountDockerSocket()).thenReturn(imageMock) 40 | when(imageMock.mountDockerSocket(anyBoolean())).thenReturn(imageMock) 41 | when(imageMock.inside(anyString(), any())).thenAnswer(new Answer() { 42 | @Override 43 | Object answer(InvocationOnMock invocation) throws Throwable { 44 | actualInsideArgs += invocation.getArgument(0) 45 | Closure closure = invocation.getArgument(1) 46 | closure.call() 47 | } 48 | }) 49 | return dockerMock 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /test/com/cloudogu/gitopsbuildlib/ScriptMock.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.gitopsbuildlib 2 | 3 | import com.cloudogu.ces.cesbuildlib.Git 4 | import groovy.yaml.YamlSlurper 5 | 6 | import static org.mockito.Mockito.mock 7 | 8 | class ScriptMock { 9 | 10 | DockerMock dockerMock = new DockerMock() 11 | Git gitMock = mock(Git.class) 12 | 13 | List actualShArgs = new LinkedList<>() 14 | List actualEchoArgs = new LinkedList<>() 15 | List actualErrorArgs = new LinkedList<>() 16 | List actualReadYamlArgs = new LinkedList<>() 17 | List actualGitArgs = new LinkedList<>() 18 | List actualDir = new LinkedList<>() 19 | def configYaml = '' 20 | List actualWriteYamlArgs = new LinkedList<>() 21 | List actualReadFileArgs = new LinkedList<>() 22 | List actualWriteFileArgs = new LinkedList<>() 23 | List actualFileExistsArgs = new LinkedList<>() 24 | 25 | def mock = 26 | [ 27 | cesBuildLib: [ 28 | Docker: [ 29 | new: { args -> return dockerMock.createMock() } 30 | ], 31 | Git: [ 32 | new: { args -> return gitMock }, 33 | ], 34 | ], 35 | docker: dockerMock.createMock(), 36 | git: { args -> actualGitArgs += args.toString() }, 37 | pwd : { 'pwd' }, 38 | sh : { args -> actualShArgs += args.toString() }, 39 | echo : { args -> actualEchoArgs += args.toString() }, 40 | error : { args -> actualErrorArgs += args.toString() }, 41 | readYaml: { args -> actualReadYamlArgs += args.toString(); return new YamlSlurper().parseText(configYaml) }, 42 | writeYaml: { args -> actualWriteYamlArgs += args.toString() }, 43 | readFile : { args -> actualReadFileArgs += args.toString(); return configYaml}, 44 | writeFile: { args -> actualWriteFileArgs += args.toString() }, 45 | fileExists: { args -> actualFileExistsArgs += args.toString(); return true }, 46 | env : [ 47 | WORKSPACE: 'workspace' 48 | ], 49 | dir: { dir, closure -> println(dir); actualDir += dir.toString(); return closure.call() } 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /test/com/cloudogu/gitopsbuildlib/deployment/DeploymentTest.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.gitopsbuildlib.deployment 2 | 3 | import com.cloudogu.gitopsbuildlib.ScriptMock 4 | import com.cloudogu.gitopsbuildlib.validation.HelmKubeval 5 | import com.cloudogu.gitopsbuildlib.validation.Kubeval 6 | import com.cloudogu.gitopsbuildlib.validation.Yamllint 7 | import org.junit.jupiter.api.* 8 | import static org.assertj.core.api.Assertions.assertThat 9 | 10 | class DeploymentTest { 11 | 12 | def scriptMock = new ScriptMock() 13 | 14 | Deployment deploymentUnderTest = new DeploymentUnderTest(scriptMock.mock, [ 15 | application: 'app', 16 | gitopsTool: 'FLUX', 17 | stages: [ 18 | staging: [ 19 | namespace: 'fluxv1-staging' 20 | ] 21 | ], 22 | deployments: [ 23 | sourcePath: 'k8s', 24 | destinationRootPath: '.', 25 | plain: [:] 26 | ], 27 | folderStructureStrategy: 'GLOBAL_ENV', 28 | buildImages: [ 29 | kubectl: [ 30 | image: "http://my-private-registry.com/repo/kubectlImage", 31 | credentialsId: "credentials" 32 | ] 33 | ], 34 | validators: [ 35 | yamllint: [ 36 | validator: new Yamllint(scriptMock.mock), 37 | enabled: true, 38 | config: [ 39 | image: 'img' 40 | ] 41 | ], 42 | kubeval: [ 43 | validator: new Kubeval(scriptMock.mock), 44 | enabled: true, 45 | config: [ 46 | image: 'img' 47 | ] 48 | ], 49 | helmKubeval: [ 50 | validator: new HelmKubeval(scriptMock.mock), 51 | enabled: true, 52 | config: [ 53 | image: 'img' 54 | ] 55 | ] 56 | ], 57 | fileConfigmaps: [ 58 | [ 59 | name : "index", 60 | sourceFilePath : "../index.html", 61 | stage: ["staging"] 62 | ] 63 | ] 64 | ] as Map) 65 | 66 | @Test 67 | void 'creating folders for plain deployment'() { 68 | deploymentUnderTest.createFoldersAndCopyK8sResources('staging',) 69 | assertThat(scriptMock.actualEchoArgs[0]).isEqualTo('Copying k8s payload from application repo to gitOps Repo: \'k8s/staging/*\' to \'staging/app/\'') 70 | assertThat(scriptMock.actualShArgs[0]).isEqualTo('mkdir -p staging/app/') 71 | assertThat(scriptMock.actualShArgs[1]).isEqualTo('mkdir -p .config/') 72 | assertThat(scriptMock.actualShArgs[2]).isEqualTo('cp -r workspace/k8s/staging/* staging/app/ || true') 73 | assertThat(scriptMock.actualShArgs[3]).isEqualTo('cp workspace/*.yamllint.yaml .config/ || true') 74 | } 75 | 76 | @Test 77 | void 'creating folders for plain deployment with ENV_PER_APP and other destinationRootPath'() { 78 | deploymentUnderTest.gitopsConfig['folderStructureStrategy'] = 'ENV_PER_APP' 79 | deploymentUnderTest.gitopsConfig['deployments']['destinationRootPath'] = 'apps' 80 | 81 | deploymentUnderTest.createFoldersAndCopyK8sResources('staging',) 82 | 83 | assertThat(scriptMock.actualEchoArgs[0]).isEqualTo('Copying k8s payload from application repo to gitOps Repo: \'k8s/staging/*\' to \'apps/app/staging/\'') 84 | assertThat(scriptMock.actualShArgs[0]).isEqualTo('mkdir -p apps/app/staging/') 85 | assertThat(scriptMock.actualShArgs[1]).isEqualTo('mkdir -p .config/') 86 | assertThat(scriptMock.actualShArgs[2]).isEqualTo('cp -r workspace/k8s/staging/* apps/app/staging/ || true') 87 | assertThat(scriptMock.actualShArgs[3]).isEqualTo('cp workspace/*.yamllint.yaml .config/ || true') 88 | } 89 | 90 | @Test 91 | void 'create configmaps from files'() { 92 | 93 | deploymentUnderTest.createFileConfigmaps('staging') 94 | 95 | assertThat(scriptMock.dockerMock.actualRegistryArgs[0]).isEqualTo('https://http://my-private-registry.com/repo') 96 | assertThat(scriptMock.dockerMock.actualRegistryArgs[1]).isEqualTo('credentials') 97 | 98 | assertThat(scriptMock.actualShArgs[0]).isEqualTo('[returnStdout:true, script:kubectl create configmap index --from-file=index.html=workspace/k8s/../index.html --dry-run=client -o yaml -n fluxv1-staging]') 99 | 100 | assertThat(scriptMock.actualWriteFileArgs[0]).contains('[file:staging/app/generatedResources/index.yaml') 101 | } 102 | 103 | class DeploymentUnderTest extends Deployment { 104 | 105 | DeploymentUnderTest(Object script, Object gitopsConfig) { 106 | super(script, gitopsConfig) 107 | } 108 | 109 | @Override 110 | def preValidation(String stage) { 111 | return null 112 | } 113 | 114 | @Override 115 | def postValidation(String stage) { 116 | return null 117 | } 118 | 119 | @Override 120 | def validate(String stage) { 121 | return null 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /test/com/cloudogu/gitopsbuildlib/deployment/HelmTest.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.gitopsbuildlib.deployment 2 | 3 | import com.cloudogu.gitopsbuildlib.ScriptMock 4 | import com.cloudogu.gitopsbuildlib.deployment.helm.Helm 5 | import com.cloudogu.gitopsbuildlib.validation.HelmKubeval 6 | import com.cloudogu.gitopsbuildlib.validation.Kubeval 7 | import com.cloudogu.gitopsbuildlib.validation.Yamllint 8 | import org.junit.jupiter.api.* 9 | 10 | import static org.assertj.core.api.Assertions.assertThat 11 | 12 | class HelmTest { 13 | 14 | def gitRepo = [ 15 | sourcePath: 'k8s', 16 | destinationRootPath: '.', 17 | helm : [ 18 | repoType: 'GIT', 19 | repoUrl: 'repoUrl', 20 | chartPath: 'chartPath' 21 | ] 22 | ] 23 | 24 | def helmRepo = [ 25 | sourcePath: 'k8s', 26 | destinationRootPath: '.', 27 | helm : [ 28 | repoType: 'HELM', 29 | repoUrl: 'repoUrl', 30 | chartName: 'chartName', 31 | version: '1.0' 32 | ] 33 | ] 34 | 35 | def localRepo = [ 36 | sourcePath: 'k8s', 37 | destinationRootPath: '.', 38 | helm : [ 39 | repoType: 'LOCAL', 40 | chartPath: 'chart/path' 41 | ] 42 | ] 43 | 44 | private Map getGitopsConfig(Map deployment, String tool = 'FLUX') { 45 | return [ 46 | application: 'app', 47 | gitopsTool: tool, 48 | stages: [ 49 | staging: [ 50 | namespace: 'fluxv1-staging' 51 | ] 52 | ], 53 | folderStructureStrategy: 'GLOBAL_ENV', 54 | buildImages: [ 55 | helm: [ 56 | image: 'helmImage', 57 | ], 58 | kubectl: [ 59 | image: 'kubectlImage' 60 | ] 61 | ], 62 | deployments: deployment, 63 | validators: [ 64 | yamllint: [ 65 | validator: new Yamllint(scriptMock.mock), 66 | enabled: true, 67 | config: [ 68 | image: 'img' 69 | ] 70 | ], 71 | kubeval: [ 72 | validator: new Kubeval(scriptMock.mock), 73 | enabled: true, 74 | config: [ 75 | image: 'img' 76 | ] 77 | ], 78 | helmKubeval: [ 79 | validator: new HelmKubeval(scriptMock.mock), 80 | enabled: true, 81 | config: [ 82 | image: 'img' 83 | ] 84 | ] 85 | ], 86 | fileConfigmaps: [ 87 | [ 88 | name : "index", 89 | sourceFilePath : "../index.html", 90 | stage: ["staging"] 91 | ] 92 | ] 93 | ] 94 | } 95 | 96 | def scriptMock = new ScriptMock() 97 | def dockerMock = scriptMock.dockerMock 98 | def helmGit = new Helm(scriptMock.mock, getGitopsConfig(gitRepo)) 99 | def helmHelm = new Helm(scriptMock.mock, getGitopsConfig(helmRepo)) 100 | def helmLocal = new Helm(scriptMock.mock, getGitopsConfig(localRepo, 'ARGO')) 101 | 102 | @BeforeEach 103 | void init () { 104 | scriptMock.configYaml = ''' 105 | to: 106 | be: 107 | changed: 'oldValue' 108 | ''' 109 | } 110 | 111 | @Test 112 | void 'creating helm release with git repo'() { 113 | helmGit.preValidation('staging') 114 | 115 | assertThat(dockerMock.actualImages[0]).contains('helmImage') 116 | assertThat(scriptMock.actualShArgs[0]).isEqualTo('helm dep update workspace/.helmChartTempDir/chart/chartPath') 117 | assertThat(scriptMock.actualShArgs[1]).isEqualTo('[returnStdout:true, script:helm values workspace/.helmChartTempDir/chart/chartPath -f workspace/k8s/values-staging.yaml -f workspace/k8s/values-shared.yaml ]') 118 | assertThat(scriptMock.actualWriteFileArgs[0]).isEqualTo('[file:workspace/.helmChartTempDir/mergedValues.yaml, text:[helm dep update workspace/.helmChartTempDir/chart/chartPath, [returnStdout:true, script:helm values workspace/.helmChartTempDir/chart/chartPath -f workspace/k8s/values-staging.yaml -f workspace/k8s/values-shared.yaml ]]]') 119 | assertThat(scriptMock.actualWriteFileArgs[1]).isEqualTo('''[file:staging/app/applicationRelease.yaml, text:apiVersion: helm.fluxcd.io/v1 120 | kind: HelmRelease 121 | metadata: 122 | name: app 123 | namespace: fluxv1-staging 124 | annotations: 125 | fluxcd.io/automated: "false" 126 | spec: 127 | releaseName: app 128 | chart: 129 | git: repoUrl 130 | ref: null 131 | path: chartPath 132 | values: 133 | 134 | to: 135 | be: 136 | changed: \'oldValue\' 137 | ]''') 138 | } 139 | 140 | @Test 141 | void 'creating helm release with helm repo'() { 142 | helmHelm.preValidation('staging') 143 | 144 | assertThat(dockerMock.actualImages[0]).contains('helmImage') 145 | assertThat(scriptMock.actualShArgs[0]).isEqualTo('helm repo add chartRepo repoUrl') 146 | assertThat(scriptMock.actualShArgs[1]).isEqualTo('helm repo update') 147 | assertThat(scriptMock.actualShArgs[2]).isEqualTo('helm pull chartRepo/chartName --version=1.0 --untar --untardir=workspace/.helmChartTempDir/chart') 148 | assertThat(scriptMock.actualShArgs[3]).isEqualTo('[returnStdout:true, script:helm values workspace/.helmChartTempDir/chart/chartName -f workspace/k8s/values-staging.yaml -f workspace/k8s/values-shared.yaml ]') 149 | assertThat(scriptMock.actualWriteFileArgs[0]).isEqualTo('[file:workspace/.helmChartTempDir/mergedValues.yaml, text:[helm repo add chartRepo repoUrl, helm repo update, helm pull chartRepo/chartName --version=1.0 --untar --untardir=workspace/.helmChartTempDir/chart, [returnStdout:true, script:helm values workspace/.helmChartTempDir/chart/chartName -f workspace/k8s/values-staging.yaml -f workspace/k8s/values-shared.yaml ]]]') 150 | assertThat(scriptMock.actualWriteFileArgs[1]).isEqualTo('''[file:staging/app/applicationRelease.yaml, text:apiVersion: helm.fluxcd.io/v1 151 | kind: HelmRelease 152 | metadata: 153 | name: app 154 | namespace: fluxv1-staging 155 | annotations: 156 | fluxcd.io/automated: "false" 157 | spec: 158 | releaseName: app 159 | chart: 160 | repository: repoUrl 161 | name: chartName 162 | version: 1.0 163 | values: 164 | 165 | to: 166 | be: 167 | changed: \'oldValue\' 168 | ]''') 169 | } 170 | 171 | @Test 172 | void 'creating helm release with git repo with ENV_PER_APP and other destinationRootPath'() { 173 | helmGit.gitopsConfig['folderStructureStrategy'] = 'ENV_PER_APP' 174 | helmGit.gitopsConfig['deployments']['destinationRootPath'] = 'apps' 175 | 176 | helmGit.preValidation('staging') 177 | 178 | assertThat(dockerMock.actualImages[0]).contains('helmImage') 179 | assertThat(scriptMock.actualShArgs[0]).isEqualTo('helm dep update workspace/.helmChartTempDir/chart/chartPath') 180 | assertThat(scriptMock.actualShArgs[1]).isEqualTo('[returnStdout:true, script:helm values workspace/.helmChartTempDir/chart/chartPath -f workspace/k8s/values-staging.yaml -f workspace/k8s/values-shared.yaml ]') 181 | assertThat(scriptMock.actualWriteFileArgs[0]).isEqualTo('[file:workspace/.helmChartTempDir/mergedValues.yaml, text:[helm dep update workspace/.helmChartTempDir/chart/chartPath, [returnStdout:true, script:helm values workspace/.helmChartTempDir/chart/chartPath -f workspace/k8s/values-staging.yaml -f workspace/k8s/values-shared.yaml ]]]') 182 | assertThat(scriptMock.actualWriteFileArgs[1]).isEqualTo('''[file:apps/app/staging/applicationRelease.yaml, text:apiVersion: helm.fluxcd.io/v1 183 | kind: HelmRelease 184 | metadata: 185 | name: app 186 | namespace: fluxv1-staging 187 | annotations: 188 | fluxcd.io/automated: "false" 189 | spec: 190 | releaseName: app 191 | chart: 192 | git: repoUrl 193 | ref: null 194 | path: chartPath 195 | values: 196 | 197 | to: 198 | be: 199 | changed: \'oldValue\' 200 | ]''') 201 | } 202 | 203 | @Test 204 | void 'creating helm release with helm repo with ENV_PER_APP and other destinationRootPath'() { 205 | helmHelm.gitopsConfig['folderStructureStrategy'] = 'ENV_PER_APP' 206 | helmHelm.gitopsConfig['deployments']['destinationRootPath'] = 'apps' 207 | 208 | helmHelm.preValidation('staging') 209 | 210 | assertThat(dockerMock.actualImages[0]).contains('helmImage') 211 | assertThat(scriptMock.actualShArgs[0]).isEqualTo('helm repo add chartRepo repoUrl') 212 | assertThat(scriptMock.actualShArgs[1]).isEqualTo('helm repo update') 213 | assertThat(scriptMock.actualShArgs[2]).isEqualTo('helm pull chartRepo/chartName --version=1.0 --untar --untardir=workspace/.helmChartTempDir/chart') 214 | assertThat(scriptMock.actualShArgs[3]).isEqualTo('[returnStdout:true, script:helm values workspace/.helmChartTempDir/chart/chartName -f workspace/k8s/values-staging.yaml -f workspace/k8s/values-shared.yaml ]') 215 | assertThat(scriptMock.actualWriteFileArgs[0]).isEqualTo('[file:workspace/.helmChartTempDir/mergedValues.yaml, text:[helm repo add chartRepo repoUrl, helm repo update, helm pull chartRepo/chartName --version=1.0 --untar --untardir=workspace/.helmChartTempDir/chart, [returnStdout:true, script:helm values workspace/.helmChartTempDir/chart/chartName -f workspace/k8s/values-staging.yaml -f workspace/k8s/values-shared.yaml ]]]') 216 | assertThat(scriptMock.actualWriteFileArgs[1]).isEqualTo('''[file:apps/app/staging/applicationRelease.yaml, text:apiVersion: helm.fluxcd.io/v1 217 | kind: HelmRelease 218 | metadata: 219 | name: app 220 | namespace: fluxv1-staging 221 | annotations: 222 | fluxcd.io/automated: "false" 223 | spec: 224 | releaseName: app 225 | chart: 226 | repository: repoUrl 227 | name: chartName 228 | version: 1.0 229 | values: 230 | 231 | to: 232 | be: 233 | changed: \'oldValue\' 234 | ]''') 235 | } 236 | 237 | @Test 238 | void 'values files getting parameters attached with gitRepo'() { 239 | def output = helmGit.valuesFilesWithParameter(['file1.yaml', 'file2.yaml'] as String[]) 240 | assertThat(output).isEqualTo('-f file1.yaml -f file2.yaml ') 241 | } 242 | 243 | @Test 244 | void 'values files getting parameters attached with helmRepo'() { 245 | def output = helmHelm.valuesFilesWithParameter(['file1.yaml', 'file2.yaml'] as String[]) 246 | assertThat(output).isEqualTo('-f file1.yaml -f file2.yaml ') 247 | } 248 | 249 | @Test 250 | void 'flux helm validates with yamllint and kubeval and helmKubeval'() { 251 | helmHelm.validate('staging') 252 | 253 | assertThat(scriptMock.actualEchoArgs[0]).isEqualTo('Starting validator Yamllint for FLUX in PLAIN resources') 254 | assertThat(scriptMock.actualEchoArgs[1]).isEqualTo('Skipping validator Yamllint because it is configured as enabled=false or doesn\'t support the given gitopsTool=FLUX or sourceType=HELM') 255 | assertThat(scriptMock.actualEchoArgs[2]).isEqualTo('Starting validator Kubeval for FLUX in PLAIN resources') 256 | assertThat(scriptMock.actualEchoArgs[3]).isEqualTo('Skipping validator Kubeval because it is configured as enabled=false or doesn\'t support the given gitopsTool=FLUX or sourceType=HELM') 257 | assertThat(scriptMock.actualEchoArgs[4]).isEqualTo('Skipping validator HelmKubeval because it is configured as enabled=false or doesn\'t support the given gitopsTool=FLUX or sourceType=PLAIN') 258 | assertThat(scriptMock.actualEchoArgs[5]).isEqualTo('Starting validator HelmKubeval for FLUX in HELM resources') 259 | 260 | assertThat(scriptMock.actualShArgs[0]).isEqualTo('yamllint -f standard staging/app') 261 | assertThat(scriptMock.actualShArgs[1]).isEqualTo('kubeval -d staging/app -v null --strict --ignore-missing-schemas') 262 | assertThat(scriptMock.actualShArgs[2]).isEqualTo('helm kubeval workspace/.helmChartTempDir/chart/chartName -f workspace/.helmChartTempDir/mergedValues.yaml -v null --strict --ignore-missing-schemas') 263 | } 264 | 265 | @Test 266 | void 'argo helm validates with yamllint and kubeval'() { 267 | helmHelm.gitopsConfig['gitopsTool'] = 'ARGO' 268 | helmHelm.validate('staging') 269 | 270 | assertThat(scriptMock.actualEchoArgs[0]).isEqualTo('Starting validator Yamllint for ARGO in PLAIN resources') 271 | assertThat(scriptMock.actualEchoArgs[1]).isEqualTo('Skipping validator Yamllint because it is configured as enabled=false or doesn\'t support the given gitopsTool=ARGO or sourceType=HELM') 272 | assertThat(scriptMock.actualEchoArgs[2]).isEqualTo('Starting validator Kubeval for ARGO in PLAIN resources') 273 | assertThat(scriptMock.actualEchoArgs[3]).isEqualTo('Skipping validator Kubeval because it is configured as enabled=false or doesn\'t support the given gitopsTool=ARGO or sourceType=HELM') 274 | assertThat(scriptMock.actualEchoArgs[4]).isEqualTo('Skipping validator HelmKubeval because it is configured as enabled=false or doesn\'t support the given gitopsTool=ARGO or sourceType=PLAIN') 275 | assertThat(scriptMock.actualEchoArgs[5]).isEqualTo('Skipping validator HelmKubeval because it is configured as enabled=false or doesn\'t support the given gitopsTool=ARGO or sourceType=HELM') 276 | 277 | assertThat(scriptMock.actualShArgs[0]).isEqualTo('yamllint -f standard staging/app') 278 | assertThat(scriptMock.actualShArgs[1]).isEqualTo('kubeval -d staging/app -v null --strict --ignore-missing-schemas') 279 | } 280 | 281 | @Test 282 | void 'creating helm release with local repo'() { 283 | helmLocal.preValidation('staging') 284 | 285 | assertThat(dockerMock.actualImages[0]).contains('helmImage') 286 | assertThat(scriptMock.actualShArgs[0]).isEqualTo('[returnStdout:true, script:helm values workspace/chart/path -f workspace/k8s/values-staging.yaml -f workspace/k8s/values-shared.yaml ]') 287 | } 288 | } 289 | -------------------------------------------------------------------------------- /test/com/cloudogu/gitopsbuildlib/deployment/PlainTest.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.gitopsbuildlib.deployment 2 | 3 | import com.cloudogu.gitopsbuildlib.ScriptMock 4 | import com.cloudogu.gitopsbuildlib.deployment.plain.Plain 5 | import com.cloudogu.gitopsbuildlib.validation.HelmKubeval 6 | import com.cloudogu.gitopsbuildlib.validation.Kubeval 7 | import com.cloudogu.gitopsbuildlib.validation.Yamllint 8 | import org.junit.jupiter.api.BeforeEach 9 | import org.junit.jupiter.api.Test 10 | 11 | import static org.assertj.core.api.Assertions.assertThat 12 | 13 | class PlainTest { 14 | 15 | def scriptMock = new ScriptMock() 16 | def plain = new Plain(scriptMock.mock, [ 17 | application: 'app', 18 | gitopsTool: 'FLUX', 19 | deployments: [ 20 | sourcePath: 'k8s', 21 | destinationRootPath: '.', 22 | plain: [ 23 | updateImages: [ 24 | [filename : "deployment.yaml", // relative to deployments.path 25 | containerName: 'application', 26 | imageName : 'imageNameReplacedTest'] 27 | ] 28 | ] 29 | ], 30 | folderStructureStrategy: 'GLOBAL_ENV', 31 | validators: [ 32 | yamllint: [ 33 | validator: new Yamllint(scriptMock.mock), 34 | enabled: true, 35 | config: [ 36 | image: 'img' 37 | ] 38 | ], 39 | kubeval: [ 40 | validator: new Kubeval(scriptMock.mock), 41 | enabled: true, 42 | config: [ 43 | image: 'img' 44 | ] 45 | ], 46 | helmKubeval: [ 47 | validator: new HelmKubeval(scriptMock.mock), 48 | enabled: true, 49 | config: [ 50 | image: 'img' 51 | ] 52 | ] 53 | ], 54 | ]) 55 | 56 | String deploymentYaml = ''' 57 | kind: Deployment 58 | spec: 59 | template: 60 | spec: 61 | containers: 62 | - name: 'application' 63 | image: 'oldImageName' 64 | ''' 65 | 66 | String cronJobYaml = ''' 67 | kind: CronJob 68 | spec: 69 | jobTemplate: 70 | spec: 71 | template: 72 | spec: 73 | containers: 74 | - name: 'application' 75 | image: 'oldImageName' 76 | - name: 'other' 77 | image: 'otherImageName' 78 | ''' 79 | 80 | @BeforeEach 81 | void init () { 82 | scriptMock.configYaml = deploymentYaml 83 | } 84 | 85 | @Test 86 | void 'successful update'() { 87 | 88 | plain.preValidation('staging') 89 | assertThat(scriptMock.actualReadYamlArgs[0]).isEqualTo('[file:staging/app/deployment.yaml]') 90 | assertThat(scriptMock.actualWriteYamlArgs[0]).isEqualTo('[file:staging/app/deployment.yaml, data:[kind:Deployment, spec:[template:[spec:[containers:[[image:imageNameReplacedTest, name:application]]]]]], overwrite:true]') 91 | } 92 | 93 | @Test 94 | void 'successful update with statefulSet'() { 95 | scriptMock.configYaml = scriptMock.configYaml.replace('kind: Deployment', 'kind: StatefulSet') 96 | plain.preValidation('staging') 97 | assertThat(scriptMock.actualReadYamlArgs[0]).isEqualTo('[file:staging/app/deployment.yaml]') 98 | assertThat(scriptMock.actualWriteYamlArgs[0]).isEqualTo('[file:staging/app/deployment.yaml, data:[kind:StatefulSet, spec:[template:[spec:[containers:[[image:imageNameReplacedTest, name:application]]]]]], overwrite:true]') 99 | } 100 | 101 | @Test 102 | void 'successful update with cronjob'() { 103 | scriptMock.configYaml = cronJobYaml 104 | 105 | plain.preValidation('staging') 106 | assertThat(scriptMock.actualReadYamlArgs[0]).isEqualTo('[file:staging/app/deployment.yaml]') 107 | assertThat(scriptMock.actualWriteYamlArgs[0]).isEqualTo('[file:staging/app/deployment.yaml, data:[kind:CronJob, spec:[jobTemplate:[spec:[template:[spec:[containers:[[image:imageNameReplacedTest, name:application], [image:otherImageName, name:other]]]]]]]], overwrite:true]') 108 | } 109 | 110 | @Test 111 | void 'successful update with other resource'() { 112 | scriptMock.configYaml = scriptMock.configYaml.replace('kind: Deployment', 'kind: SomethingElse') 113 | plain.preValidation('staging') 114 | assertThat(scriptMock.actualReadYamlArgs[0]).isEqualTo('[file:staging/app/deployment.yaml]') 115 | assertThat(scriptMock.actualWriteYamlArgs[0]).isEqualTo('[file:staging/app/deployment.yaml, data:[kind:SomethingElse, spec:[template:[spec:[containers:[[image:imageNameReplacedTest, name:application]]]]]], overwrite:true]') 116 | assertThat(scriptMock.actualEchoArgs).contains('Warning: Kind \'SomethingElse\' is unknown, using best effort to find \'containers\' in YAML') 117 | } 118 | 119 | @Test 120 | void 'successful update with ENV_PER_APP and other destinationRootPath '() { 121 | plain.gitopsConfig['folderStructureStrategy'] = 'ENV_PER_APP' 122 | plain.gitopsConfig['deployments']['destinationRootPath'] = 'apps' 123 | 124 | plain.preValidation('staging') 125 | assertThat(scriptMock.actualReadYamlArgs[0]).isEqualTo('[file:apps/app/staging/deployment.yaml]') 126 | assertThat(scriptMock.actualWriteYamlArgs[0]).isEqualTo('[file:apps/app/staging/deployment.yaml, data:[kind:Deployment, spec:[template:[spec:[containers:[[image:imageNameReplacedTest, name:application]]]]]], overwrite:true]') 127 | } 128 | 129 | @Test 130 | void 'flux plain validates with yamllint and kubeval'() { 131 | plain.validate('staging') 132 | 133 | assertThat(scriptMock.actualEchoArgs[0]).isEqualTo('Starting validator Yamllint for FLUX in PLAIN resources') 134 | assertThat(scriptMock.actualEchoArgs[1]).isEqualTo('Starting validator Kubeval for FLUX in PLAIN resources') 135 | assertThat(scriptMock.actualEchoArgs[2]).isEqualTo('Skipping validator HelmKubeval because it is configured as enabled=false or doesn\'t support the given gitopsTool=FLUX or sourceType=PLAIN') 136 | 137 | assertThat(scriptMock.actualShArgs[0]).isEqualTo('yamllint -f standard staging/app') 138 | assertThat(scriptMock.actualShArgs[1]).isEqualTo('kubeval -d staging/app -v null --strict --ignore-missing-schemas') 139 | } 140 | 141 | @Test 142 | void 'argo plain validates with yamllint and kubeval'() { 143 | plain.gitopsConfig['gitopsTool'] = 'ARGO' 144 | plain.validate('staging') 145 | 146 | assertThat(scriptMock.actualEchoArgs[0]).isEqualTo('Starting validator Yamllint for ARGO in PLAIN resources') 147 | assertThat(scriptMock.actualEchoArgs[1]).isEqualTo('Starting validator Kubeval for ARGO in PLAIN resources') 148 | assertThat(scriptMock.actualEchoArgs[2]).isEqualTo('Skipping validator HelmKubeval because it is configured as enabled=false or doesn\'t support the given gitopsTool=ARGO or sourceType=PLAIN') 149 | 150 | assertThat(scriptMock.actualShArgs[0]).isEqualTo('yamllint -f standard staging/app') 151 | assertThat(scriptMock.actualShArgs[1]).isEqualTo('kubeval -d staging/app -v null --strict --ignore-missing-schemas') 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /test/com/cloudogu/gitopsbuildlib/deployment/helm/helmrelease/ArgoCDReleaseTest.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.gitopsbuildlib.deployment.helm.helmrelease 2 | 3 | import com.cloudogu.gitopsbuildlib.ScriptMock 4 | import org.junit.jupiter.api.Test 5 | 6 | import static org.assertj.core.api.Assertions.assertThat 7 | 8 | class ArgoCDReleaseTest { 9 | 10 | def scriptMock = new ScriptMock() 11 | def argoCdReleaseTest = new ArgoCDRelease(scriptMock.mock) 12 | 13 | @Test 14 | void 'correct helm release with git repo and chartPath'() { 15 | argoCdReleaseTest.create([ 16 | k8sVersion: '1.24.8', 17 | application: 'app', 18 | deployments: [ 19 | helm: [ 20 | repoType : 'GIT', 21 | repoUrl : 'url', 22 | chartName: 'chartName', 23 | chartPath: 'path', 24 | version : '1.0' 25 | ] 26 | ], 27 | buildImages: [ 28 | helm: [ 29 | image: 'helmImg' 30 | ] 31 | ] 32 | ], 33 | 'namespace', 34 | 'this/is/a/valuesfile') 35 | 36 | assertThat(scriptMock.dockerMock.actualImages[0]).isEqualTo('helmImg') 37 | assertThat(scriptMock.actualShArgs[0]).isEqualTo('[returnStdout:true, script:helm template app workspace/.helmChartTempDir/chart/path -n namespace --kube-version 1.24.8 -f this/is/a/valuesfile]') 38 | } 39 | 40 | @Test 41 | void 'correct helm release with git repo without chartPath'() { 42 | argoCdReleaseTest.create([ 43 | k8sVersion: '1.24.8', 44 | application: 'app', 45 | deployments: [ 46 | helm: [ 47 | repoType : 'GIT', 48 | repoUrl : 'url', 49 | chartName: 'chartName', 50 | version : '1.0' 51 | ] 52 | ], 53 | buildImages: [ 54 | helm: [ 55 | image: 'helmImg' 56 | ] 57 | ] 58 | ], 59 | 'namespace', 60 | 'this/is/a/valuesfile') 61 | 62 | assertThat(scriptMock.dockerMock.actualImages[0]).isEqualTo('helmImg') 63 | assertThat(scriptMock.actualShArgs[0]).isEqualTo('[returnStdout:true, script:helm template app workspace/.helmChartTempDir/chart/ -n namespace --kube-version 1.24.8 -f this/is/a/valuesfile]') 64 | } 65 | 66 | @Test 67 | void 'correct helm release with helm repo'() { 68 | argoCdReleaseTest.create([ 69 | k8sVersion: '1.24.8', 70 | application: 'app', 71 | deployments: [ 72 | helm: [ 73 | repoType : 'HELM', 74 | repoUrl : 'url', 75 | chartName: 'chartName', 76 | version : '1.0' 77 | ] 78 | ], 79 | buildImages: [ 80 | helm: [ 81 | image: 'helmImg' 82 | ] 83 | ] 84 | ], 85 | 'namespace', 86 | 'this/is/a/valuesfile') 87 | 88 | assertThat(scriptMock.dockerMock.actualImages[0]).isEqualTo('helmImg') 89 | assertThat(scriptMock.actualShArgs[0]).isEqualTo('[returnStdout:true, script:helm template app workspace/.helmChartTempDir/chart/chartName -n namespace --kube-version 1.24.8 -f this/is/a/valuesfile]') 90 | } 91 | 92 | @Test 93 | void 'correct helm release with local repo'() { 94 | argoCdReleaseTest.create([ 95 | k8sVersion: '1.24.8', 96 | application: 'app', 97 | deployments: [ 98 | helm: [ 99 | repoType : 'LOCAL', 100 | chartPath: 'my/path', 101 | version : '1.0' 102 | ] 103 | ], 104 | buildImages: [ 105 | helm: [ 106 | image: 'helmImg' 107 | ] 108 | ] 109 | ], 110 | 'namespace', 111 | 'this/is/a/valuesfile') 112 | 113 | assertThat(scriptMock.dockerMock.actualImages[0]).isEqualTo('helmImg') 114 | assertThat(scriptMock.actualShArgs[0]).isEqualTo('[returnStdout:true, ' + 115 | 'script:helm template app workspace/my/path -n namespace --kube-version 1.24.8 -f this/is/a/valuesfile]') 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /test/com/cloudogu/gitopsbuildlib/deployment/helm/helmrelease/FluxV1ReleaseTest.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.gitopsbuildlib.deployment.helm.helmrelease 2 | 3 | import com.cloudogu.gitopsbuildlib.ScriptMock 4 | import org.junit.jupiter.api.BeforeEach 5 | import org.junit.jupiter.api.Test 6 | 7 | import static org.assertj.core.api.Assertions.assertThat 8 | 9 | class FluxV1ReleaseTest { 10 | 11 | def scriptMock = new ScriptMock() 12 | def fluxV1Release = new FluxV1Release(scriptMock.mock) 13 | 14 | @BeforeEach 15 | void init () { 16 | scriptMock.configYaml = 'a: b' 17 | } 18 | 19 | @Test 20 | void 'correct helm release with git repo'() { 21 | def output = fluxV1Release.create([ 22 | application: 'app', 23 | deployments: [ 24 | helm: [ 25 | repoType: 'GIT', 26 | repoUrl: 'url', 27 | chartName: 'chartName', 28 | version: '1.0' 29 | ] 30 | ] 31 | ], 32 | 'namespace', 'this/is/a/valuesfile') 33 | 34 | assertThat(output).isEqualTo("""apiVersion: helm.fluxcd.io/v1 35 | kind: HelmRelease 36 | metadata: 37 | name: app 38 | namespace: namespace 39 | annotations: 40 | fluxcd.io/automated: "false" 41 | spec: 42 | releaseName: app 43 | chart: 44 | git: url 45 | ref: 1.0 46 | path: . 47 | values: 48 | a: b 49 | """) 50 | } 51 | 52 | @Test 53 | void 'correct helm release with helm repo'() { 54 | def output = fluxV1Release.create([ 55 | application: 'app', 56 | deployments: [ 57 | helm: [ 58 | repoType: 'HELM', 59 | repoUrl: 'url', 60 | chartName: 'chartName', 61 | version: '1.0' 62 | ] 63 | ] 64 | ], 65 | 'namespace', 66 | 'this/is/a/valuesfile') 67 | 68 | assertThat(output).isEqualTo("""apiVersion: helm.fluxcd.io/v1 69 | kind: HelmRelease 70 | metadata: 71 | name: app 72 | namespace: namespace 73 | annotations: 74 | fluxcd.io/automated: "false" 75 | spec: 76 | releaseName: app 77 | chart: 78 | repository: url 79 | name: chartName 80 | version: 1.0 81 | values: 82 | a: b 83 | """) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /test/com/cloudogu/gitopsbuildlib/deployment/helm/helmrelease/HelmReleaseTest.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.gitopsbuildlib.deployment.helm.helmrelease 2 | 3 | import com.cloudogu.gitopsbuildlib.ScriptMock 4 | import org.junit.jupiter.api.BeforeEach 5 | import org.junit.jupiter.api.Test 6 | 7 | import static org.assertj.core.api.Assertions.assertThat 8 | 9 | class HelmReleaseTest { 10 | 11 | def scriptMock = new ScriptMock() 12 | def repoType = new HelmReleaseUnderTest(scriptMock.mock) 13 | 14 | @BeforeEach 15 | void init () { 16 | scriptMock.configYaml = 'a: b' 17 | } 18 | 19 | @Test 20 | void 'inline yaml test'() { 21 | def output = repoType.fileToInlineYaml('filepath') 22 | assertThat(scriptMock.actualReadFileArgs[0]).isEqualTo('filepath') 23 | assertThat(output).isEqualTo(' a: b') 24 | } 25 | 26 | class HelmReleaseUnderTest extends HelmRelease { 27 | 28 | HelmReleaseUnderTest(Object script) { 29 | super(script) 30 | } 31 | 32 | @Override 33 | String create(Map gitopsConfig, String namespace, String valuesFile) { 34 | return null 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test/com/cloudogu/gitopsbuildlib/deployment/helm/repotype/GitRepoTest.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.gitopsbuildlib.deployment.helm.repotype 2 | 3 | import com.cloudogu.gitopsbuildlib.ScriptMock 4 | import org.junit.jupiter.api.Test 5 | import org.mockito.ArgumentCaptor 6 | 7 | import static org.assertj.core.api.Assertions.assertThat 8 | import static org.mockito.Mockito.times 9 | import static org.mockito.Mockito.verify 10 | 11 | class GitRepoTest { 12 | 13 | def scriptMock = new ScriptMock() 14 | def gitRepo = new GitRepo(scriptMock.mock) 15 | 16 | @Test 17 | void 'merges values successfully'() { 18 | gitRepo.prepareRepo([ 19 | buildImages: [ 20 | helm: [ 21 | image: 'helmImage' 22 | ] 23 | ], 24 | deployments: [ 25 | helm: [ 26 | repoUrl: 'url', 27 | chartPath: 'chartPath', 28 | version: '1.0' 29 | ] 30 | ] 31 | ], ".helmChartTempDir", "chartRootDir") 32 | 33 | assertThat(scriptMock.actualShArgs[0]).isEqualTo('helm dep update workspace/.helmChartTempDir/chartRootDir/chartPath') 34 | 35 | ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Map.class) 36 | verify(scriptMock.gitMock).call(argumentCaptor.capture()) 37 | assertThat(argumentCaptor.getValue().url).isEqualTo('url') 38 | assertThat(argumentCaptor.getValue().branch).isEqualTo('main') 39 | verify(scriptMock.gitMock, times(1)).fetch() 40 | } 41 | 42 | @Test 43 | void 'Respects different main branch of helm repo'() { 44 | gitRepo.prepareRepo([ 45 | buildImages: [ 46 | helm: [ 47 | image: 'helmImage' 48 | ] 49 | ], 50 | deployments: [ 51 | helm: [ 52 | repoUrl: 'url', 53 | chartPath: 'chartPath', 54 | version: '1.0', 55 | mainBranch: 'other' 56 | ] 57 | ] 58 | ], ".helmChartTempDir", "chartRootDir") 59 | 60 | ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Map.class) 61 | verify(scriptMock.gitMock).call(argumentCaptor.capture()) 62 | assertThat(argumentCaptor.getValue().url).isEqualTo('url') 63 | assertThat(argumentCaptor.getValue().branch).isEqualTo('other') 64 | verify(scriptMock.gitMock, times(1)).fetch() 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /test/com/cloudogu/gitopsbuildlib/deployment/helm/repotype/HelmRepoTest.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.gitopsbuildlib.deployment.helm.repotype 2 | 3 | import com.cloudogu.gitopsbuildlib.ScriptMock 4 | import org.junit.jupiter.api.* 5 | import static org.assertj.core.api.Assertions.assertThat 6 | 7 | class HelmRepoTest { 8 | 9 | def scriptMock = new ScriptMock() 10 | def helmRepo = new HelmRepo(scriptMock.mock) 11 | 12 | @Test 13 | void 'merges values successfully'() { 14 | helmRepo.prepareRepo([ 15 | buildImages: [ 16 | helm: [ 17 | image: 'helmImage' 18 | ] 19 | ], 20 | deployments: [ 21 | helm: [ 22 | repoUrl: 'url', 23 | chartName: 'chartName', 24 | version: '1.0' 25 | ] 26 | ] 27 | ], ".helmChartTempDir", "chartRoot") 28 | 29 | assertThat(scriptMock.actualShArgs[0]).isEqualTo('helm repo add chartRepo url') 30 | assertThat(scriptMock.actualShArgs[1]).isEqualTo('helm repo update') 31 | assertThat(scriptMock.actualShArgs[2]).isEqualTo('helm pull chartRepo/chartName --version=1.0 --untar --untardir=workspace/.helmChartTempDir/chartRoot') 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test/com/cloudogu/gitopsbuildlib/docker/DockerWrapperTest.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.gitopsbuildlib.docker 2 | 3 | import com.cloudogu.gitopsbuildlib.ScriptMock 4 | import org.junit.jupiter.api.Test 5 | 6 | import static org.assertj.core.api.Assertions.assertThat 7 | 8 | class DockerWrapperTest { 9 | 10 | public static final String EXPECTED_IMAGE = 'ghcr.io/cloudogu/helm:3.11.1-2' 11 | 12 | def scriptMock = new ScriptMock() 13 | 14 | def dockerWrapper = new DockerWrapper(scriptMock.mock) 15 | 16 | def imageConfigMap = [ 17 | image: EXPECTED_IMAGE, 18 | ] 19 | def imageConfigMapWithCredentials = [ 20 | image: EXPECTED_IMAGE, 21 | credentialsId: 'myCredentials' 22 | ] 23 | def imageConfigString = EXPECTED_IMAGE 24 | 25 | @Test 26 | void 'works with imageConfig string'() { 27 | 28 | dockerWrapper.withDockerImage(imageConfigString) { 29 | } 30 | assertThat(scriptMock.dockerMock.actualImages[0]).isEqualTo(EXPECTED_IMAGE) 31 | } 32 | 33 | @Test 34 | void 'works with imageConfig Map'() { 35 | dockerWrapper.withDockerImage(imageConfigMap) { 36 | } 37 | assertThat(scriptMock.dockerMock.actualImages[0]).isEqualTo(EXPECTED_IMAGE) 38 | } 39 | 40 | @Test 41 | void 'works with imageConfig Map with Credentials'() { 42 | dockerWrapper.withDockerImage(imageConfigMapWithCredentials) { 43 | } 44 | assertThat(scriptMock.dockerMock.actualImages[0]).isEqualTo(EXPECTED_IMAGE) 45 | assertThat(scriptMock.dockerMock.actualRegistryArgs[0]).isEqualTo('https://ghcr.io/cloudogu') 46 | assertThat(scriptMock.dockerMock.actualRegistryArgs[1]).isEqualTo('myCredentials') 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /test/com/cloudogu/gitopsbuildlib/validation/HelmKubevalTest.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.gitopsbuildlib.validation 2 | 3 | import com.cloudogu.gitopsbuildlib.ScriptMock 4 | import com.cloudogu.gitopsbuildlib.validation.HelmKubeval 5 | import org.junit.jupiter.api.Test 6 | 7 | import static org.assertj.core.api.Assertions.assertThat 8 | 9 | class HelmKubevalTest { 10 | def scriptMock = new ScriptMock() 11 | def helmKubeval = new HelmKubeval(scriptMock.mock) 12 | 13 | @Test 14 | void 'is executed with repoType GIT'() { 15 | helmKubeval.validate( 16 | 'target', 17 | [ 18 | k8sSchemaVersion: '1.5' 19 | ], 20 | [ 21 | deployments:[ 22 | helm: [ 23 | repoType: 'GIT', 24 | repoUrl: 'chartRepo/namespace/repoPath', 25 | chartPath: 'chartPath', 26 | version: 'version' 27 | ] 28 | ] 29 | ] 30 | ) 31 | assertThat(scriptMock.actualShArgs[0]).isEqualTo('helm kubeval target/chart/chartPath -f target/mergedValues.yaml -v 1.5 --strict --ignore-missing-schemas') 32 | } 33 | 34 | @Test 35 | void 'is executed with repoType HELM'() { 36 | helmKubeval.validate( 37 | 'target', 38 | [ 39 | k8sSchemaVersion: '1.5' 40 | ], 41 | [ 42 | deployments:[ 43 | helm: [ 44 | repoType: 'HELM', 45 | chartName: 'chart', 46 | repoUrl: 'chartRepo', 47 | version: 'version' 48 | ] 49 | ] 50 | ] 51 | ) 52 | assertThat(scriptMock.actualShArgs[0]).isEqualTo('helm kubeval target/chart/chart -f target/mergedValues.yaml -v 1.5 --strict --ignore-missing-schemas') 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /test/com/cloudogu/gitopsbuildlib/validation/KubevalTest.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.gitopsbuildlib.validation 2 | 3 | import com.cloudogu.gitopsbuildlib.ScriptMock 4 | import com.cloudogu.gitopsbuildlib.validation.Kubeval 5 | import org.junit.jupiter.api.Test 6 | 7 | import static org.assertj.core.api.Assertions.assertThat 8 | 9 | class KubevalTest { 10 | def scriptMock = new ScriptMock() 11 | def dockerMock = scriptMock.dockerMock 12 | def kubeval = new Kubeval(scriptMock.mock) 13 | 14 | @Test 15 | void 'is executed with defaults'() { 16 | kubeval.validate( 17 | 'target', 18 | [ 19 | k8sSchemaVersion: '1.5' 20 | ], 21 | [ 22 | sourcePath: 'k8s', 23 | destinationRootPath: '.', 24 | plain: [] 25 | ] 26 | ) 27 | assertThat(scriptMock.actualShArgs[0]).isEqualTo( 28 | 'kubeval -d target -v 1.5 --strict --ignore-missing-schemas' 29 | ) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/com/cloudogu/gitopsbuildlib/validation/ValidatorTest.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.gitopsbuildlib.validation 2 | 3 | import com.cloudogu.gitopsbuildlib.ScriptMock 4 | import com.cloudogu.gitopsbuildlib.deployment.GitopsTool 5 | import com.cloudogu.gitopsbuildlib.deployment.SourceType 6 | import org.junit.jupiter.api.Test 7 | import static org.assertj.core.api.Assertions.assertThat 8 | 9 | class ValidatorTest { 10 | def scriptMock = new ScriptMock() 11 | def dockerMock = scriptMock.dockerMock 12 | def validator = new ValidatorUnderTest(scriptMock.mock) 13 | boolean validateCalled = false 14 | 15 | @Test 16 | void 'withDockerImage mounts workspace'() { 17 | validator.validate(true, GitopsTool.ARGO, SourceType.HELM, "helmDir", 18 | [ 19 | imageRef: 'helm' 20 | ], 21 | [ 22 | buildImages: [ 23 | helm: [ 24 | image: 'helmImage' 25 | ] 26 | ] 27 | ]) 28 | assertThat(dockerMock.actualImages[0]).isEqualTo('helmImage') 29 | assertThat(dockerMock.actualInsideArgs[0]).isEqualTo('-v workspace:workspace --entrypoint=""') 30 | assertThat(validateCalled).as("Validate was not called").isTrue() 31 | } 32 | 33 | @Test 34 | void 'withDockerImage doesnt mount workspace if already in workspace'() { 35 | scriptMock.mock.pwd = { scriptMock.mock.env.WORKSPACE } 36 | validator.validate(true, GitopsTool.ARGO, SourceType.HELM, "helmDir", [ 37 | image: 'helmImage' 38 | ], 39 | [ 40 | buildImages: [ 41 | helm: [ 42 | image: 'helmImageNotBeingUsed' 43 | ] 44 | ] 45 | ]) 46 | assertThat(dockerMock.actualImages[0]).isEqualTo('helmImage') 47 | assertThat(dockerMock.actualInsideArgs[0]).isEqualTo('--entrypoint=""') 48 | assertThat(validateCalled).as("Validate was not called").isTrue() 49 | } 50 | 51 | @Test 52 | void 'skip validator if disabled'() { 53 | validator.validate(false, GitopsTool.ARGO, SourceType.HELM, "helmDir", [ 54 | imageRef: 'helm' 55 | ], 56 | [ 57 | buildImages: [ 58 | helm: 'helmImage' 59 | ] 60 | ]) 61 | assertThat(dockerMock.actualImages[0]).isEqualTo(null) 62 | assertThat(validateCalled).as("Validate was called").isFalse() 63 | assertThat(scriptMock.actualEchoArgs[0]) 64 | .isEqualTo("Skipping validator ValidatorUnderTest because it is configured as enabled=false or doesn't support the given gitopsTool=ARGO or sourceType=HELM") 65 | } 66 | 67 | @Test 68 | void 'get null if no imageRef or image is set in validator'() { 69 | def output = validator.getImageConfig([:], [:]) 70 | 71 | assertThat(output).isEqualTo(null) 72 | } 73 | 74 | @Test 75 | void 'get image if specifically set in validator while also having an imageRef'() { 76 | def output = validator.getImageConfig( 77 | [ 78 | buildImages: [ 79 | notUsedImage: 'nope' 80 | ] 81 | ], 82 | [ 83 | image: 'realImage', 84 | imageRef: 'buildimage.notUsedImage' 85 | ]) 86 | 87 | def expected = [image: 'realImage'] 88 | 89 | assertThat(output).isEqualTo(expected) 90 | } 91 | 92 | @Test 93 | void 'get image if specifically set in validator whisle also having an imageRef'() { 94 | def output = validator.getImageConfig( 95 | [ 96 | buildImages: [ 97 | usedImage: 'yes' 98 | ] 99 | ], 100 | [ 101 | 102 | imageRef: 'usedImage' 103 | ]) 104 | 105 | assertThat(output).isEqualTo('yes') 106 | } 107 | 108 | class ValidatorUnderTest extends Validator { 109 | 110 | ValidatorUnderTest(Object script) { 111 | super(script) 112 | } 113 | 114 | @Override 115 | void validate(String targetDirectory, Map validatorConfig, Map gitopsConfig) { 116 | println "validatorundertest validate" 117 | validateCalled = true 118 | } 119 | 120 | @Override 121 | SourceType[] getSupportedSourceTypes() { 122 | return [SourceType.PLAIN, SourceType.HELM] 123 | } 124 | 125 | @Override 126 | GitopsTool[] getSupportedGitopsTools() { 127 | return [GitopsTool.FLUX, GitopsTool.ARGO] 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /test/com/cloudogu/gitopsbuildlib/validation/YamllintTest.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.gitopsbuildlib.validation 2 | 3 | import com.cloudogu.gitopsbuildlib.ScriptMock 4 | import com.cloudogu.gitopsbuildlib.validation.Yamllint 5 | import org.junit.jupiter.api.Test 6 | 7 | import static org.assertj.core.api.Assertions.assertThat 8 | 9 | class YamllintTest { 10 | def scriptMock = new ScriptMock() 11 | def dockerMock = scriptMock.dockerMock 12 | def yamllint = new Yamllint(scriptMock.mock) 13 | 14 | @Test 15 | void 'is executed with defaults'() { 16 | yamllint.validate( 17 | 'target', 18 | [image : 'img', 19 | profile: 'pro'], 20 | [plain: []] 21 | ) 22 | assertThat(scriptMock.actualShArgs[0]).isEqualTo( 23 | 'yamllint -d pro -f standard target' 24 | ) 25 | } 26 | 27 | @Test 28 | void 'is executed without profile'() { 29 | yamllint.validate( 30 | 'target', 31 | [image: 'img'], 32 | [plain: []] 33 | ) 34 | assertThat(scriptMock.actualShArgs[0]).isEqualTo( 35 | 'yamllint -f standard target' 36 | ) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test/com/cloudogu/gitopsbuildlib/validation/utils/KubevalArgsParserTest.groovy: -------------------------------------------------------------------------------- 1 | package com.cloudogu.gitopsbuildlib.validation.utils 2 | 3 | import org.junit.jupiter.api.Test 4 | import static org.assertj.core.api.Assertions.assertThat 5 | 6 | class KubevalArgsParserTest { 7 | 8 | @Test 9 | void 'no config yields correct defaults'() { 10 | KubevalArgsParser argsParser = new KubevalArgsParser() 11 | String output = argsParser.parse([:]) 12 | 13 | assertThat(output).isEqualTo(' --strict --ignore-missing-schemas') 14 | } 15 | 16 | @Test 17 | void 'setting all args yields correct output'() { 18 | KubevalArgsParser argsParser = new KubevalArgsParser() 19 | String output = argsParser.parse([ 20 | strict: true, 21 | ignoreMissingSchemas: true, 22 | skipKinds: ['kind1', 'kind2'] 23 | ]) 24 | 25 | assertThat(output).isEqualTo(' --strict --ignore-missing-schemas --skip-kinds kind1,kind2') 26 | } 27 | 28 | @Test 29 | void 'disabling args yields correct output'() { 30 | KubevalArgsParser argsParser = new KubevalArgsParser() 31 | String output = argsParser.parse([ 32 | strict: false, 33 | ignoreMissingSchemas: false, 34 | skipKinds: [] 35 | ]) 36 | 37 | assertThat(output).isEqualTo('') 38 | } 39 | 40 | @Test 41 | void 'mixing config yields correct output'() { 42 | KubevalArgsParser argsParser = new KubevalArgsParser() 43 | String output = argsParser.parse([ 44 | strict: false, 45 | skipKinds: ['kind1'] 46 | ]) 47 | 48 | assertThat(output).isEqualTo(' --ignore-missing-schemas --skip-kinds kind1') 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /vars/deployViaGitops.groovy: -------------------------------------------------------------------------------- 1 | #!groovy 2 | 3 | import com.cloudogu.gitopsbuildlib.GitRepo 4 | import com.cloudogu.gitopsbuildlib.deployment.Deployment 5 | import com.cloudogu.gitopsbuildlib.deployment.FolderStructureStrategy 6 | import com.cloudogu.gitopsbuildlib.deployment.GitopsTool 7 | import com.cloudogu.gitopsbuildlib.deployment.helm.Helm 8 | import com.cloudogu.gitopsbuildlib.deployment.helm.repotype.RepoType 9 | import com.cloudogu.gitopsbuildlib.deployment.plain.Plain 10 | import com.cloudogu.gitopsbuildlib.scm.SCMManager 11 | import com.cloudogu.gitopsbuildlib.scm.SCMProvider 12 | import com.cloudogu.gitopsbuildlib.validation.HelmKubeval 13 | import com.cloudogu.gitopsbuildlib.validation.Kubeval 14 | import com.cloudogu.gitopsbuildlib.validation.Yamllint 15 | 16 | List getMandatoryFields() { 17 | return [ 18 | 'scm.provider', 'scm.baseUrl', 'scm.repositoryUrl', 'application', 'stages', 'gitopsTool' 19 | ] 20 | } 21 | 22 | Map createDefaultConfig(String k8sVersion) { 23 | 24 | if (k8sVersion == null || k8sVersion == ""){ 25 | k8sVersion = "1.29.8" 26 | } 27 | String helmVersion = '3.16.1-1' 28 | 29 | return [ 30 | k8sVersion : "${k8sVersion}", 31 | cesBuildLibRepo : 'https://github.com/cloudogu/ces-build-lib', 32 | cesBuildLibVersion : '2.5.0', 33 | cesBuildLibCredentialsId: '', 34 | mainBranch : 'main', 35 | buildImages : [ 36 | helm: [ 37 | credentialsId: '', 38 | image: "ghcr.io/cloudogu/helm:${helmVersion}" 39 | ], 40 | kubectl: [ 41 | credentialsId: '', 42 | image: "bitnami/kubectl:${k8sVersion}" 43 | ], 44 | // We use the helm image (that also contains kubeval plugin) to speed up builds by allowing to reuse image 45 | kubeval: [ 46 | credentialsId: '', 47 | image: "ghcr.io/cloudogu/helm:${helmVersion}" 48 | ], 49 | helmKubeval: [ 50 | credentialsId: '', 51 | image: "ghcr.io/cloudogu/helm:${helmVersion}" 52 | ], 53 | yamllint: [ 54 | credentialsId: '', 55 | image: 'cytopia/yamllint:1.26-0.9' 56 | ] 57 | ], 58 | deployments : [ 59 | sourcePath: 'k8s', 60 | destinationRootPath: '.' 61 | ], 62 | validators : [ 63 | kubeval : [ 64 | validator: new Kubeval(this), 65 | enabled : false, 66 | config : [ 67 | // imageRef's are referencing the key in gitopsConfig.buildImages 68 | imageRef : 'kubeval', 69 | k8sSchemaVersion: '1.18.1' 70 | ] 71 | ], 72 | helmKubeval: [ 73 | validator: new HelmKubeval(this), 74 | enabled : false, 75 | config : [ 76 | imageRef : 'helmKubeval', 77 | k8sSchemaVersion: '1.18.1' 78 | ] 79 | ], 80 | yamllint : [ 81 | validator: new Yamllint(this), 82 | //disabled due to image is not longer maintained 83 | enabled : false, 84 | config : [ 85 | imageRef : 'yamllint', 86 | // Default to relaxed profile because it's feasible for mere mortalYAML programmers. 87 | // It still fails on syntax errors. 88 | profile: 'relaxed' 89 | ] 90 | ] 91 | ], 92 | stages : [ 93 | staging : [deployDirectly: true], 94 | production: [deployDirectly: false], 95 | ], 96 | folderStructureStrategy: 'GLOBAL_ENV' 97 | ] 98 | } 99 | 100 | void call(Map gitopsConfig) { 101 | // Merge default config with the one passed as parameter 102 | gitopsConfig = mergeMaps(createDefaultConfig(gitopsConfig.k8sVersion as String), gitopsConfig) 103 | if (validateConfig(gitopsConfig)) { 104 | printWelcomeAndConfigs(gitopsConfig) 105 | cesBuildLib = initCesBuildLib(gitopsConfig.cesBuildLibRepo, gitopsConfig.cesBuildLibVersion, gitopsConfig.cesBuildLibCredentialsId) 106 | deploy(gitopsConfig) 107 | } 108 | } 109 | 110 | def mergeMaps(Map a, Map b) { 111 | return b.inject(a.clone()) { map, entry -> 112 | if (map[entry.key] instanceof Map && entry.value instanceof Map) { 113 | 114 | // due to the stages being the definition of the environment its not a merge but overwriting 115 | if (entry.key == 'stages') 116 | map[entry.key] = entry.value 117 | else 118 | map[entry.key] = mergeMaps(map[entry.key], entry.value) 119 | } else { 120 | map[entry.key] = entry.value 121 | } 122 | return map 123 | } 124 | } 125 | 126 | // Note: had to do this little hack because groovy tests should fail on invalid configurations 127 | // but the error class is jenkins specific so a basic boolean return value was needed. 128 | def validateConfig(Map gitopsConfig) { 129 | return validateMandatoryFields(gitopsConfig) && validateDeploymentConfig(gitopsConfig) 130 | } 131 | 132 | // recursive call used to find keys and values that are not toplevel declarations inside the map 133 | Map findMandatoryFieldKeyValue(def config, List keys) { 134 | for (String key : keys) { 135 | if (config.containsKey(key)) 136 | if (keys.size() == 1) 137 | return [first: true, second: (String) config.get(key)] 138 | else 139 | return this.findMandatoryFieldKeyValue(config.get(key), keys - key) 140 | 141 | else 142 | return [first: false, second: ''] 143 | } 144 | } 145 | 146 | def validateMandatoryFields(Map gitopsConfig) { 147 | def nonValidFields = [] 148 | for (String mandatoryField : mandatoryFields) { 149 | if (mandatoryField.contains('.')) { 150 | Map mandatoryFieldKeyValue = findMandatoryFieldKeyValue(gitopsConfig, mandatoryField.tokenize('.')) 151 | if (!mandatoryFieldKeyValue.get('first') || (mandatoryFieldKeyValue.get('first') && !mandatoryFieldKeyValue.get('second'))) { 152 | nonValidFields += mandatoryField 153 | } 154 | 155 | // Note: "[]" syntax (and also getProperty()) leads to 156 | // Scripts not permitted to use staticMethod org.codehaus.groovy.runtime.DefaultGroovyMethods getAt 157 | } else if (!gitopsConfig.containsKey(mandatoryField)) { 158 | nonValidFields += mandatoryField 159 | } else { 160 | def mandatoryFieldValue = gitopsConfig.get(mandatoryField) 161 | if (!mandatoryFieldValue) 162 | nonValidFields += mandatoryField 163 | } 164 | } 165 | 166 | if (nonValidFields) { 167 | error 'The following fields in the gitops config are mandatory but were not set or have invalid values: ' + nonValidFields 168 | return false 169 | } 170 | return true 171 | } 172 | 173 | def validateDeploymentConfig(Map gitopsConfig) { 174 | // choose whether to execute plain or helm deployments 175 | if (gitopsConfig.deployments.containsKey('plain') && gitopsConfig.deployments.containsKey('helm')) { 176 | error 'Please choose between \'deployments.plain\' and \'deployments.helm\'. Setting both properties is not possible!' 177 | return false 178 | } else if (!gitopsConfig.deployments.containsKey('plain') && !gitopsConfig.deployments.containsKey('helm')) { 179 | error 'One of \'deployments.plain\' or \'deployments.helm\' must be set!' 180 | return false 181 | } 182 | 183 | if (!GitopsTool.get(gitopsConfig.gitopsTool)) { 184 | error "The specified 'gitopsTool' is invalid. Please choose one of the following: ${GitopsTool.values()}" 185 | } 186 | 187 | if (gitopsConfig.containsKey('folderStructureStrategy') && !FolderStructureStrategy.get(gitopsConfig.folderStructureStrategy)) { 188 | error "The specified 'folderStructureStrategy' is invalid. Please choose one of the following: ${FolderStructureStrategy.values()}" 189 | } 190 | 191 | if (gitopsConfig.deployments.containsKey('plain')) { 192 | deployment = new Plain(this, gitopsConfig) 193 | } else if (gitopsConfig.deployments.containsKey('helm')) { 194 | if (!RepoType.create(gitopsConfig.deployments.helm.repoType, this)) { 195 | error("Unknown helm repo type: ${gitopsConfig.deployments.helm.repoType}") 196 | } 197 | deployment = new Helm(this, gitopsConfig) 198 | } 199 | 200 | // load the scm-provider that got selected 201 | switch (gitopsConfig.scm.provider) { 202 | case 'SCMManager': 203 | provider = new SCMManager(this) 204 | provider.setCredentials(gitopsConfig.scm.credentialsId) 205 | provider.setBaseUrl(gitopsConfig.scm.baseUrl) 206 | provider.setRepositoryUrl(gitopsConfig.scm.repositoryUrl) 207 | return true 208 | 209 | default: 210 | error 'The given scm-provider seems to be invalid. Please choose one of the following: \'SCMManager\'.' 211 | return false 212 | } 213 | } 214 | 215 | protected initCesBuildLib(cesBuildLibRepo, cesBuildLibVersion, credentialsId) { 216 | Map retrieverParams = [$class: 'GitSCMSource', remote: cesBuildLibRepo] 217 | if (credentialsId?.trim()) { 218 | retrieverParams << [credentialsId: credentialsId] 219 | } 220 | 221 | return library(identifier: "ces-build-lib@${cesBuildLibVersion}", 222 | retriever: modernSCM(retrieverParams) 223 | ).com.cloudogu.ces.cesbuildlib 224 | } 225 | 226 | protected void deploy(Map gitopsConfig) { 227 | def git = cesBuildLib.Git.new(this, gitopsConfig.scm.credentialsId ?: '') 228 | def gitRepo = prepareGitRepo(git) 229 | def changesOnGitOpsRepo = '' 230 | 231 | try { 232 | dir(gitRepo.configRepoTempDir) { 233 | 234 | git url: provider.getRepositoryUrl(), branch: gitopsConfig.mainBranch, changelog: false, poll: false 235 | git.fetch() 236 | 237 | changesOnGitOpsRepo = aggregateChangesOnGitOpsRepo(syncGitopsRepoPerStage(gitopsConfig, git, gitRepo)) 238 | } 239 | } finally { 240 | sh "rm -rf ${gitRepo.configRepoTempDir}" 241 | } 242 | 243 | if (gitopsConfig.deployments.containsKey('plain')) { 244 | currentBuild.description = createBuildDescription(changesOnGitOpsRepo, gitopsConfig.deployments.plain.updateImages.imageName as String) 245 | } else if (gitopsConfig.deployments.containsKey('helm')) { 246 | currentBuild.description = createBuildDescription(changesOnGitOpsRepo) 247 | } 248 | } 249 | 250 | protected Map prepareGitRepo(def git) { 251 | // Query and store info about application repo before cloning into gitops repo 252 | GitRepo applicationRepo = GitRepo.create(git) 253 | 254 | // Display that Jenkins made the GitOps commits, not the application repo author 255 | git.committerName = 'Jenkins' 256 | git.committerEmail = 'jenkins@cloudogu.com' 257 | 258 | def configRepoTempDir = '.configRepoTempDir' 259 | 260 | return [ 261 | applicationRepo : applicationRepo, 262 | configRepoTempDir: configRepoTempDir 263 | ] 264 | } 265 | 266 | protected HashSet syncGitopsRepoPerStage(Map gitopsConfig, def git, Map gitRepo) { 267 | HashSet allRepoChanges = new HashSet() 268 | 269 | gitopsConfig.stages.each { stage, config -> 270 | //checkout the main_branch before creating a new stage_branch. so it won't be branched off of an already checked out stage_branch 271 | git.checkoutOrCreate(gitopsConfig.mainBranch) 272 | if (config.deployDirectly) { 273 | allRepoChanges += syncGitopsRepo(stage, gitopsConfig.mainBranch, git, gitRepo) 274 | } else { 275 | String stageBranch = "${stage}_${gitopsConfig.application}" 276 | git.checkoutOrCreate(stageBranch) 277 | String repoChanges = syncGitopsRepo(stage, stageBranch, git, gitRepo) 278 | 279 | if (repoChanges) { 280 | def title = 'created by service \'' + gitopsConfig.application + '\' for stage \'' + stage + '\'' 281 | //TODO description functionality needs to be implemented 282 | def description = '' 283 | 284 | provider.createOrUpdatePullRequest(stageBranch, gitopsConfig.mainBranch, title, description) 285 | allRepoChanges += repoChanges 286 | } 287 | } 288 | } 289 | return allRepoChanges 290 | } 291 | 292 | protected String syncGitopsRepo(String stage, String branch, def git, Map gitRepo) { 293 | deployment.create(stage) 294 | return commitAndPushToStage(stage, branch, git, gitRepo) 295 | } 296 | 297 | protected String commitAndPushToStage(String stage, String branch, def git, Map gitRepo) { 298 | String commitPrefix = "[${stage}] " 299 | git.add('.') 300 | if (git.areChangesStagedForCommit()) { 301 | git.commit(commitPrefix + createApplicationCommitMessage(gitRepo.applicationRepo), gitRepo.applicationRepo.authorName, gitRepo.applicationRepo.authorEmail) 302 | 303 | // If some else pushes between the pull above and this push, the build will fail. 304 | // So we pull if push fails and try again 305 | git.pushAndPullOnFailure("origin ${branch}") 306 | return "${stage} (${git.commitHashShort})" 307 | } else { 308 | echo "No changes on gitOps repo for ${stage} (branch: ${branch}). Not committing or pushing." 309 | return '' 310 | } 311 | } 312 | 313 | protected String aggregateChangesOnGitOpsRepo(changes) { 314 | // Remove empty 315 | (changes - '') 316 | // and concat into string 317 | .join('; ') 318 | } 319 | 320 | private String createApplicationCommitMessage(GitRepo applicationRepo) { 321 | String issueIds = (applicationRepo.commitMessage =~ /#\d*/).collect { "${it} " }.join('') 322 | 323 | String[] urlSplit = applicationRepo.repositoryUrl.split('/') 324 | def repoNamespace = urlSplit[-2] 325 | def repoName = urlSplit[-1] 326 | String message = "${issueIds}${repoNamespace}/${repoName}@${applicationRepo.commitHash}" 327 | 328 | return message 329 | } 330 | 331 | protected String createBuildDescription(String pushedChanges, String imageName) { 332 | String description = createBuildDescription(pushedChanges) 333 | description += "\nImage: ${imageName}" 334 | return description 335 | } 336 | 337 | protected String createBuildDescription(String pushedChanges) { 338 | String description = '' 339 | description += "GitOps commits: " 340 | if (pushedChanges) { 341 | description += pushedChanges 342 | } else { 343 | description += 'No changes' 344 | } 345 | return description 346 | } 347 | 348 | private printWelcomeAndConfigs(Map gitopsConfig){ 349 | 350 | print(""" 351 | ################################################################################################################ 352 | 353 | _ _ _ _ _ _ _ _ _ 354 | (_) | | | (_) | | | | (_) | 355 | __ _ _| |_ ___ _ __ ___ ______| |__ _ _ _| | __| |______| |_| |__ 356 | / _` | | __/ _ \\| '_ \\/ __|______| '_ \\| | | | | |/ _` |______| | | '_ \\ 357 | | (_| | | || (_) | |_) \\__ \\ | |_) | |_| | | | (_| | | | | |_) | 358 | \\__, |_|\\__\\___/| .__/|___/ |_.__/ \\__,_|_|_|\\__,_| |_|_|_.__/ 359 | __/ | | | 360 | |___/ |_| 361 | 362 | config: 363 | ${gitopsConfig.collect { key, value -> " $key: $value" }.join("\n")} 364 | 365 | ################################################################################################################ 366 | """) 367 | } 368 | 369 | def cesBuildLib 370 | Deployment deployment 371 | SCMProvider provider 372 | --------------------------------------------------------------------------------