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

Add support for sidecar container #423

Closed
Show file tree
Hide file tree
Changes from all 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
@@ -1,5 +1,5 @@
/*
* Copyright 2015-2020 the original author or authors.
* Copyright 2015-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -258,7 +258,7 @@ PodSpec createPodSpec(AppDeploymentRequest appDeploymentRequest) {
if (initContainer != null) {
podSpec.addToInitContainers(initContainer);
}

podSpec.addAllToContainers(this.deploymentPropertiesResolver.getAdditionalContainers(deploymentProperties));
return podSpec.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,30 @@ Container getInitContainer(Map<String, String> kubernetesDeployerProperties) {
return container;
}

List<Container> getAdditionalContainers(Map<String, String> deploymentProperties) {
List<Container> containers = new ArrayList<>();

KubernetesDeployerProperties deployerProperties = bindProperties(deploymentProperties,
this.propertyPrefix + ".additionalContainers", "additionalContainers" );

if (deployerProperties.getAdditionalContainers() != null) {
deployerProperties.getAdditionalContainers().forEach(container ->
containers.add(container));
}

// Add the containers from the original properties excluding the containers with the matching names from the
// deployment properties
if (this.properties.getAdditionalContainers() != null) {
this.properties.getAdditionalContainers().stream()
.filter(container -> containers.stream()
.noneMatch(existing -> existing.getName().equals(container.getName())))
.collect(Collectors.toList())
.forEach(container -> containers.add(container));
}

return containers;
}

Map<String, String> getPodAnnotations(Map<String, String> kubernetesDeployerProperties) {
String annotationsValue = PropertyParserUtils.getDeploymentPropertyValue(kubernetesDeployerProperties,
this.propertyPrefix + ".podAnnotations", "");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,13 @@ public Long getFsGroup() {
}
}

public static class InitContainer {
public static class InitContainer extends ContainerProperties {
}

static class Container extends io.fabric8.kubernetes.api.model.Container {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I am curious if we can use the same approach of using fabric8 api models to other supported properties as well. I believe this can eliminate some explicit models with a fewer properties (for instance the ContainerProperties we have for InitContainer).

Also, I thought having an explicit type named Container could help here. But, I am ok if we can directly use the fabric8 Container type as is in this class.

Copy link
Contributor

Choose a reason for hiding this comment

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

you could use the fabric8 objects, but thats a bit leakly. theres an open issue around this: #296

}

public static class ContainerProperties {
private String imageName;
private String containerName;
private List<String> commands;
Expand Down Expand Up @@ -687,6 +693,11 @@ public void setVolumeMounts(List<VolumeMount> volumeMounts) {
*/
private InitContainer initContainer;

/**
* The additional containers one can add to the main application container.
*/
private List<Container> additionalContainers;

public String getNamespace() {
return namespace;
}
Expand Down Expand Up @@ -1311,4 +1322,12 @@ public InitContainer getInitContainer() {
public void setInitContainer(InitContainer initContainer) {
this.initContainer = initContainer;
}

public List<Container> getAdditionalContainers() {
return this.additionalContainers;
}

public void setAdditionalContainers(List<Container> additionalContainers) {
this.additionalContainers = additionalContainers;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
import java.util.HashMap;
Expand Down Expand Up @@ -1208,6 +1209,196 @@ public void testCreateInitContainerWithVolumeMounts() {
Matchers.hasProperty("state", is(unknown))), timeout.maxAttempts, timeout.pause));
}

