Could not find a valid Docker environment when using Gradle Shared Build Service (ClassNotFoundException when loading strategies)

Hi,

I am trying to setup my project to use MySql testcontainer as part of the gradle build process. Here is what I am trying to achieve when the gradle build will trigger.

  1. Start the MySql testcontainer as a Gradle Shared Service
  2. Apply DB migrations using liquibase against testcontianer MySql DB
  3. Generate Jooq code against testcontianer MySql DB
  4. Compile the code.
  5. Run the unit tests
  6. Run the integrationTest task against testcontianer MySql DB
  7. Stop the MySql testcontainer. (It will be managed by the Gradle Shared Service)

When I did the setup, it worked for me but after a day or so, I am seeing the below error when I am running ./gradlew clean build in my local (Mac OS Sonoma 14.5)

Error

Can't instantiate a strategy from org.testcontainers.dockerclient.UnixSocketClientProviderStrategy (ClassNotFoundException). This probably means that cached configuration refers to a client provider class that is not available in this version of Testcontainers. Other strategies will be tried instead.
Could not find a valid Docker environment. Please check configuration. Attempted configurations were:
As no valid configuration was found, execution cannot continue.
See https://java.testcontainers.org/on_failure.html for more details.

* What went wrong:
Execution failed for task ':startMySQLContainer'.
> Failed to create service 'mysqlContainerService'.
   > Could not create an instance of type MySQLContainerService.
      > Could not find a valid Docker environment. Please see logs and check configuration

Here is my gradle code where I am trying to start the container.

import org.testcontainers.containers.MySQLContainer;
import org.gradle.api.services.BuildService
import org.gradle.api.services.BuildServiceParameters

buildscript {
    repositories {
        maven {
            name repoName
            url repoUrl
            credentials() {
                username = repoUsername
                password = repoPassword
            }
        }
    }
    dependencies {
        classpath "org.testcontainers:testcontainers:${dependencyManagement.importedProperties['testcontainers.version']}"
        classpath "org.testcontainers:mysql:${dependencyManagement.importedProperties['testcontainers.version']}"
        classpath "com.mysql:mysql-connector-j:${dependencyManagement.importedProperties['mysql.version']}"
    }
}


// Here we register service for providing our database during the build.
gradle.sharedServices.registerIfAbsent('mysqlContainerService', MySQLContainerService) { spec ->
    spec.parameters.databaseSchemaName.set(project.ext.databaseSchemaName)
}

/**
 * Build service for providing database container.
 */
abstract class MySQLContainerService implements BuildService<MySQLContainerService.Params>, AutoCloseable {
    interface Params extends BuildServiceParameters {
        Property<String> getDatabaseSchemaName()
    }
    private final MySQLContainer mysqlContainer;

    @javax.inject.Inject
    MySQLContainerService(Params params) {
        // Services are initialized lazily, on first request to them, so we start container immediately.
        long startTime = System.currentTimeMillis()
        println("DB Schema Name: ${params.getDatabaseSchemaName().get()}")
        mysqlContainer = new MySQLContainer(MySQLContainer.IMAGE)
                .withDatabaseName(params.getDatabaseSchemaName().get())
        mysqlContainer.start()
        long duration = System.currentTimeMillis() - startTime
        println("MySQL Testcontainer Started successfully with URL: ${mysqlContainer.getJdbcUrl()}. Total time taken: ${duration} ms")
    }

    String getJdbcUrl() {
        return mysqlContainer.getJdbcUrl()
    }

    String getUsername() {
        return mysqlContainer.getUsername()
    }

    String getPassword() {
        return mysqlContainer.getPassword()
    }

    String getDriverClassName() {
        return mysqlContainer.getDriverClassName()
    }


    @Override
    void close() {
        // Ensure to stop container in the end
        if (mysqlContainer != null) {
            try {
                mysqlContainer.stop()
                println("MySQL container stopped successfully.")
            } catch (Exception e) {
                println("Failed to stop MySQL container: ${e.message}")
            }
        } else {
            println("MySQL container is already null (was not started).")
        }
    }
}

