Might be related to https://github.com/spring-cloud/spring-cloud-config/issues/1974, though I could not replicate that issue with the latest 3.1 RC.

Setup

  • Spring Boot 2.6.0
  • Spring Cloud Config 3.1 RC1
  • Apache Maven 3.8.x
  • OpenJDK 11

Overview

I have a multi-module Apache Maven project that is set up with the following modules:

  1. bootstrap: contains a PropertySourceLocator for BootstrapConfiguration, defined in spring.factories file.
  2. starter: depends on bootstrap, and it's a (servlet-based) web application
  3. reference: deploys the starter application using the Maven Cargo plugin, deploying into an Apache Tomcat 9.0.55

Runtime

  • The starter module declares a configuration class, annotated with @PropertySource("wa.properties"). This wa.properties on the classpath of the starter module has a setting: cas.authn.syncope.name=Starter
  • The starter module has a ServletInitializer that sets the spring.config.name property to "wa" when building the spring application.
  • The reference module only has a wa-embedded.properties file on the classpath with a setting: cas.authn.syncope.name=Embedded
  • The reference module starts with the spring activated profiles: embedded,all

Note: the cas.authn.syncope.name is bound to a Java POJO, CasConfigurationProperties, that is annotated with @ConfigurationProperties("cas").

Observation

The following bean in the application exists, simplified for this post:

@Bean
@ConditionalOnMissingBean(name = "something")
@RefreshScope(proxyMode = ScopedProxyMode.DEFAULT)
public Something something(ApplicationContext ctx, CasConfigurationProperties cas) {
    ...
}
  • If I look at the contents of cas.getAuthn().getSyncope().getName()), it shows: "Starter"
  • If I look at ctx.getEnvironment().getProperty("cas.authn.syncope.name"), it shows "Embedded".

In other words, property binding used during the bootstrapping process does not match the actual environment for the application's context.

Analysis

  • It appears that when the bootstrap application context is created, wa-embedded.properties, a profile-specific property is not read. In fact, the only property source that is used for binding is wa.properties as part of "localProperties", which I believe comes from @PropertySource("wa.properties"). Nothing else is read or found.

  • Then, property binding takes place binding CasConfigurationProperties and cas.authn.syncope.name initialized from @PropertySource("wa.properties"). The value of this property is set to Starter.

  • Then, the application servlet context is initialized and its environment is post-processed with profiles and the appropriate listener and Spring beans are created. In particular, this bean:

@Bean
@ConditionalOnMissingBean(name = "something")
@RefreshScope(proxyMode = ScopedProxyMode.DEFAULT)
public Something something(ApplicationContext ctx, CasConfigurationProperties cas) {
    ...
}

...shows that ctx is the actual application context with an environment that is post-processed via all profiles and shows ctx.getEnvironment().getProperty("cas.authn.syncope.name") as "Embedded".

However, CasConfigurationProperties was processed using the Bootstrap context only, and its equivalent property shows "Starter".

...which means the bean would be created using the wrong values in CasConfigurationProperties.

Research

This setup works OK using Spring Boot 2.5.6 and Spring Cloud 3.0.5. I don't think anything in Spring Boot has changed that would affect this, but I do see a number of differences in Cloud between 3.0 and 3.1.

Sample

Working on it. I am not sure I can create a reproducer to adequately showcase this, but I will try.

Comment From: ryanjbaxter

We will wait for your sample

Comment From: mmoayyed

Here is a reproducer: https://github.com/mmoayyed/cloud-config-reproducer

Overview

Please note that this is a stripped-down version of the Apache Syncope codebase: https://github.com/apache/syncope. I removed all the other components to make sure the project setup is as lean as it can be, though I suppose it's not quite ideal. That said, the setup should be very simple.

Once you clone the repository,

  • From the root, run mvn clean install -PskipTests
  • cd into fit/wa-reference and run mvn -Pdebug

You can tail the logs via tail fit/wa-reference/target/log

The project setup matches the issue description. The project is deployed into Apache Tomcat 9.0.55 via the cargo plugin. Remote debugging is available via port 8001. The application is launched using the embedded and all profiles, and the spring.config.name is set to wa.

