├── .github ├── dependabot.yml ├── release-drafter.yml └── workflows │ ├── cd.yaml │ └── jenkins-security-scan.yml ├── .gitignore ├── .mvn ├── extensions.xml └── maven.config ├── Jenkinsfile ├── LICENSE ├── README.md ├── docs ├── add-cloud.png ├── add-hcloud-button.png ├── add-log-recorder.png ├── add-token.png ├── server-detail.png ├── server-template.png └── template.pkr.hcl ├── pom.xml └── src ├── main ├── java │ └── cloud │ │ └── dnation │ │ └── jenkins │ │ └── plugins │ │ └── hetzner │ │ ├── ConfigurationValidator.java │ │ ├── ControllerListener.java │ │ ├── Helper.java │ │ ├── HetznerCloud.java │ │ ├── HetznerCloudResourceManager.java │ │ ├── HetznerConstants.java │ │ ├── HetznerServerAgent.java │ │ ├── HetznerServerComputer.java │ │ ├── HetznerServerInfo.java │ │ ├── HetznerServerTemplate.java │ │ ├── JenkinsSecretTokenProvider.java │ │ ├── NodeCallable.java │ │ ├── OrphanedNodesCleaner.java │ │ ├── connect │ │ ├── AbstractConnectivity.java │ │ ├── Both.java │ │ ├── BothV6.java │ │ ├── ConnectivityType.java │ │ ├── PrivateOnly.java │ │ ├── PublicOnly.java │ │ └── PublicV6Only.java │ │ ├── launcher │ │ ├── AbstractConnectionMethod.java │ │ ├── AbstractHetznerSshConnector.java │ │ ├── DefaultConnectionMethod.java │ │ ├── DefaultSshConnector.java │ │ ├── DefaultV6ConnectionMethod.java │ │ ├── HetznerServerComputerLauncher.java │ │ ├── PublicAddressOnly.java │ │ ├── PublicV6AddressOnly.java │ │ └── SshConnectorAsRoot.java │ │ ├── primaryip │ │ ├── AbstractByLabelSelector.java │ │ ├── AbstractPrimaryIpStrategy.java │ │ ├── ByLabelSelectorFailing.java │ │ ├── ByLabelSelectorIgnoring.java │ │ └── DefaultStrategy.java │ │ └── shutdown │ │ ├── AbstractShutdownPolicy.java │ │ ├── BeforeHourWrapsPolicy.java │ │ └── IdlePeriodPolicy.java └── resources │ ├── cloud │ └── dnation │ │ └── jenkins │ │ └── plugins │ │ └── hetzner │ │ ├── HetznerCloud │ │ ├── config.jelly │ │ ├── help-credentialsId.html │ │ └── help-name.html │ │ ├── HetznerServerAgent │ │ └── configure-entries.jelly │ │ ├── HetznerServerComputer │ │ └── main.jelly │ │ ├── HetznerServerTemplate │ │ ├── config.jelly │ │ ├── help-automountVolumes.html │ │ ├── help-bootDeadline.html │ │ ├── help-connectivity.html │ │ ├── help-connector.html │ │ ├── help-image.html │ │ ├── help-jvmOpts.html │ │ ├── help-labelStr.html │ │ ├── help-location.html │ │ ├── help-name.html │ │ ├── help-network.html │ │ ├── help-numExecutors.html │ │ ├── help-placementGroup.html │ │ ├── help-primaryIp.html │ │ ├── help-remoteFs.html │ │ ├── help-serverType.html │ │ ├── help-shutdownPolicy.html │ │ ├── help-userData.html │ │ └── help-volumeIds.html │ │ ├── Messages.properties │ │ ├── launcher │ │ └── AbstractHetznerSshConnector │ │ │ └── config.jelly │ │ ├── primaryip │ │ ├── ByLabelSelectorFailing │ │ │ ├── config.jelly │ │ │ └── help-selector.html │ │ └── ByLabelSelectorIgnoring │ │ │ ├── config.jelly │ │ │ └── help-selector.html │ │ └── shutdown │ │ ├── BeforeHourWrapsPolicy │ │ └── help.html │ │ └── IdlePeriodPolicy │ │ ├── config.jelly │ │ └── help-idleMinutes.html │ └── index.jelly └── test ├── java └── cloud │ └── dnation │ └── jenkins │ └── plugins │ └── hetzner │ ├── HelperTest.java │ ├── HetznerCloudResourceManagerTest.java │ ├── HetznerCloudSimpleTest.java │ ├── HetznerCloudTest.java │ ├── JCasCTest.java │ ├── TestHelper.java │ ├── launcher │ └── TestPublicV6AddressOnly.java │ └── primaryip │ └── PrimaryIpStrategyTest.java └── resources ├── cloud └── dnation │ └── jenkins │ └── plugins │ └── hetzner │ └── jcasc.yaml ├── id_ed25519 ├── id_ed25519.pub ├── id_rsa └── id_rsa.pub /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | _extends: .github 2 | version-template: $MAJOR.$MINOR.$PATCH -------------------------------------------------------------------------------- /.github/workflows/cd.yaml: -------------------------------------------------------------------------------- 1 | # Note: additional setup is required, see https://www.jenkins.io/redirect/continuous-delivery-of-plugins 2 | 3 | name: cd 4 | on: 5 | workflow_dispatch: 6 | check_run: 7 | types: 8 | - completed 9 | 10 | jobs: 11 | maven-cd: 12 | uses: jenkins-infra/github-reusable-workflows/.github/workflows/maven-cd.yml@v1 13 | secrets: 14 | MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }} 15 | MAVEN_TOKEN: ${{ secrets.MAVEN_TOKEN }} 16 | -------------------------------------------------------------------------------- /.github/workflows/jenkins-security-scan.yml: -------------------------------------------------------------------------------- 1 | name: Jenkins Security Scan 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | types: [ opened, synchronize, reopened ] 9 | workflow_dispatch: 10 | 11 | permissions: 12 | security-events: write 13 | contents: read 14 | actions: read 15 | 16 | jobs: 17 | security-scan: 18 | uses: jenkins-infra/jenkins-security-scan/.github/workflows/jenkins-security-scan.yaml@v2 19 | with: 20 | java-cache: 'maven' # Optionally enable use of a build dependency cache. Specify 'maven' or 'gradle' as appropriate. 21 | # java-version: 21 # Optionally specify what version of Java to set up for the build, or remove to use a recent default. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | bin 3 | build 4 | .settings 5 | .classpath 6 | .project 7 | .idea 8 | *.iml 9 | gradle 10 | gradlew.bat 11 | gradlew 12 | work 13 | target 14 | -------------------------------------------------------------------------------- /.mvn/extensions.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | io.jenkins.tools.incrementals 4 | git-changelist-maven-extension 5 | 1.2 6 | 7 | 8 | -------------------------------------------------------------------------------- /.mvn/maven.config: -------------------------------------------------------------------------------- 1 | -Pmight-produce-incrementals 2 | -Pconsume-incrementals 3 | -Dchangelist.format=%d.v%s 4 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | /* 2 | See the documentation for more options: 3 | https://github.com/jenkins-infra/pipeline-library/ 4 | */ 5 | buildPlugin( 6 | useContainerAgent: true, 7 | configurations: [ 8 | [platform: 'linux', jdk: '21'], 9 | [platform: 'windows', jdk: '17'], 10 | ] 11 | ) 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /docs/add-cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/hetzner-cloud-plugin/6442434099094e730286e0f6423b7d423f6e6d6d/docs/add-cloud.png -------------------------------------------------------------------------------- /docs/add-hcloud-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/hetzner-cloud-plugin/6442434099094e730286e0f6423b7d423f6e6d6d/docs/add-hcloud-button.png -------------------------------------------------------------------------------- /docs/add-log-recorder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/hetzner-cloud-plugin/6442434099094e730286e0f6423b7d423f6e6d6d/docs/add-log-recorder.png -------------------------------------------------------------------------------- /docs/add-token.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/hetzner-cloud-plugin/6442434099094e730286e0f6423b7d423f6e6d6d/docs/add-token.png -------------------------------------------------------------------------------- /docs/server-detail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/hetzner-cloud-plugin/6442434099094e730286e0f6423b7d423f6e6d6d/docs/server-detail.png -------------------------------------------------------------------------------- /docs/server-template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/hetzner-cloud-plugin/6442434099094e730286e0f6423b7d423f6e6d6d/docs/server-template.png -------------------------------------------------------------------------------- /docs/template.pkr.hcl: -------------------------------------------------------------------------------- 1 | # Copyright 2021 https://dnation.cloud 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | variable "image_name" { 16 | description = "Name of image" 17 | type = string 18 | default = "ubuntu20-docker" 19 | 20 | validation { 21 | condition = can(regex("^[\\w-_]+", var.image_name)) 22 | error_message = "The 'image_name' value must be a valid image identifier (only alphanumeric characters or any of '_.-')." 23 | } 24 | } 25 | 26 | variable "location" { 27 | description = "Location where to create image" 28 | type = string 29 | default = "fsn1" 30 | } 31 | 32 | source "hcloud" "jenkins" { 33 | image = "ubuntu-20.04" 34 | location = var.location 35 | server_type = "cx11" 36 | ssh_username = "root" 37 | server_name = "${var.image_name}-image-builder" 38 | snapshot_name = var.image_name 39 | snapshot_labels = { 40 | vendor = "dnation.cloud" 41 | name = var.image_name 42 | } 43 | } 44 | 45 | build { 46 | sources = [ 47 | "source.hcloud.jenkins" 48 | ] 49 | 50 | provisioner "shell" { 51 | environment_vars = [ 52 | "DEBIAN_FRONTEND=noninteractive" 53 | ] 54 | inline = [ 55 | "apt-get clean; apt-get update", 56 | "apt-get install -y --no-install-recommends openjdk-11-jdk apt-transport-https ca-certificates curl gnupg lsb-release", 57 | "useradd --uid 1000 --groups sudo,adm --create-home --home-dir /home/jenkins jenkins", 58 | "echo 'jenkins ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/jenkins", 59 | "curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg", 60 | "echo \"deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] \\", 61 | " https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable\" \\", 62 | " | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null", 63 | "apt-get update; apt-get install -y docker-ce docker-ce-cli containerd.io", 64 | "systemctl enable docker", 65 | "usermod jenkins -a -G docker" 66 | ] 67 | } 68 | } -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 16 | 18 | 4.0.0 19 | io.jenkins.plugins 20 | hetzner-cloud 21 | ${changelist} 22 | hpi 23 | https://github.com/jenkinsci/hetzner-cloud-plugin 24 | 25 | org.jenkins-ci.plugins 26 | plugin 27 | 4.82 28 | 29 | Hetzner Cloud 30 | 31 | 32 | The Apache Software License, Version 2.0 33 | https://www.apache.org/licenses/LICENSE-2.0.txt 34 | repo 35 | 36 | 37 | 38 | 39 | rkosegi 40 | Richard Kosegi 41 | richard.kosegi@gmail.com 42 | 43 | 44 | 45 | scm:git:git@github.com:${gitHubRepo}.git 46 | scm:git:git@github.com:${gitHubRepo}.git 47 | https://github.com:${gitHubRepo} 48 | ${scmTag} 49 | 50 | 51 | 999999-SNAPSHOT 52 | jenkinsci/hetzner-cloud-plugin 53 | 54 | 2.426 55 | ${jenkins.baseline}.3 56 | 57 | 58 | 59 | maven.jenkins-ci.org 60 | https://repo.jenkins-ci.org/releases 61 | 62 | 63 | 64 | 65 | repo.jenkins-ci.org 66 | https://repo.jenkins-ci.org/public/ 67 | 68 | 69 | 70 | 71 | repo.jenkins-ci.org 72 | https://repo.jenkins-ci.org/public/ 73 | 74 | 75 | 76 | 77 | 78 | io.jenkins.tools.bom 79 | bom-${jenkins.baseline}.x 80 | 2555.v3190a_8a_c60c6 81 | pom 82 | import 83 | 84 | 85 | 86 | 87 | 88 | 89 | org.apache.maven.plugins 90 | maven-javadoc-plugin 91 | 92 | false 93 | 94 | 95 | 96 | 97 | 98 | 99 | org.jenkins-ci.plugins 100 | ssh-slaves 101 | 102 | 103 | org.jenkins-ci.plugins 104 | ssh-credentials 105 | 106 | 107 | org.jenkins-ci.plugins 108 | credentials 109 | 110 | 111 | org.jenkins-ci.plugins 112 | plain-credentials 113 | 114 | 115 | org.projectlombok 116 | lombok 117 | 1.18.32 118 | 119 | 120 | org.jenkins-ci.plugins 121 | bouncycastle-api 122 | 2.27 123 | 124 | 125 | cloud.dnation.integration 126 | hetzner-cloud-client-java 127 | 1.8.0 128 | 129 | 130 | org.jenkins-ci.plugins 131 | trilead-api 132 | 133 | 134 | io.jenkins.plugins 135 | eddsa-api 136 | 0.3.0-4.v84c6f0f4969e 137 | 138 | 139 | org.jenkins-ci.plugins 140 | cloud-stats 141 | 0.27 142 | 143 | 144 | com.github.spotbugs 145 | spotbugs-annotations 146 | 147 | 148 | org.mockito 149 | mockito-core 150 | test 151 | 152 | 153 | io.jenkins.configuration-as-code 154 | test-harness 155 | test 156 | 157 | 158 | org.awaitility 159 | awaitility 160 | 4.2.0 161 | test 162 | 163 | 164 | org.hamcrest 165 | hamcrest 166 | 167 | 168 | 169 | 170 | 171 | -------------------------------------------------------------------------------- /src/main/java/cloud/dnation/jenkins/plugins/hetzner/ConfigurationValidator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 https://dnation.cloud 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cloud.dnation.jenkins.plugins.hetzner; 17 | 18 | import cloud.dnation.hetznerclient.ClientFactory; 19 | import cloud.dnation.hetznerclient.GetDatacentersResponse; 20 | import cloud.dnation.hetznerclient.GetImageByIdResponse; 21 | import cloud.dnation.hetznerclient.GetImagesBySelectorResponse; 22 | import cloud.dnation.hetznerclient.GetLocationsResponse; 23 | import cloud.dnation.hetznerclient.GetNetworkByIdResponse; 24 | import cloud.dnation.hetznerclient.GetNetworksBySelectorResponse; 25 | import cloud.dnation.hetznerclient.GetPlacementGroupByIdResponse; 26 | import cloud.dnation.hetznerclient.GetPlacementGroupsResponse; 27 | import cloud.dnation.hetznerclient.GetServerTypesResponse; 28 | import cloud.dnation.hetznerclient.GetVolumeByIdResponse; 29 | import cloud.dnation.hetznerclient.HetznerApi; 30 | import com.google.common.base.Preconditions; 31 | import com.google.common.base.Strings; 32 | import com.google.common.base.Throwables; 33 | import com.google.common.primitives.Ints; 34 | import hudson.util.FormValidation; 35 | import lombok.Data; 36 | import lombok.extern.slf4j.Slf4j; 37 | 38 | import java.util.Arrays; 39 | 40 | @Slf4j 41 | public class ConfigurationValidator { 42 | /** 43 | * Simple validation of credentialsId. This is implemented by listing all datacenters. 44 | * 45 | * @param credentialsId credentialsId used for client connection 46 | * @return ValidationResult 47 | */ 48 | static ValidationResult validateCloudConfig(String credentialsId) { 49 | return validateWithClient(api -> { 50 | final GetDatacentersResponse result = api.getAllDatacenters().execute().body(); 51 | Preconditions.checkArgument(result.getDatacenters().size() > 0, "Expected some data"); 52 | return ValidationResult.OK; 53 | }, credentialsId); 54 | } 55 | 56 | /** 57 | * Perform given {@link ValidationAction} using {@link HetznerApi} created from credentialsId. 58 | * 59 | * @param action action to perform 60 | * @param credentialsId credentials for API client 61 | * @return ValidationResult 62 | */ 63 | private static ValidationResult validateWithClient(ValidationAction action, String credentialsId) { 64 | final HetznerApi client = ClientFactory.create(JenkinsSecretTokenProvider.forCredentialsId(credentialsId)); 65 | try { 66 | return action.validate(client); 67 | } catch (Exception e) { 68 | return ValidationResult.fromException(e); 69 | } 70 | } 71 | 72 | /** 73 | * Attempt to validate provided string as valid name of datacenter. 74 | * 75 | * @param datacenter name of datacenter to validate. 76 | * @param credentialsId credentialsId used for client connection 77 | * @return ValidationResult representing result 78 | */ 79 | static ValidationResult validateDatacenter(String datacenter, String credentialsId) { 80 | if (Strings.isNullOrEmpty(datacenter)) { 81 | return new ValidationResult(false, "Datacenter is empty"); 82 | } 83 | return validateWithClient(api -> { 84 | final GetDatacentersResponse result = api.getAllDatacentersWithName(datacenter) 85 | .execute().body(); 86 | Preconditions.checkArgument(result.getDatacenters().size() == 1, 87 | "Expected exactly one result, got %s", result.getDatacenters().size()); 88 | return new ValidationResult(true, "Found: " + 89 | result.getDatacenters().get(0).getDescription()); 90 | 91 | }, credentialsId); 92 | } 93 | 94 | /** 95 | * Attempt to validate provided label expression as valid filter for image. 96 | * 97 | * @param image label expression that should resolve to single image. 98 | * @param credentialsId credentialsId used for client connection 99 | * @return ValidationResult representing result 100 | */ 101 | static ValidationResult verifyImage(String image, String credentialsId) { 102 | if (Strings.isNullOrEmpty(image)) { 103 | return new ValidationResult(false, "Image label expression is empty"); 104 | } 105 | return validateWithClient(api -> { 106 | if (Helper.isLabelExpression(image)) { 107 | GetImagesBySelectorResponse result = api.getImagesBySelector(image).execute().body(); 108 | Preconditions.checkArgument(result.getImages().size() == 1, 109 | "Expected exactly one result, got %s", result.getImages().size()); 110 | return new ValidationResult(true, "Found: " + 111 | result.getImages().get(0).getDescription()); 112 | } else if (Helper.isPossiblyLong(image)) { 113 | final GetImageByIdResponse result = api.getImageById(Long.parseLong(image)).execute().body(); 114 | return new ValidationResult(true, "Found: " + 115 | result.getImage().getDescription()); 116 | } else { 117 | return new ValidationResult(false, "Image expression unsupported : " + image); 118 | } 119 | }, credentialsId); 120 | } 121 | 122 | static ValidationResult verifyNetwork(String network, String credentialsId) { 123 | if (Strings.isNullOrEmpty(network)) { 124 | return new ValidationResult(false, "Network label expression is empty"); 125 | } 126 | return validateWithClient(api -> { 127 | if (Helper.isLabelExpression(network)) { 128 | final GetNetworksBySelectorResponse result = api.getNetworkBySelector(network).execute().body(); 129 | Preconditions.checkArgument(result.getNetworks().size() == 1, 130 | "Expected exactly one result, got %s", result.getNetworks().size()); 131 | return new ValidationResult(true, "Found: " + 132 | result.getNetworks().get(0).getName() + " " + 133 | result.getNetworks().get(0).getIpRange()); 134 | } else if (Helper.isPossiblyLong(network)) { 135 | final GetNetworkByIdResponse result = api.getNetworkById(Integer.parseInt(network)).execute().body(); 136 | return new ValidationResult(true, "Found: " + 137 | result.getNetwork().getName() + " " + result.getNetwork().getIpRange()); 138 | } else { 139 | return new ValidationResult(false, "Network expression unsupported : " + network); 140 | } 141 | }, credentialsId); 142 | } 143 | 144 | static ValidationResult verifyPlacementGroup(String placementGroup, String credentialsId) { 145 | if (Strings.isNullOrEmpty(placementGroup)) { 146 | return new ValidationResult(false, "Placement group expression is empty"); 147 | } 148 | return validateWithClient(api -> { 149 | if (Helper.isLabelExpression(placementGroup)) { 150 | final GetPlacementGroupsResponse result = api.getPlacementGroups(placementGroup).execute().body(); 151 | Preconditions.checkArgument(result.getPlacementGroups().size() == 1, 152 | "Expected exactly one result, got %s", result.getPlacementGroups().size()); 153 | return new ValidationResult(true, "Found: " + 154 | result.getPlacementGroups().get(0).getName() + " " + 155 | result.getPlacementGroups().get(0).getId()); 156 | } else if (Helper.isPossiblyLong(placementGroup)) { 157 | final GetPlacementGroupByIdResponse result = api.getPlacementGroupById(Integer.parseInt(placementGroup)).execute().body(); 158 | return new ValidationResult(true, "Found: " + 159 | result.getPlacementGroup().getName() + " " + result.getPlacementGroup().getId()); 160 | } else { 161 | return new ValidationResult(false, "Placement group expression unsupported : " + placementGroup); 162 | } 163 | }, credentialsId); 164 | } 165 | 166 | /** 167 | * Attempt to validate given server type name. 168 | * 169 | * @param serverType server type name 170 | * @param credentialsId credentialsId used for client connection 171 | * @return ValidationResult representing result 172 | */ 173 | static ValidationResult verifyServerType(String serverType, String credentialsId) { 174 | if (Strings.isNullOrEmpty(serverType)) { 175 | return new ValidationResult(false, "Server type is empty"); 176 | } 177 | return validateWithClient(api -> { 178 | final GetServerTypesResponse result = api.getAllServerTypesWithName(serverType).execute().body(); 179 | Preconditions.checkArgument(result.getServerTypes().size() == 1, 180 | "Expected exactly one result, got {}", result.getServerTypes().size()); 181 | return new ValidationResult(true, "Found: " + 182 | result.getServerTypes().get(0).getDescription()); 183 | 184 | }, credentialsId); 185 | } 186 | 187 | /** 188 | * Attempt to validate location name. 189 | * 190 | * @param location name of location to validate 191 | * @param credentialsId credentialsId used for client connection 192 | * @return ValidationResult representing result 193 | */ 194 | static ValidationResult verifyLocation(String location, String credentialsId) { 195 | if (Strings.isNullOrEmpty(location)) { 196 | return new ValidationResult(false, "Location is empty"); 197 | } 198 | if (location.contains("-")) { 199 | return validateDatacenter(location, credentialsId); 200 | } 201 | return validateWithClient(api -> { 202 | final GetLocationsResponse result = api.getAllLocationsWithName(location).execute().body(); 203 | Preconditions.checkArgument(result.getLocations().size() == 1, 204 | "Expected exactly one result, got {}", result.getLocations().size()); 205 | return new ValidationResult(true, "Found: " + 206 | result.getLocations().get(0).getDescription()); 207 | 208 | }, credentialsId); 209 | } 210 | 211 | static ValidationResult verifyVolume(String volume, String credentialsId) { 212 | return validateWithClient(api -> { 213 | if (!Helper.isPossiblyLong(volume)) { 214 | return new ValidationResult(false, String.format("not a valid volume ID: %s", volume)); 215 | } 216 | final GetVolumeByIdResponse result = api.getVolumeById(Long.parseLong(volume)).execute().body(); 217 | if (result == null) { 218 | return new ValidationResult(false, String.format("Volume %s not found", volume)); 219 | }else { 220 | return new ValidationResult(true, String.format("%s: %s", 221 | result.getVolume().getName(), result.getVolume().getFormat())); 222 | } 223 | }, credentialsId); 224 | } 225 | 226 | static ValidationResult verifyVolumes(String volumeIds, String credentialId) { 227 | log.info("volumeIds: {}", volumeIds); 228 | return Arrays.stream(volumeIds.split(",")).map(volId -> verifyVolume(volId, credentialId)) 229 | .filter(res -> !res.isSuccess()).findFirst().orElse(ValidationResult.OK); 230 | } 231 | 232 | public static FormValidation doCheckPositiveInt(String value, String name) { 233 | if (Ints.tryParse(value) == null) { 234 | return FormValidation.error(name + " must be positive integer : " + value); 235 | } 236 | return FormValidation.ok(); 237 | } 238 | 239 | public static FormValidation doCheckNonEmpty(String value, String name) { 240 | if (Strings.isNullOrEmpty(value)) { 241 | return FormValidation.error(name + " must be specified"); 242 | } 243 | return FormValidation.ok(); 244 | } 245 | 246 | // can't use java.util.Function due to declared checked exception 247 | private interface ValidationAction { 248 | ValidationResult validate(HetznerApi client) throws Exception; 249 | } 250 | 251 | @Data 252 | static class ValidationResult { 253 | static final ValidationResult OK = new ValidationResult(true, "OK"); 254 | private final boolean success; 255 | private final String message; 256 | 257 | static ValidationResult fromException(Throwable e) { 258 | log.warn("API invocation failed", e); 259 | return new ValidationResult(false, Throwables.getRootCause(e).getMessage()); 260 | } 261 | 262 | /** 263 | * Convert this instance to {@link FormValidation}. 264 | * 265 | * @return FormValidation 266 | */ 267 | FormValidation toFormValidation() { 268 | if (success) { 269 | return FormValidation.ok(message); 270 | } else { 271 | return FormValidation.error(message); 272 | } 273 | } 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /src/main/java/cloud/dnation/jenkins/plugins/hetzner/ControllerListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 https://dnation.cloud 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cloud.dnation.jenkins.plugins.hetzner; 17 | 18 | import edu.umd.cs.findbugs.annotations.NonNull; 19 | import hudson.Extension; 20 | import hudson.model.Computer; 21 | import hudson.model.TaskListener; 22 | import hudson.slaves.ComputerListener; 23 | import hudson.slaves.OfflineCause; 24 | import jenkins.model.Jenkins; 25 | import lombok.extern.slf4j.Slf4j; 26 | 27 | import java.io.IOException; 28 | import java.util.Arrays; 29 | 30 | /** 31 | * {@link ComputerListener} that is responsible to perform cleanup tasks when Jenkins' controller node 32 | * comes online or offline. 33 | */ 34 | @Slf4j 35 | @Extension 36 | public class ControllerListener extends ComputerListener { 37 | @Override 38 | public void onOnline(Computer c, TaskListener listener) throws IOException, InterruptedException { 39 | //on controller startup, check for any orphan VMs in cloud 40 | if ("".equals(c.getName())) { 41 | OrphanedNodesCleaner.doCleanup(); 42 | } 43 | super.onOnline(c, listener); 44 | } 45 | 46 | @Override 47 | public void onOffline(@NonNull Computer c, OfflineCause cause) { 48 | //on controller shutdown, terminate any existing Hetzner agent and computer 49 | if ("".equals(c.getName())) { 50 | Helper.getHetznerAgents().forEach(this::terminateAgent); 51 | Arrays.stream(Jenkins.get().getComputers()) 52 | .filter(HetznerServerComputer.class::isInstance) 53 | .forEach(this::deleteComputer); 54 | } 55 | super.onOffline(c, cause); 56 | } 57 | 58 | private void deleteComputer(Computer computer) { 59 | try { 60 | log.info("Deleting computer {}", computer); 61 | computer.doDoDelete(); 62 | } catch (IOException e) { 63 | log.error("Failed to delete computer", e); 64 | } 65 | } 66 | 67 | private void terminateAgent(HetznerServerAgent agent) { 68 | try { 69 | log.info("Terminating Hetzner agent {}", agent.getDisplayName()); 70 | agent.terminate(); 71 | } catch (Exception e) { 72 | log.error("Failed to terminate agent", e); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/cloud/dnation/jenkins/plugins/hetzner/Helper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 https://dnation.cloud 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cloud.dnation.jenkins.plugins.hetzner; 17 | 18 | import com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey; 19 | import com.cloudbees.plugins.credentials.CredentialsMatchers; 20 | import com.cloudbees.plugins.credentials.CredentialsProvider; 21 | import com.google.common.base.Preconditions; 22 | import com.google.common.base.Strings; 23 | import com.trilead.ssh2.crypto.PEMDecoder; 24 | import hudson.security.ACL; 25 | import jenkins.model.Jenkins; 26 | import lombok.Getter; 27 | import lombok.RequiredArgsConstructor; 28 | import lombok.experimental.UtilityClass; 29 | import net.i2p.crypto.eddsa.EdDSAPublicKey; 30 | import org.bouncycastle.crypto.params.AsymmetricKeyParameter; 31 | import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters; 32 | import org.bouncycastle.crypto.params.RSAKeyParameters; 33 | import org.bouncycastle.crypto.util.OpenSSHPublicKeyUtil; 34 | import org.slf4j.Logger; 35 | import retrofit2.Response; 36 | 37 | import javax.annotation.Nonnull; 38 | import javax.annotation.Nullable; 39 | import java.io.IOException; 40 | import java.io.PrintStream; 41 | import java.security.KeyPair; 42 | import java.security.PublicKey; 43 | import java.security.interfaces.RSAPublicKey; 44 | import java.time.Duration; 45 | import java.time.LocalDateTime; 46 | import java.time.ZoneOffset; 47 | import java.time.format.DateTimeFormatter; 48 | import java.util.Arrays; 49 | import java.util.Base64; 50 | import java.util.Collections; 51 | import java.util.List; 52 | import java.util.Optional; 53 | import java.util.function.Function; 54 | import java.util.logging.Level; 55 | import java.util.logging.LogRecord; 56 | import java.util.logging.SimpleFormatter; 57 | import java.util.regex.Pattern; 58 | import java.util.stream.Collectors; 59 | 60 | import static cloud.dnation.jenkins.plugins.hetzner.HetznerConstants.SHUTDOWN_TIME_BUFFER; 61 | 62 | @UtilityClass 63 | public class Helper { 64 | private static final Pattern LABEL_VALUE_RE = Pattern.compile("^(?![0-9]+$)(?!-)[a-zA-Z0-9-_.]{0,63}(?true if given expression could be label expression, false otherwise 100 | */ 101 | public static boolean isLabelExpression(String expression) { 102 | return expression.contains("="); 103 | } 104 | 105 | /** 106 | * Check if given string could be parsed as positive long. 107 | * 108 | * @param str string to check 109 | * @return true if given string could be parsed as positive long, false otherwise 110 | */ 111 | public static boolean isPossiblyLong(String str) { 112 | try { 113 | final long value = Long.parseLong(str); 114 | return value > 0; 115 | } catch (NumberFormatException e) { 116 | return false; 117 | } 118 | } 119 | 120 | public static List idList(String str) { 121 | return Arrays.stream(str.split(",")).map(Long::parseLong).collect(Collectors.toList()); 122 | } 123 | 124 | public static List getPayload(@Nonnull Response response, @Nonnull Function> mapper) { 125 | final T body = response.body(); 126 | if (body == null) { 127 | return Collections.emptyList(); 128 | } 129 | return Optional.ofNullable(mapper.apply(body)).orElse(Collections.emptyList()); 130 | } 131 | 132 | public static E assertValidResponse(Response response, Function mapper) { 133 | Preconditions.checkState(response.isSuccessful(), "Invalid API response : %s", 134 | response.code()); 135 | return mapper.apply(response.body()); 136 | } 137 | 138 | public static void assertValidResponse(Response response) { 139 | assertValidResponse(response, (Function) t -> null); 140 | } 141 | 142 | public static BasicSSHUserPrivateKey assertSshKey(String credentialsId) { 143 | final BasicSSHUserPrivateKey privateKey = CredentialsMatchers.firstOrNull( 144 | CredentialsProvider.lookupCredentials(BasicSSHUserPrivateKey.class, Jenkins.get(), ACL.SYSTEM, 145 | Collections.emptyList()), 146 | CredentialsMatchers.withId(credentialsId)); 147 | 148 | Preconditions.checkState(privateKey != null, 149 | "No SSH credentials found with ID '%s'", credentialsId); 150 | 151 | return privateKey; 152 | } 153 | 154 | public static String getStringOrDefault(String value, String defValue) { 155 | if(Strings.isNullOrEmpty(value)) { 156 | return defValue; 157 | } 158 | return value; 159 | } 160 | 161 | @RequiredArgsConstructor 162 | public static class LogAdapter { 163 | private static final SimpleFormatter FORMATTER = new SimpleFormatter(); 164 | @Getter 165 | private final PrintStream stream; 166 | private final Logger logger; 167 | 168 | public void info(String message) { 169 | logger.info(message); 170 | final LogRecord rec = new LogRecord(Level.INFO, message); 171 | rec.setLoggerName(logger.getName()); 172 | stream.println(FORMATTER.format(rec)); 173 | } 174 | 175 | public void error(String message, Throwable cause) { 176 | logger.error(message, cause); 177 | final LogRecord rec = new LogRecord(Level.SEVERE, message + " Cause: " + cause); 178 | rec.setLoggerName(logger.getName()); 179 | rec.setThrown(cause); 180 | stream.println(FORMATTER.format(rec)); 181 | } 182 | } 183 | 184 | /** 185 | * Check if idle server can be shut down. 186 | *

