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

Allow reading configuration from Kubernetes secrets #10939

Merged
merged 3 commits into from
Jul 30, 2020
Merged
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
2 changes: 1 addition & 1 deletion bom/application/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@
<aws-alexa-sdk.version>2.31.0</aws-alexa-sdk.version>
<azure-functions-java-library.version>1.3.0</azure-functions-java-library.version>
<kotlin.version>1.3.72</kotlin.version>
<dekorate.version>0.12.6</dekorate.version>
<dekorate.version>0.12.7</dekorate.version>
<maven-artifact-transfer.version>0.10.0</maven-artifact-transfer.version>
<jline.version>2.14.6</jline.version>
<maven-invoker.version>3.0.1</maven-invoker.version>
Expand Down
25 changes: 17 additions & 8 deletions docs/src/main/asciidoc/kubernetes-config.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ https://github.com/quarkusio/quarkus/tree/master/docs/src/main/asciidoc
include::./attributes.adoc[]


Quarkus includes the `kubernetes-config` extension which allows developers to use Kubernetes https://cloud.google.com/kubernetes-engine/docs/concepts/configmap[ConfigMaps] as a configuration source, without having to mount the ConfigMaps into the https://kubernetes.io/docs/concepts/workloads/pods/pod/[Pod] running the Quarkus application.
Quarkus includes the `kubernetes-config` extension which allows developers to use Kubernetes https://cloud.google.com/kubernetes-engine/docs/concepts/configmap[ConfigMaps] and https://cloud.google.com/kubernetes-engine/docs/concepts/secret[Secrets] as a configuration source, without having to mount them into the https://kubernetes.io/docs/concepts/workloads/pods/pod/[Pod] running the Quarkus application.


== Configuration
Expand All @@ -33,19 +33,23 @@ This will add the following to your `pom.xml`:

== Usage

The extension works by reading ConfigMaps directly from the Kubernetes API server using the link:kubernetes-client[Kubernetes Client].
The extension works by reading ConfigMaps and Secrets directly from the Kubernetes API server using the link:kubernetes-client[Kubernetes Client].

The extension understands the following types of ConfigMaps as input sources:
The extension understands the following types of ConfigMaps and Secrets as input sources:

