Skip to content

Commit 3b88431

Browse files
authored
Merge pull request #1503 from Vlatombe/JENKINS-71956-octal-mode-improvements
[JENKINS-71956] Octal notation fixes
2 parents 8a13bd0 + d876d6e commit 3b88431

File tree

4 files changed

+169
-13
lines changed

4 files changed

+169
-13
lines changed

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

+58-13
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
import hudson.model.Label;
1515
import hudson.model.Node;
1616
import hudson.slaves.NodeProperty;
17-
import io.fabric8.kubernetes.api.model.ConfigMapProjection;
1817
import io.fabric8.kubernetes.api.model.Container;
1918
import io.fabric8.kubernetes.api.model.ContainerBuilder;
2019
import io.fabric8.kubernetes.api.model.EnvFromSource;
@@ -27,7 +26,6 @@
2726
import io.fabric8.kubernetes.api.model.PodSpec;
2827
import io.fabric8.kubernetes.api.model.Quantity;
2928
import io.fabric8.kubernetes.api.model.ResourceRequirements;
30-
import io.fabric8.kubernetes.api.model.SecretProjection;
3129
import io.fabric8.kubernetes.api.model.Toleration;
3230
import io.fabric8.kubernetes.api.model.Volume;
3331
import io.fabric8.kubernetes.api.model.VolumeMount;
@@ -72,6 +70,14 @@ public class PodTemplateUtils {
7270
@SuppressFBWarnings(value = "MS_SHOULD_BE_FINAL", justification = "tests & emergency admin")
7371
public static boolean SUBSTITUTE_ENV = Boolean.getBoolean(PodTemplateUtils.class.getName() + ".SUBSTITUTE_ENV");
7472

73+
/**
74+
* If true, all modes permissions provided to pods are expected to be provided in decimal notation.
75+
* Otherwise, the plugin will consider they are written in octal notation.
76+
*/
77+
@SuppressFBWarnings(value = "MS_SHOULD_BE_FINAL", justification = "tests & emergency admin")
78+
public static /* almost final*/ boolean DISABLE_OCTAL_MODES =
79+
Boolean.getBoolean(PodTemplateUtils.class.getName() + ".DISABLE_OCTAL_MODES");
80+
7581
/**
7682
* Combines a {@link ContainerTemplate} with its parent.
7783
* @param parent The parent container template (nullable).
@@ -705,24 +711,42 @@ public static Pod parseFromYaml(String yaml) {
705711
if (podFromYaml.getSpec() == null) {
706712
podFromYaml.setSpec(new PodSpec());
707713
}
708-
fixOctal(podFromYaml);
714+
if (!DISABLE_OCTAL_MODES) {
715+
fixOctal(podFromYaml);
716+
}
709717
return podFromYaml;
710718
}
711719
}
712720

713721
private static void fixOctal(@NonNull Pod podFromYaml) {
722+
podFromYaml.getSpec().getVolumes().stream().map(Volume::getConfigMap).forEach(configMap -> {
723+
if (configMap != null) {
724+
var defaultMode = configMap.getDefaultMode();
725+
if (defaultMode != null) {
726+
configMap.setDefaultMode(convertPermissionToOctal(defaultMode));
727+
}
728+
}
729+
});
730+
podFromYaml.getSpec().getVolumes().stream().map(Volume::getSecret).forEach(secretVolumeSource -> {
731+
if (secretVolumeSource != null) {
732+
var defaultMode = secretVolumeSource.getDefaultMode();
733+
if (defaultMode != null) {
734+
secretVolumeSource.setDefaultMode(convertPermissionToOctal(defaultMode));
735+
}
736+
}
737+
});
714738
podFromYaml.getSpec().getVolumes().stream().map(Volume::getProjected).forEach(projected -> {
715739
if (projected != null) {
716-
Integer defaultMode = projected.getDefaultMode();
740+
var defaultMode = projected.getDefaultMode();
717741
if (defaultMode != null) {
718-
projected.setDefaultMode(convertToOctal(defaultMode));
742+
projected.setDefaultMode(convertPermissionToOctal(defaultMode));
719743
}
720-
projected.getSources().stream().forEach(source -> {
721-
ConfigMapProjection configMap = source.getConfigMap();
744+
projected.getSources().forEach(source -> {
745+
var configMap = source.getConfigMap();
722746
if (configMap != null) {
723747
convertDecimalIntegersToOctal(configMap.getItems());
724748
}
725-
SecretProjection secret = source.getSecret();
749+
var secret = source.getSecret();
726750
if (secret != null) {
727751
convertDecimalIntegersToOctal(secret.getItems());
728752
}
@@ -732,16 +756,37 @@ private static void fixOctal(@NonNull Pod podFromYaml) {
732756
}
733757

734758
private static void convertDecimalIntegersToOctal(List<KeyToPath> items) {
735-
items.stream().forEach(i -> {
736-
Integer mode = i.getMode();
759+
items.forEach(i -> {
760+
var mode = i.getMode();
737761
if (mode != null) {
738-
i.setMode(convertToOctal(mode));
762+
i.setMode(convertPermissionToOctal(mode));
739763
}
740764
});
741765
}
742766

743-
private static int convertToOctal(Integer defaultMode) {
744-
return Integer.parseInt(Integer.toString(defaultMode, 10), 8);
767+
/**
768+
* Permissions are generally expressed in octal notation, e.g. 0777.
769+
* After parsing, this is stored as the integer 777, but the snakeyaml-engine does not convert to decimal first.
770+
* When the client later sends the pod spec to the server, it sends the integer as is through the json schema,
771+
* however the server expects a decimal, which means an integer between 0 and 511.
772+
*
773+
* The user can also provide permissions as a decimal integer, e.g. 511.
774+
*
775+
* This method attempts to guess whether the user provided a decimal or octal integer, and converts to octal if needed,
776+
* so that the resulting can be submitted to the server.
777+
*
778+
*/
779+
static int convertPermissionToOctal(Integer i) {
780+
// Permissions are expressed as octal integers
781+
// octal goes from 0000 to 0777
782+
// decimal goes from 0 to 511
783+
var s = Integer.toString(i, 10);
784+
// If the input has a digit which is 8 or 9, this was likely a decimal input. Best effort support here.
785+
if (s.chars().map(c -> c - '0').anyMatch(a -> a > 7)) {
786+
return i;
787+
} else {
788+
return Integer.parseInt(s, 8);
789+
}
745790
}
746791

747792
public static Collection<String> validateYamlContainerNames(List<String> yamls) {

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

+53
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,16 @@
4747
import io.fabric8.kubernetes.api.model.Toleration;
4848
import io.fabric8.kubernetes.api.model.VolumeMount;
4949
import io.fabric8.kubernetes.api.model.VolumeMountBuilder;
50+
import java.io.IOException;
51+
import java.nio.charset.StandardCharsets;
5052
import java.util.ArrayList;
5153
import java.util.Arrays;
5254
import java.util.Collections;
5355
import java.util.HashMap;
5456
import java.util.List;
5557
import java.util.Map;
5658
import java.util.stream.Collectors;
59+
import org.apache.commons.io.IOUtils;
5760
import org.csanchez.jenkins.plugins.kubernetes.model.KeyValueEnvVar;
5861
import org.csanchez.jenkins.plugins.kubernetes.model.SecretEnvVar;
5962
import org.csanchez.jenkins.plugins.kubernetes.volumes.HostPathVolume;
@@ -884,4 +887,54 @@ public void testParseYaml() {
884887
PodTemplateUtils.parseFromYaml(null);
885888
PodTemplateUtils.parseFromYaml("");
886889
}
890+
891+
@Test
892+
public void octalParsing() throws IOException {
893+
var fileStream = getClass().getResourceAsStream(getClass().getSimpleName() + "/octal.yaml");
894+
assertNotNull(fileStream);
895+
var pod = parseFromYaml(IOUtils.toString(fileStream, StandardCharsets.UTF_8));
896+
checkParsed(pod);
897+
}
898+
899+
@Test
900+
public void decimalParsing() throws IOException {
901+
try {
902+
DISABLE_OCTAL_MODES = true;
903+
var fileStream = getClass().getResourceAsStream(getClass().getSimpleName() + "/decimal.yaml");
904+
assertNotNull(fileStream);
905+
var pod = parseFromYaml(IOUtils.toString(fileStream, StandardCharsets.UTF_8));
906+
checkParsed(pod);
907+
} finally {
908+
DISABLE_OCTAL_MODES = false;
909+
}
910+
}
911+
912+
private static void checkParsed(Pod pod) {
913+
assertEquals(
914+
Integer.valueOf("755", 8),
915+
pod.getSpec().getVolumes().get(0).getConfigMap().getDefaultMode());
916+
assertEquals(
917+
Integer.valueOf("744", 8),
918+
pod.getSpec().getVolumes().get(1).getSecret().getDefaultMode());
919+
var projectedVolume = pod.getSpec().getVolumes().get(2).getProjected();
920+
assertEquals(Integer.valueOf("644", 8), projectedVolume.getDefaultMode());
921+
assertEquals(
922+
Integer.valueOf("400", 8),
923+
projectedVolume
924+
.getSources()
925+
.get(0)
926+
.getConfigMap()
927+
.getItems()
928+
.get(0)
929+
.getMode());
930+
assertEquals(
931+
Integer.valueOf("600", 8),
932+
projectedVolume
933+
.getSources()
934+
.get(1)
935+
.getSecret()
936+
.getItems()
937+
.get(0)
938+
.getMode());
939+
}
887940
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Identical to octal.yaml with values converted to decimal
2+
apiVersion: "v1"
3+
kind: "Pod"
4+
spec:
5+
volumes:
6+
- configMap:
7+
name: cm1
8+
defaultMode: 493
9+
name: "volume1"
10+
- secret:
11+
secretName: secret1
12+
defaultMode: 484
13+
name: "volume2"
14+
- projected:
15+
sources:
16+
- configMap:
17+
name: cm2
18+
items:
19+
- key: username
20+
path: my-group/my-username
21+
mode: 256
22+
- secret:
23+
name: secret2
24+
items:
25+
- key: username
26+
path: my-group/my-username
27+
mode: 384
28+
defaultMode: 420
29+
name: "volume3"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Octal modes
2+
apiVersion: "v1"
3+
kind: "Pod"
4+
spec:
5+
volumes:
6+
- configMap:
7+
name: cm1
8+
defaultMode: 0755
9+
name: "volume1"
10+
- secret:
11+
secretName: secret1
12+
defaultMode: 0744
13+
name: "volume2"
14+
- projected:
15+
sources:
16+
- configMap:
17+
name: cm2
18+
items:
19+
- key: username
20+
path: my-group/my-username
21+
mode: 0400
22+
- secret:
23+
name: secret2
24+
items:
25+
- key: username
26+
path: my-group/my-username
27+
mode: 0600
28+
defaultMode: 0644
29+
name: "volume3"

0 commit comments

Comments
 (0)