187 | * According to Hetzner billing policy, 188 | * you are billed for every hour of existence of server, so it makes sense to keep server running as long as next hour did 189 | * not start yet. 190 | * 191 | * @param createdStr RFC3339-formatted instant when server was created. See ServerDetail#getCreated(). 192 | * @param currentTime current time. Kept as argument to allow unit-testing. 193 | * @return true if server should be shut down, false otherwise. 194 | * Note: we keep small time buffer for corner cases like clock skew or Jenkins's queue manager overload, which could 195 | * lead to unnecessary 1-hour over-billing. 196 | */ 197 | public static boolean canShutdownServer(@Nonnull String createdStr, LocalDateTime currentTime) { 198 | final LocalDateTime created = LocalDateTime.from(DateTimeFormatter.ISO_DATE_TIME.parse(createdStr)) 199 | .atOffset(ZoneOffset.UTC).toLocalDateTime(); 200 | long diff = Duration.between(created, currentTime.atOffset(ZoneOffset.UTC).toLocalDateTime()).toMinutes() % 60; 201 | return (60 - SHUTDOWN_TIME_BUFFER) <= diff; 202 | } 203 | 204 | /** 205 | * Get all nodes that are {@link HetznerServerAgent}. 206 | * 207 | * @return list of all {@link HetznerServerAgent} nodes 208 | */ 209 | public static List getHetznerAgents() { 210 | return Jenkins.get().getNodes() 211 | .stream() 212 | .filter(HetznerServerAgent.class::isInstance) 213 | .map(HetznerServerAgent.class::cast) 214 | .collect(Collectors.toList()); 215 | } 216 | 217 | public static boolean isValidLabelValue(String value) { 218 | if (Strings.isNullOrEmpty(value)) { 219 | return false; 220 | } 221 | return LABEL_VALUE_RE.matcher(value).matches(); 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /src/main/java/cloud/dnation/jenkins/plugins/hetzner/HetznerCloud.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 https://dnation.cloud 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cloud.dnation.jenkins.plugins.hetzner; 17 | 18 | import com.cloudbees.plugins.credentials.CredentialsMatchers; 19 | import com.cloudbees.plugins.credentials.CredentialsProvider; 20 | import com.cloudbees.plugins.credentials.common.StandardListBoxModel; 21 | import com.google.common.primitives.Ints; 22 | import hudson.Extension; 23 | import hudson.model.Computer; 24 | import hudson.model.Descriptor; 25 | import hudson.model.Item; 26 | import hudson.model.Label; 27 | import hudson.model.Node; 28 | import hudson.security.ACL; 29 | import hudson.slaves.AbstractCloudImpl; 30 | import hudson.slaves.Cloud; 31 | import hudson.slaves.NodeProvisioner.PlannedNode; 32 | import hudson.util.FormValidation; 33 | import hudson.util.ListBoxModel; 34 | import jenkins.model.Jenkins; 35 | import lombok.Getter; 36 | import lombok.NonNull; 37 | import lombok.SneakyThrows; 38 | import lombok.extern.slf4j.Slf4j; 39 | import org.apache.commons.lang.RandomStringUtils; 40 | import org.jenkinsci.plugins.cloudstats.ProvisioningActivity; 41 | import org.jenkinsci.plugins.cloudstats.TrackedPlannedNode; 42 | import org.jenkinsci.plugins.plaincredentials.impl.StringCredentialsImpl; 43 | import org.kohsuke.accmod.Restricted; 44 | import org.kohsuke.accmod.restrictions.NoExternalUse; 45 | import org.kohsuke.stapler.AncestorInPath; 46 | import org.kohsuke.stapler.DataBoundConstructor; 47 | import org.kohsuke.stapler.DataBoundSetter; 48 | import org.kohsuke.stapler.QueryParameter; 49 | import org.kohsuke.stapler.interceptor.RequirePOST; 50 | 51 | import java.io.IOException; 52 | import java.util.ArrayList; 53 | import java.util.Collection; 54 | import java.util.Collections; 55 | import java.util.List; 56 | import java.util.Locale; 57 | import java.util.stream.Collectors; 58 | 59 | @Slf4j 60 | public class HetznerCloud extends AbstractCloudImpl { 61 | @Getter 62 | private final String credentialsId; 63 | @Getter 64 | private List serverTemplates; 65 | @Getter 66 | private transient HetznerCloudResourceManager resourceManager; 67 | 68 | @DataBoundConstructor 69 | public HetznerCloud(String name, String credentialsId, String instanceCapStr, 70 | List serverTemplates) { 71 | super(name, instanceCapStr); 72 | this.credentialsId = credentialsId; 73 | this.serverTemplates = serverTemplates; 74 | readResolve(); 75 | } 76 | 77 | /** 78 | * Pick random template from provided list. 79 | * 80 | * @param matchingTemplates List of all matching templates. 81 | * @return picked template 82 | */ 83 | private static HetznerServerTemplate pickTemplate(List matchingTemplates) { 84 | if (matchingTemplates.size() == 1) { 85 | return matchingTemplates.get(0); 86 | } 87 | final List shuffled = new ArrayList<>(matchingTemplates); 88 | Collections.shuffle(shuffled); 89 | return shuffled.get(0); 90 | } 91 | 92 | @DataBoundSetter 93 | public void setServerTemplates(List serverTemplates) { 94 | if (serverTemplates == null) { 95 | this.serverTemplates = Collections.emptyList(); 96 | } else { 97 | this.serverTemplates = serverTemplates; 98 | } 99 | readResolve(); 100 | } 101 | 102 | protected Object readResolve() { 103 | resourceManager = HetznerCloudResourceManager.create(credentialsId); 104 | if (serverTemplates == null) { 105 | setServerTemplates(Collections.emptyList()); 106 | } 107 | for (HetznerServerTemplate template : serverTemplates) { 108 | template.setCloud(this); 109 | template.readResolve(); 110 | } 111 | return this; 112 | } 113 | 114 | @SneakyThrows 115 | private int runningNodeCount() { 116 | return Ints.checkedCast(resourceManager.fetchAllServers(name) 117 | .stream() 118 | .filter(sd -> HetznerConstants.RUNNABLE_STATE_SET.contains(sd.getStatus())) 119 | .count()); 120 | } 121 | 122 | @Override 123 | public Collection provision(CloudState state, int excessWorkload) { 124 | log.debug("provision(cloud={},label={},excessWorkload={})", name, state.getLabel(), excessWorkload); 125 | final List plannedNodes = new ArrayList<>(); 126 | final Label label = state.getLabel(); 127 | final List matchingTemplates = getTemplates(label); 128 | final Jenkins jenkinsInstance = Jenkins.get(); 129 | try { 130 | while (excessWorkload > 0) { 131 | if (jenkinsInstance.isQuietingDown() || jenkinsInstance.isTerminating()) { 132 | log.warn("Jenkins is going down, no new nodes will be provisioned"); 133 | break; 134 | } 135 | int running = runningNodeCount(); 136 | int instanceCap = getInstanceCap(); 137 | int available = instanceCap - running; 138 | final HetznerServerTemplate template = pickTemplate(matchingTemplates); 139 | log.info("Creating new agent with {} executors, have {} running VMs", template.getNumExecutors(), running); 140 | if (available <= 0) { 141 | log.warn("Cloud capacity reached ({}). Has {} VMs running, but want {} more executors", 142 | instanceCap, running , excessWorkload); 143 | break; 144 | } else { 145 | final String serverName = "hcloud-" + RandomStringUtils.randomAlphanumeric(16) 146 | .toLowerCase(Locale.ROOT); 147 | final ProvisioningActivity.Id provisioningId = new ProvisioningActivity.Id(name, template.getName(), 148 | serverName); 149 | final HetznerServerAgent agent = template.createAgent(provisioningId, serverName); 150 | agent.setMode(template.getMode()); 151 | plannedNodes.add(new TrackedPlannedNode( 152 | provisioningId, 153 | agent.getNumExecutors(), 154 | Computer.threadPoolForRemoting.submit(new NodeCallable(agent, this) 155 | ) 156 | ) 157 | ); 158 | excessWorkload -= agent.getNumExecutors(); 159 | } 160 | } 161 | 162 | } catch (IOException | Descriptor.FormException e) { 163 | log.error("Unable to provision node", e); 164 | } 165 | return plannedNodes; 166 | } 167 | 168 | @Override 169 | public boolean canProvision(CloudState state) { 170 | return !getTemplates(state.getLabel()).isEmpty(); 171 | } 172 | 173 | private List getTemplates(Label label) { 174 | return serverTemplates.stream().filter(t -> { 175 | //no labels has been provided in template 176 | if (t.getLabels().isEmpty()) { 177 | return Node.Mode.NORMAL.equals(t.getMode()); 178 | } else { 179 | if (Node.Mode.NORMAL.equals(t.getMode())) { 180 | return label == null || label.matches(t.getLabels()); 181 | } else { 182 | return label != null && label.matches(t.getLabels()); 183 | } 184 | } 185 | }) 186 | .collect(Collectors.toList()); 187 | } 188 | 189 | @SuppressWarnings("unused") 190 | @Extension 191 | public static class DescriptorImpl extends Descriptor { 192 | @Override 193 | @NonNull 194 | public String getDisplayName() { 195 | return Messages.plugin_displayName(); 196 | } 197 | 198 | @Restricted(NoExternalUse.class) 199 | @RequirePOST 200 | public FormValidation doVerifyConfiguration(@QueryParameter String credentialsId) { 201 | Jenkins.get().checkPermission(Jenkins.ADMINISTER); 202 | final ConfigurationValidator.ValidationResult result = ConfigurationValidator.validateCloudConfig(credentialsId); 203 | if (result.isSuccess()) { 204 | return FormValidation.ok(Messages.cloudConfigPassed()); 205 | } else { 206 | return FormValidation.error(result.getMessage()); 207 | } 208 | } 209 | 210 | @Restricted(NoExternalUse.class) 211 | @RequirePOST 212 | public FormValidation doCheckCloudName(@QueryParameter String name) { 213 | if (Helper.isValidLabelValue(name)) { 214 | return FormValidation.ok(); 215 | } 216 | return FormValidation.error("Cloud name is not a valid label value: %s", name); 217 | } 218 | 219 | @Restricted(NoExternalUse.class) 220 | @RequirePOST 221 | public ListBoxModel doFillCredentialsIdItems(@AncestorInPath Item owner) { 222 | final StandardListBoxModel result = new StandardListBoxModel(); 223 | if (owner == null) { 224 | if (!Jenkins.get().hasPermission(Jenkins.ADMINISTER)) { 225 | return result; 226 | } 227 | } else { 228 | if (!owner.hasPermission(owner.EXTENDED_READ) 229 | && !owner.hasPermission(CredentialsProvider.USE_ITEM)) { 230 | return result; 231 | } 232 | } 233 | return new StandardListBoxModel() 234 | .includeEmptyValue() 235 | .includeMatchingAs(ACL.SYSTEM, owner, StringCredentialsImpl.class, 236 | Collections.emptyList(), CredentialsMatchers.always()); 237 | } 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /src/main/java/cloud/dnation/jenkins/plugins/hetzner/HetznerConstants.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 https://dnation.cloud 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cloud.dnation.jenkins.plugins.hetzner; 17 | 18 | import cloud.dnation.jenkins.plugins.hetzner.connect.AbstractConnectivity; 19 | import cloud.dnation.jenkins.plugins.hetzner.connect.Both; 20 | import cloud.dnation.jenkins.plugins.hetzner.launcher.AbstractConnectionMethod; 21 | import cloud.dnation.jenkins.plugins.hetzner.launcher.DefaultConnectionMethod; 22 | import cloud.dnation.jenkins.plugins.hetzner.primaryip.AbstractPrimaryIpStrategy; 23 | import cloud.dnation.jenkins.plugins.hetzner.primaryip.DefaultStrategy; 24 | import cloud.dnation.jenkins.plugins.hetzner.shutdown.IdlePeriodPolicy; 25 | import com.google.common.collect.ImmutableSet; 26 | import lombok.experimental.UtilityClass; 27 | 28 | import java.util.Set; 29 | 30 | @UtilityClass 31 | public class HetznerConstants { 32 | /** 33 | * Namespace for labels added to all objects managed by this plugin. 34 | */ 35 | public static final String LABEL_NS = "jenkins.io/"; 36 | 37 | /** 38 | * Name of label for credentials-id to apply to SSH key. 39 | */ 40 | public static final String LABEL_CREDENTIALS_ID = LABEL_NS + "credentials-id"; 41 | 42 | /** 43 | * Name of label for cloud instance associated with server. 44 | */ 45 | public static final String LABEL_CLOUD_NAME = LABEL_NS + "cloud-name"; 46 | 47 | /** 48 | * Name of label for all objects managed by this plugin. 49 | */ 50 | public static final String LABEL_MANAGED_BY = LABEL_NS + "managed-by"; 51 | 52 | /** 53 | * Internal identifier used to label cloud resources that this plugin manages. 54 | */ 55 | public static final String LABEL_VALUE_PLUGIN = "hetzner-jenkins-plugin"; 56 | 57 | /** 58 | * Default remote working directory. 59 | */ 60 | public static final String DEFAULT_REMOTE_FS = "/home/jenkins"; 61 | 62 | public static final int DEFAULT_NUM_EXECUTORS = 1; 63 | 64 | public static final int DEFAULT_BOOT_DEADLINE = 1; 65 | 66 | /** 67 | * Set of server states that are considered as runnable. 68 | */ 69 | public static final Set RUNNABLE_STATE_SET = ImmutableSet.builder() 70 | .add("running") 71 | .add("initializing") 72 | .add("starting") 73 | .build(); 74 | 75 | /** 76 | * Default shutdown policy to use. 77 | */ 78 | static final IdlePeriodPolicy DEFAULT_SHUTDOWN_POLICY = new IdlePeriodPolicy(10); 79 | 80 | /* 81 | * Arbitrary value in minutes which gives us some time to shut down server before usage hour wraps. 82 | */ 83 | public static final int SHUTDOWN_TIME_BUFFER = 5; 84 | 85 | /** 86 | * Default strategy to get primary IP. 87 | */ 88 | public static final AbstractPrimaryIpStrategy DEFAULT_PRIMARY_IP_STRATEGY = DefaultStrategy.SINGLETON; 89 | 90 | public static final AbstractConnectionMethod DEFAULT_CONNECTION_METHOD = DefaultConnectionMethod.SINGLETON; 91 | 92 | /** 93 | * Default networking setup. 94 | */ 95 | public static final AbstractConnectivity DEFAULT_CONNECTIVITY = new Both(); 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/cloud/dnation/jenkins/plugins/hetzner/HetznerServerAgent.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 https://dnation.cloud 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cloud.dnation.jenkins.plugins.hetzner; 17 | 18 | import cloud.dnation.jenkins.plugins.hetzner.launcher.HetznerServerComputerLauncher; 19 | import edu.umd.cs.findbugs.annotations.NonNull; 20 | import hudson.Extension; 21 | import hudson.model.Descriptor; 22 | import hudson.model.Node; 23 | import hudson.model.TaskListener; 24 | import hudson.slaves.AbstractCloudComputer; 25 | import hudson.slaves.AbstractCloudSlave; 26 | import hudson.slaves.ComputerLauncher; 27 | import hudson.slaves.EphemeralNode; 28 | import lombok.AccessLevel; 29 | import lombok.Getter; 30 | import lombok.Setter; 31 | import lombok.extern.slf4j.Slf4j; 32 | import org.jenkinsci.plugins.cloudstats.CloudStatistics; 33 | import org.jenkinsci.plugins.cloudstats.ProvisioningActivity; 34 | import org.jenkinsci.plugins.cloudstats.TrackedItem; 35 | 36 | import javax.annotation.Nonnull; 37 | import java.io.IOException; 38 | import java.util.Objects; 39 | import java.util.Optional; 40 | 41 | @Slf4j 42 | public class HetznerServerAgent extends AbstractCloudSlave implements EphemeralNode, TrackedItem { 43 | private static final long serialVersionUID = 1; 44 | private final ProvisioningActivity.Id provisioningId; 45 | @Getter 46 | private final transient HetznerCloud cloud; 47 | @Getter 48 | @NonNull 49 | private final transient HetznerServerTemplate template; 50 | @Getter(AccessLevel.PUBLIC) 51 | @Setter(AccessLevel.PACKAGE) 52 | private transient HetznerServerInfo serverInstance; 53 | 54 | public HetznerServerAgent(@NonNull ProvisioningActivity.Id provisioningId, 55 | @NonNull String name, String remoteFS, ComputerLauncher launcher, 56 | @NonNull HetznerCloud cloud, @NonNull HetznerServerTemplate template) 57 | throws IOException, Descriptor.FormException { 58 | super(name, remoteFS, launcher); 59 | this.cloud = Objects.requireNonNull(cloud); 60 | this.template = Objects.requireNonNull(template); 61 | this.provisioningId = Objects.requireNonNull(provisioningId); 62 | setLabelString(template.getLabelStr()); 63 | setNumExecutors(template.getNumExecutors()); 64 | setMode(template.getMode() == null ? Mode.EXCLUSIVE : template.getMode()); 65 | setRetentionStrategy(template.getShutdownPolicy().getRetentionStrategy()); 66 | readResolve(); 67 | } 68 | 69 | @SuppressWarnings("rawtypes") 70 | @Override 71 | public AbstractCloudComputer createComputer() { 72 | return new HetznerServerComputer(this); 73 | } 74 | 75 | @Override 76 | public String getDisplayName() { 77 | if (serverInstance != null && serverInstance.getServerDetail() != null) { 78 | return getNodeName() + " in " + serverInstance.getServerDetail().getDatacenter() 79 | .getLocation().getDescription(); 80 | } 81 | return super.getDisplayName(); 82 | } 83 | 84 | @Override 85 | protected void _terminate(TaskListener listener) { 86 | ((HetznerServerComputerLauncher) getLauncher()).signalTermination(); 87 | cloud.getResourceManager().destroyServer(serverInstance.getServerDetail()); 88 | Optional.ofNullable(CloudStatistics.get().getActivityFor(this)) 89 | .ifPresent(a -> a.enterIfNotAlready(ProvisioningActivity.Phase.COMPLETED)); 90 | } 91 | 92 | @Override 93 | public Node asNode() { 94 | return this; 95 | } 96 | 97 | @Nonnull 98 | @Override 99 | public ProvisioningActivity.Id getId() { 100 | return provisioningId; 101 | } 102 | 103 | /** 104 | * Check if server associated with this agent is running. 105 | * 106 | * @return true if status of server is "running", false otherwise 107 | */ 108 | public boolean isAlive() { 109 | serverInstance = cloud.getResourceManager().refreshServerInfo(serverInstance); 110 | return serverInstance.getServerDetail().getStatus().equals("running"); 111 | } 112 | 113 | @SuppressWarnings("unused") 114 | @Extension 115 | public static final class DescriptorImpl extends SlaveDescriptor { 116 | @NonNull 117 | @Override 118 | public String getDisplayName() { 119 | return Messages.plugin_displayName(); 120 | } 121 | 122 | public boolean isInstantiable() { 123 | return false; 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/main/java/cloud/dnation/jenkins/plugins/hetzner/HetznerServerComputer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 https://dnation.cloud 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cloud.dnation.jenkins.plugins.hetzner; 17 | 18 | import hudson.slaves.AbstractCloudComputer; 19 | import org.jenkinsci.plugins.cloudstats.ProvisioningActivity; 20 | import org.jenkinsci.plugins.cloudstats.TrackedItem; 21 | 22 | import javax.annotation.Nonnull; 23 | 24 | public class HetznerServerComputer extends AbstractCloudComputer implements TrackedItem { 25 | private final ProvisioningActivity.Id provisioningId; 26 | 27 | public HetznerServerComputer(HetznerServerAgent agent) { 28 | super(agent); 29 | this.provisioningId = agent.getId(); 30 | } 31 | 32 | @Nonnull 33 | @Override 34 | public ProvisioningActivity.Id getId() { 35 | return provisioningId; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/cloud/dnation/jenkins/plugins/hetzner/HetznerServerInfo.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 https://dnation.cloud 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cloud.dnation.jenkins.plugins.hetzner; 17 | 18 | import cloud.dnation.hetznerclient.ServerDetail; 19 | import cloud.dnation.hetznerclient.SshKeyDetail; 20 | import lombok.Data; 21 | 22 | @Data 23 | public class HetznerServerInfo { 24 | private final SshKeyDetail sshKeyDetail; 25 | private ServerDetail serverDetail; 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/cloud/dnation/jenkins/plugins/hetzner/HetznerServerTemplate.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 https://dnation.cloud 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cloud.dnation.jenkins.plugins.hetzner; 17 | 18 | import cloud.dnation.jenkins.plugins.hetzner.connect.AbstractConnectivity; 19 | import cloud.dnation.jenkins.plugins.hetzner.launcher.AbstractHetznerSshConnector; 20 | import cloud.dnation.jenkins.plugins.hetzner.primaryip.AbstractPrimaryIpStrategy; 21 | import cloud.dnation.jenkins.plugins.hetzner.shutdown.AbstractShutdownPolicy; 22 | import com.google.common.base.Strings; 23 | import hudson.Extension; 24 | import hudson.Util; 25 | import hudson.model.AbstractDescribableImpl; 26 | import hudson.model.Descriptor; 27 | import hudson.model.Label; 28 | import hudson.model.Node.Mode; 29 | import hudson.model.labels.LabelAtom; 30 | import hudson.util.FormValidation; 31 | import jenkins.model.Jenkins; 32 | import lombok.AccessLevel; 33 | import lombok.Getter; 34 | import lombok.NonNull; 35 | import lombok.Setter; 36 | import lombok.ToString; 37 | import lombok.extern.slf4j.Slf4j; 38 | import org.jenkinsci.plugins.cloudstats.ProvisioningActivity; 39 | import org.kohsuke.accmod.Restricted; 40 | import org.kohsuke.accmod.restrictions.NoExternalUse; 41 | import org.kohsuke.stapler.DataBoundConstructor; 42 | import org.kohsuke.stapler.DataBoundSetter; 43 | import org.kohsuke.stapler.QueryParameter; 44 | import org.kohsuke.stapler.interceptor.RequirePOST; 45 | 46 | import java.io.IOException; 47 | import java.util.Set; 48 | 49 | import static cloud.dnation.jenkins.plugins.hetzner.ConfigurationValidator.doCheckNonEmpty; 50 | import static cloud.dnation.jenkins.plugins.hetzner.ConfigurationValidator.doCheckPositiveInt; 51 | import static cloud.dnation.jenkins.plugins.hetzner.ConfigurationValidator.verifyImage; 52 | import static cloud.dnation.jenkins.plugins.hetzner.ConfigurationValidator.verifyLocation; 53 | import static cloud.dnation.jenkins.plugins.hetzner.ConfigurationValidator.verifyNetwork; 54 | import static cloud.dnation.jenkins.plugins.hetzner.ConfigurationValidator.verifyPlacementGroup; 55 | import static cloud.dnation.jenkins.plugins.hetzner.ConfigurationValidator.verifyServerType; 56 | import static cloud.dnation.jenkins.plugins.hetzner.ConfigurationValidator.verifyVolumes; 57 | import static cloud.dnation.jenkins.plugins.hetzner.Helper.getStringOrDefault; 58 | import static cloud.dnation.jenkins.plugins.hetzner.HetznerConstants.DEFAULT_REMOTE_FS; 59 | 60 | @ToString 61 | @Slf4j 62 | public class HetznerServerTemplate extends AbstractDescribableImpl { 63 | @Getter 64 | private final String name; 65 | 66 | @Getter 67 | private final String labelStr; 68 | 69 | @Getter 70 | private final String image; 71 | 72 | @Getter 73 | private final String location; 74 | 75 | @Getter 76 | private final String serverType; 77 | 78 | @Getter 79 | private transient Set labels; 80 | 81 | @Setter(AccessLevel.PACKAGE) 82 | @Getter(AccessLevel.PACKAGE) 83 | @NonNull 84 | private transient HetznerCloud cloud; 85 | 86 | @Setter(onMethod = @__({@DataBoundSetter})) 87 | @Getter 88 | private AbstractHetznerSshConnector connector; 89 | 90 | @Setter(onMethod = @__({@DataBoundSetter})) 91 | @Getter 92 | private String remoteFs; 93 | 94 | @Setter(onMethod = @__({@DataBoundSetter})) 95 | @Getter 96 | private String placementGroup; 97 | 98 | @Setter(onMethod = @__({@DataBoundSetter})) 99 | @Getter 100 | private String userData; 101 | 102 | @Setter(onMethod = @__({@DataBoundSetter})) 103 | @Getter 104 | private String jvmOpts; 105 | 106 | @Getter 107 | @Setter(onMethod = @__({@DataBoundSetter})) 108 | private int numExecutors; 109 | 110 | @Getter 111 | @Setter(onMethod = @__({@DataBoundSetter})) 112 | private int bootDeadline; 113 | 114 | @Getter 115 | @Setter(onMethod = @__({@DataBoundSetter})) 116 | private String network; 117 | 118 | @Getter 119 | @Setter(onMethod = @__({@DataBoundSetter})) 120 | private Mode mode = Mode.EXCLUSIVE; 121 | 122 | @Getter 123 | @Setter(onMethod = @__({@DataBoundSetter})) 124 | private AbstractShutdownPolicy shutdownPolicy; 125 | 126 | @Getter 127 | @Setter(onMethod = @__({@DataBoundSetter})) 128 | private AbstractPrimaryIpStrategy primaryIp; 129 | 130 | @Getter 131 | @Setter(onMethod = @__({@DataBoundSetter})) 132 | private AbstractConnectivity connectivity; 133 | 134 | @Getter 135 | @Setter(onMethod = @__({@DataBoundSetter})) 136 | private boolean automountVolumes; 137 | 138 | @Getter 139 | @Setter(onMethod = @__({@DataBoundSetter})) 140 | private String volumeIds; 141 | 142 | @DataBoundConstructor 143 | public HetznerServerTemplate(String name, String labelStr, String image, 144 | String location, String serverType) { 145 | this.name = name; 146 | this.labelStr = Util.fixNull(labelStr); 147 | this.image = image; 148 | this.location = location; 149 | this.serverType = serverType; 150 | readResolve(); 151 | } 152 | 153 | protected Object readResolve() { 154 | Jenkins.get().checkPermission(Jenkins.ADMINISTER); 155 | labels = Label.parse(labelStr); 156 | if (Strings.isNullOrEmpty(location)) { 157 | throw new IllegalArgumentException("Location must be specified"); 158 | } 159 | if (numExecutors == 0) { 160 | setNumExecutors(HetznerConstants.DEFAULT_NUM_EXECUTORS); 161 | } 162 | if (bootDeadline == 0) { 163 | setBootDeadline(HetznerConstants.DEFAULT_BOOT_DEADLINE); 164 | } 165 | if (shutdownPolicy == null) { 166 | shutdownPolicy = HetznerConstants.DEFAULT_SHUTDOWN_POLICY; 167 | } 168 | if (primaryIp == null) { 169 | primaryIp = HetznerConstants.DEFAULT_PRIMARY_IP_STRATEGY; 170 | } 171 | if (connectivity == null ) { 172 | connectivity = HetznerConstants.DEFAULT_CONNECTIVITY; 173 | } 174 | if (placementGroup == null) { 175 | placementGroup = ""; 176 | } 177 | if (userData == null) { 178 | userData = ""; 179 | } 180 | if (volumeIds == null) { 181 | volumeIds = ""; 182 | } 183 | return this; 184 | } 185 | 186 | /** 187 | * Create new {@link HetznerServerAgent}. 188 | * 189 | * @param provisioningId ID to track activity of provisioning 190 | * @param nodeName name of server 191 | * @return new agent instance 192 | */ 193 | HetznerServerAgent createAgent(ProvisioningActivity.Id provisioningId, String nodeName) 194 | throws IOException, Descriptor.FormException { 195 | return new HetznerServerAgent( 196 | provisioningId, 197 | nodeName, 198 | getStringOrDefault(remoteFs, DEFAULT_REMOTE_FS), 199 | connector.createLauncher(), 200 | cloud, 201 | this 202 | ); 203 | } 204 | 205 | @SuppressWarnings("unused") 206 | @Extension 207 | public static final class DescriptorImpl extends Descriptor { 208 | @Override 209 | @NonNull 210 | public String getDisplayName() { 211 | return Messages.serverTemplate_displayName(); 212 | } 213 | 214 | @Restricted(NoExternalUse.class) 215 | @RequirePOST 216 | public FormValidation doVerifyLocation(@QueryParameter String location, 217 | @QueryParameter String credentialsId) { 218 | return verifyLocation(location, credentialsId).toFormValidation(); 219 | } 220 | 221 | @Restricted(NoExternalUse.class) 222 | @RequirePOST 223 | public FormValidation doVerifyImage(@QueryParameter String image, 224 | @QueryParameter String credentialsId) { 225 | return verifyImage(image, credentialsId).toFormValidation(); 226 | } 227 | 228 | @Restricted(NoExternalUse.class) 229 | @RequirePOST 230 | public FormValidation doVerifyNetwork(@QueryParameter String network, 231 | @QueryParameter String credentialsId) { 232 | return verifyNetwork(network, credentialsId).toFormValidation(); 233 | } 234 | 235 | @Restricted(NoExternalUse.class) 236 | @RequirePOST 237 | public FormValidation doVerifyPlacementGroup(@QueryParameter String placementGroup, 238 | @QueryParameter String credentialsId) { 239 | return verifyPlacementGroup(placementGroup, credentialsId).toFormValidation(); 240 | } 241 | 242 | @Restricted(NoExternalUse.class) 243 | @RequirePOST 244 | public FormValidation doVerifyServerType(@QueryParameter String serverType, 245 | @QueryParameter String credentialsId) { 246 | return verifyServerType(serverType, credentialsId).toFormValidation(); 247 | } 248 | 249 | @Restricted(NoExternalUse.class) 250 | @RequirePOST 251 | public FormValidation doVerifyVolumes(@QueryParameter String volumeIds, 252 | @QueryParameter String credentialsId) { 253 | return verifyVolumes(volumeIds, credentialsId).toFormValidation(); 254 | } 255 | 256 | @Restricted(NoExternalUse.class) 257 | @RequirePOST 258 | public FormValidation doCheckImage(@QueryParameter String image) { 259 | return doCheckNonEmpty(image, "Image"); 260 | } 261 | 262 | @Restricted(NoExternalUse.class) 263 | @RequirePOST 264 | public FormValidation doCheckLabelStr(@QueryParameter String labelStr, @QueryParameter Mode mode) { 265 | if (Strings.isNullOrEmpty(labelStr) && Mode.EXCLUSIVE == mode) { 266 | return FormValidation.warning("You may want to assign labels to this node;" 267 | + " it's marked to only run jobs that are exclusively tied to itself or a label."); 268 | } 269 | return FormValidation.ok(); 270 | } 271 | 272 | @Restricted(NoExternalUse.class) 273 | @RequirePOST 274 | public FormValidation doCheckServerType(@QueryParameter String serverType) { 275 | return doCheckNonEmpty(serverType, "Server type"); 276 | } 277 | 278 | @Restricted(NoExternalUse.class) 279 | @RequirePOST 280 | public FormValidation doCheckLocation(@QueryParameter String location) { 281 | return doCheckNonEmpty(location, "Location"); 282 | } 283 | 284 | @Restricted(NoExternalUse.class) 285 | @RequirePOST 286 | public FormValidation doCheckName(@QueryParameter String name) { 287 | return doCheckNonEmpty(name, "Name"); 288 | } 289 | 290 | @Restricted(NoExternalUse.class) 291 | @RequirePOST 292 | public FormValidation doCheckNumExecutors(@QueryParameter String numExecutors) { 293 | return doCheckPositiveInt(numExecutors, "Number of executors"); 294 | } 295 | 296 | @Restricted(NoExternalUse.class) 297 | @RequirePOST 298 | public FormValidation doCheckBootDeadline(@QueryParameter String bootDeadline) { 299 | return doCheckPositiveInt(bootDeadline, "Boot deadline"); 300 | } 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /src/main/java/cloud/dnation/jenkins/plugins/hetzner/JenkinsSecretTokenProvider.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 https://dnation.cloud 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cloud.dnation.jenkins.plugins.hetzner; 17 | 18 | import com.cloudbees.plugins.credentials.CredentialsMatchers; 19 | import com.cloudbees.plugins.credentials.CredentialsProvider; 20 | import jenkins.model.Jenkins; 21 | import org.jenkinsci.plugins.plaincredentials.StringCredentials; 22 | 23 | import java.util.function.Supplier; 24 | 25 | public class JenkinsSecretTokenProvider implements Supplier { 26 | private final String credentialsId; 27 | 28 | private JenkinsSecretTokenProvider(String credentialsId) { 29 | this.credentialsId = credentialsId; 30 | } 31 | 32 | public static JenkinsSecretTokenProvider forCredentialsId(String credentialsId) { 33 | return new JenkinsSecretTokenProvider(credentialsId); 34 | } 35 | 36 | @Override 37 | public String get() { 38 | final StringCredentials secret = CredentialsMatchers.firstOrNull( 39 | CredentialsProvider.lookupCredentials(StringCredentials.class, Jenkins.get()), 40 | CredentialsMatchers.withId(credentialsId)); 41 | if (secret == null) { 42 | throw new IllegalStateException("Can't find credentials with ID '" + credentialsId + "'"); 43 | } 44 | return secret.getSecret().getPlainText(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/cloud/dnation/jenkins/plugins/hetzner/NodeCallable.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 https://dnation.cloud 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cloud.dnation.jenkins.plugins.hetzner; 17 | 18 | import com.google.common.base.Preconditions; 19 | import com.google.common.util.concurrent.Uninterruptibles; 20 | import hudson.model.Computer; 21 | import hudson.model.Node; 22 | import jenkins.model.Jenkins; 23 | import lombok.RequiredArgsConstructor; 24 | import lombok.extern.slf4j.Slf4j; 25 | 26 | import java.util.concurrent.Callable; 27 | import java.util.concurrent.ExecutionException; 28 | import java.util.concurrent.TimeUnit; 29 | 30 | @Slf4j 31 | @RequiredArgsConstructor 32 | class NodeCallable implements Callable { 33 | private final HetznerServerAgent agent; 34 | private final HetznerCloud cloud; 35 | 36 | @Override 37 | public Node call() throws Exception { 38 | Computer computer = agent.getComputer(); 39 | if (computer != null && computer.isOnline()) { 40 | return agent; 41 | } 42 | final HetznerServerInfo serverInfo = cloud.getResourceManager().createServer(agent); 43 | agent.setServerInstance(serverInfo); 44 | final String serverName = serverInfo.getServerDetail().getName(); 45 | boolean running = false; 46 | final int bootDeadline = agent.getTemplate().getBootDeadline(); 47 | //wait for status == "running", but at most 15 minutes 48 | final WaitStrategy waitStrategy = new WaitStrategy(bootDeadline, 45, 15); 49 | while (!waitStrategy.isDeadLineOver()) { 50 | waitStrategy.waitNext(); 51 | if (agent.isAlive()) { 52 | log.info("Server '{}' is now running, waiting 10 seconds before proceeding", serverName); 53 | Uninterruptibles.sleepUninterruptibly(10, TimeUnit.SECONDS); 54 | running = true; 55 | break; 56 | } 57 | } 58 | Preconditions.checkState(running, "Server '%s' didn't start after 15 minutes, giving up", 59 | serverName); 60 | Jenkins.get().addNode(agent); 61 | computer = agent.toComputer(); 62 | int retry = 5; 63 | boolean connected = false; 64 | if (computer != null) { 65 | while (--retry > 0) { 66 | try { 67 | computer.connect(false).get(); 68 | connected = true; 69 | break; 70 | } catch (InterruptedException | ExecutionException e) { 71 | log.warn("Connection to '{}' has failed, remaining retries {}", computer.getDisplayName(), 72 | retry, e); 73 | TimeUnit.SECONDS.sleep(10); 74 | } 75 | } 76 | if (!connected) { 77 | throw new IllegalStateException("Computer is not connected : " + computer.getName()); 78 | } 79 | } else { 80 | throw new IllegalStateException("No computer object in agent " + agent.getDisplayName()); 81 | } 82 | 83 | return agent; 84 | } 85 | 86 | private static final class WaitStrategy { 87 | private final int firstInterval; 88 | private final int subsequentIntervals; 89 | private final long deadlineNanos; 90 | private boolean first = true; 91 | 92 | private WaitStrategy(int deadlineMinutes, int firstInterval, int subsequentIntervals) { 93 | deadlineNanos = System.nanoTime() + deadlineMinutes * 60L * 1_000_000_000L; 94 | this.firstInterval = firstInterval; 95 | this.subsequentIntervals = subsequentIntervals; 96 | } 97 | 98 | boolean isDeadLineOver() { 99 | return System.nanoTime() > deadlineNanos; 100 | } 101 | 102 | void waitNext() { 103 | final int waitSeconds; 104 | if (first) { 105 | first = false; 106 | waitSeconds = firstInterval; 107 | } else { 108 | waitSeconds = subsequentIntervals; 109 | } 110 | Uninterruptibles.sleepUninterruptibly(waitSeconds, TimeUnit.SECONDS); 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/main/java/cloud/dnation/jenkins/plugins/hetzner/OrphanedNodesCleaner.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 https://dnation.cloud 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cloud.dnation.jenkins.plugins.hetzner; 17 | 18 | import cloud.dnation.hetznerclient.ServerDetail; 19 | import hudson.Extension; 20 | import hudson.model.PeriodicWork; 21 | import jenkins.model.Jenkins; 22 | import lombok.extern.slf4j.Slf4j; 23 | import org.jenkinsci.Symbol; 24 | 25 | import java.io.IOException; 26 | import java.util.List; 27 | import java.util.Set; 28 | import java.util.stream.Collectors; 29 | 30 | @Extension 31 | @Symbol("OrphanedNodesCleaner") 32 | @Slf4j 33 | public class OrphanedNodesCleaner extends PeriodicWork { 34 | @Override 35 | public long getRecurrencePeriod() { 36 | return HOUR; 37 | } 38 | 39 | private static Set getHetznerClouds() { 40 | return Jenkins.get().clouds.stream() 41 | .filter(HetznerCloud.class::isInstance) 42 | .map(HetznerCloud.class::cast) 43 | .collect(Collectors.toSet()); 44 | } 45 | 46 | @Override 47 | protected void doRun() { 48 | doCleanup(); 49 | } 50 | 51 | static void doCleanup() { 52 | getHetznerClouds().forEach(OrphanedNodesCleaner::cleanCloud); 53 | } 54 | 55 | private static void cleanCloud(HetznerCloud cloud) { 56 | try { 57 | final List allInstances = cloud.getResourceManager() 58 | .fetchAllServers(cloud.name); 59 | final List jenkinsNodes = Helper.getHetznerAgents() 60 | .stream() 61 | .map(HetznerServerAgent::getNodeName) 62 | .collect(Collectors.toList()); 63 | allInstances.stream().filter(server -> !jenkinsNodes.contains(server.getName())) 64 | .forEach(serverDetail -> terminateServer(serverDetail, cloud)); 65 | } catch (IOException e) { 66 | log.warn("Error while fetching all servers", e); 67 | } 68 | } 69 | 70 | private static void terminateServer(ServerDetail serverDetail, HetznerCloud cloud) { 71 | log.info("Terminating orphaned server {}", serverDetail.getName()); 72 | cloud.getResourceManager().destroyServer(serverDetail); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/cloud/dnation/jenkins/plugins/hetzner/connect/AbstractConnectivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 https://dnation.cloud 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cloud.dnation.jenkins.plugins.hetzner.connect; 17 | 18 | import hudson.model.AbstractDescribableImpl; 19 | 20 | public abstract class AbstractConnectivity extends AbstractDescribableImpl { 21 | public abstract ConnectivityType getType(); 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/cloud/dnation/jenkins/plugins/hetzner/connect/Both.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 https://dnation.cloud 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cloud.dnation.jenkins.plugins.hetzner.connect; 17 | 18 | import cloud.dnation.jenkins.plugins.hetzner.Messages; 19 | import edu.umd.cs.findbugs.annotations.NonNull; 20 | import hudson.Extension; 21 | import hudson.model.Descriptor; 22 | import lombok.NoArgsConstructor; 23 | import org.jenkinsci.Symbol; 24 | import org.kohsuke.stapler.DataBoundConstructor; 25 | 26 | @NoArgsConstructor(onConstructor = @__({@DataBoundConstructor})) 27 | public class Both extends AbstractConnectivity{ 28 | public ConnectivityType getType() { 29 | return ConnectivityType.BOTH; 30 | } 31 | 32 | @Extension 33 | @Symbol("both") 34 | public static final class DescriptorImpl extends Descriptor { 35 | @NonNull 36 | @Override 37 | public String getDisplayName() { 38 | return Messages.connectivity_both(); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/cloud/dnation/jenkins/plugins/hetzner/connect/BothV6.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 https://dnation.cloud 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cloud.dnation.jenkins.plugins.hetzner.connect; 17 | 18 | import cloud.dnation.jenkins.plugins.hetzner.Messages; 19 | import edu.umd.cs.findbugs.annotations.NonNull; 20 | import hudson.Extension; 21 | import hudson.model.Descriptor; 22 | import lombok.NoArgsConstructor; 23 | import org.jenkinsci.Symbol; 24 | import org.kohsuke.stapler.DataBoundConstructor; 25 | 26 | @NoArgsConstructor(onConstructor = @__({@DataBoundConstructor})) 27 | public class BothV6 extends AbstractConnectivity{ 28 | public ConnectivityType getType() { 29 | return ConnectivityType.BOTH_V6; 30 | } 31 | 32 | @Extension 33 | @Symbol("both-v6") 34 | public static final class DescriptorImpl extends Descriptor { 35 | @NonNull 36 | @Override 37 | public String getDisplayName() { 38 | return Messages.connectivity_bothV6(); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/cloud/dnation/jenkins/plugins/hetzner/connect/ConnectivityType.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 https://dnation.cloud 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cloud.dnation.jenkins.plugins.hetzner.connect; 17 | 18 | public enum ConnectivityType { 19 | PRIVATE, 20 | PUBLIC, 21 | PUBLIC_V6, 22 | BOTH, 23 | BOTH_V6 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/cloud/dnation/jenkins/plugins/hetzner/connect/PrivateOnly.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 https://dnation.cloud 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cloud.dnation.jenkins.plugins.hetzner.connect; 17 | 18 | import cloud.dnation.jenkins.plugins.hetzner.Messages; 19 | import edu.umd.cs.findbugs.annotations.NonNull; 20 | import hudson.Extension; 21 | import hudson.model.Descriptor; 22 | import lombok.NoArgsConstructor; 23 | import org.jenkinsci.Symbol; 24 | import org.kohsuke.stapler.DataBoundConstructor; 25 | 26 | @NoArgsConstructor(onConstructor = @__({@DataBoundConstructor})) 27 | public class PrivateOnly extends AbstractConnectivity { 28 | public ConnectivityType getType() { 29 | return ConnectivityType.PRIVATE; 30 | } 31 | @Extension 32 | @Symbol("private-only") 33 | public static final class DescriptorImpl extends Descriptor { 34 | @NonNull 35 | @Override 36 | public String getDisplayName() { 37 | return Messages.connectivity_private_only(); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/cloud/dnation/jenkins/plugins/hetzner/connect/PublicOnly.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 https://dnation.cloud 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cloud.dnation.jenkins.plugins.hetzner.connect; 17 | 18 | import cloud.dnation.jenkins.plugins.hetzner.Messages; 19 | import edu.umd.cs.findbugs.annotations.NonNull; 20 | import hudson.Extension; 21 | import hudson.model.Descriptor; 22 | import lombok.NoArgsConstructor; 23 | import org.jenkinsci.Symbol; 24 | import org.kohsuke.stapler.DataBoundConstructor; 25 | 26 | @NoArgsConstructor(onConstructor = @__({@DataBoundConstructor})) 27 | public class PublicOnly extends AbstractConnectivity { 28 | public ConnectivityType getType() { 29 | return ConnectivityType.PUBLIC; 30 | } 31 | 32 | @Extension 33 | @Symbol("public-only") 34 | public static final class DescriptorImpl extends Descriptor { 35 | @NonNull 36 | @Override 37 | public String getDisplayName() { 38 | return Messages.connectivity_public_only(); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/cloud/dnation/jenkins/plugins/hetzner/connect/PublicV6Only.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 https://dnation.cloud 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cloud.dnation.jenkins.plugins.hetzner.connect; 17 | 18 | import cloud.dnation.jenkins.plugins.hetzner.Messages; 19 | import edu.umd.cs.findbugs.annotations.NonNull; 20 | import hudson.Extension; 21 | import hudson.model.Descriptor; 22 | import lombok.NoArgsConstructor; 23 | import org.jenkinsci.Symbol; 24 | import org.kohsuke.stapler.DataBoundConstructor; 25 | 26 | @NoArgsConstructor(onConstructor = @__({@DataBoundConstructor})) 27 | public class PublicV6Only extends AbstractConnectivity { 28 | public ConnectivityType getType() { 29 | return ConnectivityType.PUBLIC_V6; 30 | } 31 | 32 | @Extension 33 | @Symbol("publicV6-only") 34 | public static final class DescriptorImpl extends Descriptor { 35 | @NonNull 36 | @Override 37 | public String getDisplayName() { 38 | return Messages.connectivity_publicV6_only(); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/cloud/dnation/jenkins/plugins/hetzner/launcher/AbstractConnectionMethod.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 https://dnation.cloud 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cloud.dnation.jenkins.plugins.hetzner.launcher; 17 | 18 | import cloud.dnation.hetznerclient.ServerDetail; 19 | import hudson.model.AbstractDescribableImpl; 20 | 21 | public abstract class AbstractConnectionMethod extends AbstractDescribableImpl { 22 | public abstract String getAddress(ServerDetail server); 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/cloud/dnation/jenkins/plugins/hetzner/launcher/AbstractHetznerSshConnector.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 https://dnation.cloud 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cloud.dnation.jenkins.plugins.hetzner.launcher; 17 | 18 | import com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey; 19 | import com.cloudbees.plugins.credentials.CredentialsMatchers; 20 | import com.cloudbees.plugins.credentials.CredentialsProvider; 21 | import com.cloudbees.plugins.credentials.common.StandardListBoxModel; 22 | import hudson.model.AbstractDescribableImpl; 23 | import hudson.model.Descriptor; 24 | import hudson.model.Item; 25 | import hudson.security.ACL; 26 | import hudson.util.FormValidation; 27 | import hudson.util.ListBoxModel; 28 | import jenkins.model.Jenkins; 29 | import lombok.Getter; 30 | import lombok.Setter; 31 | import lombok.extern.slf4j.Slf4j; 32 | import org.kohsuke.accmod.Restricted; 33 | import org.kohsuke.accmod.restrictions.NoExternalUse; 34 | import org.kohsuke.stapler.AncestorInPath; 35 | import org.kohsuke.stapler.DataBoundSetter; 36 | import org.kohsuke.stapler.QueryParameter; 37 | import org.kohsuke.stapler.interceptor.RequirePOST; 38 | 39 | import javax.annotation.Nullable; 40 | import java.util.Collections; 41 | 42 | import static cloud.dnation.jenkins.plugins.hetzner.ConfigurationValidator.doCheckNonEmpty; 43 | 44 | @Slf4j 45 | public abstract class AbstractHetznerSshConnector extends AbstractDescribableImpl { 46 | /** 47 | * SSH connection will be authenticated using different user then specified in credentials if this field is non-null. 48 | */ 49 | @Nullable 50 | @Getter 51 | @Setter(onMethod = @__({@DataBoundSetter})) 52 | protected String usernameOverride; 53 | 54 | @Getter 55 | @Setter(onMethod = @__({@DataBoundSetter})) 56 | protected String sshCredentialsId; 57 | 58 | @Getter 59 | @Setter(onMethod = @__({@DataBoundSetter})) 60 | protected AbstractConnectionMethod connectionMethod = DefaultConnectionMethod.SINGLETON; 61 | 62 | public HetznerServerComputerLauncher createLauncher() { 63 | return new HetznerServerComputerLauncher(this); 64 | } 65 | 66 | public static abstract class DescriptorImpl extends Descriptor { 67 | // this method does not have any side effect, nor does it read any state. 68 | @SuppressWarnings("lgtm[jenkins/no-permission-check]") 69 | @Restricted(NoExternalUse.class) 70 | @RequirePOST 71 | public FormValidation doCheckSshCredentialsId(@QueryParameter String sshCredentialsId) { 72 | return doCheckNonEmpty(sshCredentialsId, "SSH credentials"); 73 | } 74 | 75 | @Restricted(NoExternalUse.class) 76 | @RequirePOST 77 | public ListBoxModel doFillSshCredentialsIdItems(@AncestorInPath Item owner) { 78 | final StandardListBoxModel result = new StandardListBoxModel(); 79 | if (owner == null) { 80 | if (!Jenkins.get().hasPermission(Jenkins.ADMINISTER)) { 81 | return result; 82 | } 83 | } else { 84 | if (!owner.hasPermission(owner.EXTENDED_READ) 85 | && !owner.hasPermission(CredentialsProvider.USE_ITEM)) { 86 | return result; 87 | } 88 | } 89 | return new StandardListBoxModel() 90 | .includeEmptyValue() 91 | .includeMatchingAs(ACL.SYSTEM, owner, BasicSSHUserPrivateKey.class, 92 | Collections.emptyList(), CredentialsMatchers.always()); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/main/java/cloud/dnation/jenkins/plugins/hetzner/launcher/DefaultConnectionMethod.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 https://dnation.cloud 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cloud.dnation.jenkins.plugins.hetzner.launcher; 17 | 18 | import cloud.dnation.hetznerclient.ServerDetail; 19 | import cloud.dnation.jenkins.plugins.hetzner.Messages; 20 | import edu.umd.cs.findbugs.annotations.NonNull; 21 | import hudson.Extension; 22 | import hudson.model.Descriptor; 23 | import lombok.NoArgsConstructor; 24 | import org.jenkinsci.Symbol; 25 | import org.kohsuke.stapler.DataBoundConstructor; 26 | 27 | @NoArgsConstructor(onConstructor = @__({@DataBoundConstructor})) 28 | public class DefaultConnectionMethod extends AbstractConnectionMethod { 29 | public static final DefaultConnectionMethod SINGLETON = new DefaultConnectionMethod(); 30 | 31 | @Override 32 | public String getAddress(ServerDetail server) { 33 | if (server.getPrivateNet() != null && !server.getPrivateNet().isEmpty()) { 34 | return server.getPrivateNet().get(0).getIp(); 35 | } else { 36 | return server.getPublicNet().getIpv4().getIp(); 37 | } 38 | } 39 | 40 | @Extension 41 | @Symbol("default") 42 | public static final class DescriptorImpl extends Descriptor { 43 | @NonNull 44 | @Override 45 | public String getDisplayName() { 46 | return Messages.connection_method_default(); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/cloud/dnation/jenkins/plugins/hetzner/launcher/DefaultSshConnector.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 https://dnation.cloud 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cloud.dnation.jenkins.plugins.hetzner.launcher; 17 | 18 | import cloud.dnation.jenkins.plugins.hetzner.Messages; 19 | import edu.umd.cs.findbugs.annotations.NonNull; 20 | import hudson.Extension; 21 | import org.jenkinsci.Symbol; 22 | import org.kohsuke.stapler.DataBoundConstructor; 23 | 24 | import java.io.Serializable; 25 | 26 | /** 27 | * Connect as user configured in credentials. 28 | */ 29 | public class DefaultSshConnector extends AbstractHetznerSshConnector implements Serializable { 30 | @DataBoundConstructor 31 | public DefaultSshConnector(String sshCredentialsId) { 32 | setSshCredentialsId(sshCredentialsId); 33 | } 34 | 35 | @Extension 36 | @Symbol("default") 37 | public static final class DescriptorImpl extends AbstractHetznerSshConnector.DescriptorImpl { 38 | @NonNull 39 | @Override 40 | public String getDisplayName() { 41 | return Messages.connector_SshAsNonRoot(); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/cloud/dnation/jenkins/plugins/hetzner/launcher/DefaultV6ConnectionMethod.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 https://dnation.cloud 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cloud.dnation.jenkins.plugins.hetzner.launcher; 17 | 18 | import cloud.dnation.hetznerclient.ServerDetail; 19 | import cloud.dnation.jenkins.plugins.hetzner.Messages; 20 | import edu.umd.cs.findbugs.annotations.NonNull; 21 | import hudson.Extension; 22 | import hudson.model.Descriptor; 23 | import lombok.NoArgsConstructor; 24 | import org.jenkinsci.Symbol; 25 | import org.kohsuke.stapler.DataBoundConstructor; 26 | 27 | @NoArgsConstructor(onConstructor = @__({@DataBoundConstructor})) 28 | public class DefaultV6ConnectionMethod extends AbstractConnectionMethod { 29 | @Override 30 | public String getAddress(ServerDetail server) { 31 | if (server.getPrivateNet() != null && !server.getPrivateNet().isEmpty()) { 32 | return server.getPrivateNet().get(0).getIp(); 33 | } else { 34 | return server.getPublicNet().getIpv6().getIp(); 35 | } 36 | } 37 | 38 | @Extension 39 | @Symbol("defaultV6") 40 | public static final class DescriptorImpl extends Descriptor { 41 | @NonNull 42 | @Override 43 | public String getDisplayName() { 44 | return Messages.connection_method_defaultV6(); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/cloud/dnation/jenkins/plugins/hetzner/launcher/HetznerServerComputerLauncher.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 https://dnation.cloud 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cloud.dnation.jenkins.plugins.hetzner.launcher; 17 | 18 | import cloud.dnation.hetznerclient.ServerDetail; 19 | import cloud.dnation.jenkins.plugins.hetzner.Helper; 20 | import cloud.dnation.jenkins.plugins.hetzner.HetznerConstants; 21 | import cloud.dnation.jenkins.plugins.hetzner.HetznerServerAgent; 22 | import cloud.dnation.jenkins.plugins.hetzner.HetznerServerComputer; 23 | import com.cloudbees.jenkins.plugins.sshcredentials.SSHAuthenticator; 24 | import com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey; 25 | import com.google.common.base.Preconditions; 26 | import com.google.common.util.concurrent.Uninterruptibles; 27 | import com.trilead.ssh2.Connection; 28 | import com.trilead.ssh2.SCPClient; 29 | import com.trilead.ssh2.ServerHostKeyVerifier; 30 | import com.trilead.ssh2.Session; 31 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 32 | import hudson.AbortException; 33 | import hudson.Util; 34 | import hudson.model.TaskListener; 35 | import hudson.remoting.Channel; 36 | import hudson.slaves.ComputerLauncher; 37 | import hudson.slaves.SlaveComputer; 38 | import jenkins.model.Jenkins; 39 | import lombok.RequiredArgsConstructor; 40 | import lombok.extern.slf4j.Slf4j; 41 | 42 | import java.io.IOException; 43 | import java.nio.charset.StandardCharsets; 44 | import java.util.concurrent.TimeUnit; 45 | import java.util.concurrent.atomic.AtomicBoolean; 46 | 47 | import static cloud.dnation.jenkins.plugins.hetzner.Helper.assertSshKey; 48 | import static cloud.dnation.jenkins.plugins.hetzner.Helper.getStringOrDefault; 49 | import static cloud.dnation.jenkins.plugins.hetzner.HetznerConstants.DEFAULT_REMOTE_FS; 50 | import static hudson.plugins.sshslaves.SSHLauncher.AGENT_JAR; 51 | 52 | @RequiredArgsConstructor 53 | @Slf4j 54 | public class HetznerServerComputerLauncher extends ComputerLauncher { 55 | private static final String AGENT_SCRIPT = ".agent.start.sh"; 56 | private final AtomicBoolean terminated = new AtomicBoolean(false); 57 | private final AbstractHetznerSshConnector connector; 58 | 59 | private static String getRemoteFs(HetznerServerAgent agent) { 60 | final String res = getStringOrDefault(agent.getRemoteFS(), DEFAULT_REMOTE_FS); 61 | //trim trailing slash 62 | if (res.endsWith("/")) { 63 | return res.substring(0, res.length() - 1); 64 | } 65 | return res; 66 | } 67 | 68 | private void copyAgent(Connection connection, 69 | HetznerServerComputer computer, 70 | Helper.LogAdapter logger, 71 | String remoteFs) throws IOException { 72 | final byte[] agentBlob = Jenkins.get().getJnlpJars(AGENT_JAR).readFully(); 73 | final String remoteAgentPath = remoteFs + "/" + AGENT_JAR; 74 | final byte[] launchScriptContent = ("#!/bin/sh" + '\n' + getAgentCommand(computer, remoteFs) + '\n') 75 | .getBytes(StandardCharsets.UTF_8); 76 | final String launchScriptPath = remoteFs + "/" + AGENT_SCRIPT; 77 | final SCPClient scp = connection.createSCPClient(); 78 | logger.info("Copying agent JAR - " + agentBlob.length + " bytes into " + remoteAgentPath); 79 | scp.put(agentBlob, AGENT_JAR, remoteFs, "0644"); 80 | logger.info("Copying agent script - " + launchScriptContent.length + " bytes into " + launchScriptPath); 81 | scp.put(launchScriptContent, AGENT_SCRIPT, remoteFs, "0755"); 82 | } 83 | 84 | @SuppressFBWarnings(value = {"NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE", "NP_NULL_PARAM_DEREF"}, 85 | justification = "NULLnes is checked already") 86 | @Override 87 | public void launch(final SlaveComputer computer, TaskListener listener) throws IOException, InterruptedException { 88 | if (!(computer instanceof HetznerServerComputer)) { 89 | throw new AbortException("Incompatible computer : " + computer); 90 | } 91 | if(connector.getConnectionMethod() == null) { 92 | connector.setConnectionMethod(HetznerConstants.DEFAULT_CONNECTION_METHOD); 93 | } 94 | final HetznerServerComputer hcomputer = (HetznerServerComputer) computer; 95 | final Helper.LogAdapter logger = new Helper.LogAdapter(listener.getLogger(), log); 96 | final HetznerServerAgent node = hcomputer.getNode(); 97 | Preconditions.checkState(node != null && node.getServerInstance() != null, 98 | "Missing node or server instance data in computer %s", computer.getName()); 99 | final String remoteFs = getRemoteFs(node); 100 | final Connection connection = setupConnection(node, logger, listener); 101 | copyAgent(connection, hcomputer, logger, remoteFs); 102 | launchAgent(connection, hcomputer, logger, listener, remoteFs); 103 | } 104 | 105 | @SuppressFBWarnings(value = "NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE", 106 | justification = "NULLnes of node is checked in launch method") 107 | private String getAgentCommand(HetznerServerComputer computer, String remoteFs) { 108 | final String jvmOpts = Util.fixNull(computer.getNode().getTemplate().getJvmOpts()); 109 | return "java " + jvmOpts + " -jar " + remoteFs + "/remoting.jar -workDir " + remoteFs; 110 | } 111 | 112 | @SuppressFBWarnings(value = "NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE", 113 | justification = "NULLnes of node is checked in launch method") 114 | private void launchAgent(Connection connection, 115 | HetznerServerComputer computer, 116 | Helper.LogAdapter logger, 117 | TaskListener listener, 118 | String remoteFs 119 | ) 120 | throws IOException, InterruptedException { 121 | final HetznerServerAgent node = computer.getNode(); 122 | final Session session = connection.openSession(); 123 | final String scriptCmd = "/bin/sh " + remoteFs + "/" + AGENT_SCRIPT; 124 | final String launchCmd; 125 | final String username = connector.getUsernameOverride(); 126 | if (username != null) { 127 | final String credentialsId = node.getTemplate().getConnector().getSshCredentialsId(); 128 | final BasicSSHUserPrivateKey privateKey = assertSshKey(credentialsId); 129 | launchCmd = "sudo -n -u " + privateKey.getUsername() + " " + scriptCmd; 130 | } else { 131 | launchCmd = scriptCmd; 132 | } 133 | 134 | logger.info("Launching agent using '" + launchCmd + "'"); 135 | session.execCommand(launchCmd); 136 | computer.setChannel(session.getStdout(), session.getStdin(), listener, new Channel.Listener() { 137 | @Override 138 | public void onClosed(Channel channel, IOException cause) { 139 | session.close(); 140 | connection.close(); 141 | } 142 | }); 143 | } 144 | 145 | private Connection setupConnection(HetznerServerAgent node, 146 | Helper.LogAdapter logger, 147 | TaskListener taskListener) throws InterruptedException, AbortException { 148 | int retries = 10; 149 | while (!terminated.get() && retries-- > 0) { 150 | final ServerDetail serverDetail = node.getServerInstance().getServerDetail(); 151 | final String ipv4 = connector.getConnectionMethod().getAddress(serverDetail); 152 | final Connection conn = new Connection(ipv4, 22); 153 | try { 154 | conn.connect(AllowAnyServerHostKeyVerifier.INSTANCE, 155 | 30_000, 10_000); 156 | logger.info("Connected to " + node.getNodeName() + " via " + ipv4); 157 | final String credentialsId = node.getTemplate().getConnector().getSshCredentialsId(); 158 | final BasicSSHUserPrivateKey privateKey = assertSshKey(credentialsId); 159 | final String username = Util.fixNull(node.getTemplate().getConnector().getUsernameOverride(), 160 | privateKey.getUsername()); 161 | 162 | logger.info("Authenticating using username '" + username + "'"); 163 | 164 | final SSHAuthenticator authenticator = SSHAuthenticator 165 | .newInstance(conn, privateKey, username); 166 | 167 | if (authenticator.authenticate(taskListener) && conn.isAuthenticationComplete()) { 168 | logger.info("Authentication succeeded"); 169 | return conn; 170 | } else { 171 | throw new AbortException("Authentication failed"); 172 | } 173 | } catch (IOException e) { 174 | logger.error("Connection to " + ipv4 + " failed. Will wait 10 seconds before retry", e); 175 | Uninterruptibles.sleepUninterruptibly(10, TimeUnit.SECONDS); 176 | } 177 | } 178 | throw new AbortException("Failed to launch agent"); 179 | } 180 | 181 | public void signalTermination() { 182 | terminated.set(true); 183 | } 184 | 185 | //TODO: is there a way to verify hostkey of newly created server? 186 | //its is usually generated by cloud-init 187 | private static class AllowAnyServerHostKeyVerifier implements ServerHostKeyVerifier { 188 | static final AllowAnyServerHostKeyVerifier INSTANCE = new AllowAnyServerHostKeyVerifier(); 189 | 190 | @Override 191 | public boolean verifyServerHostKey(String hostname, int port, 192 | String serverHostKeyAlgorithm, 193 | byte[] serverHostKey) throws Exception { 194 | return true; 195 | } 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/main/java/cloud/dnation/jenkins/plugins/hetzner/launcher/PublicAddressOnly.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 https://dnation.cloud 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cloud.dnation.jenkins.plugins.hetzner.launcher; 17 | 18 | import cloud.dnation.hetznerclient.ServerDetail; 19 | import cloud.dnation.jenkins.plugins.hetzner.Messages; 20 | import edu.umd.cs.findbugs.annotations.NonNull; 21 | import hudson.Extension; 22 | import hudson.model.Descriptor; 23 | import lombok.NoArgsConstructor; 24 | import org.jenkinsci.Symbol; 25 | import org.kohsuke.stapler.DataBoundConstructor; 26 | 27 | @NoArgsConstructor(onConstructor = @__({@DataBoundConstructor})) 28 | public class PublicAddressOnly extends AbstractConnectionMethod { 29 | @Override 30 | public String getAddress(ServerDetail server) { 31 | return server.getPublicNet().getIpv4().getIp(); 32 | } 33 | 34 | @Extension 35 | @Symbol("public") 36 | public static final class DescriptorImpl extends Descriptor { 37 | @NonNull 38 | @Override 39 | public String getDisplayName() { 40 | return Messages.connection_method_public(); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/cloud/dnation/jenkins/plugins/hetzner/launcher/PublicV6AddressOnly.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 https://dnation.cloud 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cloud.dnation.jenkins.plugins.hetzner.launcher; 17 | 18 | import cloud.dnation.hetznerclient.ServerDetail; 19 | import cloud.dnation.jenkins.plugins.hetzner.Messages; 20 | import com.google.common.base.Strings; 21 | import edu.umd.cs.findbugs.annotations.NonNull; 22 | import hudson.Extension; 23 | import hudson.model.Descriptor; 24 | import lombok.NoArgsConstructor; 25 | import org.jenkinsci.Symbol; 26 | import org.kohsuke.stapler.DataBoundConstructor; 27 | 28 | @NoArgsConstructor(onConstructor = @__({@DataBoundConstructor})) 29 | public class PublicV6AddressOnly extends AbstractConnectionMethod { 30 | @Override 31 | public String getAddress(ServerDetail server) { 32 | if (server.getPublicNet().getIpv6() == null || Strings.isNullOrEmpty(server.getPublicNet().getIpv6().getIp())) { 33 | throw new IllegalArgumentException("Connection method requires IPv6 address"); 34 | } 35 | // value returned by API ends with "::/64" so replace it with "1" 36 | return server.getPublicNet().getIpv6().getIp().replaceFirst("::/64$", "::1"); 37 | } 38 | 39 | @Extension 40 | @Symbol("publicV6") 41 | public static final class DescriptorImpl extends Descriptor { 42 | @NonNull 43 | @Override 44 | public String getDisplayName() { 45 | return Messages.connection_method_publicV6(); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/cloud/dnation/jenkins/plugins/hetzner/launcher/SshConnectorAsRoot.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 https://dnation.cloud 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cloud.dnation.jenkins.plugins.hetzner.launcher; 17 | 18 | import cloud.dnation.jenkins.plugins.hetzner.Messages; 19 | import edu.umd.cs.findbugs.annotations.NonNull; 20 | import hudson.Extension; 21 | import org.jenkinsci.Symbol; 22 | import org.kohsuke.stapler.DataBoundConstructor; 23 | 24 | /** 25 | * Connect as "root" user, but launch agent as user configured in credentials. 26 | */ 27 | public class SshConnectorAsRoot extends AbstractHetznerSshConnector { 28 | @DataBoundConstructor 29 | public SshConnectorAsRoot(String sshCredentialsId) { 30 | setUsernameOverride("root"); 31 | setSshCredentialsId(sshCredentialsId); 32 | } 33 | 34 | @Extension 35 | @Symbol("root") 36 | public static final class DescriptorImpl extends AbstractHetznerSshConnector.DescriptorImpl { 37 | @NonNull 38 | @Override 39 | public String getDisplayName() { 40 | return Messages.connector_SshAsRoot(); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/cloud/dnation/jenkins/plugins/hetzner/primaryip/AbstractByLabelSelector.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 https://dnation.cloud 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cloud.dnation.jenkins.plugins.hetzner.primaryip; 17 | 18 | import cloud.dnation.hetznerclient.CreateServerRequest; 19 | import cloud.dnation.hetznerclient.HetznerApi; 20 | import cloud.dnation.hetznerclient.PrimaryIpDetail; 21 | import cloud.dnation.hetznerclient.PublicNetRequest; 22 | import com.google.common.annotations.VisibleForTesting; 23 | import com.google.common.base.Strings; 24 | import lombok.Getter; 25 | import lombok.extern.slf4j.Slf4j; 26 | 27 | import java.io.IOException; 28 | 29 | @Slf4j 30 | public abstract class AbstractByLabelSelector extends AbstractPrimaryIpStrategy { 31 | @Getter 32 | private final String selector; 33 | 34 | public AbstractByLabelSelector(boolean failIfError, String selector) { 35 | super(failIfError); 36 | this.selector = selector; 37 | } 38 | 39 | @Override 40 | public void applyInternal(HetznerApi api, CreateServerRequest server) throws IOException { 41 | final PrimaryIpDetail pip = api.getAllPrimaryIps(selector).execute().body().getPrimaryIps().stream() 42 | .filter(ip -> isIpUsable(ip, server)).findFirst().get(); 43 | final PublicNetRequest net = new PublicNetRequest(); 44 | net.setIpv4(pip.getId()); 45 | net.setEnableIpv6(false); 46 | net.setEnableIpv4(true); 47 | server.setPublicNet(net); 48 | } 49 | 50 | @VisibleForTesting 51 | static boolean isIpUsable(PrimaryIpDetail ip, CreateServerRequest server) { 52 | if (ip.getAssigneeId() != null) { 53 | return false; 54 | } 55 | if (!Strings.isNullOrEmpty(server.getLocation())) { 56 | if (server.getLocation().equals(ip.getDatacenter().getLocation().getName())) { 57 | return true; 58 | } 59 | } 60 | if (!Strings.isNullOrEmpty(server.getDatacenter())) { 61 | return server.getDatacenter().equals(ip.getDatacenter().getName()); 62 | } 63 | return false; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/cloud/dnation/jenkins/plugins/hetzner/primaryip/AbstractPrimaryIpStrategy.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 https://dnation.cloud 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cloud.dnation.jenkins.plugins.hetzner.primaryip; 17 | 18 | import cloud.dnation.hetznerclient.CreateServerRequest; 19 | import cloud.dnation.hetznerclient.HetznerApi; 20 | import hudson.model.AbstractDescribableImpl; 21 | import lombok.RequiredArgsConstructor; 22 | import lombok.extern.slf4j.Slf4j; 23 | 24 | import java.io.IOException; 25 | 26 | @Slf4j 27 | @RequiredArgsConstructor 28 | public abstract class AbstractPrimaryIpStrategy extends AbstractDescribableImpl { 29 | protected final boolean failIfError; 30 | 31 | public void apply(HetznerApi api, CreateServerRequest server) { 32 | try { 33 | applyInternal(api, server); 34 | } catch (Exception e) { 35 | if (failIfError) { 36 | throw new RuntimeException(e); 37 | } else { 38 | log.error("Fail to apply primary IP to server", e); 39 | } 40 | } 41 | } 42 | 43 | protected abstract void applyInternal(HetznerApi api, CreateServerRequest server) throws IOException; 44 | } -------------------------------------------------------------------------------- /src/main/java/cloud/dnation/jenkins/plugins/hetzner/primaryip/ByLabelSelectorFailing.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 https://dnation.cloud 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cloud.dnation.jenkins.plugins.hetzner.primaryip; 17 | 18 | import cloud.dnation.jenkins.plugins.hetzner.Messages; 19 | import edu.umd.cs.findbugs.annotations.NonNull; 20 | import hudson.Extension; 21 | import hudson.model.Descriptor; 22 | import org.jenkinsci.Symbol; 23 | import org.kohsuke.stapler.DataBoundConstructor; 24 | 25 | public class ByLabelSelectorFailing extends AbstractByLabelSelector { 26 | @DataBoundConstructor 27 | public ByLabelSelectorFailing(String selector) { 28 | super(true, selector); 29 | } 30 | 31 | @Extension 32 | @Symbol("bylabelselector-failing") 33 | public static final class DescriptorImpl extends Descriptor { 34 | @NonNull 35 | @Override 36 | public String getDisplayName() { 37 | return Messages.primaryip_bylabelselector_failing(); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/cloud/dnation/jenkins/plugins/hetzner/primaryip/ByLabelSelectorIgnoring.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 https://dnation.cloud 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cloud.dnation.jenkins.plugins.hetzner.primaryip; 17 | 18 | import cloud.dnation.jenkins.plugins.hetzner.Messages; 19 | import edu.umd.cs.findbugs.annotations.NonNull; 20 | import hudson.Extension; 21 | import hudson.model.Descriptor; 22 | import org.jenkinsci.Symbol; 23 | import org.kohsuke.stapler.DataBoundConstructor; 24 | 25 | public class ByLabelSelectorIgnoring extends AbstractByLabelSelector { 26 | @DataBoundConstructor 27 | public ByLabelSelectorIgnoring(String selector) { 28 | super(false, selector); 29 | } 30 | 31 | @Extension 32 | @Symbol("bylabelselector-ignoring") 33 | public static final class DescriptorImpl extends Descriptor { 34 | @NonNull 35 | @Override 36 | public String getDisplayName() { 37 | return Messages.primaryip_bylabelselector_ignoring(); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/cloud/dnation/jenkins/plugins/hetzner/primaryip/DefaultStrategy.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 https://dnation.cloud 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cloud.dnation.jenkins.plugins.hetzner.primaryip; 17 | 18 | import cloud.dnation.hetznerclient.CreateServerRequest; 19 | import cloud.dnation.hetznerclient.HetznerApi; 20 | import cloud.dnation.jenkins.plugins.hetzner.Messages; 21 | import edu.umd.cs.findbugs.annotations.NonNull; 22 | import hudson.Extension; 23 | import hudson.model.Descriptor; 24 | import org.jenkinsci.Symbol; 25 | import org.kohsuke.stapler.DataBoundConstructor; 26 | 27 | public class DefaultStrategy extends AbstractPrimaryIpStrategy { 28 | public static final DefaultStrategy SINGLETON = new DefaultStrategy(); 29 | @DataBoundConstructor 30 | public DefaultStrategy() { 31 | super(false); 32 | } 33 | 34 | @Override 35 | public void applyInternal(HetznerApi api, CreateServerRequest server) { 36 | //NOOP 37 | } 38 | 39 | @Extension 40 | @Symbol("default") 41 | public static final class DescriptorImpl extends Descriptor { 42 | @NonNull 43 | @Override 44 | public String getDisplayName() { 45 | return Messages.primaryip_default(); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/cloud/dnation/jenkins/plugins/hetzner/shutdown/AbstractShutdownPolicy.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 https://dnation.cloud 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cloud.dnation.jenkins.plugins.hetzner.shutdown; 17 | 18 | import hudson.model.AbstractDescribableImpl; 19 | import hudson.slaves.AbstractCloudComputer; 20 | import hudson.slaves.RetentionStrategy; 21 | import lombok.Getter; 22 | 23 | import java.util.Objects; 24 | 25 | @SuppressWarnings("rawtypes") 26 | public abstract class AbstractShutdownPolicy extends AbstractDescribableImpl { 27 | @Getter 28 | protected final transient RetentionStrategy retentionStrategy; 29 | 30 | protected AbstractShutdownPolicy(RetentionStrategy strategy) { 31 | this.retentionStrategy = Objects.requireNonNull(strategy); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/cloud/dnation/jenkins/plugins/hetzner/shutdown/BeforeHourWrapsPolicy.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 https://dnation.cloud 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cloud.dnation.jenkins.plugins.hetzner.shutdown; 17 | 18 | import cloud.dnation.jenkins.plugins.hetzner.Helper; 19 | import cloud.dnation.jenkins.plugins.hetzner.HetznerServerAgent; 20 | import cloud.dnation.jenkins.plugins.hetzner.Messages; 21 | import edu.umd.cs.findbugs.annotations.NonNull; 22 | import hudson.Extension; 23 | import hudson.model.Descriptor; 24 | import hudson.slaves.AbstractCloudComputer; 25 | import hudson.slaves.RetentionStrategy; 26 | import lombok.extern.slf4j.Slf4j; 27 | import net.jcip.annotations.GuardedBy; 28 | import org.jenkinsci.Symbol; 29 | import org.kohsuke.stapler.DataBoundConstructor; 30 | 31 | import java.io.IOException; 32 | import java.time.LocalDateTime; 33 | 34 | @Slf4j 35 | public class BeforeHourWrapsPolicy extends AbstractShutdownPolicy { 36 | @SuppressWarnings("rawtypes") 37 | private static final RetentionStrategy STRATEGY_SINGLETON = new RetentionStrategyImpl(); 38 | 39 | @DataBoundConstructor 40 | public BeforeHourWrapsPolicy() { 41 | super(STRATEGY_SINGLETON); 42 | } 43 | 44 | @SuppressWarnings("rawtypes") 45 | @Override 46 | public RetentionStrategy getRetentionStrategy() { 47 | return STRATEGY_SINGLETON; 48 | } 49 | 50 | @SuppressWarnings("rawtypes") 51 | private static class RetentionStrategyImpl extends RetentionStrategy { 52 | @Override 53 | public void start(AbstractCloudComputer c) { 54 | c.connect(false); 55 | } 56 | 57 | @Override 58 | @GuardedBy("hudson.model.Queue.lock") 59 | public long check(final AbstractCloudComputer c) { 60 | final HetznerServerAgent agent = (HetznerServerAgent) c.getNode(); 61 | if (c.isIdle() && agent != null && agent.getServerInstance() != null) { 62 | if (Helper.canShutdownServer(agent.getServerInstance().getServerDetail().getCreated(), 63 | LocalDateTime.now())) { 64 | log.info("Disconnecting {}", c.getName()); 65 | try { 66 | agent.terminate(); 67 | } catch (InterruptedException | IOException e) { 68 | log.warn("Failed to terminate {}", c.getName(), e); 69 | } 70 | } 71 | } 72 | return 1; 73 | } 74 | } 75 | 76 | @Extension 77 | @Symbol("hour-wrap") 78 | public static final class DescriptorImpl extends Descriptor { 79 | @NonNull 80 | @Override 81 | public String getDisplayName() { 82 | return Messages.policy_shutdown_beforeHourWrap(); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/cloud/dnation/jenkins/plugins/hetzner/shutdown/IdlePeriodPolicy.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 https://dnation.cloud 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cloud.dnation.jenkins.plugins.hetzner.shutdown; 17 | 18 | import cloud.dnation.jenkins.plugins.hetzner.Messages; 19 | import edu.umd.cs.findbugs.annotations.NonNull; 20 | import hudson.Extension; 21 | import hudson.model.Descriptor; 22 | import hudson.slaves.CloudRetentionStrategy; 23 | import lombok.Getter; 24 | import org.jenkinsci.Symbol; 25 | import org.kohsuke.stapler.DataBoundConstructor; 26 | 27 | public class IdlePeriodPolicy extends AbstractShutdownPolicy { 28 | @Getter 29 | private final int idleMinutes; 30 | 31 | @DataBoundConstructor 32 | public IdlePeriodPolicy(int idleMinutes) { 33 | super(new CloudRetentionStrategy(idleMinutes)); 34 | this.idleMinutes = idleMinutes; 35 | } 36 | 37 | @Extension 38 | @Symbol("idle") 39 | public static final class DescriptorImpl extends Descriptor { 40 | @NonNull 41 | @Override 42 | public String getDisplayName() { 43 | return Messages.policy_shutdown_idle(); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/resources/cloud/dnation/jenkins/plugins/hetzner/HetznerCloud/config.jelly: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 30 | 31 | 32 | 33 |