Test

The app should be reading wa.properties file, and wa-embedded.properties file. The latter should replace values and overwrite what is found in the former for property binding. That is not the case, and is the heart of this issue.

When it's up (Ignore exceptions related to Syncope registration during startup), you should be able to go to: http://localhost:9080/syncope-wa/

...and see this output:

{
  configName: "DefaultSyncopeAuthModuleEmbedded", 
  handlerNames: "DefaultSyncopeAuthModuleWAStarter", // problem!
  envName: "DefaultSyncopeAuthModuleEmbedded"
}

Components

The controller that outputs this data demonstrates the issue, and is as follows:

    @RestController("cas")
    public static class MyController {
        @Autowired
        private CasConfigurationProperties properties;

        @Autowired
        @Qualifier("syncopeAuthenticationHandlers")
        private BeanContainer<AuthenticationHandler> syncopeAuthenticationHandlers;

        @Autowired
        private ConfigurableApplicationContext applicationContext;

        @GetMapping(produces = "application/json")
        public Map value() {
            var map = new LinkedHashMap<>();
            map.put("configName", properties.getAuthn().getSyncope().getName());
            map.put("handlerNames", syncopeAuthenticationHandlers.toList().stream().map(h -> h.getName()).collect(Collectors.joining()));
            map.put("envName", applicationContext.getEnvironment().getProperty("cas.authn.syncope.name"));
            return map;
        }
    }

The handlerNames field is fetched from the syncopeAuthenticationHandlers bean. This bean is initialized during the bootstrap process, with only the properties that do come from wa.properties. wa-embedded.properties which is a property file for a specific embedded profile is not used when syncopeAuthenticationHandlers bean is created.

The syncopeAuthenticationHandlers is part of the "cas-server-support-syncope-authentication" module. It's defined as such:

    @ConditionalOnMissingBean(name = "syncopeAuthenticationHandlers")
    @Bean
    @RefreshScope(proxyMode = ScopedProxyMode.DEFAULT)
    public BeanContainer<AuthenticationHandler> syncopeAuthenticationHandlers(
        final CasConfigurationProperties casProperties, // configuration binding...
        final ConfigurableApplicationContext applicationContext,
        @Qualifier("syncopePrincipalFactory")
        final PrincipalFactory syncopePrincipalFactory,
        @Qualifier("syncopePasswordPolicyConfiguration")
        final PasswordPolicyContext syncopePasswordPolicyConfiguration,
        @Qualifier(ServicesManager.BEAN_NAME)
        final ServicesManager servicesManager) {

        val syncope = casProperties.getAuthn().getSyncope();
        val handlers = Splitter.on(",").splitToList(syncope.getDomain())
            .stream()
            .map(domain -> {
                // here, syncope.getName() does not point to the value in the property profile...
                // This should come from wa-embedded.properties.
                // Instead, it always comes from the wa.properties
                val h = new SyncopeAuthenticationHandler(syncope.getName(), servicesManager,
                    syncopePrincipalFactory, syncope.getUrl(), domain.trim());
                h.setState(syncope.getState());
                h.setPasswordEncoder(PasswordEncoderUtils.newPasswordEncoder(syncope.getPasswordEncoder(), applicationContext));
                h.setPasswordPolicyConfiguration(syncopePasswordPolicyConfiguration);
                val predicate = CoreAuthenticationUtils.newCredentialSelectionPredicate(syncope.getCredentialCriteria());
                h.setCredentialSelectionPredicate(predicate);
                val transformer = PrincipalNameTransformerUtils.newPrincipalNameTransformer(syncope.getPrincipalTransformation());
                h.setPrincipalNameTransformer(transformer);
                return h;
            })
            .map(AuthenticationHandler.class::cast)
            .collect(Collectors.toList());
        return BeanContainer.of(handlers);
    }

Expectation

The endpoint should always produce DefaultSyncopeAuthModuleEmbedded as the value for all 3 fields. This is the current behavior with Spring Boot 2.5.7 and Spring Cloud 3.0.5.

Comment From: mmoayyed