// Ensure the integrationTest uses the same container and stops it after
tasks.named('integrationTest').configure {
    doFirst {
        def service = gradle.sharedServices.getRegistrations().getByName('mysqlContainerService').service.get()
        systemProperty 'spring.datasource.url', service.getJdbcUrl()
        systemProperty 'spring.datasource.username', service.getUsername()
        systemProperty 'spring.datasource.password', service.getPassword()
        systemProperty 'spring.datasource.driver-class-name', service.getDriverClassName()
    }

    doLast {
        systemProperties.remove('spring.datasource.url')
        systemProperties.remove('spring.datasource.username')
        systemProperties.remove('spring.datasource.password')
        systemProperties.remove('spring.datasource.driver-class-name')
    }
}

The error is on line mysqlContainer.start()

I have started a discussion with testcontainers team and as per them it seems like an issue with Gradle Shared services. They have created an issue on their end but wanted me to check with Gradle team about this.

This is what I heard from their team

When using the Gradle Shared Service, the JVM class loader seems to not be able to instantiate the class (which is part of Testcontainers). That seems to be like something to ask Gradle about.

Here is the ticket for testcontainers GH: Could not find a valid Docker environment when using Gradle Shared Build Service (`ClassNotFoundException` when loading strategies) · Issue #9050 · testcontainers/testcontainers-java · GitHub

I am really blocked here, it will be great if someone can help me on this.

Thanks in advance.

  • Deba

wanted me to check with Gradle team about this.

Well, then you have to turn to the Gradle team probably.
This is a community forum where mainly users like me help other users.
The Gradle team is not very active here.
If you think you are hitting a Gradle bug, you need to open an issue on GitHub and there will get attention by Gradle folks.

Regarding the code you showed, you should not have an explicit startMySQLContainer task, just declare properly which tasks use the service, for example using @ServiceReference in a binary task, or with usesService in an ad-hoc task, then the service will automatically be instantiated before the task starts. Not doing so also is deprecated.

You should also not get the service registration by name like you do it, but use the provider you get back from the registerIfAbsent call, like shown in the shared service documentation.

Furthermore, you should not change the configuration of a task like integrationTest from its execution phase. Latest when you hope to use configuration cache, this is impossible, but even without, this is highly discouraged bad practice and also will interfere with up-to-date checks and build cache. Besides that removing from the tasks system properties in doLast does not even make any sense at all, ever.

Where the classloading problem comes from is hard to guess without an MCVE.

Regarding the code you showed, you should not have an explicit startMySQLContainer task, just declare properly which tasks use the service, for example using @ServiceReference in a binary task, or with usesService in an ad-hoc task, then the service will automatically be instantiated before the task starts. Not doing so also is deprecated.

The updated code what I have is here in my original post. If you are referring to the testcontainers GH post, I have updated the code after I realized that I dont need startMySQLContainer

You should also not get the service registration by name like you do it, but use the provider you get back from the registerIfAbsent call, like shown in the shared service documentation.

I believe you are referring this way:

// Register the service
Provider<MySQLContainerService> serviceProvider = project.getGradle().getSharedServices().registerIfAbsent("mysqlContainerService", MySQLContainerService.class, spec -> {
    // Provide databaseSchemaName as a parameter
    spec.getParameters().getDatabaseSchemaName().set(project.ext.databaseSchemaName);
});

I am still not clear whether in my case I need usesService or not because I am reading the service using it.

def service = gradle.sharedServices.getRegistrations().getByName('mysqlContainerService').service.get()

Do you still think I should use @ServiceReference or usesService? Do you have an example which I can refer?

Furthermore, you should not change the configuration of a task like integrationTest from its execution phase. Latest when you hope to use configuration cache, this is impossible, but even without, this is highly discouraged bad practice and also will interfere with up-to-date checks and build cache. Besides that removing from the tasks system properties in doLast does not even make any sense at all, ever.

