Skip to content

Commit

Permalink
Add support for multiple initContainers to KubernetesDeployerProperti…
Browse files Browse the repository at this point in the history
…es. (#466)

* Add support for initContainers to KubernetesDeployerProperties.

Fixes #465

* Remove initContainer that has been merged in initContainers.
  • Loading branch information
corneil authored Aug 27, 2024
1 parent 65dbb7d commit 293a986
Show file tree
Hide file tree
Showing 4 changed files with 155 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

package org.springframework.cloud.deployer.spi.kubernetes;


import java.util.Collection;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
Expand Down Expand Up @@ -64,6 +66,7 @@
* @author Enrique Medina Montenegro
* @author Ilayaperumal Gopinathan
* @author Chris Bono
* @author Corneil du Plessis
*/
public class AbstractKubernetesDeployer {

Expand Down Expand Up @@ -283,12 +286,14 @@ PodSpec createPodSpec(AppDeploymentRequest appDeploymentRequest) {
podSpec.withAffinity(affinity);
}

Container initContainer = this.deploymentPropertiesResolver.getInitContainer(deploymentProperties);
if (initContainer != null) {
if (initContainer.getSecurityContext() == null && containerSecurityContext != null) {
initContainer.setSecurityContext(containerSecurityContext);
Collection<Container> initContainers = this.deploymentPropertiesResolver.getInitContainers(deploymentProperties);
if (initContainers != null && !initContainers.isEmpty()) {
for (Container initContainer : initContainers) {
if (initContainer.getSecurityContext() == null && containerSecurityContext != null) {
initContainer.setSecurityContext(containerSecurityContext);
}
podSpec.addToInitContainers(initContainer);
}
podSpec.addToInitContainers(initContainer);
}

Boolean shareProcessNamespace = this.deploymentPropertiesResolver.getShareProcessNamespace(deploymentProperties);
Expand All @@ -310,9 +315,7 @@ PodSpec createPodSpec(AppDeploymentRequest appDeploymentRequest) {

List<Container> allContainers = new ArrayList<>();
allContainers.add(container);
if (initContainer != null) {
allContainers.add(initContainer);
}

allContainers.addAll(additionalContainers);
// only add volumes with corresponding volume mounts in any container.
podSpec.withVolumes(this.deploymentPropertiesResolver.getVolumes(deploymentProperties).stream()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
Expand Down Expand Up @@ -65,6 +66,7 @@
import org.springframework.cloud.deployer.spi.util.ByteSizeUtils;
import org.springframework.cloud.deployer.spi.util.CommandLineTokenizer;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
Expand All @@ -77,6 +79,7 @@
* @author Chris Schaefer
* @author Ilayaperumal Gopinathan
* @author Chris Bono
* @author Corneil du Plessis
*/

class DeploymentPropertiesResolver {
Expand Down Expand Up @@ -586,44 +589,79 @@ Affinity getAffinityRules(Map<String, String> kubernetesDeployerProperties) {
return affinity;
}

Container getInitContainer(Map<String, String> kubernetesDeployerProperties) {
Collection<Container> getInitContainers(Map<String, String> kubernetesDeployerProperties) {
Collection<Container> initContainers = new ArrayList<>();
KubernetesDeployerProperties deployerProperties = bindProperties(kubernetesDeployerProperties,
this.propertyPrefix + ".initContainer", "initContainer");

// Deployment prop passed in for entire '.initContainer'
InitContainer initContainerProps = deployerProperties.getInitContainer();
if (initContainerProps != null) {
return containerFromProps(initContainerProps);
initContainers.add(containerFromProps(initContainerProps));
} else {
String propertyKey = this.propertyPrefix + ".initContainer";
Container container = initContainerFromProperties(kubernetesDeployerProperties, propertyKey);
if (container != null) {
initContainers.add(container);
} else {
initContainerProps = this.properties.getInitContainer();
if (initContainerProps != null) {
initContainers.add(containerFromProps(initContainerProps));
}
}
}
KubernetesDeployerProperties initContainerDeployerProperties = bindProperties(kubernetesDeployerProperties,
this.propertyPrefix + ".initContainers", "initContainers");
for (InitContainer initContainer : initContainerDeployerProperties.getInitContainers()) {
initContainers.add(containerFromProps(initContainer));
}
if(initContainerDeployerProperties.getInitContainers().isEmpty()) {
for (int i = 0; ; i++) {
String propertyKey = this.propertyPrefix + ".initContainers[" + i + "]";
// Get properties using binding
KubernetesDeployerProperties kubeProps = bindProperties(kubernetesDeployerProperties, propertyKey, "initContainer");
if (kubeProps.getInitContainer() != null) {
initContainers.add(containerFromProps(kubeProps.getInitContainer()));
} else {
// Get properties using FQN
Container initContainer = initContainerFromProperties(kubernetesDeployerProperties, propertyKey);
if (initContainer != null) {
initContainers.add(initContainer);
} else {
// Use default is configured
if (properties.getInitContainers().size() > i) {
initContainers.add(containerFromProps(properties.getInitContainers().get(i)));
}
break;
}
}
}
}
if (!properties.getInitContainers().isEmpty()) {
// Add remaining defaults.
for (int i = initContainers.size(); i < properties.getInitContainers().size(); i++) {
initContainers.add(containerFromProps(properties.getInitContainers().get(i)));
}
}
return initContainers;
}

// Deployment props passed in for specific '.initContainer.<property>'
String containerName = PropertyParserUtils.getDeploymentPropertyValue(kubernetesDeployerProperties,
this.propertyPrefix + ".initContainer.containerName");
String imageName = PropertyParserUtils.getDeploymentPropertyValue(kubernetesDeployerProperties,
this.propertyPrefix + ".initContainer.imageName");

private @Nullable Container initContainerFromProperties(Map<String, String> kubeProps, String propertyKey) {
String containerName = PropertyParserUtils.getDeploymentPropertyValue(kubeProps, propertyKey + ".containerName");
String imageName = PropertyParserUtils.getDeploymentPropertyValue(kubeProps, propertyKey + ".imageName");
if (StringUtils.hasText(containerName) && StringUtils.hasText(imageName)) {
String commandsStr = PropertyParserUtils.getDeploymentPropertyValue(kubernetesDeployerProperties,
this.propertyPrefix + ".initContainer.commands");
List<String> commands = StringUtils.hasText(commandsStr) ? Arrays.stream(commandsStr.split(",")).collect(Collectors.toList()) : Collections.emptyList();
String envString = PropertyParserUtils.getDeploymentPropertyValue(kubernetesDeployerProperties,
this.propertyPrefix + ".initContainer.environmentVariables");
List<VolumeMount> vms = this.getInitContainerVolumeMounts(kubernetesDeployerProperties);
String commandsStr = PropertyParserUtils.getDeploymentPropertyValue(kubeProps, propertyKey + ".commands");
List<String> commands = StringUtils.hasText(commandsStr) ? Arrays.asList(commandsStr.split(",")) : Collections.emptyList();
String envString = PropertyParserUtils.getDeploymentPropertyValue(kubeProps, propertyKey + "environmentVariables");
List<VolumeMount> vms = this.getInitContainerVolumeMounts(kubeProps, propertyKey);
return new ContainerBuilder()
.withName(containerName)
.withImage(imageName)
.withCommand(commands)
.withEnv(toEnvironmentVariables((envString != null)? envString.split(","): new String[0]))
.withEnv(toEnvironmentVariables((envString != null) ? envString.split(",") : new String[0]))
.addAllToVolumeMounts(vms)
.build();
}

// Default is global initContainer
initContainerProps = this.properties.getInitContainer();
if (initContainerProps != null) {
return containerFromProps(initContainerProps);
}

return null;
}

Expand Down Expand Up @@ -836,9 +874,11 @@ List<VolumeMount> getVolumeMounts(Map<String, String> deploymentProperties) {
* @param deploymentProperties the deployment properties from {@link AppDeploymentRequest}
* @return the configured volume mounts
*/
private List<VolumeMount> getInitContainerVolumeMounts(Map<String, String> deploymentProperties) {
return this.getVolumeMounts(PropertyParserUtils.getDeploymentPropertyValue(deploymentProperties,
this.propertyPrefix + ".initContainer.volumeMounts"));
private List<VolumeMount> getInitContainerVolumeMounts(Map<String, String> deploymentProperties, String propertyKey) {
return this.getVolumeMounts(PropertyParserUtils.getDeploymentPropertyValue(
deploymentProperties,
propertyKey + ".volumeMounts")
);
}

private List<VolumeMount> getVolumeMounts(String propertyValue) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1439,7 +1439,11 @@ public void setTaskServiceAccountName(String taskServiceAccountName) {
* A custom init container to apply.
*/
private InitContainer initContainer;

/**
* Apply multiple custom init containers.
* Will add initContainer first if it exists and add to list.
*/
private List<InitContainer> initContainers = new ArrayList<>();
/**
* Lifecycle spec to apply.
*/
Expand Down Expand Up @@ -2494,6 +2498,14 @@ public void setInitContainer(InitContainer initContainer) {
this.initContainer = initContainer;
}

public List<InitContainer> getInitContainers() {
return initContainers;
}

public void setInitContainers(List<InitContainer> initContainers) {
this.initContainers = initContainers;
}

public List<Container> getAdditionalContainers() {
return this.additionalContainers;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
* @author Chris Schaefer
* @author Enrique Medina Montenegro
* @author Chris Bono
* @author Corneil du Plessis
*/
@DisplayName("KubernetesAppDeployer")
public class KubernetesAppDeployerTests {
Expand Down Expand Up @@ -891,6 +892,71 @@ public void testConfigMapKeyRefGlobalFromYaml() throws Exception {
assertThat(configMapKeySelector.getName()).as("Unexpected config map name").isEqualTo("myConfigMap");
assertThat(configMapKeySelector.getKey()).as("Unexpected config map data key").isEqualTo("envName");
}
@Test
public void testInitContainerProperties() {
Map<String, String> props = new HashMap<>();
props.put("spring.cloud.deployer.kubernetes.initContainer", "{ \"imageName\": \"busybox:1\", \"containerName\": \"bb_s1\", \"commands\": [\"sh\", \"-c\", \"script1.sh\"] }");

AppDefinition definition = new AppDefinition("app-test", null);
AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props);

deployer = k8sAppDeployer(new KubernetesDeployerProperties());
PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest);
assertThat(podSpec.getInitContainers()).isNotEmpty();
Container container = podSpec.getInitContainers().get(0);
assertThat(container.getImage()).isEqualTo("busybox:1");
assertThat(container.getName()).isEqualTo("bb_s1");
assertThat(container.getCommand()).containsExactly("sh", "-c", "script1.sh");
}
@Test
public void testInitContainerJsonArrayProperties() {
Map<String, String> props = new HashMap<>();
props.put("spring.cloud.deployer.kubernetes.init-containers", "[{ \"imageName\": \"busybox:1\", \"containerName\": \"bb_s1\", \"commands\": [\"sh\", \"-c\", \"script1.sh\"] }]");

AppDefinition definition = new AppDefinition("app-test", null);
AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props);

deployer = k8sAppDeployer(new KubernetesDeployerProperties());
PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest);
assertThat(podSpec.getInitContainers()).isNotEmpty();
Container container = podSpec.getInitContainers().get(0);
assertThat(container.getImage()).isEqualTo("busybox:1");
assertThat(container.getName()).isEqualTo("bb_s1");
assertThat(container.getCommand()).containsExactly("sh", "-c", "script1.sh");
}
@Test
public void testMultipleInitContainerProperties() {
Map<String, String> props = new HashMap<>();
props.put("spring.cloud.deployer.kubernetes.initContainers[0]", "{ \"imageName\": \"busybox:1\", \"containerName\": \"bb_s1\", \"commands\": [\"sh\", \"-c\", \"script1.sh\"] }");
props.put("spring.cloud.deployer.kubernetes.initContainers[1].imageName", "busybox:2");
props.put("spring.cloud.deployer.kubernetes.initContainers[1].containerName", "bb_s2");
props.put("spring.cloud.deployer.kubernetes.initContainers[1].commands", "sh,-c,script2.sh");
props.put("spring.cloud.deployer.kubernetes.initContainers[2]", "{ \"imageName\": \"busybox:3\", \"containerName\": \"bb_s3\", \"commands\": [\"sh\", \"-c\", \"script3.sh\"], \"volumeMounts\": [{\"mountPath\": \"/data\", \"name\": \"s3vol\", \"readOnly\": true}] }");

AppDefinition definition = new AppDefinition("app-test", null);
AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props);

deployer = k8sAppDeployer(new KubernetesDeployerProperties());
PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest);
assertThat(podSpec.getInitContainers()).isNotEmpty();
assertThat(podSpec.getInitContainers().size()).isEqualTo(3);
Container container0 = podSpec.getInitContainers().get(0);
assertThat(container0.getImage()).isEqualTo("busybox:1");
assertThat(container0.getName()).isEqualTo("bb_s1");
assertThat(container0.getCommand()).containsExactly("sh", "-c", "script1.sh");
Container container1 = podSpec.getInitContainers().get(1);
assertThat(container1.getImage()).isEqualTo("busybox:2");
assertThat(container1.getName()).isEqualTo("bb_s2");
assertThat(container1.getCommand()).containsExactly("sh", "-c", "script2.sh");
Container container2 = podSpec.getInitContainers().get(2);
assertThat(container2.getImage()).isEqualTo("busybox:3");
assertThat(container2.getName()).isEqualTo("bb_s3");
assertThat(container2.getCommand()).containsExactly("sh", "-c", "script3.sh");
assertThat(container2.getVolumeMounts()).isNotEmpty();
assertThat(container2.getVolumeMounts().get(0).getName()).isEqualTo("s3vol");
assertThat(container2.getVolumeMounts().get(0).getMountPath()).isEqualTo("/data");
assertThat(container2.getVolumeMounts().get(0).getReadOnly()).isTrue();
}

@Test
public void testNodeAffinityProperty() {
Expand Down

0 comments on commit 293a986

Please sign in to comment.