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

Explicit user/password for hikari/liquibase is ignored when using Docker Compose support #40771

Open
sdavids opened this issue May 16, 2024 · 16 comments
Labels
status: pending-design-work Needs design work before any code can be developed type: enhancement A general enhancement
Milestone

Comments

@sdavids
Copy link

sdavids commented May 16, 2024

Spring Boot 3.2.5

The explicitly configured usernames and passwords are not used when using the Docker Compose support:

spring:
  datasource:
    hikari:
      username: example_rw
      password: example_rw
  liquibase:
    user: example_ow
    password: example_ow

they should not be overwritten by the one configured in compose.yaml:

services:
  db:
    environment:
      POSTGRES_USER: sa
      POSTGRES_PASSWORD: sa

Logs

$ ./gradlew bootRun
...
liquibase.database : Connected to sa@jdbc:postgresql://127.0.0.1:5432/example?ApplicationName=docker-compose-datasource-test
...
com.zaxxer.hikari.HikariConfig : jdbcUrl.........................jdbc:postgresql://127.0.0.1:5432/example?ApplicationName=docker-compose-datasource-test
...
com.zaxxer.hikari.HikariConfig : schema.........................."example"
...
com.zaxxer.hikari.HikariConfig : username........................"sa"
$ docker compose logs db -f

... POSTGRES_DB from environment is created with 'sa' - correct

db-1  | 2024-05-16 09:25:45.782 UTC [47] LOG:  connection received: host=[local]
db-1  | 2024-05-16 09:25:45.783 UTC [47] LOG:  connection authorized: user=sa database=postgres application_name=psql
db-1  | 2024-05-16 09:25:45.785 UTC [47] LOG:  statement: CREATE DATABASE "example" ;

... init scripts use 'sa' - correct

db-1  | /usr/local/bin/docker-entrypoint.sh: running /docker-entrypoint-initdb.d/001-create-users-and-database.sh
db-1  | 2024-05-16 09:25:45.832 UTC [50] LOG:  connection received: host=[local]
db-1  | 2024-05-16 09:25:45.832 UTC [50] LOG:  connection authorized: user=sa database=example application_name=psql
db-1  | 2024-05-16 09:25:45.842 UTC [50] LOG:  statement: REVOKE ALL PRIVILEGES ON DATABASE postgres FROM PUBLIC;

... liquibase uses 'sa' - should be 'example_ow'

db-1  | 2024-05-16 09:25:48.670 UTC [65] LOG:  connection received: host=192.168.65.1 port=40715
db-1  | 2024-05-16 09:25:48.751 UTC [65] LOG:  connection authenticated: identity="sa" method=scram-sha-256 (/var/lib/postgresql/data/pg_hba.conf:128)
db-1  | 2024-05-16 09:25:48.751 UTC [65] LOG:  connection authorized: user=sa database=example
...
db-1  | 2024-05-16 09:25:49.509 UTC [65] LOG:  execute <unnamed>: CREATE TABLE example.databasechangeloglock (ID INTEGER NOT NULL, LOCKED BOOLEAN NOT NULL, LOCKGRANTED TIMESTAMP WITHOUT TIME ZONE, LOCKEDBY VARCHAR(255), CONSTRAINT databasechangeloglock_pkey PRIMARY KEY (ID))

... hikari uses 'sa' - should be 'example_rw'

db-1  | 2024-05-16 09:26:38.544 UTC [32] LOG:  connection received: host=192.168.65.1 port=40792
db-1  | 2024-05-16 09:26:38.559 UTC [32] LOG:  connection authenticated: identity="sa" method=scram-sha-256 (/var/lib/postgresql/data/pg_hba.conf:128)
db-1  | 2024-05-16 09:26:38.559 UTC [32] LOG:  connection authorized: user=sa database=example
db-1  | 2024-05-16 09:26:38.562 UTC [32] LOG:  execute <unnamed>: SET extra_float_digits = 3
db-1  | 2024-05-16 09:26:38.563 UTC [32] LOG:  execute <unnamed>: SET application_name = 'docker-compose-datasource-test'
db-1  | 2024-05-16 09:26:38.564 UTC [32] LOG:  execute <unnamed>: SET SESSION search_path TO 'example'