If I dont override the values of those 4 properties before executing the integrationTest task, its not connecting to the MySql testcontainer DB. So is there any other way to achieve this ? I will be happy to adopt that.

systemProperty(...) unfortunately does not yet support Property/ Provider, though this hopefully changes with Gradle 9.
Currently the value is “lazy” by the dirty-old-toString laziness.
So set as value some object where in its toString() you get the value from the service and return it.

You should also not get the service registration by name like you do it, but use the provider you get back from the registerIfAbsent call, like shown in the shared service documentation.

I believe you are referring this way:

// Register the service
Provider<MySQLContainerService> serviceProvider = project.getGradle().getSharedServices().registerIfAbsent("mysqlContainerService", MySQLContainerService.class, spec -> {
    // Provide databaseSchemaName as a parameter
    spec.getParameters().getDatabaseSchemaName().set(project.ext.databaseSchemaName);
});

I am still not clear whether in my case I need usesService or not because I am reading the service using it.

def service = gradle.sharedServices.getRegistrations().getByName('mysqlContainerService').service.get()

Do you still think I should use @ServiceReference or usesService? Do you have an example which I can refer?

Never use sharedServices.getRegistrations().
Always use @ServiceReference or usesService or Gradle does not know which tasks need the service, so for example does not know when it safely can shut it down, and as I said, not doing it is deprecated.

Do you have an example which I can refer?

As I said, the docs should do.

In doc all the examples are creating custom tasks but in my case, I have existing tasks where I wanted to use this service. I dont see any example where it uses the service…

For example I have a liquibase.gradle where I waned to use this service.

configurations {
    liquibaseRuntime.extendsFrom runtimeClasspath
}

dependencies {
    liquibaseRuntime sourceSets.main.output
    runtimeOnly 'org.liquibase:liquibase-core'
    implementation 'info.picocli:picocli:4.+'
}

tasks.named('update') {
    doFirst {
        def service = gradle.sharedServices.getRegistrations().getByName('mysqlContainerService').service.get()
        liquibase {
            activities {
                main {
                    changelogFile 'liquibase/testcontainerDbChangeLog.yaml'
                    url service.getJdbcUrl()
                    username service.getUsername()
                    password service.getPassword()
                }
            }
            runList = 'main'
        }
    }
}

If I have to replace with @ServiceReference or usesService, how I have to do it ?

I have existing tasks where I wanted to use this service.

Doesn’t really matter.
Well, you cannot use @ServiceReference in that case, as that is an annotation on a property.
But usesService is runtime API, you can call that on any task.

If I have to replace with @ServiceReference or usesService , how I have to do it ?

No, that’s not what I said.
You always have to use @ServiceReference or usesService to tell Gradle that this task is using that service.
And then you use it, for example in doFirst, but never by getting the service registration, but as I said already, get the service from the result of the registerIfAbsent call.

Besides that, the recent snippet you shared is still the highly discouraged bad practice mentioned above. You do not configure the tasks configuration directly from its execution phase. But you configure the extension that the task is using, so implicitly change its configuration. This is still not a good idea at all.

I’m not too familiar with the Liquibase plugin, but you probably would have similar to the system properties values where in the toString method the value is retrieved from the service and additionally you would mark all tasks that use these values with usesService, so probably something like

tasks.withType(LiquibaseTask).configureEach {
    usesService(...)
}

First of all I am sorry if at all I am not able to follow you properly what you suggested previously. As I said I am new to this gradle thing and also Shared build service one, so trying to learn and update my code accordingly.

Just repeat what I am trying to achieve. I have three different gradle files.

  1. dbcontainer.gradle
  2. liquibase.gradle
  3. jooq.gradle

Those three gradle files are applied from my main build.gradle in the sequene its given above. Because I am trying to start the testcontainer first and use that to apply DB migrations using liquibase and also generate java code using Jooq task.