34 | 35 |
36 | 37 | 38 | 39 | 40 | 42 | -------------------------------------------------------------------------------- /src/main/resources/cloud/dnation/jenkins/plugins/hetzner/HetznerCloud/help-credentialsId.html: -------------------------------------------------------------------------------- 1 | 16 |
17 | Select text credentials containing token to access Hetzner project 18 |
19 | -------------------------------------------------------------------------------- /src/main/resources/cloud/dnation/jenkins/plugins/hetzner/HetznerCloud/help-name.html: -------------------------------------------------------------------------------- 1 | 16 |
17 | Provide a name for this Hetzner Cloud. 18 | Must be a valid label value. 19 |

20 | Valid label values must be a string of 63 characters or less and must be empty or begin and end with an alphanumeric character ([a-z0-9A-Z]) with dashes (-), underscores (_), dots (.), and alphanumerics between. 21 |

22 |
23 | -------------------------------------------------------------------------------- /src/main/resources/cloud/dnation/jenkins/plugins/hetzner/HetznerServerAgent/configure-entries.jelly: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | STOP! You are not supposed to create or re-configure server in Hetzner cloud this way. 19 |

20 | Servers are provisioned as they are needed, based on labels assigned to them. 21 |

22 | An attempt to save this configuration will lead to an error. 23 |