@Test
public void testCreateAdditionalContainers() {
log.info("Testing {}...", "CreateAdditionalContainers");
KubernetesAppDeployer kubernetesAppDeployer = new KubernetesAppDeployer(new KubernetesDeployerProperties(),
kubernetesClient);

Map<String, String> props = Stream.of(new String[][]{
{
"spring.cloud.deployer.kubernetes.volumes",
"[{name: 'test-volume', emptyDir: {}}]",
},
{
"spring.cloud.deployer.kubernetes.volumeMounts",
"[{name: 'test-volume', mountPath: '/tmp'}]",
},
{
"spring.cloud.deployer.kubernetes.additional-containers",
"[{name: 'c1', image: 'busybox:latest', command: ['sh', '-c', 'echo hello1'], volumeMounts: [{name: 'test-volume', mountPath: '/tmp', readOnly: true}]},"
+ "{name: 'c2', image: 'busybox:1.26.1', command: ['sh', '-c', 'echo hello2']}]"
}}).collect(Collectors.toMap(data -> data[0], data -> data[1]));

AppDefinition definition = new AppDefinition(randomName(), null);
AppDeploymentRequest request = new AppDeploymentRequest(definition, testApplication(), props);

log.info("Deploying {}...", request.getDefinition().getName());
String deploymentId = kubernetesAppDeployer.deploy(request);
Timeout timeout = deploymentTimeout();
assertThat(deploymentId, eventually(hasStatusThat(
Matchers.hasProperty("state", is(deployed))), timeout.maxAttempts, timeout.pause));

Deployment deployment = kubernetesClient.apps().deployments().withName(request.getDefinition().getName()).get();
List<Container> containers = deployment.getSpec().getTemplate().getSpec().getContainers();

assertTrue("Number of containers is incorrect", containers.size() == 3);

Optional<Container> additionalContainer1 = containers.stream().filter(i -> i.getName().equals("c1")).findFirst();
assertTrue("Additional container c1 not found", additionalContainer1.isPresent());

Container testAdditionalContainer1 = additionalContainer1.get();

assertEquals("Unexpected additional container name", testAdditionalContainer1.getName(), "c1");
assertEquals("Unexpected additional container image", testAdditionalContainer1.getImage(), "busybox:latest");

List<String> commands = testAdditionalContainer1.getCommand();

assertTrue("Additional container commands missing", commands != null && !commands.isEmpty());
assertEquals("Invalid number of additional container commands", 3, commands.size());
assertEquals("sh", commands.get(0));
assertEquals("-c", commands.get(1));
assertEquals("echo hello1", commands.get(2));

List<VolumeMount> volumeMounts = testAdditionalContainer1.getVolumeMounts();

assertTrue("Volume mount size is incorrect", volumeMounts.size() == 1);
assertEquals("test-volume", volumeMounts.get(0).getName());
assertEquals("/tmp", volumeMounts.get(0).getMountPath());
assertEquals(Boolean.TRUE, volumeMounts.get(0).getReadOnly());

Optional<Container> additionalContainer2 = containers.stream().filter(i -> i.getName().equals("c2")).findFirst();
assertTrue("Additional container c2 not found", additionalContainer2.isPresent());

Container testAdditionalContainer2 = additionalContainer2.get();

assertEquals("Unexpected additional container name", testAdditionalContainer2.getName(), "c2");
assertEquals("Unexpected additional container image", testAdditionalContainer2.getImage(), "busybox:1.26.1");

List<String> container2Commands = testAdditionalContainer2.getCommand();

assertTrue("Additional container commands missing", container2Commands != null && !container2Commands.isEmpty());
assertEquals("Invalid number of additional container commands", 3, container2Commands.size());
assertEquals("sh", container2Commands.get(0));
assertEquals("-c", container2Commands.get(1));
assertEquals("echo hello2", container2Commands.get(2));

log.info("Undeploying {}...", deploymentId);
timeout = undeploymentTimeout();
kubernetesAppDeployer.undeploy(deploymentId);
assertThat(deploymentId, eventually(hasStatusThat(
Matchers.hasProperty("state", is(unknown))), timeout.maxAttempts, timeout.pause));
}