What I have done is just separate the concerns in different gradle files.

NOTE: I am found a blog on this and trying to follow that.

Here is what I have changed in my code to follow your best practice suggestions.

dbcontainer.gradle

import org.testcontainers.containers.MySQLContainer;
import org.gradle.api.services.BuildService
import org.gradle.api.services.BuildServiceParameters

buildscript {
    repositories {
        maven {
            name repoName
            url repoUrl
            credentials() {
                username = repoUsername
                password = repoPassword
            }
        }
    }
    dependencies {
        classpath "org.testcontainers:testcontainers:${dependencyManagement.importedProperties['testcontainers.version']}"
        classpath "org.testcontainers:mysql:${dependencyManagement.importedProperties['testcontainers.version']}"
        classpath "com.mysql:mysql-connector-j:${dependencyManagement.importedProperties['mysql.version']}"
    }
}


// Register the service
Provider<MySQLContainerService> serviceProvider = project.getGradle().getSharedServices().registerIfAbsent("mysqlContainerService", MySQLContainerService.class, spec -> {
    // Provide databaseSchemaName as a parameter
    spec.getParameters().getDatabaseSchemaName().set(project.ext.databaseSchemaName);
});

/**
 * Build service for providing database container.
 */
abstract class MySQLContainerService implements BuildService<MySQLContainerService.Params>, AutoCloseable {
    interface Params extends BuildServiceParameters {
        Property<String> getDatabaseSchemaName()
    }
    private final MySQLContainer mysqlContainer;

    @javax.inject.Inject
    MySQLContainerService(Params params) {
        // Services are initialized lazily, on first request to them, so we start container immediately.
        long startTime = System.currentTimeMillis()
        println("DB Schema Name: ${params.getDatabaseSchemaName().get()}")
        mysqlContainer = new MySQLContainer(MySQLContainer.IMAGE)
                .withDatabaseName(params.getDatabaseSchemaName().get())
        mysqlContainer.start()
        long duration = System.currentTimeMillis() - startTime
        println("MySQL Testcontainer Started successfully with URL: ${mysqlContainer.getJdbcUrl()}. Total time taken: ${duration} ms")
    }

    String getJdbcUrl() {
        return mysqlContainer.getJdbcUrl()
    }

    String getUsername() {
        return mysqlContainer.getUsername()
    }

    String getPassword() {
        return mysqlContainer.getPassword()
    }

    String getDriverClassName() {
        return mysqlContainer.getDriverClassName()
    }


    @Override
    void close() {
        // Ensure to stop container in the end
        if (mysqlContainer != null) {
            try {
                mysqlContainer.stop()
                println("MySQL container stopped successfully.")
            } catch (Exception e) {
                println("Failed to stop MySQL container: ${e.message}")
            }
        } else {
            println("MySQL container is already null (was not started).")
        }
    }
}

// Ensure the integrationTest uses the same container and stops it after
tasks.named('integrationTest').configure {
    usesService(serviceProvider)

    doFirst {
        def dbContainer = serviceProvider.get()
        systemProperty 'spring.datasource.url', dbContainer.getJdbcUrl()
        systemProperty 'spring.datasource.username', dbContainer.getUsername()
        systemProperty 'spring.datasource.password', dbContainer.getPassword()
        systemProperty 'spring.datasource.driver-class-name', dbContainer.getDriverClassName()
    }
}

liquibase.gradle

// Register the service
Provider<MySQLContainerService> serviceProvider = project.getGradle().getSharedServices().registerIfAbsent("mysqlContainerService", MySQLContainerService.class, spec -> {
    // Provide databaseSchemaName as a parameter
    spec.getParameters().getDatabaseSchemaName().set(project.ext.databaseSchemaName);
});

configurations {
    liquibaseRuntime.extendsFrom runtimeClasspath
}

dependencies {
    liquibaseRuntime sourceSets.main.output
    runtimeOnly 'org.liquibase:liquibase-core'
    implementation 'info.picocli:picocli:4.+'
}