Setup

application.yaml

spring:
  application:
    name: docker-compose-datasource-test
  datasource:
    hikari:
      schema:  example
      username: example_rw
      password: example_rw
  liquibase:
    default-schema: example
    user: example_ow
    password: example_ow
logging:
  level:
    com:
      zaxxer:
        hikari:
          HikariConfig: DEBUG
    liquibase:
      database: DEBUG
  pattern:
    console: '%c : %m%n'

compose.yaml

services:
  db:
    image: postgres:16.3-alpine3.19
    restart: always
    ports:
      - '5432:5432'
    command: ["postgres", "-c", "log_statement=all", "-c", "log_connections=true"]
    environment:
      POSTGRES_USER: sa
      POSTGRES_PASSWORD: sa
      POSTGRES_DB: example
    volumes:
      - ./docker/db/init/001-create-users-and-database.sh:/docker-entrypoint-initdb.d/001-create-users-and-database.sh
      - ./docker/db/init/002-create-schema.sh:/docker-entrypoint-initdb.d/002-create-schema.sh
    labels:
      org.springframework.boot.jdbc.parameters: 'ApplicationName=docker-compose-datasource-test'

docker/db/init/001-create-users-and-database.sh

#!/usr/bin/env bash

set -Eeu -o pipefail -o posix

readonly example_admin_pw='example_admin'
readonly example_ow_pw='example_ow'
readonly example_rw_pw='example_rw'
readonly example_ro_pw='example_ro'

psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
  REVOKE ALL PRIVILEGES ON DATABASE postgres FROM PUBLIC;
  GRANT ALL PRIVILEGES ON DATABASE postgres TO $POSTGRES_USER;

  CREATE USER example_admin WITH LOGIN REPLICATION PASSWORD '$example_admin_pw';
  CREATE USER example_ow WITH LOGIN PASSWORD '$example_ow_pw';
  CREATE USER example_rw WITH LOGIN PASSWORD '$example_rw_pw';
  CREATE USER example_ro WITH LOGIN PASSWORD '$example_ro_pw';

  CREATE DATABASE tmp;

  \c tmp

  DROP DATABASE IF EXISTS example;

  CREATE DATABASE example WITH OWNER example_admin TEMPLATE template0
    ENCODING UTF8 LC_COLLATE 'de_DE.UTF8' LC_CTYPE 'de_DE.UTF8';

  \c example

  DROP DATABASE IF EXISTS tmp;

  DROP SCHEMA IF EXISTS public;

  REVOKE ALL ON DATABASE example FROM PUBLIC;

  GRANT ALL ON DATABASE example TO $POSTGRES_USER;
  GRANT ALL ON DATABASE example TO example_admin;

  GRANT CONNECT,TEMPORARY ON DATABASE example TO example_ow;
  GRANT CONNECT,TEMPORARY ON DATABASE example TO example_rw;
  GRANT CONNECT ON DATABASE example TO example_ro;
EOSQL

docker/db/init/002-create-schema.sh

#!/usr/bin/env bash

set -Eeu -o pipefail -o posix

psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
  CREATE SCHEMA IF NOT EXISTS example AUTHORIZATION example_ow;

  REVOKE ALL ON SCHEMA example FROM PUBLIC;

  GRANT ALL ON SCHEMA example TO $POSTGRES_USER;
  GRANT ALL ON SCHEMA example TO example_admin;

  GRANT ALL ON SCHEMA example TO example_ow;
  ALTER ROLE example_ow IN DATABASE example SET search_path = 'example';

  GRANT pg_read_all_data, pg_write_all_data TO example_rw;
  ALTER ROLE example_rw IN DATABASE example SET search_path = 'example';

  GRANT pg_read_all_data TO example_ro;
  ALTER ROLE example_ro IN DATABASE example SET search_path = 'example';
EOSQL

src/main/resources/db/changelog/db.changelog-master.yaml

