testcontainers-java icon indicating copy to clipboard operation
testcontainers-java copied to clipboard

java.lang.NoClassDefFoundError: com/amazonaws/auth/AWSCredentials with localstack module and AWS SDK2

Open joshiste opened this issue 4 years ago • 5 comments

When using the AWS SDK v2 the com.amazonaws.auth.AWSCredentials class is not present and a java.lang.NoClassDefFoundError is thrown.

Imho either the local stack module should declare a dependency on the AWS SDK v1 or not use the classes at all. (I'd prefer the latter).

my current workaround, is to add the classes just for tests:

        <dependency>
            <groupId>com.amazonaws</groupId>
            <artifactId>aws-java-sdk-core</artifactId>
            <version>1.11.959</version>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>*</groupId>
                    <artifactId>*</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

joshiste avatar Jul 12 '21 11:07 joshiste

This is affecting me too

wjlow avatar Sep 01 '21 01:09 wjlow

This is an issue for me too

mhball avatar Feb 15 '22 19:02 mhball

Same here, came to the same conclusion for the solution. Declaring the dependency would make this obvious up front.

sm-florian-widmer avatar Jul 05 '22 04:07 sm-florian-widmer

Looks like the problematic methods have already been marked as deprecated in #5489

dropofwill avatar Aug 02 '22 21:08 dropofwill

I got this issue too !

jxerome avatar Sep 09 '22 09:09 jxerome

This issue has been open for more than a year. Any updates? Any real solution other than importing AWS Java SDK v1 as a test-only dependency, even if you only want to work with v2?

DVader666 avatar Feb 03 '23 04:02 DVader666

There is a PR addressing the issue but it seems to be deferred until 2.0: https://github.com/testcontainers/testcontainers-java/pull/5827#issuecomment-1238270056

sm-florian-widmer avatar Feb 03 '23 07:02 sm-florian-widmer

I made my own!

import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.experimental.FieldDefaults;
import lombok.extern.slf4j.Slf4j;
import org.rnorth.ducttape.Preconditions;
import org.testcontainers.DockerClientFactory;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.utility.ComparableVersion;
import org.testcontainers.utility.DockerImageName;

import java.net.InetAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

@Slf4j
public class AwsSdkV2LocalStackContainer extends GenericContainer<AwsSdkV2LocalStackContainer> {

    static final int PORT = 4566;

    private static final String HOSTNAME_EXTERNAL_ENV_VAR = "HOSTNAME_EXTERNAL";

    private final List<EnabledService> services = new ArrayList<>();

    private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("localstack/localstack");

    private static final String DEFAULT_REGION = "us-east-1";

    /**
     * Whether or to assume that all APIs run on different ports (when <code>true</code>) or are
     * exposed on a single port (<code>false</code>). From the Localstack README:
     *
     * <blockquote>Note: Starting with version 0.11.0, all APIs are exposed via a single edge
     * service [...] The API-specific endpoints below are still left for backward-compatibility but
     * may get removed in a future release - please reconfigure your client SDKs to start using the
     * single edge endpoint URL!</blockquote>
     * <p>
     * Testcontainers will use the tag of the docker image to infer whether the used version
     * of Localstack supports this feature.
     */
    private final boolean legacyMode;

    /**
     * Starting with version 0.13.0, setting services list on Localstack is not required. When <code>false</code>,
     * containers are started lazily. When <code>true</code>, container fails to start if services list is not provided.
     *
     * Testcontainers will use the tag of the docker image to infer whether the used version
     * of Localstack required services list.
     */
    private final boolean servicesEnvVarRequired;

    /**
     * @param dockerImageName    image name to use for Localstack
     */
    public AwsSdkV2LocalStackContainer(final DockerImageName dockerImageName) {
        this(dockerImageName, shouldRunInLegacyMode(dockerImageName.getVersionPart()));
    }

    /**
     * @param dockerImageName    image name to use for Localstack
     * @param useLegacyMode      if true, each AWS service is exposed on a different port
     */
    public AwsSdkV2LocalStackContainer(final DockerImageName dockerImageName, boolean useLegacyMode) {
        super(dockerImageName);
        dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME);

        this.legacyMode = useLegacyMode;
        this.servicesEnvVarRequired = isServicesEnvVarRequired(dockerImageName.getVersionPart());

        withFileSystemBind(DockerClientFactory.instance().getRemoteDockerUnixSocketPath(), "/var/run/docker.sock");
        waitingFor(Wait.forLogMessage(".*Ready\\.\n", 1));
    }

    private static boolean isServicesEnvVarRequired(String version) {
        if (version.equals("latest")) {
            return false;
        }

        ComparableVersion comparableVersion = new ComparableVersion(version);
        if (comparableVersion.isSemanticVersion()) {
            return comparableVersion.isLessThan("0.13");
        }

        log.warn("Version {} is not a semantic version, services list is required.", version);

        return true;
    }

    private static boolean shouldRunInLegacyMode(String version) {
        if (version.equals("latest")) {
            return false;
        }

        ComparableVersion comparableVersion = new ComparableVersion(version);
        if (comparableVersion.isSemanticVersion()) {
            return comparableVersion.isLessThan("0.11");
        }

        log.warn("Version {} is not a semantic version, LocalStack will run in legacy mode.", version);
        log.warn(
            "Consider using \"LocalStackContainer(DockerImageName dockerImageName, boolean legacyMode)\" constructor if you want to disable legacy mode."
        );
        return true;
    }

    @Override
    protected void configure() {
        super.configure();

        if (servicesEnvVarRequired) {
            Preconditions.check("services list must not be empty", !services.isEmpty());
        }

        if (!services.isEmpty()) {
            withEnv("SERVICES", services.stream().map(EnabledService::getName).collect(Collectors.joining(",")));
            if (this.servicesEnvVarRequired) {
                withEnv("EAGER_SERVICE_LOADING", "1");
            }
        }

        String hostnameExternalReason;
        if (getEnvMap().containsKey(HOSTNAME_EXTERNAL_ENV_VAR)) {
            // do nothing
            hostnameExternalReason = "explicitly as environment variable";
        } else if (getNetwork() != null && getNetworkAliases().size() >= 1) {
            withEnv(HOSTNAME_EXTERNAL_ENV_VAR, getNetworkAliases().get(getNetworkAliases().size() - 1)); // use the last network alias set
            hostnameExternalReason = "to match last network alias on container with non-default network";
        } else {
            withEnv(HOSTNAME_EXTERNAL_ENV_VAR, getHost());
            hostnameExternalReason = "to match host-routable address for container";
        }
        logger()
            .info(
                "{} environment variable set to {} ({})",
                HOSTNAME_EXTERNAL_ENV_VAR,
                getEnvMap().get(HOSTNAME_EXTERNAL_ENV_VAR),
                hostnameExternalReason
            );

        exposePorts();
    }

    private void exposePorts() {
        if (legacyMode) {
            services.stream().map(this::getServicePort).distinct().forEach(this::addExposedPort);
        } else {
            this.addExposedPort(PORT);
        }
    }

    public AwsSdkV2LocalStackContainer withServices(Service... services) {
        this.services.addAll(Arrays.asList(services));
        return self();
    }

    /**
     * Declare a set of simulated AWS services that should be launched by this container.
     * @param services one or more service names
     * @return this container object
     */
    public AwsSdkV2LocalStackContainer withServices(EnabledService... services) {
        this.services.addAll(Arrays.asList(services));
        return self();
    }

    public URI getEndpointOverride(Service service) {
        return getEndpointOverride((EnabledService) service);
    }

    /**
     * Provides an endpoint override that is preconfigured to communicate with a given simulated service.
     * The provided endpoint override should be set in the AWS Java SDK v2 when building a client, e.g.:
     * <pre><code>S3Client s3 = S3Client
     .builder()
     .endpointOverride(localstack.getEndpointOverride(LocalStackContainer.Service.S3))
     .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create(
     localstack.getAccessKey(), localstack.getSecretKey()
     )))
     .region(Region.of(localstack.getRegion()))
     .build()
     </code></pre>
     * <p><strong>Please note that this method is only intended to be used for configuring AWS SDK clients
     * that are running on the test host. If other containers need to call this one, they should be configured
     * specifically to do so using a Docker network and appropriate addressing.</strong></p>
     *
     * @param service the service that is to be accessed
     * @return an {@link URI} endpoint override
     */
    public URI getEndpointOverride(EnabledService service) {
        try {
            final String address = getHost();
            String ipAddress = address;
            // resolve IP address and use that as the endpoint so that path-style access is automatically used for S3
            ipAddress = InetAddress.getByName(address).getHostAddress();
            return new URI("http://" + ipAddress + ":" + getMappedPort(getServicePort(service)));
        } catch (UnknownHostException | URISyntaxException e) {
            throw new IllegalStateException("Cannot obtain endpoint URL", e);
        }
    }

    private int getServicePort(EnabledService service) {
        return legacyMode ? service.getPort() : PORT;
    }

    /**
     * Provides a default access key that is preconfigured to communicate with a given simulated service.
     * The access key can be used to construct AWS SDK v2 clients:
     * <pre><code>S3Client s3 = S3Client
     .builder()
     .endpointOverride(localstack.getEndpointOverride(LocalStackContainer.Service.S3))
     .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create(
     localstack.getAccessKey(), localstack.getSecretKey()
     )))
     .region(Region.of(localstack.getRegion()))
     .build()
     </code></pre>
     * @return a default access key
     */
    public String getAccessKey() {
        return "accesskey";
    }

    /**
     * Provides a default secret key that is preconfigured to communicate with a given simulated service.
     * The secret key can be used to construct AWS SDK v2 clients:
     * <pre><code>S3Client s3 = S3Client
     .builder()
     .endpointOverride(localstack.getEndpointOverride(LocalStackContainer.Service.S3))
     .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create(
     localstack.getAccessKey(), localstack.getSecretKey()
     )))
     .region(Region.of(localstack.getRegion()))
     .build()
     </code></pre>
     * @return a default secret key
     */
    public String getSecretKey() {
        return "secretkey";
    }

    /**
     * Provides a default region that is preconfigured to communicate with a given simulated service.
     * The region can be used to construct AWS SDK v2 clients:
     * <pre><code>S3Client s3 = S3Client
     .builder()
     .endpointOverride(localstack.getEndpointOverride(LocalStackContainer.Service.S3))
     .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create(
     localstack.getAccessKey(), localstack.getSecretKey()
     )))
     .region(Region.of(localstack.getRegion()))
     .build()
     </code></pre>
     * @return a default region
     */
    public String getRegion() {
        return this.getEnvMap().getOrDefault("DEFAULT_REGION", DEFAULT_REGION);
    }

    public interface EnabledService {
        static EnabledService named(String name) {
            return () -> name;
        }

        String getName();

        default int getPort() {
            return PORT;
        }
    }

    @RequiredArgsConstructor
    @Getter
    @FieldDefaults(makeFinal = true)
    public enum Service implements EnabledService {
        API_GATEWAY("apigateway", 4567),
        EC2("ec2", 4597),
        KINESIS("kinesis", 4568),
        DYNAMODB("dynamodb", 4569),
        DYNAMODB_STREAMS("dynamodbstreams", 4570),
        // TODO: Clarify usage for ELASTICSEARCH and ELASTICSEARCH_SERVICE
        //        ELASTICSEARCH("es",           4571),
        S3("s3", 4572),
        FIREHOSE("firehose", 4573),
        LAMBDA("lambda", 4574),
        SNS("sns", 4575),
        SQS("sqs", 4576),
        REDSHIFT("redshift", 4577),
        //        ELASTICSEARCH_SERVICE("",   4578),
        SES("ses", 4579),
        ROUTE53("route53", 4580),
        CLOUDFORMATION("cloudformation", 4581),
        CLOUDWATCH("cloudwatch", 4582),
        SSM("ssm", 4583),
        SECRETSMANAGER("secretsmanager", 4584),
        STEPFUNCTIONS("stepfunctions", 4585),
        CLOUDWATCHLOGS("logs", 4586),
        STS("sts", 4592),
        IAM("iam", 4593),
        KMS("kms", 4599);

        String localStackName;

        int port;

        @Override
        public String getName() {
            return localStackName;
        }

        @Deprecated
        /*
            Since version 0.11, LocalStack exposes all services on a single (4566) port.
         */
        public int getPort() {
            return port;
        }
    }
}

charlie-harvey avatar Feb 16 '23 15:02 charlie-harvey

I see what you did there. Noticed that my version solved the problem just be removing deprecated constructors. Sweet.

charlie-harvey avatar Feb 16 '23 19:02 charlie-harvey