Skip to content

Commit f9b70af

Browse files
committed
Add support for generic ephemeral storage
Signed-off-by: Marc Liechti <[email protected]>
1 parent 3954583 commit f9b70af

File tree

10 files changed

+378
-12
lines changed

10 files changed

+378
-12
lines changed
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package org.csanchez.jenkins.plugins.kubernetes.volumes;
2+
3+
import edu.umd.cs.findbugs.annotations.CheckForNull;
4+
import edu.umd.cs.findbugs.annotations.NonNull;
5+
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
6+
import hudson.Extension;
7+
import hudson.Util;
8+
import hudson.model.Descriptor;
9+
import hudson.util.ListBoxModel;
10+
import io.fabric8.kubernetes.api.model.Quantity;
11+
import io.fabric8.kubernetes.api.model.Volume;
12+
import io.fabric8.kubernetes.api.model.VolumeBuilder;
13+
import org.jenkinsci.Symbol;
14+
import org.kohsuke.accmod.Restricted;
15+
import org.kohsuke.accmod.restrictions.DoNotUse;
16+
import org.kohsuke.stapler.DataBoundConstructor;
17+
import org.kohsuke.stapler.DataBoundSetter;
18+
import org.kohsuke.stapler.interceptor.RequirePOST;
19+
20+
import java.util.Map;
21+
import java.util.Objects;
22+
import java.util.UUID;
23+
24+
/**
25+
* Uses a generic ephemeral volume, that is created before the agent pod is created, and terminated afterwards.
26+
* See <a href="https://kubernetes.io/docs/concepts/storage/ephemeral-volumes/#generic-ephemeral-volumes">Kubernetes documentation</a>
27+
*/
28+
@SuppressFBWarnings(
29+
value = "SE_NO_SERIALVERSIONID",
30+
justification = "Serialization happens exclusively through XStream and not Java Serialization.")
31+
public class GenericEphemeralVolume extends PodVolume {
32+
private String id;
33+
private String storageClassName;
34+
private String requestsSize;
35+
private String accessModes;
36+
private String mountPath;
37+
38+
@DataBoundConstructor
39+
public GenericEphemeralVolume() {
40+
this.id = UUID.randomUUID().toString().substring(0, 8);
41+
}
42+
43+
@CheckForNull
44+
public String getAccessModes() {
45+
return accessModes;
46+
}
47+
48+
@DataBoundSetter
49+
public void setAccessModes(@CheckForNull String accessModes) {
50+
this.accessModes = Util.fixEmpty(accessModes);
51+
}
52+
53+
@CheckForNull
54+
public String getRequestsSize() {
55+
return requestsSize;
56+
}
57+
58+
@DataBoundSetter
59+
public void setRequestsSize(@CheckForNull String requestsSize) {
60+
this.requestsSize = Util.fixEmpty(requestsSize);
61+
}
62+
63+
@CheckForNull
64+
public String getStorageClassName() {
65+
return storageClassName;
66+
}
67+
68+
@DataBoundSetter
69+
public void setStorageClassName(@CheckForNull String storageClassName) {
70+
this.storageClassName = Util.fixEmpty(storageClassName);
71+
}
72+
73+
@Override
74+
public String getMountPath() {
75+
return mountPath;
76+
}
77+
78+
@Override
79+
public Volume buildVolume(String volumeName, String podName) {
80+
return new VolumeBuilder().
81+
withName(volumeName).
82+
withNewEphemeral().
83+
withNewVolumeClaimTemplate().
84+
withNewSpec().
85+
withAccessModes(getAccessModes()).
86+
withStorageClassName(getStorageClassName()).
87+
withNewResources().
88+
withRequests(Map.of("storage", new Quantity(getRequestsSize()))).
89+
endResources().
90+
endSpec().
91+
endVolumeClaimTemplate().
92+
endEphemeral().
93+
build();
94+
}
95+
96+
@DataBoundSetter
97+
public void setMountPath(String mountPath) {
98+
this.mountPath = mountPath;
99+
}
100+
101+
@Override
102+
public boolean equals(Object o) {
103+
if (this == o) return true;
104+
if (o == null || getClass() != o.getClass()) return false;
105+
GenericEphemeralVolume that = (GenericEphemeralVolume) o;
106+
return Objects.equals(id, that.id)
107+
&& Objects.equals(storageClassName, that.storageClassName)
108+
&& Objects.equals(requestsSize, that.requestsSize)
109+
&& Objects.equals(accessModes, that.accessModes);
110+
}
111+
112+
@Override
113+
public int hashCode() {
114+
return Objects.hash(id, storageClassName, requestsSize, accessModes);
115+
}
116+
117+
@Extension
118+
@Symbol("genericEphemeralVolume")
119+
public static class DescriptorImpl extends Descriptor<PodVolume> {
120+
@Override
121+
public String getDisplayName() {
122+
return "Generic ephemeral volume";
123+
}
124+
125+
@SuppressWarnings("unused") // by stapler
126+
@RequirePOST
127+
@Restricted(DoNotUse.class) // stapler only
128+
public ListBoxModel doFillAccessModesItems() {
129+
return PVCVolumeUtils.ACCESS_MODES_BOX;
130+
}
131+
}
132+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package org.csanchez.jenkins.plugins.kubernetes.volumes.workspace;
2+
3+
import edu.umd.cs.findbugs.annotations.CheckForNull;
4+
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
5+
import hudson.Extension;
6+
import hudson.Util;
7+
import hudson.model.Descriptor;
8+
import hudson.util.ListBoxModel;
9+
import io.fabric8.kubernetes.api.model.Quantity;
10+
import io.fabric8.kubernetes.api.model.Volume;
11+
import io.fabric8.kubernetes.api.model.VolumeBuilder;
12+
import org.csanchez.jenkins.plugins.kubernetes.volumes.PVCVolumeUtils;
13+
import org.jenkinsci.Symbol;
14+
import org.kohsuke.accmod.Restricted;
15+
import org.kohsuke.accmod.restrictions.DoNotUse;
16+
import org.kohsuke.stapler.DataBoundConstructor;
17+
import org.kohsuke.stapler.interceptor.RequirePOST;
18+
19+
import java.util.Map;
20+
21+
/**
22+
* Uses a generic ephemeral volume, that is created before the agent pod is created, and terminated afterwards.
23+
*/
24+
@SuppressFBWarnings(
25+
value = "SE_NO_SERIALVERSIONID",
26+
justification = "Serialization happens exclusively through XStream and not Java Serialization.")
27+
public class GenericEphemeralWorkspaceVolume extends WorkspaceVolume {
28+
29+
private String storageClassName;
30+
private String requestsSize;
31+
private String accessModes;
32+
33+
@DataBoundConstructor
34+
public GenericEphemeralWorkspaceVolume(String storageClassName, String requestsSize, String accessModes) {
35+
this.storageClassName = storageClassName;
36+
this.requestsSize = requestsSize;
37+
this.accessModes = accessModes;
38+
}
39+
40+
public String getStorageClassName() {
41+
return storageClassName;
42+
}
43+
44+
public void setStorageClassName(String storageClassName) {
45+
this.storageClassName = storageClassName;
46+
}
47+
48+
public String getRequestsSize() {
49+
return requestsSize;
50+
}
51+
52+
public void setRequestsSize(@CheckForNull String requestsSize) {
53+
this.requestsSize = Util.fixEmpty(requestsSize);
54+
}
55+
56+
public String getAccessModes() {
57+
return accessModes;
58+
}
59+
60+
public void setAccessModes(String accessModes) {
61+
this.accessModes = accessModes;
62+
}
63+
64+
@Override
65+
public Volume buildVolume(String volumeName, String podName) {
66+
return new VolumeBuilder()
67+
.withName(volumeName)
68+
.withNewEphemeral()
69+
.withNewVolumeClaimTemplate()
70+
.withNewSpec()
71+
.withAccessModes(getAccessModes())
72+
.withStorageClassName(getStorageClassName())
73+
.withNewResources()
74+
.withRequests(Map.of("storage", new Quantity(getRequestsSize())))
75+
.endResources()
76+
.endSpec()
77+
.endVolumeClaimTemplate()
78+
.endEphemeral()
79+
.build();
80+
}
81+
82+
@Extension
83+
@Symbol("genericEphemeralWorkspaceVolume")
84+
public static class DescriptorImpl extends Descriptor<WorkspaceVolume> {
85+
@Override
86+
public String getDisplayName() {
87+
return "Generic Ephemerel Volume";
88+
}
89+
90+
@SuppressWarnings("unused") // by stapler
91+
@RequirePOST
92+
@Restricted(DoNotUse.class) // stapler only
93+
public ListBoxModel doFillAccessModesItems() {
94+
return PVCVolumeUtils.ACCESS_MODES_BOX;
95+
}
96+
}
97+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?jelly escape-by-default='true'?>
2+
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout"
3+
xmlns:t="/lib/hudson" xmlns:f="/lib/form">
4+
<f:description>
5+
Creates a generic ephemeral volume mount for a pod.
6+
</f:description>
7+
8+
<f:entry title="${%Mount path}" field="mountPath">
9+
<f:textbox />
10+
</f:entry>
11+
12+
<f:entry title="${%Storage Class Name}" field="storageClassName">
13+
<f:textbox />
14+
</f:entry>
15+
16+
<f:entry title="${%Requests Size}" field="requestsSize">
17+
<f:textbox />
18+
</f:entry>
19+
20+
<f:entry title="${%Access Modes}" field="accessModes">
21+
<f:select default="ReadWriteOnce"/>
22+
</f:entry>
23+
24+
</j:jelly>
25+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?jelly escape-by-default='true'?>
2+
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout"
3+
xmlns:t="/lib/hudson" xmlns:f="/lib/form">
4+
<f:description>
5+
Uses a generic ephemeral volume using the specified parameters.
6+
</f:description>
7+
8+
<f:entry title="${%Storage Class Name}" field="storageClassName">
9+
<f:textbox />
10+
</f:entry>
11+
12+
<f:entry title="${%Requests Size}" field="requestsSize">
13+
<f:textbox />
14+
</f:entry>
15+
16+
<f:entry title="${%Access Modes}" field="accessModes">
17+
<f:select default="ReadWriteOnce"/>
18+
</f:entry>
19+
20+
</j:jelly>
21+

src/test/java/org/csanchez/jenkins/plugins/kubernetes/casc/VolumeCasCTest.java

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,7 @@
99
import java.util.List;
1010
import org.csanchez.jenkins.plugins.kubernetes.KubernetesCloud;
1111
import org.csanchez.jenkins.plugins.kubernetes.PodTemplate;
12-
import org.csanchez.jenkins.plugins.kubernetes.volumes.ConfigMapVolume;
13-
import org.csanchez.jenkins.plugins.kubernetes.volumes.EmptyDirVolume;
14-
import org.csanchez.jenkins.plugins.kubernetes.volumes.HostPathVolume;
15-
import org.csanchez.jenkins.plugins.kubernetes.volumes.NfsVolume;
16-
import org.csanchez.jenkins.plugins.kubernetes.volumes.PersistentVolumeClaim;
17-
import org.csanchez.jenkins.plugins.kubernetes.volumes.PodVolume;
12+
import org.csanchez.jenkins.plugins.kubernetes.volumes.*;
1813
import org.junit.runner.RunWith;
1914
import org.junit.runners.Parameterized;
2015
import org.jvnet.hudson.test.RestartableJenkinsRule;
@@ -38,6 +33,7 @@ public static Object[] permutations() {
3833
{new HostPathVolumeStrategy(), "hostPath"},
3934
{new NfsVolumeStrategy(), "nfs"},
4035
{new PVCVolumeStrategy(), "pvc"},
36+
{new GenericEphemeralVolumeStrategy(), "genericEphemeral"},
4137
};
4238
}
4339

@@ -132,4 +128,17 @@ void _verify(PodVolume volume) {
132128
assertEquals("my-claim", d.getClaimName());
133129
}
134130
}
131+
132+
static class GenericEphemeralVolumeStrategy extends VolumeStrategy {
133+
134+
@Override
135+
void _verify(PodVolume volume) {
136+
assertThat(volume, instanceOf(GenericEphemeralVolume.class));
137+
GenericEphemeralVolume d = (GenericEphemeralVolume) volume;
138+
assertEquals("ReadWriteMany", d.getAccessModes());
139+
assertEquals("10Gi", d.getRequestsSize());
140+
assertEquals("/mnt/path", d.getMountPath());
141+
assertEquals("test-storageclass", d.getStorageClassName());
142+
}
143+
}
135144
}