24 | You can still create server via Hetzner Cloud console and add it as 25 | Permanent Agent. 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/main/resources/cloud/dnation/jenkins/plugins/hetzner/HetznerServerComputer/main.jelly: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 |

Server details

19 | 20 | 21 | ${%No details available} 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 |
Server Id${instance.id}
Template${template.name}
Name${instance.name}
Created${instance.created}
Status${instance.status}
Image${instance.image.description}
vCPU${instance.serverType.cores}
RAM${instance.serverType.memory} GB
Disk${instance.serverType.disk} GB
Public IPv4 address${instance.publicNet.ipv4.ip}
Public IPv6 address${instance.publicNet.ipv6.ip}
Private IPv4 address${instance.privateNet.get(0).ip}
Datacenter${instance.datacenter.name}
Location${instance.datacenter.location.description}
City${instance.datacenter.location.city}
92 |
93 |
94 |
95 | -------------------------------------------------------------------------------- /src/main/resources/cloud/dnation/jenkins/plugins/hetzner/HetznerServerTemplate/config.jelly: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 30 | 31 | 32 | 33 | 35 | 36 | 37 | 38 | 40 | 41 | 42 | 43 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /src/main/resources/cloud/dnation/jenkins/plugins/hetzner/HetznerServerTemplate/help-automountVolumes.html: -------------------------------------------------------------------------------- 1 | 16 |
17 | Auto-mount Volumes after attach. 18 |
19 | -------------------------------------------------------------------------------- /src/main/resources/cloud/dnation/jenkins/plugins/hetzner/HetznerServerTemplate/help-bootDeadline.html: -------------------------------------------------------------------------------- 1 | 16 |
17 | Configure maximum allowed time for VM to boot (for server to have status running). 18 |
19 | -------------------------------------------------------------------------------- /src/main/resources/cloud/dnation/jenkins/plugins/hetzner/HetznerServerTemplate/help-connectivity.html: -------------------------------------------------------------------------------- 1 | 16 |
17 | Network connectivity that is configured on server. 18 | 19 |
20 | Make sure to align with connection method 21 |
-------------------------------------------------------------------------------- /src/main/resources/cloud/dnation/jenkins/plugins/hetzner/HetznerServerTemplate/help-connector.html: -------------------------------------------------------------------------------- 1 | 16 |
17 | Method of connecting Jenkins master with newly provisioned server. 18 |
-------------------------------------------------------------------------------- /src/main/resources/cloud/dnation/jenkins/plugins/hetzner/HetznerServerTemplate/help-image.html: -------------------------------------------------------------------------------- 1 | 16 |
17 | Image label expression or image ID. 18 | When label expression is provided, it is expected that it will resolve to single image. 19 | Only type snapshot with status available is considered. 20 | Alternatively you can supply image ID. 21 | 22 | To obtain list of all images, use following curl command: 23 |

