Add suport for Secrets based PropertySource

This commit is contained in:
lburgazzoli
2016-09-26 17:33:47 +02:00
committed by Ioannis Canellos
parent 064af9822b
commit a3c2e54311
10 changed files with 331 additions and 44 deletions

View File

@@ -66,31 +66,33 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.fabric8</groupId>
<artifactId>kubernetes-client</artifactId>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.fabric8</groupId>
<artifactId>mockwebserver</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-spring</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>

View File

@@ -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);
}
}
}

View File

@@ -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<String, String> getData(KubernetesClient client, String name, String namespace) {

View File

@@ -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);

View File

@@ -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() {
}
}

View File

@@ -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<String, String> 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<String, String> getLabels() {
return labels;
}
}

View File

@@ -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<String, Object> getSourceData(KubernetesClient client, Environment env, SecretsConfigProperties config) {
String name = getApplicationName(env, config);
String namespace = getApplicationNamespace(client, config);
Map<String, Object> 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<String, Object> result) {
if (secret != null && secret.getData() != null) {
secret.getData().forEach((k, v) -> result.put(
k,
new String(Base64.getDecoder().decode(v)).trim())
);
}
}
}

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -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<String, String> 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');
}
}