├── .github ├── release-drafter.yml ├── dependabot.yml └── workflows │ ├── cd.yaml │ └── jenkins-security-scan.yml ├── docs ├── add-cloud.png ├── add-token.png ├── server-detail.png ├── add-hcloud-button.png ├── add-log-recorder.png ├── server-template.png └── template.pkr.hcl ├── .mvn ├── maven.config └── extensions.xml ├── src ├── test │ ├── resources │ │ ├── id_ed25519.pub │ │ ├── id_rsa.pub │ │ ├── id_ed25519 │ │ ├── id_rsa │ │ └── cloud │ │ │ └── dnation │ │ │ └── jenkins │ │ │ └── plugins │ │ │ └── hetzner │ │ │ └── jcasc.yaml │ └── java │ │ └── cloud │ │ └── dnation │ │ └── jenkins │ │ └── plugins │ │ └── hetzner │ │ ├── TestHelper.java │ │ ├── launcher │ │ └── TestPublicV6AddressOnly.java │ │ ├── HetznerServerTemplateTest.java │ │ ├── HetznerCloudTest.java │ │ ├── JCasCTest.java │ │ ├── HetznerCloudResourceManagerTest.java │ │ ├── primaryip │ │ └── PrimaryIpStrategyTest.java │ │ ├── HelperTest.java │ │ └── HetznerCloudSimpleTest.java └── main │ ├── resources │ ├── cloud │ │ └── dnation │ │ │ └── jenkins │ │ │ └── plugins │ │ │ └── hetzner │ │ │ ├── HetznerServerTemplate │ │ │ ├── help-name.html │ │ │ ├── help-jvmOpts.html │ │ │ ├── help-automountVolumes.html │ │ │ ├── help-shutdownPolicy.html │ │ │ ├── help-connector.html │ │ │ ├── help-numExecutors.html │ │ │ ├── help-bootDeadline.html │ │ │ ├── help-userData.html │ │ │ ├── help-primaryIp.html │ │ │ ├── help-connectivity.html │ │ │ ├── help-remoteFs.html │ │ │ ├── help-volumeIds.html │ │ │ ├── help-prefix.html │ │ │ ├── help-labelStr.html │ │ │ ├── help-serverType.html │ │ │ ├── help-network.html │ │ │ ├── help-placementGroup.html │ │ │ ├── help-firewall.html │ │ │ ├── help-image.html │ │ │ ├── help-location.html │ │ │ └── config.jelly │ │ │ ├── launcher │ │ │ └── AbstractHetznerSshConnector │ │ │ │ ├── help-sshPort.html │ │ │ │ └── config.jelly │ │ │ ├── HetznerCloud │ │ │ ├── help-credentialsId.html │ │ │ ├── help-name.html │ │ │ └── config.jelly │ │ │ ├── primaryip │ │ │ ├── ByLabelSelectorFailing │ │ │ │ ├── help-selector.html │ │ │ │ └── config.jelly │ │ │ └── ByLabelSelectorIgnoring │ │ │ │ ├── help-selector.html │ │ │ │ └── config.jelly │ │ │ ├── shutdown │ │ │ ├── IdlePeriodPolicy │ │ │ │ ├── help-idleMinutes.html │ │ │ │ └── config.jelly │ │ │ └── BeforeHourWrapsPolicy │ │ │ │ └── help.html │ │ │ ├── HetznerServerAgent │ │ │ └── configure-entries.jelly │ │ │ ├── Messages.properties │ │ │ └── HetznerServerComputer │ │ │ └── main.jelly │ └── index.jelly │ └── java │ └── cloud │ └── dnation │ └── jenkins │ └── plugins │ └── hetzner │ ├── connect │ ├── ConnectivityType.java │ ├── AbstractConnectivity.java │ ├── Both.java │ ├── BothV6.java │ ├── PrivateOnly.java │ ├── PublicOnly.java │ └── PublicV6Only.java │ ├── HetznerServerInfo.java │ ├── launcher │ ├── AbstractConnectionMethod.java │ ├── DefaultSshConnector.java │ ├── SshConnectorAsRoot.java │ ├── PublicAddressOnly.java │ ├── DefaultV6ConnectionMethod.java │ ├── DefaultConnectionMethod.java │ ├── PublicV6AddressOnly.java │ ├── AbstractHetznerSshConnector.java │ └── HetznerServerComputerLauncher.java │ ├── shutdown │ ├── AbstractShutdownPolicy.java │ ├── IdlePeriodPolicy.java │ └── BeforeHourWrapsPolicy.java │ ├── HetznerServerComputer.java │ ├── primaryip │ ├── ByLabelSelectorFailing.java │ ├── ByLabelSelectorIgnoring.java │ ├── AbstractPrimaryIpStrategy.java │ ├── DefaultStrategy.java │ └── AbstractByLabelSelector.java │ ├── JenkinsSecretTokenProvider.java │ ├── OrphanedNodesCleaner.java │ ├── ControllerListener.java │ ├── HetznerConstants.java │ ├── NodeCallable.java │ ├── HetznerServerAgent.java │ ├── Helper.java │ ├── HetznerCloud.java │ └── HetznerServerTemplate.java ├── .gitignore ├── Jenkinsfile ├── pom.xml └── LICENSE /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | _extends: .github 2 | version-template: $MAJOR.$MINOR.$PATCH -------------------------------------------------------------------------------- /docs/add-cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/hetzner-cloud-plugin/HEAD/docs/add-cloud.png -------------------------------------------------------------------------------- /docs/add-token.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/hetzner-cloud-plugin/HEAD/docs/add-token.png -------------------------------------------------------------------------------- /.mvn/maven.config: -------------------------------------------------------------------------------- 1 | -Pmight-produce-incrementals 2 | -Pconsume-incrementals 3 | -Dchangelist.format=%d.v%s 4 | -------------------------------------------------------------------------------- /docs/server-detail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/hetzner-cloud-plugin/HEAD/docs/server-detail.png -------------------------------------------------------------------------------- /src/test/resources/id_ed25519.pub: -------------------------------------------------------------------------------- 1 | ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINxsioNj2EUlv37/IqE+lllmeNPDIYtKaWdmKtvmmzVg -------------------------------------------------------------------------------- /docs/add-hcloud-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/hetzner-cloud-plugin/HEAD/docs/add-hcloud-button.png -------------------------------------------------------------------------------- /docs/add-log-recorder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/hetzner-cloud-plugin/HEAD/docs/add-log-recorder.png -------------------------------------------------------------------------------- /docs/server-template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/hetzner-cloud-plugin/HEAD/docs/server-template.png -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/test/resources/id_rsa.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDIybTnTV7wKcwY7vZzl+KEL/8qF04ZqGC9sIZtLmY6TjN+m+j4m6LeEgk463oNbZq0BpujzBRcuD0of1xceYqzpIwa5F3ubl91NpTM7Qb/9mbNfnWmGpJi/omxSf+8rGo55W0hbNce4L6U60TJH4EoDZRAk5NF47h2/ygC31wEZI3iA4i1zuzTuCAr5kP5La6BJOl/mBdicP2qr1MW4TIwUsmL2BYT8zdz9l54PJ9N5V5h1tqraojc01S1RhcHgSzRex3NFlJ8yE+CAlrDyOGUxgWckT7w7aAmJau+TLU1iGi5EPZDN1MLAOgbnwArYSmd3TAgTWIMAdTplVBqPUFr -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.mvn/extensions.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | io.jenkins.tools.incrementals 4 | git-changelist-maven-extension 5 | 1.10 6 | 7 | 8 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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-jvmOpts.html: -------------------------------------------------------------------------------- 1 | 16 |
17 | Additional JVM options for agent. 18 |
19 | -------------------------------------------------------------------------------- /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-shutdownPolicy.html: -------------------------------------------------------------------------------- 1 | 16 |
17 | Defines how idle server is shutdown. 18 |
19 | -------------------------------------------------------------------------------- /src/main/resources/cloud/dnation/jenkins/plugins/hetzner/launcher/AbstractHetznerSshConnector/help-sshPort.html: -------------------------------------------------------------------------------- 1 | 16 |
17 | SSH port number for client connection. 18 |
19 | -------------------------------------------------------------------------------- /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/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-numExecutors.html: -------------------------------------------------------------------------------- 1 | 16 |
17 | Allows set the number of executors on provisioned server. 18 |
19 | -------------------------------------------------------------------------------- /src/main/resources/index.jelly: -------------------------------------------------------------------------------- 1 | 16 | 17 |
18 | This plugin provides integration with Hetzner Cloud 19 |
-------------------------------------------------------------------------------- /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/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/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/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-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-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-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-remoteFs.html: -------------------------------------------------------------------------------- 1 | 16 |
17 | Agent working directory. See here for more details. 18 |
19 | -------------------------------------------------------------------------------- /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/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/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/primaryip/ByLabelSelectorFailing/config.jelly: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /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/shutdown/IdlePeriodPolicy/config.jelly: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/main/resources/cloud/dnation/jenkins/plugins/hetzner/HetznerServerTemplate/help-prefix.html: -------------------------------------------------------------------------------- 1 | 16 |
17 | Optional prefix for node name. Must match regular expression ^[a-z][\w_-]+$. 18 |

