Skip to content

Commit

Permalink
Document how to map properties using Quarkus K8s Config extension
Browse files Browse the repository at this point in the history
fix #261
  • Loading branch information
Sgitario committed Jun 22, 2023
1 parent cbb27b8 commit 925b9bd
Show file tree
Hide file tree
Showing 12 changed files with 239 additions and 37 deletions.
34 changes: 34 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,40 @@ jobs:
- name: Delete namespace "helmdeps2"
run: kubectl delete namespace helmdeps2

- name: Verify Integration Test With Config
run: |
K8S_NAMESPACE=helmconfig
KIND_REGISTRY_GROUP=local
VERSION=latest
NAME=quarkus-helm-integration-tests-kubernetes-config
kubectl create namespace $K8S_NAMESPACE
# create image plus push to a Helm repository
cd integration-tests/helm-kubernetes-config
mvn clean package -DskipTests -Dquarkus.container-image.build=true \
-Dquarkus.container-image.push=true \
-Dquarkus.container-image.registry=$KIND_REGISTRY \
-Dquarkus.container-image.group=$KIND_REGISTRY_GROUP \
-Dquarkus.container-image.tag=$VERSION \
-Dquarkus.container-image.insecure=true
# And install application
helm install quarkus-with-config target/helm/kubernetes/quarkus-helm-integration-tests-kubernetes-config -n $K8S_NAMESPACE --set app.image=$KIND_REGISTRY/$KIND_REGISTRY_GROUP/$NAME:$VERSION --set app.number=3 --set app.flag=true --set app.message="Hello %s from helm"
# Wait for the app to start
kubectl wait --for=condition=ready pod -l app.kubernetes.io/name=$NAME -n $K8S_NAMESPACE
# Verify application
# POD_NAME=$(kubectl get pod -l app.kubernetes.io/name=$NAME -n $K8S_NAMESPACE -o name)
# RESULT=$(kubectl exec -n $K8S_NAMESPACE $POD_NAME -- wget -qO- http://localhost:8080/)
# if [[ "$RESULT" = *"Hello World from helm, number=3, flag=true"* ]]
# then
# exit 0
# fi
# echo "Application is not working. Result was: $RESULT"
# exit 1
- name: Delete namespace "helmconfig"
run: kubectl delete namespace helmconfig

- name: Verify Super Hero microservice from Super Hero workshop using Helm Repository
run: |
K8S_NAMESPACE=super-heroes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,17 @@
import static io.quarkiverse.helm.deployment.utils.MapUtils.toMultiValueUnsortedMap;
import static io.quarkiverse.helm.deployment.utils.MapUtils.toPlainMap;
import static io.quarkiverse.helm.deployment.utils.ValuesSchemaUtils.createSchema;
import static io.quarkiverse.helm.deployment.utils.YamlExpressionParserUtils.EMPTY;
import static io.quarkiverse.helm.deployment.utils.YamlExpressionParserUtils.END_EXPRESSION_TOKEN;
import static io.quarkiverse.helm.deployment.utils.YamlExpressionParserUtils.END_TAG;
import static io.quarkiverse.helm.deployment.utils.YamlExpressionParserUtils.SEPARATOR_QUOTES;
import static io.quarkiverse.helm.deployment.utils.YamlExpressionParserUtils.SEPARATOR_TOKEN;
import static io.quarkiverse.helm.deployment.utils.YamlExpressionParserUtils.START_EXPRESSION_TOKEN;
import static io.quarkiverse.helm.deployment.utils.YamlExpressionParserUtils.START_TAG;
import static io.quarkiverse.helm.deployment.utils.YamlExpressionParserUtils.read;
import static io.quarkiverse.helm.deployment.utils.YamlExpressionParserUtils.readAndSet;
import static io.quarkiverse.helm.deployment.utils.YamlExpressionParserUtils.set;
import static io.quarkiverse.helm.deployment.utils.YamlExpressionParserUtils.toExpression;