databaseChangeLog:
  - changeSet:
      id: INIT-1-1
      logicalFilePath: INIT-1
      author: sdavids
      changes:
        - tagDatabase:
            tag: INIT-1

I have not verified but I suspect that all spring.datasource.*.username and spring.datasource.*.password properties are affected.

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label May 16, 2024
@wilkinsona
Copy link
Member

When you're using the Docker Compose support, the auto-configured DataSource is created using the service connection details that comes from any SQL database service. These details are the JDBC URL, the username, and the password. They intentionally take precedence over the JDBC URL, username, and password configured in application.yaml as those details won't, typically, allow a connection to the Docker Compose-managed database to be established.

Can you please describe what you're trying to achieve here? I think I might be able to reverse engineer it from the scripts and configuration that you have shared, but a description directly from you will be considerably more accurate.

@scottfrederick scottfrederick added the status: waiting-for-feedback We need additional information before we can continue label May 16, 2024
@sdavids
Copy link
Author

sdavids commented May 16, 2024

The setup above in words:

Postgres instance; admin super user 'sa'

Each bounded context gets its own database and super user; database 'example`, super user 'example_admin'.

Each bounded context creates one or more schemas in its database and uses a dedicated admin user for each schema—all created database objects will be owned by the admin user; schema 'example`, admin user 'example_ow'

Two additional users: One with read-only permissions and one with read-write (but not create) permissions; user 'example_ro' and user 'example_rw'.

Each bounded context will be provisioned by Liquibase with the corresponding admin user; admin user 'example_ow'.

The bounded context's application uses the read-write user; user 'example_rw'.

Other stuff (Reporting/QA/etc.) uses the read-only user; user 'example_ro'.


The basic premise is: The development setup should be as close as possible to production.

As it is now, everything is done with the super user 'sa'—therefore no permission checks are performed because 'sa' has all permissions.

Therefore, if you forget to setup permissions correctly in the Liquibase migration scripts the local development setup will work.

Once you deploy to production it might not work because you set the permissions incorrectly (or forgot to set them up altogether), i.e. missing GRANT statements.


TL;DR

By using the super user one cannot test if the database roles and permissions are set up correctly.

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels May 16, 2024
@sdavids
Copy link
Author

sdavids commented May 16, 2024

At first, I tried replicating our setup with Using Testcontainers at Development Time but unfortunately the Testcontainers Postgres support does not allow multiple init scripts running against different databases:

testcontainers/testcontainers-java#8634

@wilkinsona
Copy link
Member

wilkinsona commented May 16, 2024

Thanks for the additional details. While cumbersome, I think you can achieve what you want with a custom connection details factory:

package com.example;

import org.springframework.boot.autoconfigure.jdbc.JdbcConnectionDetails;
import org.springframework.boot.docker.compose.core.RunningService;
import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory;
import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionSource;
import org.springframework.boot.docker.compose.service.connection.jdbc.JdbcUrlBuilder;
import org.springframework.core.Ordered;

class CustomUsernameAndPasswordPostgresJdbcDockerComposeConnectionDetailsFactory
		extends DockerComposeConnectionDetailsFactory<JdbcConnectionDetails> implements Ordered {

	protected CustomUsernameAndPasswordPostgresJdbcDockerComposeConnectionDetailsFactory() {
		super("postgres");
	}

	@Override
	protected JdbcConnectionDetails getDockerComposeConnectionDetails(DockerComposeConnectionSource source) {
		return new PostgresJdbcDockerComposeConnectionDetails(source.getRunningService());
	}

	static class PostgresJdbcDockerComposeConnectionDetails extends DockerComposeConnectionDetails
			implements JdbcConnectionDetails {

		private static final JdbcUrlBuilder jdbcUrlBuilder = new JdbcUrlBuilder("postgresql", 5432);

		private final String jdbcUrl;

		PostgresJdbcDockerComposeConnectionDetails(RunningService service) {
			super(service);
			this.jdbcUrl = jdbcUrlBuilder.build(service, "example");
		}

		@Override
		public String getUsername() {
			return "example_rw";
		}

		@Override
		public String getPassword() {
			return "example_rw";
		}

		@Override
		public String getJdbcUrl() {
			return this.jdbcUrl;
		}

	}

	@Override
	public int getOrder() {
		return 0;
	}

}