tasks.named('update') {
    usesService(serviceProvider)
    doFirst {
        def dbContainer = serviceProvider.get() as Object
        liquibase {
            activities {
                main {
                    changelogFile 'liquibase/testcontainerDbChangeLog.yaml'
                    url dbContainer.getJdbcUrl()
                    username dbContainer.getUsername()
                    password dbContainer.getPassword()
                }
            }
            runList = 'main'
        }
    }
}

But on your last comment, you are asking me to do something like

tasks.withType('update').configureEach {
    usesService(serviceProvider)
}

So in that case the updated version of my code would be as below.

/ Register the service
Provider<MySQLContainerService> serviceProvider = project.getGradle().getSharedServices().registerIfAbsent("mysqlContainerService", MySQLContainerService.class, spec -> {
    // Provide databaseSchemaName as a parameter
    spec.getParameters().getDatabaseSchemaName().set(project.ext.databaseSchemaName);
});

configurations {
    liquibaseRuntime.extendsFrom runtimeClasspath
}

dependencies {
    liquibaseRuntime sourceSets.main.output
    runtimeOnly 'org.liquibase:liquibase-core'
    implementation 'info.picocli:picocli:4.+'
}

tasks.withType('update').configureEach {
    usesService(serviceProvider)
}

tasks.named('update') {
    doFirst {
       def dbContainer = serviceProvider.get() as Object
        liquibase {
            activities {
                main {
                    changelogFile 'liquibase/testcontainerDbChangeLog.yaml'
                    url dbContainer.getJdbcUrl()
                    username dbContainer.getUsername()
                    password dbContainer.getPassword()
                }
            }
            runList = 'main'
        }
    }
}

Is that what is the right approach or I am missing again the steps ?

Thanks again for the help and guidance though

Those three gradle files are applied from my main build.gradle in the sequene its given above.

If you mean those are legacy script plugins and not precompiled script plugins, so you apply them using apply from, you should stop doing that. Legacy script plugins have many subtly quirks and are discouraged. If you want to reuse build logic or separat concerns, you should instead consider using convention plugins in buildSrc or - what I prefer - an included build, for example implemented as precompiled script plugins.

Also, I highly recommend you abandon Groovy DSL and instead use Kotlin DSL. By now it is the default DSL, you immediately get type-safe build scripts, actually helpful error messages if you mess up the syntax, and amazingly better IDE support if you use a good IDE like IntelliJ IDEA or Android Studio.

Here is what I have changed in my code to follow your best practice suggestions

You actually use very clunky Java-like syntax while using a language than can be much more sleek.
For example
project.getGradle().getSharedServices().registerIfAbsent("mysqlContainerService", MySQLContainerService.class, spec -> { ... })
is identical to
gradle.sharedServices.registerIfAbsent("mysqlContainerService", MySQLContainerService) { ... }
or
spec.getParameters().getDatabaseSchemaName().set(project.ext.databaseSchemaName);
is identical to
spec.parameters.databaseSchemaName = databaseSchemaName

Also, generally, using ext / extra properties is almost always a code smell and a sign that you are doing a quick hack instead of doing something properly.

And never, in any JVM code, not only Gradle logic, use System.currentTimeMilis() to measure durations within one JVM. This method is depending on wall-clock and can any time jump around due to computer clock changes and similar. Always use System.nanoTime() to measure durations inside one JVM.

Also, better do not use println in build logic unless just for a quick debug help output. Use the logger with appropriate level, in your case for example info is probably much more appropriate than quiet which means it is always printed even with --quiet, while is should better only come with --info or you easily miss warnings or errors behind a wall of text if too many messages are on lifecycle or quiet level.

Regarding “First of all I am sorry if at all I am not able to follow you properly what you suggested previously.”, maybe you should read again the whole conversation. You for example still modify the integrationTest task’s configuration from its execution phase and you really should not do that, as I explained at least twice in this thread already.
Unfortunately I give it a quick try now to give you more concrete code, hoping you understand that better than text, but found that it is a bit problematic like I suggested.
The code should be something like