import java.io.ByteArrayOutputStream;
import java.io.File;
Expand Down Expand Up @@ -92,11 +96,6 @@ public class QuarkusHelmWriterSessionListener {
private static final String KIND = "kind";
private static final String METADATA = "metadata";
private static final String NAME = "name";
private static final String START_TAG = "{{";
private static final String END_TAG = "}}";
private static final String VALUES_START_TAG = START_TAG + " .Values.";
private static final String VALUES_END_TAG = " " + END_TAG;
private static final String EMPTY = "";
private static final String ENVIRONMENT_PROPERTY_GROUP = "envs.";
private static final String IF_STATEMENT_START_TAG = "{{- if .Values.%s }}";
private static final String TEMPLATE_FUNCTION_START_TAG = "{{- define";
Expand Down Expand Up @@ -548,12 +547,8 @@ private List<Map<Object, Object>> populateValuesFromConfigReferences(io.dekorate
if (!valueIsEnvironmentProperty(valueReference)) {
String valueReferenceProperty = deductProperty(helmConfig, valueReference.getProperty());

String expression = Optional.ofNullable(valueReference.getExpression())
.filter(Strings::isNotNullOrEmpty)
.orElse(VALUES_START_TAG + valueReferenceProperty + VALUES_END_TAG);

processValueReference(valueReferenceProperty, valueReference.getValue(), expression,
valueReference, values, parser, seen, paths);
processValueReference(valueReferenceProperty, valueReference.getValue(), valueReference, values, parser,
seen, paths);
}
}

Expand All @@ -575,15 +570,8 @@ private List<Map<Object, Object>> populateValuesFromConfigReferences(io.dekorate
}
}

String conversion = EMPTY;
if (valueReferenceValue != null && !(valueReferenceValue instanceof String)) {
conversion = " | quote";
}

String expression = VALUES_START_TAG + valueReferenceProperty + conversion + VALUES_END_TAG;

processValueReference(valueReferenceProperty, valueReferenceValue, expression, valueReference, values,
parser, seen, paths);
processValueReference(valueReferenceProperty, valueReferenceValue, valueReference, values, parser, seen,
paths);
}
}

Expand All @@ -607,8 +595,7 @@ private String getEnvironmentPropertyName(ConfigReference valueReference) {
return property;
}