Registered in META-INF/spring.factories under the org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory key, this should give you complete control of mapping from the running Docker Compose service to the JDBC connection details. You could do similar for Liquibase by implementing another factory that produces LiquibaseConnectionDetails.

Note that ConnectionDetailsFactory implementations don't have access to the Environment so you'd have to either hardcode the username and password or provide them via some other means for now at least.

@sdavids
Copy link
Author

sdavids commented May 16, 2024

I will try it later today, thanks …

@wilkinsona wilkinsona added status: waiting-for-feedback We need additional information before we can continue and removed status: feedback-provided Feedback has been provided labels May 16, 2024
@sdavids
Copy link
Author

sdavids commented May 17, 2024

I tried your suggestion above:

java.lang.IllegalStateException: Duplicate connection details supplied for org.springframework.boot.autoconfigure.jdbc.JdbcConnectionDetails
	at org.springframework.util.Assert.state(Assert.java:97) ~[spring-core-6.1.6.jar:6.1.6]
	at org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactories.getConnectionDetails(ConnectionDetailsFactories.java:84) ~[spring-boot-autoconfigure-3.2.5.jar:3.2.5]
	at org.springframework.boot.docker.compose.service.connection.DockerComposeServiceConnectionsApplicationListener.registerConnectionDetails(DockerComposeServiceConnectionsApplicationListener.java:68) ~[spring-boot-docker-compose-3.2.5.jar:3.2.5]

public <S> Map<Class<?>, ConnectionDetails> getConnectionDetails(S source, boolean required)
throws ConnectionDetailsFactoryNotFoundException, ConnectionDetailsNotFoundException {
List<Registration<S, ?>> registrations = getRegistrations(source, required);
Map<Class<?>, ConnectionDetails> result = new LinkedHashMap<>();
for (Registration<S, ?> registration : registrations) {
ConnectionDetails connectionDetails = registration.factory().getConnectionDetails(source);
if (connectionDetails != null) {
Class<?> connectionDetailsType = registration.connectionDetailsType();
ConnectionDetails previous = result.put(connectionDetailsType, connectionDetails);
Assert.state(previous == null, () -> "Duplicate connection details supplied for %s"
.formatted(connectionDetailsType.getName()));
}
}
if (required && result.isEmpty()) {
throw new ConnectionDetailsNotFoundException(source);
}
return Map.copyOf(result);
}

src/main/java/com.example.CustomPostgresJdbcDockerComposeConnectionDetailsFactory

package com.example;

import org.springframework.boot.autoconfigure.jdbc.JdbcConnectionDetails;
import org.springframework.boot.docker.compose.core.RunningService;
import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory;
import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionSource;
import org.springframework.boot.docker.compose.service.connection.jdbc.JdbcUrlBuilder;
import org.springframework.core.Ordered;

class CustomPostgresJdbcDockerComposeConnectionDetailsFactory
    extends DockerComposeConnectionDetailsFactory<JdbcConnectionDetails> implements Ordered {

  CustomPostgresJdbcDockerComposeConnectionDetailsFactory() {
    super("postgres");
  }

  @Override
  public int getOrder() {
    return 0;
  }

  @Override
  protected JdbcConnectionDetails getDockerComposeConnectionDetails(
      DockerComposeConnectionSource source) {

    return new PostgresJdbcDockerComposeConnectionDetails(source.getRunningService());
  }

  static class PostgresJdbcDockerComposeConnectionDetails
      extends DockerComposeConnectionDetailsFactory.DockerComposeConnectionDetails
      implements JdbcConnectionDetails {

    private final String jdbcUrl;

    PostgresJdbcDockerComposeConnectionDetails(RunningService service) {
      super(service);
      this.jdbcUrl = new JdbcUrlBuilder("postgresql", 5432).build(service, "example");
    }

    @Override
    public String getUsername() {
      return "example_rw";
    }

    @Override
    public String getPassword() {
      return "example_rw";
    }

    @Override
    public String getJdbcUrl() {
      return this.jdbcUrl;
    }
  }
}