tasks.named('integrationTest').configure {
   usesService(serviceProvider)
   systemProperty 'spring.datasource.url', new Object() {
      @Override
      String toString() {
         serviceProvider.get().jdbcUrl
      }
   }
}

but the problem is, that for the up-to-date checks Groovy tries to serialize the task inputs including systemProperty and serviceProvider is not serializable.
The above code would work if you disable up-to-date checks for the task and let it run every time, using doNotTrackState(...), just in case that is appropriate for those tests, for example if they also involve some 3rd party systems.

Luckily, jvm argument providers come to the rescue.
This should work properly, cleanly, and lazily until systemProperty learnt how to handle Provider instances (hopefully in Gradle 9):

tasks.named('integrationTest') {
   usesService(serviceProvider)
   jvmArgumentProviders.add(new CommandLineArgumentProvider() {
      @Override
      Iterable<String> asArguments() {
         ["-Dspring.datasource.url=${serviceProvider.get().jdbcUrl}"]
      }
   })
}

Also for the liquibase stuff you still ignore what I told at least two times before in the thread, do not change configuration at execution time.
This should look for example like I described:

liquibase {
   activities {
      main {
         url new Object() {
            @Override
            String toString() {
               serviceProvider.get().jdbcUrl
            }
         }
      }
   }
   runList = 'main'
}
tasks.withType(LiquibaseTask).configureEach {
   usesService(serviceProvider)
}

But on your last comment, you are asking me to do something like

No, I did not. update is not a task type. You just wildly replaced things in what I gave you, thinking it was a placeholder. It is not, it the class name of all liquibase tasks, as you are configuring an extension and I greatly assume that all liquibase tasks will use those values, so all those tasks should be declared to use the service.

So in that case the updated version of my code would be as below.

No, you still ignore that you really should not change configuration in the execution phase, see above.


Even if you do not do it intentionally, you constantly ignore most of what I say, and being new to a topic is in no way an excuse to not listening to what people trying to help you say. Instead this is one of the major characteristics of a “help vampire”, google it. :wink:

Thank you so much for the detail explanation :slight_smile:

If you mean those are legacy script plugins and not precompiled script plugins

What do you mean by legacy script plugins or not precompiled script plugins ?

Because out of the three gradle files which I talked about, I I have already put the code for two gradle files above.

Which category will they fall into ?

legacy script plugins ?

OR
not precompiled script plugins ?

That is not an “OR” in your question.
There are legacy script plugins - the things you use with apply from that have many quirks and are highly discouraged.
And there are precompiled script plugins - I linked you to documentation about them.
So “not precompiled script plugin” == “legacy script plugin”, there are no more types of script plugins.

I made all the changes and now its working for me. Not sure whether the issue is resolved or it will come back again once I restart my computer which happened like last time.

But here are the changes I made.

  1. Create buildSrc folder under my project directory. Also sub folders under that src\main\groovy
  2. Added a build.gradle file under buildSrc directory.
plugins {
    id 'groovy'
}

repositories {
    mavenCentral()
}

dependencies {
    implementation "org.codehaus.groovy:groovy-all:3.0.21"
    implementation "org.testcontainers:testcontainers:1.19.+"
    implementation "org.testcontainers:mysql:1.19.+"
    implementation "com.mysql:mysql-connector-j:8.4.0"
    implementation gradleApi()
    implementation localGroovy()
}
  1. Create a new groovy file MySQLContainerService.groovy and moved the shared build service code inside that.
/**
 * This gradle file is responsible to start the db testcontainer as a gradle build shared service.
 */
import org.testcontainers.containers.MySQLContainer
import org.gradle.api.services.BuildService
import org.gradle.api.services.BuildServiceParameters
import org.gradle.api.provider.Property
import org.slf4j.LoggerFactory
import org.gradle.api.logging.Logger;

