Skip to content

Commit

Permalink
feat: support customer managed instance configurations (#1742)
Browse files Browse the repository at this point in the history
* feat: implementation changes to add support for
customer managed instance configurations.

* test: add more unit tests to Instance admin client

Unit tests each for create, update and delete
instance config operations have been added.

* feat: add samples for create, update and deleting
user managed instance configs.

* chore: add test instance config to samples pom

* test: add CUD instance config integration tests

* feat: add proto file as reference.

* feat: lint the code changes, add documentation

* feat: minor alignment changes

* feat: fix checkstyle violations.

* feat: change read-only fields setters to protected.

* feat: change setConfigType to protected.

* feat: change some more method access modifiers

* feat: add optional fields, some design changes.

* feat: some documentation changes

* feat: change BASE_CONFIG project name in tests.

* feat: pom.xml changes

* feat: add support for adding readonly replicas
while creating instance config.

* feat: clirr changes

* feat: some refactoring

* feat: some method doc changes.

* feat: incorporate review comments.

* feat: remove samples

* Update pom.xml

* Update SampleIdGenerator.java

* Update SampleRunner.java

* Update SampleTestBase.java

* feat: changes to InstanceConfigTest

* feat: changes to pom.xml

* Update google-cloud-spanner/src/main/java/com/google/cloud/spanner/InstanceAdminClient.java doc

Co-authored-by: Knut Olav Løite <koloite@gmail.com>

* Update google-cloud-spanner/src/main/java/com/google/cloud/spanner/InstanceAdminClient.java

Co-authored-by: Knut Olav Løite <koloite@gmail.com>

* Update google-cloud-spanner/src/main/java/com/google/cloud/spanner/InstanceAdminClient.java

Co-authored-by: Knut Olav Løite <koloite@gmail.com>

* Update google-cloud-spanner/src/main/java/com/google/cloud/spanner/InstanceAdminClient.java

Co-authored-by: Knut Olav Løite <koloite@gmail.com>

* Update google-cloud-spanner/src/main/java/com/google/cloud/spanner/InstanceConfigInfo.java

Co-authored-by: Knut Olav Løite <koloite@gmail.com>

* Update google-cloud-spanner/src/main/java/com/google/cloud/spanner/InstanceConfigInfo.java

Co-authored-by: Knut Olav Løite <koloite@gmail.com>

* feat: make setBaseConfig protected.

* feat: add method doc for addReadOnlyReplicas

* feat: incorporate review comments.

* feat: deprecate few constructors

* feat: make some methods private

* feat: move initialization to property declaration.

* feat: make InstanceConfig.Builder setters return
InstanceConfig.Builder

* feat: some doc changes

* feat: add support for ListInstanceConfigOperations

* fix: linting

* feat: few changes

* Update spanner_instance_admin.proto

* Update SpannerRpc.java

* feat: remove unnecessary parameter from
GapicSpannerRpc

* feat: clirr fix

* 🦉 Updates from OwlBot post-processor

See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md

* feat: revert unintended sample changes

Co-authored-by: Knut Olav Løite <koloite@gmail.com>
Co-authored-by: Owl Bot <gcf-owl-bot[bot]@users.noreply.github.com>
  • Loading branch information
3 people committed Sep 29, 2022
1 parent 98c2303 commit c1c805c
Show file tree
Hide file tree
Showing 10 changed files with 1,280 additions and 50 deletions.
40 changes: 40 additions & 0 deletions google-cloud-spanner/clirr-ignored-differences.xml
Expand Up @@ -35,6 +35,46 @@
<className>com/google/cloud/spanner/connection/ConnectionOptions</className>
<method>com.google.cloud.spanner.Dialect getDialect()</method>
</difference>
<difference>
<differenceType>7012</differenceType>
<className>com/google/cloud/spanner/InstanceAdminClient</className>
<method>com.google.api.gax.longrunning.OperationFuture createInstanceConfig(com.google.cloud.spanner.InstanceConfigInfo, com.google.cloud.spanner.Options$CreateAdminApiOption[])</method>
</difference>
<difference>
<differenceType>7012</differenceType>
<className>com/google/cloud/spanner/InstanceAdminClient</className>
<method>com.google.api.gax.longrunning.OperationFuture updateInstanceConfig(com.google.cloud.spanner.InstanceConfigInfo, java.lang.Iterable, com.google.cloud.spanner.Options$UpdateAdminApiOption[])</method>
</difference>
<difference>
<differenceType>7012</differenceType>
<className>com/google/cloud/spanner/InstanceAdminClient</className>
<method>void deleteInstanceConfig(java.lang.String, com.google.cloud.spanner.Options$DeleteAdminApiOption[])</method>
</difference>
<difference>
<differenceType>7012</differenceType>
<className>com/google/cloud/spanner/InstanceAdminClient</className>
<method>com.google.api.gax.paging.Page listInstanceConfigOperations(com.google.cloud.spanner.Options$ListOption[])</method>
</difference>
<difference>
<differenceType>7012</differenceType>
<className>com/google/cloud/spanner/spi/v1/SpannerRpc</className>
<method>com.google.api.gax.longrunning.OperationFuture createInstanceConfig(java.lang.String, java.lang.String, com.google.spanner.admin.instance.v1.InstanceConfig, java.lang.Boolean)</method>
</difference>
<difference>
<differenceType>7012</differenceType>
<className>com/google/cloud/spanner/spi/v1/SpannerRpc</className>
<method>com.google.api.gax.longrunning.OperationFuture updateInstanceConfig(com.google.spanner.admin.instance.v1.InstanceConfig, java.lang.Boolean, com.google.protobuf.FieldMask)</method>
</difference>
<difference>
<differenceType>7012</differenceType>
<className>com/google/cloud/spanner/spi/v1/SpannerRpc</className>
<method>void deleteInstanceConfig(java.lang.String, java.lang.String, java.lang.Boolean)</method>
</difference>
<difference>
<differenceType>7012</differenceType>
<className>com/google/cloud/spanner/spi/v1/SpannerRpc</className>
<method>com.google.cloud.spanner.spi.v1.SpannerRpc$Paginated listInstanceConfigOperations(int, java.lang.String, java.lang.String)</method>
</difference>
<difference>
<differenceType>7013</differenceType>
<className>com/google/cloud/spanner/BackupInfo$Builder</className>
Expand Down
Expand Up @@ -19,14 +19,141 @@
import com.google.api.gax.longrunning.OperationFuture;
import com.google.api.gax.paging.Page;
import com.google.cloud.Policy;
import com.google.cloud.spanner.Options.CreateAdminApiOption;
import com.google.cloud.spanner.Options.DeleteAdminApiOption;
import com.google.cloud.spanner.Options.ListOption;
import com.google.cloud.spanner.Options.UpdateAdminApiOption;
import com.google.longrunning.Operation;
import com.google.spanner.admin.instance.v1.CreateInstanceConfigMetadata;
import com.google.spanner.admin.instance.v1.CreateInstanceMetadata;
import com.google.spanner.admin.instance.v1.UpdateInstanceConfigMetadata;
import com.google.spanner.admin.instance.v1.UpdateInstanceMetadata;

/** Client to do admin operations on Cloud Spanner Instance and Instance Configs. */
public interface InstanceAdminClient {

/**
* Creates an instance config and begins preparing it to be used. The returned {@code Operation}
* can be used to track the progress of preparing the new instance config. The instance config
* name is assigned by the caller and must start with the string 'custom'. If the named instance
* config already exists, a SpannerException is thrown.
*
* <p>Immediately after the request returns:
*
* <ul>
* <li>The instance config is readable via the API, with all requested attributes.
* <li>The instance config's {@code reconciling} field is set to true. Its state is {@code
* CREATING}.
* </ul>
*
* While the operation is pending:
*
* <ul>
* <li>Cancelling the operation renders the instance config immediately unreadable via the API.
* <li>Except for deleting the creating resource, all other attempts to modify the instance
* config are rejected.
* </ul>
*
* Upon completion of the returned operation:
*
* <ul>
* <li>Instances can be created using the instance configuration.
* <li>The instance config's {@code reconciling} field becomes false.
* <li>Its state becomes {@code READY}.
* </ul>
*
* <!--SNIPPET instance_admin_client_create_instance_config-->
*
* <pre>{@code
* String projectId = "my-project";
* String baseInstanceConfig = "my-base-config";
* String instanceConfigId = "custom-user-config";
*
* final InstanceConfig baseConfig = instanceAdminClient.getInstanceConfig(baseInstanceConfig);
*
* List<ReplicaInfo> readOnlyReplicas = ImmutableList.of(baseConfig.getOptionalReplicas().get(0));
*
* InstanceConfigInfo instanceConfigInfo =
* InstanceConfigInfo.newBuilder(InstanceConfigId.of(projectId, instanceConfigId), baseConfig)
* .setDisplayName(instanceConfigId)
* .addReadOnlyReplicas(readOnlyReplicas)
* .build();
*
* final OperationFuture<InstanceConfig, CreateInstanceConfigMetadata> operation =
* instanceAdminClient.createInstanceConfig(instanceConfigInfo);
*
* InstanceConfig instanceConfig = op.get(5, TimeUnit.MINUTES)
* }</pre>
*
* <!--SNIPPET instance_admin_client_create_instance_config-->
*/
default OperationFuture<InstanceConfig, CreateInstanceConfigMetadata> createInstanceConfig(
InstanceConfigInfo instanceConfig, CreateAdminApiOption... options) throws SpannerException {
throw new UnsupportedOperationException("Not implemented");
}

/**
* Updates a custom instance config. This can not be used to update a Google managed instance
* config. The returned {@code Operation} can be used to track the progress of updating the
* instance. If the named instance config does not exist, a SpannerException is thrown. The
* request must include at least one field to update.
*
* <p>Only user managed configurations can be updated.
*
* <p>Immediately after the request returns:
*
* <ul>
* <li>The instance config's {@code reconciling} field is set to true.
* </ul>
*
* While the operation is pending:
*
* <ul>
* <li>Cancelling the operation sets its metadata's cancel_time.
* <li>The operation is guaranteed to succeed at undoing all changes, after which point it
* terminates with a `CANCELLED` status.
* <li>All other attempts to modify the instance config are rejected.
* <li>Reading the instance config via the API continues to give the pre-request values.
* </ul>
*
* Upon completion of the returned operation:
*
* <ul>
* <li>Creating instances using the instance configuration uses the new values.
* <li>The instance config's new values are readable via the API.
* <li>The instance config's {@code reconciling} field becomes false.
* </ul>
*
* <!--SNIPPET instance_admin_client_update_instance_config-->
*
* <pre>{@code
* String projectId = "my-project";
* String instanceConfigId = "custom-user-config";
* String displayName = "my-display-name";
*
* InstanceConfigInfo instanceConfigInfo =
* InstanceConfigInfo.newBuilder(InstanceConfigId.of(projectId, instanceConfigId))
* .setDisplayName(displayName)
* .build();
*
* // Only update display name.
* final OperationFuture<InstanceConfig, UpdateInstanceConfigMetadata> operation =
* instanceAdminClient.updateInstanceConfig(
* instanceConfigInfo, ImmutableList.of(InstanceConfigField.DISPLAY_NAME));
*
* InstanceConfig instanceConfig = operation.get(5, TimeUnit.MINUTES);
* }</pre>
*
* <!--SNIPPET instance_admin_client_update_instance_config-->
*/
default OperationFuture<InstanceConfig, UpdateInstanceConfigMetadata> updateInstanceConfig(
InstanceConfigInfo instanceConfig,
Iterable<InstanceConfigInfo.InstanceConfigField> fieldsToUpdate,
UpdateAdminApiOption... options)
throws SpannerException {
throw new UnsupportedOperationException("Not implemented");
}

/** Gets an instance config. */
/* <!--SNIPPET instance_admin_client_get_instance_config-->
* <pre>{@code
Expand All @@ -37,6 +164,28 @@ public interface InstanceAdminClient {
*/
InstanceConfig getInstanceConfig(String configId) throws SpannerException;

/**
* Deletes a custom instance config. Deletion is only allowed for custom instance configs and when
* no instances are using the configuration. If any instances are using the config, a
* SpannerException is thrown.
*
* <p>Only user managed configurations can be deleted.
* <!--SNIPPET instance_admin_client_delete_instance_config-->
*
* <pre>{@code
* String projectId = "my-project";
* String instanceConfigId = "custom-user-config";
*
* instanceAdminClient.deleteInstanceConfig(instanceConfigId);
* }</pre>
*
* <!--SNIPPET instance_admin_client_delete_instance_config-->
*/
default void deleteInstanceConfig(String instanceConfigId, DeleteAdminApiOption... options)
throws SpannerException {
throw new UnsupportedOperationException("Not implemented");
}

/** Lists the supported instance configs for current project. */
/* <!--SNIPPET instance_admin_client_list_configs-->
* <pre>{@code
Expand All @@ -47,6 +196,11 @@ public interface InstanceAdminClient {
*/
Page<InstanceConfig> listInstanceConfigs(ListOption... options) throws SpannerException;

/** Lists long-running instance config operations. */
default Page<Operation> listInstanceConfigOperations(ListOption... options) {
throw new UnsupportedOperationException("Not implemented");
}

/**
* Creates an instance and begins preparing it to begin serving. The returned {@code Operation}
* can be used to track the progress of preparing the new instance. The instance name is assigned
Expand Down
Expand Up @@ -23,14 +23,19 @@
import com.google.api.pathtemplate.PathTemplate;
import com.google.cloud.Policy;
import com.google.cloud.Policy.DefaultMarshaller;
import com.google.cloud.spanner.Options.CreateAdminApiOption;
import com.google.cloud.spanner.Options.DeleteAdminApiOption;
import com.google.cloud.spanner.Options.ListOption;
import com.google.cloud.spanner.Options.UpdateAdminApiOption;
import com.google.cloud.spanner.SpannerImpl.PageFetcher;
import com.google.cloud.spanner.spi.v1.SpannerRpc;
import com.google.cloud.spanner.spi.v1.SpannerRpc.Paginated;
import com.google.common.base.Preconditions;
import com.google.longrunning.Operation;
import com.google.protobuf.FieldMask;
import com.google.spanner.admin.instance.v1.CreateInstanceConfigMetadata;
import com.google.spanner.admin.instance.v1.CreateInstanceMetadata;
import com.google.spanner.admin.instance.v1.UpdateInstanceConfigMetadata;
import com.google.spanner.admin.instance.v1.UpdateInstanceMetadata;

/** Default implementation of {@link InstanceAdminClient} */
Expand Down Expand Up @@ -60,13 +65,81 @@ protected com.google.iam.v1.Policy toPb(Policy policy) {
this.dbClient = dbClient;
}

@Override
public OperationFuture<InstanceConfig, CreateInstanceConfigMetadata> createInstanceConfig(
InstanceConfigInfo instanceConfig, CreateAdminApiOption... options) throws SpannerException {
final Options createAdminApiOptions = Options.fromAdminApiOptions(options);
String projectName = PROJECT_NAME_TEMPLATE.instantiate("project", projectId);
OperationFuture<
com.google.spanner.admin.instance.v1.InstanceConfig, CreateInstanceConfigMetadata>
rawOperationFuture =
rpc.createInstanceConfig(
projectName,
instanceConfig.getId().getInstanceConfig(),
instanceConfig.toProto(),
createAdminApiOptions.validateOnly());

return new OperationFutureImpl<>(
rawOperationFuture.getPollingFuture(),
rawOperationFuture.getInitialFuture(),
snapshot ->
InstanceConfig.fromProto(
ProtoOperationTransformers.ResponseTransformer.create(
com.google.spanner.admin.instance.v1.InstanceConfig.class)
.apply(snapshot),
InstanceAdminClientImpl.this),
ProtoOperationTransformers.MetadataTransformer.create(CreateInstanceConfigMetadata.class),
e -> {
throw SpannerExceptionFactory.newSpannerException(e);
});
}

@Override
public OperationFuture<InstanceConfig, UpdateInstanceConfigMetadata> updateInstanceConfig(
InstanceConfigInfo instanceConfig,
Iterable<InstanceConfigInfo.InstanceConfigField> fieldsToUpdate,
UpdateAdminApiOption... options)
throws SpannerException {
final Options deleteAdminApiOptions = Options.fromAdminApiOptions(options);
FieldMask fieldMask = InstanceConfigInfo.InstanceConfigField.toFieldMask(fieldsToUpdate);

OperationFuture<
com.google.spanner.admin.instance.v1.InstanceConfig, UpdateInstanceConfigMetadata>
rawOperationFuture =
rpc.updateInstanceConfig(
instanceConfig.toProto(), deleteAdminApiOptions.validateOnly(), fieldMask);
return new OperationFutureImpl<>(
rawOperationFuture.getPollingFuture(),
rawOperationFuture.getInitialFuture(),
snapshot ->
InstanceConfig.fromProto(
ProtoOperationTransformers.ResponseTransformer.create(
com.google.spanner.admin.instance.v1.InstanceConfig.class)
.apply(snapshot),
InstanceAdminClientImpl.this),
ProtoOperationTransformers.MetadataTransformer.create(UpdateInstanceConfigMetadata.class),
e -> {
throw SpannerExceptionFactory.newSpannerException(e);
});
}

@Override
public InstanceConfig getInstanceConfig(String configId) throws SpannerException {
String instanceConfigName = new InstanceConfigId(projectId, configId).getName();
return InstanceConfig.fromProto(
rpc.getInstanceConfig(instanceConfigName), InstanceAdminClientImpl.this);
}

@Override
public void deleteInstanceConfig(final String instanceConfigId, DeleteAdminApiOption... options)
throws SpannerException {
final Options deleteAdminApiOptions = Options.fromAdminApiOptions(options);
rpc.deleteInstanceConfig(
new InstanceConfigId(projectId, instanceConfigId).getName(),
deleteAdminApiOptions.etag(),
deleteAdminApiOptions.validateOnly());
}

@Override
public Page<InstanceConfig> listInstanceConfigs(ListOption... options) {
final Options listOptions = Options.fromListOptions(options);
Expand All @@ -93,6 +166,30 @@ public InstanceConfig fromProto(
return pageFetcher.getNextPage();
}

@Override
public final Page<Operation> listInstanceConfigOperations(ListOption... options) {
final Options listOptions = Options.fromListOptions(options);
final int pageSize = listOptions.hasPageSize() ? listOptions.pageSize() : 0;
final String filter = listOptions.hasFilter() ? listOptions.filter() : null;

PageFetcher<Operation, Operation> pageFetcher =
new PageFetcher<Operation, Operation>() {
@Override
public Paginated<Operation> getNextPage(String nextPageToken) {
return rpc.listInstanceConfigOperations(pageSize, filter, nextPageToken);
}

@Override
public Operation fromProto(Operation proto) {
return proto;
}
};
if (listOptions.hasPageToken()) {
pageFetcher.setNextPageToken(listOptions.pageToken());
}
return pageFetcher.getNextPage();
}

@Override
public OperationFuture<Instance, CreateInstanceMetadata> createInstance(InstanceInfo instance)
throws SpannerException {
Expand Down

0 comments on commit c1c805c

Please sign in to comment.