src/test/java/org/csanchez/jenkins/plugins/kubernetes/casc/WorkspaceVolumeCasCTest.java

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,8 @@
99
import java.util.List;
1010
import org.csanchez.jenkins.plugins.kubernetes.KubernetesCloud;
1111
import org.csanchez.jenkins.plugins.kubernetes.PodTemplate;
12-
import org.csanchez.jenkins.plugins.kubernetes.volumes.workspace.DynamicPVCWorkspaceVolume;
13-
import org.csanchez.jenkins.plugins.kubernetes.volumes.workspace.EmptyDirWorkspaceVolume;
14-
import org.csanchez.jenkins.plugins.kubernetes.volumes.workspace.HostPathWorkspaceVolume;
15-
import org.csanchez.jenkins.plugins.kubernetes.volumes.workspace.NfsWorkspaceVolume;
16-
import org.csanchez.jenkins.plugins.kubernetes.volumes.workspace.PersistentVolumeClaimWorkspaceVolume;
17-
import org.csanchez.jenkins.plugins.kubernetes.volumes.workspace.WorkspaceVolume;
12+
import org.csanchez.jenkins.plugins.kubernetes.volumes.GenericEphemeralVolume;
13+
import org.csanchez.jenkins.plugins.kubernetes.volumes.workspace.*;
1814
import org.junit.runner.RunWith;
1915
import org.junit.runners.Parameterized;
2016
import org.jvnet.hudson.test.RestartableJenkinsRule;
@@ -38,6 +34,7 @@ public static Object[] permutations() {
3834
{new HostPathWorkspaceVolumeStrategy(), "hostPath"},
3935
{new NfsWorkspaceVolumeStrategy(), "nfs"},
4036
{new PVCWorkspaceVolumeStrategy(), "pvc"},
37+
{new GenericEphemeralWorkspaceVolumeStrategy(), "genericEphemeral"},
4138
};
4239
}
4340