24 |

25 |     curl -H "Authorization: Bearer $API_TOKEN" https://api.hetzner.cloud/v1/images
26 |     
27 |

28 | API documentation 29 |
30 | -------------------------------------------------------------------------------- /src/main/resources/cloud/dnation/jenkins/plugins/hetzner/HetznerServerTemplate/help-jvmOpts.html: -------------------------------------------------------------------------------- 1 | 16 |
17 | Additional JVM options for agent. 18 |
19 | -------------------------------------------------------------------------------- /src/main/resources/cloud/dnation/jenkins/plugins/hetzner/HetznerServerTemplate/help-labelStr.html: -------------------------------------------------------------------------------- 1 | 16 |
17 | Labels that identifies jobs that could run on node created from this template. 18 | Multiple values can be specified when separated by space. 19 | When no labels are specified and usage mode is set to Use this node as much as possible, 20 | then no restrictions will apply and node will be eligible to execute any job. 21 |
22 | -------------------------------------------------------------------------------- /src/main/resources/cloud/dnation/jenkins/plugins/hetzner/HetznerServerTemplate/help-location.html: -------------------------------------------------------------------------------- 1 | 16 |
17 | Name of location or datacenter where to create server such as fsn1 or nbg1-dc3 18 |

19 | To obtain list of locations, you can use following curl command: 20 |

