Skip to content

Commit c646b71

Browse files
ysmaouiYacine SmaouiVlatombe
authored
[JENKINS-73056] cancel queue Item that really matches the pod causing the cancelling (#1539)
Co-authored-by: Yacine Smaoui <[email protected]> Co-authored-by: Vincent Latombe <[email protected]>
1 parent 4017ba2 commit c646b71

File tree

10 files changed

+158
-129
lines changed

10 files changed

+158
-129
lines changed

src/main/java/org/csanchez/jenkins/plugins/kubernetes/PodTemplate.java

+7-21
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@ public class PodTemplate extends AbstractDescribableImpl<PodTemplate> implements
7676
public static final Integer DEFAULT_SLAVE_JENKINS_CONNECTION_TIMEOUT =
7777
Integer.getInteger(PodTemplate.class.getName() + ".connectionTimeout", 1000);
7878

79+
public static final String JENKINS_LABEL = "jenkins/label";
80+
public static final String JENKINS_LABEL_DIGEST = "jenkins/label-digest";
81+
7982
/**
8083
* Digest function that is used to compute the kubernetes label "jenkins/label-digest"
8184
* Not used for security.
@@ -477,22 +480,6 @@ public Map<String, String> getLabelsMap() {
477480
return labelsMap;
478481
}
479482

480-
static String sanitizeLabel(String input) {
481-
String label = input;
482-
int max = 63;
483-
// Kubernetes limit
484-
// a valid label must be an empty string or consist of alphanumeric characters, '-', '_' or '.', and must
485-
// start and end with an alphanumeric character (e.g. 'MyValue', or 'my_value', or '12345', regex used
486-
// for validation is '(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?')
487-
if (label.length() > max) {
488-
label = label.substring(label.length() - max);
489-
}
490-
label = label.replaceAll("[^_.a-zA-Z0-9-]", "_")
491-
.replaceFirst("^[^a-zA-Z0-9]", "x")
492-
.replaceFirst("[^a-zA-Z0-9]$", "x");
493-
return label;
494-
}
495-
496483
@DataBoundSetter
497484
public void setLabel(String label) {
498485
this.label = Util.fixEmptyAndTrim(label);
@@ -503,14 +490,13 @@ private void recomputeLabelDerivedFields() {
503490
this.labelSet = Label.parse(label);
504491
Map<String, String> tempMap = new HashMap<>();
505492
if (label == null) {
506-
tempMap.put("jenkins/label", DEFAULT_LABEL);
507-
tempMap.put("jenkins/label-digest", "0");
493+
tempMap.put(JENKINS_LABEL, DEFAULT_LABEL);
494+
tempMap.put(JENKINS_LABEL_DIGEST, "0");
508495
} else {
509496
MessageDigest labelDigestFunction = getLabelDigestFunction();
510497
labelDigestFunction.update(label.getBytes(StandardCharsets.UTF_8));
511-
tempMap.put("jenkins/label", sanitizeLabel(label));
512-
tempMap.put(
513-
"jenkins/label-digest", String.format("%040x", new BigInteger(1, labelDigestFunction.digest())));
498+
tempMap.put(JENKINS_LABEL, PodTemplateUtils.sanitizeLabel(label));
499+
tempMap.put(JENKINS_LABEL_DIGEST, String.format("%040x", new BigInteger(1, labelDigestFunction.digest())));
514500
}
515501
labelsMap = Collections.unmodifiableMap(tempMap);
516502
}

src/main/java/org/csanchez/jenkins/plugins/kubernetes/PodTemplateUtils.java

+26
Original file line numberDiff line numberDiff line change
@@ -757,6 +757,32 @@ public static boolean validateImage(String image) {
757757
return image != null && image.matches("\\S+");
758758
}
759759

760+
/**
761+
* <p>Sanitizes the input string to create a valid Kubernetes label.
762+
* <p>The input string is truncated to a maximum length of 57 characters,
763+
* and any characters that are not alphanumeric or hyphens are replaced with underscores. If the input string starts with a non-alphanumeric
764+
* character, it is replaced with 'x'.
765+
*
766+
* @param input the input string to be sanitized
767+
* @return the sanitized and validated label
768+
* @throws AssertionError if the generated label is not valid
769+
*/
770+
public static String sanitizeLabel(String input) {
771+
int max = /* Kubernetes limit */ 63 - /* hyphen */ 1 - /* suffix */ 5;
772+
String label;
773+
if (input.length() > max) {
774+
label = input.substring(input.length() - max);
775+
} else {
776+
label = input;
777+
}
778+
label = label.replaceAll("[^_a-zA-Z0-9-]", "_")
779+
.replaceFirst("^[^a-zA-Z0-9]", "x")
780+
.replaceFirst("[^a-zA-Z0-9]$", "x");
781+
782+
assert PodTemplateUtils.validateLabel(label) : label;
783+
return label;
784+
}
785+
760786
private static List<EnvVar> combineEnvVars(Container parent, Container template) {
761787
Map<String, EnvVar> combinedEnvVars = new HashMap<>();
762788
Stream.of(parent.getEnv(), template.getEnv())

src/main/java/org/csanchez/jenkins/plugins/kubernetes/PodUtils.java

+40-32
Original file line numberDiff line numberDiff line change
@@ -19,24 +19,28 @@
1919
import edu.umd.cs.findbugs.annotations.CheckForNull;
2020
import edu.umd.cs.findbugs.annotations.NonNull;
2121
import hudson.Util;
22-
import hudson.model.Queue;
22+
import hudson.model.Label;
2323
import io.fabric8.kubernetes.api.model.ContainerStatus;
2424
import io.fabric8.kubernetes.api.model.ObjectMeta;
2525
import io.fabric8.kubernetes.api.model.Pod;
2626
import io.fabric8.kubernetes.api.model.PodStatus;
2727
import io.fabric8.kubernetes.client.KubernetesClient;
2828
import io.fabric8.kubernetes.client.KubernetesClientException;
29+
import java.util.Arrays;
2930
import java.util.Collections;
3031
import java.util.List;
31-
import java.util.Map;
32+
import java.util.Optional;
3233
import java.util.function.Predicate;
3334
import java.util.logging.Level;
3435
import java.util.logging.Logger;
3536
import java.util.stream.Collectors;
3637
import jenkins.model.Jenkins;
3738
import org.apache.commons.lang.StringUtils;
39+
import org.csanchez.jenkins.plugins.kubernetes.pipeline.PodTemplateStepExecution;
3840

3941
public final class PodUtils {
42+
private PodUtils() {}
43+
4044
private static final Logger LOGGER = Logger.getLogger(PodUtils.class.getName());
4145

4246
public static final Predicate<ContainerStatus> CONTAINER_IS_TERMINATED =
@@ -66,52 +70,56 @@ public static List<ContainerStatus> getContainers(Pod pod, Predicate<ContainerSt
6670
}
6771

6872
/**
69-
* Cancel queue items matching the given pod.
70-
* It uses the annotation "runUrl" added to the pod to do the matching.
71-
*
72-
* It uses the current thread context to list item queues,
73+
* <p>Cancel queue items matching the given pod.
74+
* <p>The queue item has to have a task url matching the pod "runUrl"-annotation
75+
* and the queue item assigned label needs to match the label jenkins/label of the pod.
76+
* <p>It uses the current thread context to list item queues,
7377
* so make sure to be in the right context before calling this method.
7478
*
7579
* @param pod The pod to cancel items for.
7680
* @param reason The reason the item are being cancelled.
7781
*/
7882
public static void cancelQueueItemFor(Pod pod, String reason) {
79-
Queue q = Jenkins.get().getQueue();
80-
boolean cancelled = false;
81-
ObjectMeta metadata = pod.getMetadata();
83+
var metadata = pod.getMetadata();
8284
if (metadata == null) {
8385
return;
8486
}
85-
Map<String, String> annotations = metadata.getAnnotations();
87+
String podName = metadata.getName();
88+
String podNamespace = metadata.getNamespace();
89+
String podDisplayName = podNamespace + "/" + podName;
90+
var annotations = metadata.getAnnotations();
8691
if (annotations == null) {
87-
LOGGER.log(Level.FINE, "Pod .metadata.annotations is null: {0}/{1}", new Object[] {
88-
metadata.getNamespace(), metadata.getName()
89-
});
92+
LOGGER.log(Level.FINE, () -> "Pod " + podDisplayName + " .metadata.annotations is null");
9093
return;
9194
}
92-
String runUrl = annotations.get("runUrl");
95+
var runUrl = annotations.get(PodTemplateStepExecution.POD_ANNOTATION_RUN_URL);
9396
if (runUrl == null) {
94-
LOGGER.log(Level.FINE, "Pod .metadata.annotations.runUrl is null: {0}/{1}", new Object[] {
95-
metadata.getNamespace(), metadata.getName()
96-
});
97+
LOGGER.log(Level.FINE, () -> "Pod " + podDisplayName + " .metadata.annotations.runUrl is null");
9798
return;
9899
}
99-
for (Queue.Item item : q.getItems()) {
100-
Queue.Task task = item.task;
101-
if (runUrl.equals(task.getUrl())) {
102-
LOGGER.log(Level.FINE, "Cancelling queue item: \"{0}\"\n{1}", new Object[] {
103-
task.getDisplayName(), !StringUtils.isBlank(reason) ? "due to " + reason : ""
104-
});
105-
q.cancel(item);
106-
cancelled = true;
107-
break;
108-
}
109-
}
110-
if (!cancelled) {
111-
LOGGER.log(Level.FINE, "No queue item found for pod: {0}/{1}", new Object[] {
112-
metadata.getNamespace(), metadata.getName()
113-
});
100+
var labels = metadata.getLabels();
101+
if (labels == null) {
102+
LOGGER.log(Level.FINE, () -> "Pod " + podDisplayName + " .metadata.labels is null");
103+
return;
114104
}
105+
var jenkinsLabel = labels.get(PodTemplate.JENKINS_LABEL);
106+
var queue = Jenkins.get().getQueue();
107+
Arrays.stream(queue.getItems())
108+
.filter(item -> item.getTask().getUrl().equals(runUrl))
109+
.filter(item -> Optional.ofNullable(item.getAssignedLabel())
110+
.map(Label::getName)
111+
.map(name -> PodTemplateUtils.sanitizeLabel(name).equals(jenkinsLabel))
112+
.orElse(false))
113+
.findFirst()
114+
.ifPresentOrElse(
115+
item -> {
116+
LOGGER.log(
117+
Level.FINE,
118+
() -> "Cancelling queue item: \"" + item.task.getDisplayName() + "\"\n"
119+
+ (!StringUtils.isBlank(reason) ? "due to " + reason : ""));
120+
queue.cancel(item);
121+
},
122+
() -> LOGGER.log(Level.FINE, () -> "No queue item found for pod " + podDisplayName));
115123
}
116124

117125
@CheckForNull

src/main/java/org/csanchez/jenkins/plugins/kubernetes/pipeline/PodTemplateStepExecution.java

+13-16
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ public class PodTemplateStepExecution extends AbstractStepExecutionImpl {
4141
private static final long serialVersionUID = -6139090518333729333L;
4242

4343
private static final String NAME_FORMAT = "%s-%s";
44+
public static final String POD_ANNOTATION_BUILD_URL = "buildUrl";
45+
public static final String POD_ANNOTATION_RUN_URL = "runUrl";
4446

4547
private static /* almost final */ boolean VERBOSE =
4648
Boolean.parseBoolean(System.getProperty(PodTemplateStepExecution.class.getName() + ".verbose"));
@@ -70,9 +72,15 @@ public boolean start() throws Exception {
7072
PodTemplateContext podTemplateContext = getContext().get(PodTemplateContext.class);
7173
String parentTemplates = podTemplateContext != null ? podTemplateContext.getName() : null;
7274

73-
String label = step.getLabel();
74-
if (label == null) {
75-
label = labelify(run.getExternalizableId());
75+
String label;
76+
String podTemplateLabel = step.getLabel();
77+
if (podTemplateLabel == null) {
78+
var sanitized = PodTemplateUtils.sanitizeLabel(run.getExternalizableId()) + "-"
79+
+ RandomStringUtils.random(5, "bcdfghjklmnpqrstvwxz0123456789");
80+
assert PodTemplateUtils.validateLabel(sanitized) : sanitized;
81+
label = sanitized;
82+
} else {
83+
label = podTemplateLabel;
7684
}
7785

7886
// Let's generate a random name based on the user specified to make sure that we don't have
@@ -121,8 +129,8 @@ public boolean start() throws Exception {
121129
newTemplate.setInheritYamlMergeStrategy(step.isInheritYamlMergeStrategy());
122130
String url = cloud.getJenkinsUrlOrNull();
123131
if (url != null) {
124-
newTemplate.getAnnotations().add(new PodAnnotation("buildUrl", url + run.getUrl()));
125-
newTemplate.getAnnotations().add(new PodAnnotation("runUrl", run.getUrl()));
132+
newTemplate.getAnnotations().add(new PodAnnotation(POD_ANNOTATION_BUILD_URL, url + run.getUrl()));
133+
newTemplate.getAnnotations().add(new PodAnnotation(POD_ANNOTATION_RUN_URL, run.getUrl()));
126134
}
127135
}
128136
newTemplate.setImagePullSecrets(step.getImagePullSecrets().stream()
@@ -190,17 +198,6 @@ private static KubernetesCloud resolveCloud(final String cloudName) throws Abort
190198
return cloud;
191199
}
192200

193-
static String labelify(String input) {
194-
int max = /* Kubernetes limit */ 63 - /* hyphen */ 1 - /* suffix */ 5;
195-
if (input.length() > max) {
196-
input = input.substring(input.length() - max);
197-
}
198-
input = input.replaceAll("[^_a-zA-Z0-9-]", "_").replaceFirst("^[^a-zA-Z0-9]", "x");
199-
String label = input + "-" + RandomStringUtils.random(5, "bcdfghjklmnpqrstvwxz0123456789");
200-
assert PodTemplateUtils.validateLabel(label) : label;
201-
return label;
202-
}
203-
204201
/**
205202
* Check if the current Job is permitted to use the cloud.
206203
*

src/test/java/org/csanchez/jenkins/plugins/kubernetes/PodTemplateTest.java

-15
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package org.csanchez.jenkins.plugins.kubernetes;
22

3-
import static org.csanchez.jenkins.plugins.kubernetes.PodTemplate.*;
43
import static org.hamcrest.MatcherAssert.assertThat;
54
import static org.hamcrest.Matchers.contains;
65
import static org.hamcrest.Matchers.empty;
@@ -32,18 +31,4 @@ public void copyConstructor() throws Exception {
3231
pt.setIdleMinutes(99);
3332
assertEquals(xs.toXML(pt), xs.toXML(new PodTemplate(pt)));
3433
}
35-
36-
@Test
37-
public void sanitizeLabels() {
38-
assertEquals("label1", sanitizeLabel("label1"));
39-
assertEquals("label1_label2", sanitizeLabel("label1 label2"));
40-
assertEquals(
41-
"el1_label2_verylooooooooooooooooooooooooooooonglabelover63chars",
42-
sanitizeLabel("label1 label2 verylooooooooooooooooooooooooooooonglabelover63chars"));
43-
assertEquals("xfoo_bar", sanitizeLabel(":foo:bar"));
44-
assertEquals("xfoo_barx", sanitizeLabel(":foo:bar:"));
45-
assertEquals(
46-
"l2_verylooooooooooooooooooooooooooooonglabelendinginunderscorex",
47-
sanitizeLabel("label1 label2 verylooooooooooooooooooooooooooooonglabelendinginunderscore_"));
48-
}
4934
}

src/test/java/org/csanchez/jenkins/plugins/kubernetes/PodTemplateUtilsTest.java

+43-5
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,29 @@
2424

2525
package org.csanchez.jenkins.plugins.kubernetes;
2626

27-
import static java.util.Arrays.*;
28-
import static java.util.Collections.*;
29-
import static org.csanchez.jenkins.plugins.kubernetes.PodTemplateUtils.*;
27+
import static java.util.Arrays.asList;
28+
import static java.util.Collections.singletonList;
29+
import static org.csanchez.jenkins.plugins.kubernetes.PodTemplateUtils.combine;
30+
import static org.csanchez.jenkins.plugins.kubernetes.PodTemplateUtils.parseFromYaml;
31+
import static org.csanchez.jenkins.plugins.kubernetes.PodTemplateUtils.sanitizeLabel;
32+
import static org.csanchez.jenkins.plugins.kubernetes.PodTemplateUtils.substitute;
33+
import static org.csanchez.jenkins.plugins.kubernetes.PodTemplateUtils.unwrap;
34+
import static org.csanchez.jenkins.plugins.kubernetes.PodTemplateUtils.validateLabel;
3035
import static org.hamcrest.MatcherAssert.assertThat;
31-
import static org.hamcrest.Matchers.*;
32-
import static org.junit.Assert.*;
36+
import static org.hamcrest.Matchers.contains;
37+
import static org.hamcrest.Matchers.containsInAnyOrder;
38+
import static org.hamcrest.Matchers.empty;
39+
import static org.hamcrest.Matchers.equalTo;
40+
import static org.hamcrest.Matchers.hasEntry;
41+
import static org.hamcrest.Matchers.hasItems;
42+
import static org.hamcrest.Matchers.hasSize;
43+
import static org.hamcrest.Matchers.is;
44+
import static org.junit.Assert.assertEquals;
45+
import static org.junit.Assert.assertFalse;
46+
import static org.junit.Assert.assertNotNull;
47+
import static org.junit.Assert.assertNull;
48+
import static org.junit.Assert.assertTrue;
49+
import static org.junit.Assert.fail;
3350

3451
import hudson.model.Node;
3552
import hudson.tools.ToolLocationNodeProperty;
@@ -944,4 +961,25 @@ public void shouldIgnoreContainerEmptyArgs() {
944961
assertEquals(List.of("arg1", "arg2"), result.getArgs());
945962
assertEquals(List.of("parent command"), result.getCommand());
946963
}
964+
965+
@Test
966+
public void shouldSanitizeJenkinsLabel() {
967+
assertEquals("foo", sanitizeLabel("foo"));
968+
assertEquals("foo_bar__3", sanitizeLabel("foo bar #3"));
969+
assertEquals("This_Thing", sanitizeLabel("This/Thing"));
970+
assertEquals("xwhatever", sanitizeLabel("/whatever"));
971+
assertEquals(
972+
"xprolix-for-the-sixty-three-character-limit-in-kubernetes",
973+
sanitizeLabel("way-way-way-too-prolix-for-the-sixty-three-character-limit-in-kubernetes"));
974+
assertEquals("label1", sanitizeLabel("label1"));
975+
assertEquals("label1_label2", sanitizeLabel("label1 label2"));
976+
assertEquals(
977+
"bel2_verylooooooooooooooooooooooooooooonglabelover63chars",
978+
sanitizeLabel("label1 label2 verylooooooooooooooooooooooooooooonglabelover63chars"));
979+
assertEquals("xfoo_bar", sanitizeLabel(":foo:bar"));
980+
assertEquals("xfoo_barx", sanitizeLabel(":foo:bar:"));
981+
assertEquals(
982+
"ylooooooooooooooooooooooooooooonglabelendinginunderscorex",
983+
sanitizeLabel("label1 label2 verylooooooooooooooooooooooooooooonglabelendinginunderscore_"));
984+
}
947985
}

src/test/java/org/csanchez/jenkins/plugins/kubernetes/pipeline/AbstractKubernetesPipelineTest.java

+2
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
import org.csanchez.jenkins.plugins.kubernetes.KubernetesComputer;
5151
import org.csanchez.jenkins.plugins.kubernetes.KubernetesTestUtil;
5252
import org.csanchez.jenkins.plugins.kubernetes.PodTemplate;
53+
import org.csanchez.jenkins.plugins.kubernetes.PodUtils;
5354
import org.csanchez.jenkins.plugins.kubernetes.model.KeyValueEnvVar;
5455
import org.csanchez.jenkins.plugins.kubernetes.model.SecretEnvVar;
5556
import org.csanchez.jenkins.plugins.kubernetes.model.TemplateEnvVar;
@@ -81,6 +82,7 @@ public abstract class AbstractKubernetesPipelineTest {
8182
public LoggerRule logs = new LoggerRule()
8283
.recordPackage(KubernetesCloud.class, Level.FINE)
8384
.recordPackage(NoDelayProvisionerStrategy.class, Level.FINE)
85+
.record(PodUtils.class, Level.FINE)
8486
.record(NodeProvisioner.class, Level.FINE)
8587
.record(KubernetesAgentErrorCondition.class, Level.FINE);
8688

src/test/java/org/csanchez/jenkins/plugins/kubernetes/pipeline/KubernetesPipelineTest.java

+8
Original file line numberDiff line numberDiff line change
@@ -844,4 +844,12 @@ private <R extends Run> R assertBuildStatus(R run, Result... status) throws Exce
844844
MatcherAssert.assertThat(msg, run.getResult(), Matchers.is(oneOf(status)));
845845
return run;
846846
}
847+
848+
@Test
849+
public void cancelOnlyRelevantQueueItem() throws Exception {
850+
r.waitForMessage("cancelled pod item by now", b);
851+
r.createOnlineSlave(Label.get("special-agent"));
852+
r.assertBuildStatus(Result.ABORTED, r.waitForCompletion(b));
853+
r.assertLogContains("ran on special agent", b);
854+
}
847855
}

0 commit comments

Comments
 (0)