Skip to content

Commit c32e3b8

Browse files
committed
Added gRPC support to the App Engine Images service for image transformations and composition.
PiperOrigin-RevId: 917228259 Change-Id: Id32b915dd3b835d4dd78fab223b658739947e272
1 parent f5199cc commit c32e3b8

16 files changed

Lines changed: 1567 additions & 66 deletions

File tree

api/pom.xml

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,12 +147,43 @@
147147
<artifactId>guava-testlib</artifactId>
148148
<scope>test</scope>
149149
</dependency>
150+
150151
<dependency>
151152
<groupId>jakarta.servlet</groupId>
152153
<artifactId>jakarta.servlet-api</artifactId>
153154
</dependency>
155+
<dependency>
156+
<groupId>io.grpc</groupId>
157+
<artifactId>grpc-netty-shaded</artifactId>
158+
</dependency>
159+
<dependency>
160+
<groupId>io.grpc</groupId>
161+
<artifactId>grpc-protobuf</artifactId>
162+
</dependency>
163+
<dependency>
164+
<groupId>io.grpc</groupId>
165+
<artifactId>grpc-stub</artifactId>
166+
</dependency>
167+
<dependency>
168+
<groupId>io.grpc</groupId>
169+
<artifactId>grpc-auth</artifactId>
170+
</dependency>
171+
<dependency>
172+
<groupId>com.google.auth</groupId>
173+
<artifactId>google-auth-library-oauth2-http</artifactId>
174+
</dependency>
175+
<dependency>
176+
<groupId>io.grpc</groupId>
177+
<artifactId>grpc-testing</artifactId>
178+
<scope>test</scope>
179+
</dependency>
180+
<dependency>
181+
<groupId>com.google.cloud</groupId>
182+
<artifactId>google-cloud-storage</artifactId>
183+
</dependency>
154184
</dependencies>
155185
<build>
186+
156187
<plugins>
157188
<plugin>
158189
<groupId>org.apache.maven.plugins</groupId>
@@ -213,8 +244,13 @@
213244
<failOnWarnings>false</failOnWarnings>
214245
</configuration>
215246
</plugin>
247+
248+
249+
250+
216251
</plugins>
217252
</build>
253+
218254
<profiles>
219255
<profile>
220256
<id>docFX</id>
@@ -266,12 +302,12 @@
266302
<path>
267303
<groupId>com.google.auto.service</groupId>
268304
<artifactId>auto-service</artifactId>
269-
<version>1.1.1</version>
305+
<version>${auto-service.version}</version>
270306
</path>
271307
<path>
272308
<groupId>com.google.auto.value</groupId>
273309
<artifactId>auto-value</artifactId>
274-
<version>1.11.1</version>
310+
<version>${auto-value.version}</version>
275311
</path>
276312
</annotationProcessorPaths>
277313
</configuration>
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Copyright 2021 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.appengine.api;
18+
19+
/** A simple wrapper around {@link System} to allow for easier testing. */
20+
public class SystemEnvironmentProvider implements EnvironmentProvider {
21+
/**
22+
* Gets the value of the specified environment variable.
23+
*
24+
* @param name the name of the environment variable
25+
* @return the string value of the variable, or {@code null} if the variable is not defined
26+
*/
27+
@Override
28+
public String getenv(String name) {
29+
return System.getenv(name);
30+
}
31+
32+
/**
33+
* Gets the value of the specified environment variable, returning a default value if the variable
34+
* is not defined.
35+
*
36+
* @param name the name of the environment variable
37+
* @param defaultValue the default value to return
38+
* @return the string value of the variable, or the default value if the variable is not defined
39+
*/
40+
@Override
41+
public String getenv(String name, String defaultValue) {
42+
String value = System.getenv(name);
43+
return value != null ? value : defaultValue;
44+
}
45+
}

api/src/main/java/com/google/appengine/api/images/Composite.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616

1717
package com.google.appengine.api.images;
1818

19+
import com.google.appengine.api.images.ImagesServicePb.ImageData;
1920
import java.util.Map;
21+
import java.util.function.Function;
2022

2123
/**
2224
* A {@code Composite} represents a composition of an image onto a canvas.
@@ -34,10 +36,13 @@ public static enum Anchor {TOP_LEFT, TOP_CENTER, TOP_RIGHT, CENTER_LEFT,
3436

3537
/**
3638
* Adds this compositing operation to a Composite request.
39+
*
3740
* @param request Request for this composite to be added to.
3841
* @param imageIndexMap Map of images and their indexes in the request.
42+
* @param imageDataConverter Function to convert an Image to ImageData.
3943
*/
40-
abstract void apply(ImagesServicePb.ImagesCompositeRequest.Builder request,
41-
Map<Image, Integer> imageIndexMap);
42-
44+
abstract void apply(
45+
ImagesServicePb.ImagesCompositeRequest.Builder request,
46+
Map<Image, Integer> imageIndexMap,
47+
Function<Image, ImageData> imageDataConverter);
4348
}