abstract class MySQLContainerService implements BuildService<MySQLContainerService.Params>, AutoCloseable {
    interface Params extends BuildServiceParameters {
        Property<String> getDatabaseSchemaName()
    }
    def logger = LoggerFactory.getLogger('MySQLContainerService')

    private final MySQLContainer mysqlContainer;

    @javax.inject.Inject
    MySQLContainerService(Params params) {
        // Services are initialized lazily, on first request to them, so we start container immediately.
        long startTime = System.nanoTime()
        logger.info("DB Schema Name: ${params.getDatabaseSchemaName().get()}")
        mysqlContainer = new MySQLContainer(MySQLContainer.IMAGE)
                .withDatabaseName(params.getDatabaseSchemaName().get())
        mysqlContainer.start()
        long duration = System.nanoTime() - startTime
        logger.info("MySQL Testcontainer Started successfully with URL: ${mysqlContainer.getJdbcUrl()}. Total time taken: ${duration} ms")
    }

    String getJdbcUrl() {
        return mysqlContainer.getJdbcUrl()
    }

    String getUsername() {
        return mysqlContainer.getUsername()
    }

    String getPassword() {
        return mysqlContainer.getPassword()
    }

    String getDriverClassName() {
        return mysqlContainer.getDriverClassName()
    }

    @Override
    void close() {
        // Ensure to stop container in the end
        if (mysqlContainer != null) {
            try {
                mysqlContainer.stop()
                logger.info("MySQL container stopped successfully.")
            } catch (Exception e) {
                logger.error("Failed to stop MySQL container: ${e.message}")
            }
        } else {
            logger.info("MySQL container is already null (was not started).")
        }
    }
}
  1. Deleted dbcontainer.gradle because that is no more needed.
  2. Moved integrationTest task related changes inside liquibase.gradle file.
// Register the service
def serviceProvider = gradle.sharedServices.registerIfAbsent("mysqlContainerService", MySQLContainerService) {
    it.parameters.databaseSchemaName = project.ext.databaseSchemaName
}

configurations {
    liquibaseRuntime.extendsFrom runtimeClasspath
}

dependencies {
    liquibaseRuntime sourceSets.main.output
    runtimeOnly 'org.liquibase:liquibase-core'
    implementation 'info.picocli:picocli:4.+'
}

tasks.named('update') {
    usesService(serviceProvider)
    doFirst {
        def dbContainer = serviceProvider.get()
        liquibase {
            activities {
                main {
                    changelogFile 'liquibase/testcontainerDbChangeLog.yaml'
                    url dbContainer.getJdbcUrl()
                    username dbContainer.getUsername()
                    password dbContainer.getPassword()
                }
            }
            runList = 'main'
        }
    }
}


// Ensure the integrationTest uses the same container and stops it after
tasks.named('integrationTest') {
    usesService(serviceProvider)
    jvmArgumentProviders.add(new CommandLineArgumentProvider() {
        @Override
        Iterable<String> asArguments() {
            ["-Dspring.datasource.url=${serviceProvider.get().jdbcUrl}",
             "-Dspring.datasource.username=${serviceProvider.get().username}",
             "-Dspring.datasource.password=${serviceProvider.get().password}",
             "-Dspring.datasource.driver-class-name=${serviceProvider.get().driverClassName}",
            ]
        }
    })
}

Now two things I could not resolve.

  1. I have added the logging inside MySQLContainerService.groovy but its not printing when I am running the gradle build.

  2. When I changed the liquibase.gradle to use the code which you suggested, I am getting the error. But I have the plugin in my build.gradle file which is under my project root directory. And also settings.gradle has the mapping for that as well.

import org.liquibase.gradle.LiquibaseTask
// Register the service
def serviceProvider = gradle.sharedServices.registerIfAbsent("mysqlContainerService", MySQLContainerService) {
    it.parameters.databaseSchemaName = project.ext.databaseSchemaName
}

