) t -> null);
140 | }
141 |
142 | public static BasicSSHUserPrivateKey assertSshKey(String credentialsId) {
143 | final BasicSSHUserPrivateKey privateKey = CredentialsMatchers.firstOrNull(
144 | CredentialsProvider.lookupCredentials(BasicSSHUserPrivateKey.class, Jenkins.get(), ACL.SYSTEM,
145 | Collections.emptyList()),
146 | CredentialsMatchers.withId(credentialsId));
147 |
148 | Preconditions.checkState(privateKey != null,
149 | "No SSH credentials found with ID '%s'", credentialsId);
150 |
151 | return privateKey;
152 | }
153 |
154 | public static String getStringOrDefault(String value, String defValue) {
155 | if(Strings.isNullOrEmpty(value)) {
156 | return defValue;
157 | }
158 | return value;
159 | }
160 |
161 | @RequiredArgsConstructor
162 | public static class LogAdapter {
163 | private static final SimpleFormatter FORMATTER = new SimpleFormatter();
164 | @Getter
165 | private final PrintStream stream;
166 | private final Logger logger;
167 |
168 | public void info(String message) {
169 | logger.info(message);
170 | final LogRecord rec = new LogRecord(Level.INFO, message);
171 | rec.setLoggerName(logger.getName());
172 | stream.println(FORMATTER.format(rec));
173 | }
174 |
175 | public void error(String message, Throwable cause) {
176 | logger.error(message, cause);
177 | final LogRecord rec = new LogRecord(Level.SEVERE, message + " Cause: " + cause);
178 | rec.setLoggerName(logger.getName());
179 | rec.setThrown(cause);
180 | stream.println(FORMATTER.format(rec));
181 | }
182 | }
183 |
184 | /**
185 | * Check if idle server can be shut down.
186 | *
187 | * According to Hetzner billing policy,
188 | * you are billed for every hour of existence of server, so it makes sense to keep server running as long as next hour did
189 | * not start yet.
190 | *
191 | * @param createdStr RFC3339-formatted instant when server was created. See ServerDetail#getCreated().
192 | * @param currentTime current time. Kept as argument to allow unit-testing.
193 | * @return true
if server should be shut down, false
otherwise.
194 | * Note: we keep small time buffer for corner cases like clock skew or Jenkins's queue manager overload, which could
195 | * lead to unnecessary 1-hour over-billing.
196 | */
197 | public static boolean canShutdownServer(@Nonnull String createdStr, LocalDateTime currentTime) {
198 | final LocalDateTime created = LocalDateTime.from(DateTimeFormatter.ISO_DATE_TIME.parse(createdStr))
199 | .atOffset(ZoneOffset.UTC).toLocalDateTime();
200 | long diff = Duration.between(created, currentTime.atOffset(ZoneOffset.UTC).toLocalDateTime()).toMinutes() % 60;
201 | return (60 - SHUTDOWN_TIME_BUFFER) <= diff;
202 | }
203 |
204 | /**
205 | * Get all nodes that are {@link HetznerServerAgent}.
206 | *
207 | * @return list of all {@link HetznerServerAgent} nodes
208 | */
209 | public static List getHetznerAgents() {
210 | return Jenkins.get().getNodes()
211 | .stream()
212 | .filter(HetznerServerAgent.class::isInstance)
213 | .map(HetznerServerAgent.class::cast)
214 | .collect(Collectors.toList());
215 | }
216 |
217 | public static boolean isValidLabelValue(String value) {
218 | if (Strings.isNullOrEmpty(value)) {
219 | return false;
220 | }
221 | return LABEL_VALUE_RE.matcher(value).matches();
222 | }
223 | }
224 |
--------------------------------------------------------------------------------
/src/main/java/cloud/dnation/jenkins/plugins/hetzner/HetznerCloud.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 https://dnation.cloud
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package cloud.dnation.jenkins.plugins.hetzner;
17 |
18 | import com.cloudbees.plugins.credentials.CredentialsMatchers;
19 | import com.cloudbees.plugins.credentials.CredentialsProvider;
20 | import com.cloudbees.plugins.credentials.common.StandardListBoxModel;
21 | import com.google.common.primitives.Ints;
22 | import hudson.Extension;
23 | import hudson.model.Computer;
24 | import hudson.model.Descriptor;
25 | import hudson.model.Item;
26 | import hudson.model.Label;
27 | import hudson.model.Node;
28 | import hudson.security.ACL;
29 | import hudson.slaves.AbstractCloudImpl;
30 | import hudson.slaves.Cloud;
31 | import hudson.slaves.NodeProvisioner.PlannedNode;
32 | import hudson.util.FormValidation;
33 | import hudson.util.ListBoxModel;
34 | import jenkins.model.Jenkins;
35 | import lombok.Getter;
36 | import lombok.NonNull;
37 | import lombok.SneakyThrows;
38 | import lombok.extern.slf4j.Slf4j;
39 | import org.apache.commons.lang.RandomStringUtils;
40 | import org.jenkinsci.plugins.cloudstats.ProvisioningActivity;
41 | import org.jenkinsci.plugins.cloudstats.TrackedPlannedNode;
42 | import org.jenkinsci.plugins.plaincredentials.impl.StringCredentialsImpl;
43 | import org.kohsuke.accmod.Restricted;
44 | import org.kohsuke.accmod.restrictions.NoExternalUse;
45 | import org.kohsuke.stapler.AncestorInPath;
46 | import org.kohsuke.stapler.DataBoundConstructor;
47 | import org.kohsuke.stapler.DataBoundSetter;
48 | import org.kohsuke.stapler.QueryParameter;
49 | import org.kohsuke.stapler.interceptor.RequirePOST;
50 |
51 | import java.io.IOException;
52 | import java.util.ArrayList;
53 | import java.util.Collection;
54 | import java.util.Collections;
55 | import java.util.List;
56 | import java.util.Locale;
57 | import java.util.stream.Collectors;
58 |
59 | @Slf4j
60 | public class HetznerCloud extends AbstractCloudImpl {
61 | @Getter
62 | private final String credentialsId;
63 | @Getter
64 | private List serverTemplates;
65 | @Getter
66 | private transient HetznerCloudResourceManager resourceManager;
67 |
68 | @DataBoundConstructor
69 | public HetznerCloud(String name, String credentialsId, String instanceCapStr,
70 | List serverTemplates) {
71 | super(name, instanceCapStr);
72 | this.credentialsId = credentialsId;
73 | this.serverTemplates = serverTemplates;
74 | readResolve();
75 | }
76 |
77 | /**
78 | * Pick random template from provided list.
79 | *
80 | * @param matchingTemplates List of all matching templates.
81 | * @return picked template
82 | */
83 | private static HetznerServerTemplate pickTemplate(List matchingTemplates) {
84 | if (matchingTemplates.size() == 1) {
85 | return matchingTemplates.get(0);
86 | }
87 | final List shuffled = new ArrayList<>(matchingTemplates);
88 | Collections.shuffle(shuffled);
89 | return shuffled.get(0);
90 | }
91 |
92 | @DataBoundSetter
93 | public void setServerTemplates(List serverTemplates) {
94 | if (serverTemplates == null) {
95 | this.serverTemplates = Collections.emptyList();
96 | } else {
97 | this.serverTemplates = serverTemplates;
98 | }
99 | readResolve();
100 | }
101 |
102 | protected Object readResolve() {
103 | resourceManager = HetznerCloudResourceManager.create(credentialsId);
104 | if (serverTemplates == null) {
105 | setServerTemplates(Collections.emptyList());
106 | }
107 | for (HetznerServerTemplate template : serverTemplates) {
108 | template.setCloud(this);
109 | template.readResolve();
110 | }
111 | return this;
112 | }
113 |
114 | @SneakyThrows
115 | private int runningNodeCount() {
116 | return Ints.checkedCast(resourceManager.fetchAllServers(name)
117 | .stream()
118 | .filter(sd -> HetznerConstants.RUNNABLE_STATE_SET.contains(sd.getStatus()))
119 | .count());
120 | }
121 |
122 | @Override
123 | public Collection provision(CloudState state, int excessWorkload) {
124 | log.debug("provision(cloud={},label={},excessWorkload={})", name, state.getLabel(), excessWorkload);
125 | final List plannedNodes = new ArrayList<>();
126 | final Label label = state.getLabel();
127 | final List matchingTemplates = getTemplates(label);
128 | final Jenkins jenkinsInstance = Jenkins.get();
129 | try {
130 | while (excessWorkload > 0) {
131 | if (jenkinsInstance.isQuietingDown() || jenkinsInstance.isTerminating()) {
132 | log.warn("Jenkins is going down, no new nodes will be provisioned");
133 | break;
134 | }
135 | int running = runningNodeCount();
136 | int instanceCap = getInstanceCap();
137 | int available = instanceCap - running;
138 | final HetznerServerTemplate template = pickTemplate(matchingTemplates);
139 | log.info("Creating new agent with {} executors, have {} running VMs", template.getNumExecutors(), running);
140 | if (available <= 0) {
141 | log.warn("Cloud capacity reached ({}). Has {} VMs running, but want {} more executors",
142 | instanceCap, running , excessWorkload);
143 | break;
144 | } else {
145 | final String serverName = "hcloud-" + RandomStringUtils.randomAlphanumeric(16)
146 | .toLowerCase(Locale.ROOT);
147 | final ProvisioningActivity.Id provisioningId = new ProvisioningActivity.Id(name, template.getName(),
148 | serverName);
149 | final HetznerServerAgent agent = template.createAgent(provisioningId, serverName);
150 | agent.setMode(template.getMode());
151 | plannedNodes.add(new TrackedPlannedNode(
152 | provisioningId,
153 | agent.getNumExecutors(),
154 | Computer.threadPoolForRemoting.submit(new NodeCallable(agent, this)
155 | )
156 | )
157 | );
158 | excessWorkload -= agent.getNumExecutors();
159 | }
160 | }
161 |
162 | } catch (IOException | Descriptor.FormException e) {
163 | log.error("Unable to provision node", e);
164 | }
165 | return plannedNodes;
166 | }
167 |
168 | @Override
169 | public boolean canProvision(CloudState state) {
170 | return !getTemplates(state.getLabel()).isEmpty();
171 | }
172 |
173 | private List getTemplates(Label label) {
174 | return serverTemplates.stream().filter(t -> {
175 | //no labels has been provided in template
176 | if (t.getLabels().isEmpty()) {
177 | return Node.Mode.NORMAL.equals(t.getMode());
178 | } else {
179 | if (Node.Mode.NORMAL.equals(t.getMode())) {
180 | return label == null || label.matches(t.getLabels());
181 | } else {
182 | return label != null && label.matches(t.getLabels());
183 | }
184 | }
185 | })
186 | .collect(Collectors.toList());
187 | }
188 |
189 | @SuppressWarnings("unused")
190 | @Extension
191 | public static class DescriptorImpl extends Descriptor {
192 | @Override
193 | @NonNull
194 | public String getDisplayName() {
195 | return Messages.plugin_displayName();
196 | }
197 |
198 | @Restricted(NoExternalUse.class)
199 | @RequirePOST
200 | public FormValidation doVerifyConfiguration(@QueryParameter String credentialsId) {
201 | Jenkins.get().checkPermission(Jenkins.ADMINISTER);
202 | final ConfigurationValidator.ValidationResult result = ConfigurationValidator.validateCloudConfig(credentialsId);
203 | if (result.isSuccess()) {
204 | return FormValidation.ok(Messages.cloudConfigPassed());
205 | } else {
206 | return FormValidation.error(result.getMessage());
207 | }
208 | }
209 |
210 | @Restricted(NoExternalUse.class)
211 | @RequirePOST
212 | public FormValidation doCheckCloudName(@QueryParameter String name) {
213 | if (Helper.isValidLabelValue(name)) {
214 | return FormValidation.ok();
215 | }
216 | return FormValidation.error("Cloud name is not a valid label value: %s", name);
217 | }
218 |
219 | @Restricted(NoExternalUse.class)
220 | @RequirePOST
221 | public ListBoxModel doFillCredentialsIdItems(@AncestorInPath Item owner) {
222 | final StandardListBoxModel result = new StandardListBoxModel();
223 | if (owner == null) {
224 | if (!Jenkins.get().hasPermission(Jenkins.ADMINISTER)) {
225 | return result;
226 | }
227 | } else {
228 | if (!owner.hasPermission(owner.EXTENDED_READ)
229 | && !owner.hasPermission(CredentialsProvider.USE_ITEM)) {
230 | return result;
231 | }
232 | }
233 | return new StandardListBoxModel()
234 | .includeEmptyValue()
235 | .includeMatchingAs(ACL.SYSTEM, owner, StringCredentialsImpl.class,
236 | Collections.emptyList(), CredentialsMatchers.always());
237 | }
238 | }
239 | }
240 |
--------------------------------------------------------------------------------
/src/main/java/cloud/dnation/jenkins/plugins/hetzner/HetznerConstants.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 https://dnation.cloud
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package cloud.dnation.jenkins.plugins.hetzner;
17 |
18 | import cloud.dnation.jenkins.plugins.hetzner.connect.AbstractConnectivity;
19 | import cloud.dnation.jenkins.plugins.hetzner.connect.Both;
20 | import cloud.dnation.jenkins.plugins.hetzner.launcher.AbstractConnectionMethod;
21 | import cloud.dnation.jenkins.plugins.hetzner.launcher.DefaultConnectionMethod;
22 | import cloud.dnation.jenkins.plugins.hetzner.primaryip.AbstractPrimaryIpStrategy;
23 | import cloud.dnation.jenkins.plugins.hetzner.primaryip.DefaultStrategy;
24 | import cloud.dnation.jenkins.plugins.hetzner.shutdown.IdlePeriodPolicy;
25 | import com.google.common.collect.ImmutableSet;
26 | import lombok.experimental.UtilityClass;
27 |
28 | import java.util.Set;
29 |
30 | @UtilityClass
31 | public class HetznerConstants {
32 | /**
33 | * Namespace for labels added to all objects managed by this plugin.
34 | */
35 | public static final String LABEL_NS = "jenkins.io/";
36 |
37 | /**
38 | * Name of label for credentials-id to apply to SSH key.
39 | */
40 | public static final String LABEL_CREDENTIALS_ID = LABEL_NS + "credentials-id";
41 |
42 | /**
43 | * Name of label for cloud instance associated with server.
44 | */
45 | public static final String LABEL_CLOUD_NAME = LABEL_NS + "cloud-name";
46 |
47 | /**
48 | * Name of label for all objects managed by this plugin.
49 | */
50 | public static final String LABEL_MANAGED_BY = LABEL_NS + "managed-by";
51 |
52 | /**
53 | * Internal identifier used to label cloud resources that this plugin manages.
54 | */
55 | public static final String LABEL_VALUE_PLUGIN = "hetzner-jenkins-plugin";
56 |
57 | /**
58 | * Default remote working directory.
59 | */
60 | public static final String DEFAULT_REMOTE_FS = "/home/jenkins";
61 |
62 | public static final int DEFAULT_NUM_EXECUTORS = 1;
63 |
64 | public static final int DEFAULT_BOOT_DEADLINE = 1;
65 |
66 | /**
67 | * Set of server states that are considered as runnable.
68 | */
69 | public static final Set RUNNABLE_STATE_SET = ImmutableSet.builder()
70 | .add("running")
71 | .add("initializing")
72 | .add("starting")
73 | .build();
74 |
75 | /**
76 | * Default shutdown policy to use.
77 | */
78 | static final IdlePeriodPolicy DEFAULT_SHUTDOWN_POLICY = new IdlePeriodPolicy(10);
79 |
80 | /*
81 | * Arbitrary value in minutes which gives us some time to shut down server before usage hour wraps.
82 | */
83 | public static final int SHUTDOWN_TIME_BUFFER = 5;
84 |
85 | /**
86 | * Default strategy to get primary IP.
87 | */
88 | public static final AbstractPrimaryIpStrategy DEFAULT_PRIMARY_IP_STRATEGY = DefaultStrategy.SINGLETON;
89 |
90 | public static final AbstractConnectionMethod DEFAULT_CONNECTION_METHOD = DefaultConnectionMethod.SINGLETON;
91 |
92 | /**
93 | * Default networking setup.
94 | */
95 | public static final AbstractConnectivity DEFAULT_CONNECTIVITY = new Both();
96 | }
97 |
--------------------------------------------------------------------------------
/src/main/java/cloud/dnation/jenkins/plugins/hetzner/HetznerServerAgent.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 https://dnation.cloud
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package cloud.dnation.jenkins.plugins.hetzner;
17 |
18 | import cloud.dnation.jenkins.plugins.hetzner.launcher.HetznerServerComputerLauncher;
19 | import edu.umd.cs.findbugs.annotations.NonNull;
20 | import hudson.Extension;
21 | import hudson.model.Descriptor;
22 | import hudson.model.Node;
23 | import hudson.model.TaskListener;
24 | import hudson.slaves.AbstractCloudComputer;
25 | import hudson.slaves.AbstractCloudSlave;
26 | import hudson.slaves.ComputerLauncher;
27 | import hudson.slaves.EphemeralNode;
28 | import lombok.AccessLevel;
29 | import lombok.Getter;
30 | import lombok.Setter;
31 | import lombok.extern.slf4j.Slf4j;
32 | import org.jenkinsci.plugins.cloudstats.CloudStatistics;
33 | import org.jenkinsci.plugins.cloudstats.ProvisioningActivity;
34 | import org.jenkinsci.plugins.cloudstats.TrackedItem;
35 |
36 | import javax.annotation.Nonnull;
37 | import java.io.IOException;
38 | import java.util.Objects;
39 | import java.util.Optional;
40 |
41 | @Slf4j
42 | public class HetznerServerAgent extends AbstractCloudSlave implements EphemeralNode, TrackedItem {
43 | private static final long serialVersionUID = 1;
44 | private final ProvisioningActivity.Id provisioningId;
45 | @Getter
46 | private final transient HetznerCloud cloud;
47 | @Getter
48 | @NonNull
49 | private final transient HetznerServerTemplate template;
50 | @Getter(AccessLevel.PUBLIC)
51 | @Setter(AccessLevel.PACKAGE)
52 | private transient HetznerServerInfo serverInstance;
53 |
54 | public HetznerServerAgent(@NonNull ProvisioningActivity.Id provisioningId,
55 | @NonNull String name, String remoteFS, ComputerLauncher launcher,
56 | @NonNull HetznerCloud cloud, @NonNull HetznerServerTemplate template)
57 | throws IOException, Descriptor.FormException {
58 | super(name, remoteFS, launcher);
59 | this.cloud = Objects.requireNonNull(cloud);
60 | this.template = Objects.requireNonNull(template);
61 | this.provisioningId = Objects.requireNonNull(provisioningId);
62 | setLabelString(template.getLabelStr());
63 | setNumExecutors(template.getNumExecutors());
64 | setMode(template.getMode() == null ? Mode.EXCLUSIVE : template.getMode());
65 | setRetentionStrategy(template.getShutdownPolicy().getRetentionStrategy());
66 | readResolve();
67 | }
68 |
69 | @SuppressWarnings("rawtypes")
70 | @Override
71 | public AbstractCloudComputer createComputer() {
72 | return new HetznerServerComputer(this);
73 | }
74 |
75 | @Override
76 | public String getDisplayName() {
77 | if (serverInstance != null && serverInstance.getServerDetail() != null) {
78 | return getNodeName() + " in " + serverInstance.getServerDetail().getDatacenter()
79 | .getLocation().getDescription();
80 | }
81 | return super.getDisplayName();
82 | }
83 |
84 | @Override
85 | protected void _terminate(TaskListener listener) {
86 | ((HetznerServerComputerLauncher) getLauncher()).signalTermination();
87 | cloud.getResourceManager().destroyServer(serverInstance.getServerDetail());
88 | Optional.ofNullable(CloudStatistics.get().getActivityFor(this))
89 | .ifPresent(a -> a.enterIfNotAlready(ProvisioningActivity.Phase.COMPLETED));
90 | }
91 |
92 | @Override
93 | public Node asNode() {
94 | return this;
95 | }
96 |
97 | @Nonnull
98 | @Override
99 | public ProvisioningActivity.Id getId() {
100 | return provisioningId;
101 | }
102 |
103 | /**
104 | * Check if server associated with this agent is running.
105 | *
106 | * @return true
if status of server is "running", false
otherwise
107 | */
108 | public boolean isAlive() {
109 | serverInstance = cloud.getResourceManager().refreshServerInfo(serverInstance);
110 | return serverInstance.getServerDetail().getStatus().equals("running");
111 | }
112 |
113 | @SuppressWarnings("unused")
114 | @Extension
115 | public static final class DescriptorImpl extends SlaveDescriptor {
116 | @NonNull
117 | @Override
118 | public String getDisplayName() {
119 | return Messages.plugin_displayName();
120 | }
121 |
122 | public boolean isInstantiable() {
123 | return false;
124 | }
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/src/main/java/cloud/dnation/jenkins/plugins/hetzner/HetznerServerComputer.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 https://dnation.cloud
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package cloud.dnation.jenkins.plugins.hetzner;
17 |
18 | import hudson.slaves.AbstractCloudComputer;
19 | import org.jenkinsci.plugins.cloudstats.ProvisioningActivity;
20 | import org.jenkinsci.plugins.cloudstats.TrackedItem;
21 |
22 | import javax.annotation.Nonnull;
23 |
24 | public class HetznerServerComputer extends AbstractCloudComputer implements TrackedItem {
25 | private final ProvisioningActivity.Id provisioningId;
26 |
27 | public HetznerServerComputer(HetznerServerAgent agent) {
28 | super(agent);
29 | this.provisioningId = agent.getId();
30 | }
31 |
32 | @Nonnull
33 | @Override
34 | public ProvisioningActivity.Id getId() {
35 | return provisioningId;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/main/java/cloud/dnation/jenkins/plugins/hetzner/HetznerServerInfo.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 https://dnation.cloud
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package cloud.dnation.jenkins.plugins.hetzner;
17 |
18 | import cloud.dnation.hetznerclient.ServerDetail;
19 | import cloud.dnation.hetznerclient.SshKeyDetail;
20 | import lombok.Data;
21 |
22 | @Data
23 | public class HetznerServerInfo {
24 | private final SshKeyDetail sshKeyDetail;
25 | private ServerDetail serverDetail;
26 | }
27 |
--------------------------------------------------------------------------------
/src/main/java/cloud/dnation/jenkins/plugins/hetzner/HetznerServerTemplate.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 https://dnation.cloud
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package cloud.dnation.jenkins.plugins.hetzner;
17 |
18 | import cloud.dnation.jenkins.plugins.hetzner.connect.AbstractConnectivity;
19 | import cloud.dnation.jenkins.plugins.hetzner.launcher.AbstractHetznerSshConnector;
20 | import cloud.dnation.jenkins.plugins.hetzner.primaryip.AbstractPrimaryIpStrategy;
21 | import cloud.dnation.jenkins.plugins.hetzner.shutdown.AbstractShutdownPolicy;
22 | import com.google.common.base.Strings;
23 | import hudson.Extension;
24 | import hudson.Util;
25 | import hudson.model.AbstractDescribableImpl;
26 | import hudson.model.Descriptor;
27 | import hudson.model.Label;
28 | import hudson.model.Node.Mode;
29 | import hudson.model.labels.LabelAtom;
30 | import hudson.util.FormValidation;
31 | import jenkins.model.Jenkins;
32 | import lombok.AccessLevel;
33 | import lombok.Getter;
34 | import lombok.NonNull;
35 | import lombok.Setter;
36 | import lombok.ToString;
37 | import lombok.extern.slf4j.Slf4j;
38 | import org.jenkinsci.plugins.cloudstats.ProvisioningActivity;
39 | import org.kohsuke.accmod.Restricted;
40 | import org.kohsuke.accmod.restrictions.NoExternalUse;
41 | import org.kohsuke.stapler.DataBoundConstructor;
42 | import org.kohsuke.stapler.DataBoundSetter;
43 | import org.kohsuke.stapler.QueryParameter;
44 | import org.kohsuke.stapler.interceptor.RequirePOST;
45 |
46 | import java.io.IOException;
47 | import java.util.Set;
48 |
49 | import static cloud.dnation.jenkins.plugins.hetzner.ConfigurationValidator.doCheckNonEmpty;
50 | import static cloud.dnation.jenkins.plugins.hetzner.ConfigurationValidator.doCheckPositiveInt;
51 | import static cloud.dnation.jenkins.plugins.hetzner.ConfigurationValidator.verifyImage;
52 | import static cloud.dnation.jenkins.plugins.hetzner.ConfigurationValidator.verifyLocation;
53 | import static cloud.dnation.jenkins.plugins.hetzner.ConfigurationValidator.verifyNetwork;
54 | import static cloud.dnation.jenkins.plugins.hetzner.ConfigurationValidator.verifyPlacementGroup;
55 | import static cloud.dnation.jenkins.plugins.hetzner.ConfigurationValidator.verifyServerType;
56 | import static cloud.dnation.jenkins.plugins.hetzner.ConfigurationValidator.verifyVolumes;
57 | import static cloud.dnation.jenkins.plugins.hetzner.Helper.getStringOrDefault;
58 | import static cloud.dnation.jenkins.plugins.hetzner.HetznerConstants.DEFAULT_REMOTE_FS;
59 |
60 | @ToString
61 | @Slf4j
62 | public class HetznerServerTemplate extends AbstractDescribableImpl {
63 | @Getter
64 | private final String name;
65 |
66 | @Getter
67 | private final String labelStr;
68 |
69 | @Getter
70 | private final String image;
71 |
72 | @Getter
73 | private final String location;
74 |
75 | @Getter
76 | private final String serverType;
77 |
78 | @Getter
79 | private transient Set labels;
80 |
81 | @Setter(AccessLevel.PACKAGE)
82 | @Getter(AccessLevel.PACKAGE)
83 | @NonNull
84 | private transient HetznerCloud cloud;
85 |
86 | @Setter(onMethod = @__({@DataBoundSetter}))
87 | @Getter
88 | private AbstractHetznerSshConnector connector;
89 |
90 | @Setter(onMethod = @__({@DataBoundSetter}))
91 | @Getter
92 | private String remoteFs;
93 |
94 | @Setter(onMethod = @__({@DataBoundSetter}))
95 | @Getter
96 | private String placementGroup;
97 |
98 | @Setter(onMethod = @__({@DataBoundSetter}))
99 | @Getter
100 | private String userData;
101 |
102 | @Setter(onMethod = @__({@DataBoundSetter}))
103 | @Getter
104 | private String jvmOpts;
105 |
106 | @Getter
107 | @Setter(onMethod = @__({@DataBoundSetter}))
108 | private int numExecutors;
109 |
110 | @Getter
111 | @Setter(onMethod = @__({@DataBoundSetter}))
112 | private int bootDeadline;
113 |
114 | @Getter
115 | @Setter(onMethod = @__({@DataBoundSetter}))
116 | private String network;
117 |
118 | @Getter
119 | @Setter(onMethod = @__({@DataBoundSetter}))
120 | private Mode mode = Mode.EXCLUSIVE;
121 |
122 | @Getter
123 | @Setter(onMethod = @__({@DataBoundSetter}))
124 | private AbstractShutdownPolicy shutdownPolicy;
125 |
126 | @Getter
127 | @Setter(onMethod = @__({@DataBoundSetter}))
128 | private AbstractPrimaryIpStrategy primaryIp;
129 |
130 | @Getter
131 | @Setter(onMethod = @__({@DataBoundSetter}))
132 | private AbstractConnectivity connectivity;
133 |
134 | @Getter
135 | @Setter(onMethod = @__({@DataBoundSetter}))
136 | private boolean automountVolumes;
137 |
138 | @Getter
139 | @Setter(onMethod = @__({@DataBoundSetter}))
140 | private String volumeIds;
141 |
142 | @DataBoundConstructor
143 | public HetznerServerTemplate(String name, String labelStr, String image,
144 | String location, String serverType) {
145 | this.name = name;
146 | this.labelStr = Util.fixNull(labelStr);
147 | this.image = image;
148 | this.location = location;
149 | this.serverType = serverType;
150 | readResolve();
151 | }
152 |
153 | protected Object readResolve() {
154 | Jenkins.get().checkPermission(Jenkins.ADMINISTER);
155 | labels = Label.parse(labelStr);
156 | if (Strings.isNullOrEmpty(location)) {
157 | throw new IllegalArgumentException("Location must be specified");
158 | }
159 | if (numExecutors == 0) {
160 | setNumExecutors(HetznerConstants.DEFAULT_NUM_EXECUTORS);
161 | }
162 | if (bootDeadline == 0) {
163 | setBootDeadline(HetznerConstants.DEFAULT_BOOT_DEADLINE);
164 | }
165 | if (shutdownPolicy == null) {
166 | shutdownPolicy = HetznerConstants.DEFAULT_SHUTDOWN_POLICY;
167 | }
168 | if (primaryIp == null) {
169 | primaryIp = HetznerConstants.DEFAULT_PRIMARY_IP_STRATEGY;
170 | }
171 | if (connectivity == null ) {
172 | connectivity = HetznerConstants.DEFAULT_CONNECTIVITY;
173 | }
174 | if (placementGroup == null) {
175 | placementGroup = "";
176 | }
177 | if (userData == null) {
178 | userData = "";
179 | }
180 | if (volumeIds == null) {
181 | volumeIds = "";
182 | }
183 | return this;
184 | }
185 |
186 | /**
187 | * Create new {@link HetznerServerAgent}.
188 | *
189 | * @param provisioningId ID to track activity of provisioning
190 | * @param nodeName name of server
191 | * @return new agent instance
192 | */
193 | HetznerServerAgent createAgent(ProvisioningActivity.Id provisioningId, String nodeName)
194 | throws IOException, Descriptor.FormException {
195 | return new HetznerServerAgent(
196 | provisioningId,
197 | nodeName,
198 | getStringOrDefault(remoteFs, DEFAULT_REMOTE_FS),
199 | connector.createLauncher(),
200 | cloud,
201 | this
202 | );
203 | }
204 |
205 | @SuppressWarnings("unused")
206 | @Extension
207 | public static final class DescriptorImpl extends Descriptor {
208 | @Override
209 | @NonNull
210 | public String getDisplayName() {
211 | return Messages.serverTemplate_displayName();
212 | }
213 |
214 | @Restricted(NoExternalUse.class)
215 | @RequirePOST
216 | public FormValidation doVerifyLocation(@QueryParameter String location,
217 | @QueryParameter String credentialsId) {
218 | return verifyLocation(location, credentialsId).toFormValidation();
219 | }
220 |
221 | @Restricted(NoExternalUse.class)
222 | @RequirePOST
223 | public FormValidation doVerifyImage(@QueryParameter String image,
224 | @QueryParameter String credentialsId) {
225 | return verifyImage(image, credentialsId).toFormValidation();
226 | }
227 |
228 | @Restricted(NoExternalUse.class)
229 | @RequirePOST
230 | public FormValidation doVerifyNetwork(@QueryParameter String network,
231 | @QueryParameter String credentialsId) {
232 | return verifyNetwork(network, credentialsId).toFormValidation();
233 | }
234 |
235 | @Restricted(NoExternalUse.class)
236 | @RequirePOST
237 | public FormValidation doVerifyPlacementGroup(@QueryParameter String placementGroup,
238 | @QueryParameter String credentialsId) {
239 | return verifyPlacementGroup(placementGroup, credentialsId).toFormValidation();
240 | }
241 |
242 | @Restricted(NoExternalUse.class)
243 | @RequirePOST
244 | public FormValidation doVerifyServerType(@QueryParameter String serverType,
245 | @QueryParameter String credentialsId) {
246 | return verifyServerType(serverType, credentialsId).toFormValidation();
247 | }
248 |
249 | @Restricted(NoExternalUse.class)
250 | @RequirePOST
251 | public FormValidation doVerifyVolumes(@QueryParameter String volumeIds,
252 | @QueryParameter String credentialsId) {
253 | return verifyVolumes(volumeIds, credentialsId).toFormValidation();
254 | }
255 |
256 | @Restricted(NoExternalUse.class)
257 | @RequirePOST
258 | public FormValidation doCheckImage(@QueryParameter String image) {
259 | return doCheckNonEmpty(image, "Image");
260 | }
261 |
262 | @Restricted(NoExternalUse.class)
263 | @RequirePOST
264 | public FormValidation doCheckLabelStr(@QueryParameter String labelStr, @QueryParameter Mode mode) {
265 | if (Strings.isNullOrEmpty(labelStr) && Mode.EXCLUSIVE == mode) {
266 | return FormValidation.warning("You may want to assign labels to this node;"
267 | + " it's marked to only run jobs that are exclusively tied to itself or a label.");
268 | }
269 | return FormValidation.ok();
270 | }
271 |
272 | @Restricted(NoExternalUse.class)
273 | @RequirePOST
274 | public FormValidation doCheckServerType(@QueryParameter String serverType) {
275 | return doCheckNonEmpty(serverType, "Server type");
276 | }
277 |
278 | @Restricted(NoExternalUse.class)
279 | @RequirePOST
280 | public FormValidation doCheckLocation(@QueryParameter String location) {
281 | return doCheckNonEmpty(location, "Location");
282 | }
283 |
284 | @Restricted(NoExternalUse.class)
285 | @RequirePOST
286 | public FormValidation doCheckName(@QueryParameter String name) {
287 | return doCheckNonEmpty(name, "Name");
288 | }
289 |
290 | @Restricted(NoExternalUse.class)
291 | @RequirePOST
292 | public FormValidation doCheckNumExecutors(@QueryParameter String numExecutors) {
293 | return doCheckPositiveInt(numExecutors, "Number of executors");
294 | }
295 |
296 | @Restricted(NoExternalUse.class)
297 | @RequirePOST
298 | public FormValidation doCheckBootDeadline(@QueryParameter String bootDeadline) {
299 | return doCheckPositiveInt(bootDeadline, "Boot deadline");
300 | }
301 | }
302 | }
303 |
--------------------------------------------------------------------------------
/src/main/java/cloud/dnation/jenkins/plugins/hetzner/JenkinsSecretTokenProvider.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 https://dnation.cloud
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package cloud.dnation.jenkins.plugins.hetzner;
17 |
18 | import com.cloudbees.plugins.credentials.CredentialsMatchers;
19 | import com.cloudbees.plugins.credentials.CredentialsProvider;
20 | import jenkins.model.Jenkins;
21 | import org.jenkinsci.plugins.plaincredentials.StringCredentials;
22 |
23 | import java.util.function.Supplier;
24 |
25 | public class JenkinsSecretTokenProvider implements Supplier {
26 | private final String credentialsId;
27 |
28 | private JenkinsSecretTokenProvider(String credentialsId) {
29 | this.credentialsId = credentialsId;
30 | }
31 |
32 | public static JenkinsSecretTokenProvider forCredentialsId(String credentialsId) {
33 | return new JenkinsSecretTokenProvider(credentialsId);
34 | }
35 |
36 | @Override
37 | public String get() {
38 | final StringCredentials secret = CredentialsMatchers.firstOrNull(
39 | CredentialsProvider.lookupCredentials(StringCredentials.class, Jenkins.get()),
40 | CredentialsMatchers.withId(credentialsId));
41 | if (secret == null) {
42 | throw new IllegalStateException("Can't find credentials with ID '" + credentialsId + "'");
43 | }
44 | return secret.getSecret().getPlainText();
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/main/java/cloud/dnation/jenkins/plugins/hetzner/NodeCallable.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 https://dnation.cloud
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package cloud.dnation.jenkins.plugins.hetzner;
17 |
18 | import com.google.common.base.Preconditions;
19 | import com.google.common.util.concurrent.Uninterruptibles;
20 | import hudson.model.Computer;
21 | import hudson.model.Node;
22 | import jenkins.model.Jenkins;
23 | import lombok.RequiredArgsConstructor;
24 | import lombok.extern.slf4j.Slf4j;
25 |
26 | import java.util.concurrent.Callable;
27 | import java.util.concurrent.ExecutionException;
28 | import java.util.concurrent.TimeUnit;
29 |
30 | @Slf4j
31 | @RequiredArgsConstructor
32 | class NodeCallable implements Callable {
33 | private final HetznerServerAgent agent;
34 | private final HetznerCloud cloud;
35 |
36 | @Override
37 | public Node call() throws Exception {
38 | Computer computer = agent.getComputer();
39 | if (computer != null && computer.isOnline()) {
40 | return agent;
41 | }
42 | final HetznerServerInfo serverInfo = cloud.getResourceManager().createServer(agent);
43 | agent.setServerInstance(serverInfo);
44 | final String serverName = serverInfo.getServerDetail().getName();
45 | boolean running = false;
46 | final int bootDeadline = agent.getTemplate().getBootDeadline();
47 | //wait for status == "running", but at most 15 minutes
48 | final WaitStrategy waitStrategy = new WaitStrategy(bootDeadline, 45, 15);
49 | while (!waitStrategy.isDeadLineOver()) {
50 | waitStrategy.waitNext();
51 | if (agent.isAlive()) {
52 | log.info("Server '{}' is now running, waiting 10 seconds before proceeding", serverName);
53 | Uninterruptibles.sleepUninterruptibly(10, TimeUnit.SECONDS);
54 | running = true;
55 | break;
56 | }
57 | }
58 | Preconditions.checkState(running, "Server '%s' didn't start after 15 minutes, giving up",
59 | serverName);
60 | Jenkins.get().addNode(agent);
61 | computer = agent.toComputer();
62 | int retry = 5;
63 | boolean connected = false;
64 | if (computer != null) {
65 | while (--retry > 0) {
66 | try {
67 | computer.connect(false).get();
68 | connected = true;
69 | break;
70 | } catch (InterruptedException | ExecutionException e) {
71 | log.warn("Connection to '{}' has failed, remaining retries {}", computer.getDisplayName(),
72 | retry, e);
73 | TimeUnit.SECONDS.sleep(10);
74 | }
75 | }
76 | if (!connected) {
77 | throw new IllegalStateException("Computer is not connected : " + computer.getName());
78 | }
79 | } else {
80 | throw new IllegalStateException("No computer object in agent " + agent.getDisplayName());
81 | }
82 |
83 | return agent;
84 | }
85 |
86 | private static final class WaitStrategy {
87 | private final int firstInterval;
88 | private final int subsequentIntervals;
89 | private final long deadlineNanos;
90 | private boolean first = true;
91 |
92 | private WaitStrategy(int deadlineMinutes, int firstInterval, int subsequentIntervals) {
93 | deadlineNanos = System.nanoTime() + deadlineMinutes * 60L * 1_000_000_000L;
94 | this.firstInterval = firstInterval;
95 | this.subsequentIntervals = subsequentIntervals;
96 | }
97 |
98 | boolean isDeadLineOver() {
99 | return System.nanoTime() > deadlineNanos;
100 | }
101 |
102 | void waitNext() {
103 | final int waitSeconds;
104 | if (first) {
105 | first = false;
106 | waitSeconds = firstInterval;
107 | } else {
108 | waitSeconds = subsequentIntervals;
109 | }
110 | Uninterruptibles.sleepUninterruptibly(waitSeconds, TimeUnit.SECONDS);
111 | }
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/src/main/java/cloud/dnation/jenkins/plugins/hetzner/OrphanedNodesCleaner.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 https://dnation.cloud
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package cloud.dnation.jenkins.plugins.hetzner;
17 |
18 | import cloud.dnation.hetznerclient.ServerDetail;
19 | import hudson.Extension;
20 | import hudson.model.PeriodicWork;
21 | import jenkins.model.Jenkins;
22 | import lombok.extern.slf4j.Slf4j;
23 | import org.jenkinsci.Symbol;
24 |
25 | import java.io.IOException;
26 | import java.util.List;
27 | import java.util.Set;
28 | import java.util.stream.Collectors;
29 |
30 | @Extension
31 | @Symbol("OrphanedNodesCleaner")
32 | @Slf4j
33 | public class OrphanedNodesCleaner extends PeriodicWork {
34 | @Override
35 | public long getRecurrencePeriod() {
36 | return HOUR;
37 | }
38 |
39 | private static Set getHetznerClouds() {
40 | return Jenkins.get().clouds.stream()
41 | .filter(HetznerCloud.class::isInstance)
42 | .map(HetznerCloud.class::cast)
43 | .collect(Collectors.toSet());
44 | }
45 |
46 | @Override
47 | protected void doRun() {
48 | doCleanup();
49 | }
50 |
51 | static void doCleanup() {
52 | getHetznerClouds().forEach(OrphanedNodesCleaner::cleanCloud);
53 | }
54 |
55 | private static void cleanCloud(HetznerCloud cloud) {
56 | try {
57 | final List allInstances = cloud.getResourceManager()
58 | .fetchAllServers(cloud.name);
59 | final List jenkinsNodes = Helper.getHetznerAgents()
60 | .stream()
61 | .map(HetznerServerAgent::getNodeName)
62 | .collect(Collectors.toList());
63 | allInstances.stream().filter(server -> !jenkinsNodes.contains(server.getName()))
64 | .forEach(serverDetail -> terminateServer(serverDetail, cloud));
65 | } catch (IOException e) {
66 | log.warn("Error while fetching all servers", e);
67 | }
68 | }
69 |
70 | private static void terminateServer(ServerDetail serverDetail, HetznerCloud cloud) {
71 | log.info("Terminating orphaned server {}", serverDetail.getName());
72 | cloud.getResourceManager().destroyServer(serverDetail);
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/main/java/cloud/dnation/jenkins/plugins/hetzner/connect/AbstractConnectivity.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2022 https://dnation.cloud
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package cloud.dnation.jenkins.plugins.hetzner.connect;
17 |
18 | import hudson.model.AbstractDescribableImpl;
19 |
20 | public abstract class AbstractConnectivity extends AbstractDescribableImpl {
21 | public abstract ConnectivityType getType();
22 | }
23 |
--------------------------------------------------------------------------------
/src/main/java/cloud/dnation/jenkins/plugins/hetzner/connect/Both.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2022 https://dnation.cloud
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package cloud.dnation.jenkins.plugins.hetzner.connect;
17 |
18 | import cloud.dnation.jenkins.plugins.hetzner.Messages;
19 | import edu.umd.cs.findbugs.annotations.NonNull;
20 | import hudson.Extension;
21 | import hudson.model.Descriptor;
22 | import lombok.NoArgsConstructor;
23 | import org.jenkinsci.Symbol;
24 | import org.kohsuke.stapler.DataBoundConstructor;
25 |
26 | @NoArgsConstructor(onConstructor = @__({@DataBoundConstructor}))
27 | public class Both extends AbstractConnectivity{
28 | public ConnectivityType getType() {
29 | return ConnectivityType.BOTH;
30 | }
31 |
32 | @Extension
33 | @Symbol("both")
34 | public static final class DescriptorImpl extends Descriptor {
35 | @NonNull
36 | @Override
37 | public String getDisplayName() {
38 | return Messages.connectivity_both();
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/main/java/cloud/dnation/jenkins/plugins/hetzner/connect/BothV6.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2024 https://dnation.cloud
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package cloud.dnation.jenkins.plugins.hetzner.connect;
17 |
18 | import cloud.dnation.jenkins.plugins.hetzner.Messages;
19 | import edu.umd.cs.findbugs.annotations.NonNull;
20 | import hudson.Extension;
21 | import hudson.model.Descriptor;
22 | import lombok.NoArgsConstructor;
23 | import org.jenkinsci.Symbol;
24 | import org.kohsuke.stapler.DataBoundConstructor;
25 |
26 | @NoArgsConstructor(onConstructor = @__({@DataBoundConstructor}))
27 | public class BothV6 extends AbstractConnectivity{
28 | public ConnectivityType getType() {
29 | return ConnectivityType.BOTH_V6;
30 | }
31 |
32 | @Extension
33 | @Symbol("both-v6")
34 | public static final class DescriptorImpl extends Descriptor {
35 | @NonNull
36 | @Override
37 | public String getDisplayName() {
38 | return Messages.connectivity_bothV6();
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/main/java/cloud/dnation/jenkins/plugins/hetzner/connect/ConnectivityType.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2022 https://dnation.cloud
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package cloud.dnation.jenkins.plugins.hetzner.connect;
17 |
18 | public enum ConnectivityType {
19 | PRIVATE,
20 | PUBLIC,
21 | PUBLIC_V6,
22 | BOTH,
23 | BOTH_V6
24 | }
25 |
--------------------------------------------------------------------------------
/src/main/java/cloud/dnation/jenkins/plugins/hetzner/connect/PrivateOnly.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2022 https://dnation.cloud
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package cloud.dnation.jenkins.plugins.hetzner.connect;
17 |
18 | import cloud.dnation.jenkins.plugins.hetzner.Messages;
19 | import edu.umd.cs.findbugs.annotations.NonNull;
20 | import hudson.Extension;
21 | import hudson.model.Descriptor;
22 | import lombok.NoArgsConstructor;
23 | import org.jenkinsci.Symbol;
24 | import org.kohsuke.stapler.DataBoundConstructor;
25 |
26 | @NoArgsConstructor(onConstructor = @__({@DataBoundConstructor}))
27 | public class PrivateOnly extends AbstractConnectivity {
28 | public ConnectivityType getType() {
29 | return ConnectivityType.PRIVATE;
30 | }
31 | @Extension
32 | @Symbol("private-only")
33 | public static final class DescriptorImpl extends Descriptor {
34 | @NonNull
35 | @Override
36 | public String getDisplayName() {
37 | return Messages.connectivity_private_only();
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/main/java/cloud/dnation/jenkins/plugins/hetzner/connect/PublicOnly.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2022 https://dnation.cloud
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package cloud.dnation.jenkins.plugins.hetzner.connect;
17 |
18 | import cloud.dnation.jenkins.plugins.hetzner.Messages;
19 | import edu.umd.cs.findbugs.annotations.NonNull;
20 | import hudson.Extension;
21 | import hudson.model.Descriptor;
22 | import lombok.NoArgsConstructor;
23 | import org.jenkinsci.Symbol;
24 | import org.kohsuke.stapler.DataBoundConstructor;
25 |
26 | @NoArgsConstructor(onConstructor = @__({@DataBoundConstructor}))
27 | public class PublicOnly extends AbstractConnectivity {
28 | public ConnectivityType getType() {
29 | return ConnectivityType.PUBLIC;
30 | }
31 |
32 | @Extension
33 | @Symbol("public-only")
34 | public static final class DescriptorImpl extends Descriptor {
35 | @NonNull
36 | @Override
37 | public String getDisplayName() {
38 | return Messages.connectivity_public_only();
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/main/java/cloud/dnation/jenkins/plugins/hetzner/connect/PublicV6Only.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2024 https://dnation.cloud
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package cloud.dnation.jenkins.plugins.hetzner.connect;
17 |
18 | import cloud.dnation.jenkins.plugins.hetzner.Messages;
19 | import edu.umd.cs.findbugs.annotations.NonNull;
20 | import hudson.Extension;
21 | import hudson.model.Descriptor;
22 | import lombok.NoArgsConstructor;
23 | import org.jenkinsci.Symbol;
24 | import org.kohsuke.stapler.DataBoundConstructor;
25 |
26 | @NoArgsConstructor(onConstructor = @__({@DataBoundConstructor}))
27 | public class PublicV6Only extends AbstractConnectivity {
28 | public ConnectivityType getType() {
29 | return ConnectivityType.PUBLIC_V6;
30 | }
31 |
32 | @Extension
33 | @Symbol("publicV6-only")
34 | public static final class DescriptorImpl extends Descriptor {
35 | @NonNull
36 | @Override
37 | public String getDisplayName() {
38 | return Messages.connectivity_publicV6_only();
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/main/java/cloud/dnation/jenkins/plugins/hetzner/launcher/AbstractConnectionMethod.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2022 https://dnation.cloud
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package cloud.dnation.jenkins.plugins.hetzner.launcher;
17 |
18 | import cloud.dnation.hetznerclient.ServerDetail;
19 | import hudson.model.AbstractDescribableImpl;
20 |
21 | public abstract class AbstractConnectionMethod extends AbstractDescribableImpl {
22 | public abstract String getAddress(ServerDetail server);
23 | }
24 |
--------------------------------------------------------------------------------
/src/main/java/cloud/dnation/jenkins/plugins/hetzner/launcher/AbstractHetznerSshConnector.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 https://dnation.cloud
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package cloud.dnation.jenkins.plugins.hetzner.launcher;
17 |
18 | import com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey;
19 | import com.cloudbees.plugins.credentials.CredentialsMatchers;
20 | import com.cloudbees.plugins.credentials.CredentialsProvider;
21 | import com.cloudbees.plugins.credentials.common.StandardListBoxModel;
22 | import hudson.model.AbstractDescribableImpl;
23 | import hudson.model.Descriptor;
24 | import hudson.model.Item;
25 | import hudson.security.ACL;
26 | import hudson.util.FormValidation;
27 | import hudson.util.ListBoxModel;
28 | import jenkins.model.Jenkins;
29 | import lombok.Getter;
30 | import lombok.Setter;
31 | import lombok.extern.slf4j.Slf4j;
32 | import org.kohsuke.accmod.Restricted;
33 | import org.kohsuke.accmod.restrictions.NoExternalUse;
34 | import org.kohsuke.stapler.AncestorInPath;
35 | import org.kohsuke.stapler.DataBoundSetter;
36 | import org.kohsuke.stapler.QueryParameter;
37 | import org.kohsuke.stapler.interceptor.RequirePOST;
38 |
39 | import javax.annotation.Nullable;
40 | import java.util.Collections;
41 |
42 | import static cloud.dnation.jenkins.plugins.hetzner.ConfigurationValidator.doCheckNonEmpty;
43 |
44 | @Slf4j
45 | public abstract class AbstractHetznerSshConnector extends AbstractDescribableImpl {
46 | /**
47 | * SSH connection will be authenticated using different user then specified in credentials if this field is non-null.
48 | */
49 | @Nullable
50 | @Getter
51 | @Setter(onMethod = @__({@DataBoundSetter}))
52 | protected String usernameOverride;
53 |
54 | @Getter
55 | @Setter(onMethod = @__({@DataBoundSetter}))
56 | protected String sshCredentialsId;
57 |
58 | @Getter
59 | @Setter(onMethod = @__({@DataBoundSetter}))
60 | protected AbstractConnectionMethod connectionMethod = DefaultConnectionMethod.SINGLETON;
61 |
62 | public HetznerServerComputerLauncher createLauncher() {
63 | return new HetznerServerComputerLauncher(this);
64 | }
65 |
66 | public static abstract class DescriptorImpl extends Descriptor {
67 | // this method does not have any side effect, nor does it read any state.
68 | @SuppressWarnings("lgtm[jenkins/no-permission-check]")
69 | @Restricted(NoExternalUse.class)
70 | @RequirePOST
71 | public FormValidation doCheckSshCredentialsId(@QueryParameter String sshCredentialsId) {
72 | return doCheckNonEmpty(sshCredentialsId, "SSH credentials");
73 | }
74 |
75 | @Restricted(NoExternalUse.class)
76 | @RequirePOST
77 | public ListBoxModel doFillSshCredentialsIdItems(@AncestorInPath Item owner) {
78 | final StandardListBoxModel result = new StandardListBoxModel();
79 | if (owner == null) {
80 | if (!Jenkins.get().hasPermission(Jenkins.ADMINISTER)) {
81 | return result;
82 | }
83 | } else {
84 | if (!owner.hasPermission(owner.EXTENDED_READ)
85 | && !owner.hasPermission(CredentialsProvider.USE_ITEM)) {
86 | return result;
87 | }
88 | }
89 | return new StandardListBoxModel()
90 | .includeEmptyValue()
91 | .includeMatchingAs(ACL.SYSTEM, owner, BasicSSHUserPrivateKey.class,
92 | Collections.emptyList(), CredentialsMatchers.always());
93 | }
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/src/main/java/cloud/dnation/jenkins/plugins/hetzner/launcher/DefaultConnectionMethod.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2022 https://dnation.cloud
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package cloud.dnation.jenkins.plugins.hetzner.launcher;
17 |
18 | import cloud.dnation.hetznerclient.ServerDetail;
19 | import cloud.dnation.jenkins.plugins.hetzner.Messages;
20 | import edu.umd.cs.findbugs.annotations.NonNull;
21 | import hudson.Extension;
22 | import hudson.model.Descriptor;
23 | import lombok.NoArgsConstructor;
24 | import org.jenkinsci.Symbol;
25 | import org.kohsuke.stapler.DataBoundConstructor;
26 |
27 | @NoArgsConstructor(onConstructor = @__({@DataBoundConstructor}))
28 | public class DefaultConnectionMethod extends AbstractConnectionMethod {
29 | public static final DefaultConnectionMethod SINGLETON = new DefaultConnectionMethod();
30 |
31 | @Override
32 | public String getAddress(ServerDetail server) {
33 | if (server.getPrivateNet() != null && !server.getPrivateNet().isEmpty()) {
34 | return server.getPrivateNet().get(0).getIp();
35 | } else {
36 | return server.getPublicNet().getIpv4().getIp();
37 | }
38 | }
39 |
40 | @Extension
41 | @Symbol("default")
42 | public static final class DescriptorImpl extends Descriptor {
43 | @NonNull
44 | @Override
45 | public String getDisplayName() {
46 | return Messages.connection_method_default();
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/main/java/cloud/dnation/jenkins/plugins/hetzner/launcher/DefaultSshConnector.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 https://dnation.cloud
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package cloud.dnation.jenkins.plugins.hetzner.launcher;
17 |
18 | import cloud.dnation.jenkins.plugins.hetzner.Messages;
19 | import edu.umd.cs.findbugs.annotations.NonNull;
20 | import hudson.Extension;
21 | import org.jenkinsci.Symbol;
22 | import org.kohsuke.stapler.DataBoundConstructor;
23 |
24 | import java.io.Serializable;
25 |
26 | /**
27 | * Connect as user configured in credentials.
28 | */
29 | public class DefaultSshConnector extends AbstractHetznerSshConnector implements Serializable {
30 | @DataBoundConstructor
31 | public DefaultSshConnector(String sshCredentialsId) {
32 | setSshCredentialsId(sshCredentialsId);
33 | }
34 |
35 | @Extension
36 | @Symbol("default")
37 | public static final class DescriptorImpl extends AbstractHetznerSshConnector.DescriptorImpl {
38 | @NonNull
39 | @Override
40 | public String getDisplayName() {
41 | return Messages.connector_SshAsNonRoot();
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/main/java/cloud/dnation/jenkins/plugins/hetzner/launcher/DefaultV6ConnectionMethod.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2024 https://dnation.cloud
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package cloud.dnation.jenkins.plugins.hetzner.launcher;
17 |
18 | import cloud.dnation.hetznerclient.ServerDetail;
19 | import cloud.dnation.jenkins.plugins.hetzner.Messages;
20 | import edu.umd.cs.findbugs.annotations.NonNull;
21 | import hudson.Extension;
22 | import hudson.model.Descriptor;
23 | import lombok.NoArgsConstructor;
24 | import org.jenkinsci.Symbol;
25 | import org.kohsuke.stapler.DataBoundConstructor;
26 |
27 | @NoArgsConstructor(onConstructor = @__({@DataBoundConstructor}))
28 | public class DefaultV6ConnectionMethod extends AbstractConnectionMethod {
29 | @Override
30 | public String getAddress(ServerDetail server) {
31 | if (server.getPrivateNet() != null && !server.getPrivateNet().isEmpty()) {
32 | return server.getPrivateNet().get(0).getIp();
33 | } else {
34 | return server.getPublicNet().getIpv6().getIp();
35 | }
36 | }
37 |
38 | @Extension
39 | @Symbol("defaultV6")
40 | public static final class DescriptorImpl extends Descriptor {
41 | @NonNull
42 | @Override
43 | public String getDisplayName() {
44 | return Messages.connection_method_defaultV6();
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/main/java/cloud/dnation/jenkins/plugins/hetzner/launcher/HetznerServerComputerLauncher.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 https://dnation.cloud
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package cloud.dnation.jenkins.plugins.hetzner.launcher;
17 |
18 | import cloud.dnation.hetznerclient.ServerDetail;
19 | import cloud.dnation.jenkins.plugins.hetzner.Helper;
20 | import cloud.dnation.jenkins.plugins.hetzner.HetznerConstants;
21 | import cloud.dnation.jenkins.plugins.hetzner.HetznerServerAgent;
22 | import cloud.dnation.jenkins.plugins.hetzner.HetznerServerComputer;
23 | import com.cloudbees.jenkins.plugins.sshcredentials.SSHAuthenticator;
24 | import com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey;
25 | import com.google.common.base.Preconditions;
26 | import com.google.common.util.concurrent.Uninterruptibles;
27 | import com.trilead.ssh2.Connection;
28 | import com.trilead.ssh2.SCPClient;
29 | import com.trilead.ssh2.ServerHostKeyVerifier;
30 | import com.trilead.ssh2.Session;
31 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
32 | import hudson.AbortException;
33 | import hudson.Util;
34 | import hudson.model.TaskListener;
35 | import hudson.remoting.Channel;
36 | import hudson.slaves.ComputerLauncher;
37 | import hudson.slaves.SlaveComputer;
38 | import jenkins.model.Jenkins;
39 | import lombok.RequiredArgsConstructor;
40 | import lombok.extern.slf4j.Slf4j;
41 |
42 | import java.io.IOException;
43 | import java.nio.charset.StandardCharsets;
44 | import java.util.concurrent.TimeUnit;
45 | import java.util.concurrent.atomic.AtomicBoolean;
46 |
47 | import static cloud.dnation.jenkins.plugins.hetzner.Helper.assertSshKey;
48 | import static cloud.dnation.jenkins.plugins.hetzner.Helper.getStringOrDefault;
49 | import static cloud.dnation.jenkins.plugins.hetzner.HetznerConstants.DEFAULT_REMOTE_FS;
50 | import static hudson.plugins.sshslaves.SSHLauncher.AGENT_JAR;
51 |
52 | @RequiredArgsConstructor
53 | @Slf4j
54 | public class HetznerServerComputerLauncher extends ComputerLauncher {
55 | private static final String AGENT_SCRIPT = ".agent.start.sh";
56 | private final AtomicBoolean terminated = new AtomicBoolean(false);
57 | private final AbstractHetznerSshConnector connector;
58 |
59 | private static String getRemoteFs(HetznerServerAgent agent) {
60 | final String res = getStringOrDefault(agent.getRemoteFS(), DEFAULT_REMOTE_FS);
61 | //trim trailing slash
62 | if (res.endsWith("/")) {
63 | return res.substring(0, res.length() - 1);
64 | }
65 | return res;
66 | }
67 |
68 | private void copyAgent(Connection connection,
69 | HetznerServerComputer computer,
70 | Helper.LogAdapter logger,
71 | String remoteFs) throws IOException {
72 | final byte[] agentBlob = Jenkins.get().getJnlpJars(AGENT_JAR).readFully();
73 | final String remoteAgentPath = remoteFs + "/" + AGENT_JAR;
74 | final byte[] launchScriptContent = ("#!/bin/sh" + '\n' + getAgentCommand(computer, remoteFs) + '\n')
75 | .getBytes(StandardCharsets.UTF_8);
76 | final String launchScriptPath = remoteFs + "/" + AGENT_SCRIPT;
77 | final SCPClient scp = connection.createSCPClient();
78 | logger.info("Copying agent JAR - " + agentBlob.length + " bytes into " + remoteAgentPath);
79 | scp.put(agentBlob, AGENT_JAR, remoteFs, "0644");
80 | logger.info("Copying agent script - " + launchScriptContent.length + " bytes into " + launchScriptPath);
81 | scp.put(launchScriptContent, AGENT_SCRIPT, remoteFs, "0755");
82 | }
83 |
84 | @SuppressFBWarnings(value = {"NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE", "NP_NULL_PARAM_DEREF"},
85 | justification = "NULLnes is checked already")
86 | @Override
87 | public void launch(final SlaveComputer computer, TaskListener listener) throws IOException, InterruptedException {
88 | if (!(computer instanceof HetznerServerComputer)) {
89 | throw new AbortException("Incompatible computer : " + computer);
90 | }
91 | if(connector.getConnectionMethod() == null) {
92 | connector.setConnectionMethod(HetznerConstants.DEFAULT_CONNECTION_METHOD);
93 | }
94 | final HetznerServerComputer hcomputer = (HetznerServerComputer) computer;
95 | final Helper.LogAdapter logger = new Helper.LogAdapter(listener.getLogger(), log);
96 | final HetznerServerAgent node = hcomputer.getNode();
97 | Preconditions.checkState(node != null && node.getServerInstance() != null,
98 | "Missing node or server instance data in computer %s", computer.getName());
99 | final String remoteFs = getRemoteFs(node);
100 | final Connection connection = setupConnection(node, logger, listener);
101 | copyAgent(connection, hcomputer, logger, remoteFs);
102 | launchAgent(connection, hcomputer, logger, listener, remoteFs);
103 | }
104 |
105 | @SuppressFBWarnings(value = "NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE",
106 | justification = "NULLnes of node is checked in launch method")
107 | private String getAgentCommand(HetznerServerComputer computer, String remoteFs) {
108 | final String jvmOpts = Util.fixNull(computer.getNode().getTemplate().getJvmOpts());
109 | return "java " + jvmOpts + " -jar " + remoteFs + "/remoting.jar -workDir " + remoteFs;
110 | }
111 |
112 | @SuppressFBWarnings(value = "NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE",
113 | justification = "NULLnes of node is checked in launch method")
114 | private void launchAgent(Connection connection,
115 | HetznerServerComputer computer,
116 | Helper.LogAdapter logger,
117 | TaskListener listener,
118 | String remoteFs
119 | )
120 | throws IOException, InterruptedException {
121 | final HetznerServerAgent node = computer.getNode();
122 | final Session session = connection.openSession();
123 | final String scriptCmd = "/bin/sh " + remoteFs + "/" + AGENT_SCRIPT;
124 | final String launchCmd;
125 | final String username = connector.getUsernameOverride();
126 | if (username != null) {
127 | final String credentialsId = node.getTemplate().getConnector().getSshCredentialsId();
128 | final BasicSSHUserPrivateKey privateKey = assertSshKey(credentialsId);
129 | launchCmd = "sudo -n -u " + privateKey.getUsername() + " " + scriptCmd;
130 | } else {
131 | launchCmd = scriptCmd;
132 | }
133 |
134 | logger.info("Launching agent using '" + launchCmd + "'");
135 | session.execCommand(launchCmd);
136 | computer.setChannel(session.getStdout(), session.getStdin(), listener, new Channel.Listener() {
137 | @Override
138 | public void onClosed(Channel channel, IOException cause) {
139 | session.close();
140 | connection.close();
141 | }
142 | });
143 | }
144 |
145 | private Connection setupConnection(HetznerServerAgent node,
146 | Helper.LogAdapter logger,
147 | TaskListener taskListener) throws InterruptedException, AbortException {
148 | int retries = 10;
149 | while (!terminated.get() && retries-- > 0) {
150 | final ServerDetail serverDetail = node.getServerInstance().getServerDetail();
151 | final String ipv4 = connector.getConnectionMethod().getAddress(serverDetail);
152 | final Connection conn = new Connection(ipv4, 22);
153 | try {
154 | conn.connect(AllowAnyServerHostKeyVerifier.INSTANCE,
155 | 30_000, 10_000);
156 | logger.info("Connected to " + node.getNodeName() + " via " + ipv4);
157 | final String credentialsId = node.getTemplate().getConnector().getSshCredentialsId();
158 | final BasicSSHUserPrivateKey privateKey = assertSshKey(credentialsId);
159 | final String username = Util.fixNull(node.getTemplate().getConnector().getUsernameOverride(),
160 | privateKey.getUsername());
161 |
162 | logger.info("Authenticating using username '" + username + "'");
163 |
164 | final SSHAuthenticator authenticator = SSHAuthenticator
165 | .newInstance(conn, privateKey, username);
166 |
167 | if (authenticator.authenticate(taskListener) && conn.isAuthenticationComplete()) {
168 | logger.info("Authentication succeeded");
169 | return conn;
170 | } else {
171 | throw new AbortException("Authentication failed");
172 | }
173 | } catch (IOException e) {
174 | logger.error("Connection to " + ipv4 + " failed. Will wait 10 seconds before retry", e);
175 | Uninterruptibles.sleepUninterruptibly(10, TimeUnit.SECONDS);
176 | }
177 | }
178 | throw new AbortException("Failed to launch agent");
179 | }
180 |
181 | public void signalTermination() {
182 | terminated.set(true);
183 | }
184 |
185 | //TODO: is there a way to verify hostkey of newly created server?
186 | //its is usually generated by cloud-init
187 | private static class AllowAnyServerHostKeyVerifier implements ServerHostKeyVerifier {
188 | static final AllowAnyServerHostKeyVerifier INSTANCE = new AllowAnyServerHostKeyVerifier();
189 |
190 | @Override
191 | public boolean verifyServerHostKey(String hostname, int port,
192 | String serverHostKeyAlgorithm,
193 | byte[] serverHostKey) throws Exception {
194 | return true;
195 | }
196 | }
197 | }
198 |
--------------------------------------------------------------------------------
/src/main/java/cloud/dnation/jenkins/plugins/hetzner/launcher/PublicAddressOnly.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2022 https://dnation.cloud
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package cloud.dnation.jenkins.plugins.hetzner.launcher;
17 |
18 | import cloud.dnation.hetznerclient.ServerDetail;
19 | import cloud.dnation.jenkins.plugins.hetzner.Messages;
20 | import edu.umd.cs.findbugs.annotations.NonNull;
21 | import hudson.Extension;
22 | import hudson.model.Descriptor;
23 | import lombok.NoArgsConstructor;
24 | import org.jenkinsci.Symbol;
25 | import org.kohsuke.stapler.DataBoundConstructor;
26 |
27 | @NoArgsConstructor(onConstructor = @__({@DataBoundConstructor}))
28 | public class PublicAddressOnly extends AbstractConnectionMethod {
29 | @Override
30 | public String getAddress(ServerDetail server) {
31 | return server.getPublicNet().getIpv4().getIp();
32 | }
33 |
34 | @Extension
35 | @Symbol("public")
36 | public static final class DescriptorImpl extends Descriptor {
37 | @NonNull
38 | @Override
39 | public String getDisplayName() {
40 | return Messages.connection_method_public();
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/main/java/cloud/dnation/jenkins/plugins/hetzner/launcher/PublicV6AddressOnly.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2024 https://dnation.cloud
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package cloud.dnation.jenkins.plugins.hetzner.launcher;
17 |
18 | import cloud.dnation.hetznerclient.ServerDetail;
19 | import cloud.dnation.jenkins.plugins.hetzner.Messages;
20 | import com.google.common.base.Strings;
21 | import edu.umd.cs.findbugs.annotations.NonNull;
22 | import hudson.Extension;
23 | import hudson.model.Descriptor;
24 | import lombok.NoArgsConstructor;
25 | import org.jenkinsci.Symbol;
26 | import org.kohsuke.stapler.DataBoundConstructor;
27 |
28 | @NoArgsConstructor(onConstructor = @__({@DataBoundConstructor}))
29 | public class PublicV6AddressOnly extends AbstractConnectionMethod {
30 | @Override
31 | public String getAddress(ServerDetail server) {
32 | if (server.getPublicNet().getIpv6() == null || Strings.isNullOrEmpty(server.getPublicNet().getIpv6().getIp())) {
33 | throw new IllegalArgumentException("Connection method requires IPv6 address");
34 | }
35 | // value returned by API ends with "::/64" so replace it with "1"
36 | return server.getPublicNet().getIpv6().getIp().replaceFirst("::/64$", "::1");
37 | }
38 |
39 | @Extension
40 | @Symbol("publicV6")
41 | public static final class DescriptorImpl extends Descriptor {
42 | @NonNull
43 | @Override
44 | public String getDisplayName() {
45 | return Messages.connection_method_publicV6();
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/main/java/cloud/dnation/jenkins/plugins/hetzner/launcher/SshConnectorAsRoot.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 https://dnation.cloud
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package cloud.dnation.jenkins.plugins.hetzner.launcher;
17 |
18 | import cloud.dnation.jenkins.plugins.hetzner.Messages;
19 | import edu.umd.cs.findbugs.annotations.NonNull;
20 | import hudson.Extension;
21 | import org.jenkinsci.Symbol;
22 | import org.kohsuke.stapler.DataBoundConstructor;
23 |
24 | /**
25 | * Connect as "root" user, but launch agent as user configured in credentials.
26 | */
27 | public class SshConnectorAsRoot extends AbstractHetznerSshConnector {
28 | @DataBoundConstructor
29 | public SshConnectorAsRoot(String sshCredentialsId) {
30 | setUsernameOverride("root");
31 | setSshCredentialsId(sshCredentialsId);
32 | }
33 |
34 | @Extension
35 | @Symbol("root")
36 | public static final class DescriptorImpl extends AbstractHetznerSshConnector.DescriptorImpl {
37 | @NonNull
38 | @Override
39 | public String getDisplayName() {
40 | return Messages.connector_SshAsRoot();
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/main/java/cloud/dnation/jenkins/plugins/hetzner/primaryip/AbstractByLabelSelector.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2022 https://dnation.cloud
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package cloud.dnation.jenkins.plugins.hetzner.primaryip;
17 |
18 | import cloud.dnation.hetznerclient.CreateServerRequest;
19 | import cloud.dnation.hetznerclient.HetznerApi;
20 | import cloud.dnation.hetznerclient.PrimaryIpDetail;
21 | import cloud.dnation.hetznerclient.PublicNetRequest;
22 | import com.google.common.annotations.VisibleForTesting;
23 | import com.google.common.base.Strings;
24 | import lombok.Getter;
25 | import lombok.extern.slf4j.Slf4j;
26 |
27 | import java.io.IOException;
28 |
29 | @Slf4j
30 | public abstract class AbstractByLabelSelector extends AbstractPrimaryIpStrategy {
31 | @Getter
32 | private final String selector;
33 |
34 | public AbstractByLabelSelector(boolean failIfError, String selector) {
35 | super(failIfError);
36 | this.selector = selector;
37 | }
38 |
39 | @Override
40 | public void applyInternal(HetznerApi api, CreateServerRequest server) throws IOException {
41 | final PrimaryIpDetail pip = api.getAllPrimaryIps(selector).execute().body().getPrimaryIps().stream()
42 | .filter(ip -> isIpUsable(ip, server)).findFirst().get();
43 | final PublicNetRequest net = new PublicNetRequest();
44 | net.setIpv4(pip.getId());
45 | net.setEnableIpv6(false);
46 | net.setEnableIpv4(true);
47 | server.setPublicNet(net);
48 | }
49 |
50 | @VisibleForTesting
51 | static boolean isIpUsable(PrimaryIpDetail ip, CreateServerRequest server) {
52 | if (ip.getAssigneeId() != null) {
53 | return false;
54 | }
55 | if (!Strings.isNullOrEmpty(server.getLocation())) {
56 | if (server.getLocation().equals(ip.getDatacenter().getLocation().getName())) {
57 | return true;
58 | }
59 | }
60 | if (!Strings.isNullOrEmpty(server.getDatacenter())) {
61 | return server.getDatacenter().equals(ip.getDatacenter().getName());
62 | }
63 | return false;
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/main/java/cloud/dnation/jenkins/plugins/hetzner/primaryip/AbstractPrimaryIpStrategy.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2022 https://dnation.cloud
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package cloud.dnation.jenkins.plugins.hetzner.primaryip;
17 |
18 | import cloud.dnation.hetznerclient.CreateServerRequest;
19 | import cloud.dnation.hetznerclient.HetznerApi;
20 | import hudson.model.AbstractDescribableImpl;
21 | import lombok.RequiredArgsConstructor;
22 | import lombok.extern.slf4j.Slf4j;
23 |
24 | import java.io.IOException;
25 |
26 | @Slf4j
27 | @RequiredArgsConstructor
28 | public abstract class AbstractPrimaryIpStrategy extends AbstractDescribableImpl {
29 | protected final boolean failIfError;
30 |
31 | public void apply(HetznerApi api, CreateServerRequest server) {
32 | try {
33 | applyInternal(api, server);
34 | } catch (Exception e) {
35 | if (failIfError) {
36 | throw new RuntimeException(e);
37 | } else {
38 | log.error("Fail to apply primary IP to server", e);
39 | }
40 | }
41 | }
42 |
43 | protected abstract void applyInternal(HetznerApi api, CreateServerRequest server) throws IOException;
44 | }
--------------------------------------------------------------------------------
/src/main/java/cloud/dnation/jenkins/plugins/hetzner/primaryip/ByLabelSelectorFailing.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2022 https://dnation.cloud
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package cloud.dnation.jenkins.plugins.hetzner.primaryip;
17 |
18 | import cloud.dnation.jenkins.plugins.hetzner.Messages;
19 | import edu.umd.cs.findbugs.annotations.NonNull;
20 | import hudson.Extension;
21 | import hudson.model.Descriptor;
22 | import org.jenkinsci.Symbol;
23 | import org.kohsuke.stapler.DataBoundConstructor;
24 |
25 | public class ByLabelSelectorFailing extends AbstractByLabelSelector {
26 | @DataBoundConstructor
27 | public ByLabelSelectorFailing(String selector) {
28 | super(true, selector);
29 | }
30 |
31 | @Extension
32 | @Symbol("bylabelselector-failing")
33 | public static final class DescriptorImpl extends Descriptor {
34 | @NonNull
35 | @Override
36 | public String getDisplayName() {
37 | return Messages.primaryip_bylabelselector_failing();
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/main/java/cloud/dnation/jenkins/plugins/hetzner/primaryip/ByLabelSelectorIgnoring.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2022 https://dnation.cloud
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package cloud.dnation.jenkins.plugins.hetzner.primaryip;
17 |
18 | import cloud.dnation.jenkins.plugins.hetzner.Messages;
19 | import edu.umd.cs.findbugs.annotations.NonNull;
20 | import hudson.Extension;
21 | import hudson.model.Descriptor;
22 | import org.jenkinsci.Symbol;
23 | import org.kohsuke.stapler.DataBoundConstructor;
24 |
25 | public class ByLabelSelectorIgnoring extends AbstractByLabelSelector {
26 | @DataBoundConstructor
27 | public ByLabelSelectorIgnoring(String selector) {
28 | super(false, selector);
29 | }
30 |
31 | @Extension
32 | @Symbol("bylabelselector-ignoring")
33 | public static final class DescriptorImpl extends Descriptor {
34 | @NonNull
35 | @Override
36 | public String getDisplayName() {
37 | return Messages.primaryip_bylabelselector_ignoring();
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/main/java/cloud/dnation/jenkins/plugins/hetzner/primaryip/DefaultStrategy.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2022 https://dnation.cloud
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package cloud.dnation.jenkins.plugins.hetzner.primaryip;
17 |
18 | import cloud.dnation.hetznerclient.CreateServerRequest;
19 | import cloud.dnation.hetznerclient.HetznerApi;
20 | import cloud.dnation.jenkins.plugins.hetzner.Messages;
21 | import edu.umd.cs.findbugs.annotations.NonNull;
22 | import hudson.Extension;
23 | import hudson.model.Descriptor;
24 | import org.jenkinsci.Symbol;
25 | import org.kohsuke.stapler.DataBoundConstructor;
26 |
27 | public class DefaultStrategy extends AbstractPrimaryIpStrategy {
28 | public static final DefaultStrategy SINGLETON = new DefaultStrategy();
29 | @DataBoundConstructor
30 | public DefaultStrategy() {
31 | super(false);
32 | }
33 |
34 | @Override
35 | public void applyInternal(HetznerApi api, CreateServerRequest server) {
36 | //NOOP
37 | }
38 |
39 | @Extension
40 | @Symbol("default")
41 | public static final class DescriptorImpl extends Descriptor {
42 | @NonNull
43 | @Override
44 | public String getDisplayName() {
45 | return Messages.primaryip_default();
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/main/java/cloud/dnation/jenkins/plugins/hetzner/shutdown/AbstractShutdownPolicy.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2022 https://dnation.cloud
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package cloud.dnation.jenkins.plugins.hetzner.shutdown;
17 |
18 | import hudson.model.AbstractDescribableImpl;
19 | import hudson.slaves.AbstractCloudComputer;
20 | import hudson.slaves.RetentionStrategy;
21 | import lombok.Getter;
22 |
23 | import java.util.Objects;
24 |
25 | @SuppressWarnings("rawtypes")
26 | public abstract class AbstractShutdownPolicy extends AbstractDescribableImpl {
27 | @Getter
28 | protected final transient RetentionStrategy retentionStrategy;
29 |
30 | protected AbstractShutdownPolicy(RetentionStrategy strategy) {
31 | this.retentionStrategy = Objects.requireNonNull(strategy);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/main/java/cloud/dnation/jenkins/plugins/hetzner/shutdown/BeforeHourWrapsPolicy.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2022 https://dnation.cloud
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package cloud.dnation.jenkins.plugins.hetzner.shutdown;
17 |
18 | import cloud.dnation.jenkins.plugins.hetzner.Helper;
19 | import cloud.dnation.jenkins.plugins.hetzner.HetznerServerAgent;
20 | import cloud.dnation.jenkins.plugins.hetzner.Messages;
21 | import edu.umd.cs.findbugs.annotations.NonNull;
22 | import hudson.Extension;
23 | import hudson.model.Descriptor;
24 | import hudson.slaves.AbstractCloudComputer;
25 | import hudson.slaves.RetentionStrategy;
26 | import lombok.extern.slf4j.Slf4j;
27 | import net.jcip.annotations.GuardedBy;
28 | import org.jenkinsci.Symbol;
29 | import org.kohsuke.stapler.DataBoundConstructor;
30 |
31 | import java.io.IOException;
32 | import java.time.LocalDateTime;
33 |
34 | @Slf4j
35 | public class BeforeHourWrapsPolicy extends AbstractShutdownPolicy {
36 | @SuppressWarnings("rawtypes")
37 | private static final RetentionStrategy STRATEGY_SINGLETON = new RetentionStrategyImpl();
38 |
39 | @DataBoundConstructor
40 | public BeforeHourWrapsPolicy() {
41 | super(STRATEGY_SINGLETON);
42 | }
43 |
44 | @SuppressWarnings("rawtypes")
45 | @Override
46 | public RetentionStrategy getRetentionStrategy() {
47 | return STRATEGY_SINGLETON;
48 | }
49 |
50 | @SuppressWarnings("rawtypes")
51 | private static class RetentionStrategyImpl extends RetentionStrategy {
52 | @Override
53 | public void start(AbstractCloudComputer c) {
54 | c.connect(false);
55 | }
56 |
57 | @Override
58 | @GuardedBy("hudson.model.Queue.lock")
59 | public long check(final AbstractCloudComputer c) {
60 | final HetznerServerAgent agent = (HetznerServerAgent) c.getNode();
61 | if (c.isIdle() && agent != null && agent.getServerInstance() != null) {
62 | if (Helper.canShutdownServer(agent.getServerInstance().getServerDetail().getCreated(),
63 | LocalDateTime.now())) {
64 | log.info("Disconnecting {}", c.getName());
65 | try {
66 | agent.terminate();
67 | } catch (InterruptedException | IOException e) {
68 | log.warn("Failed to terminate {}", c.getName(), e);
69 | }
70 | }
71 | }
72 | return 1;
73 | }
74 | }
75 |
76 | @Extension
77 | @Symbol("hour-wrap")
78 | public static final class DescriptorImpl extends Descriptor {
79 | @NonNull
80 | @Override
81 | public String getDisplayName() {
82 | return Messages.policy_shutdown_beforeHourWrap();
83 | }
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/main/java/cloud/dnation/jenkins/plugins/hetzner/shutdown/IdlePeriodPolicy.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2022 https://dnation.cloud
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package cloud.dnation.jenkins.plugins.hetzner.shutdown;
17 |
18 | import cloud.dnation.jenkins.plugins.hetzner.Messages;
19 | import edu.umd.cs.findbugs.annotations.NonNull;
20 | import hudson.Extension;
21 | import hudson.model.Descriptor;
22 | import hudson.slaves.CloudRetentionStrategy;
23 | import lombok.Getter;
24 | import org.jenkinsci.Symbol;
25 | import org.kohsuke.stapler.DataBoundConstructor;
26 |
27 | public class IdlePeriodPolicy extends AbstractShutdownPolicy {
28 | @Getter
29 | private final int idleMinutes;
30 |
31 | @DataBoundConstructor
32 | public IdlePeriodPolicy(int idleMinutes) {
33 | super(new CloudRetentionStrategy(idleMinutes));
34 | this.idleMinutes = idleMinutes;
35 | }
36 |
37 | @Extension
38 | @Symbol("idle")
39 | public static final class DescriptorImpl extends Descriptor {
40 | @NonNull
41 | @Override
42 | public String getDisplayName() {
43 | return Messages.policy_shutdown_idle();
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/main/resources/cloud/dnation/jenkins/plugins/hetzner/HetznerCloud/config.jelly:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
42 |
--------------------------------------------------------------------------------
/src/main/resources/cloud/dnation/jenkins/plugins/hetzner/HetznerCloud/help-credentialsId.html:
--------------------------------------------------------------------------------
1 |
16 |
17 | Select text credentials containing token to access Hetzner project
18 |
19 |
--------------------------------------------------------------------------------
/src/main/resources/cloud/dnation/jenkins/plugins/hetzner/HetznerCloud/help-name.html:
--------------------------------------------------------------------------------
1 |
16 |
17 | Provide a name for this Hetzner Cloud.
18 | Must be a valid
label value.
19 |
20 | Valid label values must be a string of 63 characters or less and must be empty or begin and end with an alphanumeric character ([a-z0-9A-Z]) with dashes (-), underscores (_), dots (.), and alphanumerics between.
21 |
22 |
23 |
--------------------------------------------------------------------------------
/src/main/resources/cloud/dnation/jenkins/plugins/hetzner/HetznerServerAgent/configure-entries.jelly:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 | STOP! You are not supposed to create or re-configure server in Hetzner cloud this way.
19 |
20 | Servers are provisioned as they are needed, based on labels assigned to them.
21 |
22 | An attempt to save this configuration will lead to an error.
23 |
24 | You can still create server via Hetzner Cloud console and add it as
25 | Permanent Agent.
26 |
27 |
28 |
--------------------------------------------------------------------------------
/src/main/resources/cloud/dnation/jenkins/plugins/hetzner/HetznerServerComputer/main.jelly:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 | Server details
19 |
20 |
21 | ${%No details available}
22 |
23 |
24 |
25 |
26 |
27 |
28 | Server Id |
29 | ${instance.id} |
30 |
31 |
32 | Template |
33 | ${template.name} |
34 |
35 |
36 | Name |
37 | ${instance.name} |
38 |
39 |
40 | Created |
41 | ${instance.created} |
42 |
43 |
44 | Status |
45 | ${instance.status} |
46 |
47 |
48 | Image |
49 | ${instance.image.description} |
50 |
51 |
52 | vCPU |
53 | ${instance.serverType.cores} |
54 |
55 |
56 | RAM |
57 | ${instance.serverType.memory} GB |
58 |
59 |
60 | Disk |
61 | ${instance.serverType.disk} GB |
62 |
63 |
64 | Public IPv4 address |
65 | ${instance.publicNet.ipv4.ip} |
66 |
67 |
68 | Public IPv6 address |
69 | ${instance.publicNet.ipv6.ip} |
70 |
71 |
72 |
73 |
74 | Private IPv4 address |
75 | ${instance.privateNet.get(0).ip} |
76 |
77 |
78 |
79 |
80 | Datacenter |
81 | ${instance.datacenter.name} |
82 |
83 |
84 | Location |
85 | ${instance.datacenter.location.description} |
86 |
87 |
88 | City |
89 | ${instance.datacenter.location.city} |
90 |
91 |
92 |
93 |
94 |
95 |
--------------------------------------------------------------------------------
/src/main/resources/cloud/dnation/jenkins/plugins/hetzner/HetznerServerTemplate/config.jelly:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
30 |
31 |
32 |
33 |
35 |
36 |
37 |
38 |
40 |
41 |
42 |
43 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
--------------------------------------------------------------------------------
/src/main/resources/cloud/dnation/jenkins/plugins/hetzner/HetznerServerTemplate/help-automountVolumes.html:
--------------------------------------------------------------------------------
1 |
16 |
17 | Auto-mount Volumes after attach.
18 |
19 |
--------------------------------------------------------------------------------
/src/main/resources/cloud/dnation/jenkins/plugins/hetzner/HetznerServerTemplate/help-bootDeadline.html:
--------------------------------------------------------------------------------
1 |
16 |
17 | Configure maximum allowed time for VM to boot (for server to have status running
).
18 |
19 |
--------------------------------------------------------------------------------
/src/main/resources/cloud/dnation/jenkins/plugins/hetzner/HetznerServerTemplate/help-connectivity.html:
--------------------------------------------------------------------------------
1 |
16 |
17 | Network connectivity that is configured on server.
18 |
19 |
20 | Make sure to align with connection method
21 |
--------------------------------------------------------------------------------
/src/main/resources/cloud/dnation/jenkins/plugins/hetzner/HetznerServerTemplate/help-connector.html:
--------------------------------------------------------------------------------
1 |
16 |
17 | Method of connecting Jenkins master with newly provisioned server.
18 |
--------------------------------------------------------------------------------
/src/main/resources/cloud/dnation/jenkins/plugins/hetzner/HetznerServerTemplate/help-image.html:
--------------------------------------------------------------------------------
1 |
16 |
17 | Image label expression or image ID.
18 | When label expression is provided, it is expected that it will resolve to single image.
19 | Only type
snapshot with status
available is considered.
20 | Alternatively you can supply image ID.
21 |
22 | To obtain list of all images, use following
curl
command:
23 |
24 |
25 | curl -H "Authorization: Bearer $API_TOKEN" https://api.hetzner.cloud/v1/images
26 |
27 |
28 |
API documentation
29 |
30 |
--------------------------------------------------------------------------------
/src/main/resources/cloud/dnation/jenkins/plugins/hetzner/HetznerServerTemplate/help-jvmOpts.html:
--------------------------------------------------------------------------------
1 |
16 |
17 | Additional JVM options for agent.
18 |
19 |
--------------------------------------------------------------------------------
/src/main/resources/cloud/dnation/jenkins/plugins/hetzner/HetznerServerTemplate/help-labelStr.html:
--------------------------------------------------------------------------------
1 |
16 |
17 | Labels that identifies jobs that could run on node created from this template.
18 | Multiple values can be specified when separated by space.
19 | When no labels are specified and usage mode is set to Use this node as much as possible,
20 | then no restrictions will apply and node will be eligible to execute any job.
21 |
22 |
--------------------------------------------------------------------------------
/src/main/resources/cloud/dnation/jenkins/plugins/hetzner/HetznerServerTemplate/help-location.html:
--------------------------------------------------------------------------------
1 |
16 |
17 | Name of location or datacenter where to create server such as
fsn1
or
nbg1-dc3
18 |
19 | To obtain list of locations, you can use following curl command:
20 |
21 |
22 | curl -H "Authorization: Bearer $API_TOKEN" https://api.hetzner.cloud/v1/locations
23 |
24 |
25 |
API documentation
26 |
27 |
28 | To obtain list of datacenters, you can use following curl command:
29 |
30 |
31 | curl -H "Authorization: Bearer $API_TOKEN" https://api.hetzner.cloud/v1/datacenters
32 |
33 |
34 |
35 |
API documentation
36 |
37 |
--------------------------------------------------------------------------------
/src/main/resources/cloud/dnation/jenkins/plugins/hetzner/HetznerServerTemplate/help-name.html:
--------------------------------------------------------------------------------
1 |
16 |
17 | Name of server template.
18 |
19 |
--------------------------------------------------------------------------------
/src/main/resources/cloud/dnation/jenkins/plugins/hetzner/HetznerServerTemplate/help-network.html:
--------------------------------------------------------------------------------
1 |
16 |
17 | Network label expression or network ID.
18 | When label expression is provided, it is expected that it will resolve to single network.
19 | Alternatively you can supply network ID.
20 |
21 | To obtain list of all networks, use following
curl
command:
22 |
23 |
24 | curl -H "Authorization: Bearer $API_TOKEN" https://api.hetzner.cloud/v1/networks
25 |
26 |
27 |
API documentation
28 |
29 |
--------------------------------------------------------------------------------
/src/main/resources/cloud/dnation/jenkins/plugins/hetzner/HetznerServerTemplate/help-numExecutors.html:
--------------------------------------------------------------------------------
1 |
16 |
17 | Allows set the number of executors on provisioned server.
18 |
19 |
--------------------------------------------------------------------------------
/src/main/resources/cloud/dnation/jenkins/plugins/hetzner/HetznerServerTemplate/help-placementGroup.html:
--------------------------------------------------------------------------------
1 |
16 |
17 | ID of
placement group
18 | or label expression that resolves to single placement group
19 | To obtain list of all placement groups, use following
curl
command:
20 |
21 |
22 | curl -H "Authorization: Bearer $API_TOKEN" https://api.hetzner.cloud/v1/placement_groups
23 |
24 |
25 |
API documentation
26 |
27 |
28 |
--------------------------------------------------------------------------------
/src/main/resources/cloud/dnation/jenkins/plugins/hetzner/HetznerServerTemplate/help-primaryIp.html:
--------------------------------------------------------------------------------
1 |
16 |
17 | Defines how
Primary IP is allocated to the server.
18 |
19 |
--------------------------------------------------------------------------------
/src/main/resources/cloud/dnation/jenkins/plugins/hetzner/HetznerServerTemplate/help-remoteFs.html:
--------------------------------------------------------------------------------
1 |
16 |
17 | Agent working directory. See
here for more details.
18 |
19 |
--------------------------------------------------------------------------------
/src/main/resources/cloud/dnation/jenkins/plugins/hetzner/HetznerServerTemplate/help-serverType.html:
--------------------------------------------------------------------------------
1 |
16 |
17 | Name of server type to use for creating server such as
cx21.
18 | To obtain list of server types, you can use following
curl
command:
19 |
20 |
21 | curl -H "Authorization: Bearer $API_TOKEN" https://api.hetzner.cloud/v1/server_types
22 |
23 |
24 |
API documentation
25 |
26 |
--------------------------------------------------------------------------------
/src/main/resources/cloud/dnation/jenkins/plugins/hetzner/HetznerServerTemplate/help-shutdownPolicy.html:
--------------------------------------------------------------------------------
1 |
16 |
17 | Defines how idle server is shutdown.
18 |
19 |
--------------------------------------------------------------------------------
/src/main/resources/cloud/dnation/jenkins/plugins/hetzner/HetznerServerTemplate/help-userData.html:
--------------------------------------------------------------------------------
1 |
16 |
17 | Cloud-Init user data to use during Server creation.
18 | This field is limited to 32KiB by Hetzner Cloud itself.
19 |
20 |
--------------------------------------------------------------------------------
/src/main/resources/cloud/dnation/jenkins/plugins/hetzner/HetznerServerTemplate/help-volumeIds.html:
--------------------------------------------------------------------------------
1 |
16 |
17 | Volume IDs which should be attached to the Server at the creation time. Volumes must be in the same Location.
18 | Note that volumes can be mounted into single server at the time.
19 |
20 |
--------------------------------------------------------------------------------
/src/main/resources/cloud/dnation/jenkins/plugins/hetzner/Messages.properties:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright 2021 https://dnation.cloud
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 |
16 | cloudConfigPassed=Cloud configuration seems to be valid
17 | plugin.description=Plugin for launching build Agents as Hetzner compute resources
18 | plugin.displayName=Hetzner
19 | serverTemplate.displayName=Hetzner server template
20 | connector.SshAsRoot=Connect via SSH as root, but launch agent as user configured in credentials
21 | connector.SshAsNonRoot=Connect via SSH as user configured in credentials
22 | policy.shutdown.idle=Removes server after it's idle for period of time
23 | policy.shutdown.beforeHourWrap=Removes idle server just before current hour of billing cycle completes
24 | primaryip.default=Use default behavior
25 | primaryip.bylabelselector.failing=Allocate primary IPv4 using label selector, fail if none is available
26 | primaryip.bylabelselector.ignoring=Allocate primary IPv4 using label selector, ignore any error
27 | connection.method.default=Connect using private IPv4 address if available, otherwise using public IPv4 address
28 | connection.method.defaultV6=Connect using private IPv4 address if available, otherwise using public IPv6 address
29 | connection.method.public=Connect using public IPv4 address only
30 | connection.method.publicV6=Connect using public IPv6 address only
31 | connectivity.private-only=Only private networking will be used. Network ID or label expression must be provided as well.
32 | connectivity.public-only=Only public networking will be allocated
33 | connectivity.publicV6-only=Only public IPv6 networking will be allocated
34 | connectivity.both=Configure both private and public networking. Additional constrains may apply
35 | connectivity.bothV6=Configure both private network and public IPv6. Additional constrains may apply
--------------------------------------------------------------------------------
/src/main/resources/cloud/dnation/jenkins/plugins/hetzner/launcher/AbstractHetznerSshConnector/config.jelly:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/src/main/resources/cloud/dnation/jenkins/plugins/hetzner/primaryip/ByLabelSelectorFailing/config.jelly:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/main/resources/cloud/dnation/jenkins/plugins/hetzner/primaryip/ByLabelSelectorFailing/help-selector.html:
--------------------------------------------------------------------------------
1 |
16 |
17 | Label selector used to filter existing Primary IP addresses.
18 |
19 |
--------------------------------------------------------------------------------
/src/main/resources/cloud/dnation/jenkins/plugins/hetzner/primaryip/ByLabelSelectorIgnoring/config.jelly:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/main/resources/cloud/dnation/jenkins/plugins/hetzner/primaryip/ByLabelSelectorIgnoring/help-selector.html:
--------------------------------------------------------------------------------
1 |
16 |
17 | Label selector used to filter existing Primary IP addresses.
18 |
19 |
--------------------------------------------------------------------------------
/src/main/resources/cloud/dnation/jenkins/plugins/hetzner/shutdown/BeforeHourWrapsPolicy/help.html:
--------------------------------------------------------------------------------
1 |
16 |
17 | Server will be kept alive until current hour of billing cycle completes.
18 | Make sure that Jenkins controller's clock are configured correctly as skew may lead to 1 hour over-billing.
19 |
20 |
--------------------------------------------------------------------------------
/src/main/resources/cloud/dnation/jenkins/plugins/hetzner/shutdown/IdlePeriodPolicy/config.jelly:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/main/resources/cloud/dnation/jenkins/plugins/hetzner/shutdown/IdlePeriodPolicy/help-idleMinutes.html:
--------------------------------------------------------------------------------
1 |
16 |
17 | Server will be kept alive for defined number of minutes after being idle.
18 |
19 |
--------------------------------------------------------------------------------
/src/main/resources/index.jelly:
--------------------------------------------------------------------------------
1 |
16 |
17 |
--------------------------------------------------------------------------------
/src/test/java/cloud/dnation/jenkins/plugins/hetzner/HelperTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 https://dnation.cloud
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package cloud.dnation.jenkins.plugins.hetzner;
17 |
18 | import org.junit.Test;
19 |
20 | import java.io.IOException;
21 | import java.time.LocalDateTime;
22 | import java.time.format.DateTimeFormatter;
23 |
24 | import static org.junit.Assert.assertEquals;
25 | import static org.junit.Assert.assertFalse;
26 | import static org.junit.Assert.assertTrue;
27 |
28 | public class HelperTest {
29 | @Test
30 | public void testExtractPublicKeyRSA() throws IOException {
31 | final String pubKeyStr = TestHelper.resourceAsString("id_rsa.pub");
32 | final String privKeyStr = TestHelper.resourceAsString("id_rsa");
33 | assertEquals(pubKeyStr, Helper.getSSHPublicKeyFromPrivate(privKeyStr, null));
34 | }
35 |
36 | @Test
37 | public void testExtractPublicKeyED25519() throws IOException {
38 | final String pubKeyStr = TestHelper.resourceAsString("id_ed25519.pub");
39 | final String privKeyStr = TestHelper.resourceAsString("id_ed25519");
40 | assertEquals(pubKeyStr, Helper.getSSHPublicKeyFromPrivate(privKeyStr, null));
41 | }
42 |
43 | private static LocalDateTime time(String str) {
44 | return LocalDateTime.from(DateTimeFormatter.ISO_DATE_TIME.parse(str + "+02:00"));
45 | }
46 |
47 | @Test
48 | public void testCanShutdownServer() {
49 | //server started at 10:41 UTC, so it can be shutdown in minutes 36-40
50 | String str = "2022-05-21T10:41:19+00:00";
51 | assertFalse(Helper.canShutdownServer(str, time("2022-05-21T10:50:11")));
52 | assertTrue(Helper.canShutdownServer(str, time("2022-05-21T11:36:19")));
53 | assertTrue(Helper.canShutdownServer(str, time("2022-05-21T11:40:13")));
54 | assertFalse(Helper.canShutdownServer(str, time("2022-05-21T10:41:14")));
55 | //server started at 10:01, so it can be shutdown in minutes 56-00
56 | str = "2022-05-21T10:01:19+00:00";
57 | assertFalse(Helper.canShutdownServer(str, time("2022-05-21T10:55:15")));
58 | assertTrue(Helper.canShutdownServer(str, time("2022-05-21T10:56:19")));
59 | assertTrue(Helper.canShutdownServer(str, time("2022-05-21T10:59:17")));
60 | assertFalse(Helper.canShutdownServer(str, time("2022-05-21T10:00:18")));
61 | assertTrue(Helper.canShutdownServer(str, time("2022-05-21T11:00:18")));
62 | assertFalse(Helper.canShutdownServer(str, time("2022-05-21T10:01:19")));
63 | assertFalse(Helper.canShutdownServer(str, time("2022-05-21T10:32:20")));
64 | str = "2022-08-08T11:03:55+00:00";
65 | assertFalse(Helper.canShutdownServer(str, time("2022-08-08T11:03:02")));
66 | assertTrue(Helper.canShutdownServer(str, time("2022-08-08T11:59:02")));
67 | }
68 |
69 | @Test
70 | public void testIsPossiblyLong() {
71 | assertTrue(Helper.isPossiblyLong("1"));
72 | assertFalse(Helper.isPossiblyLong("0"));
73 | assertFalse(Helper.isPossiblyLong("not-a-number"));
74 | }
75 |
76 | @Test
77 | public void testIsValidLabelValue() {
78 | assertFalse(Helper.isValidLabelValue(""));
79 | assertFalse(Helper.isValidLabelValue(null));
80 | assertTrue(Helper.isValidLabelValue("cloud-01"));
81 | assertTrue(Helper.isValidLabelValue("cloud_01"));
82 | assertFalse(Helper.isValidLabelValue("cloud 01"));
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/test/java/cloud/dnation/jenkins/plugins/hetzner/HetznerCloudResourceManagerTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2024 https://dnation.cloud
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package cloud.dnation.jenkins.plugins.hetzner;
17 |
18 | import cloud.dnation.hetznerclient.CreateServerRequest;
19 | import cloud.dnation.jenkins.plugins.hetzner.connect.ConnectivityType;
20 | import org.junit.Test;
21 |
22 | import java.io.IOException;
23 |
24 | import static cloud.dnation.jenkins.plugins.hetzner.HetznerCloudResourceManager.customizeNetworking;
25 | import static org.junit.Assert.assertEquals;
26 |
27 | public class HetznerCloudResourceManagerTest {
28 | @Test
29 | public void testCustomizeNetworking() throws IOException {
30 | CreateServerRequest req;
31 |
32 | req = new CreateServerRequest();
33 | customizeNetworking(ConnectivityType.BOTH, req, "", (s1, s2) -> {
34 | });
35 | assertEquals(true, req.getPublicNet().getEnableIpv4());
36 | assertEquals(true, req.getPublicNet().getEnableIpv6());
37 |
38 | req = new CreateServerRequest();
39 | customizeNetworking(ConnectivityType.BOTH_V6, req, "", (s1, s2) -> {
40 | });
41 | assertEquals(false, req.getPublicNet().getEnableIpv4());
42 | assertEquals(true, req.getPublicNet().getEnableIpv6());
43 |
44 | req = new CreateServerRequest();
45 | customizeNetworking(ConnectivityType.PUBLIC, req, "", (s1, s2) -> {
46 | });
47 | assertEquals(true, req.getPublicNet().getEnableIpv4());
48 | assertEquals(true, req.getPublicNet().getEnableIpv6());
49 |
50 | req = new CreateServerRequest();
51 | customizeNetworking(ConnectivityType.PUBLIC_V6, req, "", (s1, s2) -> {
52 | });
53 | assertEquals(false, req.getPublicNet().getEnableIpv4());
54 | assertEquals(true, req.getPublicNet().getEnableIpv6());
55 |
56 | req = new CreateServerRequest();
57 | customizeNetworking(ConnectivityType.PRIVATE, req, "", (s1, s2) -> {
58 | });
59 | assertEquals(false, req.getPublicNet().getEnableIpv4());
60 | assertEquals(false, req.getPublicNet().getEnableIpv6());
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/test/java/cloud/dnation/jenkins/plugins/hetzner/HetznerCloudSimpleTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 https://dnation.cloud
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package cloud.dnation.jenkins.plugins.hetzner;
17 |
18 | import cloud.dnation.jenkins.plugins.hetzner.launcher.AbstractHetznerSshConnector;
19 | import com.google.common.collect.Iterables;
20 | import com.google.common.collect.Lists;
21 | import hudson.model.Node;
22 | import hudson.model.labels.LabelAtom;
23 | import hudson.slaves.Cloud;
24 | import hudson.slaves.NodeProvisioner;
25 | import jenkins.model.Jenkins;
26 | import lombok.extern.slf4j.Slf4j;
27 | import org.junit.After;
28 | import org.junit.Before;
29 | import org.junit.Test;
30 | import org.junit.runner.RunWith;
31 | import org.mockito.MockedStatic;
32 | import org.mockito.Mockito;
33 | import org.mockito.stubbing.Answer;
34 |
35 | import java.io.IOException;
36 | import java.util.Collection;
37 | import java.util.concurrent.TimeUnit;
38 |
39 | import static org.awaitility.Awaitility.await;
40 | import static org.junit.Assert.assertFalse;
41 | import static org.junit.Assert.assertTrue;
42 | import static org.mockito.ArgumentMatchers.anyString;
43 | import static org.mockito.Mockito.doAnswer;
44 | import static org.mockito.Mockito.mock;
45 | import static org.mockito.Mockito.times;
46 | import static org.mockito.Mockito.verify;
47 |
48 | @Slf4j
49 | public class HetznerCloudSimpleTest {
50 | private HetznerCloudResourceManager rsrcMgr;
51 |
52 | MockedStatic jenkinsMock;
53 | MockedStatic hetznerCloudResourceManagerMockedStatic;
54 | @Before
55 | public void setupBefore() {
56 | jenkinsMock = Mockito.mockStatic(Jenkins.class);
57 | hetznerCloudResourceManagerMockedStatic = Mockito.mockStatic(HetznerCloudResourceManager.class);
58 | //PowerMockito.mockStatic(Jenkins.class, HetznerCloudResourceManager.class);
59 | rsrcMgr = mock(HetznerCloudResourceManager.class);
60 | Mockito.when(HetznerCloudResourceManager.create(anyString())).thenReturn(rsrcMgr);
61 |
62 | Jenkins jenkins = mock(Jenkins.class);
63 | doAnswer((Answer) invocationOnMock -> new LabelAtom(invocationOnMock.getArgument(0)))
64 | .when(jenkins).getLabelAtom(anyString());
65 | Mockito.when(Jenkins.get()).thenReturn(jenkins);
66 | }
67 |
68 | @After
69 | public void cleanMock() {
70 | jenkinsMock.close();
71 | hetznerCloudResourceManagerMockedStatic.close();
72 | }
73 |
74 | @Test
75 | public void testCanProvision() throws IOException {
76 |
77 | HetznerServerTemplate template1 = new HetznerServerTemplate("template-1", "java",
78 | "name=img1", "nbg1", "cx21");
79 | final AbstractHetznerSshConnector connector = mock(AbstractHetznerSshConnector.class);
80 | template1.setConnector(connector);
81 |
82 | final HetznerCloud cloud = new HetznerCloud("hcloud-01", "mock-credentials", "10",
83 | Lists.newArrayList(template1));
84 | Cloud.CloudState cloudState = new Cloud.CloudState(new LabelAtom("java"), 1);
85 | assertTrue(cloud.canProvision(cloudState));
86 |
87 | final Collection plannedNodes = cloud.provision(cloudState, 1);
88 | final NodeProvisioner.PlannedNode node = Iterables.getOnlyElement(plannedNodes);
89 | await().atMost(30, TimeUnit.SECONDS).until(node.future::isDone);
90 | verify(connector, times(1)).createLauncher();
91 | verify(rsrcMgr, times(1)).fetchAllServers(anyString());
92 |
93 | Cloud.CloudState cloudState2 = new Cloud.CloudState(new LabelAtom("unknown"), 1);
94 | assertFalse(cloud.canProvision(cloudState2));
95 | }
96 |
97 | @Test
98 | public void testCannotProvisionInExclusiveMode() {
99 | HetznerServerTemplate tmpl1 = new HetznerServerTemplate("tmpl1", "label1", "img1", "fsn1", "cx31");
100 | tmpl1.setMode(Node.Mode.EXCLUSIVE);
101 | final HetznerCloud cloud = new HetznerCloud("hcloud-01", "mock-credentials", "10",
102 | Lists.newArrayList(tmpl1)
103 | );
104 | Cloud.CloudState cloudState = new Cloud.CloudState(new LabelAtom("java"), 1);
105 | assertFalse(cloud.canProvision(cloudState));
106 | }
107 |
108 | @Test
109 | public void testCanProvisionInNormalMode() {
110 | HetznerServerTemplate tmpl1 = new HetznerServerTemplate("tmpl1", null, "img1", "fsn1", "cx31");
111 | tmpl1.setMode(Node.Mode.NORMAL);
112 | final HetznerCloud cloud = new HetznerCloud("hcloud-01", "mock-credentials", "10",
113 | Lists.newArrayList(tmpl1)
114 | );
115 | Cloud.CloudState cloudState = new Cloud.CloudState(new LabelAtom("java"), 1);
116 | assertTrue(cloud.canProvision(cloudState));
117 | }
118 |
119 | //see https://github.com/jenkinsci/hetzner-cloud-plugin/issues/15
120 | @Test
121 | public void testCanProvisionNullJobLabel() {
122 | HetznerServerTemplate tmpl1 = new HetznerServerTemplate("tmpl1", null, "img1", "fsn1", "cx31");
123 | tmpl1.setMode(Node.Mode.NORMAL);
124 | HetznerServerTemplate tmpl2 = new HetznerServerTemplate("tmpl1", "label2,label3", "img1", "fsn1", "cx31");
125 | tmpl2.setMode(Node.Mode.EXCLUSIVE);
126 | final HetznerCloud cloud = new HetznerCloud("hcloud-01", "mock-credentials", "10",
127 | Lists.newArrayList(tmpl1, tmpl2)
128 | );
129 | Cloud.CloudState cloudState = new Cloud.CloudState(null, 1);
130 | assertTrue(cloud.canProvision(cloudState));
131 | Cloud.CloudState cloudState2 = new Cloud.CloudState(new LabelAtom("label3"), 1);
132 | assertTrue(cloud.canProvision(cloudState2));
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/src/test/java/cloud/dnation/jenkins/plugins/hetzner/HetznerCloudTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 https://dnation.cloud
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package cloud.dnation.jenkins.plugins.hetzner;
17 |
18 | import org.htmlunit.html.DomElement;
19 | import org.htmlunit.html.HtmlPage;
20 | import org.junit.Assert;
21 | import org.junit.Rule;
22 | import org.junit.Test;
23 | import org.jvnet.hudson.test.JenkinsRule;
24 |
25 | import java.util.ArrayList;
26 |
27 | public class HetznerCloudTest {
28 | @Rule
29 | public JenkinsRule j = new JenkinsRule();
30 |
31 | @Test
32 | public void test() throws Exception {
33 | final HetznerCloud cloud = new HetznerCloud("hcloud-01", "mock-credentials", "10",
34 | new ArrayList<>());
35 | j.jenkins.clouds.add(cloud);
36 | j.jenkins.save();
37 | try (JenkinsRule.WebClient wc = j.createWebClient()) {
38 | HtmlPage p = wc.goTo("manage/cloud/");
39 | DomElement domElement = p.getElementById("cloud_" + cloud.name);
40 | Assert.assertNotNull(domElement);
41 | p = wc.goTo("manage/cloud/hcloud-01/configure");
42 | Assert.assertTrue("No input with value " + cloud.name, p.getElementsByTagName("input").stream()
43 | .filter(element -> element.hasAttribute("value"))
44 | .anyMatch(element -> cloud.name.equals(element.getAttribute("value"))));
45 |
46 |
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/test/java/cloud/dnation/jenkins/plugins/hetzner/JCasCTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 https://dnation.cloud
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package cloud.dnation.jenkins.plugins.hetzner;
17 |
18 | import io.jenkins.plugins.casc.ConfigurationContext;
19 | import io.jenkins.plugins.casc.ConfiguratorRegistry;
20 | import io.jenkins.plugins.casc.misc.ConfiguredWithCode;
21 | import io.jenkins.plugins.casc.misc.JenkinsConfiguredWithCodeRule;
22 | import io.jenkins.plugins.casc.model.CNode;
23 | import jenkins.model.Jenkins;
24 | import org.junit.ClassRule;
25 | import org.junit.Test;
26 |
27 | import static io.jenkins.plugins.casc.misc.Util.getJenkinsRoot;
28 | import static org.junit.Assert.assertEquals;
29 | import static org.junit.Assert.assertNotNull;
30 |
31 | public class JCasCTest {
32 |
33 | @ClassRule
34 | @ConfiguredWithCode("jcasc.yaml")
35 | public static JenkinsConfiguredWithCodeRule j = new JenkinsConfiguredWithCodeRule();
36 |
37 | @Test
38 | public void testConfigure() {
39 | final HetznerCloud cloud = (HetznerCloud) Jenkins.get().clouds.getByName("hcloud-01");
40 | assertNotNull(cloud);
41 | assertEquals("hcloud-01", cloud.getDisplayName());
42 | assertEquals(10, cloud.getInstanceCap());
43 | assertEquals(1, cloud.getServerTemplates().size());
44 | assertEquals("name=jenkins", cloud.getServerTemplates().get(0).getImage());
45 | assertEquals("fsn1", cloud.getServerTemplates().get(0).getLocation());
46 | }
47 |
48 | @Test
49 | public void testExport() throws Exception {
50 | final ConfigurationContext ctx = new ConfigurationContext(ConfiguratorRegistry.get());
51 | final CNode cloud = getJenkinsRoot(ctx).get("clouds");
52 | assertNotNull(cloud);
53 | }
54 | }
55 |
56 |
--------------------------------------------------------------------------------
/src/test/java/cloud/dnation/jenkins/plugins/hetzner/TestHelper.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 https://dnation.cloud
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package cloud.dnation.jenkins.plugins.hetzner;
17 |
18 | import com.google.common.io.ByteStreams;
19 | import com.google.common.io.Resources;
20 | import lombok.experimental.UtilityClass;
21 |
22 | import java.io.ByteArrayOutputStream;
23 | import java.io.IOException;
24 | import java.io.InputStream;
25 | import java.nio.charset.StandardCharsets;
26 |
27 | @UtilityClass
28 | public class TestHelper {
29 | public static String inputStreamAsString(InputStream is) throws IOException {
30 | ByteArrayOutputStream os = new ByteArrayOutputStream();
31 | ByteStreams.copy(is, os);
32 | return os.toString(StandardCharsets.UTF_8.name());
33 | }
34 |
35 | public static String resourceAsString(String name) throws IOException {
36 | try (InputStream is = Resources.getResource(name).openStream()) {
37 | return inputStreamAsString(is);
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/test/java/cloud/dnation/jenkins/plugins/hetzner/launcher/TestPublicV6AddressOnly.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2024 https://dnation.cloud
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package cloud.dnation.jenkins.plugins.hetzner.launcher;
17 |
18 | import cloud.dnation.hetznerclient.Ipv4Detail;
19 | import cloud.dnation.hetznerclient.Ipv6Detail;
20 | import cloud.dnation.hetznerclient.PublicNetDetail;
21 | import cloud.dnation.hetznerclient.ServerDetail;
22 | import org.junit.Test;
23 |
24 | import static org.junit.Assert.assertEquals;
25 | import static org.junit.Assert.fail;
26 |
27 | public class TestPublicV6AddressOnly {
28 | @Test(expected = IllegalArgumentException.class)
29 | public void testMissingV6Address() {
30 | final PublicV6AddressOnly addr = new PublicV6AddressOnly();
31 | addr.getAddress(new ServerDetail().publicNet(new PublicNetDetail().ipv4(new Ipv4Detail())));
32 | fail();
33 | }
34 |
35 | @Test
36 | public void testValid() {
37 | final PublicV6AddressOnly addr = new PublicV6AddressOnly();
38 | final String res = addr.getAddress(new ServerDetail().publicNet(
39 | new PublicNetDetail().ipv6(new Ipv6Detail().ip("2a01:4e3:a0a:9b7b::/64"))));
40 | assertEquals("2a01:4e3:a0a:9b7b::1", res);
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/test/java/cloud/dnation/jenkins/plugins/hetzner/primaryip/PrimaryIpStrategyTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2022 https://dnation.cloud
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package cloud.dnation.jenkins.plugins.hetzner.primaryip;
17 |
18 | import cloud.dnation.hetznerclient.CreateServerRequest;
19 | import cloud.dnation.hetznerclient.DatacenterDetail;
20 | import cloud.dnation.hetznerclient.LocationDetail;
21 | import cloud.dnation.hetznerclient.PrimaryIpDetail;
22 | import org.junit.Test;
23 |
24 | import static cloud.dnation.jenkins.plugins.hetzner.primaryip.AbstractByLabelSelector.isIpUsable;
25 | import static org.junit.Assert.assertFalse;
26 | import static org.junit.Assert.assertTrue;
27 |
28 | public class PrimaryIpStrategyTest {
29 | private static final DatacenterDetail FSN1DC14;
30 | private static final DatacenterDetail NBG1DC4;
31 | private static final LocationDetail FSN1;
32 | private static final LocationDetail NBG1;
33 | static {
34 | FSN1 = new LocationDetail();
35 | FSN1.setName("fsn1");
36 | FSN1DC14 = new DatacenterDetail();
37 | FSN1DC14.setName("fsn1-dc14");
38 | FSN1DC14.setLocation(FSN1);
39 | NBG1 = new LocationDetail();
40 | NBG1.setName("nbg1");
41 | NBG1DC4 = new DatacenterDetail();
42 | NBG1DC4.setName("nbg1-dc3");
43 | NBG1DC4.setLocation(NBG1);
44 | }
45 | @Test
46 | public void testIpIsUsable() {
47 | final CreateServerRequest server = new CreateServerRequest();
48 | final PrimaryIpDetail ip = new PrimaryIpDetail();
49 | //Same datacenter
50 | server.setDatacenter(FSN1DC14.getName());
51 | ip.setDatacenter(FSN1DC14);
52 | assertTrue(isIpUsable(ip, server));
53 |
54 | //Same location
55 | server.setDatacenter(null);
56 | server.setLocation("fsn1");
57 | assertTrue(isIpUsable(ip, server));
58 |
59 | //Different datacenter
60 | ip.setDatacenter(NBG1DC4);
61 | server.setDatacenter(FSN1DC14.getName());
62 | server.setLocation(null);
63 | assertFalse(isIpUsable(ip, server));
64 |
65 | //Already allocated
66 | ip.setAssigneeId(0L);
67 | assertFalse(isIpUsable(ip, server));
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/test/resources/cloud/dnation/jenkins/plugins/hetzner/jcasc.yaml:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright 2021 https://dnation.cloud
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 | ---
16 | jenkins:
17 | clouds:
18 | - hetzner:
19 | name: "hcloud-01"
20 | credentialsId: "hcloud-api-token"
21 | instanceCapStr: "10"
22 | serverTemplates:
23 | - name: ubuntu2-cx21
24 | serverType: cx21
25 | remoteFs: /var/lib/jenkins
26 | location: fsn1
27 | image: name=jenkins
28 | labelStr: java
29 | connector:
30 | root:
31 | sshCredentialsId: 'ssh-private-key'
32 | credentials:
33 | system:
34 | domainCredentials:
35 | - credentials:
36 | - stringCredentialsImpl:
37 | scope: SYSTEM
38 | id: "hcloud-api-token"
39 | description: "Hetzner cloud API token"
40 | secret: "aUJjQICU4jOrOqISL4kCwgcgh6weyHm0btc0lxScpMa3s28ci+7h7mtp3PLeHPPwCGCbWxG8iA0r"
41 | - basicSSHUserPrivateKey:
42 | scope: SYSTEM
43 | id: "ssh-private-key"
44 | username: "jenkins"
45 | privateKeySource:
46 | directEntry:
47 | privateKey: |
48 | -----BEGIN OPENSSH PRIVATE KEY-----
49 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn
50 | NhAAAAAwEAAQAAAQEAyMm0501e8CnMGO72c5fihC//KhdOGahgvbCGbS5mOk4zfpvo+Jui
51 | 3hIJOOt6DW2atAabo8wUXLg9KH9cXHmKs6SMGuRd7m5fdTaUzO0G//ZmzX51phqSYv6JsU
52 | n/vKxqOeVtIWzXHuC+lOtEyR+BKA2UQJOTReO4dv8oAt9cBGSN4gOItc7s07ggK+ZD+S2u
53 | gSTpf5gXYnD9qq9TFuEyMFLJi9gWE/M3c/ZeeDyfTeVeYdbaq2qI3NNUtUYXB4Es0XsdzR
54 | ZSfMhPggJaw8jhlMYFnJE+8O2gJiWrvky1NYhouRD2QzdTCwDoG58AK2Epnd0wIE1iDAHU
55 | 6ZVQaj1BawAAA8iwi+/UsIvv1AAAAAdzc2gtcnNhAAABAQDIybTnTV7wKcwY7vZzl+KEL/
56 | 8qF04ZqGC9sIZtLmY6TjN+m+j4m6LeEgk463oNbZq0BpujzBRcuD0of1xceYqzpIwa5F3u
57 | bl91NpTM7Qb/9mbNfnWmGpJi/omxSf+8rGo55W0hbNce4L6U60TJH4EoDZRAk5NF47h2/y
58 | gC31wEZI3iA4i1zuzTuCAr5kP5La6BJOl/mBdicP2qr1MW4TIwUsmL2BYT8zdz9l54PJ9N
59 | 5V5h1tqraojc01S1RhcHgSzRex3NFlJ8yE+CAlrDyOGUxgWckT7w7aAmJau+TLU1iGi5EP
60 | ZDN1MLAOgbnwArYSmd3TAgTWIMAdTplVBqPUFrAAAAAwEAAQAAAQEAhe/vad/1tZTcHcHB
61 | yqgFpRHzT1uOcJUeO0r20PwDm18xAIL2LGh9g09asYp6t1xmtzI1PlVTO+p2eX5D2TgGaw
62 | EXqJSvh+4+ZQ0Mw4pVggcW2ntB9ZSCE+Ehbo8jNfN5RLejTYmyElnvJ52tG9CVMmekflMz
63 | CYr3MQHR6eCfHBnbOgdMFiIyTgFliT1MAZBlWRtVJDQkr8DZMBjN1qoVHldLISl2nREvVt
64 | b6TM1gCCp3fuey6+pe5BOm5gn/FjxOXiOiBrmfAu0Wu5ITnblCeje9Y/z0dHJNgKxhKfuo
65 | hp78EE+fgHwVpUMeAunId9uRBwu8/u8eewtb7tMcYyK82QAAAIB8rhliWV/AOJqk+RhadS
66 | uF+640Zekk8dw0EFQiyYeK9IABi+WGs0+XTd3a0k/bUUM0jxxa2os1fSsnojOhMCYpLt5A
67 | UzmVWENG4xixscX0xdtJeYI91/Q7JuPmRbR2rGCL76WGyVnFvrKfpih1IgUKd9xkMT32WN
68 | yp/rSKab78sQAAAIEA/M+MJjP+v1bA/2efBD70Vp4JgtnrsHIP80ZQOWxVWT+4Ea17OlBR
69 | k+AfU1vJsrS9yLAk4LqHc3Zx6P3kd1sVvb9+dkIvQwy189T+sc7f4karRg9msu/aoAuzNE
70 | LsaI9VieYN2eF6ET243G8SUA6rKSCpvGDicVEjbbYI8PAaEE8AAACBAMtSJsXF2cFaWOrd
71 | pBYI3ZsseI9mlLXCX1Y+P/6QBo7U51/Vw0vLjLgyHyVGveLH26Fr0/b07QWoI3XQSXA3ZO
72 | asXVVgiyAEsUaqxEr0NsqACTfYA3reHcIFD/FthDRYh5a5sXzBtRHeqDhsmV0Vj42YAqq2
73 | baewZMKBL1QECTolAAAADHJrb3NlZ2lAbDQ4MAECAwQFBg==
74 | -----END OPENSSH PRIVATE KEY-----
75 |
--------------------------------------------------------------------------------
/src/test/resources/id_ed25519:
--------------------------------------------------------------------------------
1 | -----BEGIN OPENSSH PRIVATE KEY-----
2 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
3 | QyNTUxOQAAACDcbIqDY9hFJb9+/yKhPpZZZnjTwyGLSmlnZirb5ps1YAAAAJACffNsAn3z
4 | bAAAAAtzc2gtZWQyNTUxOQAAACDcbIqDY9hFJb9+/yKhPpZZZnjTwyGLSmlnZirb5ps1YA
5 | AAAEAc7nxaUJgtIKaS0nNK+fTleBeF3o1kNajufRxpKIU0fNxsioNj2EUlv37/IqE+lllm
6 | eNPDIYtKaWdmKtvmmzVgAAAAB2plbmtpbnMBAgMEBQY=
7 | -----END OPENSSH PRIVATE KEY-----
8 |
--------------------------------------------------------------------------------
/src/test/resources/id_ed25519.pub:
--------------------------------------------------------------------------------
1 | ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINxsioNj2EUlv37/IqE+lllmeNPDIYtKaWdmKtvmmzVg
--------------------------------------------------------------------------------
/src/test/resources/id_rsa:
--------------------------------------------------------------------------------
1 | -----BEGIN OPENSSH PRIVATE KEY-----
2 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn
3 | NhAAAAAwEAAQAAAQEAyMm0501e8CnMGO72c5fihC//KhdOGahgvbCGbS5mOk4zfpvo+Jui
4 | 3hIJOOt6DW2atAabo8wUXLg9KH9cXHmKs6SMGuRd7m5fdTaUzO0G//ZmzX51phqSYv6JsU
5 | n/vKxqOeVtIWzXHuC+lOtEyR+BKA2UQJOTReO4dv8oAt9cBGSN4gOItc7s07ggK+ZD+S2u
6 | gSTpf5gXYnD9qq9TFuEyMFLJi9gWE/M3c/ZeeDyfTeVeYdbaq2qI3NNUtUYXB4Es0XsdzR
7 | ZSfMhPggJaw8jhlMYFnJE+8O2gJiWrvky1NYhouRD2QzdTCwDoG58AK2Epnd0wIE1iDAHU
8 | 6ZVQaj1BawAAA8iwi+/UsIvv1AAAAAdzc2gtcnNhAAABAQDIybTnTV7wKcwY7vZzl+KEL/
9 | 8qF04ZqGC9sIZtLmY6TjN+m+j4m6LeEgk463oNbZq0BpujzBRcuD0of1xceYqzpIwa5F3u
10 | bl91NpTM7Qb/9mbNfnWmGpJi/omxSf+8rGo55W0hbNce4L6U60TJH4EoDZRAk5NF47h2/y
11 | gC31wEZI3iA4i1zuzTuCAr5kP5La6BJOl/mBdicP2qr1MW4TIwUsmL2BYT8zdz9l54PJ9N
12 | 5V5h1tqraojc01S1RhcHgSzRex3NFlJ8yE+CAlrDyOGUxgWckT7w7aAmJau+TLU1iGi5EP
13 | ZDN1MLAOgbnwArYSmd3TAgTWIMAdTplVBqPUFrAAAAAwEAAQAAAQEAhe/vad/1tZTcHcHB
14 | yqgFpRHzT1uOcJUeO0r20PwDm18xAIL2LGh9g09asYp6t1xmtzI1PlVTO+p2eX5D2TgGaw
15 | EXqJSvh+4+ZQ0Mw4pVggcW2ntB9ZSCE+Ehbo8jNfN5RLejTYmyElnvJ52tG9CVMmekflMz
16 | CYr3MQHR6eCfHBnbOgdMFiIyTgFliT1MAZBlWRtVJDQkr8DZMBjN1qoVHldLISl2nREvVt
17 | b6TM1gCCp3fuey6+pe5BOm5gn/FjxOXiOiBrmfAu0Wu5ITnblCeje9Y/z0dHJNgKxhKfuo
18 | hp78EE+fgHwVpUMeAunId9uRBwu8/u8eewtb7tMcYyK82QAAAIB8rhliWV/AOJqk+RhadS
19 | uF+640Zekk8dw0EFQiyYeK9IABi+WGs0+XTd3a0k/bUUM0jxxa2os1fSsnojOhMCYpLt5A
20 | UzmVWENG4xixscX0xdtJeYI91/Q7JuPmRbR2rGCL76WGyVnFvrKfpih1IgUKd9xkMT32WN
21 | yp/rSKab78sQAAAIEA/M+MJjP+v1bA/2efBD70Vp4JgtnrsHIP80ZQOWxVWT+4Ea17OlBR
22 | k+AfU1vJsrS9yLAk4LqHc3Zx6P3kd1sVvb9+dkIvQwy189T+sc7f4karRg9msu/aoAuzNE
23 | LsaI9VieYN2eF6ET243G8SUA6rKSCpvGDicVEjbbYI8PAaEE8AAACBAMtSJsXF2cFaWOrd
24 | pBYI3ZsseI9mlLXCX1Y+P/6QBo7U51/Vw0vLjLgyHyVGveLH26Fr0/b07QWoI3XQSXA3ZO
25 | asXVVgiyAEsUaqxEr0NsqACTfYA3reHcIFD/FthDRYh5a5sXzBtRHeqDhsmV0Vj42YAqq2
26 | baewZMKBL1QECTolAAAADHJrb3NlZ2lAbDQ4MAECAwQFBg==
27 | -----END OPENSSH PRIVATE KEY-----
--------------------------------------------------------------------------------
/src/test/resources/id_rsa.pub:
--------------------------------------------------------------------------------
1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDIybTnTV7wKcwY7vZzl+KEL/8qF04ZqGC9sIZtLmY6TjN+m+j4m6LeEgk463oNbZq0BpujzBRcuD0of1xceYqzpIwa5F3ubl91NpTM7Qb/9mbNfnWmGpJi/omxSf+8rGo55W0hbNce4L6U60TJH4EoDZRAk5NF47h2/ygC31wEZI3iA4i1zuzTuCAr5kP5La6BJOl/mBdicP2qr1MW4TIwUsmL2BYT8zdz9l54PJ9N5V5h1tqraojc01S1RhcHgSzRex3NFlJ8yE+CAlrDyOGUxgWckT7w7aAmJau+TLU1iGi5EPZDN1MLAOgbnwArYSmd3TAgTWIMAdTplVBqPUFr
--------------------------------------------------------------------------------