21 |

22 |     curl -H "Authorization: Bearer $API_TOKEN" https://api.hetzner.cloud/v1/locations
23 |     
24 |

25 | API documentation 26 | 27 |

28 | To obtain list of datacenters, you can use following curl command: 29 |

30 |

31 |     curl -H "Authorization: Bearer $API_TOKEN" https://api.hetzner.cloud/v1/datacenters
32 |     
33 |

34 | 35 | API documentation 36 |
37 | -------------------------------------------------------------------------------- /src/main/resources/cloud/dnation/jenkins/plugins/hetzner/HetznerServerTemplate/help-name.html: -------------------------------------------------------------------------------- 1 | 16 |
17 | Name of server template. 18 |
19 | -------------------------------------------------------------------------------- /src/main/resources/cloud/dnation/jenkins/plugins/hetzner/HetznerServerTemplate/help-network.html: -------------------------------------------------------------------------------- 1 | 16 |
17 | Network label expression or network ID. 18 | When label expression is provided, it is expected that it will resolve to single network. 19 | Alternatively you can supply network ID. 20 | 21 | To obtain list of all networks, use following curl command: 22 |

23 |

24 |     curl -H "Authorization: Bearer $API_TOKEN" https://api.hetzner.cloud/v1/networks
25 |     
26 |