src/main/resources/META-INF/spring.factories

org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory=\
com.example.CustomPostgresJdbcDockerComposeConnectionDetailsFactory

The current registration algorithm does not support Ordered.

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels May 17, 2024
@wilkinsona
Copy link
Member

Sorry, I'd incorrectly recalled that the first factory would win, hence it implementing Ordered.

You can get PostgresJdbcDockerComposeConnectionDetailsFactory to ignore your db service by hiding the fact that it's Postgres. This will then allow the custom factory to take control. First, add a org.springframework.boot.service-connection label to the service with a value that's anything other than postgres, say custom-postgres. Then update the constructor of CustomPostgresJdbcDockerComposeConnectionDetailsFactory to pass that value to its super constructor:

	protected CustomUsernameAndPasswordPostgresJdbcDockerComposeConnectionDetailsFactory() {
		super("custom-postgres");
	}

This should ensure that only the custom factory is used for your db service.

@sdavids
Copy link
Author

sdavids commented May 17, 2024

src/main/java/com/example/CustomDockerComposeConnectionDetailsFactory.java

package com.example;

import org.springframework.boot.autoconfigure.jdbc.JdbcConnectionDetails;
import org.springframework.boot.docker.compose.core.RunningService;
import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory;
import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionSource;
import org.springframework.boot.docker.compose.service.connection.jdbc.JdbcUrlBuilder;

class CustomDockerComposeConnectionDetailsFactory
    extends DockerComposeConnectionDetailsFactory<JdbcConnectionDetails> {

  CustomDockerComposeConnectionDetailsFactory() {
    super("custom-postgres");
  }

  @Override
  protected JdbcConnectionDetails getDockerComposeConnectionDetails(
      DockerComposeConnectionSource source) {
    return new CustomDockerComposeConnectionDetails(source.getRunningService());
  }

  static class CustomDockerComposeConnectionDetails extends DockerComposeConnectionDetails
      implements JdbcConnectionDetails {

    private final String jdbcUrl;

    CustomDockerComposeConnectionDetails(RunningService service) {
      super(service);
      this.jdbcUrl = new JdbcUrlBuilder("postgresql", 5432).build(service, "example");
    }

    @Override
    public String getUsername() {
      return "example_rw";
    }

    @Override
    public String getPassword() {
      return "example_rw";
    }

    @Override
    public String getJdbcUrl() {
      return jdbcUrl;
    }
  }
}

src/main/resources/META-INF/spring.factories

org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory=\
com.example.CustomDockerComposeConnectionDetailsFactory

compose.yaml

services:
  db:
...
    labels:
      org.springframework.boot.service-connection: 'custom-postgres'

The setup above works.

But once Liquibase is in the mix we are back at square one.

src/main/java/com/example/CustomLiquibaseConnectionDetailsFactory.java

package com.example;

import org.springframework.boot.autoconfigure.jdbc.JdbcConnectionDetails;
import org.springframework.boot.autoconfigure.liquibase.LiquibaseConnectionDetails;
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory;

class CustomLiquibaseConnectionDetailsFactory
    implements ConnectionDetailsFactory<JdbcConnectionDetails, LiquibaseConnectionDetails> {

  CustomLiquibaseConnectionDetailsFactory() {}

  @Override
  public LiquibaseConnectionDetails getConnectionDetails(JdbcConnectionDetails input) {
    return new MyLiquibaseConnectionDetails(input);
  }

  static class MyLiquibaseConnectionDetails implements LiquibaseConnectionDetails {

    private final String jdbcUrl;
    private final String driverClassName;

    public MyLiquibaseConnectionDetails(JdbcConnectionDetails input) {
      jdbcUrl = input.getJdbcUrl();
      driverClassName = input.getDriverClassName();
    }

    @Override
    public String getUsername() {
      return "example_ow";
    }

    @Override
    public String getPassword() {
      return "example_ow";
    }

    @Override
    public String getJdbcUrl() {
      return jdbcUrl;
    }

    @Override
    public String getDriverClassName() {
      return driverClassName;
    }
  }
}

