diff --git a/spring-cloud-kubernetes-core/pom.xml b/spring-cloud-kubernetes-core/pom.xml index d8e30162..3bbd4005 100644 --- a/spring-cloud-kubernetes-core/pom.xml +++ b/spring-cloud-kubernetes-core/pom.xml @@ -66,31 +66,33 @@ spring-boot-starter-test test - org.springframework.boot spring-boot-starter-web test - io.fabric8 kubernetes-client test-jar test - io.fabric8 mockwebserver test - org.spockframework spock-spring test + + + org.springframework.boot + spring-boot-configuration-processor + true + \ No newline at end of file diff --git a/spring-cloud-kubernetes-core/src/main/java/io/fabric8/spring/cloud/kubernetes/config/ConfigMapBootstrapConfiguration.java b/spring-cloud-kubernetes-core/src/main/java/io/fabric8/spring/cloud/kubernetes/config/BootstrapConfiguration.java similarity index 81% rename from spring-cloud-kubernetes-core/src/main/java/io/fabric8/spring/cloud/kubernetes/config/ConfigMapBootstrapConfiguration.java rename to spring-cloud-kubernetes-core/src/main/java/io/fabric8/spring/cloud/kubernetes/config/BootstrapConfiguration.java index 64c2d917..74315b30 100644 --- a/spring-cloud-kubernetes-core/src/main/java/io/fabric8/spring/cloud/kubernetes/config/ConfigMapBootstrapConfiguration.java +++ b/spring-cloud-kubernetes-core/src/main/java/io/fabric8/spring/cloud/kubernetes/config/BootstrapConfiguration.java @@ -18,6 +18,7 @@ package io.fabric8.spring.cloud.kubernetes.config; import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.Secret; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.spring.cloud.kubernetes.KubernetesAutoConfiguration; import org.springframework.beans.factory.annotation.Autowired; @@ -30,12 +31,12 @@ import org.springframework.context.annotation.Import; @Configuration @ConditionalOnProperty(value = "spring.cloud.kubernetes.enabled", matchIfMissing = true) -@ConditionalOnClass(ConfigMap.class) -public class ConfigMapBootstrapConfiguration { +@ConditionalOnClass({ ConfigMap.class, Secret.class }) +public class BootstrapConfiguration { @Configuration - @EnableConfigurationProperties(ConfigMapConfigProperties.class) @Import(KubernetesAutoConfiguration.class) + @EnableConfigurationProperties({ ConfigMapConfigProperties.class, SecretsConfigProperties.class }) @ConditionalOnProperty(name = "spring.cloud.kubernetes.config.enabled", matchIfMissing = true) protected static class KubernetesPropertySourceConfiguration { @Autowired @@ -45,5 +46,10 @@ public class ConfigMapBootstrapConfiguration { public ConfigMapPropertySourceLocator configMapPropertySourceLocator(ConfigMapConfigProperties properties) { return new ConfigMapPropertySourceLocator(client, properties); } + + @Bean + public SecretsPropertySourceLocator secretsPropertySourceLocator(SecretsConfigProperties properties) { + return new SecretsPropertySourceLocator(client, properties); + } } } diff --git a/spring-cloud-kubernetes-core/src/main/java/io/fabric8/spring/cloud/kubernetes/config/ConfigMapPropertySource.java b/spring-cloud-kubernetes-core/src/main/java/io/fabric8/spring/cloud/kubernetes/config/ConfigMapPropertySource.java index 6bbaa05f..42347216 100644 --- a/spring-cloud-kubernetes-core/src/main/java/io/fabric8/spring/cloud/kubernetes/config/ConfigMapPropertySource.java +++ b/spring-cloud-kubernetes-core/src/main/java/io/fabric8/spring/cloud/kubernetes/config/ConfigMapPropertySource.java @@ -17,14 +17,6 @@ package io.fabric8.spring.cloud.kubernetes.config; -import io.fabric8.kubernetes.api.model.ConfigMap; -import io.fabric8.kubernetes.client.KubernetesClient; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.config.YamlPropertiesFactoryBean; -import org.springframework.core.env.MapPropertySource; -import org.springframework.core.io.ByteArrayResource; - import java.io.ByteArrayInputStream; import java.io.IOException; import java.util.HashMap; @@ -33,17 +25,22 @@ import java.util.Properties; import java.util.function.Function; import java.util.stream.Collectors; -public class ConfigMapPropertySource extends MapPropertySource { +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.client.KubernetesClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.config.YamlPropertiesFactoryBean; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.io.ByteArrayResource; +public class ConfigMapPropertySource extends MapPropertySource { private static final Logger LOGGER = LoggerFactory.getLogger(ConfigMapPropertySource.class); - private static final String CONFIGMAP_PATH = "/configmaps"; private static final String APPLICATION_YML = "application.yml"; private static final String APPLICATION_YAML = "application.yaml"; private static final String APPLICATION_PROPERTIES = "application.properties"; private static final String PREFIX = "configmap"; - private static final String SEPARATOR = "."; public ConfigMapPropertySource(KubernetesClient client, String name) { this(client, name, null); @@ -54,9 +51,13 @@ public class ConfigMapPropertySource extends MapPropertySource { } private static String getName(KubernetesClient client, String name, String namespace) { - StringBuilder sb = new StringBuilder(); - sb.append(PREFIX).append(SEPARATOR).append(name).append(SEPARATOR).append(namespace == null || namespace.isEmpty() ? client.getNamespace() : namespace); - return sb.toString(); + return new StringBuilder() + .append(PREFIX) + .append(Constants.PROPERTY_SOURCE_NAME_SEPARATOR) + .append(name) + .append(Constants.PROPERTY_SOURCE_NAME_SEPARATOR) + .append(namespace == null || namespace.isEmpty() ? client.getNamespace() : namespace) + .toString(); } private static Map getData(KubernetesClient client, String name, String namespace) { diff --git a/spring-cloud-kubernetes-core/src/main/java/io/fabric8/spring/cloud/kubernetes/config/ConfigMapPropertySourceLocator.java b/spring-cloud-kubernetes-core/src/main/java/io/fabric8/spring/cloud/kubernetes/config/ConfigMapPropertySourceLocator.java index 6644463f..98e6579e 100644 --- a/spring-cloud-kubernetes-core/src/main/java/io/fabric8/spring/cloud/kubernetes/config/ConfigMapPropertySourceLocator.java +++ b/spring-cloud-kubernetes-core/src/main/java/io/fabric8/spring/cloud/kubernetes/config/ConfigMapPropertySourceLocator.java @@ -26,10 +26,6 @@ import org.springframework.core.env.PropertySource; @Order(0) public class ConfigMapPropertySourceLocator implements PropertySourceLocator { - - private static final String SPRING_APPLICATION_NAME = "spring.application.name"; - private static final String FALLBACK_APPLICATION_NAME = "application"; - private final KubernetesClient client; private final ConfigMapConfigProperties properties; @@ -42,7 +38,7 @@ public class ConfigMapPropertySourceLocator implements PropertySourceLocator { public PropertySource locate(Environment environment) { if (environment instanceof ConfigurableEnvironment) { ConfigurableEnvironment env = (ConfigurableEnvironment) environment; - String appName = env.getProperty(SPRING_APPLICATION_NAME, FALLBACK_APPLICATION_NAME); + String appName = env.getProperty(Constants.SPRING_APPLICATION_NAME, Constants.FALLBACK_APPLICATION_NAME); String name = properties.getName() == null || properties.getName().isEmpty() ? appName : properties.getName(); String namespace = properties.getNamespace(); return new ConfigMapPropertySource(client, name, namespace); diff --git a/spring-cloud-kubernetes-core/src/main/java/io/fabric8/spring/cloud/kubernetes/config/Constants.java b/spring-cloud-kubernetes-core/src/main/java/io/fabric8/spring/cloud/kubernetes/config/Constants.java new file mode 100644 index 00000000..b3e46800 --- /dev/null +++ b/spring-cloud-kubernetes-core/src/main/java/io/fabric8/spring/cloud/kubernetes/config/Constants.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2016 to the 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 + * + * http://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.fabric8.spring.cloud.kubernetes.config; + +final class Constants { + static final String SPRING_APPLICATION_NAME = "spring.application.name"; + static final String FALLBACK_APPLICATION_NAME = "application"; + static final String PROPERTY_SOURCE_NAME_SEPARATOR = "."; + + private Constants() { + } +} diff --git a/spring-cloud-kubernetes-core/src/main/java/io/fabric8/spring/cloud/kubernetes/config/SecretsConfigProperties.java b/spring-cloud-kubernetes-core/src/main/java/io/fabric8/spring/cloud/kubernetes/config/SecretsConfigProperties.java new file mode 100644 index 00000000..5d5f6b55 --- /dev/null +++ b/spring-cloud-kubernetes-core/src/main/java/io/fabric8/spring/cloud/kubernetes/config/SecretsConfigProperties.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2016 to the 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 + * + * http://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.fabric8.spring.cloud.kubernetes.config; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties("spring.cloud.kubernetes.secrets") +public class SecretsConfigProperties { + + private boolean enabled = true; + private String name; + private String namespace; + private Map labels = new HashMap<>(); + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getNamespace() { + return namespace; + } + + public void setNamespace(String namespace) { + this.namespace = namespace; + } + + public Map getLabels() { + return labels; + } +} diff --git a/spring-cloud-kubernetes-core/src/main/java/io/fabric8/spring/cloud/kubernetes/config/SecretsPropertySource.java b/spring-cloud-kubernetes-core/src/main/java/io/fabric8/spring/cloud/kubernetes/config/SecretsPropertySource.java new file mode 100644 index 00000000..fa37dc51 --- /dev/null +++ b/spring-cloud-kubernetes-core/src/main/java/io/fabric8/spring/cloud/kubernetes/config/SecretsPropertySource.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2016 to the 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 + * + * http://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.fabric8.spring.cloud.kubernetes.config; + +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; + +import io.fabric8.kubernetes.api.model.Secret; +import io.fabric8.kubernetes.client.KubernetesClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.env.Environment; +import org.springframework.core.env.MapPropertySource; +import org.springframework.util.StringUtils; + +public class SecretsPropertySource extends MapPropertySource { + private static final Logger LOGGER = LoggerFactory.getLogger(SecretsPropertySource.class); + + private static final String PREFIX = "secrets"; + + public SecretsPropertySource(KubernetesClient client, Environment env, SecretsConfigProperties config) { + super( + getSourceName(client, env, config), + getSourceData(client, env, config) + ); + } + + private static String getSourceName(KubernetesClient client, Environment env, SecretsConfigProperties config) { + return new StringBuilder() + .append(PREFIX) + .append(Constants.PROPERTY_SOURCE_NAME_SEPARATOR) + .append(getApplicationName(env,config)) + .append(Constants.PROPERTY_SOURCE_NAME_SEPARATOR) + .append(getApplicationNamespace(client, config)) + .toString(); + } + + private static Map getSourceData(KubernetesClient client, Environment env, SecretsConfigProperties config) { + String name = getApplicationName(env, config); + String namespace = getApplicationNamespace(client, config); + + Map result = new HashMap<>(); + try { + if (config.getLabels().isEmpty()) { + if (StringUtils.isEmpty(namespace)) { + putAll( + client.secrets() + .withName(name) + .get(), + result); + } else { + putAll( + client.secrets() + .inNamespace(namespace) + .withName(name) + .get(), + result); + } + } else { + if (StringUtils.isEmpty(namespace)) { + client.secrets() + .withLabels(config.getLabels()) + .list() + .getItems() + .forEach(s -> putAll(s, result)); + } else { + client.secrets() + .inNamespace(namespace) + .withLabels(config.getLabels()) + .list() + .getItems() + .forEach(s -> putAll(s, result)); + + } + } + } catch (Exception e) { + LOGGER.warn("Can't read secret with name: [{}] or labels [{}] in namespace:[{}]. Ignoring", + config.getName(), + config.getLabels(), + namespace, + e); + } + return result; + } + + // ***************************** + // Helpers + // ***************************** + + private static String getApplicationName(Environment env, SecretsConfigProperties config) { + String name = config.getName(); + if (StringUtils.isEmpty(name)) { + name = env.getProperty( + Constants.SPRING_APPLICATION_NAME, + Constants.FALLBACK_APPLICATION_NAME); + } + + return name; + } + + private static String getApplicationNamespace(KubernetesClient client, SecretsConfigProperties config) { + String namespace = config.getNamespace(); + if (StringUtils.isEmpty(namespace)) { + namespace = client.getNamespace(); + } + + return namespace; + } + + private static void putAll(Secret secret, Map result) { + if (secret != null && secret.getData() != null) { + secret.getData().forEach((k, v) -> result.put( + k, + new String(Base64.getDecoder().decode(v)).trim()) + ); + } + } +} diff --git a/spring-cloud-kubernetes-core/src/main/java/io/fabric8/spring/cloud/kubernetes/config/SecretsPropertySourceLocator.java b/spring-cloud-kubernetes-core/src/main/java/io/fabric8/spring/cloud/kubernetes/config/SecretsPropertySourceLocator.java new file mode 100644 index 00000000..6da9d449 --- /dev/null +++ b/spring-cloud-kubernetes-core/src/main/java/io/fabric8/spring/cloud/kubernetes/config/SecretsPropertySourceLocator.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2016 to the 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 + * + * http://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.fabric8.spring.cloud.kubernetes.config; + +import io.fabric8.kubernetes.client.KubernetesClient; +import org.springframework.cloud.bootstrap.config.PropertySourceLocator; +import org.springframework.core.annotation.Order; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.Environment; +import org.springframework.core.env.PropertySource; + +@Order(1) +public class SecretsPropertySourceLocator implements PropertySourceLocator { + private final KubernetesClient client; + private final SecretsConfigProperties properties; + + public SecretsPropertySourceLocator(KubernetesClient client, SecretsConfigProperties properties) { + this.client = client; + this.properties = properties; + } + + @Override + public PropertySource locate(Environment environment) { + return environment instanceof ConfigurableEnvironment + ? new SecretsPropertySource(client, environment, properties) + : null; + } +} diff --git a/spring-cloud-kubernetes-core/src/main/resources/META-INF/spring.factories b/spring-cloud-kubernetes-core/src/main/resources/META-INF/spring.factories index d055546b..9db489c5 100644 --- a/spring-cloud-kubernetes-core/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-kubernetes-core/src/main/resources/META-INF/spring.factories @@ -2,7 +2,7 @@ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ io.fabric8.spring.cloud.kubernetes.KubernetesAutoConfiguration org.springframework.cloud.bootstrap.BootstrapConfiguration=\ -io.fabric8.spring.cloud.kubernetes.config.ConfigMapBootstrapConfiguration +io.fabric8.spring.cloud.kubernetes.config.BootstrapConfiguration org.springframework.context.ApplicationContextInitializer=\ io.fabric8.spring.cloud.kubernetes.profile.KubernetesApplicationContextInitializer diff --git a/spring-cloud-kubernetes-core/src/test/groovy/io/fabric8/spring/cloud/kubernetes/config/test/CoreTest.groovy b/spring-cloud-kubernetes-core/src/test/groovy/io/fabric8/spring/cloud/kubernetes/config/test/CoreTest.groovy index db06b3dc..97c622f2 100644 --- a/spring-cloud-kubernetes-core/src/test/groovy/io/fabric8/spring/cloud/kubernetes/config/test/CoreTest.groovy +++ b/spring-cloud-kubernetes-core/src/test/groovy/io/fabric8/spring/cloud/kubernetes/config/test/CoreTest.groovy @@ -17,6 +17,7 @@ package io.fabric8.spring.cloud.kubernetes.config.test +import io.fabric8.kubernetes.api.model.SecretBuilder import io.fabric8.kubernetes.api.model.ConfigMapBuilder import io.fabric8.kubernetes.client.Config import io.fabric8.kubernetes.client.KubernetesClient @@ -27,14 +28,15 @@ import org.springframework.boot.test.IntegrationTest import org.springframework.boot.test.SpringApplicationConfiguration import org.springframework.core.env.Environment import spock.lang.Specification +import groovy.util.logging.Slf4j +@Slf4j @SpringApplicationConfiguration(TestApplication.class) -@IntegrationTest( - [ - "spring.application.name=testapp", - "spring.cloud.kubernetes.client.namespace=testns", - "spring.cloud.kubernetes.client.trustCerts=true", - "spring.cloud.kubernetes.config.namespace=testns" +@IntegrationTest([ + "spring.application.name=testapp", + "spring.cloud.kubernetes.client.namespace=testns", + "spring.cloud.kubernetes.client.trustCerts=true", + "spring.cloud.kubernetes.config.namespace=testns" ]) @EnableConfigurationProperties class CoreTest extends Specification { @@ -55,12 +57,26 @@ class CoreTest extends Specification { mockServer.init() mockClient = mockServer.createClient() - //Setup configmap data - Map data = new HashMap<>(); - data.put("spring.kubernetes.test.value", "value1") - mockServer.expect().get().withPath("/api/v1/namespaces/testns/configmaps/testapp").andReturn(200, new ConfigMapBuilder() - .withData(data) - .build()).always() + mockServer.expect().get() + .withPath("/api/v1/namespaces/testns/configmaps/testapp") + .andReturn( + 200, + new ConfigMapBuilder() + .withData([ + 'spring.kubernetes.test.value': 'value1']) + .build()) + .always() + mockServer.expect().get() + .withPath("/api/v1/namespaces/testns/secrets/testapp") + .andReturn( + 200, + new SecretBuilder() + .withData([ + 'amq.pwd': 'MWYyZDFlMmU2N2Rm', + 'amq.usr': 'YWRtaW4K' + ]) + .build()) + .always() //Configure the kubernetes master url to point to the mock server System.setProperty(Config.KUBERNETES_MASTER_SYSTEM_PROPERTY, mockClient.getConfiguration().getMasterUrl()) @@ -80,8 +96,8 @@ class CoreTest extends Specification { def "Kubernetes client config bean should be configurable via system properties"() { expect: - config.getMasterUrl().equals(mockClient.getConfiguration().getMasterUrl()); - config.getNamespace().equals("testns"); + config.getMasterUrl().equals(mockClient.getConfiguration().getMasterUrl()) + config.getNamespace().equals("testns") config.trustCerts } @@ -92,12 +108,17 @@ class CoreTest extends Specification { def "Kubernetes client should be configured from system properties"() { expect: - client.getConfiguration().getMasterUrl().equals(mockClient.getConfiguration().getMasterUrl()); + client.getConfiguration().getMasterUrl().equals(mockClient.getConfiguration().getMasterUrl()) } - def "properties should be read from config map"() { expect: - environment.getProperty("spring.kubernetes.test.value").equals("value1"); + environment.getProperty("spring.kubernetes.test.value").equals("value1") + } + + def "properties should be read from secrets"() { + expect: + environment.getProperty("amq.pwd").equals("1f2d1e2e67df") + environment.getProperty("amq.usr").equals('admin'); } }