spring-bootspring-cloud-gcp

Problem with GCP Secret Manager and Spring Boot app


Spring Boot (2.5.9) has problem to access password from the GCP Secret Manager using spring-cloud-gcp-starter-secretmanager ver 2.0.8 throwing error

AnnotationConfigApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'defaultFeignClientConfiguration': Injection of autowired dependencies failed; nested exception is org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type [com.google.protobuf.ByteString$LiteralByteString] to type [java.lang.String]

for a passsword declared in the application.properties as

webservices.security.basic.user.password=${sm://my-password}

when I will replace it with regular string or even env variable it will work fine.

Failing part of the code looks like:

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.retry.backoff.BackOffPolicy;
import org.springframework.retry.backoff.ExponentialBackOffPolicy;

import feign.Retryer;
import feign.auth.BasicAuthRequestInterceptor;
import feign.codec.ErrorDecoder;

/**
 * Default feign client configuration.  Includes retry policies, basic auth user name and password, and HTTP status decoder.
 * @author Greg Meyer
 * @since 6.0
 */
public class DefaultFeignClientConfiguration
{   
    @Value("${webservices.retry.backoff.multiplier:3}")
    protected double backoffMultiplier;
    
    @Value("${webservices.retry.backoff.initialBackoffInterval:100}")
    protected long initialBackoffInterval;  
    
    @Value("${webservices.retry.backoff.maxInterval:20000}")
    protected long maxInterval;     
    
    @Value("${webservices.security.basic.user.name:}")
    protected String user;  
    
    @Value("${webservices.security.basic.user.password:}")
    protected String pass;  
    
    /**
     * Creates an instance of the a the default HTTP status translator.
     * @return An instance of the a the default HTTP status translator
     */
    @Bean
    public ErrorDecoder feignClientErrorDecoder()
    {
        return new DefaultErrorDecoder();
    }
    
    /**
     * Creates an instance of BasicAuth interceptor configured with a username and password.   This bean is only created if the
     * "webservices.security.basic.user.name" property is set.
     * @return An instance of BasicAuth interceptor configured with a username and password
     */
    @Bean
    @ConditionalOnProperty(name="webservices.security.basic.user.name", matchIfMissing=false)
    public BasicAuthRequestInterceptor basicAuthRequestInterceptor() 
    {
        return new BasicAuthRequestInterceptor(user, pass);
    }   
    
    /**
     * Creates an instance of a back off policy used in conjuntion with the retry policy.
     * @return An instance of a back off policy
     */
    @Bean
    public LoadBalancedRetryFactory backOffPolciyFactory()
    {
        return new LoadBalancedRetryFactory() 
        {
            @Override
            public BackOffPolicy createBackOffPolicy(String service) 
            {
                final ExponentialBackOffPolicy backoffPolicy = new ExponentialBackOffPolicy();
                backoffPolicy.setMultiplier(backoffMultiplier);
                backoffPolicy.setInitialInterval(initialBackoffInterval);
                backoffPolicy.setMaxInterval(maxInterval);
                
                return backoffPolicy;
            }
        };      
    }
    
    /**
     * Creates a default http retry policy.
     * @return A default http retry policy.
     */ 
    @Bean
    public Retryer retryer() 
    {
        /*
         * Default retryer config
         */
        return new Retryer.Default(200, 1000, 5);
    }
}

Any thoughts?


Solution

  • The problem is that most likely, Feign autoconfiguration happens early on, before GcpSecretManagerEnvironmentPostProcessor had a chance to run and introduce ByteString converters.

    Basically, the solution that works is to implement a Custom Converter which implements the Generic Conversion Service and register the Converter in the main method before calling SpringApplication.run.

        public static void main(String[] args) 
        {
            ((DefaultConversionService)DefaultConversionService.getSharedInstance()).addConverter(new CustomConverter());
            SpringApplication.run(STAApplication.class, args);
        }  
    

    and custom converter:

    @Component
    public class CustomConverter implements GenericConverter {
        @Override
        public Set<ConvertiblePair> getConvertibleTypes() {
            return Collections.singleton(new ConvertiblePair(ByteString.class, String.class));
        }
    
        @Override
        public Object convert(Object source, TypeDescriptor sourceType,
                              TypeDescriptor targetType) {
            if (sourceType.getType() == String.class) {
                return source;
            }
            try {
                source = ((ByteString) source).toString("UTF-8");
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            } catch (Exception e) {
                e.printStackTrace();
            }
            return source;
        }
    }