@@ -126,4 +123,16 @@ void verify(WorkspaceVolume workspaceVolume) {
126123
assertEquals("my-claim", d.getClaimName());
127124
}
128125
}
126+
127+
static class GenericEphemeralWorkspaceVolumeStrategy extends WorkspaceVolumeStrategy {
128+
129+
@Override
130+
void verify(WorkspaceVolume workspaceVolume) {
131+
assertThat(workspaceVolume, instanceOf(GenericEphemeralWorkspaceVolume.class));
132+
GenericEphemeralWorkspaceVolume d = (GenericEphemeralWorkspaceVolume) workspaceVolume;
133+
assertEquals("my-storageclass", d.getStorageClassName());
134+
assertEquals("ReadWriteMany", d.getAccessModes());
135+
assertEquals("10Gi", d.getRequestsSize());
136+
}
137+
}
129138
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package org.csanchez.jenkins.plugins.kubernetes.volumes;
2+
3+
import io.fabric8.kubernetes.api.model.Volume;
4+
import org.junit.Test;
5+
6+
import static org.junit.Assert.assertEquals;
7+
8+
public class GenericEphemeralVolumeTest {
9+
10+
@Test
11+
public void testCreatesVolumeCorrectly() {
12+
13+
GenericEphemeralVolume genericEphemeralVolume = new GenericEphemeralVolume();
14+
genericEphemeralVolume.setAccessModes("ReadWriteOnce");
15+
genericEphemeralVolume.setRequestsSize("1Gi");
16+
genericEphemeralVolume.setStorageClassName("standard");
17+
genericEphemeralVolume.setMountPath("/tmp");
18+
19+
Volume volume = genericEphemeralVolume.buildVolume("testvolume", "testpod");
20+
21+
assertEquals("testvolume", volume.getName());
22+
assertEquals("ReadWriteOnce", volume.getEphemeral().getVolumeClaimTemplate().getSpec().getAccessModes().get(0));
23+
assertEquals("standard", volume.getEphemeral().getVolumeClaimTemplate().getSpec().getStorageClassName());
24+
assertEquals("1Gi", volume.getEphemeral().getVolumeClaimTemplate().getSpec().getResources().getRequests().get("storage").toString());
25+
}
26+
}

0 commit comments

Comments
 (0)