27 | API documentation 28 |
29 | -------------------------------------------------------------------------------- /src/main/resources/cloud/dnation/jenkins/plugins/hetzner/HetznerServerTemplate/help-numExecutors.html: -------------------------------------------------------------------------------- 1 | 16 |
17 | Allows set the number of executors on provisioned server. 18 |
19 | -------------------------------------------------------------------------------- /src/main/resources/cloud/dnation/jenkins/plugins/hetzner/HetznerServerTemplate/help-placementGroup.html: -------------------------------------------------------------------------------- 1 | 16 |
17 | ID of placement group 18 | or label expression that resolves to single placement group 19 | To obtain list of all placement groups, use following curl command: 20 |

21 |

22 |     curl -H "Authorization: Bearer $API_TOKEN" https://api.hetzner.cloud/v1/placement_groups
23 |     
24 |

25 | API documentation 26 | 27 |
28 | -------------------------------------------------------------------------------- /src/main/resources/cloud/dnation/jenkins/plugins/hetzner/HetznerServerTemplate/help-primaryIp.html: -------------------------------------------------------------------------------- 1 | 16 |
17 | Defines how Primary IP is allocated to the server. 18 |
19 | -------------------------------------------------------------------------------- /src/main/resources/cloud/dnation/jenkins/plugins/hetzner/HetznerServerTemplate/help-remoteFs.html: -------------------------------------------------------------------------------- 1 | 16 |
17 | Agent working directory. See here for more details. 18 |
19 | -------------------------------------------------------------------------------- /src/main/resources/cloud/dnation/jenkins/plugins/hetzner/HetznerServerTemplate/help-serverType.html: -------------------------------------------------------------------------------- 1 | 16 |
17 | Name of server type to use for creating server such as cx21. 18 | To obtain list of server types, you can use following curl command: 19 |

20 |

21 |     curl -H "Authorization: Bearer $API_TOKEN" https://api.hetzner.cloud/v1/server_types
22 |     
23 |