configurations {
    liquibaseRuntime.extendsFrom runtimeClasspath
}

dependencies {
    liquibaseRuntime sourceSets.main.output
    runtimeOnly 'org.liquibase:liquibase-core'
    implementation 'info.picocli:picocli:4.+'
}

liquibase {
    activities {
        main {
            changelogFile 'liquibase/testcontainerDbChangeLog.yaml'
            url new Object() {
                @Override
                String toString() {
                    serviceProvider.get().jdbcUrl
                }
            }
            username new Object(){
                @Override
                String toString() {
                    serviceProvider.get().username
                }
            }
            password new Object(){
                @Override
                String toString() {
                    serviceProvider.get().password
                }
            }
        }
    }
    runList = 'main'
}

tasks.withType(LiquibaseTask).configureEach {
    usesService(serviceProvider)
}

// Ensure the integrationTest uses the same container and stops it after
tasks.named('integrationTest') {
    usesService(serviceProvider)
    jvmArgumentProviders.add(new CommandLineArgumentProvider() {
        @Override
        Iterable<String> asArguments() {
            ["-Dspring.datasource.url=${serviceProvider.get().jdbcUrl}",
             "-Dspring.datasource.username=${serviceProvider.get().username}",
             "-Dspring.datasource.password=${serviceProvider.get().password}",
             "-Dspring.datasource.driver-class-name=${serviceProvider.get().driverClassName}",
            ]
        }
    })
}

Error

* Where:
Script '/Users/dpatra/Downloads/HSLab/BEServices/PartnerServices/lms-registration-service/gradle/liquibase.gradle' line: 1

* What went wrong:
Could not compile script '/Users/dpatra/Downloads/HSLab/BEServices/PartnerServices/lms-registration-service/gradle/liquibase.gradle'.
> startup failed:
  script '/Users/dpatra/Downloads/HSLab/BEServices/PartnerServices/lms-registration-service/gradle/liquibase.gradle': 1: unable to resolve class org.liquibase.gradle.LiquibaseTask
   @ line 1, column 1.
     import org.liquibase.gradle.LiquibaseTask
     ^
  
  1 error

I need your help to address these two issues.

Thanks in advance!

You should add a package statement to your container service, like in any JVM code it is bad practice to have stuff in the default package.

And also for your plugins you should add a dot somewhere, either in the file-name or by using a package statement. Plugin IDs without namespace / dot should be preserved for built-in plugins, so for example use my.liquibase or liquibase.convention or whatever else.

I have added the logging inside MySQLContainerService.groovy but its not printing when I am running the gradle build.

Of course, that’s the point.
Info level logging is only shown when you use -i or --info to not pollute important messages.
A normal run only shows lifecycle level and higher, so that only really important message, warnings and errors are shown, so that you do not miss warnings or errors behind a wall of text easily.

If you really really want those messages without -i, use lifecycle level, if you use it even with -q, use quiet level.

I am getting the error

What you apply in your main builds build script or have in the main builds settings script is irrelevant here.
buildSrc is an own build - very similar to an included build - that is automatically built before your main build and prefixed to all build script classpaths.

To make the compile error go away, add an implementation dependency on the liquibase Gradle plugin to your buildSrc build script.

Besides that, by just assuming the liquibase plugin is applied before your liquibase convention plugin - and you do so by configuring the liquibase extension and configuring the liquibaseRuntime configuration, you have an ordering constraint which requires that your convention plugin must be applied after the liquibase plugin.
This is also discouraged bad practice.
You have two options there. Either apply the liquibase plugin from your liquibase convention plugin (unlike in legacy script plugins you can here also properly use the plugins { ... } block to apply a plugin), or use pluginManager.withPlugin(...) { ... } to execute the code as soon as the liquibase plugin is applied, if it is applied at all.
Both of these options remove the bad ordering requirement.


Now you just have to get rid of the ext/ extra property, and come to the light side (use Kotlin DSL) :slight_smile: