diff --git a/kubernetes-discovery-client/src/main/java/io/micronaut/kubernetes/KubernetesConfiguration.java b/kubernetes-discovery-client/src/main/java/io/micronaut/kubernetes/KubernetesConfiguration.java index 699111871..96d9cdc18 100644 --- a/kubernetes-discovery-client/src/main/java/io/micronaut/kubernetes/KubernetesConfiguration.java +++ b/kubernetes-discovery-client/src/main/java/io/micronaut/kubernetes/KubernetesConfiguration.java @@ -15,6 +15,11 @@ */ package io.micronaut.kubernetes; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; import io.micronaut.context.annotation.BootstrapContextCompatible; import io.micronaut.context.annotation.ConfigurationProperties; @@ -24,12 +29,6 @@ import io.micronaut.discovery.DiscoveryConfiguration; import io.micronaut.kubernetes.client.NamespaceResolver; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Map; - /** * Encapsulates constants for Kubernetes configuration. * @@ -250,30 +249,15 @@ public void setExceptionOnPodLabelsMissing(boolean exceptionOnPodLabelsMissing) } /** - * Kubernetes secrets configuration properties. + * Base class for config-maps and secrets. */ - @ConfigurationProperties(KubernetesSecretsConfiguration.PREFIX) - @BootstrapContextCompatible - public static class KubernetesSecretsConfiguration extends AbstractKubernetesConfiguration { - - static final String PREFIX = "secrets"; - - static final boolean DEFAULT_ENABLED = false; - - private boolean enabled = DEFAULT_ENABLED; + public abstract static class AbstractConfigConfiguration extends AbstractKubernetesConfiguration { private Collection paths; private boolean useApi; + private boolean watch; - @Override - public boolean isEnabled() { - return enabled; - } - - /** - * @param enabled enabled flag. - */ - public void setEnabled(boolean enabled) { - this.enabled = enabled; + AbstractConfigConfiguration(boolean defaultWatch) { + watch = defaultWatch; } /** @@ -306,64 +290,65 @@ public boolean isUseApi() { public void setUseApi(boolean useApi) { this.useApi = useApi; } - } - - /** - * Kubernetes config maps configuration properties. - */ - @ConfigurationProperties(KubernetesConfigMapsConfiguration.PREFIX) - @BootstrapContextCompatible - public static class KubernetesConfigMapsConfiguration extends AbstractKubernetesConfiguration { - public static final String PREFIX = "config-maps"; - static final boolean DEFAULT_WATCH = true; - - private Collection paths; - private boolean useApi; - private boolean watch = DEFAULT_WATCH; /** - * @return paths where config maps are mounted + * @return whether to enable watching for the ConfigMap changes. */ - public Collection getPaths() { - if (paths == null) { - return Collections.emptySet(); - } - return paths; + public boolean isWatch() { + return watch; } /** - * @param paths where config maps are mounted + * @param watch flag to watch for the ConfigMap changes. */ - public void setPaths(Collection paths) { - this.paths = paths; + public void setWatch(boolean watch) { + this.watch = watch; } + } - /** - * @return whether to use the API to read config maps when {@link #paths} is used. - */ - public boolean isUseApi() { - return useApi; + /** + * Kubernetes secrets configuration properties. + */ + @ConfigurationProperties(KubernetesSecretsConfiguration.PREFIX) + @BootstrapContextCompatible + public static class KubernetesSecretsConfiguration extends AbstractConfigConfiguration { + + static final String PREFIX = "secrets"; + + static final boolean DEFAULT_ENABLED = false; + static final boolean DEFAULT_WATCH = false; + + private boolean enabled = DEFAULT_ENABLED; + + public KubernetesSecretsConfiguration() { + super(DEFAULT_WATCH); } - /** - * @param useApi whether to use the API to read config maps when {@link #paths} is used. - */ - public void setUseApi(boolean useApi) { - this.useApi = useApi; + @Override + public boolean isEnabled() { + return enabled; } /** - * @return whether to enable watching for the ConfigMap changes. Defaults to {@value DEFAULT_WATCH}. + * @param enabled enabled flag. */ - public boolean isWatch() { - return watch; + public void setEnabled(boolean enabled) { + this.enabled = enabled; } + } - /** - * @param watch flag to watch for the ConfigMap changes. - */ - public void setWatch(boolean watch) { - this.watch = watch; + /** + * Kubernetes config maps configuration properties. + */ + @ConfigurationProperties(KubernetesConfigMapsConfiguration.PREFIX) + @BootstrapContextCompatible + public static class KubernetesConfigMapsConfiguration extends AbstractConfigConfiguration { + public static final String PREFIX = "config-maps"; + + static final boolean DEFAULT_WATCH = true; + + public KubernetesConfigMapsConfiguration() { + super(DEFAULT_WATCH); } } } diff --git a/kubernetes-discovery-client/src/main/java/io/micronaut/kubernetes/configuration/ConfigMapLabelSupplier.java b/kubernetes-discovery-client/src/main/java/io/micronaut/kubernetes/configuration/AbstractKubernetesConfigLabelSupplier.java similarity index 61% rename from kubernetes-discovery-client/src/main/java/io/micronaut/kubernetes/configuration/ConfigMapLabelSupplier.java rename to kubernetes-discovery-client/src/main/java/io/micronaut/kubernetes/configuration/AbstractKubernetesConfigLabelSupplier.java index 42c72e116..19d1f777a 100644 --- a/kubernetes-discovery-client/src/main/java/io/micronaut/kubernetes/configuration/ConfigMapLabelSupplier.java +++ b/kubernetes-discovery-client/src/main/java/io/micronaut/kubernetes/configuration/AbstractKubernetesConfigLabelSupplier.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2021 original authors + * Copyright 2017-2024 original authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,48 +15,44 @@ */ package io.micronaut.kubernetes.configuration; -import io.micronaut.context.annotation.Requires; -import io.micronaut.context.env.Environment; -import io.micronaut.kubernetes.KubernetesConfiguration; -import io.micronaut.kubernetes.client.reactor.CoreV1ApiReactorClient; -import io.micronaut.kubernetes.util.KubernetesUtils; -import jakarta.inject.Singleton; +import java.util.Map; +import java.util.function.Supplier; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.Map; -import java.util.function.Supplier; +import io.micronaut.kubernetes.KubernetesConfiguration; +import io.micronaut.kubernetes.client.reactor.CoreV1ApiReactorClient; +import io.micronaut.kubernetes.util.KubernetesUtils; /** * Based on configuration dynamically evaluates the label selector. * * @author Pavol Gressa - * @since 3.1 */ -@Singleton -@Requires(env = Environment.KUBERNETES) -public class ConfigMapLabelSupplier implements Supplier { - - private static final Logger LOG = LoggerFactory.getLogger(ConfigMapLabelSupplier.class); +abstract class AbstractKubernetesConfigLabelSupplier implements Supplier { + private static final Logger LOG = LoggerFactory.getLogger(AbstractKubernetesConfigLabelSupplier.class); + protected final KubernetesConfiguration configuration; private final CoreV1ApiReactorClient coreV1ApiReactorClient; - private final KubernetesConfiguration configuration; - public ConfigMapLabelSupplier(CoreV1ApiReactorClient coreV1ApiReactorClient, KubernetesConfiguration configuration) { + AbstractKubernetesConfigLabelSupplier(CoreV1ApiReactorClient coreV1ApiReactorClient, KubernetesConfiguration configuration) { this.coreV1ApiReactorClient = coreV1ApiReactorClient; this.configuration = configuration; } @Override public String get() { - Map labels = configuration.getConfigMaps().getLabels(); + Map labels = getConfig().getLabels(); String labelSelector = KubernetesUtils.computePodLabelSelector(coreV1ApiReactorClient, - configuration.getConfigMaps().getPodLabels(), configuration.getNamespace(), labels, - configuration.getDiscovery().isExceptionOnPodLabelsMissing()) - .block(); + getConfig().getPodLabels(), configuration.getNamespace(), labels, + configuration.getDiscovery().isExceptionOnPodLabelsMissing()) + .block(); if (LOG.isInfoEnabled()) { - LOG.info("Computed kubernetes configuration discovery config map label selector: {}", labelSelector); + LOG.info("Computed kubernetes configuration discovery config label selector: {}", labelSelector); } return labelSelector; } + + abstract KubernetesConfiguration.AbstractConfigConfiguration getConfig(); } diff --git a/kubernetes-discovery-client/src/main/java/io/micronaut/kubernetes/configuration/AbstractKubernetesConfigWatcher.java b/kubernetes-discovery-client/src/main/java/io/micronaut/kubernetes/configuration/AbstractKubernetesConfigWatcher.java new file mode 100644 index 000000000..46c86f204 --- /dev/null +++ b/kubernetes-discovery-client/src/main/java/io/micronaut/kubernetes/configuration/AbstractKubernetesConfigWatcher.java @@ -0,0 +1,159 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kubernetes.configuration; + +import java.util.Collection; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.kubernetes.client.common.KubernetesObject; +import io.kubernetes.client.informer.ResourceEventHandler; +import io.kubernetes.client.openapi.models.V1ConfigMap; +import io.micronaut.context.env.Environment; +import io.micronaut.context.env.PropertySource; +import io.micronaut.context.event.ApplicationEventPublisher; +import io.micronaut.kubernetes.KubernetesConfiguration; +import io.micronaut.runtime.context.scope.refresh.RefreshEvent; + +/** + * Watches for ConfigMap/Secret changes and makes the appropriate changes to the {@link Environment} by adding or removing + * {@link PropertySource}s. + * + * @param the type of Kubernetes object to watch + * + * @author Álvaro Sánchez-Mariscal + * @since 1.0.0 + */ +public abstract class AbstractKubernetesConfigWatcher implements ResourceEventHandler { + + private static final Logger LOG = LoggerFactory.getLogger(AbstractKubernetesConfigWatcher.class); + + // this flag controls when to start reflecting the changes to the discovery client + final AtomicBoolean serviceStarted = new AtomicBoolean(false); + + private final Environment environment; + private final ApplicationEventPublisher eventPublisher; + + AbstractKubernetesConfigWatcher(Environment environment, ApplicationEventPublisher eventPublisher) { + this.environment = environment; + this.eventPublisher = eventPublisher; + } + + @Override + public void onAdd(T config) { + if (!serviceStarted.get()) { + return; + } + + PropertySource propertySource = null; + if (config != null) { + propertySource = readAsPropertySource(config); + } + if (passesIncludesExcludesLabelsFilters(config)) { + if (LOG.isDebugEnabled()) { + LOG.debug("PropertySource created from Config: {}", config.getMetadata().getName()); + } + + KubernetesConfigurationClient.addPropertySourceToCache(propertySource); + refreshEnvironment(); + } + } + + abstract PropertySource readAsPropertySource(T config); + + @Override + public void onUpdate(T oldConfig, T newConfig) { + if (!serviceStarted.get()) { + return; + } + PropertySource propertySource = null; + if (newConfig != null) { + propertySource = readAsPropertySource(newConfig); + } + if (passesIncludesExcludesLabelsFilters(newConfig)) { + if (LOG.isDebugEnabled()) { + LOG.debug("PropertySource modified by ConfigMap: {}", newConfig.getMetadata().getName()); + } + + KubernetesConfigurationClient.removePropertySourceFromCache(propertySource.getName()); + KubernetesConfigurationClient.addPropertySourceToCache(propertySource); + refreshEnvironment(); + } + } + + @Override + public void onDelete(T config, boolean deletedFinalStateUnknown) { + if (!serviceStarted.get()) { + return; + } + PropertySource propertySource = null; + if (config != null) { + propertySource = readAsPropertySource(config); + } + if (passesIncludesExcludesLabelsFilters(config)) { + if (LOG.isDebugEnabled()) { + LOG.debug("Removed PropertySource created from ConfigMap: {}", config.getMetadata().getName()); + } + + KubernetesConfigurationClient.removePropertySourceFromCache(propertySource.getName()); + refreshEnvironment(); + } + } + + /** + * Send a {@link RefreshEvent} when a {@link V1ConfigMap} change affects the {@link Environment}. + * + * @see io.micronaut.management.endpoint.refresh.RefreshEndpoint#refresh(Boolean) + */ + private void refreshEnvironment() { + final Map changes = environment.refreshAndDiff(); + if (LOG.isTraceEnabled()) { + LOG.trace("Changes in ConfigMap property sources: [{}]", String.join(", ", changes.keySet())); + } + if (!changes.isEmpty()) { + eventPublisher.publishEvent(new RefreshEvent(changes)); + } + } + + private boolean passesIncludesExcludesLabelsFilters(T config) { + Collection includes = getConfig().getIncludes(); + Collection excludes = getConfig().getExcludes(); + + boolean process = true; + if (!includes.isEmpty()) { + if (LOG.isTraceEnabled()) { + LOG.trace("ConfigMap includes: {}", includes); + } + process = includes.contains(config.getMetadata().getName()); + } else if (!excludes.isEmpty()) { + if (LOG.isTraceEnabled()) { + LOG.trace("ConfigMap excludes: {}", excludes); + } + process = !excludes.contains(config.getMetadata().getName()); + } + + if (!process && LOG.isTraceEnabled()) { + LOG.trace("ConfigMap {} not added because it doesn't match includes/excludes filters", config.getMetadata().getName()); + } + + return process; + } + + abstract KubernetesConfiguration.AbstractConfigConfiguration getConfig(); +} diff --git a/kubernetes-discovery-client/src/main/java/io/micronaut/kubernetes/configuration/AbstractKubernetesConfigWatcherCondition.java b/kubernetes-discovery-client/src/main/java/io/micronaut/kubernetes/configuration/AbstractKubernetesConfigWatcherCondition.java new file mode 100644 index 000000000..95433ee31 --- /dev/null +++ b/kubernetes-discovery-client/src/main/java/io/micronaut/kubernetes/configuration/AbstractKubernetesConfigWatcherCondition.java @@ -0,0 +1,54 @@ +/* + * Copyright 2017-2021 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kubernetes.configuration; + +import io.micronaut.context.condition.Condition; +import io.micronaut.context.condition.ConditionContext; +import io.micronaut.kubernetes.KubernetesConfiguration; + +/** + * Condition evaluates when the {@link AbstractKubernetesConfigWatcherCondition} is enabled. + * + * @author Pavol Gressa + * @since 3.1 + */ +public abstract class AbstractKubernetesConfigWatcherCondition implements Condition { + + @Override + public boolean matches(ConditionContext context) { + final KubernetesConfiguration.AbstractConfigConfiguration configMapsConfiguration = + getConfig(context); + + if (!configMapsConfiguration.isEnabled()) { + context.fail("configuration client for the ConfigMaps is disabled"); + return false; + } + + if (!configMapsConfiguration.isWatch()) { + context.fail("watch for the ConfigMap changes is disabled"); + return false; + } + + if (!configMapsConfiguration.getPaths().isEmpty() && !configMapsConfiguration.isUseApi()) { + context.fail("config maps paths configuration for mounted volumes is specified and use api is disabled"); + return false; + } + + return true; + } + + abstract KubernetesConfiguration.AbstractConfigConfiguration getConfig(ConditionContext context); +} diff --git a/kubernetes-discovery-client/src/main/java/io/micronaut/kubernetes/configuration/KubernetesConfigMapLabelSupplier.java b/kubernetes-discovery-client/src/main/java/io/micronaut/kubernetes/configuration/KubernetesConfigMapLabelSupplier.java new file mode 100644 index 000000000..6fff0f89f --- /dev/null +++ b/kubernetes-discovery-client/src/main/java/io/micronaut/kubernetes/configuration/KubernetesConfigMapLabelSupplier.java @@ -0,0 +1,39 @@ +/* + * Copyright 2017-2021 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kubernetes.configuration; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.context.env.Environment; +import io.micronaut.kubernetes.KubernetesConfiguration; +import io.micronaut.kubernetes.client.reactor.CoreV1ApiReactorClient; +import jakarta.inject.Singleton; + +/** + * Based on configuration dynamically evaluates the label selector for config maps. + */ +@Singleton +@Requires(env = Environment.KUBERNETES) +public class KubernetesConfigMapLabelSupplier extends AbstractKubernetesConfigLabelSupplier { + + public KubernetesConfigMapLabelSupplier(CoreV1ApiReactorClient coreV1ApiReactorClient, KubernetesConfiguration configuration) { + super(coreV1ApiReactorClient, configuration); + } + + @Override + KubernetesConfiguration.AbstractConfigConfiguration getConfig() { + return configuration.getConfigMaps(); + } +} diff --git a/kubernetes-discovery-client/src/main/java/io/micronaut/kubernetes/configuration/KubernetesConfigMapWatcher.java b/kubernetes-discovery-client/src/main/java/io/micronaut/kubernetes/configuration/KubernetesConfigMapWatcher.java index dc4091662..d13e4c7a7 100644 --- a/kubernetes-discovery-client/src/main/java/io/micronaut/kubernetes/configuration/KubernetesConfigMapWatcher.java +++ b/kubernetes-discovery-client/src/main/java/io/micronaut/kubernetes/configuration/KubernetesConfigMapWatcher.java @@ -15,9 +15,6 @@ */ package io.micronaut.kubernetes.configuration; -import io.kubernetes.client.informer.ResourceEventHandler; -import io.kubernetes.client.openapi.ApiClient; -import io.kubernetes.client.openapi.apis.CoreV1Api; import io.kubernetes.client.openapi.models.V1ConfigMap; import io.kubernetes.client.openapi.models.V1ConfigMapList; import io.micronaut.context.annotation.Context; @@ -34,14 +31,6 @@ import io.micronaut.runtime.context.scope.refresh.RefreshEvent; import io.micronaut.runtime.event.annotation.EventListener; import jakarta.inject.Inject; -import jakarta.inject.Named; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.Collection; -import java.util.Map; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.atomic.AtomicBoolean; /** * Watches for ConfigMap changes and makes the appropriate changes to the {@link Environment} by adding or removing @@ -55,40 +44,16 @@ @Requires(beans = CoreV1ApiReactorClient.class) @Requires(property = ConfigurationClient.ENABLED, value = "true", defaultValue = "false") @Requires(condition = KubernetesConfigMapWatcherCondition.class) -@Informer(apiType = V1ConfigMap.class, apiListType = V1ConfigMapList.class, resourcePlural = "configmaps", apiGroup = "", labelSelectorSupplier = ConfigMapLabelSupplier.class) -public final class KubernetesConfigMapWatcher implements ResourceEventHandler { - - private static final Logger LOG = LoggerFactory.getLogger(KubernetesConfigMapWatcher.class); +@Informer(apiType = V1ConfigMap.class, apiListType = V1ConfigMapList.class, resourcePlural = "configmaps", apiGroup = "", labelSelectorSupplier = KubernetesConfigMapLabelSupplier.class) +public final class KubernetesConfigMapWatcher extends AbstractKubernetesConfigWatcher { - private final Environment environment; private final KubernetesConfiguration configuration; - private final ApplicationEventPublisher eventPublisher; - - // this flag controls when to start reflecting the changes to the discovery client - private final AtomicBoolean serviceStarted = new AtomicBoolean(false); - - /** - * @param environment the {@link Environment} - * @param apiClient the {@link ApiClient} - * @param coreV1Api the {@link CoreV1Api} - * @param coreV1ApiReactorClient the {@link CoreV1ApiReactorClient} - * @param configuration the {@link KubernetesConfiguration} - * @param executorService the IO {@link ExecutorService} where the watch publisher will be scheduled on - * @param eventPublisher the {@link ApplicationEventPublisher} - * @deprecated Use new version {@link KubernetesConfigMapWatcher#KubernetesConfigMapWatcher(Environment, KubernetesConfiguration, ApplicationEventPublisher)} - */ - public KubernetesConfigMapWatcher(Environment environment, ApiClient apiClient, CoreV1Api coreV1Api, CoreV1ApiReactorClient coreV1ApiReactorClient, KubernetesConfiguration configuration, @Named("io") ExecutorService executorService, ApplicationEventPublisher eventPublisher) { - this(environment, configuration, eventPublisher); - if (LOG.isDebugEnabled()) { - LOG.debug("Initializing {}", getClass().getName()); - } - } @Inject public KubernetesConfigMapWatcher(Environment environment, KubernetesConfiguration configuration, ApplicationEventPublisher eventPublisher) { - this.environment = environment; + super(environment, eventPublisher); + this.configuration = configuration; - this.eventPublisher = eventPublisher; } @EventListener @@ -97,100 +62,12 @@ public void onApplicationEvent(ServiceReadyEvent event) { } @Override - public void onAdd(V1ConfigMap configMap) { - if (!serviceStarted.get()) { - return; - } - - PropertySource propertySource = null; - if (configMap != null) { - propertySource = KubernetesUtils.configMapAsPropertySource(configMap); - } - if (passesIncludesExcludesLabelsFilters(configMap)) { - if (LOG.isDebugEnabled()) { - LOG.debug("PropertySource created from ConfigMap: {}", configMap.getMetadata().getName()); - } - - KubernetesConfigurationClient.addPropertySourceToCache(propertySource); - refreshEnvironment(); - } - } - - @Override - public void onUpdate(V1ConfigMap oldObj, V1ConfigMap configMap) { - if (!serviceStarted.get()) { - return; - } - PropertySource propertySource = null; - if (configMap != null) { - propertySource = KubernetesUtils.configMapAsPropertySource(configMap); - } - if (passesIncludesExcludesLabelsFilters(configMap)) { - if (LOG.isDebugEnabled()) { - LOG.debug("PropertySource modified by ConfigMap: {}", configMap.getMetadata().getName()); - } - - KubernetesConfigurationClient.removePropertySourceFromCache(propertySource.getName()); - KubernetesConfigurationClient.addPropertySourceToCache(propertySource); - refreshEnvironment(); - } + PropertySource readAsPropertySource(V1ConfigMap configMap) { + return KubernetesUtils.configMapAsPropertySource(configMap); } @Override - public void onDelete(V1ConfigMap configMap, boolean deletedFinalStateUnknown) { - if (!serviceStarted.get()) { - return; - } - PropertySource propertySource = null; - if (configMap != null) { - propertySource = KubernetesUtils.configMapAsPropertySource(configMap); - } - if (passesIncludesExcludesLabelsFilters(configMap)) { - if (LOG.isDebugEnabled()) { - LOG.debug("Removed PropertySource created from ConfigMap: {}", configMap.getMetadata().getName()); - } - - KubernetesConfigurationClient.removePropertySourceFromCache(propertySource.getName()); - refreshEnvironment(); - } - } - - /** - * Send a {@link RefreshEvent} when a {@link V1ConfigMap} change affects the {@link Environment}. - * - * @see io.micronaut.management.endpoint.refresh.RefreshEndpoint#refresh(Boolean) - */ - private void refreshEnvironment() { - final Map changes = environment.refreshAndDiff(); - if (LOG.isTraceEnabled()) { - LOG.trace("Changes in ConfigMap property sources: [{}]", String.join(", ", changes.keySet())); - } - if (!changes.isEmpty()) { - eventPublisher.publishEvent(new RefreshEvent(changes)); - } - } - - private boolean passesIncludesExcludesLabelsFilters(V1ConfigMap configMap) { - Collection includes = configuration.getConfigMaps().getIncludes(); - Collection excludes = configuration.getConfigMaps().getExcludes(); - - boolean process = true; - if (!includes.isEmpty()) { - if (LOG.isTraceEnabled()) { - LOG.trace("ConfigMap includes: {}", includes); - } - process = includes.contains(configMap.getMetadata().getName()); - } else if (!excludes.isEmpty()) { - if (LOG.isTraceEnabled()) { - LOG.trace("ConfigMap excludes: {}", excludes); - } - process = !excludes.contains(configMap.getMetadata().getName()); - } - - if (!process && LOG.isTraceEnabled()) { - LOG.trace("ConfigMap {} not added because it doesn't match includes/excludes filters", configMap.getMetadata().getName()); - } - - return process; + KubernetesConfiguration.AbstractConfigConfiguration getConfig() { + return configuration.getConfigMaps(); } } diff --git a/kubernetes-discovery-client/src/main/java/io/micronaut/kubernetes/configuration/KubernetesConfigMapWatcherCondition.java b/kubernetes-discovery-client/src/main/java/io/micronaut/kubernetes/configuration/KubernetesConfigMapWatcherCondition.java index f785bfa25..d8c9f0d06 100644 --- a/kubernetes-discovery-client/src/main/java/io/micronaut/kubernetes/configuration/KubernetesConfigMapWatcherCondition.java +++ b/kubernetes-discovery-client/src/main/java/io/micronaut/kubernetes/configuration/KubernetesConfigMapWatcherCondition.java @@ -15,7 +15,6 @@ */ package io.micronaut.kubernetes.configuration; -import io.micronaut.context.condition.Condition; import io.micronaut.context.condition.ConditionContext; import io.micronaut.core.annotation.Internal; import io.micronaut.kubernetes.KubernetesConfiguration; @@ -27,28 +26,9 @@ * @since 3.1 */ @Internal -public class KubernetesConfigMapWatcherCondition implements Condition { - +public class KubernetesConfigMapWatcherCondition extends AbstractKubernetesConfigWatcherCondition { @Override - public boolean matches(ConditionContext context) { - final KubernetesConfiguration.KubernetesConfigMapsConfiguration configMapsConfiguration = - context.getBean(KubernetesConfiguration.KubernetesConfigMapsConfiguration.class); - - if (!configMapsConfiguration.isEnabled()) { - context.fail("configuration client for the ConfigMaps is disabled"); - return false; - } - - if (!configMapsConfiguration.isWatch()) { - context.fail("watch for the ConfigMap changes is disabled"); - return false; - } - - if (!configMapsConfiguration.getPaths().isEmpty() && !configMapsConfiguration.isUseApi()) { - context.fail("config maps paths configuration for mounted volumes is specified and use api is disabled"); - return false; - } - - return true; + KubernetesConfiguration.AbstractConfigConfiguration getConfig(ConditionContext context) { + return context.getBean(KubernetesConfiguration.KubernetesConfigMapsConfiguration.class); } } diff --git a/kubernetes-discovery-client/src/main/java/io/micronaut/kubernetes/configuration/KubernetesSecretLabelSupplier.java b/kubernetes-discovery-client/src/main/java/io/micronaut/kubernetes/configuration/KubernetesSecretLabelSupplier.java new file mode 100644 index 000000000..4a3e2d22b --- /dev/null +++ b/kubernetes-discovery-client/src/main/java/io/micronaut/kubernetes/configuration/KubernetesSecretLabelSupplier.java @@ -0,0 +1,39 @@ +/* + * Copyright 2017-2021 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kubernetes.configuration; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.context.env.Environment; +import io.micronaut.kubernetes.KubernetesConfiguration; +import io.micronaut.kubernetes.client.reactor.CoreV1ApiReactorClient; +import jakarta.inject.Singleton; + +/** + * Based on configuration dynamically evaluates the label selector for config maps. + */ +@Singleton +@Requires(env = Environment.KUBERNETES) +public class KubernetesSecretLabelSupplier extends AbstractKubernetesConfigLabelSupplier { + + public KubernetesSecretLabelSupplier(CoreV1ApiReactorClient coreV1ApiReactorClient, KubernetesConfiguration configuration) { + super(coreV1ApiReactorClient, configuration); + } + + @Override + KubernetesConfiguration.AbstractConfigConfiguration getConfig() { + return configuration.getSecrets(); + } +} diff --git a/kubernetes-discovery-client/src/main/java/io/micronaut/kubernetes/configuration/KubernetesSecretWatcher.java b/kubernetes-discovery-client/src/main/java/io/micronaut/kubernetes/configuration/KubernetesSecretWatcher.java new file mode 100644 index 000000000..6d8cef2ce --- /dev/null +++ b/kubernetes-discovery-client/src/main/java/io/micronaut/kubernetes/configuration/KubernetesSecretWatcher.java @@ -0,0 +1,73 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kubernetes.configuration; + +import io.kubernetes.client.openapi.models.V1Secret; +import io.kubernetes.client.openapi.models.V1SecretList; +import io.micronaut.context.annotation.Context; +import io.micronaut.context.annotation.Requires; +import io.micronaut.context.env.Environment; +import io.micronaut.context.env.PropertySource; +import io.micronaut.context.event.ApplicationEventPublisher; +import io.micronaut.discovery.config.ConfigurationClient; +import io.micronaut.discovery.event.ServiceReadyEvent; +import io.micronaut.kubernetes.KubernetesConfiguration; +import io.micronaut.kubernetes.client.informer.Informer; +import io.micronaut.kubernetes.client.reactor.CoreV1ApiReactorClient; +import io.micronaut.kubernetes.util.KubernetesUtils; +import io.micronaut.runtime.context.scope.refresh.RefreshEvent; +import io.micronaut.runtime.event.annotation.EventListener; +import jakarta.inject.Inject; + +/** + * Watches for Secret changes and makes the appropriate changes to the {@link Environment} by adding or removing + * {@link PropertySource}s. + * + * @author Álvaro Sánchez-Mariscal + * @since 1.0.0 + */ +@Context +@Requires(env = Environment.KUBERNETES) +@Requires(beans = CoreV1ApiReactorClient.class) +@Requires(property = ConfigurationClient.ENABLED, value = "true", defaultValue = "false") +@Requires(condition = KubernetesSecretWatcherCondition.class) +@Informer(apiType = V1Secret.class, apiListType = V1SecretList.class, resourcePlural = "secrets", apiGroup = "", labelSelectorSupplier = KubernetesSecretLabelSupplier.class) +public final class KubernetesSecretWatcher extends AbstractKubernetesConfigWatcher { + + private final KubernetesConfiguration configuration; + + @Inject + public KubernetesSecretWatcher(Environment environment, KubernetesConfiguration configuration, ApplicationEventPublisher eventPublisher) { + super(environment, eventPublisher); + + this.configuration = configuration; + } + + @EventListener + public void onApplicationEvent(ServiceReadyEvent event) { + serviceStarted.set(true); + } + + @Override + PropertySource readAsPropertySource(V1Secret secret) { + return KubernetesUtils.secretAsPropertySource(secret); + } + + @Override + KubernetesConfiguration.AbstractConfigConfiguration getConfig() { + return configuration.getSecrets(); + } +} diff --git a/kubernetes-discovery-client/src/main/java/io/micronaut/kubernetes/configuration/KubernetesSecretWatcherCondition.java b/kubernetes-discovery-client/src/main/java/io/micronaut/kubernetes/configuration/KubernetesSecretWatcherCondition.java new file mode 100644 index 000000000..fd5aeba78 --- /dev/null +++ b/kubernetes-discovery-client/src/main/java/io/micronaut/kubernetes/configuration/KubernetesSecretWatcherCondition.java @@ -0,0 +1,34 @@ +/* + * Copyright 2017-2021 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kubernetes.configuration; + +import io.micronaut.context.condition.ConditionContext; +import io.micronaut.core.annotation.Internal; +import io.micronaut.kubernetes.KubernetesConfiguration; + +/** + * Condition evaluates when the {@link KubernetesSecretWatcherCondition} is enabled. + * + * @author Pavol Gressa + * @since 3.1 + */ +@Internal +public class KubernetesSecretWatcherCondition extends AbstractKubernetesConfigWatcherCondition { + @Override + KubernetesConfiguration.AbstractConfigConfiguration getConfig(ConditionContext context) { + return context.getBean(KubernetesConfiguration.KubernetesSecretsConfiguration.class); + } +} diff --git a/kubernetes-discovery-client/src/test/groovy/io/micronaut/kubernetes/configuration/KubernetesSecretWatcherSpec.groovy b/kubernetes-discovery-client/src/test/groovy/io/micronaut/kubernetes/configuration/KubernetesSecretWatcherSpec.groovy new file mode 100644 index 000000000..4d7bcd7fe --- /dev/null +++ b/kubernetes-discovery-client/src/test/groovy/io/micronaut/kubernetes/configuration/KubernetesSecretWatcherSpec.groovy @@ -0,0 +1,76 @@ +package io.micronaut.kubernetes.configuration + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.annotation.Property +import io.micronaut.context.env.Environment +import io.micronaut.kubernetes.test.TestUtils +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import spock.lang.Requires +import spock.lang.Specification + +@MicronautTest(environments = [Environment.KUBERNETES]) +@Requires({ TestUtils.kubernetesApiAvailable() }) +@Property(name = "spec.reuseNamespace", value = "false") +class KubernetesSecretWatcherSpec extends Specification { + + void "KubernetesSecretWatcher not exists when only config-client is enabled"() { + given: + ApplicationContext applicationContext = ApplicationContext.run([ + "micronaut.config-client.enabled": "true" + ], Environment.KUBERNETES) + + expect: + !applicationContext.containsBean(KubernetesSecretWatcher) + } + + void "KubernetesSecretWatcher is explicitly enabled"() { + given: + ApplicationContext applicationContext = ApplicationContext.run([ + "kubernetes.client.secrets.watch": "true", + "kubernetes.client.secrets.enabled": "true", + "micronaut.config-client.enabled": "true" + ], Environment.KUBERNETES) + + expect: + applicationContext.containsBean(KubernetesSecretWatcher) + } + + void "KubernetesSecretWatcher is disabled when config-client is disabled"() { + given: + ApplicationContext applicationContext = ApplicationContext.run([ + "kubernetes.client.secrets.watch": "true", + "kubernetes.client.secrets.enabled": "true", + "micronaut.config-client.enabled": "false" + ], Environment.KUBERNETES) + + expect: + !applicationContext.containsBean(KubernetesSecretWatcher) + } + + void "KubernetesSecretWatcher is disabled when mounted volume paths are specified"() { + given: + ApplicationContext applicationContext = ApplicationContext.run([ + "kubernetes.client.secrets.watch": "true", + "kubernetes.client.secrets.enabled": "true", + "micronaut.config-client.enabled": "true", + "kubernetes.client.secrets.paths": ["path1"] + ], Environment.KUBERNETES) + + expect: + !applicationContext.containsBean(KubernetesSecretWatcher) + } + + void "KubernetesSecretWatcher is enabled when mounted volume paths are specified and use-api is enabled"() { + given: + ApplicationContext applicationContext = ApplicationContext.run([ + "kubernetes.client.secrets.watch": "true", + "kubernetes.client.secrets.enabled": "true", + "micronaut.config-client.enabled": "true", + "kubernetes.client.secrets.paths" : ["path1"], + "kubernetes.client.secrets.use-api": "true" + ], Environment.KUBERNETES) + + expect: + applicationContext.containsBean(KubernetesSecretWatcher) + } +} diff --git a/src/main/docs/guide/config-client.adoc b/src/main/docs/guide/config-client.adoc index b6b868625..b43f36794 100644 --- a/src/main/docs/guide/config-client.adoc +++ b/src/main/docs/guide/config-client.adoc @@ -367,3 +367,19 @@ kubernetes: In this scenario, if there are property keys defined in both type of secrets, the ones coming from mounted volumes will take precedence over the ones coming from the API. ==== + +### Watching for changes in Secrets + +If watch is enabled, this configuration module will watch for ``Secret``s added/modified/deleted, and provided that the +changes match with the above filters, they will be propagated to the `Environment` and refresh it. + +This means that those changes will be immediately available in your application without a restart. + +If you want to enable watching for Secret changes, set `kubernetes.client.secrets.watch` to `true`. +This should be done in the `bootstrap.yml` configuration file because the configuration client is initialized during the +bootstrap phase, which happens before evaluating the `application.yml` configuration file. + +[NOTE] +==== +When `kubernetes.client.secrets.use-api` is set to `false`, watching for the changes won't be started. +====