diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/dependencies/patches/awsv2sdk/Awsv2ClassPatcher.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/dependencies/patches/awsv2sdk/Awsv2ClassPatcher.java new file mode 100644 index 0000000000000..1e515afd8404b --- /dev/null +++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/dependencies/patches/awsv2sdk/Awsv2ClassPatcher.java @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch.gradle.internal.dependencies.patches.awsv2sdk; + +import org.elasticsearch.gradle.internal.dependencies.patches.PatcherInfo; +import org.elasticsearch.gradle.internal.dependencies.patches.Utils; +import org.gradle.api.artifacts.transform.CacheableTransform; +import org.gradle.api.artifacts.transform.InputArtifact; +import org.gradle.api.artifacts.transform.TransformAction; +import org.gradle.api.artifacts.transform.TransformOutputs; +import org.gradle.api.artifacts.transform.TransformParameters; +import org.gradle.api.file.FileSystemLocation; +import org.gradle.api.provider.Provider; +import org.gradle.api.tasks.Classpath; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.util.List; + +import static org.elasticsearch.gradle.internal.dependencies.patches.PatcherInfo.classPatcher; + +@CacheableTransform +public abstract class Awsv2ClassPatcher implements TransformAction { + + private static final String JAR_FILE_TO_PATCH = "aws-query-protocol"; + + private static final List CLASS_PATCHERS = List.of( + // This patcher is needed because of this AWS bug: https://github.com/aws/aws-sdk-java-v2/issues/5968 + // As soon as the bug is resolved and we upgrade our AWS SDK v2 libraries, we can remove this. + classPatcher( + "software/amazon/awssdk/protocols/query/internal/marshall/ListQueryMarshaller.class", + "213e84d9a745bdae4b844334d17aecdd6499b36df32aa73f82dc114b35043009", + StringFormatInPathResolverPatcher::new + ) + ); + + @Classpath + @InputArtifact + public abstract Provider getInputArtifact(); + + @Override + public void transform(@NotNull TransformOutputs outputs) { + File inputFile = getInputArtifact().get().getAsFile(); + + if (inputFile.getName().startsWith(JAR_FILE_TO_PATCH)) { + System.out.println("Patching " + inputFile.getName()); + File outputFile = outputs.file(inputFile.getName().replace(".jar", "-patched.jar")); + Utils.patchJar(inputFile, outputFile, CLASS_PATCHERS); + } else { + System.out.println("Skipping " + inputFile.getName()); + outputs.file(getInputArtifact()); + } + } +} diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/dependencies/patches/awsv2sdk/StringFormatInPathResolverPatcher.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/dependencies/patches/awsv2sdk/StringFormatInPathResolverPatcher.java new file mode 100644 index 0000000000000..506dab001dbe7 --- /dev/null +++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/dependencies/patches/awsv2sdk/StringFormatInPathResolverPatcher.java @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch.gradle.internal.dependencies.patches.awsv2sdk; + +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Type; + +import java.util.Locale; + +import static org.objectweb.asm.Opcodes.ASM9; +import static org.objectweb.asm.Opcodes.GETSTATIC; +import static org.objectweb.asm.Opcodes.INVOKESTATIC; + +class StringFormatInPathResolverPatcher extends ClassVisitor { + + StringFormatInPathResolverPatcher(ClassWriter classWriter) { + super(ASM9, classWriter); + } + + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + return new ReplaceCallMethodVisitor(super.visitMethod(access, name, descriptor, signature, exceptions)); + } + + /** + * Replaces calls to String.format(format, args); with calls to String.format(Locale.ROOT, format, args); + */ + private static class ReplaceCallMethodVisitor extends MethodVisitor { + private static final String CLASS_INTERNAL_NAME = Type.getInternalName(String.class); + private static final String METHOD_NAME = "format"; + private static final String OLD_METHOD_DESCRIPTOR = Type.getMethodDescriptor( + Type.getType(String.class), + Type.getType(String.class), + Type.getType(Object[].class) + ); + private static final String NEW_METHOD_DESCRIPTOR = Type.getMethodDescriptor( + Type.getType(String.class), + Type.getType(Locale.class), + Type.getType(String.class), + Type.getType(Object[].class) + ); + + private boolean foundFormatPattern = false; + + ReplaceCallMethodVisitor(MethodVisitor methodVisitor) { + super(ASM9, methodVisitor); + } + + @Override + public void visitLdcInsn(Object value) { + if (value instanceof String s && s.startsWith("%s")) { + if (foundFormatPattern) { + throw new IllegalStateException( + "A previous string format constant was not paired with a String.format() call. " + + "Patching would generate an unbalances stack" + ); + } + // Push the extra arg on the stack + mv.visitFieldInsn(GETSTATIC, Type.getInternalName(Locale.class), "ROOT", Type.getDescriptor(Locale.class)); + foundFormatPattern = true; + } + super.visitLdcInsn(value); + } + + @Override + public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { + if (opcode == INVOKESTATIC + && foundFormatPattern + && CLASS_INTERNAL_NAME.equals(owner) + && METHOD_NAME.equals(name) + && OLD_METHOD_DESCRIPTOR.equals(descriptor)) { + // Replace the call with String.format(Locale.ROOT, format, args) + mv.visitMethodInsn(INVOKESTATIC, CLASS_INTERNAL_NAME, METHOD_NAME, NEW_METHOD_DESCRIPTOR, false); + foundFormatPattern = false; + } else { + super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); + } + } + } +} diff --git a/plugins/discovery-ec2/build.gradle b/plugins/discovery-ec2/build.gradle index f4eb1d3a90f01..454508d0298f9 100644 --- a/plugins/discovery-ec2/build.gradle +++ b/plugins/discovery-ec2/build.gradle @@ -15,6 +15,31 @@ esplugin { classname ='org.elasticsearch.discovery.ec2.Ec2DiscoveryPlugin' } +def patched = Attribute.of('patched', Boolean) + +configurations { + compileClasspath { + attributes { + attribute(patched, true) + } + } + runtimeClasspath { + attributes { + attribute(patched, true) + } + } + testCompileClasspath { + attributes { + attribute(patched, true) + } + } + testRuntimeClasspath { + attributes { + attribute(patched, true) + } + } +} + dependencies { implementation "software.amazon.awssdk:annotations:${versions.awsv2sdk}" @@ -65,6 +90,17 @@ dependencies { testImplementation project(':test:fixtures:ec2-imds-fixture') internalClusterTestImplementation project(':test:fixtures:ec2-imds-fixture') + + attributesSchema { + attribute(patched) + } + artifactTypes.getByName("jar") { + attributes.attribute(patched, false) + } + registerTransform(org.elasticsearch.gradle.internal.dependencies.patches.awsv2sdk.Awsv2ClassPatcher) { + from.attribute(patched, false) + to.attribute(patched, true) + } } tasks.named("dependencyLicenses").configure { diff --git a/plugins/discovery-ec2/src/test/java/org/elasticsearch/discovery/ec2/Ec2DiscoveryTests.java b/plugins/discovery-ec2/src/test/java/org/elasticsearch/discovery/ec2/Ec2DiscoveryTests.java index a8a0c01838113..cbdbfab3ff41e 100644 --- a/plugins/discovery-ec2/src/test/java/org/elasticsearch/discovery/ec2/Ec2DiscoveryTests.java +++ b/plugins/discovery-ec2/src/test/java/org/elasticsearch/discovery/ec2/Ec2DiscoveryTests.java @@ -103,8 +103,7 @@ protected List buildDynamicHosts(Settings nodeSettings, int no final String[] params = request.split("&"); Arrays.stream(params).filter(entry -> entry.startsWith("Filter.") && entry.contains("=tag%3A")).forEach(entry -> { final int startIndex = "Filter.".length(); - // TODO ensure the filterId is an ASCII int when https://github.com/aws/aws-sdk-java-v2/issues/5968 fixed - final var filterId = entry.substring(startIndex, entry.indexOf(".", startIndex)); + final int filterId = Integer.parseInt(entry.substring(startIndex, entry.indexOf(".", startIndex))); tagsIncluded.put( entry.substring(entry.indexOf("=tag%3A") + "=tag%3A".length()), Arrays.stream(params)