* ConfigMaps that contain literal data (see https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/#create-configmaps-from-literal-values[this] for an example on how to create one)
* ConfigMaps created from files named, `application.properties`, `application.yaml` or `application.yml` (see https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/#create-configmaps-from-files[this] for an example on how to create one).
* ConfigMaps and Secrets that contain literal data (see https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/#create-configmaps-from-literal-values[this] for an example on how to create one)
* ConfigMaps and Secrets created from files named `application.properties`, `application.yaml` or `application.yml` (see https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/#create-configmaps-from-files[this] for an example on how to create one).

To configure which ConfigMaps (from the namespace that the application runs in, or the explicitly configured namespace via `quarkus.kubernetes-client.namespace`), you can set the `quarkus.kubernetes-config.config-maps` property (in any of the usual Quarkus ways). If access to ConfigMaps from a specific namespace you can set the
`quarkus.kubernetes-config.namespace` property. Keep in mind however that you will also have to explicitly enable the retrieval of ConfigMaps by setting `quarkus.kubernetes-config.enabled=true` (the default is `false` in order to make it easy to test the application locally).
You have to explicitly enable the retrieval of ConfigMaps and Secrets by setting `quarkus.kubernetes-config.enabled=true`.
The default is `false` in order to make it easy to test the application locally.

Afterwards, set the `quarkus.kubernetes-config.configmaps` property to configure which ConfigMaps should be used.
Set the `quarkus.kubernetes-config.secrets` property to configure which Secrets should be used.
To access ConfigMaps and Secrets from a specific namespace, you can set the `quarkus.kubernetes-config.namespace` property.

=== Priority of obtained properties

The properties obtained from the ConfigMaps have a higher priority (i.e. they override) any properties of the same name that are found in `application.properties` (or the YAML equivalents), but they have lower priority than properties set via Environment Variables or Java System Properties.
The properties obtained from the ConfigMaps and Secrets have a higher priority than (i.e. they override) any properties of the same name that are found in `application.properties` (or the YAML equivalents), but they have lower priority than properties set via Environment Variables or Java System Properties.

=== Kubernetes Permissions

Expand All @@ -54,6 +58,11 @@ that is used to run the application needs to have the proper permissions for suc

Thankfully, when using the `kubernetes-config` extension along with the link:deploying-to-kubernetes[Kubernetes] extension, all the necessary Kubernetes resources to make that happen are automatically generated.

==== Secrets

By default, the link:deploying-to-kubernetes[Kubernetes] extension doesn't generate the necessary resources to allow accessing secrets.
Set `quarkus.kubernetes-config.secrets.enabled=true` to generate the necessary role and corresponding role binding.

== Configuration Reference

include::{generated-dir}/config/quarkus-kubernetes-config-kubernetes-config-source-config.adoc[opts=optional, leveloffset=+1]
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
import io.quarkus.deployment.util.JandexUtil;
import io.quarkus.jackson.deployment.IgnoreJsonDeserializeClassBuildItem;
import io.quarkus.kubernetes.client.runtime.KubernetesClientProducer;
import io.quarkus.kubernetes.spi.KubernetesRoleBuildItem;
import io.quarkus.kubernetes.spi.KubernetesRoleBindingBuildItem;

public class KubernetesClientProcessor {

Expand All @@ -53,15 +53,15 @@ public class KubernetesClientProcessor {
BuildProducer<IgnoreJsonDeserializeClassBuildItem> ignoredJsonDeserializationClasses;

@Inject
BuildProducer<KubernetesRoleBuildItem> roleProducer;
BuildProducer<KubernetesRoleBindingBuildItem> roleBindingProducer;

@BuildStep
public void process(ApplicationIndexBuildItem applicationIndex, CombinedIndexBuildItem combinedIndexBuildItem,
BuildProducer<ExtensionSslNativeSupportBuildItem> sslNativeSupport,
BuildProducer<AdditionalBeanBuildItem> additionalBeanBuildItemBuildItem) {

featureProducer.produce(new FeatureBuildItem(Feature.KUBERNETES_CLIENT));
roleProducer.produce(new KubernetesRoleBuildItem("view"));
roleBindingProducer.produce(new KubernetesRoleBindingBuildItem("view", true));

// make sure the watchers fully (and not weakly) register Kubernetes classes for reflection
final Set<DotName> watchedClasses = new HashSet<>();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package io.quarkus.kubernetes.config.deployment;

import java.util.Arrays;
import java.util.Collections;

import org.jboss.logmanager.Level;

import io.quarkus.deployment.annotations.BuildProducer;
Expand All @@ -9,8 +12,11 @@
import io.quarkus.deployment.builditem.LogCategoryBuildItem;
import io.quarkus.deployment.builditem.RunTimeConfigurationSourceValueBuildItem;
import io.quarkus.kubernetes.client.runtime.KubernetesClientBuildConfig;
import io.quarkus.kubernetes.client.runtime.KubernetesConfigBuildTimeConfig;
import io.quarkus.kubernetes.client.runtime.KubernetesConfigRecorder;
import io.quarkus.kubernetes.client.runtime.KubernetesConfigSourceConfig;
import io.quarkus.kubernetes.spi.KubernetesRoleBindingBuildItem;
import io.quarkus.kubernetes.spi.KubernetesRoleBuildItem;

public class KubernetesConfigProcessor {

Expand All @@ -22,6 +28,25 @@ public RunTimeConfigurationSourceValueBuildItem configure(KubernetesConfigRecord
recorder.configSources(config, clientConfig));
}

@BuildStep
@Record(ExecutionTime.RUNTIME_INIT)
public void handleAccessToSecrets(KubernetesConfigSourceConfig config,
KubernetesConfigBuildTimeConfig buildTimeConfig,
BuildProducer<KubernetesRoleBuildItem> roleProducer,
BuildProducer<KubernetesRoleBindingBuildItem> roleBindingProducer,
KubernetesConfigRecorder recorder) {
if (buildTimeConfig.secretsEnabled) {
roleProducer.produce(new KubernetesRoleBuildItem("view-secrets", Collections.singletonList(
new KubernetesRoleBuildItem.PolicyRule(
Collections.singletonList(""),
Collections.singletonList("secrets"),
Arrays.asList("get", "list", "watch")))));
roleBindingProducer.produce(new KubernetesRoleBindingBuildItem("view-secrets", false));
}

recorder.warnAboutSecrets(config, buildTimeConfig);
}

// done in order to ensure that http logs aren't shown by default which happens because of the interplay between
// not yet setup logging (as the bootstrap config runs before logging is setup) and the configuration
// of the okhttp3.logging.HttpLoggingInterceptor by io.fabric8.kubernetes.client.utils.HttpClientUtils
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ abstract ConfigSource createPropertiesConfigSource(String kubernetesConfigSource
abstract ConfigSource createYamlConfigSource(String kubernetesConfigSourceName, String fileName, String input, int ordinal);

/**
* Returns a list of {@code ConfigSource} for the literal data that is contained in the ConfigMap
* Returns a list of {@code ConfigSource} for the literal data that is contained in the ConfigMap/Secret
* and for the application.{properties|yaml|yml} files that might be contained in it as well
*
* All the {@code ConfigSource} objects use the same ordinal which is higher than the ordinal
Expand Down Expand Up @@ -69,6 +69,9 @@ List<ConfigSource> toConfigSources(String kubernetesConfigSourceName, Map<String
+ " '" + kubernetesConfigSourceName + "'");
}
result.add(createYamlConfigSource(kubernetesConfigSourceName, fileName, rawFileData, ORDINAL));
} else {
// TODO all keys named `*.{properties,yml,yaml}` are categorized as file sources,
// but here, we only look for `application.{properties,yml,yaml}`
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.quarkus.kubernetes.client.runtime;

import io.quarkus.runtime.annotations.ConfigItem;
import io.quarkus.runtime.annotations.ConfigPhase;
import io.quarkus.runtime.annotations.ConfigRoot;

@ConfigRoot(name = "kubernetes-config", phase = ConfigPhase.BUILD_TIME)
public class KubernetesConfigBuildTimeConfig {
/**
* Whether or not configuration can be read from secrets.
* If set to {@code true}, Kubernetes resources allowing access to secrets (role and role binding) will be generated.
*/
@ConfigItem(name = "secrets.enabled", defaultValue = "false")
public boolean secretsEnabled;
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,17 @@ public RuntimeValue<ConfigSourceProvider> configSources(KubernetesConfigSourceCo
KubernetesClientUtils.createClient(clientConfig)));
}

public void warnAboutSecrets(KubernetesConfigSourceConfig config, KubernetesConfigBuildTimeConfig buildTimeConfig) {
if (config.enabled
&& config.secrets.isPresent()
&& !config.secrets.get().isEmpty()
&& !buildTimeConfig.secretsEnabled) {
log.warn("Configuration is read from Secrets " + config.secrets.get()
+ ", but quarkus.kubernetes-config.secrets.enabled is false."
+ " Check if your application's service account has enough permissions to read secrets.");
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it just me, or does the last sentence seem out of place?

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 wanted to say that what the user ultimately needs to do is not setting our config property, but make sure that the app's service account has access to secrets.

Obviously quarkus.kubernetes-config.secrets.enabled is the easiest way, if people use the Kubernetes extension. But that doesn't always have to be the case.

Copy link
Contributor

Choose a reason for hiding this comment

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

Okay

}
}

private RuntimeValue<ConfigSourceProvider> emptyRuntimeValue() {
return new RuntimeValue<>(new EmptyConfigSourceProvider());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,15 @@ public class KubernetesConfigSourceConfig {
public Optional<List<String>> configMaps;

/**
* Namespace to look for config maps. If this is not specified, then the namespace configured in the kubectl config context
* is used. If the value is specified and the namespace doesn't exist, the application will fail to start.
* Secrets to look for in the namespace that the Kubernetes Client has been configured for.
* If you use this, you probably want to enable {@code quarkus.kubernetes-config.secrets.enabled}.
*/
@ConfigItem
public Optional<List<String>> secrets;

/**
* Namespace to look for config maps and secrets. If this is not specified, then the namespace configured in the kubectl
* config context is used. If the value is specified and the namespace doesn't exist, the application will fail to start.
*/
@ConfigItem
public Optional<String> namespace;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import org.jboss.logging.Logger;

import io.fabric8.kubernetes.api.model.ConfigMap;
import io.fabric8.kubernetes.api.model.Secret;
import io.fabric8.kubernetes.client.KubernetesClient;

class KubernetesConfigSourceProvider implements ConfigSourceProvider {
Expand All @@ -19,25 +20,30 @@ class KubernetesConfigSourceProvider implements ConfigSourceProvider {
private final KubernetesClient client;

private final ConfigMapConfigSourceUtil configMapConfigSourceUtil;
private final SecretConfigSourceUtil secretConfigSourceUtil;

public KubernetesConfigSourceProvider(KubernetesConfigSourceConfig config, KubernetesClient client) {
this.config = config;
this.client = client;

this.configMapConfigSourceUtil = new ConfigMapConfigSourceUtil();
this.secretConfigSourceUtil = new SecretConfigSourceUtil();
}

@Override
public Iterable<ConfigSource> getConfigSources(ClassLoader forClassLoader) {
if (!config.configMaps.isPresent()) {
log.debug("No ConfigMaps were configured for config source lookup");
if (!config.configMaps.isPresent() && !config.secrets.isPresent()) {
log.debug("No ConfigMaps or Secrets were configured for config source lookup");
return Collections.emptyList();
}

List<ConfigSource> result = new ArrayList<>();
if (config.configMaps.isPresent()) {
result.addAll(getConfigMapConfigSources(config.configMaps.get()));
}
if (config.secrets.isPresent()) {
result.addAll(getSecretConfigSources(config.secrets.get()));
}
return result;
}

Expand All @@ -64,13 +70,46 @@ private List<ConfigSource> getConfigMapConfigSources(List<String> configMapNames
result.addAll(
configMapConfigSourceUtil.toConfigSources(configMap.getMetadata().getName(), configMap.getData()));
if (log.isDebugEnabled()) {
log.debug("Done reading ConfigMap " + configMap);
log.debug("Done reading ConfigMap " + configMap.getMetadata().getName());
}
}
}
return result;
} catch (Exception e) {
throw new RuntimeException("Unable to obtain configuration for ConfigMap objects from Kubernetes API Server at: "
+ client.getConfiguration().getMasterUrl(), e);
}
}

private List<ConfigSource> getSecretConfigSources(List<String> secretNames) {
List<ConfigSource> result = new ArrayList<>(secretNames.size());

try {
for (String secretName : secretNames) {
if (log.isDebugEnabled()) {
log.debug("Attempting to read Secret " + secretName);
}
Secret secret;
String namespace;
if (config.namespace.isPresent()) {
namespace = config.namespace.get();
secret = client.secrets().inNamespace(namespace).withName(secretName).get();
} else {
namespace = client.getNamespace();
secret = client.secrets().withName(secretName).get();
}
if (secret == null) {
logMissingOrFail(secretName, namespace, "Secret", config.failOnMissingConfig);
} else {
result.addAll(secretConfigSourceUtil.toConfigSources(secret.getMetadata().getName(), secret.getData()));
if (log.isDebugEnabled()) {
log.debug("Done reading Secret " + secret.getMetadata().getName());
}
}
}
return result;
} catch (Exception e) {
throw new RuntimeException("Unable to obtain configuration for ConfigMap objects for Kubernetes API Server at: "
throw new RuntimeException("Unable to obtain configuration for Secret objects from Kubernetes API Server at: "
+ client.getConfiguration().getMasterUrl(), e);
}
}
Expand All @@ -79,7 +118,8 @@ private void logMissingOrFail(String name, String namespace, String type, boolea
String message = type + " '" + name + "' not found";
if (namespace == null) {
message = message
+ ". No Kubernetes namespace was set (most likely because the application is running outside the Kubernetes cluster). Consider setting 'quarkus.kubernetes-client.namespace=my-namespace' to specify the namespace in which to look up the ConfigMap";
+ ". No Kubernetes namespace was set (most likely because the application is running outside the Kubernetes cluster). Consider setting 'quarkus.kubernetes-client.namespace=my-namespace' to specify the namespace in which to look up the "
+ type;
} else {
message = message + " in namespace '" + namespace + "'";
}
Expand Down
Loading