diff --git a/docs/src/main/asciidoc/spring-cloud-kubernetes-configserver.adoc b/docs/src/main/asciidoc/spring-cloud-kubernetes-configserver.adoc new file mode 100644 index 00000000..d15122c5 --- /dev/null +++ b/docs/src/main/asciidoc/spring-cloud-kubernetes-configserver.adoc @@ -0,0 +1,122 @@ +[#spring-cloud-kubernetes-configserver] +## Spring Cloud Kubernetes Config Server + +The Spring Cloud Kubernetes Config Server, is based on https://spring.io/projects/spring-cloud-config[Spring Cloud Config Server] and adds an https://docs.spring.io/spring-cloud-config/docs/current/reference/html/#_environment_repository[environment repository] for Kubernetes +https://kubernetes.io/docs/concepts/configuration/configmap/[Config Maps] and https://kubernetes.io/docs/concepts/configuration/secret/[Secrets]. + +This is component is completely optional. However, it allows you to continue to leverage configuration +you may have stored in existing environment repositories (Git, SVN, Vault, etc) with applications that you are running on Kubernetes. + +A default image is located on Docker Hub which will allow you to easily get a Config Server deployed on Kubernetes without building +the code and image yourself. However, if you need to customize the config server behavior you can easily build your own +image from the source code on GitHub and use that. + +### Configuration + +#### Enabling The Kubernetes Environment Repository +To enable the Kubernetes environment repository the `kubernetes` profile must be included in the list of active profiles. +You may activate other profiles as well to use other environment repository implementations. + +#### Config Map and Secret PropertySources +By default, only Config Map data will be fetched. To enable Secrets as well you will need to set `spring.cloud.kubernetes.secrets.enableApi=true`. +You can disable the Config Map `PropertySource` by setting `spring.cloud.kubernetes.config.enableApi=false`. + +#### Fetching Config Map and Secret Data From Additional Namespaces +By default, the Kubernetes environment repository will only fetch Config Map and Secrets from the namespace in which it is deployed. +If you want to include data from other namespaces you can set `spring.cloud.kubernetes.configserver.config-map-namespaces` and/or `spring.cloud.kubernetes.configserver.secrets-namespaces` to a comma separated +list of namespace values. + +NOTE: If you set `spring.cloud.kubernetes.configserver.config-map-namespaces` and/or `spring.cloud.kubernetes.configserver.secrets-namespaces` +you will need to include the namespace in which the Config Server is deployed in order to continue to fetch Config Map and Secret data from that namespace. + +#### Kubernetes Access Controls +The Kubernetes Config Server uses the Kubernetes API server to fetch Config Map and Secret data. In order for it to do that +it needs ability to `get` and `list` Config Map and Secrets (depending on what you enable/disable). + +### Deployment Yaml + +Below is a sample deployment, service and permissions configuration you can use to deploy a basic Config Server to Kubernetes. + +==== +[source,yaml] +---- +--- +apiVersion: v1 +kind: List +items: + - apiVersion: v1 + kind: Service + metadata: + labels: + app: spring-cloud-kubernetes-configserver + name: spring-cloud-kubernetes-configserver + spec: + ports: + - name: http + port: 8888 + targetPort: 8888 + selector: + app: spring-cloud-kubernetes-configserver + type: ClusterIP + - apiVersion: v1 + kind: ServiceAccount + metadata: + labels: + app: spring-cloud-kubernetes-configserver + name: spring-cloud-kubernetes-configserver + - apiVersion: rbac.authorization.k8s.io/v1 + kind: RoleBinding + metadata: + labels: + app: spring-cloud-kubernetes-configserver + name: spring-cloud-kubernetes-configserver:view + roleRef: + kind: Role + apiGroup: rbac.authorization.k8s.io + name: namespace-reader + subjects: + - kind: ServiceAccount + name: spring-cloud-kubernetes-configserver + - apiVersion: rbac.authorization.k8s.io/v1 + kind: Role + metadata: + namespace: default + name: namespace-reader + rules: + - apiGroups: ["", "extensions", "apps"] + resources: ["configmaps", "secrets"] + verbs: ["get", "list"] + - apiVersion: apps/v1 + kind: Deployment + metadata: + name: spring-cloud-kubernetes-configserver-deployment + spec: + selector: + matchLabels: + app: spring-cloud-kubernetes-configserver + template: + metadata: + labels: + app: spring-cloud-kubernetes-configserver + spec: + serviceAccount: spring-cloud-kubernetes-configserver + containers: + - name: spring-cloud-kubernetes-configserver + image: springcloud/spring-cloud-kubernetes-configserver + imagePullPolicy: IfNotPresent + env: + - name: SPRING_PROFILES_INCLUDE + value: "kubernetes" + readinessProbe: + httpGet: + port: 8888 + path: /actuator/health/readiness + livenessProbe: + httpGet: + port: 8888 + path: /actuator/health/liveness + ports: + - containerPort: 8888 + +---- +==== diff --git a/docs/src/main/asciidoc/spring-cloud-kubernetes.adoc b/docs/src/main/asciidoc/spring-cloud-kubernetes.adoc index b91696f7..ba6f7afe 100644 --- a/docs/src/main/asciidoc/spring-cloud-kubernetes.adoc +++ b/docs/src/main/asciidoc/spring-cloud-kubernetes.adoc @@ -31,6 +31,8 @@ include::service-registry.adoc[] include::spring-cloud-kubernetes-configuration-watcher.adoc[] +include::spring-cloud-kubernetes-configserver.adoc[] + include::examples.adoc[] include::other-resources.adoc[] diff --git a/scripts/deploy.sh b/scripts/deploy.sh index a5c61146..7b39b7eb 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -3,3 +3,4 @@ set -e ./mvnw deploy -DskipTests -B -Pfast,deploy ${@} ./mvnw dockerfile:push -pl :spring-cloud-kubernetes-configuration-watcher -Pdockerpush ${@} +./mvnw dockerfile:push -pl :spring-cloud-kubernetes-configserver -Pdockerpush ${@} diff --git a/spring-cloud-kubernetes-controllers/pom.xml b/spring-cloud-kubernetes-controllers/pom.xml index 9594985d..d6a87525 100644 --- a/spring-cloud-kubernetes-controllers/pom.xml +++ b/spring-cloud-kubernetes-controllers/pom.xml @@ -14,6 +14,7 @@ spring-cloud-kubernetes-configuration-watcher + spring-cloud-kubernetes-configserver diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/k8s/deployment.yaml b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/k8s/deployment.yaml new file mode 100644 index 00000000..563cf2b5 --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/k8s/deployment.yaml @@ -0,0 +1,79 @@ +--- +apiVersion: v1 +kind: List +items: + - apiVersion: v1 + kind: Service + metadata: + labels: + app: spring-cloud-kubernetes-configserver + name: spring-cloud-kubernetes-configserver + spec: + ports: + - name: http + port: 8888 + targetPort: 8888 + selector: + app: spring-cloud-kubernetes-configserver + type: LoadBalancer + - apiVersion: v1 + kind: ServiceAccount + metadata: + labels: + app: spring-cloud-kubernetes-configserver + name: spring-cloud-kubernetes-configserver + - apiVersion: rbac.authorization.k8s.io/v1 + kind: RoleBinding + metadata: + labels: + app: spring-cloud-kubernetes-configserver + name: spring-cloud-kubernetes-configserver:view + roleRef: + kind: Role + apiGroup: rbac.authorization.k8s.io + name: namespace-reader + subjects: + - kind: ServiceAccount + name: spring-cloud-kubernetes-configserver + - apiVersion: rbac.authorization.k8s.io/v1 + kind: Role + metadata: + namespace: default + name: namespace-reader + rules: + - apiGroups: ["", "extensions", "apps"] + resources: ["configmaps", "secrets"] + verbs: ["get", "list"] + - apiVersion: apps/v1 + kind: Deployment + metadata: + name: spring-cloud-kubernetes-configserver-deployment + spec: + selector: + matchLabels: + app: spring-cloud-kubernetes-configserver + template: + metadata: + labels: + app: spring-cloud-kubernetes-configserver + spec: + serviceAccount: spring-cloud-kubernetes-configserver + containers: + - name: spring-cloud-kubernetes-configserver + image: springcloud/spring-cloud-kubernetes-configserver:2.0.4-SNAPSHOT + imagePullPolicy: IfNotPresent + env: + - name: SPRING_PROFILES_INCLUDE + value: "kubernetes" + - name: SPRING_CLOUD_KUBERNETES_SECRETS_ENABLEAPI + value: "true" + readinessProbe: + httpGet: + port: 8888 + path: /actuator/health/readiness + livenessProbe: + httpGet: + port: 8888 + path: /actuator/health/liveness + ports: + - containerPort: 8888 diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/pom.xml b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/pom.xml new file mode 100644 index 00000000..5eca21df --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/pom.xml @@ -0,0 +1,145 @@ + + + + spring-cloud-kubernetes-controllers + org.springframework.cloud + 2.1.0-SNAPSHOT + + 4.0.0 + + spring-cloud-kubernetes-configserver + + + 1.8.0 + openjdk:8u222-slim + springcloud + 4.1.0 + + + + + org.springframework.cloud + spring-cloud-config-server + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.cloud + spring-cloud-starter-kubernetes-client-config + + + + org.springframework.boot + spring-boot-starter-test + test + + + com.github.tomakehurst + wiremock-jre8 + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + ${env.IMAGE} + + build-image + + + + package + + build-image + + + + + + + + + + dockerpush + + + + com.spotify + dockerfile-maven-plugin + 1.4.12 + + ${docker.registry.organization}/${artifactId} + ${project.version} + ${env.DOCKER_HUB_USERNAME} + ${env.DOCKER_HUB_PASSWORD} + + true + + + + + org.codehaus.plexus + plexus-archiver + ${plexus-archiver.version} + + + + + + + + imagename + + + !env.IMAGE + + + + springcloud/${project.artifactId}:${project.version} + + + + jib + + + + com.google.cloud.tools + jib-maven-plugin + ${jib.version} + + + ${base.image} + + + spring-cloud/${project.artifactId} + + + nobody:nogroup + + + + + + + package + + dockerBuild + + + + + + + + + + diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/skaffold.yaml b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/skaffold.yaml new file mode 100644 index 00000000..138b208c --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/skaffold.yaml @@ -0,0 +1,20 @@ +apiVersion: skaffold/v2alpha3 +kind: Config +metadata: + name: spring-cloud-kubernetes-configserver +build: + artifacts: + - image: springcloud/spring-cloud-kubernetes-configserver + custom: + buildCommand: "../../mvnw clean install" + dependencies: + paths: + - src + - pom.xml +# jib: { +# args: ["-Pjib"] +# } +deploy: + kubectl: + manifests: + - k8s/deployment.yaml diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/main/java/org/springframework/cloud/kubernetes/configserver/KubernetesConfigServerApplication.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/main/java/org/springframework/cloud/kubernetes/configserver/KubernetesConfigServerApplication.java new file mode 100644 index 00000000..e7b44f9a --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/main/java/org/springframework/cloud/kubernetes/configserver/KubernetesConfigServerApplication.java @@ -0,0 +1,35 @@ +/* + * Copyright 2013-2021 the original author or 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 org.springframework.cloud.kubernetes.configserver; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.cloud.config.server.ConfigServerApplication; +import org.springframework.cloud.config.server.EnableConfigServer; + +/** + * @author Ryan Baxter + */ +@SpringBootApplication +@EnableConfigServer +public class KubernetesConfigServerApplication { + + public static void main(String[] args) { + new SpringApplicationBuilder(ConfigServerApplication.class).run(args); + } + +} diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/main/java/org/springframework/cloud/kubernetes/configserver/KubernetesConfigServerAutoConfiguration.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/main/java/org/springframework/cloud/kubernetes/configserver/KubernetesConfigServerAutoConfiguration.java new file mode 100644 index 00000000..38f27ab7 --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/main/java/org/springframework/cloud/kubernetes/configserver/KubernetesConfigServerAutoConfiguration.java @@ -0,0 +1,91 @@ +/* + * Copyright 2013-2021 the original author or 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 org.springframework.cloud.kubernetes.configserver; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import io.kubernetes.client.openapi.apis.CoreV1Api; + +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.config.server.config.ConfigServerAutoConfiguration; +import org.springframework.cloud.config.server.environment.EnvironmentRepository; +import org.springframework.cloud.kubernetes.client.KubernetesClientAutoConfiguration; +import org.springframework.cloud.kubernetes.client.config.KubernetesClientConfigMapPropertySource; +import org.springframework.cloud.kubernetes.client.config.KubernetesClientSecretsPropertySource; +import org.springframework.cloud.kubernetes.commons.ConditionalOnKubernetesConfigEnabled; +import org.springframework.cloud.kubernetes.commons.ConditionalOnKubernetesEnabled; +import org.springframework.cloud.kubernetes.commons.ConditionalOnKubernetesSecretsEnabled; +import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.core.env.MapPropertySource; + +import static org.springframework.cloud.kubernetes.configserver.KubernetesPropertySourceSupplier.namespaceSplitter; + +/** + * @author Ryan Baxter + */ +@Configuration +@AutoConfigureAfter({ KubernetesClientAutoConfiguration.class }) +@AutoConfigureBefore({ ConfigServerAutoConfiguration.class }) +@ConditionalOnKubernetesEnabled +@EnableConfigurationProperties(KubernetesConfigServerProperties.class) +public class KubernetesConfigServerAutoConfiguration { + + @Bean + @Profile("kubernetes") + public EnvironmentRepository kubernetesEnvironmentRepository(CoreV1Api coreV1Api, + List kubernetesPropertySourceSuppliers, + KubernetesNamespaceProvider kubernetesNamespaceProvider) { + return new KubernetesEnvironmentRepository(coreV1Api, kubernetesPropertySourceSuppliers, + kubernetesNamespaceProvider.getNamespace()); + } + + @Bean + @ConditionalOnKubernetesConfigEnabled + @ConditionalOnProperty(value = "spring.cloud.kubernetes.config.enableApi", matchIfMissing = true) + public KubernetesPropertySourceSupplier configMapPropertySourceSupplier( + KubernetesConfigServerProperties properties) { + return (coreApi, applicationName, namespace, springEnv) -> { + List namespaces = namespaceSplitter(properties.getSecretsNamespaces(), namespace); + List propertySources = new ArrayList<>(); + namespaces.forEach(space -> propertySources + .add(new KubernetesClientConfigMapPropertySource(coreApi, applicationName, space, springEnv, ""))); + return propertySources; + }; + } + + @Bean + @ConditionalOnKubernetesSecretsEnabled + @ConditionalOnProperty("spring.cloud.kubernetes.secrets.enableApi") + public KubernetesPropertySourceSupplier secretsPropertySourceSupplier(KubernetesConfigServerProperties properties) { + return (coreApi, applicationName, namespace, springEnv) -> { + List namespaces = namespaceSplitter(properties.getSecretsNamespaces(), namespace); + List propertySources = new ArrayList<>(); + namespaces.forEach(space -> propertySources.add(new KubernetesClientSecretsPropertySource(coreApi, + applicationName, space, springEnv, new HashMap<>()))); + return propertySources; + }; + } + +} diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/main/java/org/springframework/cloud/kubernetes/configserver/KubernetesConfigServerProperties.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/main/java/org/springframework/cloud/kubernetes/configserver/KubernetesConfigServerProperties.java new file mode 100644 index 00000000..bbb88b81 --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/main/java/org/springframework/cloud/kubernetes/configserver/KubernetesConfigServerProperties.java @@ -0,0 +1,48 @@ +/* + * Copyright 2013-2021 the original author or 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 org.springframework.cloud.kubernetes.configserver; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * @author Ryan Baxter + */ + +@ConfigurationProperties("spring.cloud.kubernetes.configserver") +public class KubernetesConfigServerProperties { + + private String conigMapNamespaces = ""; + + private String secretsNamespaces = ""; + + public String getConigMapNamespaces() { + return conigMapNamespaces; + } + + public void setConigMapNamespaces(String conigMapNamespaces) { + this.conigMapNamespaces = conigMapNamespaces; + } + + public String getSecretsNamespaces() { + return secretsNamespaces; + } + + public void setSecretsNamespaces(String secretsNamespaces) { + this.secretsNamespaces = secretsNamespaces; + } + +} diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/main/java/org/springframework/cloud/kubernetes/configserver/KubernetesEnvironmentRepository.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/main/java/org/springframework/cloud/kubernetes/configserver/KubernetesEnvironmentRepository.java new file mode 100644 index 00000000..e4e7c3c2 --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/main/java/org/springframework/cloud/kubernetes/configserver/KubernetesEnvironmentRepository.java @@ -0,0 +1,94 @@ +/* + * Copyright 2013-2021 the original author or 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 org.springframework.cloud.kubernetes.configserver; + +import java.util.List; + +import io.kubernetes.client.openapi.apis.CoreV1Api; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.cloud.config.environment.Environment; +import org.springframework.cloud.config.environment.PropertySource; +import org.springframework.cloud.config.server.environment.EnvironmentRepository; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.util.StringUtils; + +/** + * @author Ryan Baxter + */ +public class KubernetesEnvironmentRepository implements EnvironmentRepository { + + private static final Log LOG = LogFactory.getLog(KubernetesEnvironmentRepository.class); + + private CoreV1Api coreApi; + + private List kubernetesPropertySourceSuppliers; + + private String namespace; + + public KubernetesEnvironmentRepository(CoreV1Api coreApi, + List kubernetesPropertySourceSuppliers, String namespace) { + this.coreApi = coreApi; + this.kubernetesPropertySourceSuppliers = kubernetesPropertySourceSuppliers; + this.namespace = namespace; + } + + @Override + public Environment findOne(String application, String profile, String label) { + return findOne(application, profile, label, true); + } + + @Override + public Environment findOne(String application, String profile, String label, boolean includeOrigin) { + String[] profiles = StringUtils.commaDelimitedListToStringArray(profile); + LOG.info("Profiles: " + profile); + LOG.info("Application: " + application); + LOG.info("Label: " + label); + Environment environment = new Environment(application, profiles, label, null, null); + try { + StandardEnvironment springEnv = new StandardEnvironment(); + springEnv.setActiveProfiles(profiles); + addApplicationConfiguration(environment, springEnv, "application"); + if (!"application".equalsIgnoreCase(application)) { + addApplicationConfiguration(environment, springEnv, application); + } + return environment; + } + catch (Exception e) { + LOG.warn(e); + } + return environment; + } + + private void addApplicationConfiguration(Environment environment, StandardEnvironment springEnv, + String applicationName) { + kubernetesPropertySourceSuppliers.stream().forEach(supplier -> { + List propertySources = supplier.get(coreApi, applicationName, namespace, springEnv); + propertySources.forEach(propertySource -> { + if (propertySource.getPropertyNames().length > 0) { + LOG.debug("Adding PropertySource " + propertySource.getName()); + LOG.debug("PropertySource Names: " + + StringUtils.arrayToCommaDelimitedString(propertySource.getPropertyNames())); + environment.add(new PropertySource(propertySource.getName(), propertySource.getSource())); + } + }); + }); + } + +} diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/main/java/org/springframework/cloud/kubernetes/configserver/KubernetesPropertySourceSupplier.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/main/java/org/springframework/cloud/kubernetes/configserver/KubernetesPropertySourceSupplier.java new file mode 100644 index 00000000..a73af430 --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/main/java/org/springframework/cloud/kubernetes/configserver/KubernetesPropertySourceSupplier.java @@ -0,0 +1,45 @@ +/* + * Copyright 2013-2021 the original author or 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 org.springframework.cloud.kubernetes.configserver; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import io.kubernetes.client.openapi.apis.CoreV1Api; + +import org.springframework.core.env.Environment; +import org.springframework.core.env.MapPropertySource; +import org.springframework.util.StringUtils; + +/** + * @author Ryan Baxter + */ +public interface KubernetesPropertySourceSupplier { + + List get(CoreV1Api coreV1Api, String name, String namespace, Environment environment); + + static List namespaceSplitter(String namespacesString, String currentNamespace) { + List namespaces = Collections.singletonList(currentNamespace); + String[] namespacesArray = StringUtils.commaDelimitedListToStringArray(namespacesString); + if (namespacesArray.length > 0) { + namespaces = Arrays.asList(namespacesArray); + } + return namespaces; + } + +} diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/main/resources/META-INF/spring.factories b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..453ef580 --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.springframework.cloud.kubernetes.configserver.KubernetesConfigServerAutoConfiguration + diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/main/resources/application.yaml b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/main/resources/application.yaml new file mode 100644 index 00000000..aed3a8c7 --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/main/resources/application.yaml @@ -0,0 +1,6 @@ +spring: + application: + name: kubernetesconfigserver + +server: + port: 8888 diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/ConfigServerIntegrationTest.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/ConfigServerIntegrationTest.java new file mode 100644 index 00000000..690f9b13 --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/ConfigServerIntegrationTest.java @@ -0,0 +1,88 @@ +/* + * Copyright 2013-2021 the original author or 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 org.springframework.cloud.kubernetes.configserver; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.WireMock; +import io.kubernetes.client.openapi.JSON; +import io.kubernetes.client.openapi.models.V1ConfigMapBuilder; +import io.kubernetes.client.openapi.models.V1ConfigMapList; +import io.kubernetes.client.openapi.models.V1ListMetaBuilder; +import io.kubernetes.client.openapi.models.V1ObjectMetaBuilder; +import io.kubernetes.client.openapi.models.V1SecretBuilder; +import io.kubernetes.client.openapi.models.V1SecretList; +import io.kubernetes.client.openapi.models.V1SecretListBuilder; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.cloud.config.environment.Environment; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Ryan Baxter + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = { "spring.cloud.kubernetes.client.namespace=default", "spring.profiles.include=kubernetes", + "spring.cloud.kubernetes.secrets.enableApi=true", "debug=true" }, + classes = { KubernetesConfigServerApplication.class }) +public class ConfigServerIntegrationTest { + + @Autowired + private TestRestTemplate testRestTemplate; + + public static WireMockServer wireMockServer; + + @BeforeEach + public void beforeEach() { + V1ConfigMapList TEST_CONFIGMAP = new V1ConfigMapList().addItemsItem(new V1ConfigMapBuilder().withMetadata( + new V1ObjectMetaBuilder().withName("test-cm").withNamespace("default").withResourceVersion("1").build()) + .addToData("app.name", "test").build()); + + V1SecretList TEST_SECRET = new V1SecretListBuilder() + .withMetadata(new V1ListMetaBuilder().withResourceVersion("1").build()) + .addToItems(new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("test-cm").withResourceVersion("0") + .withNamespace("default").build()) + .addToData("password", "p455w0rd".getBytes()).addToData("username", "user".getBytes()).build()) + .build(); + + WireMock.stubFor(get(urlMatching("^/api/v1/namespaces/default/configmaps.*")) + .willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(TEST_CONFIGMAP)))); + + WireMock.stubFor(get(urlMatching("^/api/v1/namespaces/default/secrets.*")) + .willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(TEST_SECRET)))); + } + + @Test + public void enabled() throws Exception { + Environment env = testRestTemplate.getForObject("/test-cm/default", Environment.class); + assertThat(env.getPropertySources().size()).isEqualTo(2); + assertThat(env.getPropertySources().get(0).getName().equals("configmap.test-cm.default")).isTrue(); + assertThat(env.getPropertySources().get(0).getSource().get("app.name")).isEqualTo("test"); + assertThat(env.getPropertySources().get(1).getName().equals("secrets.test-cm.default")).isTrue(); + assertThat(env.getPropertySources().get(1).getSource().get("password")).isEqualTo("p455w0rd"); + assertThat(env.getPropertySources().get(1).getSource().get("username")).isEqualTo("user"); + } + +} diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/KubernetesConfigServerAutoConfigurationTests.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/KubernetesConfigServerAutoConfigurationTests.java new file mode 100644 index 00000000..0ae3aaa7 --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/KubernetesConfigServerAutoConfigurationTests.java @@ -0,0 +1,174 @@ +/* + * Copyright 2013-2021 the original author or 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 org.springframework.cloud.kubernetes.configserver; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.config.server.environment.EnvironmentRepository; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * @author Ryan Baxter + */ +public class KubernetesConfigServerAutoConfigurationTests { + + @Configuration + static class MockConfig { + + @Bean + @Profile("kubernetesdisabled") + public EnvironmentRepository environmentRepository() { + return mock(EnvironmentRepository.class); + } + + } + + @Nested + @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = { "spring.profiles.include=kubernetes,kubernetesdisabled", + "spring.cloud.kubernetes.enabled=false", "debug=true" }, + classes = { KubernetesConfigServerApplication.class, MockConfig.class }) + public class KubernetesDisabled { + + @Autowired + ConfigurableApplicationContext context; + + @Test + void runTest() { + assertThat(context.getBeanNamesForType(KubernetesEnvironmentRepository.class)).hasSize(0); + } + + } + + @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + classes = { KubernetesConfigServerApplication.class, MockConfig.class }, + properties = { "spring.cloud.kubernetes.enabled=false", + "spring.profiles.include=kubernetes,kubernetesdisabled", "debug=true" }) + @Nested + class KubernetesProfileMissing { + + @Autowired + ConfigurableApplicationContext context; + + @Test + void runTest() { + assertThat(context.getBeanNamesForType(KubernetesEnvironmentRepository.class)).hasSize(0); + } + + } + + @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + classes = { KubernetesConfigServerApplication.class }, properties = { "spring.profiles.include=kubernetes", + "debug=true", "spring.cloud.kubernetes.client.namespace=default" }) + @Nested + class KubernetesEnabledProfileIncluded { + + @Autowired + ConfigurableApplicationContext context; + + @Test + void runTest() { + assertThat(context.getBeanNamesForType(KubernetesEnvironmentRepository.class)).hasSize(1); + assertThat(context.getBeanNamesForType(KubernetesPropertySourceSupplier.class)).hasSize(1); + } + + } + + @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + classes = { KubernetesConfigServerApplication.class }, properties = { "spring.profiles.include=kubernetes", + "debug=true", "spring.cloud.kubernetes.config.enabled=false" }) + @Nested + class KubernetesEnabledProfileIncludedConfigMapDisabled { + + @Autowired + ConfigurableApplicationContext context; + + @Test + void runTest() { + assertThat(context.getBeanNamesForType(KubernetesEnvironmentRepository.class)).hasSize(1); + assertThat(context.getBeanNamesForType(KubernetesPropertySourceSupplier.class)).hasSize(0); + } + + } + + @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + classes = { KubernetesConfigServerApplication.class }, properties = { "spring.profiles.include=kubernetes", + "debug=true", "spring.cloud.kubernetes.client.namespace=default" }) + @Nested + class KubernetesEnabledProfileIncludedSecretsApiDisabled { + + @Autowired + ConfigurableApplicationContext context; + + @Test + void runTest() { + assertThat(context.getBeanNamesForType(KubernetesEnvironmentRepository.class)).hasSize(1); + assertThat(context.getBeanNamesForType(KubernetesPropertySourceSupplier.class)).hasSize(1); + assertThat(context.getBeanNamesForType(KubernetesPropertySourceSupplier.class)[0]) + .isEqualTo("configMapPropertySourceSupplier"); + } + + } + + @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + classes = { KubernetesConfigServerApplication.class }, + properties = { "spring.profiles.include=kubernetes", "debug=true", + "spring.cloud.kubernetes.client.namespace=default", + "spring.cloud.kubernetes.secrets.enableApi=true" }) + @Nested + class KubernetesEnabledProfileIncludedSecretsApiEnabled { + + @Autowired + ConfigurableApplicationContext context; + + @Test + void runTest() { + assertThat(context.getBeanNamesForType(KubernetesEnvironmentRepository.class)).hasSize(1); + assertThat(context.getBeanNamesForType(KubernetesPropertySourceSupplier.class)).hasSize(2); + } + + } + + @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + classes = { KubernetesConfigServerApplication.class }, + properties = { "spring.profiles.include=kubernetes", "debug=true", + "spring.cloud.kubernetes.client.namespace=default", + "spring.cloud.kubernetes.config.enableApi=false" }) + @Nested + class KubernetesEnabledProfileIncludedConfigApiDisabled { + + @Autowired + ConfigurableApplicationContext context; + + @Test + void runTest() { + assertThat(context.getBeanNamesForType(KubernetesEnvironmentRepository.class)).hasSize(1); + assertThat(context.getBeanNamesForType(KubernetesPropertySourceSupplier.class)).hasSize(0); + } + + } + +} diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/KubernetesEnvironmentRepositoryTests.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/KubernetesEnvironmentRepositoryTests.java new file mode 100644 index 00000000..59b0bdf5 --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/KubernetesEnvironmentRepositoryTests.java @@ -0,0 +1,247 @@ +/* + * Copyright 2013-2021 the original author or 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 org.springframework.cloud.kubernetes.configserver; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import io.kubernetes.client.openapi.ApiException; +import io.kubernetes.client.openapi.apis.CoreV1Api; +import io.kubernetes.client.openapi.models.V1ConfigMapBuilder; +import io.kubernetes.client.openapi.models.V1ConfigMapList; +import io.kubernetes.client.openapi.models.V1ObjectMetaBuilder; +import io.kubernetes.client.openapi.models.V1SecretBuilder; +import io.kubernetes.client.openapi.models.V1SecretList; +import io.kubernetes.client.openapi.models.V1SecretListBuilder; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import org.springframework.cloud.config.environment.Environment; +import org.springframework.cloud.kubernetes.client.config.KubernetesClientConfigMapPropertySource; +import org.springframework.cloud.kubernetes.client.config.KubernetesClientSecretsPropertySource; +import org.springframework.core.env.MapPropertySource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * @author Ryan Baxter + */ +class KubernetesEnvironmentRepositoryTests { + + private static List kubernetesPropertySourceSuppliers = new ArrayList<>(); + + private static final V1ConfigMapList CONFIGMAP_DEFAULT_LIST = new V1ConfigMapList() + .addItemsItem(new V1ConfigMapBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("application").withNamespace("default") + .withResourceVersion("1").build()) + .addToData("application.yaml", + "dummy:\n property:\n string2: \"a\"\n int2: 1\n bool2: true\n") + .build()) + .addItemsItem(new V1ConfigMapBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("stores").withNamespace("default") + .withResourceVersion("1").build()) + .addToData("application.yaml", + "dummy:\n property:\n string2: \"a\"\n int2: 1\n bool2: true\n") + .build()) + .addItemsItem(new V1ConfigMapBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("stores-dev").withNamespace("default") + .withResourceVersion("1").build()) + .addToData("application.yaml", + "dummy:\n property:\n string1: \"a\"\n string2: \"b\"\n int2: 2\n bool2: false\n") + .build()); + + private static final V1ConfigMapList CONFIGMAP_DEV_LIST = new V1ConfigMapList() + .addItemsItem(new V1ConfigMapBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("stores").withNamespace("dev") + .withResourceVersion("1").build()) + .addToData("application.yaml", + "dummy:\n property:\n string2: \"dev\"\n int2: 1\n bool2: true\n") + .build()); + + private static final V1SecretList SECRET_LIST = new V1SecretListBuilder() + .addToItems(new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("application").withResourceVersion("0") + .withNamespace("default").build()) + .addToData("password", "p455w0rd".getBytes()).addToData("username", "user".getBytes()).build()) + .addToItems(new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("stores").withResourceVersion("0") + .withNamespace("default").build()) + .addToData("password", "p455w0rd".getBytes()).addToData("username", "stores".getBytes()).build()) + .addToItems(new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("stores-dev").withResourceVersion("0") + .withNamespace("default").build()) + .addToData("password", "p455w0rd".getBytes()).addToData("username", "stores-dev".getBytes()) + .build()) + .build(); + + @BeforeAll + public static void before() { + kubernetesPropertySourceSuppliers.add((coreApi, applicationName, namespace, springEnv) -> { + List propertySources = new ArrayList<>(); + propertySources.add( + new KubernetesClientConfigMapPropertySource(coreApi, applicationName, "default", springEnv, "")); + propertySources + .add(new KubernetesClientConfigMapPropertySource(coreApi, applicationName, "dev", springEnv, "")); + return propertySources; + }); + kubernetesPropertySourceSuppliers.add((coreApi, applicationName, namespace, springEnv) -> { + List propertySources = new ArrayList<>(); + propertySources.add(new KubernetesClientSecretsPropertySource(coreApi, applicationName, "default", + springEnv, new HashMap<>())); + return propertySources; + }); + } + + @Test + public void testApplicationCase() throws ApiException { + CoreV1Api coreApi = mock(CoreV1Api.class); + when(coreApi.listNamespacedConfigMap(eq("default"), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), + eq(null), eq(null), eq(null), eq(null))).thenReturn(CONFIGMAP_DEFAULT_LIST); + when(coreApi.listNamespacedSecret(eq("default"), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), + eq(null), eq(null), eq(null), eq(null))).thenReturn(SECRET_LIST); + when(coreApi.listNamespacedConfigMap(eq("dev"), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), + eq(null), eq(null), eq(null), eq(null))).thenReturn(CONFIGMAP_DEV_LIST); + KubernetesEnvironmentRepository environmentRepository = new KubernetesEnvironmentRepository(coreApi, + kubernetesPropertySourceSuppliers, "default"); + Environment environment = environmentRepository.findOne("application", "", ""); + assertThat(environment.getPropertySources().size()).isEqualTo(2); + environment.getPropertySources().forEach(propertySource -> { + assertThat(propertySource.getName().equals("configmap.application.default") + || propertySource.getName().equals("secrets.application.default")).isTrue(); + if (propertySource.getName().equals("configmap.application.default")) { + assertThat(propertySource.getSource().size()).isEqualTo(3); + assertThat(propertySource.getSource().get("dummy.property.int2")).isEqualTo(1); + assertThat(propertySource.getSource().get("dummy.property.bool2")).isEqualTo(true); + assertThat(propertySource.getSource().get("dummy.property.string2")).isEqualTo("a"); + } + if (propertySource.getName().equals("secrets.application.default")) { + assertThat(propertySource.getSource().size()).isEqualTo(2); + assertThat(propertySource.getSource().get("username")).isEqualTo("user"); + assertThat(propertySource.getSource().get("password")).isEqualTo("p455w0rd"); + } + }); + } + + @Test + public void testStoresCase() throws ApiException { + CoreV1Api coreApi = mock(CoreV1Api.class); + when(coreApi.listNamespacedConfigMap(eq("default"), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), + eq(null), eq(null), eq(null), eq(null))).thenReturn(CONFIGMAP_DEFAULT_LIST); + when(coreApi.listNamespacedConfigMap(eq("dev"), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), + eq(null), eq(null), eq(null), eq(null))).thenReturn(CONFIGMAP_DEV_LIST); + when(coreApi.listNamespacedSecret(eq("default"), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), + eq(null), eq(null), eq(null), eq(null))).thenReturn(SECRET_LIST); + KubernetesEnvironmentRepository environmentRepository = new KubernetesEnvironmentRepository(coreApi, + kubernetesPropertySourceSuppliers, "default"); + Environment environment = environmentRepository.findOne("stores", "", ""); + assertThat(environment.getPropertySources().size()).isEqualTo(5); + environment.getPropertySources().forEach(propertySource -> { + assertThat(propertySource.getName().equals("configmap.application.default") + || propertySource.getName().equals("secrets.application.default") + || propertySource.getName().equals("configmap.stores.default") + || propertySource.getName().equals("configmap.stores.dev") + || propertySource.getName().equals("secrets.stores.default")).isTrue(); + if (propertySource.getName().equals("configmap.application.default")) { + assertThat(propertySource.getSource().size()).isEqualTo(3); + assertThat(propertySource.getSource().get("dummy.property.int2")).isEqualTo(1); + assertThat(propertySource.getSource().get("dummy.property.bool2")).isEqualTo(true); + assertThat(propertySource.getSource().get("dummy.property.string2")).isEqualTo("a"); + } + if (propertySource.getName().equals("secrets.application.default")) { + assertThat(propertySource.getSource().size()).isEqualTo(2); + assertThat(propertySource.getSource().get("username")).isEqualTo("user"); + assertThat(propertySource.getSource().get("password")).isEqualTo("p455w0rd"); + } + if (propertySource.getName().equals("configmap.stores.default")) { + assertThat(propertySource.getSource().size()).isEqualTo(3); + assertThat(propertySource.getSource().get("dummy.property.int2")).isEqualTo(1); + assertThat(propertySource.getSource().get("dummy.property.bool2")).isEqualTo(true); + assertThat(propertySource.getSource().get("dummy.property.string2")).isEqualTo("a"); + } + if (propertySource.getName().equals("configmap.stores.dev")) { + assertThat(propertySource.getSource().size()).isEqualTo(3); + assertThat(propertySource.getSource().get("dummy.property.int2")).isEqualTo(1); + assertThat(propertySource.getSource().get("dummy.property.bool2")).isEqualTo(true); + assertThat(propertySource.getSource().get("dummy.property.string2")).isEqualTo("dev"); + } + if (propertySource.getName().equals("secrets.stores.default")) { + assertThat(propertySource.getSource().size()).isEqualTo(2); + assertThat(propertySource.getSource().get("username")).isEqualTo("stores"); + assertThat(propertySource.getSource().get("password")).isEqualTo("p455w0rd"); + } + }); + } + + @Test + public void testStoresProfileCase() throws ApiException { + CoreV1Api coreApi = mock(CoreV1Api.class); + when(coreApi.listNamespacedConfigMap(eq("default"), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), + eq(null), eq(null), eq(null), eq(null))).thenReturn(CONFIGMAP_DEFAULT_LIST); + when(coreApi.listNamespacedSecret(eq("default"), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), + eq(null), eq(null), eq(null), eq(null))).thenReturn(SECRET_LIST); + when(coreApi.listNamespacedConfigMap(eq("dev"), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), + eq(null), eq(null), eq(null), eq(null))).thenReturn(CONFIGMAP_DEV_LIST); + KubernetesEnvironmentRepository environmentRepository = new KubernetesEnvironmentRepository(coreApi, + kubernetesPropertySourceSuppliers, "default"); + Environment environment = environmentRepository.findOne("stores", "dev", ""); + assertThat(environment.getPropertySources().size()).isEqualTo(5); + environment.getPropertySources().forEach(propertySource -> { + assertThat(propertySource.getName().equals("configmap.application.default") + || propertySource.getName().equals("secrets.application.default") + || propertySource.getName().equals("configmap.stores.default") + || propertySource.getName().equals("configmap.stores.dev") + || propertySource.getName().equals("secrets.stores.default")).isTrue(); + if (propertySource.getName().equals("configmap.application.default")) { + assertThat(propertySource.getSource().size()).isEqualTo(3); + assertThat(propertySource.getSource().get("dummy.property.int2")).isEqualTo(1); + assertThat(propertySource.getSource().get("dummy.property.bool2")).isEqualTo(true); + assertThat(propertySource.getSource().get("dummy.property.string2")).isEqualTo("a"); + } + if (propertySource.getName().equals("secrets.application.default")) { + assertThat(propertySource.getSource().size()).isEqualTo(2); + assertThat(propertySource.getSource().get("username")).isEqualTo("user"); + assertThat(propertySource.getSource().get("password")).isEqualTo("p455w0rd"); + } + if (propertySource.getName().equals("configmap.stores.default")) { + assertThat(propertySource.getSource().size()).isEqualTo(4); + assertThat(propertySource.getSource().get("dummy.property.int2")).isEqualTo(2); + assertThat(propertySource.getSource().get("dummy.property.bool2")).isEqualTo(false); + assertThat(propertySource.getSource().get("dummy.property.string2")).isEqualTo("b"); + assertThat(propertySource.getSource().get("dummy.property.string1")).isEqualTo("a"); + } + if (propertySource.getName().equals("configmap.stores.dev")) { + assertThat(propertySource.getSource().size()).isEqualTo(3); + assertThat(propertySource.getSource().get("dummy.property.int2")).isEqualTo(1); + assertThat(propertySource.getSource().get("dummy.property.bool2")).isEqualTo(true); + assertThat(propertySource.getSource().get("dummy.property.string2")).isEqualTo("dev"); + } + // Currently KubernetesClientSecretsPropertySource does not take into account + // profiles, so that plays no role at the moment + // See https://github.com/spring-cloud/spring-cloud-kubernetes/issues/880 + if (propertySource.getName().equals("secrets.stores.default")) { + assertThat(propertySource.getSource().size()).isEqualTo(2); + assertThat(propertySource.getSource().get("username")).isEqualTo("stores"); + assertThat(propertySource.getSource().get("password")).isEqualTo("p455w0rd"); + } + }); + } + +} diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/TestBootstrapConfig.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/TestBootstrapConfig.java new file mode 100644 index 00000000..b93a81ed --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/TestBootstrapConfig.java @@ -0,0 +1,54 @@ +/* + * Copyright 2013-2021 the original author or 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 org.springframework.cloud.kubernetes.configserver; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.WireMock; +import io.kubernetes.client.openapi.ApiClient; +import io.kubernetes.client.openapi.apis.CoreV1Api; +import io.kubernetes.client.util.ClientBuilder; + +import org.springframework.context.annotation.Bean; + +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; + +/** + * @author Ryan Baxter + */ +public class TestBootstrapConfig { + + @Bean + WireMockServer wireMockServer() { + WireMockServer wireMockServer = new WireMockServer(options().dynamicPort()); + wireMockServer.start(); + WireMock.configureFor(wireMockServer.port()); + return wireMockServer; + } + + @Bean + ApiClient apiClient(WireMockServer wireMockServer) { + ApiClient apiClient = new ClientBuilder().setBasePath(wireMockServer.baseUrl()).build(); + apiClient.setDebugging(true); + return apiClient; + } + + @Bean + CoreV1Api coreApi(ApiClient apiClient) { + return new CoreV1Api(apiClient); + } + +} diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/resources/META-INF/spring.factories b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/resources/META-INF/spring.factories new file mode 100644 index 00000000..86565476 --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/resources/META-INF/spring.factories @@ -0,0 +1,4 @@ +org.springframework.cloud.bootstrap.BootstrapConfiguration=\ +org.springframework.cloud.kubernetes.configserver.TestBootstrapConfig +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.springframework.cloud.kubernetes.configserver.KubernetesConfigServerAutoConfiguration diff --git a/src/checkstyle/checkstyle-suppressions.xml b/src/checkstyle/checkstyle-suppressions.xml index b7714c41..d2aa4f0d 100644 --- a/src/checkstyle/checkstyle-suppressions.xml +++ b/src/checkstyle/checkstyle-suppressions.xml @@ -13,4 +13,5 @@ +