Skip to content

Commit

Permalink
Make Quarkus Kubernetes gitops friendly
Browse files Browse the repository at this point in the history
[x] Added new property `quarkus.kubernetes.idempotent=true` to avoid producing non idempotent resources.
[x] Added new property `quarkus.kubernetes.output-directory` to select the target directory where to generate the resources.

Fix #26928
Fix #15473
Fix #31296
  • Loading branch information
Sgitario committed Feb 23, 2023
1 parent 46ac546 commit 46c59bc
Show file tree
Hide file tree
Showing 9 changed files with 158 additions and 5 deletions.
42 changes: 42 additions & 0 deletions docs/src/main/asciidoc/deploying-to-kubernetes.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,48 @@ quarkus.container-image.tag=1.0 #optional, defaults to the application ver

The image that will be used in the generated manifests will be `quarkus/demo-app:1.0`

=== Generating idempotent resources

When generating the Kubernetes manifests, Quarkus automatically adds some labels and annotations to give extra information about the generation date or versions. For example:

[source,yaml]
----
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
app.quarkus.io/commit-id: 0f8b87788bc446a9347a7961bea8a60889fe1494
app.quarkus.io/build-timestamp: 2023-02-10 - 13:07:51 +0000
labels:
app.kubernetes.io/managed-by: quarkus
app.kubernetes.io/version: 0.0.1-SNAPSHOT
app.kubernetes.io/name: example
name: example
spec:
...
----

The `app.quarkus.io/commit-id`, `app.quarkus.io/build-timestamp` labels and the `app.kubernetes.io/version` annotation might change every time we re-build the Kubernetes manifests which can be problematic when we want to deploy these resources using a Git-Ops tool (because these tools will detect differences and hence will perform a re-deployment).