19 | When omitted or if specified value does not match pattern above, then hcloud- will be used instead. 20 |

21 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/launcher/AbstractHetznerSshConnector/config.jelly: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /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-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-firewall.html: -------------------------------------------------------------------------------- 1 | 16 |
17 | Firewall label expression or firewall ID. 18 | When label expression is provided, it is expected that it will resolve to single firewall. 19 | Alternatively you can supply firewall ID. 20 | 21 | To obtain list of all firewalls, use following curl command: 22 |

23 |

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

27 | API documentation 28 |
29 | -------------------------------------------------------------------------------- /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/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/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/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 edu.umd.cs.findbugs.annotations.NonNull; 19 | import hudson.slaves.AbstractCloudComputer; 20 | import org.jenkinsci.plugins.cloudstats.ProvisioningActivity; 21 | import org.jenkinsci.plugins.cloudstats.TrackedItem; 22 | 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/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/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/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/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.SneakyThrows; 21 | import lombok.experimental.UtilityClass; 22 | 23 | import java.io.ByteArrayOutputStream; 24 | import java.io.InputStream; 25 | import java.nio.charset.StandardCharsets; 26 | 27 | @UtilityClass 28 | public class TestHelper { 29 | 30 | @SneakyThrows 31 | public static String inputStreamAsString(InputStream is) { 32 | ByteArrayOutputStream os = new ByteArrayOutputStream(); 33 | ByteStreams.copy(is, os); 34 | return os.toString(StandardCharsets.UTF_8); 35 | } 36 | 37 | @SneakyThrows 38 | public static String resourceAsString(String name) { 39 | try (InputStream is = Resources.getResource(name).openStream()) { 40 | return inputStreamAsString(is); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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/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/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.jupiter.api.Test; 23 | 24 | import static org.junit.jupiter.api.Assertions.assertEquals; 25 | import static org.junit.jupiter.api.Assertions.assertThrows; 26 | 27 | class TestPublicV6AddressOnly { 28 | 29 | @Test 30 | void testMissingV6Address() { 31 | final PublicV6AddressOnly addr = new PublicV6AddressOnly(); 32 | assertThrows(IllegalArgumentException.class, () -> 33 | addr.getAddress(new ServerDetail().publicNet(new PublicNetDetail().ipv4(new Ipv4Detail())))); 34 | } 35 | 36 | @Test 37 | void testValid() { 38 | final PublicV6AddressOnly addr = new PublicV6AddressOnly(); 39 | final String res = addr.getAddress(new ServerDetail().publicNet( 40 | new PublicNetDetail().ipv6(new Ipv6Detail().ip("2a01:4e3:a0a:9b7b::/64")))); 41 | assertEquals("2a01:4e3:a0a:9b7b::1", res); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /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/java/cloud/dnation/jenkins/plugins/hetzner/HetznerServerTemplateTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 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 lombok.extern.slf4j.Slf4j; 19 | import org.junit.jupiter.api.BeforeEach; 20 | import org.junit.jupiter.api.Test; 21 | import org.jvnet.hudson.test.JenkinsRule; 22 | import org.jvnet.hudson.test.junit.jupiter.WithJenkins; 23 | 24 | import static org.junit.jupiter.api.Assertions.assertFalse; 25 | import static org.junit.jupiter.api.Assertions.assertTrue; 26 | 27 | @WithJenkins 28 | @Slf4j 29 | public class HetznerServerTemplateTest { 30 | private JenkinsRule j; 31 | 32 | @BeforeEach 33 | void setUp(JenkinsRule rule) { 34 | j = rule; 35 | } 36 | @Test 37 | public void testValidPrefix() { 38 | var tmpl = new HetznerServerTemplate("my-template-1", "lbl", "img", "loc", "cx12"); 39 | tmpl.setPrefix("myprefix1"); 40 | assertTrue(tmpl.isPrefixValid()); 41 | assertTrue(tmpl.generateNodeName().startsWith("myprefix1-")); 42 | tmpl.setPrefix("0"); 43 | assertFalse(tmpl.isPrefixValid()); 44 | assertTrue(tmpl.generateNodeName().startsWith("hcloud-")); 45 | tmpl.setPrefix(""); 46 | assertFalse(tmpl.isPrefixValid()); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /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/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/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 hudson.security.ACL; 21 | import jenkins.model.Jenkins; 22 | import org.jenkinsci.plugins.plaincredentials.StringCredentials; 23 | 24 | import java.util.function.Supplier; 25 | 26 | public class JenkinsSecretTokenProvider implements Supplier { 27 | private final String credentialsId; 28 | 29 | private JenkinsSecretTokenProvider(String credentialsId) { 30 | this.credentialsId = credentialsId; 31 | } 32 | 33 | public static JenkinsSecretTokenProvider forCredentialsId(String credentialsId) { 34 | return new JenkinsSecretTokenProvider(credentialsId); 35 | } 36 | 37 | @Override 38 | public String get() { 39 | final StringCredentials secret = CredentialsMatchers.firstOrNull( 40 | CredentialsProvider.lookupCredentialsInItemGroup(StringCredentials.class, Jenkins.get(), ACL.SYSTEM2), 41 | CredentialsMatchers.withId(credentialsId)); 42 | if (secret == null) { 43 | throw new IllegalStateException("Can't find credentials with ID '" + credentialsId + "'"); 44 | } 45 | return secret.getSecret().getPlainText(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /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/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/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.jupiter.api.BeforeEach; 21 | import org.junit.jupiter.api.Test; 22 | import org.jvnet.hudson.test.JenkinsRule; 23 | 24 | import java.util.ArrayList; 25 | import org.jvnet.hudson.test.junit.jupiter.WithJenkins; 26 | 27 | import static org.junit.jupiter.api.Assertions.assertNotNull; 28 | import static org.junit.jupiter.api.Assertions.assertTrue; 29 | 30 | @WithJenkins 31 | class HetznerCloudTest { 32 | 33 | private JenkinsRule j; 34 | 35 | @BeforeEach 36 | void setUp(JenkinsRule rule) { 37 | j = rule; 38 | } 39 | 40 | @Test 41 | void test() throws Exception { 42 | final HetznerCloud cloud = new HetznerCloud("hcloud-01", "mock-credentials", "10", 43 | new ArrayList<>()); 44 | j.jenkins.clouds.add(cloud); 45 | j.jenkins.save(); 46 | try (JenkinsRule.WebClient wc = j.createWebClient()) { 47 | HtmlPage p = wc.goTo("manage/cloud/"); 48 | DomElement domElement = p.getElementById("cloud_" + cloud.name); 49 | assertNotNull(domElement); 50 | p = wc.goTo("manage/cloud/hcloud-01/configure"); 51 | assertTrue(p.getElementsByTagName("input").stream() 52 | .filter(element -> element.hasAttribute("value")) 53 | .anyMatch(element -> cloud.name.equals(element.getAttribute("value"))), "No input with value " + cloud.name); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /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/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.misc.junit.jupiter.WithJenkinsConfiguredWithCode; 23 | import io.jenkins.plugins.casc.model.CNode; 24 | import jenkins.model.Jenkins; 25 | import org.junit.jupiter.api.Test; 26 | 27 | import static io.jenkins.plugins.casc.misc.Util.getJenkinsRoot; 28 | import static org.junit.jupiter.api.Assertions.assertEquals; 29 | import static org.junit.jupiter.api.Assertions.assertNotNull; 30 | 31 | @WithJenkinsConfiguredWithCode 32 | class JCasCTest { 33 | 34 | @Test 35 | @ConfiguredWithCode("jcasc.yaml") 36 | void testConfigure(JenkinsConfiguredWithCodeRule j) { 37 | final HetznerCloud cloud = (HetznerCloud) Jenkins.get().clouds.getByName("hcloud-01"); 38 | assertNotNull(cloud); 39 | assertEquals("hcloud-01", cloud.getDisplayName()); 40 | assertEquals(10, cloud.getInstanceCap()); 41 | assertEquals(1, cloud.getServerTemplates().size()); 42 | assertEquals("name=jenkins", cloud.getServerTemplates().get(0).getImage()); 43 | assertEquals("fsn1", cloud.getServerTemplates().get(0).getLocation()); 44 | } 45 | 46 | @Test 47 | @ConfiguredWithCode("jcasc.yaml") 48 | void testExport(JenkinsConfiguredWithCodeRule j) throws Exception { 49 | final ConfigurationContext ctx = new ConfigurationContext(ConfiguratorRegistry.get()); 50 | final CNode cloud = getJenkinsRoot(ctx).get("clouds"); 51 | assertNotNull(cloud); 52 | } 53 | } 54 | 55 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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.jupiter.api.Test; 21 | 22 | import static cloud.dnation.jenkins.plugins.hetzner.HetznerCloudResourceManager.customizeNetworking; 23 | import static org.junit.jupiter.api.Assertions.assertEquals; 24 | 25 | class HetznerCloudResourceManagerTest { 26 | 27 | @Test 28 | void testCustomizeNetworking() throws Exception { 29 | CreateServerRequest req; 30 | 31 | req = new CreateServerRequest(); 32 | customizeNetworking(ConnectivityType.BOTH, req, "", (s1, s2) -> { 33 | }); 34 | assertEquals(true, req.getPublicNet().getEnableIpv4()); 35 | assertEquals(true, req.getPublicNet().getEnableIpv6()); 36 | 37 | req = new CreateServerRequest(); 38 | customizeNetworking(ConnectivityType.BOTH_V6, req, "", (s1, s2) -> { 39 | }); 40 | assertEquals(false, req.getPublicNet().getEnableIpv4()); 41 | assertEquals(true, req.getPublicNet().getEnableIpv6()); 42 | 43 | req = new CreateServerRequest(); 44 | customizeNetworking(ConnectivityType.PUBLIC, req, "", (s1, s2) -> { 45 | }); 46 | assertEquals(true, req.getPublicNet().getEnableIpv4()); 47 | assertEquals(true, req.getPublicNet().getEnableIpv6()); 48 | 49 | req = new CreateServerRequest(); 50 | customizeNetworking(ConnectivityType.PUBLIC_V6, req, "", (s1, s2) -> { 51 | }); 52 | assertEquals(false, req.getPublicNet().getEnableIpv4()); 53 | assertEquals(true, req.getPublicNet().getEnableIpv6()); 54 | 55 | req = new CreateServerRequest(); 56 | customizeNetworking(ConnectivityType.PRIVATE, req, "", (s1, s2) -> { 57 | }); 58 | assertEquals(false, req.getPublicNet().getEnableIpv4()); 59 | assertEquals(false, req.getPublicNet().getEnableIpv6()); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /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.PagedResourceHelper; 21 | import cloud.dnation.hetznerclient.PrimaryIpDetail; 22 | import cloud.dnation.hetznerclient.PublicNetRequest; 23 | import com.google.common.annotations.VisibleForTesting; 24 | import com.google.common.base.Strings; 25 | import lombok.Getter; 26 | import lombok.extern.slf4j.Slf4j; 27 | 28 | import java.io.IOException; 29 | 30 | @Slf4j 31 | public abstract class AbstractByLabelSelector extends AbstractPrimaryIpStrategy { 32 | @Getter 33 | private final String selector; 34 | 35 | public AbstractByLabelSelector(boolean failIfError, String selector) { 36 | super(failIfError); 37 | this.selector = selector; 38 | } 39 | 40 | @Override 41 | public void applyInternal(HetznerApi api, CreateServerRequest server) throws IOException { 42 | final PrimaryIpDetail pip = PagedResourceHelper.getAllPrimaryIps(api, selector).stream() 43 | .filter(ip -> isIpUsable(ip, server)).findFirst().get(); 44 | final PublicNetRequest net = new PublicNetRequest(); 45 | net.setIpv4(pip.getId()); 46 | net.setEnableIpv6(false); 47 | net.setEnableIpv4(true); 48 | server.setPublicNet(net); 49 | } 50 | 51 | @VisibleForTesting 52 | static boolean isIpUsable(PrimaryIpDetail ip, CreateServerRequest server) { 53 | if (ip.getAssigneeId() != null) { 54 | return false; 55 | } 56 | if (!Strings.isNullOrEmpty(server.getLocation())) { 57 | if (server.getLocation().equals(ip.getDatacenter().getLocation().getName())) { 58 | return true; 59 | } 60 | } 61 | if (!Strings.isNullOrEmpty(server.getDatacenter())) { 62 | return server.getDatacenter().equals(ip.getDatacenter().getName()); 63 | } 64 | return false; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /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 | .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/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.jupiter.api.Test; 23 | 24 | import static cloud.dnation.jenkins.plugins.hetzner.primaryip.AbstractByLabelSelector.isIpUsable; 25 | import static org.junit.jupiter.api.Assertions.assertFalse; 26 | import static org.junit.jupiter.api.Assertions.assertTrue; 27 | 28 | class PrimaryIpStrategyTest { 29 | 30 | private static final DatacenterDetail FSN1DC14; 31 | private static final DatacenterDetail NBG1DC4; 32 | private static final LocationDetail FSN1; 33 | private static final LocationDetail NBG1; 34 | 35 | static { 36 | FSN1 = new LocationDetail(); 37 | FSN1.setName("fsn1"); 38 | FSN1DC14 = new DatacenterDetail(); 39 | FSN1DC14.setName("fsn1-dc14"); 40 | FSN1DC14.setLocation(FSN1); 41 | NBG1 = new LocationDetail(); 42 | NBG1.setName("nbg1"); 43 | NBG1DC4 = new DatacenterDetail(); 44 | NBG1DC4.setName("nbg1-dc3"); 45 | NBG1DC4.setLocation(NBG1); 46 | } 47 | 48 | @Test 49 | void testIpIsUsable() { 50 | final CreateServerRequest server = new CreateServerRequest(); 51 | final PrimaryIpDetail ip = new PrimaryIpDetail(); 52 | //Same datacenter 53 | server.setDatacenter(FSN1DC14.getName()); 54 | ip.setDatacenter(FSN1DC14); 55 | assertTrue(isIpUsable(ip, server)); 56 | 57 | //Same location 58 | server.setDatacenter(null); 59 | server.setLocation("fsn1"); 60 | assertTrue(isIpUsable(ip, server)); 61 | 62 | //Different datacenter 63 | ip.setDatacenter(NBG1DC4); 64 | server.setDatacenter(FSN1DC14.getName()); 65 | server.setLocation(null); 66 | assertFalse(isIpUsable(ip, server)); 67 | 68 | //Already allocated 69 | ip.setAssigneeId(0L); 70 | assertFalse(isIpUsable(ip, server)); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /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 (c.getName().isEmpty()) { 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 (c.getName().isEmpty()) { 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/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(@NonNull AbstractCloudComputer c) { 54 | c.connect(false); 55 | } 56 | 57 | @Override 58 | @GuardedBy("hudson.model.Queue.lock") 59 | public long check(@NonNull 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/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/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/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.jupiter.api.Test; 19 | 20 | import java.time.LocalDateTime; 21 | import java.time.format.DateTimeFormatter; 22 | 23 | import static org.junit.jupiter.api.Assertions.assertEquals; 24 | import static org.junit.jupiter.api.Assertions.assertFalse; 25 | import static org.junit.jupiter.api.Assertions.assertTrue; 26 | 27 | class HelperTest { 28 | 29 | @Test 30 | void testExtractPublicKeyRSA() throws Exception { 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 | void testExtractPublicKeyED25519() throws Exception { 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 | 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 | void testIsPossiblyLong() { 71 | assertTrue(Helper.isPossiblyLong("1")); 72 | assertFalse(Helper.isPossiblyLong("0")); 73 | assertFalse(Helper.isPossiblyLong("not-a-number")); 74 | } 75 | 76 | @Test 77 | 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/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/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 edu.umd.cs.findbugs.annotations.Nullable; 23 | import hudson.model.AbstractDescribableImpl; 24 | import hudson.model.Descriptor; 25 | import hudson.model.Item; 26 | import hudson.security.ACL; 27 | import hudson.util.FormValidation; 28 | import hudson.util.ListBoxModel; 29 | import jenkins.model.Jenkins; 30 | import lombok.Getter; 31 | import lombok.Setter; 32 | import lombok.extern.slf4j.Slf4j; 33 | import org.kohsuke.accmod.Restricted; 34 | import org.kohsuke.accmod.restrictions.NoExternalUse; 35 | import org.kohsuke.stapler.AncestorInPath; 36 | import org.kohsuke.stapler.DataBoundSetter; 37 | import org.kohsuke.stapler.QueryParameter; 38 | import org.kohsuke.stapler.interceptor.RequirePOST; 39 | 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 int sshPort = 22; 61 | 62 | @Getter 63 | @Setter(onMethod = @__({@DataBoundSetter})) 64 | protected AbstractConnectionMethod connectionMethod = DefaultConnectionMethod.SINGLETON; 65 | 66 | public HetznerServerComputerLauncher createLauncher() { 67 | return new HetznerServerComputerLauncher(this); 68 | } 69 | 70 | protected Object readResolve() { 71 | if (sshPort == 0) { 72 | sshPort = 22; 73 | } 74 | return this; 75 | } 76 | 77 | public static abstract class DescriptorImpl extends Descriptor { 78 | // this method does not have any side effect, nor does it read any state. 79 | @SuppressWarnings("lgtm[jenkins/no-permission-check]") 80 | @Restricted(NoExternalUse.class) 81 | @RequirePOST 82 | public FormValidation doCheckSshCredentialsId(@QueryParameter String sshCredentialsId) { 83 | return doCheckNonEmpty(sshCredentialsId, "SSH credentials"); 84 | } 85 | 86 | @Restricted(NoExternalUse.class) 87 | @RequirePOST 88 | public ListBoxModel doFillSshCredentialsIdItems(@AncestorInPath Item owner) { 89 | final StandardListBoxModel result = new StandardListBoxModel(); 90 | if (owner == null) { 91 | if (!Jenkins.get().hasPermission(Jenkins.ADMINISTER)) { 92 | return result; 93 | } 94 | } else { 95 | if (!owner.hasPermission(Item.EXTENDED_READ) 96 | && !owner.hasPermission(CredentialsProvider.USE_ITEM)) { 97 | return result; 98 | } 99 | } 100 | return new StandardListBoxModel() 101 | .includeEmptyValue() 102 | .includeMatchingAs(ACL.SYSTEM2, owner, BasicSSHUserPrivateKey.class, 103 | Collections.emptyList(), CredentialsMatchers.always()); 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /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 | 50 | 51 | 52 | 53 | 54 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /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/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 java.io.Serial; 29 | import lombok.AccessLevel; 30 | import lombok.Getter; 31 | import lombok.Setter; 32 | import lombok.extern.slf4j.Slf4j; 33 | import org.jenkinsci.plugins.cloudstats.CloudStatistics; 34 | import org.jenkinsci.plugins.cloudstats.ProvisioningActivity; 35 | import org.jenkinsci.plugins.cloudstats.TrackedItem; 36 | 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 | @Serial 44 | private static final long serialVersionUID = 1; 45 | private final ProvisioningActivity.Id provisioningId; 46 | @Getter 47 | private final transient HetznerCloud cloud; 48 | @Getter 49 | @NonNull 50 | private final transient HetznerServerTemplate template; 51 | @Getter(AccessLevel.PUBLIC) 52 | @Setter(AccessLevel.PACKAGE) 53 | private transient HetznerServerInfo serverInstance; 54 | 55 | public HetznerServerAgent(@NonNull ProvisioningActivity.Id provisioningId, 56 | @NonNull String name, String remoteFS, ComputerLauncher launcher, 57 | @NonNull HetznerCloud cloud, @NonNull HetznerServerTemplate template) 58 | throws IOException, Descriptor.FormException { 59 | super(name, remoteFS, launcher); 60 | this.cloud = Objects.requireNonNull(cloud); 61 | this.template = Objects.requireNonNull(template); 62 | this.provisioningId = Objects.requireNonNull(provisioningId); 63 | setLabelString(template.getLabelStr()); 64 | setNumExecutors(template.getNumExecutors()); 65 | setMode(template.getMode() == null ? Mode.EXCLUSIVE : template.getMode()); 66 | setRetentionStrategy(template.getShutdownPolicy().getRetentionStrategy()); 67 | readResolve(); 68 | } 69 | 70 | @SuppressWarnings("rawtypes") 71 | @Override 72 | public AbstractCloudComputer createComputer() { 73 | return new HetznerServerComputer(this); 74 | } 75 | 76 | @Override 77 | public String getDisplayName() { 78 | if (serverInstance != null && serverInstance.getServerDetail() != null) { 79 | return getNodeName() + " in " + serverInstance.getServerDetail().getDatacenter() 80 | .getLocation().getDescription(); 81 | } 82 | return super.getDisplayName(); 83 | } 84 | 85 | @Override 86 | protected void _terminate(TaskListener listener) { 87 | ((HetznerServerComputerLauncher) getLauncher()).signalTermination(); 88 | cloud.getResourceManager().destroyServer(serverInstance.getServerDetail()); 89 | Optional.ofNullable(CloudStatistics.get().getActivityFor(this)) 90 | .ifPresent(a -> a.enterIfNotAlready(ProvisioningActivity.Phase.COMPLETED)); 91 | } 92 | 93 | @Override 94 | public Node asNode() { 95 | return this; 96 | } 97 | 98 | @NonNull 99 | @Override 100 | public ProvisioningActivity.Id getId() { 101 | return provisioningId; 102 | } 103 | 104 | /** 105 | * Check if server associated with this agent is running. 106 | * 107 | * @return true if status of server is "running", false otherwise 108 | */ 109 | public boolean isAlive() { 110 | serverInstance = cloud.getResourceManager().refreshServerInfo(serverInstance); 111 | return serverInstance.getServerDetail().getStatus().equals("running"); 112 | } 113 | 114 | @SuppressWarnings("unused") 115 | @Extension 116 | public static final class DescriptorImpl extends SlaveDescriptor { 117 | @NonNull 118 | @Override 119 | public String getDisplayName() { 120 | return Messages.plugin_displayName(); 121 | } 122 | 123 | public boolean isInstantiable() { 124 | return false; 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /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.jupiter.api.AfterEach; 28 | import org.junit.jupiter.api.BeforeEach; 29 | import org.junit.jupiter.api.Test; 30 | import org.mockito.MockedStatic; 31 | import org.mockito.stubbing.Answer; 32 | 33 | import java.util.Collection; 34 | import java.util.concurrent.TimeUnit; 35 | 36 | import static org.awaitility.Awaitility.await; 37 | import static org.junit.jupiter.api.Assertions.assertFalse; 38 | import static org.junit.jupiter.api.Assertions.assertTrue; 39 | import static org.mockito.ArgumentMatchers.anyString; 40 | import static org.mockito.Mockito.doAnswer; 41 | import static org.mockito.Mockito.mock; 42 | import static org.mockito.Mockito.mockStatic; 43 | import static org.mockito.Mockito.times; 44 | import static org.mockito.Mockito.verify; 45 | import static org.mockito.Mockito.when; 46 | 47 | @Slf4j 48 | class HetznerCloudSimpleTest { 49 | 50 | private HetznerCloudResourceManager rsrcMgr; 51 | 52 | private MockedStatic jenkinsMock; 53 | private MockedStatic hetznerCloudResourceManagerMockedStatic; 54 | 55 | @BeforeEach 56 | void setUp() { 57 | jenkinsMock = mockStatic(Jenkins.class); 58 | hetznerCloudResourceManagerMockedStatic = mockStatic(HetznerCloudResourceManager.class); 59 | 60 | rsrcMgr = mock(HetznerCloudResourceManager.class); 61 | when(HetznerCloudResourceManager.create(anyString())).thenReturn(rsrcMgr); 62 | 63 | Jenkins jenkins = mock(Jenkins.class); 64 | doAnswer((Answer) invocationOnMock -> new LabelAtom(invocationOnMock.getArgument(0))) 65 | .when(jenkins).getLabelAtom(anyString()); 66 | when(Jenkins.get()).thenReturn(jenkins); 67 | } 68 | 69 | @AfterEach 70 | void tearDown() { 71 | jenkinsMock.close(); 72 | hetznerCloudResourceManagerMockedStatic.close(); 73 | } 74 | 75 | @Test 76 | void testCanProvision() throws Exception { 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 | 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 | 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 | 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 | -------------------------------------------------------------------------------- /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 | 5.21 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.479 55 | ${jenkins.baseline}.3 56 | false 57 | 58 | 59 | 60 | maven.jenkins-ci.org 61 | https://repo.jenkins-ci.org/releases 62 | 63 | 64 | 65 | 66 | repo.jenkins-ci.org 67 | https://repo.jenkins-ci.org/public/ 68 | 69 | 70 | 71 | 72 | repo.jenkins-ci.org 73 | https://repo.jenkins-ci.org/public/ 74 | 75 | 76 | 77 | 78 | 79 | io.jenkins.tools.bom 80 | bom-${jenkins.baseline}.x 81 | 5015.vb_52d36583443 82 | pom 83 | import 84 | 85 | 86 | 87 | 88 | 89 | 90 | org.apache.maven.plugins 91 | maven-javadoc-plugin 92 | 93 | false 94 | 95 | 96 | 97 | 98 | 99 | 100 | org.jenkins-ci.plugins 101 | ssh-slaves 102 | 103 | 104 | org.jenkins-ci.plugins 105 | ssh-credentials 106 | 107 | 108 | org.jenkins-ci.plugins 109 | credentials 110 | 111 | 112 | org.jenkins-ci.plugins 113 | plain-credentials 114 | 115 | 116 | org.projectlombok 117 | lombok 118 | 1.18.32 119 | provided 120 | 121 | 122 | org.jenkins-ci.plugins 123 | bouncycastle-api 124 | 125 | 126 | io.jenkins.plugins 127 | gson-api 128 | 129 | 130 | io.jenkins.plugins 131 | okhttp-api 132 | 133 | 134 | cloud.dnation.integration 135 | hetzner-cloud-client-java 136 | 1.10.0 137 | 138 | 139 | com.github.spotbugs 140 | spotbugs-annotations 141 | 142 | 143 | com.google.errorprone 144 | error_prone_annotations 145 | 146 | 147 | com.google.guava 148 | failureaccess 149 | 150 | 151 | com.google.guava 152 | guava 153 | 154 | 155 | com.google.guava 156 | listenablefuture 157 | 158 | 159 | org.slf4j 160 | slf4j-api 161 | 162 | 163 | 164 | 165 | org.jenkins-ci.plugins 166 | trilead-api 167 | 168 | 169 | io.jenkins.plugins 170 | eddsa-api 171 | 172 | 173 | org.jenkins-ci.plugins 174 | cloud-stats 175 | 176 | 177 | org.mockito 178 | mockito-core 179 | test 180 | 181 | 182 | io.jenkins.configuration-as-code 183 | test-harness 184 | test 185 | 186 | 187 | org.awaitility 188 | awaitility 189 | 4.3.0 190 | test 191 | 192 | 193 | org.hamcrest 194 | hamcrest 195 | 196 | 197 | 198 | 199 | 200 | -------------------------------------------------------------------------------- /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 edu.umd.cs.findbugs.annotations.NonNull; 25 | import edu.umd.cs.findbugs.annotations.Nullable; 26 | import hudson.security.ACL; 27 | import jenkins.model.Jenkins; 28 | import lombok.Getter; 29 | import lombok.RequiredArgsConstructor; 30 | import lombok.experimental.UtilityClass; 31 | import net.i2p.crypto.eddsa.EdDSAPublicKey; 32 | import org.bouncycastle.crypto.params.AsymmetricKeyParameter; 33 | import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters; 34 | import org.bouncycastle.crypto.params.RSAKeyParameters; 35 | import org.bouncycastle.crypto.util.OpenSSHPublicKeyUtil; 36 | import org.slf4j.Logger; 37 | import retrofit2.Response; 38 | 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 98 | */ 99 | public static boolean isLabelExpression(String expression) { 100 | return expression.contains("="); 101 | } 102 | 103 | /** 104 | * Check if given string could be parsed as positive long. 105 | * 106 | * @param str string to check 107 | * @return true if given string could be parsed as positive long, false otherwise 108 | */ 109 | public static boolean isPossiblyLong(String str) { 110 | try { 111 | final long value = Long.parseLong(str); 112 | return value > 0; 113 | } catch (NumberFormatException e) { 114 | return false; 115 | } 116 | } 117 | 118 | public static List idList(String str) { 119 | return Arrays.stream(str.split(",")).map(Long::parseLong).toList(); 120 | } 121 | 122 | public static List getPayload(@NonNull Response response, @NonNull Function> mapper) { 123 | final T body = response.body(); 124 | if (body == null) { 125 | return Collections.emptyList(); 126 | } 127 | return Optional.ofNullable(mapper.apply(body)).orElse(Collections.emptyList()); 128 | } 129 | 130 | public static E assertValidResponse(Response response, Function mapper) { 131 | Preconditions.checkState(response.isSuccessful(), "Invalid API response : %s", 132 | response.code()); 133 | return mapper.apply(response.body()); 134 | } 135 | 136 | public static void assertValidResponse(Response response) { 137 | assertValidResponse(response, (Function) t -> null); 138 | } 139 | 140 | public static BasicSSHUserPrivateKey assertSshKey(String credentialsId) { 141 | final BasicSSHUserPrivateKey privateKey = CredentialsMatchers.firstOrNull( 142 | CredentialsProvider.lookupCredentialsInItemGroup(BasicSSHUserPrivateKey.class, Jenkins.get(), ACL.SYSTEM2, 143 | Collections.emptyList()), 144 | CredentialsMatchers.withId(credentialsId)); 145 | 146 | Preconditions.checkState(privateKey != null, 147 | "No SSH credentials found with ID '%s'", credentialsId); 148 | 149 | return privateKey; 150 | } 151 | 152 | public static String getStringOrDefault(String value, String defValue) { 153 | if(Strings.isNullOrEmpty(value)) { 154 | return defValue; 155 | } 156 | return value; 157 | } 158 | 159 | @RequiredArgsConstructor 160 | public static class LogAdapter { 161 | private static final SimpleFormatter FORMATTER = new SimpleFormatter(); 162 | @Getter 163 | private final PrintStream stream; 164 | private final Logger logger; 165 | 166 | public void info(String message) { 167 | logger.info(message); 168 | final LogRecord rec = new LogRecord(Level.INFO, message); 169 | rec.setLoggerName(logger.getName()); 170 | stream.println(FORMATTER.format(rec)); 171 | } 172 | 173 | public void error(String message, Throwable cause) { 174 | logger.error(message, cause); 175 | final LogRecord rec = new LogRecord(Level.SEVERE, message + " Cause: " + cause); 176 | rec.setLoggerName(logger.getName()); 177 | rec.setThrown(cause); 178 | stream.println(FORMATTER.format(rec)); 179 | } 180 | } 181 | 182 | /** 183 | * Check if idle server can be shut down. 184 | *

185 | * According to Hetzner billing policy, 186 | * you are billed for every hour of existence of server, so it makes sense to keep server running as long as next hour did 187 | * not start yet. 188 | * 189 | * @param createdStr RFC3339-formatted instant when server was created. See ServerDetail#getCreated(). 190 | * @param currentTime current time. Kept as argument to allow unit-testing. 191 | * @return true if server should be shut down, false otherwise. 192 | * Note: we keep small time buffer for corner cases like clock skew or Jenkins's queue manager overload, which could 193 | * lead to unnecessary 1-hour over-billing. 194 | */ 195 | public static boolean canShutdownServer(@NonNull String createdStr, LocalDateTime currentTime) { 196 | final LocalDateTime created = LocalDateTime.from(DateTimeFormatter.ISO_DATE_TIME.parse(createdStr)) 197 | .atOffset(ZoneOffset.UTC).toLocalDateTime(); 198 | long diff = Duration.between(created, currentTime.atOffset(ZoneOffset.UTC).toLocalDateTime()).toMinutes() % 60; 199 | return (60 - SHUTDOWN_TIME_BUFFER) <= diff; 200 | } 201 | 202 | /** 203 | * Get all nodes that are {@link HetznerServerAgent}. 204 | * 205 | * @return list of all {@link HetznerServerAgent} nodes 206 | */ 207 | public static List getHetznerAgents() { 208 | return Jenkins.get().getNodes() 209 | .stream() 210 | .filter(HetznerServerAgent.class::isInstance) 211 | .map(HetznerServerAgent.class::cast) 212 | .collect(Collectors.toList()); 213 | } 214 | 215 | public static boolean isValidLabelValue(String value) { 216 | if (Strings.isNullOrEmpty(value)) { 217 | return false; 218 | } 219 | return LABEL_VALUE_RE.matcher(value).matches(); 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /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 | @Override 85 | @SuppressFBWarnings(value = "NP_NULL_PARAM_DEREF") 86 | public void launch(final SlaveComputer computer, TaskListener listener) throws IOException, InterruptedException { 87 | if (!(computer instanceof HetznerServerComputer hcomputer)) { 88 | throw new AbortException("Incompatible computer : " + computer); 89 | } 90 | if (connector.getConnectionMethod() == null) { 91 | connector.setConnectionMethod(HetznerConstants.DEFAULT_CONNECTION_METHOD); 92 | } 93 | if (connector.getSshPort() == 0) { 94 | connector.setSshPort(22); 95 | } 96 | final Helper.LogAdapter logger = new Helper.LogAdapter(listener.getLogger(), log); 97 | final HetznerServerAgent node = hcomputer.getNode(); 98 | Preconditions.checkState(node != null && node.getServerInstance() != null, 99 | "Missing node or server instance data in computer %s", computer.getName()); 100 | final String remoteFs = getRemoteFs(node); 101 | final Connection connection = setupConnection(node, logger, listener); 102 | copyAgent(connection, hcomputer, logger, remoteFs); 103 | launchAgent(connection, hcomputer, logger, listener, remoteFs); 104 | } 105 | 106 | @SuppressFBWarnings(value = "NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE", 107 | justification = "NULLnes of node is checked in launch method") 108 | private String getAgentCommand(HetznerServerComputer computer, String remoteFs) { 109 | final String jvmOpts = Util.fixNull(computer.getNode().getTemplate().getJvmOpts()); 110 | return "java " + jvmOpts + " -jar " + remoteFs + "/remoting.jar -workDir " + remoteFs; 111 | } 112 | 113 | private void launchAgent(Connection connection, 114 | HetznerServerComputer computer, 115 | Helper.LogAdapter logger, 116 | TaskListener listener, 117 | String remoteFs 118 | ) 119 | throws IOException, InterruptedException { 120 | final HetznerServerAgent node = computer.getNode(); 121 | final Session session = connection.openSession(); 122 | final String scriptCmd = "/bin/sh " + remoteFs + "/" + AGENT_SCRIPT; 123 | final String launchCmd; 124 | final String username = connector.getUsernameOverride(); 125 | if (username != null) { 126 | final String credentialsId = node.getTemplate().getConnector().getSshCredentialsId(); 127 | final BasicSSHUserPrivateKey privateKey = assertSshKey(credentialsId); 128 | launchCmd = "sudo -n -u " + privateKey.getUsername() + " " + scriptCmd; 129 | } else { 130 | launchCmd = scriptCmd; 131 | } 132 | 133 | logger.info("Launching agent using '" + launchCmd + "'"); 134 | session.execCommand(launchCmd); 135 | computer.setChannel(session.getStdout(), session.getStdin(), listener, new Channel.Listener() { 136 | @Override 137 | public void onClosed(Channel channel, IOException cause) { 138 | session.close(); 139 | connection.close(); 140 | } 141 | }); 142 | } 143 | 144 | private Connection setupConnection(HetznerServerAgent node, 145 | Helper.LogAdapter logger, 146 | TaskListener taskListener) throws InterruptedException, AbortException { 147 | int retries = 10; 148 | while (!terminated.get() && retries-- > 0) { 149 | final ServerDetail serverDetail = node.getServerInstance().getServerDetail(); 150 | final String ipv4 = connector.getConnectionMethod().getAddress(serverDetail); 151 | final int port = connector.getSshPort(); 152 | final Connection conn = new Connection(ipv4, port); 153 | try { 154 | conn.connect(AllowAnyServerHostKeyVerifier.INSTANCE, 155 | 30_000, 10_000); 156 | logger.info("Connected to " + node.getNodeName() + " via " + ipv4 + ":" + port); 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/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 edu.umd.cs.findbugs.annotations.NonNull; 23 | import hudson.Extension; 24 | import hudson.model.Computer; 25 | import hudson.model.Descriptor; 26 | import hudson.model.Item; 27 | import hudson.model.Label; 28 | import hudson.model.Node; 29 | import hudson.security.ACL; 30 | import hudson.slaves.AbstractCloudImpl; 31 | import hudson.slaves.Cloud; 32 | import hudson.slaves.NodeProvisioner.PlannedNode; 33 | import hudson.util.FormValidation; 34 | import hudson.util.ListBoxModel; 35 | import java.util.Objects; 36 | import jenkins.model.Jenkins; 37 | import lombok.Getter; 38 | import lombok.SneakyThrows; 39 | import lombok.extern.slf4j.Slf4j; 40 | import org.apache.commons.lang.RandomStringUtils; 41 | import org.jenkinsci.plugins.cloudstats.ProvisioningActivity; 42 | import org.jenkinsci.plugins.cloudstats.TrackedPlannedNode; 43 | import org.jenkinsci.plugins.plaincredentials.impl.StringCredentialsImpl; 44 | import org.kohsuke.accmod.Restricted; 45 | import org.kohsuke.accmod.restrictions.NoExternalUse; 46 | import org.kohsuke.stapler.AncestorInPath; 47 | import org.kohsuke.stapler.DataBoundConstructor; 48 | import org.kohsuke.stapler.DataBoundSetter; 49 | import org.kohsuke.stapler.QueryParameter; 50 | import org.kohsuke.stapler.interceptor.RequirePOST; 51 | 52 | import java.io.IOException; 53 | import java.util.ArrayList; 54 | import java.util.Collection; 55 | import java.util.Collections; 56 | import java.util.List; 57 | import java.util.Locale; 58 | import java.util.stream.Collectors; 59 | 60 | @Slf4j 61 | public class HetznerCloud extends AbstractCloudImpl { 62 | @Getter 63 | private final String credentialsId; 64 | @Getter 65 | private List serverTemplates; 66 | @Getter 67 | private transient HetznerCloudResourceManager resourceManager; 68 | 69 | @DataBoundConstructor 70 | public HetznerCloud(String name, String credentialsId, String instanceCapStr, 71 | List serverTemplates) { 72 | super(name, instanceCapStr); 73 | this.credentialsId = credentialsId; 74 | this.serverTemplates = serverTemplates; 75 | readResolve(); 76 | } 77 | 78 | /** 79 | * Pick random template from provided list. 80 | * 81 | * @param matchingTemplates List of all matching templates. 82 | * @return picked template 83 | */ 84 | private static HetznerServerTemplate pickTemplate(List matchingTemplates) { 85 | if (matchingTemplates.size() == 1) { 86 | return matchingTemplates.get(0); 87 | } 88 | final List shuffled = new ArrayList<>(matchingTemplates); 89 | Collections.shuffle(shuffled); 90 | return shuffled.get(0); 91 | } 92 | 93 | @DataBoundSetter 94 | public void setServerTemplates(List serverTemplates) { 95 | this.serverTemplates = Objects.requireNonNullElse(serverTemplates, Collections.emptyList()); 96 | readResolve(); 97 | } 98 | 99 | protected Object readResolve() { 100 | resourceManager = HetznerCloudResourceManager.create(credentialsId); 101 | if (serverTemplates == null) { 102 | setServerTemplates(Collections.emptyList()); 103 | } 104 | for (HetznerServerTemplate template : serverTemplates) { 105 | template.setCloud(this); 106 | template.readResolve(); 107 | } 108 | return this; 109 | } 110 | 111 | @SneakyThrows 112 | private int runningNodeCount() { 113 | return Ints.checkedCast(resourceManager.fetchAllServers(name) 114 | .stream() 115 | .filter(sd -> HetznerConstants.RUNNABLE_STATE_SET.contains(sd.getStatus())) 116 | .count()); 117 | } 118 | 119 | @Override 120 | public Collection provision(CloudState state, int excessWorkload) { 121 | log.debug("provision(cloud={},label={},excessWorkload={})", name, state.getLabel(), excessWorkload); 122 | final List plannedNodes = new ArrayList<>(); 123 | final Label label = state.getLabel(); 124 | final List matchingTemplates = getTemplates(label); 125 | final Jenkins jenkinsInstance = Jenkins.get(); 126 | try { 127 | while (excessWorkload > 0) { 128 | if (jenkinsInstance.isQuietingDown() || jenkinsInstance.isTerminating()) { 129 | log.warn("Jenkins is going down, no new nodes will be provisioned"); 130 | break; 131 | } 132 | int running = runningNodeCount(); 133 | int instanceCap = getInstanceCap(); 134 | int available = instanceCap - running; 135 | final HetznerServerTemplate template = pickTemplate(matchingTemplates); 136 | log.info("Creating new agent with {} executors, have {} running VMs", template.getNumExecutors(), running); 137 | if (available <= 0) { 138 | log.warn("Cloud capacity reached ({}). Has {} VMs running, but want {} more executors", 139 | instanceCap, running , excessWorkload); 140 | break; 141 | } else { 142 | final String serverName = template.generateNodeName(); 143 | final ProvisioningActivity.Id provisioningId = new ProvisioningActivity.Id(name, template.getName(), 144 | serverName); 145 | final HetznerServerAgent agent = template.createAgent(provisioningId, serverName); 146 | agent.setMode(template.getMode()); 147 | plannedNodes.add(new TrackedPlannedNode( 148 | provisioningId, 149 | agent.getNumExecutors(), 150 | Computer.threadPoolForRemoting.submit(new NodeCallable(agent, this) 151 | ) 152 | ) 153 | ); 154 | excessWorkload -= agent.getNumExecutors(); 155 | } 156 | } 157 | 158 | } catch (IOException | Descriptor.FormException e) { 159 | log.error("Unable to provision node", e); 160 | } 161 | return plannedNodes; 162 | } 163 | 164 | @Override 165 | public boolean canProvision(CloudState state) { 166 | return !getTemplates(state.getLabel()).isEmpty(); 167 | } 168 | 169 | private List getTemplates(Label label) { 170 | return serverTemplates.stream().filter(t -> { 171 | //no labels has been provided in template 172 | if (t.getLabels().isEmpty()) { 173 | return Node.Mode.NORMAL.equals(t.getMode()); 174 | } else { 175 | if (Node.Mode.NORMAL.equals(t.getMode())) { 176 | return label == null || label.matches(t.getLabels()); 177 | } else { 178 | return label != null && label.matches(t.getLabels()); 179 | } 180 | } 181 | }) 182 | .collect(Collectors.toList()); 183 | } 184 | 185 | @SuppressWarnings("unused") 186 | @Extension 187 | public static class DescriptorImpl extends Descriptor { 188 | @Override 189 | @NonNull 190 | public String getDisplayName() { 191 | return Messages.plugin_displayName(); 192 | } 193 | 194 | @Restricted(NoExternalUse.class) 195 | @RequirePOST 196 | public FormValidation doVerifyConfiguration(@QueryParameter String credentialsId) { 197 | Jenkins.get().checkPermission(Jenkins.ADMINISTER); 198 | final ConfigurationValidator.ValidationResult result = ConfigurationValidator.validateCloudConfig(credentialsId); 199 | if (result.isSuccess()) { 200 | return FormValidation.ok(Messages.cloudConfigPassed()); 201 | } else { 202 | return FormValidation.error(result.getMessage()); 203 | } 204 | } 205 | 206 | @Restricted(NoExternalUse.class) 207 | @RequirePOST 208 | public FormValidation doCheckCloudName(@QueryParameter String name) { 209 | if (Helper.isValidLabelValue(name)) { 210 | return FormValidation.ok(); 211 | } 212 | return FormValidation.error("Cloud name is not a valid label value: %s", name); 213 | } 214 | 215 | @Restricted(NoExternalUse.class) 216 | @RequirePOST 217 | public ListBoxModel doFillCredentialsIdItems(@AncestorInPath Item owner) { 218 | final StandardListBoxModel result = new StandardListBoxModel(); 219 | if (owner == null) { 220 | if (!Jenkins.get().hasPermission(Jenkins.ADMINISTER)) { 221 | return result; 222 | } 223 | } else { 224 | if (!owner.hasPermission(Item.EXTENDED_READ) 225 | && !owner.hasPermission(CredentialsProvider.USE_ITEM)) { 226 | return result; 227 | } 228 | } 229 | return new StandardListBoxModel() 230 | .includeEmptyValue() 231 | .includeMatchingAs(ACL.SYSTEM2, owner, StringCredentialsImpl.class, 232 | Collections.emptyList(), CredentialsMatchers.always()); 233 | } 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 edu.umd.cs.findbugs.annotations.NonNull; 24 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 25 | import hudson.Extension; 26 | import hudson.Util; 27 | import hudson.model.AbstractDescribableImpl; 28 | import hudson.model.Descriptor; 29 | import hudson.model.Label; 30 | import hudson.model.Node.Mode; 31 | import hudson.model.labels.LabelAtom; 32 | import hudson.util.FormValidation; 33 | import jenkins.model.Jenkins; 34 | import lombok.AccessLevel; 35 | import lombok.Getter; 36 | import lombok.Setter; 37 | import lombok.ToString; 38 | import lombok.extern.slf4j.Slf4j; 39 | import org.apache.commons.lang.RandomStringUtils; 40 | import org.jenkinsci.plugins.cloudstats.ProvisioningActivity; 41 | import org.kohsuke.accmod.Restricted; 42 | import org.kohsuke.accmod.restrictions.NoExternalUse; 43 | import org.kohsuke.stapler.DataBoundConstructor; 44 | import org.kohsuke.stapler.DataBoundSetter; 45 | import org.kohsuke.stapler.QueryParameter; 46 | import org.kohsuke.stapler.interceptor.RequirePOST; 47 | 48 | import java.io.IOException; 49 | import java.util.Locale; 50 | import java.util.Set; 51 | import java.util.regex.Pattern; 52 | 53 | import static cloud.dnation.jenkins.plugins.hetzner.ConfigurationValidator.doCheckNonEmpty; 54 | import static cloud.dnation.jenkins.plugins.hetzner.ConfigurationValidator.doCheckPositiveInt; 55 | import static cloud.dnation.jenkins.plugins.hetzner.ConfigurationValidator.verifyFirewall; 56 | import static cloud.dnation.jenkins.plugins.hetzner.ConfigurationValidator.verifyImage; 57 | import static cloud.dnation.jenkins.plugins.hetzner.ConfigurationValidator.verifyLocation; 58 | import static cloud.dnation.jenkins.plugins.hetzner.ConfigurationValidator.verifyNetwork; 59 | import static cloud.dnation.jenkins.plugins.hetzner.ConfigurationValidator.verifyPlacementGroup; 60 | import static cloud.dnation.jenkins.plugins.hetzner.ConfigurationValidator.verifyPrefix; 61 | import static cloud.dnation.jenkins.plugins.hetzner.ConfigurationValidator.verifyServerType; 62 | import static cloud.dnation.jenkins.plugins.hetzner.ConfigurationValidator.verifyVolumes; 63 | import static cloud.dnation.jenkins.plugins.hetzner.Helper.getStringOrDefault; 64 | import static cloud.dnation.jenkins.plugins.hetzner.HetznerConstants.DEFAULT_REMOTE_FS; 65 | 66 | @ToString 67 | @Slf4j 68 | public class HetznerServerTemplate extends AbstractDescribableImpl { 69 | private static final Pattern PREFIX_RE = Pattern.compile("^[a-z][\\w_-]+$"); 70 | @Getter 71 | private final String name; 72 | 73 | @Getter 74 | private final String labelStr; 75 | 76 | @Getter 77 | private final String image; 78 | 79 | @Getter 80 | private final String location; 81 | 82 | @Getter 83 | private final String serverType; 84 | 85 | @Getter 86 | private transient Set labels; 87 | 88 | @Setter(AccessLevel.PACKAGE) 89 | @Getter(AccessLevel.PACKAGE) 90 | @NonNull 91 | private transient HetznerCloud cloud; 92 | 93 | @Setter(onMethod = @__({@DataBoundSetter})) 94 | @Getter 95 | private AbstractHetznerSshConnector connector; 96 | 97 | @Setter(onMethod = @__({@DataBoundSetter})) 98 | @Getter 99 | private String remoteFs; 100 | 101 | @Setter(onMethod = @__({@DataBoundSetter})) 102 | @Getter 103 | private String placementGroup; 104 | 105 | @Setter(onMethod = @__({@DataBoundSetter})) 106 | @Getter 107 | private String userData; 108 | 109 | @Setter(onMethod = @__({@DataBoundSetter})) 110 | @Getter 111 | private String jvmOpts; 112 | 113 | @Getter 114 | @Setter(onMethod = @__({@DataBoundSetter})) 115 | private int numExecutors; 116 | 117 | @Getter 118 | @Setter(onMethod = @__({@DataBoundSetter})) 119 | private int bootDeadline; 120 | 121 | @Getter 122 | @Setter(onMethod = @__({@DataBoundSetter})) 123 | private String network; 124 | 125 | @Getter 126 | @Setter(onMethod = @__({@DataBoundSetter})) 127 | private String firewall; 128 | 129 | @Getter 130 | @Setter(onMethod = @__({@DataBoundSetter})) 131 | private String prefix; 132 | 133 | @Getter 134 | @Setter(onMethod = @__({@DataBoundSetter})) 135 | private Mode mode = Mode.EXCLUSIVE; 136 | 137 | @Getter 138 | @Setter(onMethod = @__({@DataBoundSetter})) 139 | private AbstractShutdownPolicy shutdownPolicy; 140 | 141 | @Getter 142 | @Setter(onMethod = @__({@DataBoundSetter})) 143 | private AbstractPrimaryIpStrategy primaryIp; 144 | 145 | @Getter 146 | @Setter(onMethod = @__({@DataBoundSetter})) 147 | private AbstractConnectivity connectivity; 148 | 149 | @Getter 150 | @Setter(onMethod = @__({@DataBoundSetter})) 151 | private boolean automountVolumes; 152 | 153 | @Getter 154 | @Setter(onMethod = @__({@DataBoundSetter})) 155 | private String volumeIds; 156 | 157 | @DataBoundConstructor 158 | @SuppressFBWarnings("NP_NONNULL_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR") 159 | public HetznerServerTemplate(String name, String labelStr, String image, 160 | String location, String serverType) { 161 | this.name = name; 162 | this.labelStr = Util.fixNull(labelStr); 163 | this.image = image; 164 | this.location = location; 165 | this.serverType = serverType; 166 | readResolve(); 167 | } 168 | 169 | protected Object readResolve() { 170 | Jenkins.get().checkPermission(Jenkins.ADMINISTER); 171 | labels = Label.parse(labelStr); 172 | if (Strings.isNullOrEmpty(location)) { 173 | throw new IllegalArgumentException("Location must be specified"); 174 | } 175 | if (numExecutors == 0) { 176 | setNumExecutors(HetznerConstants.DEFAULT_NUM_EXECUTORS); 177 | } 178 | if (bootDeadline == 0) { 179 | setBootDeadline(HetznerConstants.DEFAULT_BOOT_DEADLINE); 180 | } 181 | if (shutdownPolicy == null) { 182 | shutdownPolicy = HetznerConstants.DEFAULT_SHUTDOWN_POLICY; 183 | } 184 | if (primaryIp == null) { 185 | primaryIp = HetznerConstants.DEFAULT_PRIMARY_IP_STRATEGY; 186 | } 187 | if (connectivity == null ) { 188 | connectivity = HetznerConstants.DEFAULT_CONNECTIVITY; 189 | } 190 | if (placementGroup == null) { 191 | placementGroup = ""; 192 | } 193 | if (userData == null) { 194 | userData = ""; 195 | } 196 | if (volumeIds == null) { 197 | volumeIds = ""; 198 | } 199 | if (prefix == null) { 200 | prefix = ""; 201 | } 202 | prefix = prefix.toLowerCase(Locale.ROOT); 203 | return this; 204 | } 205 | 206 | boolean isPrefixValid() { 207 | return checkPrefixValue(prefix); 208 | } 209 | 210 | static boolean checkPrefixValue(String prefixStr) { 211 | return PREFIX_RE.matcher(prefixStr).matches(); 212 | } 213 | 214 | String generateNodeName() { 215 | final String prefixStr = isPrefixValid() ? prefix : "hcloud"; 216 | return prefixStr + "-" + RandomStringUtils.randomAlphanumeric(16) 217 | .toLowerCase(Locale.ROOT); 218 | } 219 | 220 | /** 221 | * Create new {@link HetznerServerAgent}. 222 | * 223 | * @param provisioningId ID to track activity of provisioning 224 | * @param nodeName name of server 225 | * @return new agent instance 226 | */ 227 | HetznerServerAgent createAgent(ProvisioningActivity.Id provisioningId, String nodeName) 228 | throws IOException, Descriptor.FormException { 229 | return new HetznerServerAgent( 230 | provisioningId, 231 | nodeName, 232 | getStringOrDefault(remoteFs, DEFAULT_REMOTE_FS), 233 | connector.createLauncher(), 234 | cloud, 235 | this 236 | ); 237 | } 238 | 239 | @SuppressWarnings("unused") 240 | @Extension 241 | public static final class DescriptorImpl extends Descriptor { 242 | @Override 243 | @NonNull 244 | public String getDisplayName() { 245 | return Messages.serverTemplate_displayName(); 246 | } 247 | 248 | 249 | @Restricted(NoExternalUse.class) 250 | @RequirePOST 251 | public FormValidation doVerifyPrefix(@QueryParameter String prefix) { 252 | return verifyPrefix(prefix); 253 | } 254 | 255 | @Restricted(NoExternalUse.class) 256 | @RequirePOST 257 | public FormValidation doVerifyLocation(@QueryParameter String location, 258 | @QueryParameter String credentialsId) { 259 | return verifyLocation(location, credentialsId).toFormValidation(); 260 | } 261 | 262 | @Restricted(NoExternalUse.class) 263 | @RequirePOST 264 | public FormValidation doVerifyImage(@QueryParameter String image, 265 | @QueryParameter String credentialsId) { 266 | return verifyImage(image, credentialsId).toFormValidation(); 267 | } 268 | 269 | @Restricted(NoExternalUse.class) 270 | @RequirePOST 271 | public FormValidation doVerifyNetwork(@QueryParameter String network, 272 | @QueryParameter String credentialsId) { 273 | return verifyNetwork(network, credentialsId).toFormValidation(); 274 | } 275 | 276 | @Restricted(NoExternalUse.class) 277 | @RequirePOST 278 | public FormValidation doVerifyFirewall(@QueryParameter String firewall, 279 | @QueryParameter String credentialsId) { 280 | return verifyFirewall(firewall, credentialsId).toFormValidation(); 281 | } 282 | 283 | @Restricted(NoExternalUse.class) 284 | @RequirePOST 285 | public FormValidation doVerifyPlacementGroup(@QueryParameter String placementGroup, 286 | @QueryParameter String credentialsId) { 287 | return verifyPlacementGroup(placementGroup, credentialsId).toFormValidation(); 288 | } 289 | 290 | @Restricted(NoExternalUse.class) 291 | @RequirePOST 292 | public FormValidation doVerifyServerType(@QueryParameter String serverType, 293 | @QueryParameter String credentialsId) { 294 | return verifyServerType(serverType, credentialsId).toFormValidation(); 295 | } 296 | 297 | @Restricted(NoExternalUse.class) 298 | @RequirePOST 299 | public FormValidation doVerifyVolumes(@QueryParameter String volumeIds, 300 | @QueryParameter String credentialsId) { 301 | return verifyVolumes(volumeIds, credentialsId).toFormValidation(); 302 | } 303 | 304 | @Restricted(NoExternalUse.class) 305 | @RequirePOST 306 | public FormValidation doCheckImage(@QueryParameter String image) { 307 | return doCheckNonEmpty(image, "Image"); 308 | } 309 | 310 | @Restricted(NoExternalUse.class) 311 | @RequirePOST 312 | public FormValidation doCheckLabelStr(@QueryParameter String labelStr, @QueryParameter Mode mode) { 313 | if (Strings.isNullOrEmpty(labelStr) && Mode.EXCLUSIVE == mode) { 314 | return FormValidation.warning("You may want to assign labels to this node;" 315 | + " it's marked to only run jobs that are exclusively tied to itself or a label."); 316 | } 317 | return FormValidation.ok(); 318 | } 319 | 320 | @Restricted(NoExternalUse.class) 321 | @RequirePOST 322 | public FormValidation doCheckServerType(@QueryParameter String serverType) { 323 | return doCheckNonEmpty(serverType, "Server type"); 324 | } 325 | 326 | @Restricted(NoExternalUse.class) 327 | @RequirePOST 328 | public FormValidation doCheckLocation(@QueryParameter String location) { 329 | return doCheckNonEmpty(location, "Location"); 330 | } 331 | 332 | @Restricted(NoExternalUse.class) 333 | @RequirePOST 334 | public FormValidation doCheckName(@QueryParameter String name) { 335 | return doCheckNonEmpty(name, "Name"); 336 | } 337 | 338 | @Restricted(NoExternalUse.class) 339 | @RequirePOST 340 | public FormValidation doCheckNumExecutors(@QueryParameter String numExecutors) { 341 | return doCheckPositiveInt(numExecutors, "Number of executors"); 342 | } 343 | 344 | @Restricted(NoExternalUse.class) 345 | @RequirePOST 346 | public FormValidation doCheckBootDeadline(@QueryParameter String bootDeadline) { 347 | return doCheckPositiveInt(bootDeadline, "Boot deadline"); 348 | } 349 | } 350 | } 351 | --------------------------------------------------------------------------------