From c1c805cf6e9c00f2d6796627d919338be1a0599a Mon Sep 17 00:00:00 2001 From: Rajat Bhatta <93644539+rajatbhatta@users.noreply.github.com> Date: Thu, 29 Sep 2022 23:04:12 +0530 Subject: [PATCH] feat: support customer managed instance configurations (#1742) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 * Update google-cloud-spanner/src/main/java/com/google/cloud/spanner/InstanceAdminClient.java Co-authored-by: Knut Olav Løite * Update google-cloud-spanner/src/main/java/com/google/cloud/spanner/InstanceAdminClient.java Co-authored-by: Knut Olav Løite * Update google-cloud-spanner/src/main/java/com/google/cloud/spanner/InstanceAdminClient.java Co-authored-by: Knut Olav Løite * Update google-cloud-spanner/src/main/java/com/google/cloud/spanner/InstanceConfigInfo.java Co-authored-by: Knut Olav Løite * Update google-cloud-spanner/src/main/java/com/google/cloud/spanner/InstanceConfigInfo.java Co-authored-by: Knut Olav Løite * 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 Co-authored-by: Owl Bot --- .../clirr-ignored-differences.xml | 40 ++ .../cloud/spanner/InstanceAdminClient.java | 154 ++++++ .../spanner/InstanceAdminClientImpl.java | 97 ++++ .../google/cloud/spanner/InstanceConfig.java | 122 ++++- .../cloud/spanner/InstanceConfigInfo.java | 465 +++++++++++++++++- .../com/google/cloud/spanner/Options.java | 117 ++++- .../cloud/spanner/spi/v1/GapicSpannerRpc.java | 98 ++++ .../cloud/spanner/spi/v1/SpannerRpc.java | 38 +- .../spanner/InstanceAdminClientImplTest.java | 129 +++++ .../cloud/spanner/InstanceConfigTest.java | 70 ++- 10 files changed, 1280 insertions(+), 50 deletions(-) diff --git a/google-cloud-spanner/clirr-ignored-differences.xml b/google-cloud-spanner/clirr-ignored-differences.xml index 4c422429c0..039278a643 100644 --- a/google-cloud-spanner/clirr-ignored-differences.xml +++ b/google-cloud-spanner/clirr-ignored-differences.xml @@ -35,6 +35,46 @@ com/google/cloud/spanner/connection/ConnectionOptions com.google.cloud.spanner.Dialect getDialect() + + 7012 + com/google/cloud/spanner/InstanceAdminClient + com.google.api.gax.longrunning.OperationFuture createInstanceConfig(com.google.cloud.spanner.InstanceConfigInfo, com.google.cloud.spanner.Options$CreateAdminApiOption[]) + + + 7012 + com/google/cloud/spanner/InstanceAdminClient + com.google.api.gax.longrunning.OperationFuture updateInstanceConfig(com.google.cloud.spanner.InstanceConfigInfo, java.lang.Iterable, com.google.cloud.spanner.Options$UpdateAdminApiOption[]) + + + 7012 + com/google/cloud/spanner/InstanceAdminClient + void deleteInstanceConfig(java.lang.String, com.google.cloud.spanner.Options$DeleteAdminApiOption[]) + + + 7012 + com/google/cloud/spanner/InstanceAdminClient + com.google.api.gax.paging.Page listInstanceConfigOperations(com.google.cloud.spanner.Options$ListOption[]) + + + 7012 + com/google/cloud/spanner/spi/v1/SpannerRpc + com.google.api.gax.longrunning.OperationFuture createInstanceConfig(java.lang.String, java.lang.String, com.google.spanner.admin.instance.v1.InstanceConfig, java.lang.Boolean) + + + 7012 + com/google/cloud/spanner/spi/v1/SpannerRpc + com.google.api.gax.longrunning.OperationFuture updateInstanceConfig(com.google.spanner.admin.instance.v1.InstanceConfig, java.lang.Boolean, com.google.protobuf.FieldMask) + + + 7012 + com/google/cloud/spanner/spi/v1/SpannerRpc + void deleteInstanceConfig(java.lang.String, java.lang.String, java.lang.Boolean) + + + 7012 + com/google/cloud/spanner/spi/v1/SpannerRpc + com.google.cloud.spanner.spi.v1.SpannerRpc$Paginated listInstanceConfigOperations(int, java.lang.String, java.lang.String) + 7013 com/google/cloud/spanner/BackupInfo$Builder diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/InstanceAdminClient.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/InstanceAdminClient.java index a84bf04aac..702b9c4059 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/InstanceAdminClient.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/InstanceAdminClient.java @@ -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. + * + *

Immediately after the request returns: + * + *

    + *
  • The instance config is readable via the API, with all requested attributes. + *
  • The instance config's {@code reconciling} field is set to true. Its state is {@code + * CREATING}. + *
+ * + * While the operation is pending: + * + *
    + *
  • Cancelling the operation renders the instance config immediately unreadable via the API. + *
  • Except for deleting the creating resource, all other attempts to modify the instance + * config are rejected. + *
+ * + * Upon completion of the returned operation: + * + *
    + *
  • Instances can be created using the instance configuration. + *
  • The instance config's {@code reconciling} field becomes false. + *
  • Its state becomes {@code READY}. + *
+ * + * + * + *
{@code
+   * String projectId = "my-project";
+   * String baseInstanceConfig = "my-base-config";
+   * String instanceConfigId = "custom-user-config";
+   *
+   * final InstanceConfig baseConfig = instanceAdminClient.getInstanceConfig(baseInstanceConfig);
+   *
+   * List readOnlyReplicas = ImmutableList.of(baseConfig.getOptionalReplicas().get(0));
+   *
+   * InstanceConfigInfo instanceConfigInfo =
+   *     InstanceConfigInfo.newBuilder(InstanceConfigId.of(projectId, instanceConfigId), baseConfig)
+   *         .setDisplayName(instanceConfigId)
+   *         .addReadOnlyReplicas(readOnlyReplicas)
+   *         .build();
+   *
+   * final OperationFuture operation =
+   *     instanceAdminClient.createInstanceConfig(instanceConfigInfo);
+   *
+   * InstanceConfig instanceConfig = op.get(5, TimeUnit.MINUTES)
+   * }
+ * + * + */ + default OperationFuture 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. + * + *

Only user managed configurations can be updated. + * + *

Immediately after the request returns: + * + *

    + *
  • The instance config's {@code reconciling} field is set to true. + *
+ * + * While the operation is pending: + * + *
    + *
  • Cancelling the operation sets its metadata's cancel_time. + *
  • The operation is guaranteed to succeed at undoing all changes, after which point it + * terminates with a `CANCELLED` status. + *
  • All other attempts to modify the instance config are rejected. + *
  • Reading the instance config via the API continues to give the pre-request values. + *
+ * + * Upon completion of the returned operation: + * + *
    + *
  • Creating instances using the instance configuration uses the new values. + *
  • The instance config's new values are readable via the API. + *
  • The instance config's {@code reconciling} field becomes false. + *
+ * + * + * + *
{@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 operation =
+   *     instanceAdminClient.updateInstanceConfig(
+   *         instanceConfigInfo, ImmutableList.of(InstanceConfigField.DISPLAY_NAME));
+   *
+   * InstanceConfig instanceConfig = operation.get(5, TimeUnit.MINUTES);
+   * }
+ * + * + */ + default OperationFuture updateInstanceConfig( + InstanceConfigInfo instanceConfig, + Iterable fieldsToUpdate, + UpdateAdminApiOption... options) + throws SpannerException { + throw new UnsupportedOperationException("Not implemented"); + } + /** Gets an instance config. */ /* *
{@code
@@ -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.
+   *
+   * 

Only user managed configurations can be deleted. + * + * + *

{@code
+   * String projectId = "my-project";
+   * String instanceConfigId = "custom-user-config";
+   *
+   * instanceAdminClient.deleteInstanceConfig(instanceConfigId);
+   * }
+ * + * + */ + default void deleteInstanceConfig(String instanceConfigId, DeleteAdminApiOption... options) + throws SpannerException { + throw new UnsupportedOperationException("Not implemented"); + } + /** Lists the supported instance configs for current project. */ /* *
{@code
@@ -47,6 +196,11 @@ public interface InstanceAdminClient {
    */
   Page listInstanceConfigs(ListOption... options) throws SpannerException;
 
+  /** Lists long-running instance config operations. */
+  default Page 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
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/InstanceAdminClientImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/InstanceAdminClientImpl.java
index 142540af66..195de83382 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/InstanceAdminClientImpl.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/InstanceAdminClientImpl.java
@@ -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} */
@@ -60,6 +65,64 @@ protected com.google.iam.v1.Policy toPb(Policy policy) {
     this.dbClient = dbClient;
   }
 