I see 3.1.0 is now released and will try to determine if I can reproduce the same issue with the latest release.

Comment From: ryanjbaxter

I'm guessing you will but let us know

Comment From: mmoayyed

Tried again with Cloud 3.1.0 and Spring Boot 2.6.1. Same issue.

Comment From: mmoayyed

Ran a few more experiments:

  • If I rename the wa-embedded.properties file to bootstrap-embedded.properties, no difference. The file is not read/used for property binding.

I can confirm this using the same Controller:

    @RestController("cas")
    public static class MyController {
        @Autowired
        private CasConfigurationProperties properties;

        @Autowired
        @Qualifier("syncopeAuthenticationHandlers")
        private BeanContainer<AuthenticationHandler> syncopeAuthenticationHandlers;

        @Autowired
        private ConfigurableApplicationContext applicationContext;

        @GetMapping(produces = "application/json")
        public Map value() {
            var map = new LinkedHashMap<>();
            map.put("configName", properties.getAuthn().getSyncope().getName());
            map.put("handlerNames", syncopeAuthenticationHandlers.toList().stream().map(h -> h.getName()).collect(Collectors.joining()));
            map.put("envName", applicationContext.getEnvironment().getProperty("cas.authn.syncope.name"));
            return map;
        }
    }

Same output as before.

  • If I rename the wa-embedded.properties file to bootstrap.properties, then the output changes. This time I get:
{
configName: "DefaultSyncopeAuthModuleWAStarter",
handlerNames: "DefaultSyncopeAuthModuleEmbedded",
envName: "DefaultSyncopeAuthModuleWAStarter"
}

This time, handlerNames is initialized correctly using bootstrapping process and bootstrap.properties. However, the other two fields are wrong. Now, the application context's environment does not match the bootstrap properties in the environment.

Comment From: mmoayyed

If I change the configName to "wa" using spring.cloud.bootstrap.name=wa

Then I get:

Spring Cloud Config Spring Cloud Config: Bootstrap context not loading profile-specific property files for binding

And then the endpoint shows the following:

{
configName: "DefaultSyncopeAuthModuleEmbedded",
handlerNames: "DefaultSyncopeAuthModuleEmbedded",
envName: "DefaultSyncopeAuthModuleEmbedded"
}

...which is correct.

  • Has anything changed in this area in 3.1.x?
  • Should StandardConfigDataLocationResolver also consider spring.config.name when it's looking at configuration names? It appears to be only be aware of spring.cloud.bootstrap.name which by default only looks at bootstrap.

I'll have to do some more testing with everything else, but it seems to be some sort of regression[?]

Comment From: mmoayyed

To clarify, the configName is picked up from the bootstrap listener:

Spring Cloud Config Spring Cloud Config: Bootstrap context not loading profile-specific property files for binding

and then it's used by the StandardConfigDataLocationResolver which is created here:

Spring Cloud Config Spring Cloud Config: Bootstrap context not loading profile-specific property files for binding

Comment From: cihadbaskoy

I have a similar problem with Spring Boot version 2.6.4 and Spring Cloud Config version 2021.0.1

I found a workaround as adding a configuration as:

spring:
    profiles.active: dev
    cloud:
        config: 
            profile: ${spring.profiles.active}

In the ConfigServicePropertySourceLocator class, when trying to get the profiles, it reads them from properties and this properties only looks either spring.clod.config.profile OR spring.profiles, it does not check the active profiles.

This is why adding the above lines temporarily solves the problem

The issue is noticed when Spring Cloud Server returns the propertySources in correct order but client can only handle the order of properties without the profiles first.

Comment From: ryanjbaxter

If you could try Spring Cloud 2021.0.8-SNAPSHOT we have made some changes regarding profiles and activation.

See https://docs.spring.io/spring-cloud-commons/docs/3.1.7-SNAPSHOT/reference/html/#application-context-hierarchies

Comment From: spring-cloud-issues

If you would like us to look at this issue, please provide the requested information. If the information is not provided within the next 7 days this issue will be closed.

Comment From: spring-cloud-issues

Closing due to lack of requested feedback. If you would like us to look at this issue, please provide the requested information and we will re-open the issue.