@Test
public void testCreateAdditionalContainersOverride() {
log.info("Testing {}...", "CreateAdditionalContainersOverride");
KubernetesDeployerProperties.Container container1 = new KubernetesDeployerProperties.Container();
container1.setName("c1");
container1.setImage("busybox:1.31.0");
container1.setCommand(Arrays.asList("sh", "-c", "echo hello-from-original-properties"));
KubernetesDeployerProperties.Container container2 = new KubernetesDeployerProperties.Container();
container2.setName("container2");
container2.setImage("busybox:1.31.0");
container2.setCommand(Arrays.asList("sh", "-c", "echo hello-from-original-properties"));
KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties();
kubernetesDeployerProperties.setAdditionalContainers(Arrays.asList(container1, container2));
KubernetesAppDeployer kubernetesAppDeployer = new KubernetesAppDeployer(kubernetesDeployerProperties,
kubernetesClient);

Map<String, String> props = Stream.of(new String[][]{
{
"spring.cloud.deployer.kubernetes.volumes",
"[{name: 'test-volume', emptyDir: {}}]",
},
{
"spring.cloud.deployer.kubernetes.volumeMounts",
"[{name: 'test-volume', mountPath: '/tmp'}]",
},
{
"spring.cloud.deployer.kubernetes.additional-containers",
"[{name: 'c1', image: 'busybox:latest', command: ['sh', '-c', 'echo hello1'], volumeMounts: [{name: 'test-volume', mountPath: '/tmp', readOnly: true}]},"
+ "{name: 'c2', image: 'busybox:1.26.1', command: ['sh', '-c', 'echo hello2']}]"
}}).collect(Collectors.toMap(data -> data[0], data -> data[1]));

AppDefinition definition = new AppDefinition(randomName(), null);
AppDeploymentRequest request = new AppDeploymentRequest(definition, testApplication(), props);

log.info("Deploying {}...", request.getDefinition().getName());
String deploymentId = kubernetesAppDeployer.deploy(request);
Timeout timeout = deploymentTimeout();
assertThat(deploymentId, eventually(hasStatusThat(
Matchers.hasProperty("state", is(deployed))), timeout.maxAttempts, timeout.pause));

Deployment deployment = kubernetesClient.apps().deployments().withName(request.getDefinition().getName()).get();
List<Container> containers = deployment.getSpec().getTemplate().getSpec().getContainers();

assertTrue("Number of containers is incorrect", containers.size() == 4);

// c1 from the deployment properties should have overridden the c1 from the original deployer properties
Optional<Container> additionalContainer1 = containers.stream().filter(i -> i.getName().equals("c1")).findFirst();
assertTrue("Additional container c1 not found", additionalContainer1.isPresent());

Container testAdditionalContainer1 = additionalContainer1.get();

assertEquals("Unexpected additional container name", testAdditionalContainer1.getName(), "c1");
assertEquals("Unexpected additional container image", testAdditionalContainer1.getImage(), "busybox:latest");

List<String> commands = testAdditionalContainer1.getCommand();

assertTrue("Additional container commands missing", commands != null && !commands.isEmpty());
assertEquals("Invalid number of additional container commands", 3, commands.size());
assertEquals("sh", commands.get(0));
assertEquals("-c", commands.get(1));
assertEquals("echo hello1", commands.get(2));

List<VolumeMount> volumeMounts = testAdditionalContainer1.getVolumeMounts();

assertTrue("Volume mount size is incorrect", volumeMounts.size() == 1);
assertEquals("test-volume", volumeMounts.get(0).getName());
assertEquals("/tmp", volumeMounts.get(0).getMountPath());
assertEquals(Boolean.TRUE, volumeMounts.get(0).getReadOnly());

Optional<Container> additionalContainer2 = containers.stream().filter(i -> i.getName().equals("c2")).findFirst();
assertTrue("Additional container c2 not found", additionalContainer2.isPresent());

Container testAdditionalContainer2 = additionalContainer2.get();

assertEquals("Unexpected additional container name", testAdditionalContainer2.getName(), "c2");
assertEquals("Unexpected additional container image", testAdditionalContainer2.getImage(), "busybox:1.26.1");

List<String> container2Commands = testAdditionalContainer2.getCommand();

assertTrue("Additional container commands missing", container2Commands != null && !container2Commands.isEmpty());
assertEquals("Invalid number of additional container commands", 3, container2Commands.size());
assertEquals("sh", container2Commands.get(0));
assertEquals("-c", container2Commands.get(1));
assertEquals("echo hello2", container2Commands.get(2));

// Verifying the additional container passed from the root deployer properties
Optional<Container> additionalContainer3 = containers.stream().filter(i -> i.getName().equals("container2")).findFirst();
assertTrue("Additional container c2 not found", additionalContainer3.isPresent());

Container testAdditionalContainer3 = additionalContainer3.get();

assertEquals("Unexpected additional container name", testAdditionalContainer3.getName(), "container2");
assertEquals("Unexpected additional container image", testAdditionalContainer3.getImage(), "busybox:1.31.0");

List<String> container3Commands = testAdditionalContainer3.getCommand();

assertTrue("Additional container commands missing", container3Commands != null && !container3Commands.isEmpty());
assertEquals("Invalid number of additional container commands", 3, container3Commands.size());
assertEquals("sh", container3Commands.get(0));
assertEquals("-c", container3Commands.get(1));
assertEquals("echo hello-from-original-properties", container3Commands.get(2));

log.info("Undeploying {}...", deploymentId);
timeout = undeploymentTimeout();
kubernetesAppDeployer.undeploy(deploymentId);
assertThat(deploymentId, eventually(hasStatusThat(
Matchers.hasProperty("state", is(unknown))), timeout.maxAttempts, timeout.pause));
}