private void processValueReference(String property, Object value, String expression,
ConfigReference valueReference, ValuesHolder values,
private void processValueReference(String property, Object value, ConfigReference valueReference, ValuesHolder values,
YamlExpressionParser parser, Map<String, Object> seen, Set<String> paths) {

String profile = valueReference.getProfile();
Expand All @@ -629,7 +616,7 @@ private void processValueReference(String property, Object value, String express
Object actualValue = Optional.ofNullable(value).orElse(found);

if (actualValue != null) {
set(parser, path, expression);
set(parser, path, toExpression(property, value, found, valueReference));
values.putIfAbsent(property, valueReference, actualValue, profile);
if (isNullOrEmpty(profile)) {
seen.putIfAbsent(property, actualValue);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package io.quarkiverse.helm.deployment.utils;

import java.util.Optional;
import java.util.Set;
import java.util.regex.Pattern;

import io.dekorate.ConfigReference;
import io.dekorate.utils.Strings;
import io.github.yamlpath.YamlExpressionParser;

public final class YamlExpressionParserUtils {
Expand All @@ -11,6 +14,11 @@ public final class YamlExpressionParserUtils {
public static final String SEPARATOR_QUOTES = ":DOUBLE_QUOTES";
public static final String START_EXPRESSION_TOKEN = ":START:";
public static final String END_EXPRESSION_TOKEN = ":END:";
public static final String START_TAG = "{{";
public static final String END_TAG = "}}";
public static final String EMPTY = "";
public static final String VALUES_START_TAG = START_TAG + " .Values.";
public static final String VALUES_END_TAG = " " + END_TAG;

private YamlExpressionParserUtils() {

Expand All @@ -30,6 +38,24 @@ public static Object readAndSet(YamlExpressionParser parser, String path, String
return found.stream().findFirst().orElse(null);
}

public static String toExpression(String property, Object provided, Object found, ConfigReference valueReference) {
Optional<String> expressionProvided = Optional.ofNullable(valueReference.getExpression())
.filter(Strings::isNotNullOrEmpty);

if (expressionProvided.isPresent()) {
return expressionProvided.get();
}

String conversion = EMPTY;
// we only need to quote when the found value in the generated resources is a string, but the provided type isn't.
if (provided != null && !(provided instanceof String) && found instanceof String) {
// we need conversion
conversion = " | quote";
}

return VALUES_START_TAG + property + conversion + VALUES_END_TAG;
}

private static String adaptExpression(String expression) {
return START_EXPRESSION_TOKEN +
expression.replaceAll(Pattern.quote(System.lineSeparator()), SEPARATOR_TOKEN)
Expand Down
1 change: 1 addition & 0 deletions docs/modules/ROOT/nav.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@
** xref:index.adoc#conditionally-enable-disable-resources[Conditionally enable/disable resources]
* xref:example-crud.adoc[Example: How to generate the Helm Chart of a REST CRUD Quarkus application]
* xref:example-argocd.adoc[Example: How to configure a Continuous Delivery (CD) workflow using Quarkus Helm and ArgoCD]
* xref:example-config.adoc[Example: How to use the Quarkus Kubernetes Config extension to map properties using Quarkus Helm]
* xref:includes/quarkus-helm.adoc[Configuration Reference]
140 changes: 140 additions & 0 deletions docs/modules/ROOT/pages/example-config.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
[[example-config]]
= Example: How to map Integer, String, and Boolean types

include::./includes/attributes.adoc[]

In this example, we're going to create a very simple REST CRUD application in Quarkus and configure Integer, String and boolean properties into our applications.

Sometimes, this is a challenging use case because most of the Kubernetes resources and System properties only support String/text properties.

== Prerequisites

* Maven 3.8+
* Java 17+
* Have logged into a Kubernetes cluster
* Have installed the Helm command line

== Create application

Our application will print a hello message with the string, integer, and boolean config properties. We'll only need the following extensions:

* https://quarkus.io/guides/resteasy-reactive[RESTEASY Reactive]: REST support
* https://quarkus.io/guides/kubernetes-config[Kubernetes Config Extension]: Use a config map to inject properties into the application.

Let's create our application from scratch:

[source,bash,subs=attributes+]
----
mvn io.quarkus.platform:quarkus-maven-plugin:{quarkus-version}:create \
-DprojectGroupId=org.acme \
-DprojectArtifactId=example \
-Dextensions="resteasy-reactive,kubernetes-config"
cd example
----

The generated application will contain the following Hello resource:

[source,java]
----
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
@Path("/hello")
public class GreetingResource {
@ConfigProperty(name = "hello.message")
String message;
@ConfigProperty(name = "hello.number")
int number;
@ConfigProperty(name = "hello.flag")
boolean flag;
@GET
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
return message + ", number=" + number + ", flag=" + flag;
}
}
----

Next, we need to provide the actual properties via the `application.properties` file:

[source,properties]
----
hello.message=Hello from application.properties
hello.number=1
hello.flag=true
----

Now, if we run our application and call our service via `http://localhost:8080/hello`, it will return `Hello from application.properties, number=1, flag=true`.

How can we configure Quarkus Helm to map the hello properties and update them when installing the generated Helm chart?

One way is using System properties, however this only supports String. If we map the above properties using system properties as described in xref:index.adoc#mapping-system-properties[the Mapping System Properties section]:

[source,properties]
----
hello.message=${helloMessage}
hello.number=${helloNumber}
hello.flag=${helloFlag}
----

When installing the Helm chart, this would fail because the `hello.number` and `hello.flag` properties are Strings. This is because internally Quarkus Helm is mapping the `helloMessage` system property as container environment properties which only supports String types.

Then, how can we do it? Quarkus Kubernetes Config to the rescue!

Let's add a ConfigMap resource in `src/main/kubernetes/kubernetes.yml`:

[source,yaml]
----
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
hello.message: Hello from configmap
hello.number: 2
hello.flag: false
----

The Quarkus Kubernetes extension will incorporate this ConfigMap resource in the generated resources at `target/kubernetes/kubernetes.yml`.

Next, we need to configure Quarkus to read this ConfigMap resource when starting the application in Kubernetes:

[source,properties]
----
quarkus.kubernetes-config.enabled=true
quarkus.kubernetes-config.config-maps=app-config
----

As it is, if we build our application and install the Helm chart in Kubernetes, when calling the Hello endpoint, it would return: `Hello from configmap, number=2, flag=false`.

So good so far! Next, we can map the ConfigMap values into the Helm Chart values, so we can update these properties when installing the Helm chart. We can do this by adding the following properties:

[source,properties]
----
# Map values in ConfigMap
quarkus.helm.values.message.paths=(kind == ConfigMap).data.'hello.message'
quarkus.helm.values.number.paths=(kind == ConfigMap).data.'hello.number'
quarkus.helm.values.number.value-as-int=1 <1>
quarkus.helm.values.flag.paths=(kind == ConfigMap).data.'hello.flag'
quarkus.helm.values.flag.value-as-bool=true <2>
----

<1> Since the default value type for ConfigMap is still a string, we need to let the Quarkus Helm extension that the actual type is an integer using the `value-as-int` property.
<2> And the same with the flag property. We need to instruct the Quarkus Helm extension that the actual type for the flag field is a boolean using `value-as-bool`.

Finally, after building our application, we can install the Helm chart and configure the Hello properties with the right types. For example, installing the chart as follows:

[source,bash]
----
helm install quarkus target/helm/kubernetes/example --set app.number=3 --set app.flag=true --set app.message="Hello from helm"
----

And calling the service again, it would return `Hello from helm, number=3, flag=true`.

Happy coding!
2 changes: 1 addition & 1 deletion docs/modules/ROOT/pages/example-crud.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public class GreetingResource {
@GET
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
return "Hello from RESTEasy Reactiv aaaaae";
return "Hello from RESTEasy Reactive";
}
}
----
Expand Down
2 changes: 1 addition & 1 deletion docs/modules/ROOT/pages/includes/attributes.adoc
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
:quarkus-version: 3.1.1.Final
:quarkus-version: 3.1.2.Final
:quarkus-helm-version: 1.0.8
:maven-version: 3.8.1+

Expand Down
6 changes: 6 additions & 0 deletions docs/modules/ROOT/pages/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ Apart from all the features supported by the Quarkus Helm extension, in this doc
- xref:index.adoc#cli[Using CLI]
- xref:example-crud.adoc[Example: How to generate the Helm Chart of a REST CRUD Quarkus application]
- xref:example-argocd.adoc[Example: How to configure a Continuous Delivery (CD) workflow using Quarkus Helm and ArgoCD]
- xref:example-config.adoc[Example: How to use the Quarkus Kubernetes Config extension to map properties using Quarkus Helm]

[[usage-helm-kubernetes]]
== Using the Helm Extension in Kubernetes
Expand Down Expand Up @@ -449,6 +450,11 @@ helm install helm-example ./target/helm/kubernetes/<chart name> --set app.greeti
[[mapping-system-properties]]
=== Mapping System Properties

[IMPORTANT]
====
Mapping system properties only works with string/text properties. For mapping other types, you would need to use the Kubernetes Config extension. See example xref:example-config.adoc[here].
====

It's a very common use case to expose some properties to be configurable when installing the Helm chart. For example, the data source JDBC url:

[source,properties]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ ARG RUN_JAVA_VERSION=1.3.8
ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en'
# Install java and the run-java script
# Also set up permissions for user `1001`
RUN microdnf install curl ca-certificates ${JAVA_PACKAGE} \
RUN microdnf install curl wget ca-certificates ${JAVA_PACKAGE} \
&& microdnf update \
&& microdnf clean all \
&& mkdir /deployments \
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package io.quarkiverse.helm.tests.kubernetes;

import java.util.Optional;

import jakarta.ws.rs.DefaultValue;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
Expand All @@ -14,14 +12,16 @@
public class Endpoint {

@ConfigProperty(name = "hello.message")
Optional<String> message;
String message;

@ConfigProperty(name = "hello.number")
int number;

@ConfigProperty(name = "hello.flag")
boolean flag;

@GET
public Response get(@QueryParam("name") @DefaultValue("World") String name) {
if (message.isPresent()) {
return Response.ok().entity(String.format(message.get(), name)).build();
}

return Response.serverError().entity("ConfigMap not present").build();
return Response.ok().entity(String.format(message, name) + ", number=" + number + ", flag=" + flag).build();
}
}
}
Loading

0 comments on commit 925b9bd

Please sign in to comment.