api/src/main/java/com/google/appengine/api/images/CompositeImpl.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@
1919
import static java.util.Objects.requireNonNull;
2020

2121
import com.google.appengine.api.images.ImagesServicePb.CompositeImageOptions;
22+
import com.google.appengine.api.images.ImagesServicePb.ImageData;
2223
import com.google.appengine.api.images.ImagesServicePb.ImagesCompositeRequest;
2324
import java.util.Map;
25+
import java.util.function.Function;
2426

2527
/**
2628
* Implementation of Composite using alpha blending.
@@ -68,11 +70,14 @@ final class CompositeImpl extends Composite {
6870

6971
/** {@inheritDoc} */
7072
@Override
71-
void apply(ImagesCompositeRequest.Builder request, Map<Image, Integer> imageIndexMap) {
73+
void apply(
74+
ImagesCompositeRequest.Builder request,
75+
Map<Image, Integer> imageIndexMap,
76+
Function<Image, ImageData> imageDataConverter) {
7277
// TODO: What is the purpose of this map?
7378
if (!imageIndexMap.containsKey(image)) {
7479
imageIndexMap.put(image, request.build().getImageCount());
75-
request.addImage(ImagesServiceImpl.convertImageData(image));
80+
request.addImage(imageDataConverter.apply(image));
7681
}
7782
CompositeImageOptions.Builder options = CompositeImageOptions.newBuilder();
7883
int sourceId = requireNonNull(imageIndexMap.get(image));
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
// Copyright 2021 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
package com.google.appengine.api.images;
15+
16+
import com.google.appengine.api.EnvironmentProvider;
17+
import com.google.appengine.api.SystemEnvironmentProvider;
18+
import com.google.appengine.api.images.proto.ImagesServiceGrpc;
19+
import com.google.auth.oauth2.GoogleCredentials;
20+
import com.google.auth.oauth2.IdTokenCredentials;
21+
import com.google.auth.oauth2.IdTokenProvider;
22+
import com.google.common.annotations.VisibleForTesting;
23+
import static com.google.common.base.Strings.isNullOrEmpty;
24+
import io.grpc.CallCredentials;
25+
import io.grpc.ManagedChannel;
26+
import io.grpc.auth.MoreCallCredentials;
27+
import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder;
28+
import java.io.IOException;
29+
import java.net.URI;
30+
import static java.util.concurrent.TimeUnit.SECONDS;
31+
import java.net.URISyntaxException;
32+
33+
/** Client for interacting with the gRPC based Images service. */
34+
class GrpcImagesClient {
35+
36+
private static final int MAX_MESSAGE_SIZE = 32 * 1024 * 1024; // 32MB
37+
private final ManagedChannel channel;
38+
private final EnvironmentProvider environmentProvider;
39+
private final CallCredentials callCredentials;
40+
41+
// private ImagesServiceGrpc.ImagesServiceBlockingStub blockingStub;
42+
43+
public GrpcImagesClient() {
44+
this(new SystemEnvironmentProvider(), getApplicationDefaultCredentials());
45+
}
46+
47+
// Constructor for production
48+
GrpcImagesClient(EnvironmentProvider environmentProvider, GoogleCredentials googleCredentials) {
49+
this(environmentProvider, createOidcCredentials(environmentProvider, googleCredentials));
50+
}
51+
52+
// Constructor for testing
53+
@VisibleForTesting
54+
GrpcImagesClient(EnvironmentProvider environmentProvider, CallCredentials callCredentials) {
55+
this.environmentProvider = environmentProvider;
56+
this.callCredentials = callCredentials;
57+
String target = getTarget();
58+
this.channel =
59+
NettyChannelBuilder.forTarget(target)
60+
.maxInboundMessageSize(MAX_MESSAGE_SIZE)
61+
.keepAliveTime(60, SECONDS)
62+
.useTransportSecurity()
63+
.build();
64+
}
65+
66+
private static GoogleCredentials getApplicationDefaultCredentials() {
67+
try {
68+
return GoogleCredentials.getApplicationDefault();
69+
} catch (IOException e) {
70+
throw new IllegalStateException("Failed to get Application Default Credentials", e);
71+
}
72+
}
73+
74+
private String getTarget() {
75+
String endpoint = environmentProvider.getenv("IMAGES_SERVICE_ENDPOINT");
76+
if (isNullOrEmpty(endpoint)) {
77+
throw new IllegalStateException("IMAGES_SERVICE_ENDPOINT environment variable not set.");
78+
}
79+
try {
80+
URI uri = new URI(endpoint);
81+
String host = uri.getHost();
82+
if (host == null) {
83+
throw new IllegalStateException("Invalid URI in IMAGES_SERVICE_ENDPOINT: " + endpoint);
84+
}
85+
return host + ":443";
86+
} catch (URISyntaxException e) {
87+
throw new IllegalStateException("Invalid URI in IMAGES_SERVICE_ENDPOINT: " + endpoint, e);
88+
}
89+
}
90+
91+
private static CallCredentials createOidcCredentials(
92+
EnvironmentProvider environmentProvider, GoogleCredentials googleCredentials) {
93+
String endpoint = environmentProvider.getenv("IMAGES_SERVICE_ENDPOINT");
94+
if (isNullOrEmpty(endpoint)) {
95+
throw new IllegalStateException("IMAGES_SERVICE_ENDPOINT environment variable not set.");
96+
}
97+
98+
if (!(googleCredentials instanceof IdTokenProvider idTokenProvider)) {
99+
throw new IllegalStateException(
100+
"The Application Default Credentials do not support OIDC ID token generation.");
101+
}
102+
103+
IdTokenCredentials idTokenCredentials =
104+
IdTokenCredentials.newBuilder()
105+
.setTargetAudience(endpoint)
106+
.setIdTokenProvider(idTokenProvider)
107+
.build();
108+
return MoreCallCredentials.from(idTokenCredentials);
109+
}
110+
111+
public ImagesServiceGrpc.ImagesServiceBlockingStub getBlockingStub() {
112+
return ImagesServiceGrpc.newBlockingStub(channel).withCallCredentials(callCredentials);
113+
}
114+
115+
public ImagesServiceGrpc.ImagesServiceFutureStub getFutureStub() {
116+
return ImagesServiceGrpc.newFutureStub(channel).withCallCredentials(callCredentials);
117+
}
118+
119+
public void shutdown() {
120+
if (channel != null && !channel.isShutdown()) {
121+
try {
122+
channel.shutdown().awaitTermination(5, SECONDS);
123+
} catch (InterruptedException e) {
124+
Thread.currentThread().interrupt();
125+
// Handle exception
126+
}
127+
}
128+
}
129+
}

api/src/main/java/com/google/appengine/api/images/ImagesServiceFactoryImpl.java

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,13 @@
1616

1717
package com.google.appengine.api.images;
1818

19+
import com.google.appengine.api.EnvironmentProvider;
20+
import com.google.appengine.api.SystemEnvironmentProvider;
1921
import com.google.appengine.api.blobstore.BlobKey;
2022
import com.google.appengine.api.blobstore.BlobstoreServiceFactory;
23+
import com.google.common.annotations.VisibleForTesting;
24+
import com.google.common.base.Supplier;
25+
import com.google.common.base.Suppliers;
2126
import java.util.Collection;
2227

2328
/**
@@ -27,9 +32,26 @@
2732
*/
2833
final class ImagesServiceFactoryImpl implements IImagesServiceFactory {
2934

35+
@VisibleForTesting
36+
static final String USE_CUSTOM_IMAGES_GRPC_SERVICE_ENV = "USE_CUSTOM_IMAGES_GRPC_SERVICE";
37+
38+
private EnvironmentProvider environmentProvider = new SystemEnvironmentProvider();
39+
40+
private static final Supplier<GrpcImagesClient> grpcClientSupplier =
41+
Suppliers.memoize(() -> new GrpcImagesClient());
42+
43+
@VisibleForTesting
44+
void setEnvironmentProvider(EnvironmentProvider environmentProvider) {
45+
this.environmentProvider = environmentProvider;
46+
}
47+
3048
@Override
3149
public ImagesService getImagesService() {
32-
return new ImagesServiceImpl();
50+
GrpcImagesClient client = null;
51+
if (Boolean.parseBoolean(environmentProvider.getenv(USE_CUSTOM_IMAGES_GRPC_SERVICE_ENV))) {
52+
client = grpcClientSupplier.get();
53+
}
54+
return new ImagesServiceImpl(environmentProvider, client);
3355
}
3456

3557
@Override
@@ -44,6 +66,12 @@ public Image makeImageFromBlob(BlobKey blobKey) {
4466

4567
@Override
4668
public Image makeImageFromFilename(String filename) {
69+
if (Boolean.parseBoolean(environmentProvider.getenv(USE_CUSTOM_IMAGES_GRPC_SERVICE_ENV))) {
70+
if (!filename.startsWith("/gs/")) {
71+
throw new IllegalArgumentException("Google storage filenames must be prefixed with /gs/");
72+
}
73+
return new ImageImpl(new BlobKey(filename));
74+
}
4775
BlobKey blobKey = BlobstoreServiceFactory.getBlobstoreService().createGsBlobKey(filename);
4876
return new ImageImpl(blobKey);
4977
}

0 commit comments

Comments
 (0)