Skip to content
This repository has been archived by the owner on Jan 19, 2022. It is now read-only.

Setup the Autoconfiguration for Firestore Emulator #2244

Merged
merged 12 commits into from
Apr 21, 2020
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/*
* Copyright 2019-2019 the original author or authors.
dzou marked this conversation as resolved.
Show resolved Hide resolved
*
* 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
*
* https://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 org.springframework.cloud.gcp.autoconfigure.firestore;

import java.io.IOException;
import java.net.URI;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.google.api.gax.grpc.InstantiatingGrpcChannelProvider;
import com.google.auth.Credentials;
import com.google.cloud.firestore.FirestoreOptions;
import com.google.firestore.v1.FirestoreGrpc;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import io.grpc.auth.MoreCallCredentials;

import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.gcp.autoconfigure.firestore.GcpFirestoreAutoConfiguration.FirestoreReactiveAutoConfiguration;
import org.springframework.cloud.gcp.core.GcpProjectIdProvider;
import org.springframework.cloud.gcp.data.firestore.FirestoreTemplate;
import org.springframework.cloud.gcp.data.firestore.mapping.FirestoreClassMapper;
import org.springframework.cloud.gcp.data.firestore.mapping.FirestoreMappingContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
* Provides autoconfiguration to use the Firestore emulator if enabled.
*
* @since 1.2.3
* @author Daniel Zou
*/
@Configuration
@ConditionalOnProperty("spring.cloud.gcp.firestore.emulator.enabled")
dzou marked this conversation as resolved.
Show resolved Hide resolved
@AutoConfigureBefore({
FirestoreReactiveAutoConfiguration.class, GcpFirestoreAutoConfiguration.class})
@EnableConfigurationProperties(GcpFirestoreProperties.class)
public class GcpFirestoreEmulatorAutoConfiguration {
dzou marked this conversation as resolved.
Show resolved Hide resolved

private static final String ROOT_PATH_FORMAT = "projects/%s/databases/(default)/documents";

private final String projectId;

private final String hostPort;

private final String firestoreRootPath;

GcpFirestoreEmulatorAutoConfiguration(
GcpFirestoreProperties properties,
GcpProjectIdProvider projectIdProvider) throws IOException {
this.projectId = projectIdProvider.getProjectId();
this.hostPort = properties.getHostPort();
this.firestoreRootPath = String.format(ROOT_PATH_FORMAT, this.projectId);
}

@Bean
@ConditionalOnMissingBean
public FirestoreOptions firestoreOptions() {
return FirestoreOptions.newBuilder()
.setCredentials(fakeCredentials())
.setProjectId(this.projectId)
.setChannelProvider(
InstantiatingGrpcChannelProvider.newBuilder()
.setEndpoint(this.hostPort)
.setChannelConfigurator(input -> input.usePlaintext())
.build())
.build();
}

@Bean
@ConditionalOnMissingBean
public FirestoreGrpc.FirestoreStub firestoreGrpcStub() throws IOException {
ManagedChannel channel = ManagedChannelBuilder
.forTarget(this.hostPort)
.usePlaintext()
.build();

return FirestoreGrpc.newStub(channel)
.withCallCredentials(MoreCallCredentials.from(fakeCredentials()))
.withExecutor(Runnable::run);
}

@Bean
@ConditionalOnMissingBean
public FirestoreTemplate firestoreTemplate(FirestoreGrpc.FirestoreStub firestoreStub,
FirestoreClassMapper classMapper, FirestoreMappingContext firestoreMappingContext) {
FirestoreTemplate template = new FirestoreTemplate(
firestoreStub, this.firestoreRootPath, classMapper, firestoreMappingContext);
template.setUsingStreamTokens(false);
return template;
}


private static Credentials fakeCredentials() {
dzou marked this conversation as resolved.
Show resolved Hide resolved
final Map<String, List<String>> headerMap = new HashMap<>();
headerMap.put("Authorization", Collections.singletonList("Bearer owner"));
headerMap.put("google-cloud-resource-prefix", Collections.singletonList("projects/my-project/databases/(default)"));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"my-project" -- is it because it's a fake value that's unused? Can you leave a comment about that?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. I just changed it to plug in the current project-id. I guess this is more technically correct.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or maybe I should make this so the emulator config doesn't require a real project (and not inject a projectIdProvider into the configuration)... Maybe that would make more sense; what do you think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe just hardcode "unused" as project ID?


return new Credentials() {
@Override
public String getAuthenticationType() {
return null;
}

@Override
public Map<String, List<String>> getRequestMetadata(URI uri) {
return headerMap;
}

@Override
public boolean hasRequestMetadata() {
return true;
}

@Override
public boolean hasRequestMetadataOnly() {
return true;
}

@Override
public void refresh() {
// no-op
}
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ org.springframework.cloud.gcp.autoconfigure.pubsub.GcpPubSubReactiveAutoConfigur
org.springframework.cloud.gcp.autoconfigure.spanner.GcpSpannerAutoConfiguration,\
org.springframework.cloud.gcp.autoconfigure.datastore.GcpDatastoreAutoConfiguration,\
org.springframework.cloud.gcp.autoconfigure.firestore.GcpFirestoreAutoConfiguration,\
org.springframework.cloud.gcp.autoconfigure.firestore.GcpFirestoreEmulatorAutoConfiguration,\
org.springframework.cloud.gcp.autoconfigure.datastore.health.DatastoreHealthIndicatorAutoConfiguration,\
org.springframework.cloud.gcp.autoconfigure.sql.GcpCloudSqlAutoConfiguration,\
org.springframework.cloud.gcp.autoconfigure.storage.GcpStorageAutoConfiguration,\
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ public class FirestoreTemplate implements FirestoreReactiveOperations {

private int writeBufferSize = FIRESTORE_WRITE_MAX_SIZE;

private boolean usingStreamTokens = true;
meltsufin marked this conversation as resolved.
Show resolved Hide resolved

/**
* Constructor for FirestoreTemplate.
* @param firestore Firestore gRPC stub
Expand Down Expand Up @@ -129,6 +131,24 @@ public int getWriteBufferSize() {
return this.writeBufferSize;
}

/**
* Sets whether the {@link FirestoreTemplate} should attach stream resume tokens to write
* requests.
*
* <p>Note that this should always be set to true unless you are using the
* Firestore emulator in which case it should be set to false because the emulator
* does not support using resume tokens.
*
* @param usingStreamTokens whether the template
*/
public void setUsingStreamTokens(boolean usingStreamTokens) {
dzou marked this conversation as resolved.
Show resolved Hide resolved
this.usingStreamTokens = usingStreamTokens;
}

public boolean isUsingStreamTokens() {
return usingStreamTokens;
}

@Override
public <T> Mono<Boolean> existsById(Publisher<String> idPublisher, Class<T> entityClass) {
return Flux.from(idPublisher).next()
Expand Down Expand Up @@ -269,8 +289,11 @@ private WriteRequest buildDeleteRequest(

WriteRequest.Builder writeRequestBuilder =
WriteRequest.newBuilder()
.setStreamId(writeResponse.getStreamId())
.setStreamToken(writeResponse.getStreamToken());
.setStreamId(writeResponse.getStreamId());

if (isUsingStreamTokens()) {
writeRequestBuilder.setStreamToken(writeResponse.getStreamToken());
}

documentIds.stream().map(this::createDeleteWrite).forEach(writeRequestBuilder::addWrites);

Expand Down Expand Up @@ -351,8 +374,12 @@ private StreamObserver<WriteRequest> openWriteStream(StreamObserver<WriteRespons
private <T> WriteRequest buildWriteRequest(List<T> entityList, WriteResponse writeResponse) {
WriteRequest.Builder writeRequestBuilder =
WriteRequest.newBuilder()
.setStreamId(writeResponse.getStreamId())
.setStreamToken(writeResponse.getStreamToken());
.setStreamId(writeResponse.getStreamId());

if (isUsingStreamTokens()) {
writeRequestBuilder.setStreamToken(writeResponse.getStreamToken());

}

entityList.stream().map(this::createUpdateWrite).forEach(writeRequestBuilder::addWrites);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,7 @@
# Use this setting if you want to manually specify service account credentials instead of inferring
# from the machine's environment for firestore.
# spring.cloud.gcp.firestore.credentials.location=file:{PATH_TO_YOUR_CREDENTIALS_FILE}

server.port=9000
spring.cloud.gcp.firestore.emulator.enabled=true
spring.cloud.gcp.firestore.host-port=localhost:8080
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
# You can use this setting to provide a separate service account JSON file specifically for Firestore.
# spring.cloud.gcp.firestore.credentials.location =

server.port=9000
spring.cloud.gcp.firestore.emulator.enabled=true
spring.cloud.gcp.firestore.host-port=localhost:8080