src/main/resources/META-INF/spring.factories

org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory=\
com.example.CustomDockerComposeConnectionDetailsFactory,\
com.example.CustomLiquibaseConnectionDetailsFactory

java.lang.IllegalStateException: Duplicate connection details supplied for org.springframework.boot.autoconfigure.liquibase.LiquibaseConnectionDetails
	at org.springframework.util.Assert.state(Assert.java:97) ~[spring-core-6.1.6.jar:6.1.6]
	at org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactories.getConnectionDetails(ConnectionDetailsFactories.java:84) ~[spring-boot-autoconfigure-3.2.5.jar:3.2.5]

I also tried:

src/main/java/com/example/CustomLiquibaseConnectionDetailsFactory2.java

package com.example;

import org.springframework.boot.autoconfigure.liquibase.LiquibaseConnectionDetails;
import org.springframework.boot.docker.compose.core.RunningService;
import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory;
import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionSource;
import org.springframework.boot.docker.compose.service.connection.jdbc.JdbcUrlBuilder;

class CustomLiquibaseConnectionDetailsFactory2
    extends DockerComposeConnectionDetailsFactory<LiquibaseConnectionDetails> {

  CustomLiquibaseConnectionDetailsFactory2() {
    super("custom-postgres");
  }

  @Override
  protected LiquibaseConnectionDetails getDockerComposeConnectionDetails(
      DockerComposeConnectionSource source) {
    return new CustomLiquibaseConnectionDetails(source.getRunningService());
  }

  private static class CustomLiquibaseConnectionDetails implements LiquibaseConnectionDetails {

    private final String jdbcUrl;

    CustomLiquibaseConnectionDetails(RunningService service) {
      this.jdbcUrl = new JdbcUrlBuilder("postgresql", 5432).build(service, "example");
    }

    @Override
    public String getUsername() {
      return "example_ow";
    }

    @Override
    public String getPassword() {
      return "example_ow";
    }

    @Override
    public String getJdbcUrl() {
      return jdbcUrl;
    }
  }
}

src/main/resources/META-INF/spring.factories

org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory=\
com.example.CustomDockerComposeConnectionDetailsFactory,\
com.example.CustomLiquibaseConnectionDetailsFactory2

src/main/resources/application.yaml

spring:
  main:
    allow-bean-definition-overriding: true

The application starts.

The result map contains both custom entries

result = {LinkedHashMap@4873}  size = 2
 {Class@4893} "interface org.springframework.boot.autoconfigure.liquibase.LiquibaseConnectionDetails" -> {CustomLiquibaseConnectionDetailsFactory2$CustomLiquibaseConnectionDetails@4906} 
 {Class@4898} "interface org.springframework.boot.autoconfigure.jdbc.JdbcConnectionDetails" -> {CustomDockerComposeConnectionDetailsFactory$CustomDockerComposeConnectionDetails@4907} 

Unfortunately, the bean injected into the liquibase bean is not our custom one but the default one:

public SpringLiquibase liquibase(ObjectProvider<DataSource> dataSource,
@LiquibaseDataSource ObjectProvider<DataSource> liquibaseDataSource, LiquibaseProperties properties,
LiquibaseConnectionDetails connectionDetails) {

connectionDetails = {JdbcAdaptingLiquibaseConnectionDetailsFactory$1@6852}

Implementing Ordered did not help either.

@sdavids
Copy link
Author

sdavids commented May 17, 2024

Should I author a PR adding Ordered support?

ConnectionDetails previous = result.put(connectionDetailsType, connectionDetails);
Assert.state(previous == null, () -> "Duplicate connection details supplied for %s"
.formatted(connectionDetailsType.getName()));

Basically if previous != null then check for Ordered and use the one with the lowest priority—if tied throw "duplicate" exception

@wilkinsona
Copy link
Member

Thanks for the offer. That's definitely room for improvement here but I'm not yet sure what we should do. Ideally, you wouldn't have to mess around for connection details factories at all to do what you want. We'll discuss it as team and try to figure out what we want to do here.

@wilkinsona wilkinsona added the for: team-meeting An issue we'd like to discuss as a team to make progress label May 17, 2024
@philwebb philwebb added type: enhancement A general enhancement status: pending-design-work Needs design work before any code can be developed and removed status: waiting-for-triage An issue we've not yet triaged status: feedback-provided Feedback has been provided for: team-meeting An issue we'd like to discuss as a team to make progress labels May 22, 2024
@philwebb philwebb added this to the 3.x milestone May 22, 2024
@wilkinsona
Copy link
Member

This won't help with the Liquibase side of things, but for Postgres things can be improved slightly by having the custom JdbcConnectionDetails implement EnvironmentAware. It can then retrieve properties from the environment for the username and password:

    @Override
    public void setEnvironment(Environment environment) {
        this.environment = environment;
    }

    @Override
    public String getUsername() {
      return this.environment.getProperty("spring.datasource.username");
    }

    @Override
    public String getPassword() {
    	return this.environment.getProperty("spring.datasource.password");
    }

@wilkinsona
Copy link
Member

I've experimented a bit in this branch. With no need for a custom connection details factory, it lets you have a compose file like this:

services:
  database:
    image: postgres:16.3-alpine3.19'
    ports:
      - '5432'
    environment:
      - 'POSTGRES_USER=sa'
      - 'POSTGRES_PASSWORD=sa'
      - 'POSTGRES_DB=mydatabase'
    labels:
      # $$ is required to disable Docker Compose's own interpolation
      # https://docs.docker.com/compose/compose-file/12-interpolation/
      - org.springframework.boot.jdbc.username=$${spring.datasource.username}
      - org.springframework.boot.jdbc.password=$${spring.datasource.password}

This is very much an experiment and, with this particular approach, I dislike how broad the changes would be to expand support across other service types or even just across the other factories for JdbcConnectionDetails. Putting that aside for now, I quite like the end result from an external perspective. I haven't yet looked at the Liquibase side of things.

@sdavids
Copy link
Author

sdavids commented May 24, 2024

Just one question about this approach:

Where/how do you specify the desired spring profile?

One could have fine-grained profiles containing only the credentials and then run the application with several profiles.

So instead of -Dspring.profiles.active=dev you might have -Dspring.profiles.active=dev,jpa_dev,liquibase_dev,kafka_dev,something_else_dev.

@wilkinsona
Copy link
Member

wilkinsona commented May 24, 2024

You'd specify the profiles as you normally would when starting the app. As usual, the profiles would influence the application properties and YAML files that are loaded into the Spring environment. The placeholders in the label values are then resolved against the environment.

@sdavids
Copy link
Author

sdavids commented May 24, 2024

Two alternative ideas:

Label org.springframework.boot.service.use-environment-configuration or something similarly named.

If true the Docker Compose support will use the appropriate config from the Spring environment for the annotated service.

It would not be as explicit though.


Label org.springframework.boot.service.configuration-precedence

with values like compose-only, spring-only, compose-first, spring-first.

@wilkinsona
Copy link
Member

Thanks for suggestion.

I'm not sure that Spring Boot's Docker Compose support should be that tightly coupled to the configuration properties that are defined in spring-boot-autoconfigure. Just for the case of a DataSource's username, it would require the Docker Compose support to know about spring.datasource.username, spring.datasource.hikari.username, spring.datasource.dbcp2.username, etc.

There's also the possibility that the JdbcConnectionDetails bean has been defined using some other properties. The Docker Compose support would then have no way of knowing what properties to use when overriding it with the JDBC URL from the compose-managed container.

While it may make the compose YAML slightly more verbose, I think it will be better to configure the use of properties explicitly rather than trying to do it automatically. That also allows people who want to hardcode the custom credentials rather than using their application's properties to achieve their goal too.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status: pending-design-work Needs design work before any code can be developed type: enhancement A general enhancement
Projects
None yet
Development

No branches or pull requests

5 participants