+  @Override
+  public OperationFuture 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 updateInstanceConfig(
+      InstanceConfigInfo instanceConfig,
+      Iterable 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();
@@ -67,6 +130,16 @@ public InstanceConfig getInstanceConfig(String configId) throws SpannerException
         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 listInstanceConfigs(ListOption... options) {
     final Options listOptions = Options.fromListOptions(options);
@@ -93,6 +166,30 @@ public InstanceConfig fromProto(
     return pageFetcher.getNextPage();
   }
 
+  @Override
+  public final Page 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 pageFetcher =
+        new PageFetcher() {
+          @Override
+          public Paginated 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 createInstance(InstanceInfo instance)
       throws SpannerException {
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/InstanceConfig.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/InstanceConfig.java
index 6afbc028e1..9b96dda4df 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/InstanceConfig.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/InstanceConfig.java
@@ -18,7 +18,7 @@
 
 import java.util.Collections;
 import java.util.List;
-import java.util.stream.Collectors;
+import java.util.Map;
 
 /**
  * Represents a Cloud Spanner instance config.{@code InstanceConfig} adds a layer of service related
@@ -28,10 +28,114 @@ public class InstanceConfig extends InstanceConfigInfo {
 
   private final InstanceAdminClient client;
 
+  /** Builder of {@code InstanceConfig}. */
+  public static class Builder extends InstanceConfigInfo.BuilderImpl {
+    private final InstanceAdminClient client;
+
+    Builder(InstanceConfig instanceConfig) {
+      super(instanceConfig);
+      this.client = instanceConfig.client;
+    }
+
+    Builder(InstanceAdminClient client, InstanceConfigId id) {
+      super(id);
+      this.client = client;
+    }
+
+    @Override
+    public Builder setDisplayName(String displayName) {
+      super.setDisplayName(displayName);
+      return this;
+    }
+
+    @Override
+    protected Builder setReplicas(List replicas) {
+      super.setReplicas(replicas);
+      return this;
+    }
+
+    @Override
+    public Builder setLeaderOptions(List leaderOptions) {
+      super.setLeaderOptions(leaderOptions);
+      return this;
+    }
+
+    @Override
+    protected Builder setOptionalReplicas(List optionalReplicas) {
+      super.setOptionalReplicas(optionalReplicas);
+      return this;
+    }
+
+    @Override
+    protected Builder setBaseConfig(InstanceConfigInfo baseConfig) {
+      super.setBaseConfig(baseConfig);
+      return this;
+    }
+
+    @Override
+    protected Builder setConfigType(Type configType) {
+      super.setConfigType(configType);
+      return this;
+    }
+
+    @Override
+    protected Builder setState(State state) {
+      super.setState(state);
+      return this;
+    }
+
+    @Override
+    public Builder setEtag(String etag) {
+      super.setEtag(etag);
+      return this;
+    }
+
+    @Override
+    protected Builder setReconciling(boolean reconciling) {
+      super.setReconciling(reconciling);
+      return this;
+    }
+
+    @Override
+    public Builder addLabel(String key, String value) {
+      super.addLabel(key, value);
+      return this;
+    }
+
+    @Override
+    public Builder putAllLabels(Map labels) {
+      super.putAllLabels(labels);
+      return this;
+    }
+
+    @Override
+    public Builder addReadOnlyReplicas(List readOnlyReplicas) {
+      super.addReadOnlyReplicas(readOnlyReplicas);
+      return this;
+    }
+
+    @Override
+    public InstanceConfig build() {
+      return new InstanceConfig(this);
+    }
+  }
+
+  public static Builder newBuilder(InstanceConfig instanceConfig) {
+    return new Builder(instanceConfig);
+  }
+
+  public static Builder newBuilder(InstanceAdminClient client, InstanceConfigId instanceConfigId) {
+    return new Builder(client, instanceConfigId);
+  }
+
+  /** Use {@link #newBuilder} instead */
+  @Deprecated
   public InstanceConfig(InstanceConfigId id, String displayName, InstanceAdminClient client) {
     this(id, displayName, Collections.emptyList(), Collections.emptyList(), client);
   }
 
+  /** Use {@link #newBuilder} instead */
+  @Deprecated
   public InstanceConfig(
       InstanceConfigId id,
       String displayName,
@@ -42,18 +146,18 @@ public InstanceConfig(
     this.client = client;
   }
 
+  InstanceConfig(Builder builder) {
+    super(builder);
+    this.client = builder.client;
+  }
+
   /** Gets the current state of this instance config. */
   public InstanceConfig reload() {
     return client.getInstanceConfig(getId().getInstanceConfig());
   }
 
-  static InstanceConfig fromProto(
-      com.google.spanner.admin.instance.v1.InstanceConfig proto, InstanceAdminClient client) {
-    return new InstanceConfig(
-        InstanceConfigId.of(proto.getName()),
-        proto.getDisplayName(),
-        proto.getReplicasList().stream().map(ReplicaInfo::fromProto).collect(Collectors.toList()),
-        proto.getLeaderOptionsList(),
-        client);
+  @Override
+  public Builder toBuilder() {
+    return new Builder(this);
   }
 }
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/InstanceConfigInfo.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/InstanceConfigInfo.java
index 43d4fbf953..f2b256fff4 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/InstanceConfigInfo.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/InstanceConfigInfo.java
@@ -16,33 +16,72 @@
 
 package com.google.cloud.spanner;
 
-import java.util.Collections;
+import com.google.cloud.FieldSelector;
+import com.google.common.collect.ImmutableMap;
+import com.google.protobuf.FieldMask;
+import com.google.spanner.admin.instance.v1.InstanceConfig;
+import com.google.spanner.admin.instance.v1.InstanceConfig.State;
+import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
+import java.util.stream.Collectors;
 
 /** Represents a Cloud Spanner instance config resource. */
 public class InstanceConfigInfo {
 
-  private final InstanceConfigId id;
-  private final String displayName;
-  private final List replicas;
-  private final List leaderOptions;
+  /** Represent an updatable field in Cloud Spanner InstanceConfig. */
+  public enum InstanceConfigField implements FieldSelector {
+    DISPLAY_NAME("display_name"),
+    LABELS("labels");
 
-  public InstanceConfigInfo(InstanceConfigId id, String displayName) {
-    this(id, displayName, Collections.emptyList(), Collections.emptyList());
+    private final String selector;
+
+    InstanceConfigField(String selector) {
+      this.selector = selector;
+    }
+
+    @Override
+    public String getSelector() {
+      return selector;
+    }
+
+    static FieldMask toFieldMask(Iterable fields) {
+      FieldMask.Builder builder = FieldMask.newBuilder();
+      for (InstanceConfigField field : fields) {
+        builder.addPaths(field.getSelector());
+      }
+      return builder.build();
+    }
   }
 
-  public InstanceConfigInfo(
-      InstanceConfigId id,
-      String displayName,
-      List replicas,
-      List leaderOptions) {
-    this.id = id;
-    this.displayName = displayName;
-    this.replicas = replicas;
-    this.leaderOptions = leaderOptions;
+  /** Type of the Instance config. */
+  public enum Type {
+    TYPE_UNSPECIFIED,
+    GOOGLE_MANAGED,
+    USER_MANAGED
+  }
+
+  /** Type of the Instance config. */
+  public enum State {
+    STATE_UNSPECIFIED,
+    CREATING,
+    READY
   }
 
+  private final InstanceConfigId id;
+  private final String displayName;
+  private final List replicas;
+  private final List leaderOptions;
+  private final List optionalReplicas;
+  private final InstanceConfigInfo baseConfig;
+  private final Type configType;
+  private final String etag;
+  private final boolean reconciling;
+  private final State state;
+  private final Map labels;
+
   /** Returns the id of this instance config. */
   public InstanceConfigId getId() {
     return id;
@@ -69,6 +108,254 @@ public List getLeaderOptions() {
     return leaderOptions;
   }
 
+  /**
+   * The available optional replicas to choose from for user managed configurations. Populated for
+   * Google managed configurations.
+   */
+  public List getOptionalReplicas() {
+    return optionalReplicas;
+  }
+
+  /**
+   * Base configuration, e.g. projects//instanceConfigs/nam3, based on which this
+   * configuration is created. Only set for user managed configurations. The base config must refer
+   * to a configuration of type GOOGLE_MANAGED.
+   */
+  public InstanceConfigInfo getBaseConfig() {
+    return baseConfig;
+  }
+
+  /**
+   * Config type, indicates whether this instance config is a Google or User Managed Configuration.
+   */
+  public Type getConfigType() {
+    return configType;
+  }
+
+  /**
+   * etag, which is used for optimistic concurrency control as a way to help prevent simultaneous
+   * updates of an instance config from overwriting each other.
+   */
+  public String getEtag() {
+    return etag;
+  }
+
+  /**
+   * If true, the instance config is being created or updated. If false, there are no ongoing
+   * operations for the instance config.
+   */
+  public boolean getReconciling() {
+    return reconciling;
+  }
+
+  /** The current instance config state. */
+  public State getState() {
+    return state;
+  }
+
+  /**
+   * Cloud Labels, which can be used to filter collections of resources. They can be used to control
+   * how resource metrics are aggregated.
+   */
+  public Map getLabels() {
+    return labels;
+  }
+
+  /** Builder for {@code InstanceConfigInfo}. */
+  public abstract static class Builder {
+    public abstract Builder setDisplayName(String displayName);
+
+    protected abstract Builder setReplicas(List replicas);
+
+    protected abstract Builder setOptionalReplicas(List optionalReplicas);
+
+    protected abstract Builder setBaseConfig(InstanceConfigInfo baseConfig);
+
+    /**
+     * Sets the allowed values of the "default_leader" schema option for databases in instances that
+     * use this instance configuration.
+     */
+    public abstract Builder setLeaderOptions(List leaderOptions);
+
+    protected abstract Builder setConfigType(Type configType);
+
+    protected abstract Builder setState(State state);
+
+    public abstract Builder setEtag(String etag);
+
+    protected abstract Builder setReconciling(boolean reconciling);
+
+    public abstract Builder addLabel(String key, String value);
+
+    public abstract Builder putAllLabels(Map labels);
+
+    /**
+     * Adds the read only replicas to the set of replicas for a custom instance config. Called with
+     * one or more of the optional replicas of the base config.
+     */
+    public abstract Builder addReadOnlyReplicas(List readOnlyReplicas);
+
+    public abstract InstanceConfigInfo build();
+  }
+
+  static class BuilderImpl extends Builder {
+    private InstanceConfigId id;
+    private String displayName = "";
+    private List replicas = new ArrayList<>();
+    private List leaderOptions = new ArrayList<>();
+    private List optionalReplicas = new ArrayList<>();
+    private InstanceConfigInfo baseConfig;
+    private Type configType = Type.TYPE_UNSPECIFIED;
+    private String etag = "";
+    private boolean reconciling = false;
+    private State state = State.STATE_UNSPECIFIED;
+    private Map labels = new HashMap<>();
+
+    BuilderImpl(InstanceConfigId id) {
+      this.id = id;
+    }
+
+    BuilderImpl(InstanceConfigId id, InstanceConfigInfo baseConfig) {
+      this.id = id;
+      this.baseConfig = baseConfig;
+      this.replicas = new ArrayList<>(baseConfig.replicas);
+    }
+
+    BuilderImpl(InstanceConfigInfo instanceConfigInfo) {
+      this.id = instanceConfigInfo.id;
+      this.displayName = instanceConfigInfo.displayName;
+      this.replicas = new ArrayList<>(instanceConfigInfo.replicas);
+      this.leaderOptions = new ArrayList<>(instanceConfigInfo.leaderOptions);
+      this.optionalReplicas = new ArrayList<>(instanceConfigInfo.optionalReplicas);
+      this.baseConfig = instanceConfigInfo.baseConfig;
+      this.configType = instanceConfigInfo.configType;
+      this.etag = instanceConfigInfo.etag;
+      this.reconciling = instanceConfigInfo.reconciling;
+      this.state = instanceConfigInfo.state;
+      this.labels = new HashMap<>(instanceConfigInfo.labels);
+    }
+
+    @Override
+    public Builder setDisplayName(String displayName) {
+      this.displayName = displayName;
+      return this;
+    }
+
+    @Override
+    protected Builder setReplicas(List replicas) {
+      this.replicas = replicas;
+      return this;
+    }
+
+    @Override
+    public Builder setLeaderOptions(List leaderOptions) {
+      this.leaderOptions = leaderOptions;
+      return this;
+    }
+
+    @Override
+    protected Builder setOptionalReplicas(List optionalReplicas) {
+      this.optionalReplicas = optionalReplicas;
+      return this;
+    }
+
+    @Override
+    protected Builder setBaseConfig(InstanceConfigInfo baseConfig) {
+      this.baseConfig = baseConfig;
+      return this;
+    }
+
+    @Override
+    protected Builder setConfigType(Type configType) {
+      this.configType = configType;
+      return this;
+    }
+
+    @Override
+    protected Builder setState(State state) {
+      this.state = state;
+      return this;
+    }
+
+    @Override
+    public Builder setEtag(String etag) {
+      this.etag = etag;
+      return this;
+    }
+
+    @Override
+    protected Builder setReconciling(boolean reconciling) {
+      this.reconciling = reconciling;
+      return this;
+    }
+
+    @Override
+    public Builder addLabel(String key, String value) {
+      this.labels.put(key, value);
+      return this;
+    }
+
+    @Override
+    public Builder putAllLabels(Map labels) {
+      this.labels.putAll(labels);
+      return this;
+    }
+
+    @Override
+    public Builder addReadOnlyReplicas(List readOnlyReplicas) {
+      this.replicas.addAll(readOnlyReplicas);
+      return this;
+    }
+
+    @Override
+    public InstanceConfigInfo build() {
+      return new InstanceConfigInfo(this);
+    }
+  }
+
+  /** Use {@link #newBuilder} instead */
+  @Deprecated
+  public InstanceConfigInfo(InstanceConfigId id, String displayName) {
+    this((BuilderImpl) newBuilder(id).setDisplayName(displayName));
+  }
+
+  public static Builder newBuilder(InstanceConfigId id) {
+    return new BuilderImpl(id);
+  }
+
+  public static Builder newBuilder(InstanceConfigId id, InstanceConfigInfo baseConfig) {
+    return new BuilderImpl(id, baseConfig);
+  }
+
+  /** Use {@link #newBuilder} instead */
+  @Deprecated
+  public InstanceConfigInfo(
+      InstanceConfigId id,
+      String displayName,
+      List replicas,
+      List leaderOptions) {
+    this(
+        (BuilderImpl)
+            newBuilder(id)
+                .setDisplayName(displayName)
+                .setReplicas(replicas)
+                .setLeaderOptions(leaderOptions));
+  }
+
+  InstanceConfigInfo(BuilderImpl builder) {
+    this.id = builder.id;
+    this.displayName = builder.displayName;
+    this.replicas = new ArrayList<>(builder.replicas);
+    this.leaderOptions = new ArrayList<>(builder.leaderOptions);
+    this.baseConfig = builder.baseConfig;
+    this.optionalReplicas = new ArrayList<>(builder.optionalReplicas);
+    this.configType = builder.configType;
+    this.etag = builder.etag;
+    this.reconciling = builder.reconciling;
+    this.state = builder.state;
+    this.labels = ImmutableMap.copyOf(builder.labels);
+  }
+
   @Override
   public boolean equals(Object o) {
     if (this == o) {
@@ -81,17 +368,157 @@ public boolean equals(Object o) {
     return Objects.equals(id, that.id)
         && Objects.equals(displayName, that.displayName)
         && Objects.equals(replicas, that.replicas)
-        && Objects.equals(leaderOptions, that.leaderOptions);
+        && Objects.equals(leaderOptions, that.leaderOptions)
+        && Objects.equals(optionalReplicas, that.optionalReplicas)
+        && Objects.equals(baseConfig, that.baseConfig)
+        && Objects.equals(configType, that.configType)
+        && Objects.equals(etag, that.etag)
+        && Objects.equals(reconciling, that.reconciling)
+        && Objects.equals(state, that.state)
+        && Objects.equals(labels, that.labels);
   }
 
   @Override
   public int hashCode() {
-    return Objects.hash(id, displayName, replicas, leaderOptions);
+    return Objects.hash(
+        id,
+        displayName,
+        replicas,
+        leaderOptions,
+        optionalReplicas,
+        baseConfig,
+        configType,
+        etag,
+        reconciling,
+        state,
+        labels);
+  }
+
+  public Builder toBuilder() {
+    return new BuilderImpl(this);
   }
 
   @Override
   public String toString() {
     return String.format(
-        "Instance Config[%s, %s, %s, %s]", id, displayName, replicas, leaderOptions);
+        "Instance Config[%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s]",
+        id,
+        displayName,
+        replicas,
+        leaderOptions,
+        optionalReplicas,
+        baseConfig,
+        configType,
+        etag,
+        reconciling,
+        state,
+        labels);
+  }
+
+  com.google.spanner.admin.instance.v1.InstanceConfig toProto() {
+    InstanceConfig.Builder builder =
+        com.google.spanner.admin.instance.v1.InstanceConfig.newBuilder()
+            .setName(getId().getName())
+            .setDisplayName(getDisplayName())
+            .addAllReplicas(
+                getReplicas().stream().map(ReplicaInfo::getProto).collect(Collectors.toList()))
+            .addAllLeaderOptions(getLeaderOptions())
+            .setEtag(getEtag())
+            .setReconciling(getReconciling())
+            .putAllLabels(getLabels())
+            .addAllOptionalReplicas(
+                getOptionalReplicas().stream()
+                    .map(ReplicaInfo::getProto)
+                    .collect(Collectors.toList()))
+            .setConfigType(toProtoConfigType(getConfigType()))
+            .setState(toProtoState(getState()));
+
+    if (getBaseConfig() != null) {
+      builder.setBaseConfig(getBaseConfig().getId().getName());
+    }
+
+    return builder.build();
+  }
+
+  private static InstanceConfig.Type toProtoConfigType(Type type) {
+    switch (type) {
+      case TYPE_UNSPECIFIED:
+        return com.google.spanner.admin.instance.v1.InstanceConfig.Type.TYPE_UNSPECIFIED;
+      case GOOGLE_MANAGED:
+        return com.google.spanner.admin.instance.v1.InstanceConfig.Type.GOOGLE_MANAGED;
+      case USER_MANAGED:
+        return InstanceConfig.Type.USER_MANAGED;
+      default:
+        throw new IllegalArgumentException("Unknown config type:" + type);
+    }
+  }
+
+  private static InstanceConfig.State toProtoState(State state) {
+    switch (state) {
+      case STATE_UNSPECIFIED:
+        return com.google.spanner.admin.instance.v1.InstanceConfig.State.STATE_UNSPECIFIED;
+      case CREATING:
+        return com.google.spanner.admin.instance.v1.InstanceConfig.State.CREATING;
+      case READY:
+        return com.google.spanner.admin.instance.v1.InstanceConfig.State.READY;
+      default:
+        throw new IllegalArgumentException("Unknown state:" + state);
+    }
+  }
+
+  static com.google.cloud.spanner.InstanceConfig fromProto(
+      com.google.spanner.admin.instance.v1.InstanceConfig proto, InstanceAdminClient client) {
+    com.google.cloud.spanner.InstanceConfig.Builder builder =
+        com.google.cloud.spanner.InstanceConfig.newBuilder(
+                client, InstanceConfigId.of(proto.getName()))
+            .setReconciling(proto.getReconciling())
+            .setReplicas(
+                proto.getReplicasList().stream()
+                    .map(ReplicaInfo::fromProto)
+                    .collect(Collectors.toList()))
+            .setDisplayName(proto.getDisplayName())
+            .putAllLabels(proto.getLabelsMap())
+            .setEtag(proto.getEtag())
+            .setLeaderOptions(proto.getLeaderOptionsList())
+            .setOptionalReplicas(
+                proto.getOptionalReplicasList().stream()
+                    .map(ReplicaInfo::fromProto)
+                    .collect(Collectors.toList()))
+            .setState(fromProtoState(proto.getState()))
+            .setConfigType(fromProtoConfigType(proto.getConfigType()));
+
+    if (!proto.getBaseConfig().isEmpty()) {
+      builder.setBaseConfig(newBuilder(InstanceConfigId.of(proto.getBaseConfig())).build());
+    }
+
+    return builder.build();
+  }
+
+  private static State fromProtoState(
+      com.google.spanner.admin.instance.v1.InstanceConfig.State state) {
+    switch (state) {
+      case STATE_UNSPECIFIED:
+        return State.STATE_UNSPECIFIED;
+      case CREATING:
+        return State.CREATING;
+      case READY:
+        return State.READY;
+      default:
+        throw new IllegalArgumentException("Unknown state:" + state);
+    }
+  }
+
+  private static Type fromProtoConfigType(
+      com.google.spanner.admin.instance.v1.InstanceConfig.Type type) {
+    switch (type) {
+      case TYPE_UNSPECIFIED:
+        return Type.TYPE_UNSPECIFIED;
+      case GOOGLE_MANAGED:
+        return Type.GOOGLE_MANAGED;
+      case USER_MANAGED:
+        return Type.USER_MANAGED;
+      default:
+        throw new IllegalArgumentException("Unknown config type:" + type);
+    }
   }
 }
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Options.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Options.java
index fa2cd2526e..7b411b4caa 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Options.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Options.java
@@ -51,6 +51,22 @@ public interface ReadOption {}
   public interface ReadQueryUpdateTransactionOption
       extends ReadOption, QueryOption, UpdateOption, TransactionOption {}
 
+  /**
+   * Marker interface to mark options applicable to Create, Update and Delete operations in admin
+   * API.
+   */
+  public interface CreateUpdateDeleteAdminApiOption
+      extends CreateAdminApiOption, UpdateAdminApiOption, DeleteAdminApiOption {}
+
+  /** Marker interface to mark options applicable to Create operations in admin API. */
+  public interface CreateAdminApiOption extends AdminApiOption {}
+
+  /** Marker interface to mark options applicable to Delete operations in admin API. */
+  public interface DeleteAdminApiOption extends AdminApiOption {}
+
+  /** Marker interface to mark options applicable to Update operations in admin API. */
+  public interface UpdateAdminApiOption extends AdminApiOption {}
+
   /** Marker interface to mark options applicable to query operation. */
   public interface QueryOption {}
 
@@ -63,6 +79,9 @@ public interface UpdateOption {}
   /** Marker interface to mark options applicable to list operations in admin API. */
   public interface ListOption {}
 
+  /** Marker interface to mark options applicable to operations in admin API. */
+  public interface AdminApiOption {}
+
   /** Specifying this instructs the transaction to request {@link CommitStats} from the backend. */
   public static TransactionOption commitStats() {
     return COMMIT_STATS_OPTION;
@@ -151,6 +170,33 @@ public static ListOption filter(String filter) {
     return new FilterOption(filter);
   }
 
+  /**
+   * Specifying this will help in optimistic concurrency control as a way to help prevent
+   * simultaneous deletes of an instance config from overwriting each other. Operations that support
+   * this option are:
+   *
+   * 
    + *
  • {@link InstanceAdminClient#deleteInstanceConfig} + *
+ */ + public static DeleteAdminApiOption etag(String etag) { + return new EtagOption(etag); + } + + /** + * Specifying this will not actually execute a request, and provide the same response. Operations + * that support this option are: + * + *
    + *
  • {@link InstanceAdminClient#createInstanceConfig} + *
  • {@link InstanceAdminClient#updateInstanceConfig} + *
  • {@link InstanceAdminClient#deleteInstanceConfig} + *
+ */ + public static CreateUpdateDeleteAdminApiOption validateOnly(Boolean validateOnly) { + return new ValidateOnlyOption(validateOnly); + } + /** Option to request {@link CommitStats} for read/write transactions. */ static final class CommitStatsOption extends InternalOption implements TransactionOption { @Override @@ -215,6 +261,33 @@ void appendToOptions(Options options) { } } + static final class EtagOption extends InternalOption implements DeleteAdminApiOption { + private final String etag; + + EtagOption(String etag) { + this.etag = etag; + } + + @Override + void appendToOptions(Options options) { + options.etag = etag; + } + } + + static final class ValidateOnlyOption extends InternalOption + implements CreateUpdateDeleteAdminApiOption { + private final Boolean validateOnly; + + ValidateOnlyOption(Boolean validateOnly) { + this.validateOnly = validateOnly; + } + + @Override + void appendToOptions(Options options) { + options.validateOnly = validateOnly; + } + } + private boolean withCommitStats; private Long limit; private Integer prefetchChunks; @@ -224,6 +297,8 @@ void appendToOptions(Options options) { private String filter; private RpcPriority priority; private String tag; + private String etag; + private Boolean validateOnly; // Construction is via factory methods below. private Options() {} @@ -296,6 +371,22 @@ String tag() { return tag; } + boolean hasEtag() { + return etag != null; + } + + String etag() { + return etag; + } + + boolean hasValidateOnly() { + return validateOnly != null; + } + + Boolean validateOnly() { + return validateOnly; + } + @Override public String toString() { StringBuilder b = new StringBuilder(); @@ -323,6 +414,12 @@ public String toString() { if (tag != null) { b.append("tag: ").append(tag).append(' '); } + if (etag != null) { + b.append("etag: ").append(etag).append(' '); + } + if (validateOnly != null) { + b.append("validateOnly: ").append(validateOnly).append(' '); + } return b.toString(); } @@ -354,7 +451,9 @@ public boolean equals(Object o) { && Objects.equals(pageToken(), that.pageToken()) && Objects.equals(filter(), that.filter()) && Objects.equals(priority(), that.priority()) - && Objects.equals(tag(), that.tag()); + && Objects.equals(tag(), that.tag()) + && Objects.equals(etag(), that.etag()) + && Objects.equals(validateOnly(), that.validateOnly()); } @Override @@ -387,6 +486,12 @@ public int hashCode() { if (tag != null) { result = 31 * result + tag.hashCode(); } + if (etag != null) { + result = 31 * result + etag.hashCode(); + } + if (validateOnly != null) { + result = 31 * result + validateOnly.hashCode(); + } return result; } @@ -440,6 +545,16 @@ static Options fromListOptions(ListOption... options) { return listOptions; } + static Options fromAdminApiOptions(AdminApiOption... options) { + Options adminApiOptions = new Options(); + for (AdminApiOption option : options) { + if (option instanceof InternalOption) { + ((InternalOption) option).appendToOptions(adminApiOptions); + } + } + return adminApiOptions; + } + private abstract static class InternalOption { abstract void appendToOptions(Options options); } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpc.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpc.java index 937da7e8d2..3f28d46c1c 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpc.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpc.java @@ -133,18 +133,25 @@ import com.google.spanner.admin.database.v1.UpdateBackupRequest; import com.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata; import com.google.spanner.admin.database.v1.UpdateDatabaseDdlRequest; +import com.google.spanner.admin.instance.v1.CreateInstanceConfigMetadata; +import com.google.spanner.admin.instance.v1.CreateInstanceConfigRequest; import com.google.spanner.admin.instance.v1.CreateInstanceMetadata; import com.google.spanner.admin.instance.v1.CreateInstanceRequest; +import com.google.spanner.admin.instance.v1.DeleteInstanceConfigRequest; import com.google.spanner.admin.instance.v1.DeleteInstanceRequest; import com.google.spanner.admin.instance.v1.GetInstanceConfigRequest; import com.google.spanner.admin.instance.v1.GetInstanceRequest; import com.google.spanner.admin.instance.v1.Instance; import com.google.spanner.admin.instance.v1.InstanceAdminGrpc; import com.google.spanner.admin.instance.v1.InstanceConfig; +import com.google.spanner.admin.instance.v1.ListInstanceConfigOperationsRequest; +import com.google.spanner.admin.instance.v1.ListInstanceConfigOperationsResponse; import com.google.spanner.admin.instance.v1.ListInstanceConfigsRequest; import com.google.spanner.admin.instance.v1.ListInstanceConfigsResponse; import com.google.spanner.admin.instance.v1.ListInstancesRequest; import com.google.spanner.admin.instance.v1.ListInstancesResponse; +import com.google.spanner.admin.instance.v1.UpdateInstanceConfigMetadata; +import com.google.spanner.admin.instance.v1.UpdateInstanceConfigRequest; import com.google.spanner.admin.instance.v1.UpdateInstanceMetadata; import com.google.spanner.admin.instance.v1.UpdateInstanceRequest; import com.google.spanner.v1.BatchCreateSessionsRequest; @@ -879,6 +886,48 @@ public Paginated listInstanceConfigs(int pageSize, @Nullable Str return new Paginated<>(response.getInstanceConfigsList(), response.getNextPageToken()); } + @Override + public OperationFuture createInstanceConfig( + String parent, + String instanceConfigId, + InstanceConfig instanceConfig, + @Nullable Boolean validateOnly) + throws SpannerException { + CreateInstanceConfigRequest.Builder builder = + CreateInstanceConfigRequest.newBuilder() + .setParent(parent) + .setInstanceConfigId(instanceConfigId) + .setInstanceConfig(instanceConfig); + if (validateOnly != null) { + builder.setValidateOnly(validateOnly); + } + CreateInstanceConfigRequest request = builder.build(); + GrpcCallContext context = + newCallContext(null, parent, request, InstanceAdminGrpc.getCreateInstanceConfigMethod()); + return instanceAdminStub.createInstanceConfigOperationCallable().futureCall(request, context); + } + + @Override + public OperationFuture updateInstanceConfig( + InstanceConfig instanceConfig, @Nullable Boolean validateOnly, FieldMask fieldMask) + throws SpannerException { + UpdateInstanceConfigRequest.Builder builder = + UpdateInstanceConfigRequest.newBuilder() + .setInstanceConfig(instanceConfig) + .setUpdateMask(fieldMask); + if (validateOnly != null) { + builder.setValidateOnly(validateOnly); + } + UpdateInstanceConfigRequest request = builder.build(); + GrpcCallContext context = + newCallContext( + null, + instanceConfig.getName(), + request, + InstanceAdminGrpc.getUpdateInstanceConfigMethod()); + return instanceAdminStub.updateInstanceConfigOperationCallable().futureCall(request, context); + } + @Override public InstanceConfig getInstanceConfig(String instanceConfigName) throws SpannerException { GetInstanceConfigRequest request = @@ -889,6 +938,55 @@ public InstanceConfig getInstanceConfig(String instanceConfigName) throws Spanne return get(instanceAdminStub.getInstanceConfigCallable().futureCall(request, context)); } + @Override + public void deleteInstanceConfig( + String instanceConfigName, @Nullable String etag, @Nullable Boolean validateOnly) + throws SpannerException { + DeleteInstanceConfigRequest.Builder requestBuilder = + DeleteInstanceConfigRequest.newBuilder().setName(instanceConfigName); + + if (etag != null) { + requestBuilder.setEtag(etag); + } + if (validateOnly != null) { + requestBuilder.setValidateOnly(validateOnly); + } + DeleteInstanceConfigRequest request = requestBuilder.build(); + GrpcCallContext context = + newCallContext( + null, instanceConfigName, request, InstanceAdminGrpc.getDeleteInstanceConfigMethod()); + get(instanceAdminStub.deleteInstanceConfigCallable().futureCall(request, context)); + } + + @Override + public Paginated listInstanceConfigOperations( + int pageSize, @Nullable String filter, @Nullable String pageToken) { + acquireAdministrativeRequestsRateLimiter(); + ListInstanceConfigOperationsRequest.Builder requestBuilder = + ListInstanceConfigOperationsRequest.newBuilder() + .setParent(projectName) + .setPageSize(pageSize); + if (filter != null) { + requestBuilder.setFilter(filter); + } + if (pageToken != null) { + requestBuilder.setPageToken(pageToken); + } + final ListInstanceConfigOperationsRequest request = requestBuilder.build(); + + final GrpcCallContext context = + newCallContext( + null, projectName, request, InstanceAdminGrpc.getListInstanceConfigOperationsMethod()); + ListInstanceConfigOperationsResponse response = + runWithRetryOnAdministrativeRequestsExceeded( + () -> + get( + instanceAdminStub + .listInstanceConfigOperationsCallable() + .futureCall(request, context))); + return new Paginated<>(response.getOperationsList(), response.getNextPageToken()); + } + @Override public Paginated listInstances( int pageSize, @Nullable String pageToken, @Nullable String filter) throws SpannerException { diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/SpannerRpc.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/SpannerRpc.java index 189af2fb55..2f68b9c1df 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/SpannerRpc.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/SpannerRpc.java @@ -34,10 +34,19 @@ import com.google.longrunning.Operation; import com.google.protobuf.Empty; import com.google.protobuf.FieldMask; -import com.google.spanner.admin.database.v1.*; +import com.google.spanner.admin.database.v1.Backup; +import com.google.spanner.admin.database.v1.CopyBackupMetadata; +import com.google.spanner.admin.database.v1.CreateBackupMetadata; +import com.google.spanner.admin.database.v1.CreateDatabaseMetadata; +import com.google.spanner.admin.database.v1.Database; +import com.google.spanner.admin.database.v1.DatabaseRole; +import com.google.spanner.admin.database.v1.RestoreDatabaseMetadata; +import com.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata; +import com.google.spanner.admin.instance.v1.CreateInstanceConfigMetadata; import com.google.spanner.admin.instance.v1.CreateInstanceMetadata; import com.google.spanner.admin.instance.v1.Instance; import com.google.spanner.admin.instance.v1.InstanceConfig; +import com.google.spanner.admin.instance.v1.UpdateInstanceConfigMetadata; import com.google.spanner.admin.instance.v1.UpdateInstanceMetadata; import com.google.spanner.v1.*; import java.util.List; @@ -162,8 +171,35 @@ interface StreamingCall { Paginated listInstanceConfigs(int pageSize, @Nullable String pageToken) throws SpannerException; + default OperationFuture createInstanceConfig( + String parent, + String instanceConfigId, + InstanceConfig instanceConfig, + @Nullable Boolean validateOnly) + throws SpannerException { + throw new UnsupportedOperationException("Not implemented"); + } + + default OperationFuture updateInstanceConfig( + InstanceConfig instanceConfig, @Nullable Boolean validateOnly, FieldMask fieldMask) + throws SpannerException { + throw new UnsupportedOperationException("Not implemented"); + } + InstanceConfig getInstanceConfig(String instanceConfigName) throws SpannerException; + default void deleteInstanceConfig( + String instanceConfigName, @Nullable String etag, @Nullable Boolean validateOnly) + throws SpannerException { + throw new UnsupportedOperationException("Not implemented"); + } + + /** List all long-running instance config operations on the given project. */ + default Paginated listInstanceConfigOperations( + int pageSize, @Nullable String filter, @Nullable String pageToken) { + throw new UnsupportedOperationException("Not implemented"); + } + Paginated listInstances( int pageSize, @Nullable String pageToken, @Nullable String filter) throws SpannerException; diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/InstanceAdminClientImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/InstanceAdminClientImplTest.java index 0df869de67..d8134ef29d 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/InstanceAdminClientImplTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/InstanceAdminClientImplTest.java @@ -27,6 +27,7 @@ import com.google.api.gax.longrunning.OperationFuture; import com.google.cloud.Identity; import com.google.cloud.Role; +import com.google.cloud.spanner.InstanceConfigInfo.InstanceConfigField; import com.google.cloud.spanner.spi.v1.SpannerRpc; import com.google.cloud.spanner.spi.v1.SpannerRpc.Paginated; import com.google.common.collect.ImmutableList; @@ -36,10 +37,14 @@ import com.google.iam.v1.Binding; import com.google.iam.v1.Policy; import com.google.iam.v1.TestIamPermissionsResponse; +import com.google.longrunning.Operation; +import com.google.protobuf.Any; import com.google.protobuf.ByteString; 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.InstanceConfig; +import com.google.spanner.admin.instance.v1.UpdateInstanceConfigMetadata; import com.google.spanner.admin.instance.v1.UpdateInstanceMetadata; import java.util.Arrays; import java.util.List; @@ -59,6 +64,7 @@ public class InstanceAdminClientImplTest { private static final String CONFIG_ID = "my-config"; private static final String CONFIG_NAME = "projects/my-project/instanceConfigs/my-config"; private static final String CONFIG_NAME2 = "projects/my-project/instanceConfigs/my-config2"; + private static final String BASE_CONFIG = "projects/my-project/instanceConfigs/my-base-config"; @Mock SpannerRpc rpc; @Mock DatabaseAdminClient dbClient; @@ -70,6 +76,84 @@ public void setUp() { client = new InstanceAdminClientImpl(PROJECT_ID, rpc, dbClient); } + private List getAllReplicas() { + return Arrays.asList( + com.google.spanner.admin.instance.v1.ReplicaInfo.newBuilder() + .setLocation("Replica Location 1") + .setType(com.google.spanner.admin.instance.v1.ReplicaInfo.ReplicaType.READ_WRITE) + .setDefaultLeaderLocation(true) + .build(), + com.google.spanner.admin.instance.v1.ReplicaInfo.newBuilder() + .setLocation("Replica Location 2") + .setType(com.google.spanner.admin.instance.v1.ReplicaInfo.ReplicaType.READ_ONLY) + .setDefaultLeaderLocation(false) + .build(), + com.google.spanner.admin.instance.v1.ReplicaInfo.newBuilder() + .setLocation("Replica Location 3") + .setType(com.google.spanner.admin.instance.v1.ReplicaInfo.ReplicaType.WITNESS) + .setDefaultLeaderLocation(false) + .build()); + } + + private com.google.spanner.admin.instance.v1.InstanceConfig getInstanceConfigProto() { + return com.google.spanner.admin.instance.v1.InstanceConfig.newBuilder() + .setName(CONFIG_NAME) + .setBaseConfig(BASE_CONFIG) + .addAllReplicas(getAllReplicas()) + .build(); + } + + @Test + public void createInstanceConfig() { + OperationFuture< + com.google.spanner.admin.instance.v1.InstanceConfig, CreateInstanceConfigMetadata> + rawOperationFuture = + OperationFutureUtil.immediateOperationFuture( + "createInstanceConfig", + getInstanceConfigProto(), + CreateInstanceConfigMetadata.getDefaultInstance()); + when(rpc.createInstanceConfig( + "projects/" + PROJECT_ID, CONFIG_ID, getInstanceConfigProto(), false)) + .thenReturn(rawOperationFuture); + + InstanceConfigInfo instanceConfigInfo = + InstanceConfigInfo.fromProto(getInstanceConfigProto(), client); + + OperationFuture op = + client.createInstanceConfig(instanceConfigInfo, Options.validateOnly(false)); + assertThat(op.isDone()).isTrue(); + } + + @Test + public void updateInstanceConfig() throws Exception { + com.google.spanner.admin.instance.v1.InstanceConfig instanceConfig = + com.google.spanner.admin.instance.v1.InstanceConfig.newBuilder() + .setName(CONFIG_NAME) + .setDisplayName(CONFIG_NAME) + .build(); + OperationFuture< + com.google.spanner.admin.instance.v1.InstanceConfig, UpdateInstanceConfigMetadata> + rawOperationFuture = + OperationFutureUtil.immediateOperationFuture( + "updateInstanceConfig", + getInstanceConfigProto(), + UpdateInstanceConfigMetadata.getDefaultInstance()); + when(rpc.updateInstanceConfig( + instanceConfig, false, FieldMask.newBuilder().addPaths("display_name").build())) + .thenReturn(rawOperationFuture); + InstanceConfigInfo instanceConfigInfo = + InstanceConfigInfo.newBuilder(InstanceConfigId.of(CONFIG_NAME)) + .setDisplayName(CONFIG_NAME) + .build(); + OperationFuture op = + client.updateInstanceConfig( + instanceConfigInfo, + ImmutableList.of(InstanceConfigField.DISPLAY_NAME), + Options.validateOnly(false)); + assertThat(op.isDone()).isTrue(); + assertThat(op.get().getId().getName()).isEqualTo(CONFIG_NAME); + } + @Test public void getInstanceConfig() { when(rpc.getInstanceConfig(CONFIG_NAME)) @@ -77,6 +161,51 @@ public void getInstanceConfig() { assertThat(client.getInstanceConfig(CONFIG_ID).getId().getName()).isEqualTo(CONFIG_NAME); } + @Test + public void dropInstanceConfig() { + client.deleteInstanceConfig(CONFIG_ID); + verify(rpc).deleteInstanceConfig(CONFIG_NAME, null, null); + } + + public Operation getInstanceConfigOperation(String instanceConfigId, Integer operationId) { + InstanceConfig instanceConfig = + com.google.spanner.admin.instance.v1.InstanceConfig.newBuilder() + .setName(instanceConfigId) + .setBaseConfig(BASE_CONFIG) + .addAllReplicas(getAllReplicas()) + .build(); + + CreateInstanceConfigMetadata metadata = + CreateInstanceConfigMetadata.newBuilder().setInstanceConfig(instanceConfig).build(); + + final String operationName = + String.format( + "projects/%s/instanceConfigs/%s/operations/%d", + PROJECT_ID, instanceConfigId, operationId); + return com.google.longrunning.Operation.newBuilder() + .setMetadata(Any.pack(metadata)) + .setResponse(Any.pack(instanceConfig)) + .setDone(false) + .setName(operationName) + .build(); + } + + @Test + public void listInstanceConfigOperations() { + String nextToken = "token"; + Operation operation1 = getInstanceConfigOperation("custom-instance-config-1", 1); + Operation operation2 = getInstanceConfigOperation("custom-instance-config-2", 2); + when(rpc.listInstanceConfigOperations(1, null, null)) + .thenReturn(new Paginated<>(ImmutableList.of(operation1), nextToken)); + when(rpc.listInstanceConfigOperations(1, null, nextToken)) + .thenReturn(new Paginated<>(ImmutableList.of(operation2), "")); + List operations = + Lists.newArrayList(client.listInstanceConfigOperations(Options.pageSize(1)).iterateAll()); + assertThat(operations.get(0).getName()).isEqualTo(operation1.getName()); + assertThat(operations.get(1).getName()).isEqualTo(operation2.getName()); + assertThat(operations.size()).isEqualTo(2); + } + @Test public void listInstanceConfigs() { String nextToken = "token"; diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/InstanceConfigTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/InstanceConfigTest.java index 3b0f56c088..234c322f76 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/InstanceConfigTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/InstanceConfigTest.java @@ -40,6 +40,19 @@ public void testInstanceConfigFromProto() { com.google.spanner.admin.instance.v1.InstanceConfig.newBuilder() .setDisplayName("Display Name") .setName("projects/my-project/instanceConfigs/my-instance-config") + .setBaseConfig("projects/my-project/instanceConfigs/custom-base-config") + .addAllOptionalReplicas( + Arrays.asList( + com.google.spanner.admin.instance.v1.ReplicaInfo.newBuilder() + .setLocation("Optional Replica Location 1") + .setType(ReplicaType.READ_ONLY) + .setDefaultLeaderLocation(true) + .build(), + com.google.spanner.admin.instance.v1.ReplicaInfo.newBuilder() + .setLocation("Optional Replica Location 2") + .setType(ReplicaType.READ_ONLY) + .setDefaultLeaderLocation(false) + .build())) .addAllLeaderOptions(Arrays.asList("Leader Option 1", "Leader Option 2")) .addAllReplicas( Arrays.asList( @@ -63,26 +76,43 @@ public void testInstanceConfigFromProto() { assertEquals( new InstanceConfig( - InstanceConfigId.of("my-project", "my-instance-config"), - "Display Name", - Arrays.asList( - ReplicaInfo.newBuilder() - .setLocation("Replica Location 1") - .setType(ReplicaInfo.ReplicaType.READ_WRITE) - .setDefaultLeaderLocation(true) - .build(), - ReplicaInfo.newBuilder() - .setLocation("Replica Location 2") - .setType(ReplicaInfo.ReplicaType.READ_ONLY) - .setDefaultLeaderLocation(false) - .build(), - ReplicaInfo.newBuilder() - .setLocation("Replica Location 3") - .setType(ReplicaInfo.ReplicaType.WITNESS) - .setDefaultLeaderLocation(false) - .build()), - Arrays.asList("Leader Option 1", "Leader Option 2"), - client), + new InstanceConfig.Builder( + client, InstanceConfigId.of("my-project", "my-instance-config")) + .setDisplayName("Display Name") + .setReplicas( + Arrays.asList( + ReplicaInfo.newBuilder() + .setLocation("Replica Location 1") + .setType(ReplicaInfo.ReplicaType.READ_WRITE) + .setDefaultLeaderLocation(true) + .build(), + ReplicaInfo.newBuilder() + .setLocation("Replica Location 2") + .setType(ReplicaInfo.ReplicaType.READ_ONLY) + .setDefaultLeaderLocation(false) + .build(), + ReplicaInfo.newBuilder() + .setLocation("Replica Location 3") + .setType(ReplicaInfo.ReplicaType.WITNESS) + .setDefaultLeaderLocation(false) + .build())) + .setLeaderOptions(Arrays.asList("Leader Option 1", "Leader Option 2")) + .setOptionalReplicas( + Arrays.asList( + ReplicaInfo.newBuilder() + .setLocation("Optional Replica Location 1") + .setType(ReplicaInfo.ReplicaType.READ_ONLY) + .setDefaultLeaderLocation(true) + .build(), + ReplicaInfo.newBuilder() + .setLocation("Optional Replica Location 2") + .setType(ReplicaInfo.ReplicaType.READ_ONLY) + .setDefaultLeaderLocation(false) + .build())) + .setBaseConfig( + new InstanceConfigInfo.BuilderImpl( + InstanceConfigId.of("my-project", "custom-base-config")) + .build())), instanceConfig); }