Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[JENKINS-58670] Pod Labels UI #551

Merged
merged 4 commits into from
Jul 26, 2019
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
Expand Down Expand Up @@ -116,7 +118,7 @@ public class KubernetesCloud extends Cloud {
private int retentionTimeout = DEFAULT_RETENTION_TIMEOUT_MINUTES;
private int connectTimeout;
private int readTimeout;
private Map<String, String> labels;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To achieve backward compatibility, you need to keep the old field as @deprecated / transient, then migrate the data in readResolve() method.

private List<PodLabel> labels = new ArrayList<PodLabel>();
private boolean usageRestricted;

private int maxRequestsPerHost;
Expand Down Expand Up @@ -384,18 +386,34 @@ public int getConnectTimeout() {
/**
* Labels for all pods started by the plugin
*/
public Map<String, String> getLabels() {
return labels == null || labels.isEmpty() ? DEFAULT_POD_LABELS : labels;
public List<PodLabel> getLabels() {
if (this.labels == null || labels.isEmpty()) {
List<PodLabel> labels = new ArrayList<>();
for (Map.Entry<String, String> label : DEFAULT_POD_LABELS.entrySet()) {
labels.add(new PodLabel(label.getKey(), label.getValue()));
}
return labels;
}
return this.labels;
}

/**
* No UI yet, so this is never re-set
*
* @param labels
* Set Pod labels for all pods started by the plugin.
*/
@DataBoundSetter
public void setLabels(List<PodLabel> labels) {
this.labels = new ArrayList<PodLabel>();
if (labels != null) {
this.labels.addAll(labels);
}
}

/**
* Map of labels to add to all pods started by the plugin
* @return immutable map of pod labels
*/
// @DataBoundSetter
public void setLabels(Map<String, String> labels) {
this.labels = labels;
Map<String, String> getLabelsMap() {
return PodLabel.toMap(getLabels());
}

@DataBoundSetter
Expand Down Expand Up @@ -517,32 +535,18 @@ private boolean addProvisionedSlave(@Nonnull PodTemplate template, @CheckForNull
templateNamespace = client.getNamespace();
}

PodList slaveList = client.pods().inNamespace(templateNamespace).withLabels(getLabels()).list();
List<Pod> allActiveSlavePods = null;
// JENKINS-53370 check for nulls
if (slaveList != null && slaveList.getItems() != null) {
allActiveSlavePods = slaveList.getItems().stream() //
.filter(x -> x.getStatus().getPhase().toLowerCase().matches("(running|pending)"))
.collect(Collectors.toList());
}

Map<String, String> podLabels = getLabelsMap();
List<Pod> allActiveSlavePods = getActiveSlavePods(client, templateNamespace, podLabels);
if (allActiveSlavePods != null && containerCap <= allActiveSlavePods.size() + scheduledCount) {
LOGGER.log(Level.INFO,
"Maximum number of concurrently running agent pods ({0}) reached for Kubernetes Cloud {4}, not provisioning: {1} running or pending in namespace {2} with Kubernetes labels {3}",
new Object[] { containerCap, allActiveSlavePods.size() + scheduledCount, templateNamespace, getLabels(), name });
return false;
}

Map<String, String> labelsMap = new HashMap<>(this.getLabels());
Map<String, String> labelsMap = new HashMap<>(podLabels);
labelsMap.putAll(template.getLabelsMap());
PodList templateSlaveList = client.pods().inNamespace(templateNamespace).withLabels(labelsMap).list();
// JENKINS-53370 check for nulls
List<Pod> activeTemplateSlavePods = null;
if (templateSlaveList != null && templateSlaveList.getItems() != null) {
activeTemplateSlavePods = templateSlaveList.getItems().stream()
.filter(x -> x.getStatus().getPhase().toLowerCase().matches("(running|pending)"))
.collect(Collectors.toList());
}
List<Pod> activeTemplateSlavePods = getActiveSlavePods(client, templateNamespace, labelsMap);
if (activeTemplateSlavePods != null && allActiveSlavePods != null && template.getInstanceCap() <= activeTemplateSlavePods.size() + scheduledCount) {
LOGGER.log(Level.INFO,
"Maximum number of concurrently running agent pods ({0}) reached for template {1} in Kubernetes Cloud {6}, not provisioning: {2} running or pending in namespace {3} with label \"{4}\" and Kubernetes labels {5}",
Expand All @@ -553,6 +557,21 @@ private boolean addProvisionedSlave(@Nonnull PodTemplate template, @CheckForNull
return true;
}

/**
* Query for running or pending pods
*/
private List<Pod> getActiveSlavePods(KubernetesClient client, String templateNamespace, Map<String, String> podLabels) {
PodList slaveList = client.pods().inNamespace(templateNamespace).withLabels(podLabels).list();
List<Pod> activeSlavePods = null;
// JENKINS-53370 check for nulls
if (slaveList != null && slaveList.getItems() != null) {
activeSlavePods = slaveList.getItems().stream() //
.filter(x -> x.getStatus().getPhase().toLowerCase().matches("(running|pending)"))
.collect(Collectors.toList());
}
return activeSlavePods;
}

@Override
public boolean canProvision(@CheckForNull Label label) {
return getTemplate(label) != null;
Expand Down
102 changes: 102 additions & 0 deletions src/main/java/org/csanchez/jenkins/plugins/kubernetes/PodLabel.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package org.csanchez.jenkins.plugins.kubernetes;

import com.google.common.collect.ImmutableMap;
import hudson.Extension;
import hudson.model.AbstractDescribableImpl;
import hudson.model.Descriptor;
import org.jenkinsci.Symbol;
import org.kohsuke.stapler.DataBoundConstructor;

import javax.validation.constraints.NotNull;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import static org.csanchez.jenkins.plugins.kubernetes.PodTemplateUtils.substituteEnv;

public class PodLabel extends AbstractDescribableImpl<PodLabel> implements Serializable {

private static final long serialVersionUID = -5667326362260252552L;

private String key;
private String value;

@DataBoundConstructor
public PodLabel(String key, String value) {
this.key = key;
this.value = value;
}

public String getKey() {
return key;
}

public void setKey(String key) {
this.key = key;
}

public String getValue() {
return value;
}

public void setValue(String value) {
this.value = value;
}

/**
* Create map from collection of labels. Values that environment variables placeholders will be resolved.
* @see PodTemplateUtils#substituteEnv(String)
* @param labels collection of pod labels to convert to a map
* @return immutable map of pod labels
*/
@NotNull
static Map<String, String> toMap(@NotNull Iterable<PodLabel> labels) {
ImmutableMap.Builder<String, String> builder = ImmutableMap.<String, String>builder();
if (labels != null) {
for (PodLabel podLabel : labels) {
builder.put(podLabel.getKey(), substituteEnv(podLabel.getValue()));
}
}
return builder.build();
}

/**
* Create list of pod labels from a map of label key values.
* @param labels labels map
* @return list of pod labels
*/
@NotNull
static List<PodLabel> fromMap(@NotNull Map<String, String> labels) {
List<PodLabel> list = new ArrayList<>();
for (Map.Entry<String, String> label : labels.entrySet()) {
list.add(new PodLabel(label.getKey(), label.getValue()));
}
return list;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;

PodLabel that = (PodLabel) o;

return key != null ? key.equals(that.key) : that.key == null;

}

@Override
public int hashCode() {
return key != null ? key.hashCode() : 0;
}

@Extension
@Symbol("podLabel")
public static class DescriptorImpl extends Descriptor<PodLabel> {
@Override
public String getDisplayName() {
return "Pod Label";
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ public Pod build() {

Map<String, String> labels = new HashMap<>();
if (slave != null) {
labels.putAll(slave.getKubernetesCloud().getLabels());
labels.putAll(slave.getKubernetesCloud().getLabelsMap());
}
labels.putAll(template.getLabelsMap());
if (!labels.isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@
<f:textbox default="10"/>
</f:entry>

<f:entry title="${%Pod Labels}" field="labels">
<f:repeatableHeteroProperty field="labels" hasHeader="true" addCaption="${%Add Pod Label}"
deleteCaption="${%Delete Pod Label}" />
</f:entry>

<f:dropdownDescriptorSelector title="${%Pod Retention}" field="podRetention"
descriptors="${descriptor.allowedPodRetentions}" default="${descriptor.defaultPodRetention}" />

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<div>
<a href="https://kubernetes.io/docs/concepts/overview/working-with-objects/labels">Labels</a>
for all Pods started by the plugin. Pods that match these labels count toward the <strong>Concurrency Limit</strong>.
<p>
If not configured, all Pods will be created with <code>jenkins=slave</code> by default.
</p>
<p>Examples:
<ul>
<li><code>jenkins=slave</code></li>
<li><code>app.kubernetes.io/managed-by=jenkins</code></li>
<li><code>app.kubernetes.io/part-of=cicd</code></li>
</ul>
</p>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!--
Config page
-->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout"
xmlns:t="/lib/hudson" xmlns:f="/lib/form">

<f:entry field="key" title="${%Key}">
<f:textbox/>
</f:entry>

<f:entry field="value" title="${%Value}">
<f:textbox/>
</f:entry>

</j:jelly>
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# The MIT License
#
# Copyright (c) 2018, Alauda
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

Key=\u952E
Value=\u503C
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Valid label keys have two segments: an optional prefix and name, separated by a slash (<code>/</code>). The name
segment is required and must be 63 characters or less, beginning and ending with an alphanumeric character
(<code>[a-z0-9A-Z]</code>) with dashes (<code>-</code>), underscores (<code>_</code>), dots (<code>.</code>), and
alphanumerics between. The prefix is optional. If specified, the prefix must be a DNS subdomain: a series of DNS
labels separated by dots (<code>.</code>), not longer than 253 characters in total, followed by a slash (<code>/</code>).

Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Valid label values must be 63 characters or less and must be empty or begin and end with an alphanumeric character
(<code>[a-z0-9A-Z]</code>) with dashes (<code>-</code>), underscores (<code>_</code>), dots (<code>.</code>),
and alphanumerics between.
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,13 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.google.common.collect.ImmutableMap;
import org.csanchez.jenkins.plugins.kubernetes.pod.retention.PodRetention;
import org.csanchez.jenkins.plugins.kubernetes.volumes.EmptyDirVolume;
import org.csanchez.jenkins.plugins.kubernetes.volumes.PodVolume;
Expand Down Expand Up @@ -175,4 +179,29 @@ public KubernetesClient connect() throws UnrecoverableKeyException, NoSuchAlgori
plannedNodes = cloud.provision(test, 200);
assertEquals(10, plannedNodes.size());
}

@Test
public void testLabels() {
KubernetesCloud cloud = new KubernetesCloud("name");
assertEquals(KubernetesCloud.DEFAULT_POD_LABELS, cloud.getLabelsMap());
assertEquals(ImmutableMap.of("jenkins", "slave"), cloud.getLabelsMap());
assertEquals(Arrays.asList(new PodLabel("jenkins", "slave")), cloud.getLabels());

List<PodLabel> labels = Arrays.asList(new PodLabel("foo", "bar"), new PodLabel("cat", "dog"));
cloud.setLabels(labels);
Map<String, String> expected = new LinkedHashMap<>();
expected.put("foo", "bar");
expected.put("cat", "dog");
assertEquals(expected, cloud.getLabelsMap());
assertEquals(new ArrayList<>(labels), cloud.getLabels());

cloud.setLabels(null);
assertEquals(ImmutableMap.of("jenkins", "slave"), cloud.getLabelsMap());
assertEquals(Arrays.asList(new PodLabel("jenkins", "slave")), cloud.getLabels());

cloud.setLabels(new ArrayList<>());
assertEquals(ImmutableMap.of("jenkins", "slave"), cloud.getLabelsMap());
assertEquals(Arrays.asList(new PodLabel("jenkins", "slave")), cloud.getLabels());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ public void upgradeFrom_1_1() throws Exception {
FileSystemServiceAccountCredential cred1 = (FileSystemServiceAccountCredential) credentials.get(1);
StringCredentialsImpl cred2 = (StringCredentialsImpl) credentials.get(2);
assertEquals("mytoken", Secret.toString(cred2.getSecret()));
assertThat(cloud.getLabels(), hasEntry("jenkins", "slave"));
assertThat(cloud.getLabelsMap(), hasEntry("jenkins", "slave"));
assertEquals(cloud.DEFAULT_WAIT_FOR_POD_SEC, cloud.getWaitForPodSec());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ public static KubernetesCloud setupCloud(Object test, TestName name) throws Unre
CertificateEncodingException, NoSuchAlgorithmException, KeyStoreException, IOException {
KubernetesCloud cloud = new KubernetesCloud("kubernetes");
// unique labels per test
cloud.setLabels(getLabels(cloud, test, name));
cloud.setLabels(PodLabel.fromMap(getLabels(cloud, test, name)));
KubernetesClient client = cloud.connect();

// Run in our own testing namespace
Expand Down Expand Up @@ -143,7 +143,7 @@ public static Map<String, String> getLabels(Object o, TestName name) {
public static Map<String, String> getLabels(KubernetesCloud cloud, Object o, TestName name) {
HashMap<String, String> l = Maps.newHashMap(DEFAULT_LABELS);
if (cloud != null) {
l.putAll(cloud.getLabels());
l.putAll(cloud.getLabelsMap());
}
l.put("class", o.getClass().getSimpleName());
l.put("test", name.getMethodName());
Expand Down