Immediately after the request returns: + * + *
{@code + * String projectId = "my-project"; + * String baseInstanceConfig = "my-base-config"; + * String instanceConfigId = "custom-user-config"; + * + * final InstanceConfig baseConfig = instanceAdminClient.getInstanceConfig(baseInstanceConfig); + * + * List+ * + * + */ + default OperationFuturereadOnlyReplicas = 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) + * }
Only user managed configurations can be updated. + * + *
Immediately after the request returns: + * + *
{@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+ * + * + */ + default OperationFutureoperation = + * instanceAdminClient.updateInstanceConfig( + * instanceConfigInfo, ImmutableList.of(InstanceConfigField.DISPLAY_NAME)); + * + * InstanceConfig instanceConfig = operation.get(5, TimeUnit.MINUTES); + * }
{@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 { */ PagelistInstanceConfigs(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 142540af667..195de83382a 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 6afbc028e19..9b96dda4df7 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 43d4fbf9535..f2b256fff40 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 fa2cd2526e4..7b411b4caa0 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: + * + * + *
+ */ + 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#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 937da7e8d2a..3f28d46c1c2 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- {@link InstanceAdminClient#createInstanceConfig} + *
- {@link InstanceAdminClient#updateInstanceConfig} + *
- {@link InstanceAdminClient#deleteInstanceConfig} + *
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 189af2fb55c..2f68b9c1df2 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 0df869de676..d8134ef29d6 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 3b0f56c0889..234c322f762 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); }