diff --git a/pom.xml b/pom.xml index c25aa7ac..e1bf4a36 100644 --- a/pom.xml +++ b/pom.xml @@ -27,6 +27,7 @@ trino-aws-proxy + trino-aws-proxy-glue trino-aws-proxy-spark3 trino-aws-proxy-spark4 trino-aws-proxy-spi @@ -84,6 +85,19 @@ ${project.version} + + ${project.groupId} + trino-aws-proxy + ${project.version} + test-jar + + + + ${project.groupId} + trino-aws-proxy-glue + ${project.version} + + ${project.groupId} trino-aws-proxy-spark3 @@ -186,6 +200,18 @@ + + software.amazon.awssdk + glue + ${dep.aws-sdk.version} + + + commons-logging + commons-logging + + + + software.amazon.awssdk s3 diff --git a/trino-aws-proxy-glue/README.md b/trino-aws-proxy-glue/README.md new file mode 100644 index 00000000..ee3f100d --- /dev/null +++ b/trino-aws-proxy-glue/README.md @@ -0,0 +1,69 @@ +# AWS Glue Emulation + +Implementation of the AWS Glue endpoint and model serialization. Can be used as part of the +Trino AWS S3 Proxy or as part of a separate/standalone project. + +## As part of Trino AWS S3 Proxy + +Including the `trino-aws-proxy-glue` dependency will automatically add the AWS Glue plugin (via the +JDK Service Loader). See [configration and binding](#configuration-and-binding) below for more details. + +## As part of a separate/standalone project + +The AWS Glue endpoint implementation can be added to any Airlift application by installing +the `TrinoStandaloneGlueModule`. + +## Configuration and binding + +### Endpoint + +The path to the Glue endpoint can be configured via the `aws.proxy.glue.path` configuration property. + +### Handler + +Bind an instance of `GlueRequestHandler` to handle Glue requests. Use the +`GlueRequestHandlerBinding` utility to do the binding. Your `GlueRequestHandler` +should examine the `ParsedGlueRequest` and handle any Glue requests and +return a Glue response. + +E.g. + +```java +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.Response; +import software.amazon.awssdk.services.glue.model.GetDatabasesRequest; +import software.amazon.awssdk.services.glue.model.GetDatabasesResponse; + +public class MyGlueRequestHandler + implements GlueRequestHandler +{ + @Override + public GlueResponse handle(ParsedGlueRequest request) + { + // validate credentials, etc. via: request.requestAuthorization() + + return switch (request.glueRequest()) { + case GetDatabasesRequest getDatabasesRequest -> { + // handle a GetDatabases request + + yield new GetDatabasesResponse.builder() + // ... + .build(); + } + + // ... + + default -> throw new WebApplicationException(NOT_FOUND); + }; + } +} +``` + +E.g bind your handler + +```java +Module module = binder -> { + glueRequestHandlerBinding(binder) + .bind(binding -> binding.to(MyGlueRequestHandler.class)); +}; +``` diff --git a/trino-aws-proxy-glue/pom.xml b/trino-aws-proxy-glue/pom.xml new file mode 100644 index 00000000..733590cc --- /dev/null +++ b/trino-aws-proxy-glue/pom.xml @@ -0,0 +1,173 @@ + + + 4.0.0 + + io.trino + trino-aws-proxy-root + 4-SNAPSHOT + + + trino-aws-proxy-glue + + + ${project.parent.basedir} + + + + + ${project.groupId} + trino-aws-proxy + + + + ${project.groupId} + trino-aws-proxy-spi + + + + com.fasterxml.jackson.core + jackson-core + + + + com.fasterxml.jackson.core + jackson-databind + + + + com.google.guava + guava + + + + com.google.inject + guice + + + + io.airlift + configuration + + + + io.airlift + jaxrs + + + + jakarta.validation + jakarta.validation-api + + + + jakarta.ws.rs + jakarta.ws.rs-api + + + + software.amazon.awssdk + glue + + + + software.amazon.awssdk + sdk-core + + + + software.amazon.awssdk + utils + + + + io.airlift + bootstrap + runtime + + + + io.airlift + http-server + runtime + + + + io.airlift + json + runtime + + + + io.airlift + node + runtime + + + + jakarta.annotation + jakarta.annotation-api + runtime + + + + software.amazon.awssdk + auth + runtime + + + + software.amazon.awssdk + aws-core + runtime + + + + software.amazon.awssdk + regions + runtime + + + + ${project.groupId} + trino-aws-proxy + test-jar + test + + + + org.assertj + assertj-core + test + + + + org.junit.jupiter + junit-jupiter-api + test + + + + org.testcontainers + minio + test + + + + org.testcontainers + postgresql + test + + + + org.testcontainers + testcontainers + test + + + + software.amazon.awssdk + s3 + test + + + diff --git a/trino-aws-proxy-glue/src/main/java/io/trino/aws/proxy/glue/TrinoGlueConfig.java b/trino-aws-proxy-glue/src/main/java/io/trino/aws/proxy/glue/TrinoGlueConfig.java new file mode 100644 index 00000000..6d97ba98 --- /dev/null +++ b/trino-aws-proxy-glue/src/main/java/io/trino/aws/proxy/glue/TrinoGlueConfig.java @@ -0,0 +1,37 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.aws.proxy.glue; + +import io.airlift.configuration.Config; +import io.airlift.configuration.ConfigDescription; +import jakarta.validation.constraints.NotNull; + +public class TrinoGlueConfig +{ + private String gluePath = "/api/v1/glue"; + + @NotNull + public String getGluePath() + { + return gluePath; + } + + @Config("aws.proxy.glue.path") + @ConfigDescription("URL Path for Glue operations, optional") + public TrinoGlueConfig setGluePath(String gluePath) + { + this.gluePath = gluePath; + return this; + } +} diff --git a/trino-aws-proxy-glue/src/main/java/io/trino/aws/proxy/glue/TrinoGlueModule.java b/trino-aws-proxy-glue/src/main/java/io/trino/aws/proxy/glue/TrinoGlueModule.java new file mode 100644 index 00000000..74dbba39 --- /dev/null +++ b/trino-aws-proxy-glue/src/main/java/io/trino/aws/proxy/glue/TrinoGlueModule.java @@ -0,0 +1,46 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.aws.proxy.glue; + +import com.google.inject.Binder; +import io.airlift.configuration.AbstractConfigurationAwareModule; +import io.trino.aws.proxy.glue.handler.GlueRequestHandler; +import io.trino.aws.proxy.glue.rest.ModelLoader; +import io.trino.aws.proxy.glue.rest.TrinoGlueResource; +import jakarta.ws.rs.WebApplicationException; + +import static com.google.inject.multibindings.OptionalBinder.newOptionalBinder; +import static io.airlift.jaxrs.JaxrsBinder.jaxrsBinder; +import static io.trino.aws.proxy.server.TrinoAwsProxyServerModule.bindResourceAtPath; +import static jakarta.ws.rs.core.Response.Status.NOT_FOUND; + +public class TrinoGlueModule + extends AbstractConfigurationAwareModule +{ + @Override + protected void setup(Binder binder) + { + ModelLoader modelLoader = new ModelLoader(); + modelLoader.bindSerializers(binder); + binder.bind(ModelLoader.class).toInstance(modelLoader); + + TrinoGlueConfig trinoGlueConfig = buildConfigObject(TrinoGlueConfig.class); + bindResourceAtPath(jaxrsBinder(binder), TrinoGlueResource.class, trinoGlueConfig.getGluePath()); + + newOptionalBinder(binder, GlueRequestHandler.class) + .setDefault().toInstance((_, _, _) -> { + throw new WebApplicationException(NOT_FOUND); + }); + } +} diff --git a/trino-aws-proxy-glue/src/main/java/io/trino/aws/proxy/glue/TrinoGluePlugin.java b/trino-aws-proxy-glue/src/main/java/io/trino/aws/proxy/glue/TrinoGluePlugin.java new file mode 100644 index 00000000..d303db7d --- /dev/null +++ b/trino-aws-proxy-glue/src/main/java/io/trino/aws/proxy/glue/TrinoGluePlugin.java @@ -0,0 +1,27 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.aws.proxy.glue; + +import com.google.inject.Module; +import io.trino.aws.proxy.spi.plugin.TrinoAwsProxyServerPlugin; + +public class TrinoGluePlugin + implements TrinoAwsProxyServerPlugin +{ + @Override + public Module module() + { + return new TrinoGlueModule(); + } +} diff --git a/trino-aws-proxy-glue/src/main/java/io/trino/aws/proxy/glue/TrinoStandaloneGlueModule.java b/trino-aws-proxy-glue/src/main/java/io/trino/aws/proxy/glue/TrinoStandaloneGlueModule.java new file mode 100644 index 00000000..f2e70598 --- /dev/null +++ b/trino-aws-proxy-glue/src/main/java/io/trino/aws/proxy/glue/TrinoStandaloneGlueModule.java @@ -0,0 +1,41 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.aws.proxy.glue; + +import com.google.inject.Binder; +import io.airlift.configuration.AbstractConfigurationAwareModule; +import io.trino.aws.proxy.server.credentials.CredentialsModule; +import io.trino.aws.proxy.server.rest.RestModule; +import io.trino.aws.proxy.server.signing.SigningModule; +import io.trino.aws.proxy.spi.remote.RemoteUriFacade; + +import static com.google.inject.multibindings.OptionalBinder.newOptionalBinder; + +// intended for use-cases when the TrinoAwsProxyServer isn't desired +public class TrinoStandaloneGlueModule + extends AbstractConfigurationAwareModule +{ + @Override + protected void setup(Binder binder) + { + newOptionalBinder(binder, RemoteUriFacade.class).setDefault().toInstance(_ -> { + throw new UnsupportedOperationException(); + }); + + install(new TrinoGlueModule()); + install(new SigningModule()); + install(new RestModule()); + install(new CredentialsModule()); + } +} diff --git a/trino-aws-proxy-glue/src/main/java/io/trino/aws/proxy/glue/handler/GlueRequestHandler.java b/trino-aws-proxy-glue/src/main/java/io/trino/aws/proxy/glue/handler/GlueRequestHandler.java new file mode 100644 index 00000000..47a56b16 --- /dev/null +++ b/trino-aws-proxy-glue/src/main/java/io/trino/aws/proxy/glue/handler/GlueRequestHandler.java @@ -0,0 +1,24 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.aws.proxy.glue.handler; + +import io.trino.aws.proxy.glue.rest.ParsedGlueRequest; +import io.trino.aws.proxy.server.rest.RequestLoggingSession; +import io.trino.aws.proxy.spi.signing.SigningMetadata; +import software.amazon.awssdk.services.glue.model.GlueResponse; + +public interface GlueRequestHandler +{ + GlueResponse handleRequest(ParsedGlueRequest request, SigningMetadata signingMetadata, RequestLoggingSession requestLoggingSession); +} diff --git a/trino-aws-proxy-glue/src/main/java/io/trino/aws/proxy/glue/handler/GlueRequestHandlerBinding.java b/trino-aws-proxy-glue/src/main/java/io/trino/aws/proxy/glue/handler/GlueRequestHandlerBinding.java new file mode 100644 index 00000000..285dad7d --- /dev/null +++ b/trino-aws-proxy-glue/src/main/java/io/trino/aws/proxy/glue/handler/GlueRequestHandlerBinding.java @@ -0,0 +1,31 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.aws.proxy.glue.handler; + +import com.google.inject.Binder; +import com.google.inject.binder.LinkedBindingBuilder; + +import java.util.function.Consumer; + +import static com.google.inject.multibindings.OptionalBinder.newOptionalBinder; + +public interface GlueRequestHandlerBinding +{ + void bind(Consumer> binding); + + static GlueRequestHandlerBinding glueRequestHandlerBinding(Binder binder) + { + return consumer -> consumer.accept(newOptionalBinder(binder, GlueRequestHandler.class).setBinding()); + } +} diff --git a/trino-aws-proxy-glue/src/main/java/io/trino/aws/proxy/glue/rest/GlueDeserializer.java b/trino-aws-proxy-glue/src/main/java/io/trino/aws/proxy/glue/rest/GlueDeserializer.java new file mode 100644 index 00000000..f9ecd375 --- /dev/null +++ b/trino-aws-proxy-glue/src/main/java/io/trino/aws/proxy/glue/rest/GlueDeserializer.java @@ -0,0 +1,48 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.aws.proxy.glue.rest; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import software.amazon.awssdk.core.SdkField; + +import java.io.IOException; +import java.util.Map; + +class GlueDeserializer + extends JsonDeserializer +{ + private final SerializerCommon serializerCommon; + + GlueDeserializer(Class requestClass) + { + serializerCommon = new SerializerCommon(requestClass); + } + + @SuppressWarnings("unchecked") + @Override + public T deserialize(JsonParser parser, DeserializationContext context) + throws IOException + { + Object builder = serializerCommon.newBuilder(); + + Map value = parser.readValueAs(new TypeReference<>() {}); + for (SdkField sdkField : serializerCommon.sdkFields()) { + sdkField.set(builder, value.get(sdkField.memberName())); + } + return (T) serializerCommon.build(builder); + } +} diff --git a/trino-aws-proxy-glue/src/main/java/io/trino/aws/proxy/glue/rest/GlueResourceSecurity.java b/trino-aws-proxy-glue/src/main/java/io/trino/aws/proxy/glue/rest/GlueResourceSecurity.java new file mode 100644 index 00000000..7a7f9c43 --- /dev/null +++ b/trino-aws-proxy-glue/src/main/java/io/trino/aws/proxy/glue/rest/GlueResourceSecurity.java @@ -0,0 +1,29 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.aws.proxy.glue.rest; + +import io.trino.aws.proxy.server.rest.ResourceSecurity.SigV4AccessType; +import io.trino.aws.proxy.spi.signing.SigningServiceType; + +public final class GlueResourceSecurity + implements SigV4AccessType +{ + private static final SigningServiceType GLUE = new SigningServiceType("glue"); + + @Override + public SigningServiceType signingServiceType() + { + return GLUE; + } +} diff --git a/trino-aws-proxy-glue/src/main/java/io/trino/aws/proxy/glue/rest/GlueSerializer.java b/trino-aws-proxy-glue/src/main/java/io/trino/aws/proxy/glue/rest/GlueSerializer.java new file mode 100644 index 00000000..959479f2 --- /dev/null +++ b/trino-aws-proxy-glue/src/main/java/io/trino/aws/proxy/glue/rest/GlueSerializer.java @@ -0,0 +1,48 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.aws.proxy.glue.rest; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import software.amazon.awssdk.core.SdkField; + +import java.io.IOException; + +class GlueSerializer + extends JsonSerializer +{ + private final SerializerCommon serializerCommon; + + GlueSerializer(Class requestClass) + { + serializerCommon = new SerializerCommon(requestClass); + } + + @Override + public void serialize(T value, JsonGenerator generator, SerializerProvider serializers) + throws IOException + { + generator.writeStartObject(); + + for (SdkField sdkField : serializerCommon.sdkFields()) { + Object fieldValue = sdkField.getValueOrDefault(value); + if (fieldValue != null) { + generator.writePOJOField(sdkField.memberName(), fieldValue); + } + } + + generator.writeEndObject(); + } +} diff --git a/trino-aws-proxy-glue/src/main/java/io/trino/aws/proxy/glue/rest/ModelLoader.java b/trino-aws-proxy-glue/src/main/java/io/trino/aws/proxy/glue/rest/ModelLoader.java new file mode 100644 index 00000000..63704849 --- /dev/null +++ b/trino-aws-proxy-glue/src/main/java/io/trino/aws/proxy/glue/rest/ModelLoader.java @@ -0,0 +1,98 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.aws.proxy.glue.rest; + +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.reflect.ClassPath; +import com.google.inject.Binder; +import com.google.inject.TypeLiteral; +import com.google.inject.multibindings.MapBinder; +import software.amazon.awssdk.core.SdkPojo; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.lang.reflect.Modifier; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +public class ModelLoader +{ + private static final String REQUEST_SUFFIX = "Request"; + private static final String REQUEST_PREFIX = "AWSGlue."; + + private static final LoadedClasses loadedClasses = new LoadedClasses(); + + private static class LoadedClasses + { + private final Map> requestClasses; + private final Set> modelClasses; + + private LoadedClasses() + { + Map> requestClasses = new HashMap<>(); + Set> modelClasses = new HashSet<>(); + + try { + ClassPath.from(ClassLoader.getSystemClassLoader()) + .getTopLevelClasses("software.amazon.awssdk.services.glue.model") + .stream() + .map(ClassPath.ClassInfo::load) + .filter(modelClass -> !Modifier.isAbstract(modelClass.getModifiers())) + .filter(SdkPojo.class::isAssignableFrom) + .forEach(modelClass -> { + modelClasses.add(modelClass); + + if (modelClass.getSimpleName().endsWith(REQUEST_SUFFIX)) { + requestClasses.put(targetFromRequest(modelClass), modelClass); + } + }); + } + catch (IOException e) { + throw new UncheckedIOException(e); + } + + this.requestClasses = ImmutableMap.copyOf(requestClasses); + this.modelClasses = ImmutableSet.copyOf(modelClasses); + } + } + + public void bindSerializers(Binder binder) + { + MapBinder, JsonDeserializer> deserializerBinder = MapBinder.newMapBinder(binder, new TypeLiteral<>() {}, new TypeLiteral<>() {}); + MapBinder, JsonSerializer> serializerBinder = MapBinder.newMapBinder(binder, new TypeLiteral<>() {}, new TypeLiteral<>() {}); + + loadedClasses.modelClasses.forEach(modelClass -> { + deserializerBinder.addBinding(modelClass).toInstance(new GlueDeserializer<>(modelClass)); + serializerBinder.addBinding(modelClass).toInstance(new GlueSerializer<>(modelClass)); + }); + } + + public Optional> requestClass(String target) + { + return Optional.ofNullable(loadedClasses.requestClasses.get(target)); + } + + private static String targetFromRequest(Class requestClass) + { + String baseName = requestClass.getSimpleName(); + String operationName = baseName.substring(0, baseName.length() - REQUEST_SUFFIX.length()); + return REQUEST_PREFIX + operationName; + } +} diff --git a/trino-aws-proxy-glue/src/main/java/io/trino/aws/proxy/glue/rest/ParsedGlueRequest.java b/trino-aws-proxy-glue/src/main/java/io/trino/aws/proxy/glue/rest/ParsedGlueRequest.java new file mode 100644 index 00000000..932daedf --- /dev/null +++ b/trino-aws-proxy-glue/src/main/java/io/trino/aws/proxy/glue/rest/ParsedGlueRequest.java @@ -0,0 +1,46 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.aws.proxy.glue.rest; + +import io.trino.aws.proxy.spi.rest.RequestHeaders; +import io.trino.aws.proxy.spi.signing.RequestAuthorization; +import io.trino.aws.proxy.spi.util.ImmutableMultiMap; +import io.trino.aws.proxy.spi.util.MultiMap; +import software.amazon.awssdk.services.glue.model.GlueRequest; + +import java.time.Instant; +import java.util.UUID; + +import static java.util.Objects.requireNonNull; + +public record ParsedGlueRequest( + UUID requestId, + RequestAuthorization requestAuthorization, + Instant requestDate, + String target, + GlueRequest glueRequest, + RequestHeaders requestHeaders, + MultiMap queryParameters) +{ + public ParsedGlueRequest + { + requireNonNull(requestId, "requestId is null"); + requireNonNull(requestAuthorization, "requestAuthorization is null"); + requireNonNull(requestDate, "requestDate is null"); + requireNonNull(target, "target is null"); + requireNonNull(glueRequest, "glueRequest is null"); + requireNonNull(requestHeaders, "requestHeaders is null"); + queryParameters = ImmutableMultiMap.copyOf(queryParameters); + } +} diff --git a/trino-aws-proxy-glue/src/main/java/io/trino/aws/proxy/glue/rest/SerializerCommon.java b/trino-aws-proxy-glue/src/main/java/io/trino/aws/proxy/glue/rest/SerializerCommon.java new file mode 100644 index 00000000..dd469180 --- /dev/null +++ b/trino-aws-proxy-glue/src/main/java/io/trino/aws/proxy/glue/rest/SerializerCommon.java @@ -0,0 +1,80 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.aws.proxy.glue.rest; + +import software.amazon.awssdk.core.SdkField; +import software.amazon.awssdk.core.SdkPojo; +import software.amazon.awssdk.core.SdkResponse; +import software.amazon.awssdk.utils.builder.Buildable; + +import java.lang.reflect.Method; +import java.util.List; +import java.util.function.Function; +import java.util.function.Supplier; + +import static java.util.Objects.requireNonNull; + +class SerializerCommon +{ + private final Supplier builderSupplier; + private final List> sdkFields; + private final Function builder; + + SerializerCommon(Class clazz) + { + requireNonNull(clazz, "clazz is null"); + + try { + Method builderMethod = clazz.getMethod("builder"); + builderSupplier = () -> { + try { + return (SdkPojo) builderMethod.invoke(null); + } + catch (Exception e) { + throw new RuntimeException(e); + } + }; + + SdkPojo builderAndPojo = builderSupplier.get(); + sdkFields = builderAndPojo.sdkFields(); + + // different AWS models have different builders. There's no common base. + // So, find the right builder + builder = switch (builderAndPojo) { + case Buildable _ -> o -> ((Buildable) o).build(); + case SdkResponse.Builder _ -> o -> ((SdkResponse.Builder) o).build(); + default -> throw new RuntimeException("Unexpected builder type: " + builderAndPojo); + }; + } + catch (Exception e) { + // TODO + throw new RuntimeException(e); + } + } + + Object newBuilder() + { + return builderSupplier.get(); + } + + List> sdkFields() + { + return sdkFields; + } + + Object build(Object o) + { + return builder.apply(o); + } +} diff --git a/trino-aws-proxy-glue/src/main/java/io/trino/aws/proxy/glue/rest/TrinoGlueResource.java b/trino-aws-proxy-glue/src/main/java/io/trino/aws/proxy/glue/rest/TrinoGlueResource.java new file mode 100644 index 00000000..ef64cc40 --- /dev/null +++ b/trino-aws-proxy-glue/src/main/java/io/trino/aws/proxy/glue/rest/TrinoGlueResource.java @@ -0,0 +1,99 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.aws.proxy.glue.rest; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.inject.Inject; +import io.trino.aws.proxy.glue.handler.GlueRequestHandler; +import io.trino.aws.proxy.server.rest.RequestLoggingSession; +import io.trino.aws.proxy.server.rest.ResourceSecurity; +import io.trino.aws.proxy.spi.rest.Request; +import io.trino.aws.proxy.spi.signing.SigningMetadata; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import software.amazon.awssdk.services.glue.model.GlueRequest; +import software.amazon.awssdk.services.glue.model.GlueResponse; + +import java.io.InputStream; + +import static jakarta.ws.rs.core.Response.Status.BAD_REQUEST; +import static jakarta.ws.rs.core.Response.Status.NOT_FOUND; +import static java.util.Objects.requireNonNull; + +@ResourceSecurity(GlueResourceSecurity.class) +public class TrinoGlueResource +{ + private final ObjectMapper objectMapper; + private final GlueRequestHandler requestHandler; + private final ModelLoader modelLoader; + + @Inject + public TrinoGlueResource(ObjectMapper objectMapper, GlueRequestHandler requestHandler, ModelLoader modelLoader) + { + this.objectMapper = requireNonNull(objectMapper, "objectMapper is null"); + this.requestHandler = requireNonNull(requestHandler, "requestHandler is null"); + this.modelLoader = requireNonNull(modelLoader, "modelLoader is null"); + } + + @POST + @Produces(MediaType.APPLICATION_JSON) + public Response gluePost(@Context Request request, @Context SigningMetadata signingMetadata, @Context RequestLoggingSession requestLoggingSession) + { + requestLoggingSession.logProperty("request.glue.emulated.key", signingMetadata.credentials().emulated().secretKey()); + + String target = request.requestHeaders().unmodifiedHeaders().getFirst("x-amz-target") + .orElseThrow(() -> new WebApplicationException(BAD_REQUEST)); + + requestLoggingSession.logProperty("request.glue.target", target); + + Class requestClass = modelLoader.requestClass(target) + .orElseThrow(() -> new WebApplicationException(NOT_FOUND)); + GlueRequest glueRequest = unmarshal(request, requestClass, objectMapper); + + ParsedGlueRequest parsedGlueRequest = new ParsedGlueRequest( + request.requestId(), + request.requestAuthorization(), + request.requestDate(), + target, + glueRequest, + request.requestHeaders(), + request.requestQueryParameters()); + + GlueResponse glueResponse; + try { + glueResponse = requestHandler.handleRequest(parsedGlueRequest, signingMetadata, requestLoggingSession); + } + catch (Exception e) { + requestLoggingSession.logException(e); + throw e; + } + + return Response.ok(glueResponse).build(); + } + + private GlueRequest unmarshal(Request request, Class clazz, ObjectMapper objectMapper) + { + try { + InputStream inputStream = request.requestContent().inputStream().orElseThrow(() -> new WebApplicationException(BAD_REQUEST)); + return (GlueRequest) objectMapper.readValue(inputStream, clazz); + } + catch (Exception e) { + throw new WebApplicationException(BAD_REQUEST); + } + } +} diff --git a/trino-aws-proxy-glue/src/main/resources/META-INF/services/io.trino.aws.proxy.spi.plugin.TrinoAwsProxyServerPlugin b/trino-aws-proxy-glue/src/main/resources/META-INF/services/io.trino.aws.proxy.spi.plugin.TrinoAwsProxyServerPlugin new file mode 100644 index 00000000..e3bc96d9 --- /dev/null +++ b/trino-aws-proxy-glue/src/main/resources/META-INF/services/io.trino.aws.proxy.spi.plugin.TrinoAwsProxyServerPlugin @@ -0,0 +1 @@ +io.trino.aws.proxy.glue.TrinoGluePlugin \ No newline at end of file diff --git a/trino-aws-proxy-glue/src/test/java/io/trino/aws/proxy/glue/TestGlueBase.java b/trino-aws-proxy-glue/src/test/java/io/trino/aws/proxy/glue/TestGlueBase.java new file mode 100644 index 00000000..8b252c12 --- /dev/null +++ b/trino-aws-proxy-glue/src/test/java/io/trino/aws/proxy/glue/TestGlueBase.java @@ -0,0 +1,83 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.aws.proxy.glue; + +import io.trino.aws.proxy.spi.credentials.Credentials; +import jakarta.annotation.PreDestroy; +import jakarta.ws.rs.core.UriBuilder; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.glue.GlueClient; +import software.amazon.awssdk.services.glue.model.Database; +import software.amazon.awssdk.services.glue.model.GetDatabasesRequest; +import software.amazon.awssdk.services.glue.model.GetDatabasesResponse; +import software.amazon.awssdk.services.glue.model.GetResourcePoliciesRequest; +import software.amazon.awssdk.services.glue.model.GetResourcePoliciesResponse; +import software.amazon.awssdk.services.glue.model.GluePolicy; + +import java.net.URI; + +import static io.trino.aws.proxy.glue.TestingGlueRequestHandler.DATABASE_1; +import static io.trino.aws.proxy.glue.TestingGlueRequestHandler.DATABASE_2; +import static io.trino.aws.proxy.glue.TestingGlueRequestHandler.POLICY_A; +import static io.trino.aws.proxy.glue.TestingGlueRequestHandler.POLICY_B; +import static java.util.Objects.requireNonNull; +import static org.assertj.core.api.Assertions.assertThat; + +public abstract class TestGlueBase +{ + protected final GlueClient glueClient; + protected final T context; + + protected TestGlueBase(TrinoGlueConfig config, Credentials testingCredentials, T context) + { + this.context = requireNonNull(context, "context is null"); + + URI uri = UriBuilder.fromUri(context.baseUrl()).path(config.getGluePath()).build(); + AwsBasicCredentials awsBasicCredentials = AwsBasicCredentials.create(testingCredentials.emulated().accessKey(), testingCredentials.emulated().secretKey()); + + glueClient = GlueClient.builder() + .credentialsProvider(StaticCredentialsProvider.create(awsBasicCredentials)) + .region(Region.US_EAST_1) + .endpointOverride(uri) + .build(); + } + + protected T context() + { + return context; + } + + @PreDestroy + public void shutdown() + { + glueClient.close(); + } + + @Test + public void testRequests() + { + GetDatabasesResponse databases = glueClient.getDatabases(GetDatabasesRequest.builder().build()); + assertThat(databases.databaseList()) + .extracting(Database::name) + .containsExactlyInAnyOrder(DATABASE_1, DATABASE_2); + + GetResourcePoliciesResponse resourcePolicies = glueClient.getResourcePolicies(GetResourcePoliciesRequest.builder().build()); + assertThat(resourcePolicies.getResourcePoliciesResponseList()) + .extracting(GluePolicy::policyInJson) + .containsExactlyInAnyOrder(POLICY_A, POLICY_B); + } +} diff --git a/trino-aws-proxy-glue/src/test/java/io/trino/aws/proxy/glue/TestGlueInS3Proxy.java b/trino-aws-proxy-glue/src/test/java/io/trino/aws/proxy/glue/TestGlueInS3Proxy.java new file mode 100644 index 00000000..c9db4a34 --- /dev/null +++ b/trino-aws-proxy-glue/src/test/java/io/trino/aws/proxy/glue/TestGlueInS3Proxy.java @@ -0,0 +1,58 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.aws.proxy.glue; + +import com.google.inject.Inject; +import io.airlift.http.server.testing.TestingHttpServer; +import io.trino.aws.proxy.server.testing.TestingTrinoAwsProxyServer; +import io.trino.aws.proxy.server.testing.TestingUtil.ForTesting; +import io.trino.aws.proxy.server.testing.harness.BuilderFilter; +import io.trino.aws.proxy.server.testing.harness.TrinoAwsProxyTest; +import io.trino.aws.proxy.spi.credentials.Credentials; + +import java.net.URI; + +import static io.trino.aws.proxy.glue.handler.GlueRequestHandlerBinding.glueRequestHandlerBinding; +import static java.util.Objects.requireNonNull; + +@TrinoAwsProxyTest(filters = TestGlueInS3Proxy.Filter.class) +public class TestGlueInS3Proxy + extends TestGlueBase +{ + public static class Filter + implements BuilderFilter + { + @Override + public TestingTrinoAwsProxyServer.Builder filter(TestingTrinoAwsProxyServer.Builder builder) + { + return builder.addModule(binder -> glueRequestHandlerBinding(binder) + .bind(binding -> binding.to(TestingGlueRequestHandler.class))); + } + } + + public record Context(URI baseUrl) + implements TestingGlueContext + { + public Context + { + requireNonNull(baseUrl, "baseUrl is null"); + } + } + + @Inject + public TestGlueInS3Proxy(TestingHttpServer httpServer, TrinoGlueConfig config, @ForTesting Credentials testingCredentials) + { + super(config, testingCredentials, new Context(httpServer.getBaseUrl())); + } +} diff --git a/trino-aws-proxy-glue/src/test/java/io/trino/aws/proxy/glue/TestGlueStandalone.java b/trino-aws-proxy-glue/src/test/java/io/trino/aws/proxy/glue/TestGlueStandalone.java new file mode 100644 index 00000000..d8a5185a --- /dev/null +++ b/trino-aws-proxy-glue/src/test/java/io/trino/aws/proxy/glue/TestGlueStandalone.java @@ -0,0 +1,101 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.aws.proxy.glue; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.inject.Binder; +import com.google.inject.Injector; +import com.google.inject.Module; +import com.google.inject.Scopes; +import io.airlift.bootstrap.Bootstrap; +import io.airlift.bootstrap.LifeCycleManager; +import io.airlift.configuration.AbstractConfigurationAwareModule; +import io.airlift.http.server.testing.TestingHttpServer; +import io.airlift.http.server.testing.TestingHttpServerModule; +import io.airlift.jaxrs.JaxrsModule; +import io.airlift.json.JsonModule; +import io.airlift.node.testing.TestingNodeModule; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.TestInstance; + +import java.net.URI; +import java.util.Map; + +import static io.trino.aws.proxy.glue.TestingCredentialsProvider.CREDENTIALS; +import static io.trino.aws.proxy.glue.handler.GlueRequestHandlerBinding.glueRequestHandlerBinding; +import static io.trino.aws.proxy.spi.plugin.TrinoAwsProxyServerBinding.credentialsProviderModule; +import static java.util.Objects.requireNonNull; +import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; + +@TestInstance(PER_CLASS) +public class TestGlueStandalone + extends TestGlueBase +{ + public record StandaloneContext(URI baseUrl, LifeCycleManager lifeCycleManager) + implements TestingGlueContext + { + public StandaloneContext + { + requireNonNull(baseUrl, "baseUrl is null"); + requireNonNull(lifeCycleManager, "lifeCycleManager is null"); + } + } + + public TestGlueStandalone() + { + super(new TrinoGlueConfig(), CREDENTIALS, buildContext()); + } + + @AfterAll + public void shutdown() + { + super.shutdown(); + + context().lifeCycleManager().stop(); + } + + private static StandaloneContext buildContext() + { + Module module = new AbstractConfigurationAwareModule() + { + @Override + protected void setup(Binder binder) + { + install(credentialsProviderModule("testing", TestingCredentialsProvider.class, (subBinder) -> subBinder.bind(TestingCredentialsProvider.class).in(Scopes.SINGLETON))); + glueRequestHandlerBinding(binder).bind(binding -> binding.to(TestingGlueRequestHandler.class)); + } + }; + + Map properties = ImmutableMap.of( + "credentials-provider.type", "testing", + "assumed-role-provider.type", "testing"); + + ImmutableList.Builder modules = ImmutableList.builder() + .add(module) + .add(new TrinoStandaloneGlueModule()) + .add(new TestingNodeModule()) + .add(new TestingHttpServerModule()) + .add(new JsonModule()) + .add(new JaxrsModule()); + + Bootstrap app = new Bootstrap(modules.build()); + Injector injector = app.setOptionalConfigurationProperties(properties).initialize(); + + TestingHttpServer httpServer = injector.getInstance(TestingHttpServer.class); + LifeCycleManager lifeCycleManager = injector.getInstance(LifeCycleManager.class); + + return new StandaloneContext(httpServer.getBaseUrl(), lifeCycleManager); + } +} diff --git a/trino-aws-proxy-glue/src/test/java/io/trino/aws/proxy/glue/TestingCredentialsProvider.java b/trino-aws-proxy-glue/src/test/java/io/trino/aws/proxy/glue/TestingCredentialsProvider.java new file mode 100644 index 00000000..00d74792 --- /dev/null +++ b/trino-aws-proxy-glue/src/test/java/io/trino/aws/proxy/glue/TestingCredentialsProvider.java @@ -0,0 +1,33 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.aws.proxy.glue; + +import io.trino.aws.proxy.spi.credentials.Credential; +import io.trino.aws.proxy.spi.credentials.Credentials; +import io.trino.aws.proxy.spi.credentials.CredentialsProvider; + +import java.util.Optional; +import java.util.UUID; + +public class TestingCredentialsProvider + implements CredentialsProvider +{ + public static final Credentials CREDENTIALS = Credentials.build(new Credential(UUID.randomUUID().toString(), UUID.randomUUID().toString())); + + @Override + public Optional credentials(String emulatedAccessKey, Optional session) + { + return Optional.of(CREDENTIALS); + } +} diff --git a/trino-aws-proxy-glue/src/test/java/io/trino/aws/proxy/glue/TestingGlueContext.java b/trino-aws-proxy-glue/src/test/java/io/trino/aws/proxy/glue/TestingGlueContext.java new file mode 100644 index 00000000..2b41d880 --- /dev/null +++ b/trino-aws-proxy-glue/src/test/java/io/trino/aws/proxy/glue/TestingGlueContext.java @@ -0,0 +1,21 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.aws.proxy.glue; + +import java.net.URI; + +public interface TestingGlueContext +{ + URI baseUrl(); +} diff --git a/trino-aws-proxy-glue/src/test/java/io/trino/aws/proxy/glue/TestingGlueRequestHandler.java b/trino-aws-proxy-glue/src/test/java/io/trino/aws/proxy/glue/TestingGlueRequestHandler.java new file mode 100644 index 00000000..9f138227 --- /dev/null +++ b/trino-aws-proxy-glue/src/test/java/io/trino/aws/proxy/glue/TestingGlueRequestHandler.java @@ -0,0 +1,98 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.aws.proxy.glue; + +import com.google.common.collect.ImmutableList; +import io.trino.aws.proxy.glue.handler.GlueRequestHandler; +import io.trino.aws.proxy.glue.rest.ParsedGlueRequest; +import io.trino.aws.proxy.server.rest.RequestLoggingSession; +import io.trino.aws.proxy.spi.signing.SigningMetadata; +import jakarta.ws.rs.WebApplicationException; +import software.amazon.awssdk.services.glue.model.Database; +import software.amazon.awssdk.services.glue.model.GetDatabasesRequest; +import software.amazon.awssdk.services.glue.model.GetDatabasesResponse; +import software.amazon.awssdk.services.glue.model.GetResourcePoliciesRequest; +import software.amazon.awssdk.services.glue.model.GetResourcePoliciesResponse; +import software.amazon.awssdk.services.glue.model.GluePolicy; +import software.amazon.awssdk.services.glue.model.GlueResponse; + +import java.util.Collection; + +import static jakarta.ws.rs.core.Response.Status.NOT_FOUND; + +public class TestingGlueRequestHandler + implements GlueRequestHandler +{ + public static final String POLICY_A = """ + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Service": "glue.amazonaws.com" + }, + "Action": "glue:GetDatabase", + "Resource": "arn:aws:glue:us-east-1:123456789012:database/d1" + } + ] + } + """; + + public static final String POLICY_B = """ + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Deny", + "Principal": { + "Service": "glue.amazonaws.com" + }, + "Action": "glue:GetDatabase", + "Resource": "arn:aws:glue:us-east-1:123456789012:database/d1" + } + ] + } + """; + + public static final String DATABASE_1 = "db1"; + public static final String DATABASE_2 = "db2"; + + @Override + public GlueResponse handleRequest(ParsedGlueRequest request, SigningMetadata signingMetadata, RequestLoggingSession requestLoggingSession) + { + return switch (request.glueRequest()) { + case GetDatabasesRequest _ -> GetDatabasesResponse.builder() + .databaseList(Database.builder().name(DATABASE_1).build(), Database.builder().name(DATABASE_2).build()) + .build(); + + case GetResourcePoliciesRequest _ -> GetResourcePoliciesResponse.builder() + .getResourcePoliciesResponseList(gluePolicies()) + .build(); + + default -> throw new WebApplicationException(NOT_FOUND); + }; + } + + private static Collection gluePolicies() + { + return ImmutableList.of( + GluePolicy.builder() + .policyInJson(POLICY_A) + .build(), + GluePolicy.builder() + .policyInJson(POLICY_B) + .build()); + } +}