azure-cosmos-db-emulator-docker icon indicating copy to clipboard operation
azure-cosmos-db-emulator-docker copied to clipboard

Emulator (preview) and TestContainers: not a happy marriage?

Open OnnoH opened this issue 10 months ago • 8 comments

For our local integration tests we use Testcontainers (https://testcontainers.com/modules/cosmodb/).

Depending on the CPU (our pipeline agents run on Intel, locally I have an Apple Silicon), the container image is selected:

private CosmosDBEmulatorContainer databaseContainer;

String cosmosDBEmulatorImage = "mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:latest";
String osArch = System.getProperty("os.arch");
if ("aarch64".equalsIgnoreCase(osArch) || "arm64".equalsIgnoreCase(osArch)) {
    cosmosDBEmulatorImage = "mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:vnext-preview";
}
databaseContainer = new CosmosDBEmulatorContainer(DockerImageName.parse(cosmosDBEmulatorImage))
        .withStartupTimeout(Duration.ofSeconds(120));
if ("aarch64".equalsIgnoreCase(osArch) || "arm64".equalsIgnoreCase(osArch)) {
    databaseContainer.withCommand("--protocol", "https")
            .waitingFor(Wait.forLogMessage(".*listening.*", 1));
}

databaseContainer.start();

That seems to work:

06:47:04.000 [main] INFO tc.mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:vnext-preview -- Creating container for image: mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:vnext-preview
06:47:04.042 [main] INFO tc.mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:vnext-preview -- Container mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:vnext-preview is starting: eeb9718ee468bd4f7d1e58db84a02a2fb168049f44447ab60d48786e24155c20
06:47:11.232 [main] INFO tc.mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:vnext-preview -- Container mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:vnext-preview started in PT7.232195S

and doing some certificate magic too.

String certificatePath = System.getProperty("java.io.tmpdir") + File.separator + "cosmosdev";
certList = KeyStoreHelper.extractPublicCerts(databaseContainer.getHost(), databaseContainer.getMappedPort(8081), certificatePath, ".pem");
KeyStoreHelper.createKeyStore(certList, trustStorePath, password);

However running the tests after spinning up the TestContainers ecosystem fail to connect to the CosmosDB container:

06:47:14.297+01:00  WARN 6676 --- [cosmosdb-app] [ctor-http-nio-3] c.a.c.i.RxGatewayStoreModel              : Network failure

io.netty.channel.AbstractChannel$AnnotatedConnectException: Connection refused: /127.0.0.1:8081
Caused by: java.net.ConnectException: Connection refused

The repositories are found:

2025-01-29T06:47:12.872+01:00  INFO 6676 --- [cosmosdb-app] [           main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data cosmos repositories in DEFAULT mode.
2025-01-29T06:47:12.962+01:00  INFO 6676 --- [cosmosdb-app] [           main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 86 ms. Found 4 cosmos repository interfaces.
2025-01-29T06:47:13.056+01:00  INFO 6676 --- [cosmosdb-app] [           main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data cosmos repositories in DEFAULT mode.
2025-01-29T06:47:13.064+01:00  INFO 6676 --- [cosmosdb-app] [           main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 7 ms. Found 0 cosmos repository interfaces.

Getting the database account also result in a response:

c.a.c.i.RxDocumentClientImpl             : Getting database account endpoint from https://localhost:61517
2025-01-29T06:47:14.239+01:00  INFO 6676 --- [cosmosdb-app] [ctor-http-nio-1] c.a.c.i.GlobalEndpointManager            : db account retrieved {"_self":"","id":"cosmosdev","_rid":"cosmosdev","media":"//media/","addresses":"//addresses/","_dbs":"//dbs/","enableMultipleWriteLocations":false,"writableLocations":[{"name":"Primary","databaseAccountEndpoint":"https://127.0.0.1:8081/"}],"readableLocations":[{"name":"Primary","databaseAccountEndpoint":"https://127.0.0.1:8081/"}],"userConsistencyPolicy":{"defaultConsistencyLevel":"Eventual"},"queryEngineConfiguration":"{\"sqlAllowSubQuery\":true,\"sqlAllowTop\":true,\"sqlAllowGroupByClause\":true,\"sqlAllowLike\":true,\"sqlAllowScalarSubQuery\":true,\"sqlAllowAggregateFunctions\":true,\"maxSpatialQueryCells\":2147483647,\"maxLogicalOrPerSqlQuery\":2147483647,\"maxLogicalAndPerSqlQuery\":2147483647,\"maxInExpressionItemsCount\":2147483647,\"enableSpatialIndexing\":true,\"sqlDisableOptimizationFlags\":0,\"sqlAllowNonFiniteNumbers\":false,\"spatialMaxGeometryPointCount\":256,\"queryMaxInMemorySortDocumentCount\":-500,\"maxUdfRefPerSqlQuery\":10,\"maxSqlQueryInputLength\":524288,\"maxQueryRequestTimeoutFraction\":0.9,\"maxJoinsPerSqlQuery\":10,\"allowNewKeywords\":true}"}

The client starts:

com.azure.cosmos.CosmosClientBuilder     : Cosmos Client with (Correlation) ID [00001] started up in [324] ms with the following configuration: serviceEndpoint [https://localhost:61517], preferredRegions [[]], excludedRegions [[]], connectionPolicy [ConnectionPolicy{httpNetworkRequestTimeout=PT1M, tcpNetworkRequestTimeout=PT5S, connectionMode=GATEWAY, maxConnectionPoolSize=100, idleHttpConnectionTimeout=PT24H, idleTcpConnectionTimeout=PT0S, userAgentSuffix='az-sd-cos/5.19.0', throttlingRetryOptions=RetryOptions{maxRetryAttemptsOnThrottledRequests=9, maxRetryWaitTime=PT30S}, endpointDiscoveryEnabled=true, preferredRegions=null, multipleWriteRegionsEnabled=true, proxyType=null, inetSocketProxyAddress=null, readRequestsFallbackEnabled=true, connectTimeout=PT5S, idleTcpEndpointTimeout=PT1H, maxConnectionsPerEndpoint=130, maxRequestsPerConnection=30, tcpConnectionEndpointRediscoveryEnabled=true, ioThreadPriority=5, ioThreadCountPerCoreFactor=2, tcpHealthCheckTimeoutDetectionEnabled=true, minConnectionPoolSizePerEndpoint=1, openConnectionsConcurrency=1, aggressiveWarmupConcurrency=12}], consistencyLevel [null], contentResponseOnWriteEnabled [true], sessionCapturingOverride [false], connectionSharingAcrossClients [false], clientTelemetryEnabled [false], proactiveContainerInit [null], diagnostics [{samplingRate=1.0, thresholds={pointOperationLatencyThreshold=PT1S, nonPointOperationLatencyThreshold=PT3S, requestChargeThreshold=1000.0, payloadSizeInBytesThreshold=2147483647, customFailureHandler=false}, clientCorrelationId=null, clientTelemetryEnabled=false, clientMetricsEnabled=true, transportLevelTracingEnabled=false, showQueryMode=None, customTracerProvided=false, customDiagnosticHandlers=(com.azure.cosmos.CosmosDiagnosticsLogger)}], tracing [true, false, com.azure.cosmos.implementation.DiagnosticsProvider.EnabledNoOpTracer, [com.azure.cosmos.implementation.clienttelemetry.ClientMetricsDiagnosticsHandler, com.azure.cosmos.CosmosDiagnosticsLogger]], nativeTransport [false] fastClientOpen [false] isRegionScopedSessionCapturingEnabled [false]

The property is set:

System.setProperty("spring.cloud.azure.cosmos.endpoint", "https://"+databaseContainer.getHost()+":"+databaseContainer.getMappedPort(8081));

Now the response from Getting database account endpoint worries me:

"databaseAccountEndpoint":"https://127.0.0.1:8081/"

What am I doing wrong?

OnnoH avatar Jan 29 '25 15:01 OnnoH

We are committed to make this work. We will review and see what we cna do to make this more smooth.

xgerman avatar Jan 29 '25 18:01 xgerman

We are committed to make this work. We will review and see what we cna do to make this more smooth.

That would be nice ;-) Let me know if I can help.

OnnoH avatar Jan 30 '25 06:01 OnnoH

@OnnoH May I ask if you use direct mode or gateway mode to visit the localhost endpoint? This is an article about these two modes with Java SDK: https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/tune-connection-configurations-java-sdk-v4?tabs=api-async#direct-connection-mode

lionelc avatar Feb 04 '25 07:02 lionelc

@lionelc I'm using 'gateway mode' as suggested in #158 . Maybe this issue is also related to #159 ?

OnnoH avatar Feb 04 '25 07:02 OnnoH

Hello, @OnnoH i was able to configure Java Testcontainers to be working with CosmosEmulator VNext image over https. Take a look, might be it would be helpful to you

Key points:

  1. Certificate should be extracted/copied from image /scripts/certs/domain.crt and /scripts/certs/rootCA.crt to host machine
  2. Certificates should we saved to trustStore
  3. Gateway mode should be used
  4. ⚠️ Exposed port must be equals to internal port (by default testcontainer exposes on random port that doesn't match internal). this could be fixed using following snippet
            this.port = getRandomAvailablePort();
            this.withEnv(Map.of(
                "PORT", String.valueOf(port),
                "PROTOCOL", "https"
            ));
            this.withCreateContainerCmdModifier(cmd -> cmd.withHostConfig(
                new HostConfig().withPortBindings(new PortBinding(Ports.Binding.bindPort(port), new ExposedPort(port)))
            ));
    
    For some reason it is important for cosmos-client even if you run regular docker image without testcontainers
    docker run --publish 56001:56001 --publish 1234:1234 \
      mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:vnext-preview \
      --protocol https --port 56001
    

You can see full example here: https://github.com/bitxon/java-cosmos-emulator-vnext-experiment

P.S for me this approach with VNext works on Ubuntu, Mac (arm), Windows

bitxon avatar Feb 15 '25 16:02 bitxon

@bitxon Thanks for this update! I also managed to get it working by adding the custom container class. In my Spring Boot app, only a keystore with the extracted certificates did not work: I needed to add them to the cacerts.

OnnoH avatar Feb 18 '25 18:02 OnnoH

@bitxon Thank you for trying and working it around. We have been working to make the VNext emulator directly compatible with CosmosDBEmulatorContainer with testcontainers-java. While the certs handling needs to be explicit now, the old class CosmosDBEmulatorContainer could still be used with additional options:

new CosmosDBEmulatorContainer( DockerImageName.parse("mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:vnext-preview") ) .withExposedPorts(port) ...

Stay tuned as we will make our updates on this.

lionelc avatar Feb 19 '25 16:02 lionelc

Hi guys, any updates if CosmosDBEmulatorContainer could now be tested using testcontainers when running on the apple silicone machine?

valaikamartynas avatar Sep 21 '25 15:09 valaikamartynas