Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support config yaml files embedded in env vars via spring.config.import #43657

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions spring-boot-project/spring-boot/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ dependencies {
testImplementation("com.microsoft.sqlserver:mssql-jdbc")
testImplementation("com.mysql:mysql-connector-j")
testImplementation("com.sun.xml.messaging.saaj:saaj-impl")
// TODO: Define strategy for mocking env vars and if/which 3pp to use
testImplementation("uk.org.webcompere:system-stubs-core:2.1.7")
testImplementation("io.projectreactor:reactor-test")
testImplementation("io.r2dbc:r2dbc-h2")
testImplementation("jakarta.inject:jakarta.inject-api")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.ProtocolResolver;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.support.SpringFactoriesLoader;

/**
* {@link EnvironmentPostProcessor} that loads and applies {@link ConfigData} to Spring's
Expand Down Expand Up @@ -92,7 +94,13 @@ public void postProcessEnvironment(ConfigurableEnvironment environment, SpringAp
void postProcessEnvironment(ConfigurableEnvironment environment, ResourceLoader resourceLoader,
Collection<String> additionalProfiles) {
this.logger.trace("Post-processing environment to add config data");
resourceLoader = (resourceLoader != null) ? resourceLoader : new DefaultResourceLoader();
if (resourceLoader == null) {
DefaultResourceLoader defaultResourceLoader = new DefaultResourceLoader();
SpringFactoriesLoader.forDefaultResourceLocation(defaultResourceLoader.getClassLoader())
.load(ProtocolResolver.class)
.forEach(defaultResourceLoader::addProtocolResolver);
resourceLoader = defaultResourceLoader;
}
getConfigDataEnvironment(environment, resourceLoader, additionalProfiles).processAndApply();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ boolean isPattern(String location) {
Resource getResource(String location) {
validateNonPattern(location);
location = StringUtils.cleanPath(location);
if (!ResourceUtils.isUrl(location)) {
if (!ResourceUtils.isUrl(location) && !location.contains(":")) {
location = ResourceUtils.FILE_URL_PREFIX + location;
}
return this.resourceLoader.getResource(location);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

package org.springframework.boot.context.config;

import java.util.Locale;

import org.springframework.boot.env.PropertySourceLoader;
import org.springframework.util.StringUtils;

Expand Down Expand Up @@ -51,7 +53,8 @@ class StandardConfigDataReference {
StandardConfigDataReference(ConfigDataLocation configDataLocation, String directory, String root, String profile,
String extension, PropertySourceLoader propertySourceLoader) {
this.configDataLocation = configDataLocation;
String profileSuffix = (StringUtils.hasText(profile)) ? "-" + profile : "";
String profileSuffix = (StringUtils.hasText(profile))
? root.startsWith("env:") ? "_" + profile.toUpperCase(Locale.ROOT) : "-" + profile : "";
this.resourceLocation = root + profileSuffix + ((extension != null) ? "." + extension : "");
this.directory = directory;
this.profile = profile;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright 2012-2025 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.boot.io;

import org.springframework.core.io.ProtocolResolver;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;

/**
* {@link ProtocolResolver} for resources contained in environment variables.
*
* @author Francisco Bento
*/
class EnvironmentVariableProtocolResolver implements ProtocolResolver {

@Override
public Resource resolve(String location, ResourceLoader resourceLoader) {
return EnvironmentVariableResource.fromUri(location);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* Copyright 2012-2025 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.boot.io;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

import org.springframework.core.io.AbstractResource;
import org.springframework.core.io.Resource;

/**
* {@link Resource} implementation for system environment variables.
*
* @author Francisco Bento
* @since 3.5.0
*/
public class EnvironmentVariableResource extends AbstractResource {

/** Pseudo URL prefix for loading from an environment variable: "env:". */
public static final String PSEUDO_URL_PREFIX = "env:";

/** Pseudo URL prefix indicating that the environment variable is base64-encoded. */
public static final String BASE64_ENCODED_PREFIX = "base64:";

private final String envVar;

private final boolean isBase64;

public EnvironmentVariableResource(final String envVar, final boolean isBase64) {
this.envVar = envVar;
this.isBase64 = isBase64;
}

public static EnvironmentVariableResource fromUri(String url) {
if (url.startsWith(PSEUDO_URL_PREFIX)) {
String envVar = url.substring(PSEUDO_URL_PREFIX.length());
boolean isBase64 = false;
if (envVar.startsWith(BASE64_ENCODED_PREFIX)) {
envVar = envVar.substring(BASE64_ENCODED_PREFIX.length());
isBase64 = true;
}
return new EnvironmentVariableResource(envVar, isBase64);
}
return null;
}

@Override
public boolean exists() {
return System.getenv(this.envVar) != null;
}

@Override
public String getDescription() {
return "Environment variable '" + this.envVar + "'";
}

@Override
public InputStream getInputStream() throws IOException {
return new ByteArrayInputStream(getContents());
}

protected byte[] getContents() {
String value = System.getenv(this.envVar);
if (this.isBase64) {
return Base64.getDecoder().decode(value);
}
return value.getBytes(StandardCharsets.UTF_8);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,5 @@ org.springframework.boot.orm.jpa.JpaDependsOnDatabaseInitializationDetector

# Resource Locator Protocol Resolvers
org.springframework.core.io.ProtocolResolver=\
org.springframework.boot.io.Base64ProtocolResolver
org.springframework.boot.io.Base64ProtocolResolver,\
org.springframework.boot.io.EnvironmentVariableProtocolResolver
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
Expand Down Expand Up @@ -621,6 +623,62 @@ void runWhenImportWithProfileVariantOrdersPropertySourcesCorrectly() {
.isEqualTo("application-import-with-profile-variant-imported-dev");
}

@Test
void runWhenImportYamlFromEnvironmentVariable() throws Exception {
ConfigurableApplicationContext context = uk.org.webcompere.systemstubs.SystemStubs
.withEnvironmentVariable("MY_CONFIG_YAML", """
my:
value: from-env-first-doc
---
my:
value: from-env-second-doc
""")
.execute(() -> this.application
.run("--spring.config.location=classpath:application-import-yaml-from-environment.properties"));
assertThat(context.getEnvironment().getProperty("my.value")).isEqualTo("from-env-second-doc");
}

@Test
void runWhenImportYamlFromEnvironmentVariableWithProfileVariant() throws Exception {
this.application.setAdditionalProfiles("dev");
ConfigurableApplicationContext context = uk.org.webcompere.systemstubs.SystemStubs
.withEnvironmentVariables("MY_CONFIG_YAML", """
my:
value: my_config_yaml
""", "MY_CONFIG_YAML_DEV", """
my:
value: my_config_yaml_dev
""")
.execute(() -> this.application.run(
"--spring.config.location=classpath:application-import-yaml-from-environment-with-profile-variant.properties"));
assertThat(context.getEnvironment().getProperty("my.value")).isEqualTo("my_config_yaml_dev");
}

@Test
void runWhenImportBase64YamlFromEnvironmentVariable() throws Exception {
ConfigurableApplicationContext context = uk.org.webcompere.systemstubs.SystemStubs
.withEnvironmentVariable("MY_CONFIG_BASE64_YAML", Base64.getEncoder().encodeToString("""
my:
value: from-base64-yaml
""".getBytes(StandardCharsets.UTF_8)))
.execute(() -> this.application
.run("--spring.config.location=classpath:application-import-base64-yaml-from-environment.properties"));
assertThat(context.getEnvironment().getProperty("my.value")).isEqualTo("from-base64-yaml");
}

@Test
void runWhenImportPropertiesFromEnvironmentVariable() throws Exception {
ConfigurableApplicationContext context = uk.org.webcompere.systemstubs.SystemStubs
.withEnvironmentVariable("MY_CONFIG_PROPERTIES", """
my.value1: from-properties-1
my.value2: from-properties-2
""")
.execute(() -> this.application
.run("--spring.config.location=classpath:application-import-properties-from-environment.properties"));
assertThat(context.getEnvironment().getProperty("my.value1")).isEqualTo("from-properties-1");
assertThat(context.getEnvironment().getProperty("my.value2")).isEqualTo("from-properties-2");
}

@Test
void runWhenImportWithProfileVariantAndDirectProfileImportOrdersPropertySourcesCorrectly() {
this.application.setAdditionalProfiles("dev");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ void initializeAddsProtocolResolversToApplicationContext() {
initializer.initialize(context);
assertThat(context).isInstanceOf(DefaultResourceLoader.class);
Collection<ProtocolResolver> protocolResolvers = ((DefaultResourceLoader) context).getProtocolResolvers();
assertThat(protocolResolvers).hasExactlyElementsOfTypes(Base64ProtocolResolver.class);
assertThat(protocolResolvers).hasExactlyElementsOfTypes(Base64ProtocolResolver.class,
EnvironmentVariableProtocolResolver.class);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
my.value=application-import-base64-yaml-from-environment
spring.config.import=env:base64:MY_CONFIG_BASE64_YAML[.yaml]
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
my.value=application-import-properties-from-environment
spring.config.import=env:MY_CONFIG_PROPERTIES[.properties]
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
spring.config.import=env:MY_CONFIG_YAML[.yaml]
my.value=application-import-from-environment-with-profile-variant
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
my.value=application-import-yaml-from-environment
spring.config.import=env:MY_CONFIG_YAML[.yaml]
Loading