24 | API documentation 25 |
26 | -------------------------------------------------------------------------------- /src/main/resources/cloud/dnation/jenkins/plugins/hetzner/HetznerServerTemplate/help-shutdownPolicy.html: -------------------------------------------------------------------------------- 1 | 16 |
17 | Defines how idle server is shutdown. 18 |
19 | -------------------------------------------------------------------------------- /src/main/resources/cloud/dnation/jenkins/plugins/hetzner/HetznerServerTemplate/help-userData.html: -------------------------------------------------------------------------------- 1 | 16 |
17 | Cloud-Init user data to use during Server creation. 18 | This field is limited to 32KiB by Hetzner Cloud itself. 19 |
20 | -------------------------------------------------------------------------------- /src/main/resources/cloud/dnation/jenkins/plugins/hetzner/HetznerServerTemplate/help-volumeIds.html: -------------------------------------------------------------------------------- 1 | 16 |
17 | Volume IDs which should be attached to the Server at the creation time. Volumes must be in the same Location. 18 | Note that volumes can be mounted into single server at the time. 19 |
20 | -------------------------------------------------------------------------------- /src/main/resources/cloud/dnation/jenkins/plugins/hetzner/Messages.properties: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2021 https://dnation.cloud 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | cloudConfigPassed=Cloud configuration seems to be valid 17 | plugin.description=Plugin for launching build Agents as Hetzner compute resources 18 | plugin.displayName=Hetzner 19 | serverTemplate.displayName=Hetzner server template 20 | connector.SshAsRoot=Connect via SSH as root, but launch agent as user configured in credentials 21 | connector.SshAsNonRoot=Connect via SSH as user configured in credentials 22 | policy.shutdown.idle=Removes server after it's idle for period of time 23 | policy.shutdown.beforeHourWrap=Removes idle server just before current hour of billing cycle completes 24 | primaryip.default=Use default behavior 25 | primaryip.bylabelselector.failing=Allocate primary IPv4 using label selector, fail if none is available 26 | primaryip.bylabelselector.ignoring=Allocate primary IPv4 using label selector, ignore any error 27 | connection.method.default=Connect using private IPv4 address if available, otherwise using public IPv4 address 28 | connection.method.defaultV6=Connect using private IPv4 address if available, otherwise using public IPv6 address 29 | connection.method.public=Connect using public IPv4 address only 30 | connection.method.publicV6=Connect using public IPv6 address only 31 | connectivity.private-only=Only private networking will be used. Network ID or label expression must be provided as well. 32 | connectivity.public-only=Only public networking will be allocated 33 | connectivity.publicV6-only=Only public IPv6 networking will be allocated 34 | connectivity.both=Configure both private and public networking. Additional constrains may apply 35 | connectivity.bothV6=Configure both private network and public IPv6. Additional constrains may apply -------------------------------------------------------------------------------- /src/main/resources/cloud/dnation/jenkins/plugins/hetzner/launcher/AbstractHetznerSshConnector/config.jelly: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/main/resources/cloud/dnation/jenkins/plugins/hetzner/primaryip/ByLabelSelectorFailing/config.jelly: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/main/resources/cloud/dnation/jenkins/plugins/hetzner/primaryip/ByLabelSelectorFailing/help-selector.html: -------------------------------------------------------------------------------- 1 | 16 |
17 | Label selector used to filter existing Primary IP addresses. 18 |
19 | -------------------------------------------------------------------------------- /src/main/resources/cloud/dnation/jenkins/plugins/hetzner/primaryip/ByLabelSelectorIgnoring/config.jelly: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/main/resources/cloud/dnation/jenkins/plugins/hetzner/primaryip/ByLabelSelectorIgnoring/help-selector.html: -------------------------------------------------------------------------------- 1 | 16 |
17 | Label selector used to filter existing Primary IP addresses. 18 |
19 | -------------------------------------------------------------------------------- /src/main/resources/cloud/dnation/jenkins/plugins/hetzner/shutdown/BeforeHourWrapsPolicy/help.html: -------------------------------------------------------------------------------- 1 | 16 |
17 | Server will be kept alive until current hour of billing cycle completes. 18 | Make sure that Jenkins controller's clock are configured correctly as skew may lead to 1 hour over-billing. 19 |
20 | -------------------------------------------------------------------------------- /src/main/resources/cloud/dnation/jenkins/plugins/hetzner/shutdown/IdlePeriodPolicy/config.jelly: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/main/resources/cloud/dnation/jenkins/plugins/hetzner/shutdown/IdlePeriodPolicy/help-idleMinutes.html: -------------------------------------------------------------------------------- 1 | 16 |
17 | Server will be kept alive for defined number of minutes after being idle. 18 |
19 | -------------------------------------------------------------------------------- /src/main/resources/index.jelly: -------------------------------------------------------------------------------- 1 | 16 | 17 |
18 | This plugin provides integration with Hetzner Cloud 19 |
-------------------------------------------------------------------------------- /src/test/java/cloud/dnation/jenkins/plugins/hetzner/HelperTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 https://dnation.cloud 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cloud.dnation.jenkins.plugins.hetzner; 17 | 18 | import org.junit.Test; 19 | 20 | import java.io.IOException; 21 | import java.time.LocalDateTime; 22 | import java.time.format.DateTimeFormatter; 23 | 24 | import static org.junit.Assert.assertEquals; 25 | import static org.junit.Assert.assertFalse; 26 | import static org.junit.Assert.assertTrue; 27 | 28 | public class HelperTest { 29 | @Test 30 | public void testExtractPublicKeyRSA() throws IOException { 31 | final String pubKeyStr = TestHelper.resourceAsString("id_rsa.pub"); 32 | final String privKeyStr = TestHelper.resourceAsString("id_rsa"); 33 | assertEquals(pubKeyStr, Helper.getSSHPublicKeyFromPrivate(privKeyStr, null)); 34 | } 35 | 36 | @Test 37 | public void testExtractPublicKeyED25519() throws IOException { 38 | final String pubKeyStr = TestHelper.resourceAsString("id_ed25519.pub"); 39 | final String privKeyStr = TestHelper.resourceAsString("id_ed25519"); 40 | assertEquals(pubKeyStr, Helper.getSSHPublicKeyFromPrivate(privKeyStr, null)); 41 | } 42 | 43 | private static LocalDateTime time(String str) { 44 | return LocalDateTime.from(DateTimeFormatter.ISO_DATE_TIME.parse(str + "+02:00")); 45 | } 46 | 47 | @Test 48 | public void testCanShutdownServer() { 49 | //server started at 10:41 UTC, so it can be shutdown in minutes 36-40 50 | String str = "2022-05-21T10:41:19+00:00"; 51 | assertFalse(Helper.canShutdownServer(str, time("2022-05-21T10:50:11"))); 52 | assertTrue(Helper.canShutdownServer(str, time("2022-05-21T11:36:19"))); 53 | assertTrue(Helper.canShutdownServer(str, time("2022-05-21T11:40:13"))); 54 | assertFalse(Helper.canShutdownServer(str, time("2022-05-21T10:41:14"))); 55 | //server started at 10:01, so it can be shutdown in minutes 56-00 56 | str = "2022-05-21T10:01:19+00:00"; 57 | assertFalse(Helper.canShutdownServer(str, time("2022-05-21T10:55:15"))); 58 | assertTrue(Helper.canShutdownServer(str, time("2022-05-21T10:56:19"))); 59 | assertTrue(Helper.canShutdownServer(str, time("2022-05-21T10:59:17"))); 60 | assertFalse(Helper.canShutdownServer(str, time("2022-05-21T10:00:18"))); 61 | assertTrue(Helper.canShutdownServer(str, time("2022-05-21T11:00:18"))); 62 | assertFalse(Helper.canShutdownServer(str, time("2022-05-21T10:01:19"))); 63 | assertFalse(Helper.canShutdownServer(str, time("2022-05-21T10:32:20"))); 64 | str = "2022-08-08T11:03:55+00:00"; 65 | assertFalse(Helper.canShutdownServer(str, time("2022-08-08T11:03:02"))); 66 | assertTrue(Helper.canShutdownServer(str, time("2022-08-08T11:59:02"))); 67 | } 68 | 69 | @Test 70 | public void testIsPossiblyLong() { 71 | assertTrue(Helper.isPossiblyLong("1")); 72 | assertFalse(Helper.isPossiblyLong("0")); 73 | assertFalse(Helper.isPossiblyLong("not-a-number")); 74 | } 75 | 76 | @Test 77 | public void testIsValidLabelValue() { 78 | assertFalse(Helper.isValidLabelValue("")); 79 | assertFalse(Helper.isValidLabelValue(null)); 80 | assertTrue(Helper.isValidLabelValue("cloud-01")); 81 | assertTrue(Helper.isValidLabelValue("cloud_01")); 82 | assertFalse(Helper.isValidLabelValue("cloud 01")); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/test/java/cloud/dnation/jenkins/plugins/hetzner/HetznerCloudResourceManagerTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 https://dnation.cloud 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cloud.dnation.jenkins.plugins.hetzner; 17 | 18 | import cloud.dnation.hetznerclient.CreateServerRequest; 19 | import cloud.dnation.jenkins.plugins.hetzner.connect.ConnectivityType; 20 | import org.junit.Test; 21 | 22 | import java.io.IOException; 23 | 24 | import static cloud.dnation.jenkins.plugins.hetzner.HetznerCloudResourceManager.customizeNetworking; 25 | import static org.junit.Assert.assertEquals; 26 | 27 | public class HetznerCloudResourceManagerTest { 28 | @Test 29 | public void testCustomizeNetworking() throws IOException { 30 | CreateServerRequest req; 31 | 32 | req = new CreateServerRequest(); 33 | customizeNetworking(ConnectivityType.BOTH, req, "", (s1, s2) -> { 34 | }); 35 | assertEquals(true, req.getPublicNet().getEnableIpv4()); 36 | assertEquals(true, req.getPublicNet().getEnableIpv6()); 37 | 38 | req = new CreateServerRequest(); 39 | customizeNetworking(ConnectivityType.BOTH_V6, req, "", (s1, s2) -> { 40 | }); 41 | assertEquals(false, req.getPublicNet().getEnableIpv4()); 42 | assertEquals(true, req.getPublicNet().getEnableIpv6()); 43 | 44 | req = new CreateServerRequest(); 45 | customizeNetworking(ConnectivityType.PUBLIC, req, "", (s1, s2) -> { 46 | }); 47 | assertEquals(true, req.getPublicNet().getEnableIpv4()); 48 | assertEquals(true, req.getPublicNet().getEnableIpv6()); 49 | 50 | req = new CreateServerRequest(); 51 | customizeNetworking(ConnectivityType.PUBLIC_V6, req, "", (s1, s2) -> { 52 | }); 53 | assertEquals(false, req.getPublicNet().getEnableIpv4()); 54 | assertEquals(true, req.getPublicNet().getEnableIpv6()); 55 | 56 | req = new CreateServerRequest(); 57 | customizeNetworking(ConnectivityType.PRIVATE, req, "", (s1, s2) -> { 58 | }); 59 | assertEquals(false, req.getPublicNet().getEnableIpv4()); 60 | assertEquals(false, req.getPublicNet().getEnableIpv6()); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/test/java/cloud/dnation/jenkins/plugins/hetzner/HetznerCloudSimpleTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 https://dnation.cloud 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cloud.dnation.jenkins.plugins.hetzner; 17 | 18 | import cloud.dnation.jenkins.plugins.hetzner.launcher.AbstractHetznerSshConnector; 19 | import com.google.common.collect.Iterables; 20 | import com.google.common.collect.Lists; 21 | import hudson.model.Node; 22 | import hudson.model.labels.LabelAtom; 23 | import hudson.slaves.Cloud; 24 | import hudson.slaves.NodeProvisioner; 25 | import jenkins.model.Jenkins; 26 | import lombok.extern.slf4j.Slf4j; 27 | import org.junit.After; 28 | import org.junit.Before; 29 | import org.junit.Test; 30 | import org.junit.runner.RunWith; 31 | import org.mockito.MockedStatic; 32 | import org.mockito.Mockito; 33 | import org.mockito.stubbing.Answer; 34 | 35 | import java.io.IOException; 36 | import java.util.Collection; 37 | import java.util.concurrent.TimeUnit; 38 | 39 | import static org.awaitility.Awaitility.await; 40 | import static org.junit.Assert.assertFalse; 41 | import static org.junit.Assert.assertTrue; 42 | import static org.mockito.ArgumentMatchers.anyString; 43 | import static org.mockito.Mockito.doAnswer; 44 | import static org.mockito.Mockito.mock; 45 | import static org.mockito.Mockito.times; 46 | import static org.mockito.Mockito.verify; 47 | 48 | @Slf4j 49 | public class HetznerCloudSimpleTest { 50 | private HetznerCloudResourceManager rsrcMgr; 51 | 52 | MockedStatic jenkinsMock; 53 | MockedStatic hetznerCloudResourceManagerMockedStatic; 54 | @Before 55 | public void setupBefore() { 56 | jenkinsMock = Mockito.mockStatic(Jenkins.class); 57 | hetznerCloudResourceManagerMockedStatic = Mockito.mockStatic(HetznerCloudResourceManager.class); 58 | //PowerMockito.mockStatic(Jenkins.class, HetznerCloudResourceManager.class); 59 | rsrcMgr = mock(HetznerCloudResourceManager.class); 60 | Mockito.when(HetznerCloudResourceManager.create(anyString())).thenReturn(rsrcMgr); 61 | 62 | Jenkins jenkins = mock(Jenkins.class); 63 | doAnswer((Answer) invocationOnMock -> new LabelAtom(invocationOnMock.getArgument(0))) 64 | .when(jenkins).getLabelAtom(anyString()); 65 | Mockito.when(Jenkins.get()).thenReturn(jenkins); 66 | } 67 | 68 | @After 69 | public void cleanMock() { 70 | jenkinsMock.close(); 71 | hetznerCloudResourceManagerMockedStatic.close(); 72 | } 73 | 74 | @Test 75 | public void testCanProvision() throws IOException { 76 | 77 | HetznerServerTemplate template1 = new HetznerServerTemplate("template-1", "java", 78 | "name=img1", "nbg1", "cx21"); 79 | final AbstractHetznerSshConnector connector = mock(AbstractHetznerSshConnector.class); 80 | template1.setConnector(connector); 81 | 82 | final HetznerCloud cloud = new HetznerCloud("hcloud-01", "mock-credentials", "10", 83 | Lists.newArrayList(template1)); 84 | Cloud.CloudState cloudState = new Cloud.CloudState(new LabelAtom("java"), 1); 85 | assertTrue(cloud.canProvision(cloudState)); 86 | 87 | final Collection plannedNodes = cloud.provision(cloudState, 1); 88 | final NodeProvisioner.PlannedNode node = Iterables.getOnlyElement(plannedNodes); 89 | await().atMost(30, TimeUnit.SECONDS).until(node.future::isDone); 90 | verify(connector, times(1)).createLauncher(); 91 | verify(rsrcMgr, times(1)).fetchAllServers(anyString()); 92 | 93 | Cloud.CloudState cloudState2 = new Cloud.CloudState(new LabelAtom("unknown"), 1); 94 | assertFalse(cloud.canProvision(cloudState2)); 95 | } 96 | 97 | @Test 98 | public void testCannotProvisionInExclusiveMode() { 99 | HetznerServerTemplate tmpl1 = new HetznerServerTemplate("tmpl1", "label1", "img1", "fsn1", "cx31"); 100 | tmpl1.setMode(Node.Mode.EXCLUSIVE); 101 | final HetznerCloud cloud = new HetznerCloud("hcloud-01", "mock-credentials", "10", 102 | Lists.newArrayList(tmpl1) 103 | ); 104 | Cloud.CloudState cloudState = new Cloud.CloudState(new LabelAtom("java"), 1); 105 | assertFalse(cloud.canProvision(cloudState)); 106 | } 107 | 108 | @Test 109 | public void testCanProvisionInNormalMode() { 110 | HetznerServerTemplate tmpl1 = new HetznerServerTemplate("tmpl1", null, "img1", "fsn1", "cx31"); 111 | tmpl1.setMode(Node.Mode.NORMAL); 112 | final HetznerCloud cloud = new HetznerCloud("hcloud-01", "mock-credentials", "10", 113 | Lists.newArrayList(tmpl1) 114 | ); 115 | Cloud.CloudState cloudState = new Cloud.CloudState(new LabelAtom("java"), 1); 116 | assertTrue(cloud.canProvision(cloudState)); 117 | } 118 | 119 | //see https://github.com/jenkinsci/hetzner-cloud-plugin/issues/15 120 | @Test 121 | public void testCanProvisionNullJobLabel() { 122 | HetznerServerTemplate tmpl1 = new HetznerServerTemplate("tmpl1", null, "img1", "fsn1", "cx31"); 123 | tmpl1.setMode(Node.Mode.NORMAL); 124 | HetznerServerTemplate tmpl2 = new HetznerServerTemplate("tmpl1", "label2,label3", "img1", "fsn1", "cx31"); 125 | tmpl2.setMode(Node.Mode.EXCLUSIVE); 126 | final HetznerCloud cloud = new HetznerCloud("hcloud-01", "mock-credentials", "10", 127 | Lists.newArrayList(tmpl1, tmpl2) 128 | ); 129 | Cloud.CloudState cloudState = new Cloud.CloudState(null, 1); 130 | assertTrue(cloud.canProvision(cloudState)); 131 | Cloud.CloudState cloudState2 = new Cloud.CloudState(new LabelAtom("label3"), 1); 132 | assertTrue(cloud.canProvision(cloudState2)); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/test/java/cloud/dnation/jenkins/plugins/hetzner/HetznerCloudTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 https://dnation.cloud 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cloud.dnation.jenkins.plugins.hetzner; 17 | 18 | import org.htmlunit.html.DomElement; 19 | import org.htmlunit.html.HtmlPage; 20 | import org.junit.Assert; 21 | import org.junit.Rule; 22 | import org.junit.Test; 23 | import org.jvnet.hudson.test.JenkinsRule; 24 | 25 | import java.util.ArrayList; 26 | 27 | public class HetznerCloudTest { 28 | @Rule 29 | public JenkinsRule j = new JenkinsRule(); 30 | 31 | @Test 32 | public void test() throws Exception { 33 | final HetznerCloud cloud = new HetznerCloud("hcloud-01", "mock-credentials", "10", 34 | new ArrayList<>()); 35 | j.jenkins.clouds.add(cloud); 36 | j.jenkins.save(); 37 | try (JenkinsRule.WebClient wc = j.createWebClient()) { 38 | HtmlPage p = wc.goTo("manage/cloud/"); 39 | DomElement domElement = p.getElementById("cloud_" + cloud.name); 40 | Assert.assertNotNull(domElement); 41 | p = wc.goTo("manage/cloud/hcloud-01/configure"); 42 | Assert.assertTrue("No input with value " + cloud.name, p.getElementsByTagName("input").stream() 43 | .filter(element -> element.hasAttribute("value")) 44 | .anyMatch(element -> cloud.name.equals(element.getAttribute("value")))); 45 | 46 | 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/test/java/cloud/dnation/jenkins/plugins/hetzner/JCasCTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 https://dnation.cloud 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cloud.dnation.jenkins.plugins.hetzner; 17 | 18 | import io.jenkins.plugins.casc.ConfigurationContext; 19 | import io.jenkins.plugins.casc.ConfiguratorRegistry; 20 | import io.jenkins.plugins.casc.misc.ConfiguredWithCode; 21 | import io.jenkins.plugins.casc.misc.JenkinsConfiguredWithCodeRule; 22 | import io.jenkins.plugins.casc.model.CNode; 23 | import jenkins.model.Jenkins; 24 | import org.junit.ClassRule; 25 | import org.junit.Test; 26 | 27 | import static io.jenkins.plugins.casc.misc.Util.getJenkinsRoot; 28 | import static org.junit.Assert.assertEquals; 29 | import static org.junit.Assert.assertNotNull; 30 | 31 | public class JCasCTest { 32 | 33 | @ClassRule 34 | @ConfiguredWithCode("jcasc.yaml") 35 | public static JenkinsConfiguredWithCodeRule j = new JenkinsConfiguredWithCodeRule(); 36 | 37 | @Test 38 | public void testConfigure() { 39 | final HetznerCloud cloud = (HetznerCloud) Jenkins.get().clouds.getByName("hcloud-01"); 40 | assertNotNull(cloud); 41 | assertEquals("hcloud-01", cloud.getDisplayName()); 42 | assertEquals(10, cloud.getInstanceCap()); 43 | assertEquals(1, cloud.getServerTemplates().size()); 44 | assertEquals("name=jenkins", cloud.getServerTemplates().get(0).getImage()); 45 | assertEquals("fsn1", cloud.getServerTemplates().get(0).getLocation()); 46 | } 47 | 48 | @Test 49 | public void testExport() throws Exception { 50 | final ConfigurationContext ctx = new ConfigurationContext(ConfiguratorRegistry.get()); 51 | final CNode cloud = getJenkinsRoot(ctx).get("clouds"); 52 | assertNotNull(cloud); 53 | } 54 | } 55 | 56 | -------------------------------------------------------------------------------- /src/test/java/cloud/dnation/jenkins/plugins/hetzner/TestHelper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 https://dnation.cloud 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cloud.dnation.jenkins.plugins.hetzner; 17 | 18 | import com.google.common.io.ByteStreams; 19 | import com.google.common.io.Resources; 20 | import lombok.experimental.UtilityClass; 21 | 22 | import java.io.ByteArrayOutputStream; 23 | import java.io.IOException; 24 | import java.io.InputStream; 25 | import java.nio.charset.StandardCharsets; 26 | 27 | @UtilityClass 28 | public class TestHelper { 29 | public static String inputStreamAsString(InputStream is) throws IOException { 30 | ByteArrayOutputStream os = new ByteArrayOutputStream(); 31 | ByteStreams.copy(is, os); 32 | return os.toString(StandardCharsets.UTF_8.name()); 33 | } 34 | 35 | public static String resourceAsString(String name) throws IOException { 36 | try (InputStream is = Resources.getResource(name).openStream()) { 37 | return inputStreamAsString(is); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/test/java/cloud/dnation/jenkins/plugins/hetzner/launcher/TestPublicV6AddressOnly.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 https://dnation.cloud 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cloud.dnation.jenkins.plugins.hetzner.launcher; 17 | 18 | import cloud.dnation.hetznerclient.Ipv4Detail; 19 | import cloud.dnation.hetznerclient.Ipv6Detail; 20 | import cloud.dnation.hetznerclient.PublicNetDetail; 21 | import cloud.dnation.hetznerclient.ServerDetail; 22 | import org.junit.Test; 23 | 24 | import static org.junit.Assert.assertEquals; 25 | import static org.junit.Assert.fail; 26 | 27 | public class TestPublicV6AddressOnly { 28 | @Test(expected = IllegalArgumentException.class) 29 | public void testMissingV6Address() { 30 | final PublicV6AddressOnly addr = new PublicV6AddressOnly(); 31 | addr.getAddress(new ServerDetail().publicNet(new PublicNetDetail().ipv4(new Ipv4Detail()))); 32 | fail(); 33 | } 34 | 35 | @Test 36 | public void testValid() { 37 | final PublicV6AddressOnly addr = new PublicV6AddressOnly(); 38 | final String res = addr.getAddress(new ServerDetail().publicNet( 39 | new PublicNetDetail().ipv6(new Ipv6Detail().ip("2a01:4e3:a0a:9b7b::/64")))); 40 | assertEquals("2a01:4e3:a0a:9b7b::1", res); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/test/java/cloud/dnation/jenkins/plugins/hetzner/primaryip/PrimaryIpStrategyTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 https://dnation.cloud 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cloud.dnation.jenkins.plugins.hetzner.primaryip; 17 | 18 | import cloud.dnation.hetznerclient.CreateServerRequest; 19 | import cloud.dnation.hetznerclient.DatacenterDetail; 20 | import cloud.dnation.hetznerclient.LocationDetail; 21 | import cloud.dnation.hetznerclient.PrimaryIpDetail; 22 | import org.junit.Test; 23 | 24 | import static cloud.dnation.jenkins.plugins.hetzner.primaryip.AbstractByLabelSelector.isIpUsable; 25 | import static org.junit.Assert.assertFalse; 26 | import static org.junit.Assert.assertTrue; 27 | 28 | public class PrimaryIpStrategyTest { 29 | private static final DatacenterDetail FSN1DC14; 30 | private static final DatacenterDetail NBG1DC4; 31 | private static final LocationDetail FSN1; 32 | private static final LocationDetail NBG1; 33 | static { 34 | FSN1 = new LocationDetail(); 35 | FSN1.setName("fsn1"); 36 | FSN1DC14 = new DatacenterDetail(); 37 | FSN1DC14.setName("fsn1-dc14"); 38 | FSN1DC14.setLocation(FSN1); 39 | NBG1 = new LocationDetail(); 40 | NBG1.setName("nbg1"); 41 | NBG1DC4 = new DatacenterDetail(); 42 | NBG1DC4.setName("nbg1-dc3"); 43 | NBG1DC4.setLocation(NBG1); 44 | } 45 | @Test 46 | public void testIpIsUsable() { 47 | final CreateServerRequest server = new CreateServerRequest(); 48 | final PrimaryIpDetail ip = new PrimaryIpDetail(); 49 | //Same datacenter 50 | server.setDatacenter(FSN1DC14.getName()); 51 | ip.setDatacenter(FSN1DC14); 52 | assertTrue(isIpUsable(ip, server)); 53 | 54 | //Same location 55 | server.setDatacenter(null); 56 | server.setLocation("fsn1"); 57 | assertTrue(isIpUsable(ip, server)); 58 | 59 | //Different datacenter 60 | ip.setDatacenter(NBG1DC4); 61 | server.setDatacenter(FSN1DC14.getName()); 62 | server.setLocation(null); 63 | assertFalse(isIpUsable(ip, server)); 64 | 65 | //Already allocated 66 | ip.setAssigneeId(0L); 67 | assertFalse(isIpUsable(ip, server)); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/test/resources/cloud/dnation/jenkins/plugins/hetzner/jcasc.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2021 https://dnation.cloud 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | --- 16 | jenkins: 17 | clouds: 18 | - hetzner: 19 | name: "hcloud-01" 20 | credentialsId: "hcloud-api-token" 21 | instanceCapStr: "10" 22 | serverTemplates: 23 | - name: ubuntu2-cx21 24 | serverType: cx21 25 | remoteFs: /var/lib/jenkins 26 | location: fsn1 27 | image: name=jenkins 28 | labelStr: java 29 | connector: 30 | root: 31 | sshCredentialsId: 'ssh-private-key' 32 | credentials: 33 | system: 34 | domainCredentials: 35 | - credentials: 36 | - stringCredentialsImpl: 37 | scope: SYSTEM 38 | id: "hcloud-api-token" 39 | description: "Hetzner cloud API token" 40 | secret: "aUJjQICU4jOrOqISL4kCwgcgh6weyHm0btc0lxScpMa3s28ci+7h7mtp3PLeHPPwCGCbWxG8iA0r" 41 | - basicSSHUserPrivateKey: 42 | scope: SYSTEM 43 | id: "ssh-private-key" 44 | username: "jenkins" 45 | privateKeySource: 46 | directEntry: 47 | privateKey: | 48 | -----BEGIN OPENSSH PRIVATE KEY----- 49 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn 50 | NhAAAAAwEAAQAAAQEAyMm0501e8CnMGO72c5fihC//KhdOGahgvbCGbS5mOk4zfpvo+Jui 51 | 3hIJOOt6DW2atAabo8wUXLg9KH9cXHmKs6SMGuRd7m5fdTaUzO0G//ZmzX51phqSYv6JsU 52 | n/vKxqOeVtIWzXHuC+lOtEyR+BKA2UQJOTReO4dv8oAt9cBGSN4gOItc7s07ggK+ZD+S2u 53 | gSTpf5gXYnD9qq9TFuEyMFLJi9gWE/M3c/ZeeDyfTeVeYdbaq2qI3NNUtUYXB4Es0XsdzR 54 | ZSfMhPggJaw8jhlMYFnJE+8O2gJiWrvky1NYhouRD2QzdTCwDoG58AK2Epnd0wIE1iDAHU 55 | 6ZVQaj1BawAAA8iwi+/UsIvv1AAAAAdzc2gtcnNhAAABAQDIybTnTV7wKcwY7vZzl+KEL/ 56 | 8qF04ZqGC9sIZtLmY6TjN+m+j4m6LeEgk463oNbZq0BpujzBRcuD0of1xceYqzpIwa5F3u 57 | bl91NpTM7Qb/9mbNfnWmGpJi/omxSf+8rGo55W0hbNce4L6U60TJH4EoDZRAk5NF47h2/y 58 | gC31wEZI3iA4i1zuzTuCAr5kP5La6BJOl/mBdicP2qr1MW4TIwUsmL2BYT8zdz9l54PJ9N 59 | 5V5h1tqraojc01S1RhcHgSzRex3NFlJ8yE+CAlrDyOGUxgWckT7w7aAmJau+TLU1iGi5EP 60 | ZDN1MLAOgbnwArYSmd3TAgTWIMAdTplVBqPUFrAAAAAwEAAQAAAQEAhe/vad/1tZTcHcHB 61 | yqgFpRHzT1uOcJUeO0r20PwDm18xAIL2LGh9g09asYp6t1xmtzI1PlVTO+p2eX5D2TgGaw 62 | EXqJSvh+4+ZQ0Mw4pVggcW2ntB9ZSCE+Ehbo8jNfN5RLejTYmyElnvJ52tG9CVMmekflMz 63 | CYr3MQHR6eCfHBnbOgdMFiIyTgFliT1MAZBlWRtVJDQkr8DZMBjN1qoVHldLISl2nREvVt 64 | b6TM1gCCp3fuey6+pe5BOm5gn/FjxOXiOiBrmfAu0Wu5ITnblCeje9Y/z0dHJNgKxhKfuo 65 | hp78EE+fgHwVpUMeAunId9uRBwu8/u8eewtb7tMcYyK82QAAAIB8rhliWV/AOJqk+RhadS 66 | uF+640Zekk8dw0EFQiyYeK9IABi+WGs0+XTd3a0k/bUUM0jxxa2os1fSsnojOhMCYpLt5A 67 | UzmVWENG4xixscX0xdtJeYI91/Q7JuPmRbR2rGCL76WGyVnFvrKfpih1IgUKd9xkMT32WN 68 | yp/rSKab78sQAAAIEA/M+MJjP+v1bA/2efBD70Vp4JgtnrsHIP80ZQOWxVWT+4Ea17OlBR 69 | k+AfU1vJsrS9yLAk4LqHc3Zx6P3kd1sVvb9+dkIvQwy189T+sc7f4karRg9msu/aoAuzNE 70 | LsaI9VieYN2eF6ET243G8SUA6rKSCpvGDicVEjbbYI8PAaEE8AAACBAMtSJsXF2cFaWOrd 71 | pBYI3ZsseI9mlLXCX1Y+P/6QBo7U51/Vw0vLjLgyHyVGveLH26Fr0/b07QWoI3XQSXA3ZO 72 | asXVVgiyAEsUaqxEr0NsqACTfYA3reHcIFD/FthDRYh5a5sXzBtRHeqDhsmV0Vj42YAqq2 73 | baewZMKBL1QECTolAAAADHJrb3NlZ2lAbDQ4MAECAwQFBg== 74 | -----END OPENSSH PRIVATE KEY----- 75 | -------------------------------------------------------------------------------- /src/test/resources/id_ed25519: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW 3 | QyNTUxOQAAACDcbIqDY9hFJb9+/yKhPpZZZnjTwyGLSmlnZirb5ps1YAAAAJACffNsAn3z 4 | bAAAAAtzc2gtZWQyNTUxOQAAACDcbIqDY9hFJb9+/yKhPpZZZnjTwyGLSmlnZirb5ps1YA 5 | AAAEAc7nxaUJgtIKaS0nNK+fTleBeF3o1kNajufRxpKIU0fNxsioNj2EUlv37/IqE+lllm 6 | eNPDIYtKaWdmKtvmmzVgAAAAB2plbmtpbnMBAgMEBQY= 7 | -----END OPENSSH PRIVATE KEY----- 8 | -------------------------------------------------------------------------------- /src/test/resources/id_ed25519.pub: -------------------------------------------------------------------------------- 1 | ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINxsioNj2EUlv37/IqE+lllmeNPDIYtKaWdmKtvmmzVg -------------------------------------------------------------------------------- /src/test/resources/id_rsa: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn 3 | NhAAAAAwEAAQAAAQEAyMm0501e8CnMGO72c5fihC//KhdOGahgvbCGbS5mOk4zfpvo+Jui 4 | 3hIJOOt6DW2atAabo8wUXLg9KH9cXHmKs6SMGuRd7m5fdTaUzO0G//ZmzX51phqSYv6JsU 5 | n/vKxqOeVtIWzXHuC+lOtEyR+BKA2UQJOTReO4dv8oAt9cBGSN4gOItc7s07ggK+ZD+S2u 6 | gSTpf5gXYnD9qq9TFuEyMFLJi9gWE/M3c/ZeeDyfTeVeYdbaq2qI3NNUtUYXB4Es0XsdzR 7 | ZSfMhPggJaw8jhlMYFnJE+8O2gJiWrvky1NYhouRD2QzdTCwDoG58AK2Epnd0wIE1iDAHU 8 | 6ZVQaj1BawAAA8iwi+/UsIvv1AAAAAdzc2gtcnNhAAABAQDIybTnTV7wKcwY7vZzl+KEL/ 9 | 8qF04ZqGC9sIZtLmY6TjN+m+j4m6LeEgk463oNbZq0BpujzBRcuD0of1xceYqzpIwa5F3u 10 | bl91NpTM7Qb/9mbNfnWmGpJi/omxSf+8rGo55W0hbNce4L6U60TJH4EoDZRAk5NF47h2/y 11 | gC31wEZI3iA4i1zuzTuCAr5kP5La6BJOl/mBdicP2qr1MW4TIwUsmL2BYT8zdz9l54PJ9N 12 | 5V5h1tqraojc01S1RhcHgSzRex3NFlJ8yE+CAlrDyOGUxgWckT7w7aAmJau+TLU1iGi5EP 13 | ZDN1MLAOgbnwArYSmd3TAgTWIMAdTplVBqPUFrAAAAAwEAAQAAAQEAhe/vad/1tZTcHcHB 14 | yqgFpRHzT1uOcJUeO0r20PwDm18xAIL2LGh9g09asYp6t1xmtzI1PlVTO+p2eX5D2TgGaw 15 | EXqJSvh+4+ZQ0Mw4pVggcW2ntB9ZSCE+Ehbo8jNfN5RLejTYmyElnvJ52tG9CVMmekflMz 16 | CYr3MQHR6eCfHBnbOgdMFiIyTgFliT1MAZBlWRtVJDQkr8DZMBjN1qoVHldLISl2nREvVt 17 | b6TM1gCCp3fuey6+pe5BOm5gn/FjxOXiOiBrmfAu0Wu5ITnblCeje9Y/z0dHJNgKxhKfuo 18 | hp78EE+fgHwVpUMeAunId9uRBwu8/u8eewtb7tMcYyK82QAAAIB8rhliWV/AOJqk+RhadS 19 | uF+640Zekk8dw0EFQiyYeK9IABi+WGs0+XTd3a0k/bUUM0jxxa2os1fSsnojOhMCYpLt5A 20 | UzmVWENG4xixscX0xdtJeYI91/Q7JuPmRbR2rGCL76WGyVnFvrKfpih1IgUKd9xkMT32WN 21 | yp/rSKab78sQAAAIEA/M+MJjP+v1bA/2efBD70Vp4JgtnrsHIP80ZQOWxVWT+4Ea17OlBR 22 | k+AfU1vJsrS9yLAk4LqHc3Zx6P3kd1sVvb9+dkIvQwy189T+sc7f4karRg9msu/aoAuzNE 23 | LsaI9VieYN2eF6ET243G8SUA6rKSCpvGDicVEjbbYI8PAaEE8AAACBAMtSJsXF2cFaWOrd 24 | pBYI3ZsseI9mlLXCX1Y+P/6QBo7U51/Vw0vLjLgyHyVGveLH26Fr0/b07QWoI3XQSXA3ZO 25 | asXVVgiyAEsUaqxEr0NsqACTfYA3reHcIFD/FthDRYh5a5sXzBtRHeqDhsmV0Vj42YAqq2 26 | baewZMKBL1QECTolAAAADHJrb3NlZ2lAbDQ4MAECAwQFBg== 27 | -----END OPENSSH PRIVATE KEY----- -------------------------------------------------------------------------------- /src/test/resources/id_rsa.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDIybTnTV7wKcwY7vZzl+KEL/8qF04ZqGC9sIZtLmY6TjN+m+j4m6LeEgk463oNbZq0BpujzBRcuD0of1xceYqzpIwa5F3ubl91NpTM7Qb/9mbNfnWmGpJi/omxSf+8rGo55W0hbNce4L6U60TJH4EoDZRAk5NF47h2/ygC31wEZI3iA4i1zuzTuCAr5kP5La6BJOl/mBdicP2qr1MW4TIwUsmL2BYT8zdz9l54PJ9N5V5h1tqraojc01S1RhcHgSzRex3NFlJ8yE+CAlrDyOGUxgWckT7w7aAmJau+TLU1iGi5EPZDN1MLAOgbnwArYSmd3TAgTWIMAdTplVBqPUFr --------------------------------------------------------------------------------