@Test
public void testUnknownStatusOnPendingResources() throws InterruptedException {
log.info("Testing {}...", "UnknownStatusOnPendingResources");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2016-2020 the original author or authors.
* Copyright 2016-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -20,9 +20,11 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.UUID;

import io.fabric8.kubernetes.api.model.Container;
import io.fabric8.kubernetes.api.model.Pod;
import io.fabric8.kubernetes.client.KubernetesClient;
import org.hamcrest.Matchers;
Expand All @@ -44,6 +46,7 @@
import org.springframework.core.io.Resource;

import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.springframework.cloud.deployer.spi.test.EventuallyMatcher.eventually;
Expand Down Expand Up @@ -196,4 +199,62 @@ public void testDeploymentLabels() {
Matchers.<TaskStatus>hasProperty("state", Matchers.is(LaunchState.unknown))), timeout.maxAttempts,
timeout.pause));
}

@Test
public void testTaskAdditionalContainer() {
log.info("Testing {}...", "TaskAdditionalContainer");

KubernetesTaskLauncher kubernetesTaskLauncher = new KubernetesTaskLauncher(new KubernetesDeployerProperties(),
new KubernetesTaskLauncherProperties(), kubernetesClient);

AppDefinition definition = new AppDefinition(randomName(), null);
Resource resource = testApplication();
Map<String, String> props = Collections.singletonMap("spring.cloud.deployer.kubernetes.additionalContainers",
"[{name: 'test', image: 'busybox:latest', command: ['sh', '-c', 'echo hello']}]");
AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, props);

log.info("Launching {}...", request.getDefinition().getName());

String launchId = kubernetesTaskLauncher.launch(request);
Timeout timeout = deploymentTimeout();

assertThat(launchId, eventually(hasStatusThat(
Matchers.<TaskStatus>hasProperty("state", Matchers.is(LaunchState.running))), timeout.maxAttempts,
timeout.pause));

String taskName = request.getDefinition().getName();

List<Pod> pods = kubernetesClient.pods().withLabel("task-name", taskName).list().getItems();

assertThat(pods.size(), is(1));

Pod pod = pods.get(0);

assertTrue(pod.getSpec().getContainers().size() == 2);

Optional<Container> additionalContainer = pod.getSpec().getContainers().stream().filter(i -> i.getName().equals("test")).findFirst();
assertTrue("Additional container not found", additionalContainer.isPresent());

Container testAdditionalContainer = additionalContainer.get();

assertEquals("Unexpected additional container name", testAdditionalContainer.getName(), "test");
assertEquals("Unexpected additional container image", testAdditionalContainer.getImage(), "busybox:latest");

List<String> commands = testAdditionalContainer.getCommand();

assertTrue("Additional container commands missing", commands != null && !commands.isEmpty());
assertEquals("Invalid number of additional container commands", 3, commands.size());
assertEquals("sh", commands.get(0));
assertEquals("-c", commands.get(1));
assertEquals("echo hello", commands.get(2));

log.info("Destroying {}...", taskName);

timeout = undeploymentTimeout();
kubernetesTaskLauncher.destroy(taskName);

assertThat(taskName, eventually(hasStatusThat(
Matchers.<TaskStatus>hasProperty("state", Matchers.is(LaunchState.unknown))), timeout.maxAttempts,
timeout.pause));
}
}