To make the generated resources Git-Ops friendly and only produce idempotent resources (resources that won't change every time we build the sources), we need to add the following property:

[source,properties]
----
quarkus.kubernetes.idempotent=true
----

Moreover, by default the directory where the generated resources are created is `target/kubernetes`, to change it, we need to use:

[source,properties]
----
quarkus.kubernetes.output-directory=target/kubernetes-with-idempotent
----

[NOTE]
====
Note that the property `quarkus.kubernetes.output-directory` is relative to the current project location.
====

=== Changing the generated deployment resource

Besides generating a `Deployment` resource, you can also choose to generate either a `StatefulSet`, or a `Job`, or a `CronJob` resource instead via `application.properties`:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,13 @@ public class KnativeConfig implements PlatformConfiguration {
@ConfigItem(defaultValue = "true")
boolean addNameToLabelSelectors;

/**
* Switch used to control whether non-idempotent fields are included in generated kubernetes resources to improve
* git-ops compatibility
*/
@ConfigItem(defaultValue = "false")
boolean idempotent;

public Optional<String> getPartOf() {
return partOf;
}
Expand Down Expand Up @@ -493,4 +500,9 @@ public Optional<String> getAppConfigMap() {
public SecurityContextConfig getSecurityContext() {
return securityContext;
}

@Override
public boolean isIdempotent() {
return idempotent;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
import io.dekorate.kubernetes.decorator.RemoveAnnotationDecorator;
import io.dekorate.kubernetes.decorator.RemoveFromMatchingLabelsDecorator;
import io.dekorate.kubernetes.decorator.RemoveFromSelectorDecorator;
import io.dekorate.kubernetes.decorator.RemoveLabelDecorator;
import io.dekorate.project.BuildInfo;
import io.dekorate.project.FileProjectFactory;
import io.dekorate.project.Project;
Expand Down Expand Up @@ -239,12 +240,17 @@ private static Collection<DecoratorBuildItem> createLabelDecorators(Optional<Pro
new AddLabelDecorator(name, l.getKey(), l.getValue())));
});

if (!config.isAddVersionToLabelSelectors()) {
if (!config.isAddVersionToLabelSelectors() || config.isIdempotent()) {
result.add(new DecoratorBuildItem(target, new RemoveFromSelectorDecorator(name, Labels.VERSION)));
result.add(new DecoratorBuildItem(target, new RemoveFromMatchingLabelsDecorator(name, Labels.VERSION)));
}

if (config.isIdempotent()) {
result.add(new DecoratorBuildItem(target, new RemoveLabelDecorator(name, Labels.VERSION)));
}

if (!config.isAddNameToLabelSelectors()) {
result.add(new DecoratorBuildItem(target, new RemoveLabelDecorator(name, Labels.NAME)));
result.add(new DecoratorBuildItem(target, new RemoveFromSelectorDecorator(name, Labels.NAME)));
result.add(new DecoratorBuildItem(target, new RemoveFromMatchingLabelsDecorator(name, Labels.NAME)));
}
Expand Down Expand Up @@ -642,12 +648,12 @@ private static List<DecoratorBuildItem> createAnnotationDecorators(Optional<Proj
String vcsUrl = scm != null ? scm.getRemote().get("origin") : null;
String commitId = scm != null ? scm.getCommit() : null;

//Dekorate uses its own annotations. Let's replace them with the quarkus ones.
// Dekorate uses its own annotations. Let's replace them with the quarkus ones.
result.add(new DecoratorBuildItem(target, new RemoveAnnotationDecorator(Annotations.VCS_URL)));
result.add(new DecoratorBuildItem(target, new RemoveAnnotationDecorator(Annotations.COMMIT_ID)));

//Add quarkus vcs annotations
if (commitId != null) {
if (commitId != null && !config.isIdempotent()) {
result.add(new DecoratorBuildItem(target, new AddAnnotationDecorator(name,
new Annotation(QUARKUS_ANNOTATIONS_COMMIT_ID, commitId, new String[0]))));
}
Expand All @@ -659,7 +665,7 @@ private static List<DecoratorBuildItem> createAnnotationDecorators(Optional<Proj

});

if (config.isAddBuildTimestamp()) {
if (config.isAddBuildTimestamp() && !config.isIdempotent()) {
result.add(new DecoratorBuildItem(target,
new AddAnnotationDecorator(name, new Annotation(QUARKUS_ANNOTATIONS_BUILD_TIMESTAMP,
now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd - HH:mm:ss Z")), new String[0]))));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,19 @@ public enum DeploymentResourceKind {
@ConfigItem(defaultValue = "true")
boolean externalizeInit;

/**
* Switch used to control whether non-idempotent fields are included in generated kubernetes resources to improve
* git-ops compatibility
*/
@ConfigItem(defaultValue = "false")
boolean idempotent;

/**
* Optionally set directory generated kubernetes resources will be written to. Default is `target/kubernetes`.
*/
@ConfigItem
Optional<String> outputDirectory;

public Optional<String> getPartOf() {
return partOf;
}
Expand Down Expand Up @@ -536,6 +549,11 @@ public SecurityContextConfig getSecurityContext() {
return securityContext;
}

@Override
public boolean isIdempotent() {
return idempotent;
}

public KubernetesConfig.DeploymentResourceKind getDeploymentResourceKind(Capabilities capabilities) {
if (deploymentKind.isPresent()) {
return deploymentKind.get();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,9 @@ public void build(ApplicationInfoBuildItem applicationInfo,
}
});

Path targetDirectory = kubernetesConfig.outputDirectory.map(d -> Paths.get("").toAbsolutePath().resolve(d))
.orElse(outputTarget.getOutputDirectory().resolve(KUBERNETES));

// write the generated resources to the filesystem
generatedResourcesMap = session.close();
List<String> generatedFiles = new ArrayList<>(generatedResourcesMap.size());
Expand All @@ -198,7 +201,7 @@ public void build(ApplicationInfoBuildItem applicationInfo,
continue;
}
String fileName = path.toFile().getName();
Path targetPath = outputTarget.getOutputDirectory().resolve(KUBERNETES).resolve(fileName);
Path targetPath = targetDirectory.resolve(fileName);
String relativePath = targetPath.toAbsolutePath().toString().replace(root.toAbsolutePath().toString(), "");

generatedKubernetesResourceProducer.produce(new GeneratedKubernetesResourceBuildItem(fileName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,13 @@ public EnvVarsConfig getEnv() {
@ConfigItem(defaultValue = "true")
boolean externalizeInit;

/**
* Switch used to control whether non-idempotent fields are included in generated kubernetes resources to improve
* git-ops compatibility
*/
@ConfigItem(defaultValue = "false")
boolean idempotent;

public Optional<String> getAppSecret() {
return this.appSecret;
}
Expand All @@ -573,6 +580,11 @@ public SecurityContextConfig getSecurityContext() {
return securityContext;
}

@Override
public boolean isIdempotent() {
return idempotent;
}

public static boolean isOpenshiftBuildEnabled(ContainerImageConfig containerImageConfig, Capabilities capabilities) {
boolean implicitlyEnabled = ContainerImageCapabilitiesUtil.getActiveContainerImageCapability(capabilities)
.filter(c -> c.contains(OPENSHIFT) || c.contains(S2I)).isPresent();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,5 @@ default String getConfigName() {

SecurityContextConfig getSecurityContext();

boolean isIdempotent();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@

package io.quarkus.it.kubernetes;

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

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

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.dekorate.utils.Labels;
import io.fabric8.kubernetes.api.model.HasMetadata;
import io.fabric8.kubernetes.api.model.apps.Deployment;
import io.quarkus.builder.Version;
import io.quarkus.maven.dependency.Dependency;
import io.quarkus.test.QuarkusProdModeTest;

public class KubernetesWithIdempotentTest {

private static final String APP_NAME = "kubernetes-with-idempotent";

@RegisterExtension
static final QuarkusProdModeTest config = new QuarkusProdModeTest()
.withApplicationRoot((jar) -> jar.addClasses(GreetingResource.class))
.setApplicationName(APP_NAME)
.setApplicationVersion("0.1-SNAPSHOT")
.withConfigurationResource(APP_NAME + ".properties")
.setLogFileName("k8s.log")
.setForcedDependencies(List.of(Dependency.of("io.quarkus", "quarkus-kubernetes", Version.getVersion())));

@Test
public void assertGeneratedResources() throws IOException {
final Path kubernetesDir = Paths.get("target").resolve(APP_NAME);
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).allSatisfy(resource -> {
assertThat(resource.getMetadata()).satisfies(m -> {
assertThat(m.getName()).isEqualTo(APP_NAME);
assertThat(m.getAnnotations().get("app.quarkus.io/commit-id")).isNull();
assertThat(m.getAnnotations().get("app.quarkus.io/build-timestamp")).isNull();
});

if (resource instanceof Deployment) {
Deployment deployment = (Deployment) resource;
assertThat(deployment.getSpec().getSelector().getMatchLabels()).doesNotContainKey(Labels.VERSION);
assertThat(deployment.getSpec().getTemplate().getMetadata().getLabels()).doesNotContainKey(Labels.VERSION);
}
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
quarkus.kubernetes.idempotent=true
quarkus.kubernetes.output-directory=target/kubernetes-with-idempotent

0 comments on commit 46c59bc

Please sign in to comment.