Skip to content

Commit

Permalink
Kubernetes: Merge StatefulSet resources from input if set
Browse files Browse the repository at this point in the history
When users add a StatefulSet resource and the deployment-target is set to `StatefulSet`, we need to read the StatefulSet resource from the user and populate it accordingly.

Before these changes, we were including the StatefulSet resource, but adding a new one (with the same name), so in the end, two StatefulSet resources were included in the final kubernetes.yml.

Fix quarkusio#25162
  • Loading branch information
Sgitario committed Apr 27, 2022
1 parent fc1c550 commit dbf2cce
Show file tree
Hide file tree
Showing 3 changed files with 161 additions and 21 deletions.
Original file line number Diff line number Diff line change
@@ -1,43 +1,96 @@

package io.quarkus.kubernetes.deployment;

import static io.quarkus.kubernetes.deployment.Constants.STATEFULSET;

import java.util.HashMap;
import java.util.List;
import java.util.function.Function;

import io.dekorate.kubernetes.decorator.ResourceProvidingDecorator;
import io.dekorate.utils.Strings;
import io.fabric8.kubernetes.api.model.Container;
import io.fabric8.kubernetes.api.model.HasMetadata;
import io.fabric8.kubernetes.api.model.KubernetesListFluent;
import io.fabric8.kubernetes.api.model.apps.StatefulSet;
import io.fabric8.kubernetes.api.model.apps.StatefulSetBuilder;
import io.fabric8.kubernetes.api.model.apps.StatefulSetFluent;

public class AddStatefulSetResourceDecorator extends ResourceProvidingDecorator<KubernetesListFluent<?>> {

private final String name;
private final PlatformConfiguration config;

public AddStatefulSetResourceDecorator(String name, PlatformConfiguration config) {
this.name = name;
this.config = config;
}

@SuppressWarnings("deprecation")
@Override
public void visit(KubernetesListFluent<?> list) {
list.addToItems(new StatefulSetBuilder()
.withNewMetadata()
.withName(name)
.endMetadata()
.withNewSpec()
.withReplicas(1)
.withServiceName(name)
.withNewSelector()
.withMatchLabels(new HashMap<String, String>())
StatefulSetBuilder builder = list.getItems().stream()
.filter(this::containsStatefulSetResource)
.map(replaceExistingStatefulSetResource(list))
.findAny()
.orElseGet(this::createStatefulSetResource)
.accept(StatefulSetBuilder.class, this::initStatefulSetResourceWithDefaults);

list.addToItems(builder.build());
}

private boolean containsStatefulSetResource(HasMetadata metadata) {
return STATEFULSET.equalsIgnoreCase(metadata.getKind()) && name.equals(metadata.getMetadata().getName());
}

private void initStatefulSetResourceWithDefaults(StatefulSetBuilder builder) {
StatefulSetFluent.SpecNested<StatefulSetBuilder> spec = builder.editOrNewSpec();

spec.editOrNewSelector()
.endSelector()
.withNewTemplate()
.withNewSpec()
.withTerminationGracePeriodSeconds(10L)
.addNewContainer()
.withName(name)
.endContainer()
.endSpec()
.endTemplate()
.editOrNewTemplate()
.editOrNewSpec()
.endSpec()
.build());
.endTemplate();

// defaults for:
// - replicas
if (spec.getReplicas() == null) {
spec.withReplicas(1);
}
// - service name
if (Strings.isNullOrEmpty(spec.getServiceName())) {
spec.withServiceName(name);
}
// - match labels
if (spec.getSelector().getMatchLabels() == null) {
spec.editSelector().withMatchLabels(new HashMap<>()).endSelector();
}
// - termination grace period seconds
if (spec.getTemplate().getSpec().getTerminationGracePeriodSeconds() == null) {
spec.editTemplate().editSpec().withTerminationGracePeriodSeconds(10L).endSpec().endTemplate();
}
// - container
if (!containsContainerWithName(spec)) {
spec.editTemplate().editSpec().addNewContainer().withName(name).endContainer().endSpec().endTemplate();
}

spec.endSpec();
}

public AddStatefulSetResourceDecorator(String name, PlatformConfiguration config) {
this.name = name;
this.config = config;
private StatefulSetBuilder createStatefulSetResource() {
return new StatefulSetBuilder().withNewMetadata().withName(name).endMetadata();
}

private Function<HasMetadata, StatefulSetBuilder> replaceExistingStatefulSetResource(KubernetesListFluent<?> list) {
return metadata -> {
list.removeFromItems(metadata);
return new StatefulSetBuilder((StatefulSet) metadata);
};
}

private boolean containsContainerWithName(StatefulSetFluent.SpecNested<StatefulSetBuilder> spec) {
List<Container> containers = spec.getTemplate().getSpec().getContainers();
return containers == null || containers.stream().anyMatch(c -> name.equals(c.getName()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@

package io.quarkus.it.kubernetes;

import static org.assertj.core.api.Assertions.assertThat;

import java.io.IOException;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;

import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.fabric8.kubernetes.api.model.HasMetadata;
import io.fabric8.kubernetes.api.model.apps.StatefulSet;
import io.quarkus.bootstrap.model.AppArtifact;
import io.quarkus.builder.Version;
import io.quarkus.kubernetes.spi.CustomProjectRootBuildItem;
import io.quarkus.test.ProdBuildResults;
import io.quarkus.test.ProdModeTestResults;
import io.quarkus.test.QuarkusProdModeTest;

public class KubernetesWithInputStatefulSetResourcesTest {

static final String APP_NAME = "kubernetes-with-input-statefulset-resource";

@RegisterExtension
static final QuarkusProdModeTest config = new QuarkusProdModeTest()
.withApplicationRoot((jar) -> jar.addClasses(GreetingResource.class))
.setApplicationName(APP_NAME)
.setApplicationVersion("0.1-SNAPSHOT")
.withConfigurationResource("kubernetes-with-statefulset-resource.properties")
.setLogFileName("k8s.log")
.addCustomResourceEntry(Path.of("src", "main", "kubernetes", "kubernetes.yml"),
"manifests/custom-deployment/kubernetes-with-stateful.yml")
.setForcedDependencies(
Collections.singletonList(new AppArtifact("io.quarkus", "quarkus-kubernetes", Version.getVersion())))
.addBuildChainCustomizerEntries(
new QuarkusProdModeTest.BuildChainCustomizerEntry(
KubernetesWithCustomResourcesTest.CustomProjectRootBuildItemProducerProdMode.class,
Collections.singletonList(CustomProjectRootBuildItem.class), Collections.emptyList()));

@ProdBuildResults
private ProdModeTestResults prodModeTestResults;

@Test
public void assertGeneratedResources() throws IOException {
final Path kubernetesDir = prodModeTestResults.getBuildDir().resolve("kubernetes");
assertThat(kubernetesDir)
.isDirectoryContaining(p -> p.getFileName().endsWith("kubernetes.json"))
.isDirectoryContaining(p -> p.getFileName().endsWith("kubernetes.yml"));
List<HasMetadata> kubernetesList = DeserializationUtil
.deserializeAsList(kubernetesDir.resolve("kubernetes.yml"));

assertThat(kubernetesList).filteredOn(i -> i instanceof StatefulSet).singleElement().satisfies(i -> {
assertThat(i).isInstanceOfSatisfying(StatefulSet.class, s -> {
assertThat(s.getMetadata()).satisfies(m -> {
assertThat(m.getName()).isEqualTo(APP_NAME);
});

assertThat(s.getSpec()).satisfies(statefulSetSpec -> {
assertThat(statefulSetSpec.getServiceName()).isEqualTo(APP_NAME);
assertThat(statefulSetSpec.getReplicas()).isEqualTo(42);
assertThat(statefulSetSpec.getTemplate()).satisfies(t -> {
assertThat(t.getSpec()).satisfies(podSpec -> {
assertThat(podSpec.getTerminationGracePeriodSeconds()).isEqualTo(10);
assertThat(podSpec.getContainers()).allMatch(c -> APP_NAME.equals(c.getName()));
});
});
assertThat(statefulSetSpec.getSelector()).satisfies(ls -> {
assertThat(ls.getMatchLabels()).containsEntry("app.kubernetes.io/name", APP_NAME);
assertThat(ls.getMatchLabels()).containsEntry("custom-label", "my-label");
});
});
});
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: kubernetes-with-input-statefulset-resource
spec:
selector:
matchLabels:
custom-label: my-label

0 comments on commit dbf2cce

Please sign in to comment.