diff --git a/google-cloud-spanner/acceptance/spanner/backup_operations_test.rb b/google-cloud-spanner/acceptance/spanner/backup_operations_test.rb new file mode 100644 index 000000000000..802c08f53d85 --- /dev/null +++ b/google-cloud-spanner/acceptance/spanner/backup_operations_test.rb @@ -0,0 +1,99 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +require "spanner_helper" + +describe "Spanner Database Backup Operations", :spanner do + let(:instance_id) { $spanner_instance_id } + let(:database_id) { $spanner_database_id } + let(:backup_id) { "#{$spanner_database_id}-ops" } + let(:expire_time) { Time.now + 36000 } + + it "list backup operations" do + instance = spanner.instance instance_id + instance.wont_be :nil? + + database = instance.database database_id + database.wont_be :nil? + + job = database.create_backup backup_id, expire_time + job.wait_until_done! + + # All + jobs = instance.backup_operations.all.to_a + jobs.wont_be :empty? + + jobs.each do |job| + job.must_be_kind_of Google::Cloud::Spanner::Backup::Job + + unless job.error? + job.backup.must_be_kind_of Google::Cloud::Spanner::Backup + end + + job.progress_percent.must_be :>=, 0 + job.start_time.must_be_kind_of Time + end + + job = jobs.first + job.reload!.must_be_kind_of Google::Cloud::Spanner::Backup::Job + + # Filter completed jobs + filter = "done:true" + jobs = instance.backup_operations(filter: filter).all.to_a + jobs.wont_be :empty? + jobs.each do |job| + job.must_be :done? + end + + # Filter by database name + filter = "metadata.database:#{database_id}" + jobs = instance.backup_operations(filter: filter).all.to_a + jobs.wont_be :empty? + jobs.each do |job| + job.backup.database_id.must_equal database_id unless job.error? + end + + # Filter by metdata type + filter = "metadata.@type:CreateBackupMetadata" + jobs = instance.backup_operations(filter: filter).all.to_a + jobs.wont_be :empty? + jobs.each do |job| + job.grpc.metadata.must_be_kind_of Google::Spanner::Admin::Database::V1::CreateBackupMetadata + end + + # Filter by job start time + time = (Time.now - 360000) + filter = "metadata.progress.start_time > \"#{time.iso8601}\"" + jobs = instance.backup_operations(filter: filter).all.to_a + jobs.wont_be :empty? + jobs.each do |job| + job.start_time.must_be :>, time + end + + # Filer - AND + time = (Time.now - 360000) + filter = [ + "metadata.database:#{database_id}", + "metadata.progress.start_time > \"#{time.iso8601}\"" + ].map{|f| "(#{f})"}.join(" AND ") + + jobs = instance.backup_operations(filter: filter).all.to_a + jobs.wont_be :empty? + jobs.each do |job| + job.backup.database_id.must_equal database_id unless job.error? + job.start_time.must_be :>, time + end + end +end diff --git a/google-cloud-spanner/acceptance/spanner/backup_test.rb b/google-cloud-spanner/acceptance/spanner/backup_test.rb new file mode 100644 index 000000000000..3659cce8ec2c --- /dev/null +++ b/google-cloud-spanner/acceptance/spanner/backup_test.rb @@ -0,0 +1,156 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +require "spanner_helper" + +describe "Spanner Database Backup", :spanner do + let(:instance_id) { $spanner_instance_id } + let(:database_id) { $spanner_database_id } + let(:expire_time) { Time.now + 36000 } + + it "creates, get, updates, restore and delete a database backup" do + backup_id = "#{$spanner_database_id}-crud" + database = spanner.database instance_id, database_id + database.wont_be :nil? + + # Create + job = database.create_backup backup_id, expire_time + + job.must_be_kind_of Google::Cloud::Spanner::Backup::Job + job.wont_be :done? + job.wait_until_done! + + job.must_be :done? + job.error.must_be :nil? + + backup = job.backup + backup.wont_be :nil? + backup.must_be_kind_of Google::Cloud::Spanner::Backup + backup.backup_id.must_equal backup_id + backup.database_id.must_equal database_id + backup.instance_id.must_equal instance_id + backup.project_id.must_equal spanner.project + backup.expire_time.to_i.must_equal expire_time.to_i + backup.create_time.must_be_kind_of Time + backup.size_in_bytes.must_be :>, 0 + + # Get + instance = spanner.instance instance_id + backup = instance.backup backup_id + + backup.wont_be :nil? + backup.must_be_kind_of Google::Cloud::Spanner::Backup + backup.backup_id.must_equal backup_id + backup.database_id.must_equal database_id + backup.instance_id.must_equal instance_id + backup.project_id.must_equal spanner.project + backup.expire_time.to_i.must_equal expire_time.to_i + backup.create_time.must_be_kind_of Time + backup.size_in_bytes.must_be :>, 0 + + # Update + backup.expire_time = expire_time + 3600 + backup = instance.backup backup_id + backup.expire_time.to_i.must_equal((expire_time + 3600).to_i) + + proc { + backup.expire_time = Time.now - 36000 + }.must_raise Google::Cloud::Error + backup.expire_time.to_i.must_equal((expire_time + 3600 ).to_i) + + # Restore + restore_database_id = "restore-#{database_id}" + backup = instance.backup backup_id + job = backup.restore restore_database_id + job.wont_be :done? + + job.wait_until_done! + + job.must_be :done? + job.wont_be :error? + + database = job.database + database.must_be_kind_of Google::Cloud::Spanner::Database + database.database_id.must_equal restore_database_id + database.instance_id.must_equal instance_id + database.project_id.must_equal spanner.project + + restore_info = database.restore_info + restore_info.must_be_kind_of Google::Cloud::Spanner::Database::RestoreInfo + restore_info.source_type.must_equal :BACKUP + restore_info.must_be :source_backup? + + backup_info = restore_info.backup_info + backup_info.must_be_kind_of Google::Cloud::Spanner::Database::BackupInfo + backup_info.project_id.must_equal spanner.project + backup_info.instance_id.must_equal instance_id + backup_info.backup_id.must_equal backup_id + backup_info.source_database_project_id.must_equal spanner.project + backup_info.source_database_instance_id.must_equal instance_id + backup_info.source_database_id.must_equal database_id + backup_info.create_time.must_be_kind_of Time + + # Delete + backup.delete + instance.backup(backup_id).must_be :nil? + end + + it "cancel create backup operation" do + backup_id = "#{$spanner_database_id}-cancel" + database = spanner.database instance_id, database_id + + job = database.create_backup backup_id, expire_time + job.wont_be :done? + + job.cancel + + job.reload! + job.must_be :done? + job.error.wont_be :nil? + job.error.code.must_equal 1 + job.error.description.must_equal "CANCELLED" + end + + it "lists and gets database backups" do + backup_id = "#{$spanner_database_id}-list" + database = spanner.database instance_id, database_id + database.wont_be :nil? + + job = database.create_backup backup_id, expire_time + job.wait_until_done! + backup = job.backup + + instance = spanner.instance instance_id + + # List all + all_backups = instance.backups.all.to_a + all_backups.wont_be :empty? + all_backups.each do |backup| + backup.must_be_kind_of Google::Cloud::Spanner::Backup + end + + # Filter by backup name + backups = instance.backups(filter: "name:#{backup_id}").to_a + backups.length.must_equal 1 + backups.first.backup_id.must_equal backup_id + + # Filter by database name + backups = instance.backups(filter: "database:#{database_id}").to_a + backups.wont_be :empty? + backups.first.database_id.must_equal database_id + + backup.delete + end +end diff --git a/google-cloud-spanner/acceptance/spanner/database_operations_test.rb b/google-cloud-spanner/acceptance/spanner/database_operations_test.rb new file mode 100644 index 000000000000..6879ab27c241 --- /dev/null +++ b/google-cloud-spanner/acceptance/spanner/database_operations_test.rb @@ -0,0 +1,54 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +require "spanner_helper" + +describe "Spanner Database Operations", :spanner do + let(:instance_id) { $spanner_instance_id } + let(:database_id) { $spanner_database_id } + + it "list database operations" do + instance = spanner.instance instance_id + instance.wont_be :nil? + + # All + jobs = instance.database_operations.all.to_a + jobs.wont_be :empty? + + jobs.each do |job| + job.must_be_kind_of Google::Cloud::Spanner::Database::Job + + if job.database + job.database.must_be_kind_of Google::Cloud::Spanner::Database + end + end + + # Filter completed jobs + filter = "done:true" + jobs = instance.database_operations(filter: filter).all.to_a + jobs.wont_be :empty? + jobs.each do |job| + job.must_be :done? + end + + # Filter by metdata type + filter = "metadata.@type:CreateDatabaseMetadata" + jobs = instance.database_operations(filter: filter).all.to_a + jobs.wont_be :empty? + jobs.each do |job| + job.grpc.metadata.must_be_kind_of Google::Spanner::Admin::Database::V1::CreateDatabaseMetadata + end + end +end diff --git a/google-cloud-spanner/acceptance/spanner/database_test.rb b/google-cloud-spanner/acceptance/spanner/database_test.rb index e7eebab9b817..d0d8c98dfd7f 100644 --- a/google-cloud-spanner/acceptance/spanner/database_test.rb +++ b/google-cloud-spanner/acceptance/spanner/database_test.rb @@ -44,8 +44,7 @@ job2.wait_until_done! job2.must_be :done? - job2.database.wont_be :nil? - job2.database.must_be_kind_of Google::Cloud::Spanner::Database + job2.database.must_be :nil? database.drop spanner.database(instance_id, database_id).must_be :nil? diff --git a/google-cloud-spanner/acceptance/spanner_helper.rb b/google-cloud-spanner/acceptance/spanner_helper.rb index 3c85696a15c6..e363d1c15fdc 100644 --- a/google-cloud-spanner/acceptance/spanner_helper.rb +++ b/google-cloud-spanner/acceptance/spanner_helper.rb @@ -18,6 +18,7 @@ require "minitest/autorun" require "minitest/focus" require "minitest/rg" + require "google/cloud/spanner" # define SecureRandom.int64 @@ -256,9 +257,9 @@ def default_item_rows fixture = Object.new fixture.extend Acceptance::SpannerTest::Fixtures -instance = $spanner.instance "google-cloud-ruby-tests" +instance = $spanner.instance $spanner_instance_id instance ||= begin - inst_job = $spanner.create_instance "google-cloud-ruby-tests", name: "google-cloud-ruby-tests", config: "regional-us-central1", nodes: 1 + inst_job = $spanner.create_instance $spanner_instance_id, name: "google-cloud-ruby-tests", config: "regional-us-central1", nodes: 1 inst_job.wait_until_done! fail GRPC::BadStatus.new(inst_job.error.code, inst_job.error.message) if inst_job.error? inst_job.instance @@ -274,8 +275,21 @@ def default_item_rows def clean_up_spanner_objects puts "Cleaning up instances and databases after spanner tests." $spanner.instance($spanner_instance_id).database($spanner_database_id).drop + puts "Closing the Spanner Client." $spanner_client.close + + puts "Cleaning up instances databases and backups after spanner tests." + instance = $spanner.instance($spanner_instance_id) + + # Delete test database backups. + instance.backups(filter: "name:#{$spanner_database_id}").all.each do |backup| + backup.delete + end + + # Delete test restored database. + restored_db = instance.database("restore-#{$spanner_database_id}") + restored_db.drop if restored_db rescue => e puts "Error while cleaning up instances and databases after spanner tests.\n\n#{e}" end diff --git a/google-cloud-spanner/lib/google/cloud/spanner/admin/database/v1/database_admin_client_config.json b/google-cloud-spanner/lib/google/cloud/spanner/admin/database/v1/database_admin_client_config.json index c04eb0d042d0..662c1b4d8c96 100644 --- a/google-cloud-spanner/lib/google/cloud/spanner/admin/database/v1/database_admin_client_config.json +++ b/google-cloud-spanner/lib/google/cloud/spanner/admin/database/v1/database_admin_client_config.json @@ -66,7 +66,7 @@ "retry_params_name": "default" }, "CreateBackup": { - "timeout_millis": 30000, + "timeout_millis": 3600000, "retry_codes_name": "non_idempotent", "retry_params_name": "default" }, @@ -76,12 +76,12 @@ "retry_params_name": "default" }, "UpdateBackup": { - "timeout_millis": 30000, + "timeout_millis": 3600000, "retry_codes_name": "non_idempotent", "retry_params_name": "default" }, "DeleteBackup": { - "timeout_millis": 30000, + "timeout_millis": 3600000, "retry_codes_name": "idempotent", "retry_params_name": "default" }, @@ -91,7 +91,7 @@ "retry_params_name": "default" }, "RestoreDatabase": { - "timeout_millis": 30000, + "timeout_millis": 3600000, "retry_codes_name": "non_idempotent", "retry_params_name": "default" }, diff --git a/google-cloud-spanner/lib/google/cloud/spanner/backup.rb b/google-cloud-spanner/lib/google/cloud/spanner/backup.rb new file mode 100644 index 000000000000..46035777bca3 --- /dev/null +++ b/google-cloud-spanner/lib/google/cloud/spanner/backup.rb @@ -0,0 +1,315 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +require "google/cloud/spanner/backup/job" +require "google/cloud/spanner/backup/list" +require "google/cloud/spanner/backup/restore/job" + +module Google + module Cloud + module Spanner + ## + # # Backup + # + # A backup is a representation of Cloud Spanner database backup. + # + # See {Google::Cloud::Spanner::Instance#backups}, + # {Google::Cloud::Spanner::Instance#backup}, and + # {Google::Cloud::Spanner::Database#create_backup}. + # + # @example + # require "google/cloud" + # + # spanner = Google::Cloud::Spanner.new + # database = spanner.database "my-instance", "my-database" + # + # expire_time = Time.now + 36000 + # job = database.create_backup "my-backup", expire_time + # + # job.done? #=> false + # job.reload! # API call + # job.done? #=> true + # + # if job.error? + # status = job.error + # else + # backup = job.backup + # end + # + class Backup + ## + # @private The gRPC Service object. + attr_accessor :service + + ## + # @private Creates a new Backup instance. + def initialize grpc, service + @grpc = grpc + @service = service + end + + ## + # The unique identifier for the project. + # @return [String] + def project_id + @grpc.name.split("/")[1] + end + + ## + # The unique identifier for the instance. + # @return [String] + def instance_id + @grpc.name.split("/")[3] + end + + ## + # The unique identifier for the backup. + # @return [String] + def backup_id + @grpc.name.split("/")[5] + end + + ## + # Name of the database from which this backup was created. + # @return [String] + def database_id + @grpc.database.split("/")[5] + end + + ## + # The full path for the backup. Values are of the form + # `projects//instances//backups/`. + # @return [String] + def path + @grpc.name + end + + ## + # The current backup state. Possible values are `:CREATING` and + # `:READY`. + # @return [Symbol] + def state + @grpc.state + end + + ## + # The backup is still being created. A backup is not yet available + # for the database restore operation. + # @return [Boolean] + def creating? + state == :CREATING + end + + ## + # The backup is created and can be used to restore a database. + # @return [Boolean] + def ready? + state == :READY + end + + ## + # The expiration time of the backup, with microseconds granularity. + # @return [Time] + def expire_time + Convert.timestamp_to_time @grpc.expire_time + end + + ## + # Update backup expiration time. + # + # Set expiration time of the backup, with microseconds granularity + # that must be at least 6 hours and at most 366 days from the time the + # request is received. Once the `expire_time` has passed, Cloud Spanner + # will delete the backup and free the resources used by the backup. + # + # @param [Time] time Backup expiration time. + # @raise [Google::Cloud::Error] if expire time is in past or update + # call is aborted. + # + # @example + # require "google/cloud/spanner" + # + # spanner = Google::Cloud::Spanner.new + # + # instance = spanner.instance "my-instance" + # backup = instance.backup "my-backup" + # backup.expire_time = Time.now + 36000 + # puts backup.expire_time + # + def expire_time= time + ensure_service! + + expire_time_was = @grpc.expire_time + @grpc.expire_time = Convert.time_to_timestamp time + update_mask = Google::Protobuf::FieldMask.new paths: ["expire_time"] + @grpc = service.update_backup @grpc, update_mask + rescue Google::Cloud::Error => error + @grpc.expire_time = expire_time_was + raise error + end + + ## + # Create time is approximately the time when the backup request was + # received. + # @return [Time] + def create_time + Convert.timestamp_to_time @grpc.create_time + end + + ## + # Size of the backup in bytes. + # @return [Integer] + def size_in_bytes + @grpc.size_bytes + end + + ## + # The instances of the restored databases that reference the backup. + # Referencing databases may exist in different instances. + # The existence of any referencing database prevents the backup from + # being deleted. When a restored database from the backup enters the + # `READY` state, the reference to the backup is removed. + # + # @return [Array] Returns list of + # referencing database instances. + # + # @example + # spanner = Google::Cloud::Spanner.new + # + # instance = spanner.instance "my-instance" + # backup = instance.backup "my-backup" + # + # backup.referencing_databases.each do |database| + # puts database.database_id + # end + # + def referencing_databases + ensure_service! + + @grpc.referencing_databases.map do |referencing_database| + segments = referencing_database.split "/" + database_grpc = service.get_database segments[3], segments[5] + Database.from_grpc database_grpc, service + end + end + + ## + # Permanently deletes the backup. + # + # @return [Boolean] Returns `true` if the backup was deleted. + # + # @example + # require "google/cloud/spanner" + # + # spanner = Google::Cloud::Spanner.new + # + # instance = spanner.instance "my-instance" + # backup = instance.backup "my-backup" + # backup.delete # true + # + def delete + ensure_service! + service.delete_backup instance_id, backup_id + true + end + + ## + # Restores deleted database from the backup. + # + # @param [String] database_id The unique identifier for the database, + # which cannot be changed after the database is created. Values are of + # the form `[a-z][a-z0-9_\-]*[a-z0-9]` and must be between 2 and 30 + # characters in length. Required. + # @param [String] instance_id The name of the instance in which to + # create the restored database. This instance must be in the same + # project and have the same instance configuration as the instance + # containing the source backup. Optional. Default value is same as a + # backup instance. + # @return [Database] Restored database. + # + # @example + # require "google/cloud/spanner" + # + # spanner = Google::Cloud::Spanner.new + # + # instance = spanner.instance "my-instance" + # backup = instance.backup "my-backup" + # job = backup.restore "my-restored-database" + # + # job.done? #=> false + # job.reload! # API call + # job.done? #=> true + # + # if job.error? + # status = job.error + # else + # database = job.database + # end + # + # @example Restore database in provided instance id + # require "google/cloud/spanner" + # + # spanner = Google::Cloud::Spanner.new + # + # instance = spanner.instance "my-instance" + # backup = instance.backup "my-backup" + # job = backup.restore( + # "my-restored-database", + # instance_id: "other-instance" + # ) + # + # job.done? #=> false + # job.reload! # API call + # job.done? #=> true + # + # if job.error? + # status = job.error + # else + # database = job.database + # end + # + def restore database_id, instance_id: nil + ensure_service! + + instance_id ||= self.instance_id + + grpc = service.restore_database \ + self.instance_id, + backup_id, + instance_id, + database_id + Restore::Job.from_grpc grpc, service + end + + ## + # @private + # Creates a new Backup instance from a + # {Google::Spanner::Admin::Database::V1::Backup}. + def self.from_grpc grpc, service + new grpc, service + end + + protected + + ## + # @private Raise an error unless an active connection to the service is + # available. + def ensure_service! + raise "Must have active connection to service" unless service + end + end + end + end +end diff --git a/google-cloud-spanner/lib/google/cloud/spanner/backup/job.rb b/google-cloud-spanner/lib/google/cloud/spanner/backup/job.rb new file mode 100644 index 000000000000..51ab08f8a95e --- /dev/null +++ b/google-cloud-spanner/lib/google/cloud/spanner/backup/job.rb @@ -0,0 +1,274 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +require "google/cloud/spanner/status" +require "google/cloud/spanner/backup/job/list" + +module Google + module Cloud + module Spanner + class Backup + ## + # # Job + # + # A resource representing the long-running, asynchronous processing of + # backup creation. The job can be refreshed to retrieve the backup + # object once the operation has been completed. + # + # See {Google::Cloud::Spanner::Database#create_backup} + # + # @see https://cloud.google.com/spanner/reference/rpc/google.longrunning#google.longrunning.Operation + # Long-running Operation + # + # @example + # require "google/cloud/spanner" + # + # spanner = Google::Cloud::Spanner.new + # + # database = spanner.database "my-instance", "my-database" + # expire_time = Time.now + 36000 + # job = database.create_backup "my-backup", expire_time: expire_time + # + # job.done? #=> false + # job.reload! # API call + # job.done? #=> true + # + # if job.error? + # status = job.error + # else + # backup = job.backup + # end + # + class Job + ## + # @private The Google::Gax::Operation gRPC object. + attr_accessor :grpc + + ## + # @private The gRPC Service object. + attr_accessor :service + + ## + # @private Creates a new Backup::Job instance. + def initialize + @grpc = nil + @service = nil + end + + ## + # The backup is the object of the operation. + # + # @return [Backup, nil] The backup, or + # `nil` if the operation is not complete. + # + # @example + # require "google/cloud/spanner" + # + # spanner = Google::Cloud::Spanner.new + # + # database = spanner.database "my-instance", "my-database" + # expire_time = Time.now + 36000 + # job = database.create_backup "my-backup", expire_time: expire_time + # + # job.done? #=> false + # job.reload! + # job.done? #=> true + # backup = job.backup + # + def backup + return nil unless done? + return nil unless @grpc.grpc_op.result == :response + Backup.from_grpc @grpc.results, service + end + + ## + # Checks if the processing of the backup operation is complete. + # + # @return [boolean] `true` when complete, `false` otherwise. + # + # @example + # require "google/cloud/spanner" + # + # spanner = Google::Cloud::Spanner.new + # + # database = spanner.database "my-instance", "my-database" + # expire_time = Time.now + 36000 + # job = database.create_backup "my-backup", expire_time: expire_time + # + # job.done? #=> false + # + def done? + @grpc.done? + end + + ## + # Checks if the processing of the backup operation has errored. + # + # @return [boolean] `true` when errored, `false` otherwise. + # + # @example + # require "google/cloud/spanner" + # + # spanner = Google::Cloud::Spanner.new + # + # database = spanner.database "my-instance", "my-database" + # expire_time = Time.now + 36000 + # job = database.create_backup "my-backup", expire_time: expire_time + # + # job.error? #=> false + # + def error? + @grpc.error? + end + + ## + # The status if the operation associated with this job produced an + # error. + # + # @return [Google::Cloud::Spanner::Status, nil] A status object with + # the status code and message, or `nil` if no error occurred. + # + # @example + # require "google/cloud/spanner" + # + # spanner = Google::Cloud::Spanner.new + # + # database = spanner.database "my-instance", "my-database" + # expire_time = Time.now + 36000 + # job = database.create_backup "my-backup", expire_time: expire_time + # + # job.error? # true + # + # error = job.error + # + def error + return nil unless error? + Google::Cloud::Spanner::Status.from_grpc @grpc.error + end + + ## + # Reloads the job with current data from the long-running, + # asynchronous processing of a backup operation. + # + # @return [Google::Cloud::Spanner::Backup::Job] The same job + # instance. + # + # @example + # require "google/cloud/spanner" + # + # spanner = Google::Cloud::Spanner.new + # + # database = spanner.database "my-instance", "my-database" + # expire_time = Time.now + 36000 + # job = database.create_backup "my-backup", expire_time: expire_time + # + # job.done? #=> false + # job.reload! # API call + # job.done? #=> true + # + def reload! + @grpc.reload! + self + end + alias refresh! reload! + + ## + # Reloads the job until the operation is complete. The delay between + # reloads will incrementally increase. + # + # @example + # require "google/cloud/spanner" + # + # spanner = Google::Cloud::Spanner.new + # + # database = spanner.database "my-instance", "my-database" + # expire_time = Time.now + 36000 + # job = database.create_backup "my-backup", expire_time: expire_time + # + # job.done? #=> false + # job.wait_until_done! + # job.done? #=> true + # + def wait_until_done! + @grpc.wait_until_done! + end + + ## + # Cancel the backup job. + # + # @example + # require "google/cloud/spanner" + # + # spanner = Google::Cloud::Spanner.new + # + # database = spanner.database "my-instance", "my-database" + # expire_time = Time.now + 36000 + # job = database.create_backup "my-backup", expire_time: expire_time + # + # job.done? #=> false + # job.cancel + # + def cancel + @grpc.cancel + end + + ## + # The operation progress in percentage. + # + # @return [Integer] + def progress_percent + @grpc.metadata.progress.progress_percent + end + + ## + # The operation start time. + # + # @return [Time, nil] + def start_time + return nil unless @grpc.metadata.progress.start_time + Convert.timestamp_to_time @grpc.metadata.progress.start_time + end + + ## + # The operation end time. + # + # @return [Time, nil] + def end_time + return nil unless @grpc.metadata.progress.end_time + Convert.timestamp_to_time @grpc.metadata.progress.end_time + end + + ## + # The operation canceled time. + # + # @return [Time, nil] + def cancel_time + return nil unless @grpc.metadata.cancel_time + Convert.timestamp_to_time @grpc.metadata.cancel_time + end + + ## + # @private New Backup::Job from a Google::Gax::Operation object. + def self.from_grpc grpc, service + new.tap do |job| + job.instance_variable_set :@grpc, grpc + job.instance_variable_set :@service, service + end + end + end + end + end + end +end diff --git a/google-cloud-spanner/lib/google/cloud/spanner/backup/job/list.rb b/google-cloud-spanner/lib/google/cloud/spanner/backup/job/list.rb new file mode 100644 index 000000000000..f9ac020f9eef --- /dev/null +++ b/google-cloud-spanner/lib/google/cloud/spanner/backup/job/list.rb @@ -0,0 +1,177 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +require "delegate" + +module Google + module Cloud + module Spanner + class Backup + class Job + ## + # # List + # + # List is a special case Array with additional values for backup + # operations. + # + class List < DelegateClass(::Array) + # @private + # The gRPC Service object. + attr_accessor :service + + # @private + # The gRPC page enumerable object. + attr_accessor :grpc + + ## + # @private Create a new Backup::Job::List with an array of + # Google::Lognrunning::Operation instances. + def initialize arr = [] + super arr + end + + ## + # Whether there is a next page of backup jobs. + # + # @return [Boolean] + # + # @example + # require "google/cloud/spanner" + # + # spanner = Google::Cloud::Spanner.new + # + # instance = spanner.instance "my-instance" + # + # jobs = instance.backup_operations + # if jobs.next? + # next_jobs = jobs.next + # end + # + def next? + grpc.next_page? + end + + ## + # Retrieve the next page of backup jobs. + # + # @return [Backup::Job::List] + # + # @example + # require "google/cloud/spanner" + # + # spanner = Google::Cloud::Spanner.new + # + # instance = spanner.instance "my-instance" + # + # jobs = instance.backup_operations + # if jobs.next? + # next_jobs = jobs.next + # end + # + def next + ensure_service! + + return nil unless next? + grpc.next_page + self.class.from_grpc grpc, service + end + + ## + # Retrieves remaining results by repeatedly invoking {#next} until + # {#next?} returns `false`. Calls the given block once for each + # result, which is passed as the argument to the block. + # + # An Enumerator is returned if no block is given. + # + # This method will make repeated API calls until all remaining + # results are retrieved. (Unlike `#each`, for example, which merely + # iterates over the results returned by a single API call.) Use with + # caution. + # + # @yield [job] The block for accessing each backup job. + # @yieldparam [Google::Cloud::Spanner::Backup::Job] job The backup + # job object. + # + # @return [Enumerator] + # + # @example Iterating each backup job by passing a block: + # require "google/cloud/spanner" + # + # spanner = Google::Cloud::Spanner.new + # + # instance = spanner.instance "my-instance" + # + # jobs = instance.backup_operations + # jobs.all do |job| + # puts job.backup.backup_id + # end + # + # @example Using the enumerator by not passing a block: + # require "google/cloud/spanner" + # + # spanner = Google::Cloud::Spanner.new + # + # instance = spanner.instance "my-instance" + # + # jobs = instance.backup_operations + # all_backup_ids = jobs.all.map do |job| + # job.backup.backup_id + # end + # + def all + return enum_for :all unless block_given? + + results = self + loop do + results.each { |r| yield r } + break unless next? + grpc.next_page + results = self.class.from_grpc grpc, service + end + end + + ## + # @private + # + # New Backup::Job::List from a + # Google::Gax::PagedEnumerable + # object. Operation object is a backup operation. + # + def self.from_grpc grpc, service + operations_client = \ + service.databases.instance_variable_get "@operations_client" + jobs = new(Array(grpc.response.operations).map do |job_grpc| + Job.from_grpc \ + Google::Gax::Operation.new(job_grpc, operations_client), + service + end) + jobs.grpc = grpc + jobs.service = service + jobs + end + + protected + + ## + # Raise an error unless an active service is available. + def ensure_service! + raise "Must have active connection" unless @service + end + end + end + end + end + end +end diff --git a/google-cloud-spanner/lib/google/cloud/spanner/backup/list.rb b/google-cloud-spanner/lib/google/cloud/spanner/backup/list.rb new file mode 100644 index 000000000000..bb34126020b5 --- /dev/null +++ b/google-cloud-spanner/lib/google/cloud/spanner/backup/list.rb @@ -0,0 +1,170 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +require "delegate" + +module Google + module Cloud + module Spanner + class Backup + ## + # # List + # + # Google::Cloud::Spanner::Backup::List is a special case Array with + # additional values. + # + class List < DelegateClass(::Array) + # @private + # The gRPC Service object. + attr_accessor :service + + # @private + # The gRPC page enumerable object. + attr_accessor :grpc + + ## + # @private Create a new Backup::List with an array of + # Backup instances. + def initialize arr = [] + super arr + end + + ## + # Whether there is a next page of backups. + # + # @return [Boolean] + # + # @example + # require "google/cloud/spanner" + # + # spanner = Google::Cloud::Spanner.new + # + # instance = spanner.instance "my-instance" + # backups = instance.backups + # + # if backups.next? + # next_backups = backups.next + # end + # + def next? + grpc.next_page? + end + + ## + # Retrieve the next page of backups. + # + # @return [Google::Cloud::Spanner::Backup::List] + # + # @example + # require "google/cloud/spanner" + # + # spanner = Google::Cloud::Spanner.new + # + # instance = spanner.instance "my-instance" + # backups = instance.backups + + # if backups.next? + # next_backups = backups.next + # end + # + def next + ensure_service! + + return nil unless next? + grpc.next_page + self.class.from_grpc grpc, service + end + + ## + # Retrieves remaining results by repeatedly invoking {#next} until + # {#next?} returns `false`. Calls the given block once for each + # result, which is passed as the argument to the block. + # + # An Enumerator is returned if no block is given. + # + # This method will make repeated API calls until all remaining results + # are retrieved. (Unlike `#each`, for example, which merely iterates + # over the results returned by a single API call.) Use with caution. + # + # @yield [backup] The block for accessing each backup. + # @yieldparam [Google::Cloud::Spanner::Backup] backup The backup + # object. + # + # @return [Enumerator] + # + # @example Iterating each backup by passing a block: + # require "google/cloud/spanner" + # + # spanner = Google::Cloud::Spanner.new + # + # instance = spanner.instance "my-instance" + # backups = instance.backups + # + # backups.all do |backup| + # puts backup.backup_id + # end + # + # @example Using the enumerator by not passing a block: + # require "google/cloud/spanner" + # + # spanner = Google::Cloud::Spanner.new + # + # instance = spanner.instance "my-instance" + # backups = instance.backups + # + # all_backup_ids = backups.all.map do |backup| + # backup.backup_id + # end + # + def all + return enum_for :all unless block_given? + + results = self + loop do + results.each { |r| yield r } + break unless next? + grpc.next_page + results = self.class.from_grpc grpc, service + end + end + + ## + # @private + # New Backup::List from a + # Google::Gax::PagedEnumerable + # object. + def self.from_grpc grpc, service + backups = List.new(Array(grpc.response.backups).map do |backup| + Backup.from_grpc backup, service + end) + + backups.grpc = grpc + backups.service = service + backups + end + + protected + + ## + # Raise an error unless an active service is available. + def ensure_service! + raise "Must have active connection" unless @service + end + end + end + end + end +end diff --git a/google-cloud-spanner/lib/google/cloud/spanner/backup/restore/job.rb b/google-cloud-spanner/lib/google/cloud/spanner/backup/restore/job.rb new file mode 100644 index 000000000000..e9a8a3854506 --- /dev/null +++ b/google-cloud-spanner/lib/google/cloud/spanner/backup/restore/job.rb @@ -0,0 +1,246 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +require "google/cloud/spanner/status" + +module Google + module Cloud + module Spanner + class Backup + class Restore + ## + # # Job + # + # A resource representing the long-running, asynchronous processing of + # a backup restore. The job can be refreshed to retrieve the restored + # database object once the operation has been completed. + # + # See {Backup#restore} + # + # @see https://cloud.google.com/spanner/reference/rpc/google.longrunning#google.longrunning.Operation + # Long-running Operation + # + # @example + # require "google/cloud/spanner" + # + # spanner = Google::Cloud::Spanner.new + # + # instance = spanner.instance "my-instance" + # backup = instance.backup "my-backup" + # job = backup.restore "my-restored-database" + # + # job.done? #=> false + # job.reload! # API call + # job.done? #=> true + # + # if job.error? + # status = job.error + # else + # database = job.database + # end + # + class Job + ## + # @private The Google::Gax::Operation gRPC object. + attr_accessor :grpc + + ## + # @private The gRPC Service object. + attr_accessor :service + + ## + # @private Creates a new Restore::Job instance. + def initialize + @grpc = nil + @service = nil + end + + ## + # The database is the object of the operation. + # + # @return [Database, nil] The database instance, or + # `nil` if the operation is not complete. + # + # @example + # require "google/cloud/spanner" + # + # spanner = Google::Cloud::Spanner.new + # + # instance = spanner.instance "my-instance" + # backup = instance.backup "my-backup" + # job = backup.restore "my-restored-database" + # + # job.done? #=> false + # job.reload! + # job.done? #=> true + # database = job.database + # + def database + return nil unless done? + return nil unless @grpc.grpc_op.result == :response + Database.from_grpc @grpc.results, service + end + + ## + # Checks if the processing of the restore operation is complete. + # + # @return [boolean] `true` when complete, `false` otherwise. + # + # @example + # require "google/cloud/spanner" + # + # spanner = Google::Cloud::Spanner.new + # + # instance = spanner.instance "my-instance" + # backup = instance.backup "my-backup" + # job = backup.restore "my-restored-database" + # + # job.done? #=> false + # + def done? + @grpc.done? + end + + ## + # Checks if the processing of the restore operation has errored. + # + # @return [boolean] `true` when errored, `false` otherwise. + # + # @example + # require "google/cloud/spanner" + # + # spanner = Google::Cloud::Spanner.new + # + # instance = spanner.instance "my-instance" + # backup = instance.backup "my-backup" + # job = backup.restore "my-restored-database" + # + # job.error? #=> false + # + def error? + @grpc.error? + end + + ## + # The status if the operation associated with this job produced an + # error. + # + # @return [Google::Cloud::Spanner::Status, nil] A status object with + # the status code and message, or `nil` if no error occurred. + # + # @example + # require "google/cloud/spanner" + # + # spanner = Google::Cloud::Spanner.new + # + # instance = spanner.instance "my-instance" + # backup = instance.backup "my-backup" + # job = backup.restore "my-restored-database" + # + # job.error? # true + # + # error = job.error + # + def error + return nil unless error? + Google::Cloud::Spanner::Status.from_grpc @grpc.error + end + + ## + # Reloads the job with current data from the long-running, + # asynchronous processing of a restore operation. + # + # @return [Backup::Job] The same job instance. + # + # @example + # require "google/cloud/spanner" + # + # spanner = Google::Cloud::Spanner.new + # + # instance = spanner.instance "my-instance" + # backup = instance.backup "my-backup" + # job = backup.restore "my-restored-database" + # + # job.done? #=> false + # job.reload! # API call + # job.done? #=> true + # + def reload! + @grpc.reload! + self + end + alias refresh! reload! + + ## + # Reloads the job until the operation is complete. The delay between + # reloads will incrementally increase. + # + # @example + # require "google/cloud/spanner" + # + # spanner = Google::Cloud::Spanner.new + # + # instance = spanner.instance "my-instance" + # backup = instance.backup "my-backup" + # job = backup.restore "my-restored-database" + # + # job.done? #=> false + # job.wait_until_done! + # job.done? #=> true + # + def wait_until_done! + @grpc.wait_until_done! + end + + ## + # The operation progress in percentage. + # + # @return [Integer] + def progress_percent + @grpc.metadata.progress.progress_percent + end + + ## + # The operation start time. + # + # @return [Time, nil] + def start_time + return nil unless @grpc.metadata.progress.start_time + Convert.timestamp_to_time @grpc.metadata.progress.start_time + end + + ## + # The operation end time. + # + # @return [Time, nil] + def end_time + return nil unless @grpc.metadata.progress.end_time + Convert.timestamp_to_time @grpc.metadata.progress.end_time + end + + ## + # @private New Restore::Job from a Google::Gax::Operation object. + def self.from_grpc grpc, service + new.tap do |job| + job.instance_variable_set :@grpc, grpc + job.instance_variable_set :@service, service + end + end + end + end + end + end + end +end diff --git a/google-cloud-spanner/lib/google/cloud/spanner/database.rb b/google-cloud-spanner/lib/google/cloud/spanner/database.rb index 9ccae67c4478..ef155f5eb189 100644 --- a/google-cloud-spanner/lib/google/cloud/spanner/database.rb +++ b/google-cloud-spanner/lib/google/cloud/spanner/database.rb @@ -15,6 +15,8 @@ require "google/cloud/spanner/database/job" require "google/cloud/spanner/database/list" +require "google/cloud/spanner/database/restore_info" +require "google/cloud/spanner/backup" require "google/cloud/spanner/policy" module Google @@ -114,6 +116,13 @@ def ready? state == :READY end + ## + # The database is fully created from backup and optimizing. + # @return [Boolean] + def ready_optimizing? + state == :READY_OPTIMIZING + end + ## # Retrieve the Data Definition Language (DDL) statements that define # database structures. DDL statements are used to create, update, @@ -220,6 +229,257 @@ def drop true end + + # @private + DATBASE_OPERATION_METADAT_FILTER_TEMPLATE = [ + "(metadata.@type:CreateDatabaseMetadata AND " \ + "metadata.database:%s)", + "(metadata.@type:RestoreDatabaseMetadata AND "\ + "metadata.name:%s)", + "(metadata.@type:UpdateDatabaseDdl AND "\ + "metadata.database:%s)" + ].join(" OR ") + + ## + # Retrieves the list of database operations for the given database. + # + # @param filter [String] + # A filter expression that filters what operations are returned in the + # response. + # + # The response returns a list of + # {Google::Longrunning::Operation long-running operations} whose names + # are prefixed by a database name within the specified instance. + # The long-running operation + # {Google::Longrunning::Operation#metadata metadata} field type + # `metadata.type_url` describes the type of the metadata. + # + # The filter expression must specify the field name, + # a comparison operator, and the value that you want to use for + # filtering. The value must be a string, a number, or a boolean. + # The comparison operator must be + # <, >, <=, >=, !=, =, or :. Colon ':' represents a HAS operator + # which is roughly synonymous with equality. Filter rules are case + # insensitive. + # + # The long-running operation fields eligible for filtering are: + # * `name` --> The name of the long-running operation + # * `done` --> False if the operation is in progress, else true. + # * `metadata.type_url` (using filter string `metadata.@type`) and + # fields in `metadata.value` (using filter string + # `metadata.`, where is a field in + # metadata.value) are eligible for filtering. + # * `error` --> Error associated with the long-running operation. + # * `response.type_url` (using filter string `response.@type`) and + # fields in `response.value` (using filter string + # `response.`, where is a field in + # response.value)are eligible for filtering. + # + # To filter on multiple expressions, provide each separate + # expression within parentheses. By default, each expression + # is an AND expression. However, you can include AND, OR, and NOT + # expressions explicitly. + # + # Some examples of using filters are: + # + # * `done:true` --> The operation is complete. + # * `(metadata.@type:type.googleapis.com/google.spanner.admin.\ + # database.v1.RestoreDatabaseMetadata) + # AND (metadata.source_type:BACKUP) + # AND (metadata.backup_info.backup:backup_howl) + # AND (metadata.name:restored_howl) + # AND (metadata.progress.start_time < \"2018-03-28T14:50:00Z\") + # AND (error:*)` + # --> Return RestoreDatabase operations from backups whose name + # contains "backup_howl", where the created database name + # contains the string "restored_howl", the start_time of the + # restore operation is before 2018-03-28T14:50:00Z, + # and the operation returned an error. + # @param page_size [Integer] + # The maximum number of resources contained in the underlying API + # response. If page streaming is performed per-resource, this + # parameter does not affect the return value. If page streaming is + # performed per-page, this determines the maximum number of + # resources in a page. + # + # @return [Array] List + # representing the long-running, asynchronous processing + # of a database operations. + # (See {Google::Cloud::Spanner::Database::Job::List}) + # + # @example + # require "google/cloud/spanner" + # + # spanner = Google::Cloud::Spanner.new + # + # database = spanner.database "my-instance", "my-database" + # + # jobs = database.database_operations + # jobs.each do |job| + # if job.error? + # p job.error + # else + # p job.database.database_id + # end + # end + # + # @example Retrieve all + # require "google/cloud/spanner" + # + # spanner = Google::Cloud::Spanner.new + # + # database = spanner.database "my-instance", "my-database" + # + # jobs = database.database_operations + # jobs.all do |job| + # if job.error? + # p job.error + # else + # puts job.database.database_id + # end + # end + # + # @example List by page size + # require "google/cloud/spanner" + # + # spanner = Google::Cloud::Spanner.new + # + # database = spanner.database "my-instance", "my-database" + # + # jobs = database.database_operations page_size: 10 + # jobs.each do |job| + # if job.error? + # p job.error + # else + # puts job.database.database_id + # end + # end + # + # @example Filter and list + # require "google/cloud/spanner" + # + # spanner = Google::Cloud::Spanner.new + # + # database = spanner.database "my-instance", "my-database" + # + # jobs = database.database_operations filter: "done:true" + # jobs.each do |job| + # if job.error? + # p job.error + # else + # puts job.database.database_id + # end + # end + # + def database_operations filter: nil, page_size: nil + database_filter = format( + DATBASE_OPERATION_METADAT_FILTER_TEMPLATE, + database_id: database_id + ) + + if filter + database_filter = format( + "(%s) AND (%s)", + filter: filter, database_filter: database_filter + ) + end + + grpc = service.list_database_operations \ + instance_id, + filter: database_filter, + page_size: page_size + Database::Job::List.from_grpc grpc, service + end + + ## + # Creates a database backup. + # + # @param [String] backup_id The unique identifier for the backup. + # Values are of the form `[a-z][a-z0-9_\-]*[a-z0-9]` and must be + # between 2 and 60 characters in length. Required. + # @param [Time] expire_time The expiration time of the backup, with + # microseconds granularity that must be at least 6 hours and at most + # 366 days from the time the request is received. Required. + # Once the `expire_time` has passed, Cloud Spanner will delete the + # backup and free the resources used by the backup. Required. + # @return [Google::Cloud::Spanner::Backup::Job] The job representing + # the long-running, asynchronous processing of a backup create + # operation. + # + # @example Create backup with expiration time + # require "google/cloud/spanner" + # + # spanner = Google::Cloud::Spanner.new + # database = spanner.database "my-instance", "my-database" + # + # job = database.create_backup "my-backup", Time.now + 36000 + # + # job.done? #=> false + # job.reload! # API call + # job.done? #=> true + # + # if job.error? + # status = job.error + # else + # backup = job.backup + # end + # + def create_backup backup_id, expire_time + ensure_service! + grpc = service.create_backup \ + instance_id, + database_id, + backup_id, + expire_time + Backup::Job.from_grpc grpc, service + end + + ## + # Retrieves backups belonging to the database. + # + # @param [Integer] page_size Optional. Number of backups to be returned + # in the response. If 0 or less, defaults to the server's maximum + # allowed page size. + # @return [Array] Enumerable list of + # backups. (See {Google::Cloud::Spanner::Backup::List}) + # + # @example + # require "google/cloud/spanner" + # + # spanner = Google::Cloud::Spanner.new + # database = spanner.database "my-instance", "my-database" + # + # database.backups.all.each do |backup| + # puts backup.backup_id + # end + # + # @example List backups by page size + # require "google/cloud/spanner" + # + # spanner = Google::Cloud::Spanner.new + # database = spanner.database "my-instance", "my-database" + # + # database.backups(page_size: 5).all.each do |backup| + # puts backup.backup_id + # end + # + def backups page_size: nil + ensure_service! + grpc = service.list_backups \ + instance_id, + filter: "database:#{database_id}", + page_size: page_size + Backup::List.from_grpc grpc, service + end + + # Information about the source used to restore the database. + # + # @return [Google::Cloud::Spanner::Database::RestoreInfo, nil] + def restore_info + return nil unless @grpc.restore_info + RestoreInfo.from_grpc @grpc.restore_info + end + ## # Gets the [Cloud IAM](https://cloud.google.com/iam/) access control # policy for this database. diff --git a/google-cloud-spanner/lib/google/cloud/spanner/database/backup_info.rb b/google-cloud-spanner/lib/google/cloud/spanner/database/backup_info.rb new file mode 100644 index 000000000000..390f15d91afa --- /dev/null +++ b/google-cloud-spanner/lib/google/cloud/spanner/database/backup_info.rb @@ -0,0 +1,105 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +module Google + module Cloud + module Spanner + class Database + class BackupInfo + ## + # @private Creates a new Database::BackupInfo instance. + def initialize grpc + @grpc = grpc + end + + ## + # The unique identifier for the project. + # @return [String] + def project_id + @grpc.backup.split("/")[1] + end + + ## + # The unique identifier for the instance. + # @return [String] + def instance_id + @grpc.backup.split("/")[3] + end + + ## + # The unique identifier for the backup. + # @return [String] + def backup_id + @grpc.backup.split("/")[5] + end + + ## + # The full path for the backup. Values are of the form + # `projects//instances//backups/`. + # @return [String] + def path + @grpc.backup + end + + ## + # Name of the database the backup was created from. + # @return [String] + def source_database_id + @grpc.source_database.split("/")[5] + end + + ## + # The unique identifier for the source database project. + # @return [String] + def source_database_project_id + @grpc.backup.split("/")[1] + end + + ## + # The unique identifier for the source database instance. + # @return [String] + def source_database_instance_id + @grpc.backup.split("/")[3] + end + + ## + # The full path for the source database the backup was created from. + # Values are of the form + # `projects//instances//database/`. + # @return [String] + def source_database_path + @grpc.source_database + end + + ## + # The backup contains an externally consistent copy of + # `source_database` at the timestamp specified by `create_time`. + # received. + # @return [Time] + def create_time + Convert.timestamp_to_time @grpc.create_time + end + + ## + # @private Creates a new Database::BackupInfo instance from a + # Google::Spanner::Admin::Database::V1::BackupInfo. + def self.from_grpc grpc + new grpc + end + end + end + end + end +end diff --git a/google-cloud-spanner/lib/google/cloud/spanner/database/job.rb b/google-cloud-spanner/lib/google/cloud/spanner/database/job.rb index 5951e613e3cf..549f4e537d30 100644 --- a/google-cloud-spanner/lib/google/cloud/spanner/database/job.rb +++ b/google-cloud-spanner/lib/google/cloud/spanner/database/job.rb @@ -14,6 +14,7 @@ require "google/cloud/spanner/status" +require "google/cloud/spanner/database/job/list" module Google module Cloud @@ -87,6 +88,8 @@ def initialize def database return nil unless done? return nil unless @grpc.grpc_op.result == :response + return nil unless @grpc.results.instance_of? \ + Google::Spanner::Admin::Database::V1::Database Database.from_grpc @grpc.results, service end diff --git a/google-cloud-spanner/lib/google/cloud/spanner/database/job/list.rb b/google-cloud-spanner/lib/google/cloud/spanner/database/job/list.rb new file mode 100644 index 000000000000..07539d345a80 --- /dev/null +++ b/google-cloud-spanner/lib/google/cloud/spanner/database/job/list.rb @@ -0,0 +1,177 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +require "delegate" + +module Google + module Cloud + module Spanner + class Database + class Job + ## + # # List + # + # List is a special case Array with additional values for database + # operations. + # + class List < DelegateClass(::Array) + # @private + # The gRPC Service object. + attr_accessor :service + + # @private + # The gRPC page enumerable object. + attr_accessor :grpc + + ## + # @private Create a new Database::Job::List with an array of + # Google::Lognrunning::Operation instances. + def initialize arr = [] + super arr + end + + ## + # Whether there is a next page of database jobs. + # + # @return [Boolean] + # + # @example + # require "google/cloud/spanner" + # + # spanner = Google::Cloud::Spanner.new + # + # instance = spanner.instance "my-instance" + # + # jobs = instance.database_operations + # if jobs.next? + # next_jobs = jobs.next + # end + # + def next? + grpc.next_page? + end + + ## + # Retrieve the next page of database jobs. + # + # @return [Google::Cloud::Spanner::Database::Job::List] + # + # @example + # require "google/cloud/spanner" + # + # spanner = Google::Cloud::Spanner.new + # + # instance = spanner.instance "my-instance" + # + # jobs = instance.database_operations + # if jobs.next? + # next_jobs = jobs.next + # end + # + def next + ensure_service! + + return nil unless next? + grpc.next_page + self.class.from_grpc grpc, service + end + + ## + # Retrieves remaining results by repeatedly invoking {#next} until + # {#next?} returns `false`. Calls the given block once for each + # result, which is passed as the argument to the block. + # + # An Enumerator is returned if no block is given. + # + # This method will make repeated API calls until all remaining + # results are retrieved. (Unlike `#each`, for example, which merely + # iterates over the results returned by a single API call.) Use with + # caution. + # + # @yield [job] The block for accessing each database job. + # @yieldparam [Google::Cloud::Spanner::Database::Job] job The + # database job object. + # + # @return [Enumerator] + # + # @example Iterating each database job by passing a block: + # require "google/cloud/spanner" + # + # spanner = Google::Cloud::Spanner.new + # + # instance = spanner.instance "my-instance" + # + # jobs = instance.database_operations + # jobs.all do |job| + # puts job.database.database_id + # end + # + # @example Using the enumerator by not passing a block: + # require "google/cloud/spanner" + # + # spanner = Google::Cloud::Spanner.new + # + # instance = spanner.instance "my-instance" + # + # jobs = instance.database_operations + # all_database_ids = jobs.all.map do |job| + # job.database.database_id + # end + # + def all + return enum_for :all unless block_given? + + results = self + loop do + results.each { |r| yield r } + break unless next? + grpc.next_page + results = self.class.from_grpc grpc, service + end + end + + ## + # @private + # + # New Database::Job::List from a + # Google::Gax::PagedEnumerable + # object. Operation object is a database operation. + # + def self.from_grpc grpc, service + operations_client = \ + service.databases.instance_variable_get "@operations_client" + jobs = new(Array(grpc.response.operations).map do |job_grpc| + Job.from_grpc \ + Google::Gax::Operation.new(job_grpc, operations_client), + service + end) + jobs.grpc = grpc + jobs.service = service + jobs + end + + protected + + ## + # Raise an error unless an active service is available. + def ensure_service! + raise "Must have active connection" unless @service + end + end + end + end + end + end +end diff --git a/google-cloud-spanner/lib/google/cloud/spanner/database/restore_info.rb b/google-cloud-spanner/lib/google/cloud/spanner/database/restore_info.rb new file mode 100644 index 000000000000..197470ac79bd --- /dev/null +++ b/google-cloud-spanner/lib/google/cloud/spanner/database/restore_info.rb @@ -0,0 +1,63 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +require "google/cloud/spanner/database/backup_info" + +module Google + module Cloud + module Spanner + class Database + class RestoreInfo + ## + # @private Creates a new Database::RestoreInfo instance. + def initialize grpc + @grpc = grpc + end + + ## + # The database restored from source type `:BACKUP`. + # @return [Symbol] + def source_type + @grpc.source_type + end + + ## + # Database restored from backup. + # + # @return [Boolean] + def source_backup? + @grpc.source_type == :BACKUP + end + + # Information about the backup used to restore the database. + # The backup may no longer exist. + # + # @return [Google::Cloud::Spanner::Database::BackupInfo, nil] + def backup_info + return nil unless @grpc.backup_info + BackupInfo.from_grpc @grpc.backup_info + end + + ## + # @private Creates a new Database::RestoreInfo instance from a + # Google::Spanner::Admin::Database::V1::RestoreInfo. + def self.from_grpc grpc + new grpc + end + end + end + end + end +end diff --git a/google-cloud-spanner/lib/google/cloud/spanner/instance.rb b/google-cloud-spanner/lib/google/cloud/spanner/instance.rb index 551abc01537a..01e522774fba 100644 --- a/google-cloud-spanner/lib/google/cloud/spanner/instance.rb +++ b/google-cloud-spanner/lib/google/cloud/spanner/instance.rb @@ -334,6 +334,406 @@ def create_database database_id, statements: [] Database::Job.from_grpc grpc, service end + ## + # Retrieves the list of database operations for the given instance. + # + # @param filter [String] + # A filter expression that filters what operations are returned in the + # response. + # + # The response returns a list of + # {Google::Longrunning::Operation long-running operations} whose names + # are prefixed by a database name within the specified instance. + # The long-running operation + # {Google::Longrunning::Operation#metadata metadata} field type + # `metadata.type_url` describes the type of the metadata. + # + # The filter expression must specify the field name, + # a comparison operator, and the value that you want to use for + # filtering. The value must be a string, a number, or a boolean. + # The comparison operator must be + # <, >, <=, >=, !=, =, or :. Colon ':' represents a HAS operator + # which is roughly synonymous with equality. Filter rules are case + # insensitive. + # + # The long-running operation fields eligible for filtering are: + # * `name` --> The name of the long-running operation + # * `done` --> False if the operation is in progress, else true. + # * `metadata.type_url` (using filter string `metadata.@type`) and + # fields in `metadata.value` (using filter string + # `metadata.`, where is a field in + # metadata.value) are eligible for filtering. + # * `error` --> Error associated with the long-running operation. + # * `response.type_url` (using filter string `response.@type`) and + # fields in `response.value` (using filter string + # `response.`, where is a field in + # response.value)are eligible for filtering. + # + # To filter on multiple expressions, provide each separate + # expression within parentheses. By default, each expression + # is an AND expression. However, you can include AND, OR, and NOT + # expressions explicitly. + # + # Some examples of using filters are: + # + # * `done:true` --> The operation is complete. + # * `(metadata.@type:type.googleapis.com/google.spanner.admin.\ + # database.v1.RestoreDatabaseMetadata) + # AND (metadata.source_type:BACKUP) + # AND (metadata.backup_info.backup:backup_howl) + # AND (metadata.name:restored_howl) + # AND (metadata.progress.start_time < \"2018-03-28T14:50:00Z\") + # AND (error:*)` + # --> Return RestoreDatabase operations from backups whose name + # contains "backup_howl", where the created database name + # contains the string "restored_howl", the start_time of the + # restore operation is before 2018-03-28T14:50:00Z, + # and the operation returned an error. + # @param page_size [Integer] + # The maximum number of resources contained in the underlying API + # response. If page streaming is performed per-resource, this + # parameter does not affect the return value. If page streaming is + # performed per-page, this determines the maximum number of + # resources in a page. + # + # @return [Array] List + # representing the long-running, asynchronous processing + # of a database operations. + # (See {Google::Cloud::Spanner::Database::Job::List}) + # + # @example + # require "google/cloud/spanner" + # + # spanner = Google::Cloud::Spanner.new + # + # instance = spanner.instance "my-instance" + # + # jobs = instance.database_operations + # jobs.each do |job| + # if job.error? + # p job.error + # else + # p job.database.database_id + # end + # end + # + # @example Retrieve all + # require "google/cloud/spanner" + # + # spanner = Google::Cloud::Spanner.new + # + # instance = spanner.instance "my-instance" + # + # jobs = instance.database_operations + # jobs.all do |job| + # if job.error? + # p job.error + # else + # puts job.database.database_id + # end + # end + # + # @example List by page size + # require "google/cloud/spanner" + # + # spanner = Google::Cloud::Spanner.new + # instance = spanner.instance "my-instance" + # + # jobs = instance.database_operations page_size: 10 + # jobs.each do |job| + # if job.error? + # p job.error + # else + # puts job.database.database_id + # end + # end + # + # @example Filter and list + # require "google/cloud/spanner" + # + # spanner = Google::Cloud::Spanner.new + # + # instance = spanner.instance "my-instance" + # + # filter = "metadata.@type:CreateDatabaseMetadata" + # jobs = instance.database_operations filter: filter + # jobs.each do |job| + # if job.error? + # p job.error + # else + # puts job.database.database_id + # end + # end + # + def database_operations filter: nil, page_size: nil + grpc = service.list_database_operations \ + instance_id, + filter: filter, + page_size: page_size + Database::Job::List.from_grpc grpc, service + end + + ## + # Retrieves backups belonging to the instance. + # + # @param [String] filter Optional. A filter expression that filters + # backups listed in the response. The expression must specify the + # field name, a comparison operator, and the value that you want to + # use for filtering. The value must be a string, a number, or a + # boolean. The comparison operator must be + # <, >, <=, >=, !=, =, or :. Colon ':' represents a HAS operator + # which is roughly synonymous with equality. + # Filter rules are case insensitive. + # + # The fields eligible for filtering are: + # * `name` + # * `database` + # * `state` + # * `create_time`(and values are of the format YYYY-MM-DDTHH:MM:SSZ) + # * `expire_time`(and values are of the format YYYY-MM-DDTHH:MM:SSZ) + # * `size_bytes` + # + # To filter on multiple expressions, provide each separate expression + # within parentheses. By default, each expression is an AND + # expression. However, you can include AND, OR, and NOT expressions + # explicitly. + # + # Some examples of using filters are: + # + # * `name:Howl` --> The backup's name contains the string "howl". + # * `database:prod` + # --> The database's name contains the string "prod". + # * `state:CREATING` --> The backup is pending creation. + # * `state:READY` --> The backup is fully created and ready for use. + # * `(name:howl) AND (create_time < \"2018-03-28T14:50:00Z\")` + # --> The backup name contains the string "howl" and + # `create_time` of the backup is before + # 2018-03-28T14:50:00Z. + # * `expire_time < \"2018-03-28T14:50:00Z\"` + # --> The backup `expire_time` is before 2018-03-28T14:50:00Z. + # * `size_bytes > 10000000000` --> + # The backup's size is greater than 10GB + # @param [Integer] page_size Optional. Number of backups to be returned + # in the response. If 0 or less, defaults to the server's maximum + # allowed page size. + # @return [Array] Enumerable list of + # backups. (See {Google::Cloud::Spanner::Backup::List}) + # + # @example + # require "google/cloud/spanner" + # + # spanner = Google::Cloud::Spanner.new + # instance = spanner.instance "my-instance" + # + # instance.backups.all.each do |backup| + # puts backup.backup_id + # end + # + # @example List backups by page size + # require "google/cloud/spanner" + # + # spanner = Google::Cloud::Spanner.new + # instance = spanner.instance "my-instance" + # + # instance.backups(page_size: 5).all.each do |backup| + # puts backup.backup_id + # end + # + # @example Filter and list backups + # require "google/cloud/spanner" + # + # spanner = Google::Cloud::Spanner.new + # instance = spanner.instance "my-instance" + # + # # filter backups by name. + # instance.backups(filter: "name:my-backup").all.each do |backup| + # puts backup.backup_id + # end + # + # # filter backups by database name. + # instance.backups(filter: "database:prod-db").all.each do |backup| + # puts backup.backup_id + # end + # + def backups filter: nil, page_size: nil + ensure_service! + grpc = service.list_backups \ + instance_id, + filter: filter, + page_size: page_size + Backup::List.from_grpc grpc, service + end + + ## + # Retrieves a backup belonging to the instance by identifier. + # + # @param [String] backup_id The unique identifier for the backup. + # + # @return [Google::Cloud::Spanner::Backup, nil] Returns `nil` + # if database does not exist. + # + # @example + # require "google/cloud/spanner" + # + # spanner = Google::Cloud::Spanner.new + # instance = spanner.instance "my-instance" + # backup = instance.backup "my-backup" + # + # @example Will return `nil` if backup does not exist. + # require "google/cloud/spanner" + # + # spanner = Google::Cloud::Spanner.new + # instance = spanner.instance "my-instance" + # backup = instance.backup "non-existing-backup" # nil + # + def backup backup_id + ensure_service! + grpc = service.get_backup instance_id, backup_id + Backup.from_grpc grpc, service + rescue Google::Cloud::NotFoundError + nil + end + + ## + # Retrieves the list of database backup operations for the given + # instance. + # + # @param filter [String] + # A filter expression that filters what operations are returned in the + # response. + # + # The response returns a list of + # {Google::Longrunning::Operation long-running operations} whose names + # are prefixed by a backup name within the specified instance. + # The long-running operation + # {Google::Longrunning::Operation#metadata metadata} field type + # `metadata.type_url` describes the type of the metadata. + # + # The filter expression must specify the field name of an operation, a + # comparison operator, and the value that you want to use for + # filtering. + # The value must be a string, a number, or a boolean. The comparison + # operator must be + # <, >, <=, >=, !=, =, or :. Colon ':'' represents a HAS operator + # which is roughly synonymous with equality. Filter rules are case + # insensitive. + # + # The long-running operation fields eligible for filtering are: + # * `name` --> The name of the long-running operation + # * `done` --> False if the operation is in progress, else true. + # * `metadata.type_url` (using filter string `metadata.@type`) and + # fields in `metadata.value` (using filter string + # `metadata.`, where is a field in + # metadata.value) are eligible for filtering. + # * `error` --> Error associated with the long-running operation. + # * `response.type_url` (using filter string `response.@type`) and + # fields in `response.value` (using filter string + # `response.`, where is a field in + # response.value) are eligible for filtering. + # + # To filter on multiple expressions, provide each separate + # expression within parentheses. By default, each expression is an + # AND expression. However, you can include AND, OR, and NOT + # expressions explicitly. + # + # Some examples of using filters are: + # + # * `done:true` --> The operation is complete. + # * `metadata.database:prod` + # --> The database the backup was taken from has a name containing + # the string "prod". + # * `(metadata.@type:type.googleapis.com/google.spanner.admin.\ + # database.v1.CreateBackupMetadata) + # AND (metadata.name:howl) + # AND (metadata.progress.start_time < \"2018-03-28T14:50:00Z\") + # AND (error:*)` + # --> Return CreateBackup operations where the created backup name + # contains the string "howl", the progress.start_time of the + # backup operation is before 2018-03-28T14:50:00Z, and the + # operation returned an error. + # @param page_size [Integer] + # The maximum number of resources contained in the underlying API + # response. If page streaming is performed per-resource, this + # parameter does not affect the return value. If page streaming is + # performed per-page, this determines the maximum number of + # resources in a page. + # + # @return [Array] List representing + # the long-running, asynchronous processing of a backup operations. + # (See {Google::Cloud::Spanner::Backup::Job::List}) + # + # @example + # require "google/cloud/spanner" + # + # spanner = Google::Cloud::Spanner.new + # + # instance = spanner.instance "my-instance" + # + # jobs = instance.backup_operations + # jobs.each do |job| + # if job.error? + # p job.error + # else + # p job.backup.backup_id + # end + # end + # + # @example Retrieve all + # require "google/cloud/spanner" + # + # spanner = Google::Cloud::Spanner.new + # + # instance = spanner.instance "my-instance" + # + # jobs = instance.backup_operations + # jobs.all do |job| + # if job.error? + # p job.error + # else + # p job.backup.backup_id + # end + # end + # + # @example List by page size + # require "google/cloud/spanner" + # + # spanner = Google::Cloud::Spanner.new + # instance = spanner.instance "my-instance" + # + # jobs = instance.backup_operations page_size: 10 + # jobs.each do |job| + # if job.error? + # p job.error + # else + # puts job.backup.backup_id + # end + # end + # + # @example Filter and list + # require "google/cloud/spanner" + # + # spanner = Google::Cloud::Spanner.new + # + # instance = spanner.instance "my-instance" + # + # filter = "metadata.@type:CreateBackupMetadata" + # jobs = instance.backup_operations filter: filter + # jobs.each do |job| + # if job.error? + # p job.error + # else + # puts job.backup.backup_id + # end + # end + # + def backup_operations filter: nil, page_size: nil + grpc = service.list_backup_operations \ + instance_id, + filter: filter, + page_size: page_size + Backup::Job::List.from_grpc grpc, service + end + ## # Gets the [Cloud IAM](https://cloud.google.com/iam/) access control # policy for this instance. diff --git a/google-cloud-spanner/lib/google/cloud/spanner/service.rb b/google-cloud-spanner/lib/google/cloud/spanner/service.rb index e009c0351f34..8e454913e066 100644 --- a/google-cloud-spanner/lib/google/cloud/spanner/service.rb +++ b/google-cloud-spanner/lib/google/cloud/spanner/service.rb @@ -448,6 +448,77 @@ def create_pdml session_name end end + def create_backup instance_id, database_id, backup_id, expire_time + backup = { + database: database_path(instance_id, database_id), + expire_time: expire_time + } + execute do + databases.create_backup \ + instance_path(instance_id), + backup_id, + backup + end + end + + def get_backup instance_id, backup_id + execute do + databases.get_backup backup_path(instance_id, backup_id) + end + end + + def update_backup backup, update_mask + execute do + databases.update_backup backup, update_mask + end + end + + def delete_backup instance_id, backup_id + execute do + databases.delete_backup backup_path(instance_id, backup_id) + end + end + + def list_backups instance_id, filter: nil, page_size: nil + execute do + databases.list_backups \ + instance_path(instance_id), + filter, + page_size: page_size + end + end + + def list_database_operations instance_id, filter: nil, page_size: nil + execute do + databases.list_database_operations \ + instance_path(instance_id), + filter, + page_size: page_size + end + end + + def list_backup_operations instance_id, filter: nil, page_size: nil + execute do + databases.list_backup_operations \ + instance_path(instance_id), + filter, + page_size: page_size + end + end + + def restore_database \ + backup_instance_id, + backup_id, + database_instance_id, + database_id + execute do + databases.restore_database \ + instance_path(database_instance_id), + database_id, + backup: backup_path(backup_instance_id, backup_id) + end + end + def inspect "#{self.class}(#{@project})" end @@ -518,6 +589,12 @@ def session_path instance_id, database_id, session_id ) end + def backup_path instance_id, backup_id + Admin::Database::V1::DatabaseAdminClient.backup_path( + project, instance_id, backup_id + ) + end + def execute yield rescue Google::Gax::GaxError => e diff --git a/google-cloud-spanner/support/doctest_helper.rb b/google-cloud-spanner/support/doctest_helper.rb index 5e02001bb434..81217e79050a 100644 --- a/google-cloud-spanner/support/doctest_helper.rb +++ b/google-cloud-spanner/support/doctest_helper.rb @@ -77,6 +77,8 @@ def mock_spanner doctest.skip "Google::Cloud::Spanner::Transaction#save" doctest.skip "Google::Cloud::Spanner::Instance::Job#refresh!" doctest.skip "Google::Cloud::Spanner::Database::Job#refresh!" + doctest.skip "Google::Cloud::Spanner::Backup::Job#refresh!" + doctest.skip "Google::Cloud::Spanner::Backup::Restore::Job#refresh!" doctest.before "Google::Cloud#spanner" do mock_spanner do |mock, mock_instances, mock_databases| @@ -173,6 +175,24 @@ def mock_spanner end end + doctest.before "Google::Cloud::Spanner::Instance#database_operations" do + mock_spanner do |mock, mock_instances, mock_databases| + mock_client = Minitest::Mock.new + mock_instances.expect :get_instance, OpenStruct.new(instance_hash), ["projects/my-project/instances/my-instance"] + mock_databases.expect :instance_variable_get, mock_client, ["@operations_client"] + mock_databases.expect :list_database_operations, database_operations_resp, ["projects/my-project/instances/my-instance", nil, Hash] + end + end + + doctest.before "Google::Cloud::Spanner::Instance#database_operations@Filter and list" do + mock_spanner do |mock, mock_instances, mock_databases| + mock_client = Minitest::Mock.new + mock_instances.expect :get_instance, OpenStruct.new(instance_hash), ["projects/my-project/instances/my-instance"] + mock_databases.expect :instance_variable_get, mock_client, ["@operations_client"] + mock_databases.expect :list_database_operations, database_operations_resp, ["projects/my-project/instances/my-instance", String, Hash] + end + end + # Instance::Config doctest.before "Google::Cloud::Spanner::Instance::Config" do @@ -603,6 +623,24 @@ def mock_spanner end end + doctest.before "Google::Cloud::Spanner::Database#database_operations" do + mock_spanner do |mock, mock_instances, mock_databases| + mock_client = Minitest::Mock.new + mock_databases.expect :get_database, database_resp, ["projects/my-project/instances/my-instance/databases/my-database"] + mock_databases.expect :instance_variable_get, mock_client, ["@operations_client"] + mock_databases.expect :list_database_operations, database_operations_resp, ["projects/my-project/instances/my-instance", String, Hash] + end + end + + doctest.before "Google::Cloud::Spanner::Database#database_operations@Filter and list" do + mock_spanner do |mock, mock_instances, mock_databases| + mock_client = Minitest::Mock.new + mock_databases.expect :get_database, database_resp, ["projects/my-project/instances/my-instance/databases/my-database"] + mock_databases.expect :instance_variable_get, mock_client, ["@operations_client"] + mock_databases.expect :list_database_operations, database_operations_resp, ["projects/my-project/instances/my-instance", String, Hash] + end + end + # Database::List doctest.before "Google::Cloud::Spanner::Database::List" do @@ -813,6 +851,189 @@ def mock_spanner end end + # Backup + + doctest.before "Google::Cloud::Spanner::Database#create_backup" do + mock_spanner do |mock, mock_instances, mock_databases| + mock_client = Minitest::Mock.new + mock_databases.expect :get_database, database_resp, ["projects/my-project/instances/my-instance/databases/my-database"] + mock_client.expect :get_operation, OpenStruct.new(done: true), ["1234567890", {:options=>nil}] + mock_databases.expect :create_backup, create_backup_resp(client: mock_client), ["projects/my-project/instances/my-instance", "my-backup", Hash] + end + end + + doctest.before "Google::Cloud::Spanner::Instance#backup" do + mock_spanner do |mock, mock_instances, mock_databases| + mock_instances.expect :get_instance, OpenStruct.new(instance_hash), ["projects/my-project/instances/my-instance"] + mock_databases.expect :get_backup, backup_resp, ["projects/my-project/instances/my-instance/backups/my-backup"] + end + end + + doctest.before "Google::Cloud::Spanner::Instance#backup@Will return `nil` if backup does not exist." do + mock_spanner do |mock, mock_instances, mock_databases| + mock_instances.expect :get_instance, OpenStruct.new(instance_hash), ["projects/my-project/instances/my-instance"] + mock_databases.expect :get_backup, nil, ["projects/my-project/instances/my-instance/backups/non-existing-backup"] + end + end + + doctest.before "Google::Cloud::Spanner::Instance#backups" do + mock_spanner do |mock, mock_instances, mock_databases| + mock_instances.expect :get_instance, OpenStruct.new(instance_hash), ["projects/my-project/instances/my-instance"] + mock_databases.expect :list_backups, backups_resp(token: "token"), ["projects/my-project/instances/my-instance", nil, Hash] + end + end + + doctest.before "Google::Cloud::Spanner::Instance#backups@Filter and list backups" do + mock_spanner do |mock, mock_instances, mock_databases| + mock_instances.expect :get_instance, OpenStruct.new(instance_hash), ["projects/my-project/instances/my-instance"] + mock_databases.expect :list_backups, backups_resp(token: "token"), ["projects/my-project/instances/my-instance", String, Hash] + mock_databases.expect :list_backups, backups_resp, ["projects/my-project/instances/my-instance", String, Hash] + end + end + + doctest.before "Google::Cloud::Spanner::Database#backups" do + mock_spanner do |mock, mock_instances, mock_databases| + mock_databases.expect :get_database, OpenStruct.new(database_hash), ["projects/my-project/instances/my-instance/databases/my-database"] + mock_databases.expect :list_backups, backups_resp(token: "token"), ["projects/my-project/instances/my-instance", String, Hash] + mock_databases.expect :list_backups, backups_resp, ["projects/my-project/instances/my-instance", String, Hash] + end + end + + doctest.before "Google::Cloud::Spanner::Backup" do + mock_spanner do |mock, mock_instances, mock_databases| + mock_client = Minitest::Mock.new + mock_databases.expect :get_database, database_resp, ["projects/my-project/instances/my-instance/databases/my-database"] + mock_client.expect :get_operation, OpenStruct.new(done: true), ["1234567890", {:options=>nil}] + mock_databases.expect :create_backup, create_backup_resp(client: mock_client), ["projects/my-project/instances/my-instance", "my-backup", Hash] + end + end + + doctest.before "Google::Cloud::Spanner::Backup#expire_time=" do + mock_spanner do |mock, mock_instances, mock_databases| + mock_instances.expect :get_instance, OpenStruct.new(instance_hash), ["projects/my-project/instances/my-instance"] + mock_databases.expect :get_backup, backup_resp, ["projects/my-project/instances/my-instance/backups/my-backup"] + mock_databases.expect :update_backup, backup_resp, [Google::Spanner::Admin::Database::V1::Backup, Google::Protobuf::FieldMask] + end + end + + doctest.before "Google::Cloud::Spanner::Backup#delete" do + mock_spanner do |mock, mock_instances, mock_databases| + mock_instances.expect :get_instance, OpenStruct.new(instance_hash), ["projects/my-project/instances/my-instance"] + mock_databases.expect :get_backup, backup_resp, ["projects/my-project/instances/my-instance/backups/my-backup"] + mock_databases.expect :delete_backup, nil, ["projects/my-project/instances/my-instance/backups/my-backup"] + end + end + + doctest.before "Google::Cloud::Spanner::Backup#referencing_databases" do + mock_spanner do |mock, mock_instances, mock_databases| + mock_instances.expect :get_instance, OpenStruct.new(instance_hash), ["projects/my-project/instances/my-instance"] + mock_databases.expect :get_backup, backup_resp(referencing_databases: ["my-database"]), ["projects/my-project/instances/my-instance/backups/my-backup"] + mock_databases.expect :get_database, database_resp(database_id: "my-database"), ["projects/my-project/instances/my-instance/databases/my-database"] + end + end + + doctest.before "Google::Cloud::Spanner::Backup::Job#cancel" do + mock_spanner do |mock, mock_instances, mock_databases| + mock_client = Minitest::Mock.new + mock_databases.expect :get_database, database_resp, ["projects/my-project/instances/my-instance/databases/my-database"] + mock_client.expect :cancel_operation, nil, ["1234567890"] + mock_databases.expect :create_backup, create_backup_resp(client: mock_client), ["projects/my-project/instances/my-instance", "my-backup", Hash] + end + end + + doctest.before "Google::Cloud::Spanner::Backup#restore" do + mock_spanner do |mock, mock_instances, mock_databases| + mock_client = Minitest::Mock.new + mock_instances.expect :get_instance, OpenStruct.new(instance_hash), ["projects/my-project/instances/my-instance"] + mock_databases.expect :get_backup, backup_resp, ["projects/my-project/instances/my-instance/backups/my-backup"] + mock_databases.expect :restore_database, create_backup_restore_resp(client: mock_client), [ + "projects/my-project/instances/my-instance", + "my-restored-database", + { backup: "projects/my-project/instances/my-instance/backups/my-backup"} + ] + mock_client.expect :get_operation, OpenStruct.new(done: true), ["1234567890", {options: nil}] + end + end + + doctest.before "Google::Cloud::Spanner::Backup#restore@Restore database in provided instance id" do + mock_spanner do |mock, mock_instances, mock_databases| + mock_client = Minitest::Mock.new + mock_instances.expect :get_instance, OpenStruct.new(instance_hash), ["projects/my-project/instances/my-instance"] + mock_databases.expect :get_backup, backup_resp, ["projects/my-project/instances/my-instance/backups/my-backup"] + mock_databases.expect :restore_database, create_backup_restore_resp(client: mock_client), [ + "projects/my-project/instances/other-instance", + "my-restored-database", + { backup: "projects/my-project/instances/my-instance/backups/my-backup"} + ] + mock_client.expect :get_operation, OpenStruct.new(done: true), ["1234567890", {options: nil}] + end + end + + doctest.before "Google::Cloud::Spanner::Backup::Restore::Job" do + mock_spanner do |mock, mock_instances, mock_databases| + mock_client = Minitest::Mock.new + mock_instances.expect :get_instance, OpenStruct.new(instance_hash), ["projects/my-project/instances/my-instance"] + mock_databases.expect :get_backup, backup_resp, ["projects/my-project/instances/my-instance/backups/my-backup"] + mock_databases.expect :restore_database, create_backup_restore_resp(client: mock_client), [ + "projects/my-project/instances/my-instance", + "my-restored-database", + { backup: "projects/my-project/instances/my-instance/backups/my-backup" } + ] + mock_client.expect :get_operation, OpenStruct.new(done: true), ["1234567890", {options:nil}] + end + end + + # Backup::List + + doctest.before "Google::Cloud::Spanner::Backup::List" do + mock_spanner do |mock, mock_instances, mock_databases| + mock_instances.expect :get_instance, OpenStruct.new(instance_hash), ["projects/my-project/instances/my-instance"] + mock_databases.expect :list_backups, backups_resp(token: "token"), ["projects/my-project/instances/my-instance", nil, Hash] + mock_databases.expect :list_backups, backups_resp, ["projects/my-project/instances/my-instance", nil, Hash] + end + end + + # Database::Job::List + + doctest.before "Google::Cloud::Spanner::Database::Job::List" do + mock_spanner do |mock, mock_instances, mock_databases| + mock_client = Minitest::Mock.new + mock_instances.expect :get_instance, OpenStruct.new(instance_hash), ["projects/my-project/instances/my-instance"] + mock_databases.expect :instance_variable_get, mock_client, ["@operations_client"] + mock_databases.expect :list_database_operations, database_operations_resp, ["projects/my-project/instances/my-instance", nil, Hash] + end + end + + # Backup::Job::List + + doctest.before "Google::Cloud::Spanner::Backup::Job::List" do + mock_spanner do |mock, mock_instances, mock_databases| + mock_client = Minitest::Mock.new + mock_instances.expect :get_instance, OpenStruct.new(instance_hash), ["projects/my-project/instances/my-instance"] + mock_databases.expect :instance_variable_get, mock_client, ["@operations_client"] + mock_databases.expect :list_backup_operations, backup_operations_resp, ["projects/my-project/instances/my-instance", nil, Hash] + end + end + + # Instance - backup operations + + doctest.before "Google::Cloud::Spanner::Instance#backup_operations" do + mock_spanner do |mock, mock_instances, mock_databases| + mock_client = Minitest::Mock.new + mock_instances.expect :get_instance, OpenStruct.new(instance_hash), ["projects/my-project/instances/my-instance"] + mock_databases.expect :instance_variable_get, mock_client, ["@operations_client"] + mock_databases.expect :list_backup_operations, backup_operations_resp, ["projects/my-project/instances/my-instance", nil, Hash] + end + end + + doctest.before "Google::Cloud::Spanner::Instance#backup_operations@Filter and list" do + mock_spanner do |mock, mock_instances, mock_databases| + mock_client = Minitest::Mock.new + mock_instances.expect :get_instance, OpenStruct.new(instance_hash), ["projects/my-project/instances/my-instance"] + mock_databases.expect :instance_variable_get, mock_client, ["@operations_client"] + mock_databases.expect :list_backup_operations, backup_operations_resp, ["projects/my-project/instances/my-instance", String, Hash] + end + end end # Stubs @@ -837,23 +1058,23 @@ def instance_hash name: "my-instance", nodes: 1, state: "READY", labels: {} } end -def job_grpc +def job_grpc type_url, value: "" Google::Longrunning::Operation.new( name: "1234567890", metadata: { - type_url: "google.spanner.admin.database.v1.UpdateDatabaseDdlRequest", - value: "" + type_url: type_url, + value: value } ) end def create_instance_resp client: nil Google::Gax::Operation.new( - job_grpc, - client, - Google::Spanner::Admin::Instance::V1::Instance, - Google::Spanner::Admin::Instance::V1::CreateInstanceMetadata - ) + job_grpc("google.spanner.admin.database.v1.UpdateDatabaseDdlRequest"), + client, + Google::Spanner::Admin::Instance::V1::Instance, + Google::Spanner::Admin::Instance::V1::CreateInstanceMetadata + ) end def instance_configs_hash @@ -861,10 +1082,10 @@ def instance_configs_hash instance_configs: [ { name: "projects/#{project}/instanceConfigs/regional-europe-west1", display_name: "EU West 1"}, - { name: "projects/#{project}/instanceConfigs/regional-us-west1", - display_name: "US West 1"}, - { name: "projects/#{project}/instanceConfigs/regional-us-central1", - display_name: "US Central 1"} + { name: "projects/#{project}/instanceConfigs/regional-us-west1", + display_name: "US West 1"}, + { name: "projects/#{project}/instanceConfigs/regional-us-central1", + display_name: "US Central 1"} ] } end @@ -908,11 +1129,11 @@ def instances_resp token: nil def create_database_resp client: nil Google::Gax::Operation.new( - job_grpc, - client, - Google::Spanner::Admin::Database::V1::Database, - Google::Spanner::Admin::Database::V1::CreateDatabaseMetadata - ) + job_grpc("google.spanner.admin.database.v1.CreateDatabaseRequest"), + client, + Google::Spanner::Admin::Database::V1::Database, + Google::Spanner::Admin::Database::V1::CreateDatabaseMetadata + ) end def databases_hash instance_id: "my-instance" @@ -955,7 +1176,7 @@ def policy_hash members: [ "user:viewer@example.com", "serviceAccount:1234567890@developer.gserviceaccount.com" - ] + ] }] } end @@ -970,7 +1191,6 @@ def test_permissions_res permissions: ["spanner.instances.get"] ) end - def paged_enum_struct response OpenStruct.new page: OpenStruct.new(response: response) end @@ -1058,3 +1278,109 @@ def commit_timestamp def commit_resp Google::Spanner::V1::CommitResponse.new commit_timestamp: commit_timestamp end + +def backup_hash \ + instance_id: "my-instance", + database_id: "my_databse", + backup_id: "my-backup", + state: "READY", + referencing_databases: ["db1"] + { + name: "projects/#{project}/instances/#{instance_id}/backups/#{backup_id}", + database: "projects/#{project}/instances/#{instance_id}/databases/#{database_id}", + state: state, + referencing_databases: referencing_databases.map do |database| + "projects/#{project}/instances/#{instance_id}/databases/#{database}" + end + } +end + +def backups_hash instance_id: "my-instance" + { backups: [backup_hash(instance_id: instance_id)] } +end + +def create_backup_resp client: nil + Google::Gax::Operation.new( + job_grpc("google.spanner.admin.database.v1.CreateBackupRequest"), + client, + Google::Spanner::Admin::Database::V1::Backup, + Google::Spanner::Admin::Database::V1::CreateBackupMetadata + ) +end + +def backup_resp instance_id: "my-instance", database_id: "my-database", backup_id: "my-backup", referencing_databases: ["db1"] + Google::Spanner::Admin::Database::V1::Backup.new backup_hash( + instance_id: instance_id, + database_id: database_id, + backup_id: backup_id, + referencing_databases: referencing_databases + ) +end + +def backups_resp token: nil + h = backups_hash + h[:next_page_token] = token if token + response = Google::Spanner::Admin::Database::V1::ListBackupsResponse.new h + OpenStruct.new response: response +end + +def backup_operation_grpc + Google::Longrunning::Operation.new({ + name: "1234567890", + metadata: Google::Protobuf::Any.new( + type_url: "google.spanner.admin.database.v1.CreateBackupMetadata", + value: Google::Spanner::Admin::Database::V1::CreateBackupMetadata.new.to_proto + ), + done: true, + response: Google::Protobuf::Any.new( + type_url: "google.spanner.admin.database.v1.Backup", + value: Google::Spanner::Admin::Database::V1::Backup.new(backup_hash).to_proto + ) + }) +end + +def backup_operations_hash + { operations: [backup_operation_grpc] } +end + +def backup_operations_resp token: nil + h = backup_operations_hash + h[:next_page_token] = token if token + response = Google::Spanner::Admin::Database::V1::ListBackupOperationsResponse.new h + OpenStruct.new response: response +end + +def database_operation_grpc + Google::Longrunning::Operation.new( + name: "1234567890", + metadata: Google::Protobuf::Any.new( + type_url: "google.spanner.admin.database.v1.CreateDatabaseMetadata", + value: Google::Spanner::Admin::Database::V1::CreateDatabaseMetadata.new.to_proto + ), + done: true, + response: Google::Protobuf::Any.new( + type_url: "google.spanner.admin.database.v1.Database", + value: Google::Spanner::Admin::Database::V1::Database.new(database_hash).to_proto + ) + ) +end + +def database_operations_hash + { operations: [database_operation_grpc] } +end + +def database_operations_resp token: nil + h = database_operations_hash + h[:next_page_token] = token if token + response = Google::Spanner::Admin::Database::V1::ListDatabaseOperationsResponse.new h + OpenStruct.new response: response +end + +def create_backup_restore_resp client: nil + Google::Gax::Operation.new( + job_grpc("google.spanner.admin.database.v1.RestoreDatabaseRequest"), + client, + Google::Spanner::Admin::Database::V1::Database, + Google::Spanner::Admin::Database::V1::RestoreDatabaseMetadata + ) +end diff --git a/google-cloud-spanner/test/google/cloud/spanner/backup/delete_test.rb b/google-cloud-spanner/test/google/cloud/spanner/backup/delete_test.rb new file mode 100644 index 000000000000..802e6a41bf8b --- /dev/null +++ b/google-cloud-spanner/test/google/cloud/spanner/backup/delete_test.rb @@ -0,0 +1,37 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +require "helper" + +describe Google::Cloud::Spanner::Backup, :delete, :mock_spanner do + let(:instance_id) { "my-instance-id" } + let(:database_id) { "my-database-id" } + let(:backup_id) { "my-backup-id" } + let(:backup_grpc) { + Google::Spanner::Admin::Database::V1::Backup.new( + backup_hash(instance_id: instance_id, database_id: database_id, backup_id: backup_id) + ) + } + let(:backup) { Google::Cloud::Spanner::Backup.from_grpc backup_grpc, spanner.service } + + it "can delete itself" do + mock = Minitest::Mock.new + mock.expect :delete_backup, nil, [backup_grpc.name] + spanner.service.mocked_databases = mock + + backup.delete.must_equal true + mock.verify + end +end diff --git a/google-cloud-spanner/test/google/cloud/spanner/backup/expire_time_test.rb b/google-cloud-spanner/test/google/cloud/spanner/backup/expire_time_test.rb new file mode 100644 index 000000000000..e0852cce8f5d --- /dev/null +++ b/google-cloud-spanner/test/google/cloud/spanner/backup/expire_time_test.rb @@ -0,0 +1,57 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +require "helper" + +describe Google::Cloud::Spanner::Backup, :expire_time=, :mock_spanner do + let(:instance_id) { "my-instance-id" } + let(:database_id) { "my-database-id" } + let(:backup_id) { "my-backup-id" } + let(:backup_grpc) { + Google::Spanner::Admin::Database::V1::Backup.new( + backup_hash(instance_id: instance_id, database_id: database_id, backup_id: backup_id) + ) + } + let(:backup) { Google::Cloud::Spanner::Backup.from_grpc backup_grpc, spanner.service } + + it "update backup expire time" do + mask = Google::Protobuf::FieldMask.new paths: ["expire_time"] + mock = Minitest::Mock.new + mock.expect :update_backup, backup_grpc, [backup_grpc, mask] + spanner.service.mocked_databases = mock + + expire_time = Time.now + 36000 + backup.expire_time = expire_time + backup.expire_time.must_equal expire_time + + mock.verify + end + + it "reset previous expire time on update error" do + stub = Object.new + + def stub.update_backup *args + raise Google::Cloud::InvalidArgumentError.new "invalid expire time" + end + backup.service.mocked_databases = stub + expire_time_was = backup.expire_time + + proc { + backup.expire_time = (Time.now - 36000) + }.must_raise Google::Cloud::Error + + backup.expire_time.must_equal expire_time_was + end +end diff --git a/google-cloud-spanner/test/google/cloud/spanner/backup/referencing_databases_test.rb b/google-cloud-spanner/test/google/cloud/spanner/backup/referencing_databases_test.rb new file mode 100644 index 000000000000..180859bdcbf4 --- /dev/null +++ b/google-cloud-spanner/test/google/cloud/spanner/backup/referencing_databases_test.rb @@ -0,0 +1,50 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +require "helper" + +describe Google::Cloud::Spanner::Backup, :referencing_databases, :mock_spanner do + let(:instance_id) { "my-instance-id" } + let(:database_id) { "my-database-id" } + let(:backup_id) { "my-backup-id" } + let(:referencing_database_id) { "referencing-db1" } + let(:backup_grpc) { + Google::Spanner::Admin::Database::V1::Backup.new \ + backup_hash( + instance_id: instance_id, + database_id: database_id, + backup_id: backup_id, + referencing_databases: [referencing_database_id] + ) + } + let(:backup) { Google::Cloud::Spanner::Backup.from_grpc backup_grpc, spanner.service } + + it "get referencing databases" do + get_res = Google::Spanner::Admin::Database::V1::Database.new database_hash(instance_id: instance_id, database_id: referencing_database_id) + mock = Minitest::Mock.new + mock.expect :get_database, get_res, [database_path(instance_id, referencing_database_id)] + spanner.service.mocked_databases = mock + + referencing_databases = backup.referencing_databases + mock.verify + + referencing_databases.must_be_kind_of Array + referencing_databases.length.must_equal 1 + + referencing_database = referencing_databases[0] + referencing_database.instance_id.must_equal instance_id + referencing_database.database_id.must_equal referencing_database_id + end +end diff --git a/google-cloud-spanner/test/google/cloud/spanner/backup/restore_test.rb b/google-cloud-spanner/test/google/cloud/spanner/backup/restore_test.rb new file mode 100644 index 000000000000..30a1e9ac42e6 --- /dev/null +++ b/google-cloud-spanner/test/google/cloud/spanner/backup/restore_test.rb @@ -0,0 +1,113 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +require "helper" + +describe Google::Cloud::Spanner::Backup, :restore_database, :mock_spanner do + let(:instance_id) { "my-instance-id" } + let(:database_id) { "my-database-id" } + let(:backup_id) { "my-backup-id" } + let(:backup_grpc) { + Google::Spanner::Admin::Database::V1::Backup.new( + backup_hash(instance_id: instance_id, database_id: database_id, backup_id: backup_id) + ) + } + let(:job_grpc) do + Google::Longrunning::Operation.new( + name: "1234567890", + metadata: { + type_url: "google.spanner.admin.database.v1.RestoreDatabaseRequest", + value: "" + } + ) + end + let(:job_grpc_done) do + Google::Longrunning::Operation.new( + name:"1234567890", + metadata: Google::Protobuf::Any.new( + type_url: "google.spanner.admin.database.v1.RestoreDatabaseMetadata", + value: Google::Spanner::Admin::Database::V1::RestoreDatabaseMetadata.new.to_proto + ), + done: true, + response: Google::Protobuf::Any.new( + type_url: "type.googleapis.com/google.spanner.admin.database.v1.Database", + value: Google::Spanner::Admin::Database::V1::RestoreDatabaseMetadata.new.to_proto + ) + ) + end + let(:backup) { Google::Cloud::Spanner::Backup.from_grpc backup_grpc, spanner.service } + + it "restore a database in the same instance as the backup instance" do + mock = Minitest::Mock.new + restore_res = Google::Gax::Operation.new( + job_grpc, + mock, + Google::Spanner::Admin::Database::V1::Database, + Google::Spanner::Admin::Database::V1::RestoreDatabaseMetadata, + ) + mock.expect :restore_database, restore_res, [ + instance_path(instance_id), + "restored-database", + backup: backup_path(instance_id, backup_id) + ] + mock.expect :get_operation, job_grpc_done, ["1234567890", Hash] + spanner.service.mocked_databases = mock + + job = backup.restore "restored-database" + + job.must_be_kind_of Google::Cloud::Spanner::Backup::Restore::Job + job.wont_be :done? + job.wont_be :error? + job.error.must_be :nil? + job.database.must_be :nil? + job.reload! + database = job.database + database.wont_be :nil? + database.must_be_kind_of Google::Cloud::Spanner::Database + + mock.verify + end + + it "restore a database in a different instance than the backup instance" do + mock = Minitest::Mock.new + restore_res = Google::Gax::Operation.new( + job_grpc, + mock, + Google::Spanner::Admin::Database::V1::Database, + Google::Spanner::Admin::Database::V1::RestoreDatabaseMetadata, + ) + mock.expect :restore_database, restore_res, [ + instance_path("other-instance"), + "restored-database", + backup: backup_path(instance_id, backup_id) + ] + mock.expect :get_operation, job_grpc_done, ["1234567890", Hash] + spanner.service.mocked_databases = mock + + job = backup.restore "restored-database", instance_id: "other-instance" + + job.must_be_kind_of Google::Cloud::Spanner::Backup::Restore::Job + job.wont_be :done? + job.wont_be :error? + job.error.must_be :nil? + job.database.must_be :nil? + job.reload! + database = job.database + database.wont_be :nil? + database.must_be_kind_of Google::Cloud::Spanner::Database + + mock.verify + end +end diff --git a/google-cloud-spanner/test/google/cloud/spanner/backup_test.rb b/google-cloud-spanner/test/google/cloud/spanner/backup_test.rb new file mode 100644 index 000000000000..2986cdf55f57 --- /dev/null +++ b/google-cloud-spanner/test/google/cloud/spanner/backup_test.rb @@ -0,0 +1,51 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +require "helper" + +describe Google::Cloud::Spanner::Backup, :mock_spanner do + let(:instance_id) { "my-instance-id" } + let(:database_id) { "my-database-id" } + let(:backup_id) { "my-backup-id" } + let(:backup_grpc) { + Google::Spanner::Admin::Database::V1::Backup.new( + backup_hash( + instance_id: instance_id, + database_id: database_id, + backup_id: backup_id, + expire_time: Time.now + 36000, + create_time: Time.now, + size_bytes: 1024 + ) + ) + } + let(:backup) { Google::Cloud::Spanner::Backup.from_grpc backup_grpc, spanner.service } + + it "knows the identifiers" do + backup.must_be_kind_of Google::Cloud::Spanner::Backup + backup.project_id.must_equal project + backup.instance_id.must_equal instance_id + backup.database_id.must_equal database_id + backup.backup_id.must_equal backup_id + + backup.state.must_equal :READY + backup.must_be :ready? + backup.wont_be :creating? + + backup.expire_time.must_be_kind_of Time + backup.create_time.must_be_kind_of Time + backup.size_in_bytes.must_be :>, 0 + end +end diff --git a/google-cloud-spanner/test/google/cloud/spanner/database/backup_test.rb b/google-cloud-spanner/test/google/cloud/spanner/database/backup_test.rb new file mode 100644 index 000000000000..40ab01f45ff0 --- /dev/null +++ b/google-cloud-spanner/test/google/cloud/spanner/database/backup_test.rb @@ -0,0 +1,136 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +require "helper" + +describe Google::Cloud::Spanner::Database, :backups, :mock_spanner do + let(:instance_id) { "my-instance-id" } + let(:databases_grpc) { Google::Spanner::Admin::Database::V1::Database.new database_hash } + let(:database) { Google::Cloud::Spanner::Database.from_grpc databases_grpc, spanner.service } + let(:first_page) do + h = backups_hash instance_id: instance_id + h[:next_page_token] = "next_page_token" + Google::Spanner::Admin::Database::V1::ListBackupsResponse.new h + end + let(:second_page) do + h = backups_hash instance_id: instance_id + h[:next_page_token] = "second_page_token" + Google::Spanner::Admin::Database::V1::ListBackupsResponse.new h + end + let(:last_page) do + h = backups_hash instance_id: instance_id + h[:backups].pop + Google::Spanner::Admin::Database::V1::ListBackupsResponse.new h + end + let(:next_page_options) { Google::Gax::CallOptions.new page_token: "next_page_token" } + let(:backup_filter) { "database:#{database.database_id}" } + + it "lists backups" do + get_res = MockPagedEnumerable.new([first_page]) + mock = Minitest::Mock.new + mock.expect :list_backups, get_res, [instance_path(instance_id), backup_filter, page_size: nil] + database.service.mocked_databases = mock + + backups = database.backups + + mock.verify + + backups.size.must_equal 3 + end + + it "paginates backups with page size" do + get_res = MockPagedEnumerable.new([first_page]) + mock = Minitest::Mock.new + mock.expect :list_backups, get_res, [instance_path(instance_id), backup_filter, page_size: 3] + database.service.mocked_databases = mock + + backups = database.backups page_size: 3 + + mock.verify + + backups.size.must_equal 3 + end + + it "paginates backups with next? and next" do + get_res = MockPagedEnumerable.new([first_page, last_page]) + mock = Minitest::Mock.new + mock.expect :list_backups, get_res, [instance_path(instance_id), backup_filter, page_size: nil] + database.service.mocked_databases = mock + + list = database.backups + + mock.verify + + list.size.must_equal 3 + list.next?.must_equal true + list.next.size.must_equal 2 + list.next?.must_equal false + end + + it "paginates backups with next? and next and page size" do + get_res = MockPagedEnumerable.new([first_page, last_page]) + mock = Minitest::Mock.new + mock.expect :list_backups, get_res, [instance_path(instance_id), backup_filter, page_size: 3] + database.service.mocked_databases = mock + + list = database.backups page_size: 3 + + mock.verify + + list.size.must_equal 3 + list.next?.must_equal true + list.next.size.must_equal 2 + list.next?.must_equal false + end + + it "paginates backups with all" do + get_res = MockPagedEnumerable.new([first_page, last_page]) + mock = Minitest::Mock.new + mock.expect :list_backups, get_res, [instance_path(instance_id), backup_filter, page_size: nil] + database.service.mocked_databases = mock + + backups = database.backups.all.to_a + + mock.verify + + backups.size.must_equal 5 + end + + it "paginates backups with all and page size" do + get_res = MockPagedEnumerable.new([first_page, last_page]) + mock = Minitest::Mock.new + mock.expect :list_backups, get_res, [instance_path(instance_id), backup_filter, page_size: 3] + database.service.mocked_databases = mock + + backups = database.backups(page_size: 3).all.to_a + + mock.verify + + backups.size.must_equal 5 + end + + it "iterates backups with all using Enumerator" do + get_res = MockPagedEnumerable.new([first_page, last_page]) + mock = Minitest::Mock.new + mock.expect :list_backups, get_res, [instance_path(instance_id), backup_filter, page_size: nil] + database.service.mocked_databases = mock + + backups = database.backups.all.take(5) + + mock.verify + + backups.size.must_equal 5 + end +end diff --git a/google-cloud-spanner/test/google/cloud/spanner/database/create_backup_test.rb b/google-cloud-spanner/test/google/cloud/spanner/database/create_backup_test.rb new file mode 100644 index 000000000000..7930b813a950 --- /dev/null +++ b/google-cloud-spanner/test/google/cloud/spanner/database/create_backup_test.rb @@ -0,0 +1,170 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +require "helper" + +describe Google::Cloud::Spanner::Backup, :create_backup, :mock_spanner do + let(:instance_id) { "my-instance-id" } + let(:database_id) { "my-database-id" } + let(:backup_id) { "my-backup-id" } + let(:database_grpc) { Google::Spanner::Admin::Database::V1::Database.new \ + database_hash(instance_id: instance_id, database_id: database_id) + } + let(:job_grpc) do + Google::Longrunning::Operation.new( + name: "1234567890", + metadata: { + type_url: "type.googleapis.com/google.spanner.admin.database.v1.CreateBackupMetadata", + value: Google::Spanner::Admin::Database::V1::CreateBackupMetadata.new( + progress: { start_time: Time.now } + ).to_proto + } + ) + end + let(:job_grpc_done) do + Google::Longrunning::Operation.new( + name:"1234567890", + metadata: Google::Protobuf::Any.new( + type_url: "google.spanner.admin.database.v1.CreateBackupMetadata", + value: Google::Spanner::Admin::Database::V1::CreateBackupMetadata.new( + progress: { + start_time: Time.now, + end_time: Time.now + 100, + progress_percent: 100 + } + ).to_proto + ), + done: true, + response: Google::Protobuf::Any.new( + type_url: "google.spanner.admin.database.v1.Backup", + value: Google::Spanner::Admin::Database::V1::Backup.new.to_proto + ) + ) + end + let(:job_grpc_cancel) do + Google::Longrunning::Operation.new( + name:"1234567890", + metadata: Google::Protobuf::Any.new( + type_url: "google.spanner.admin.database.v1.CreateBackupMetadata", + value: Google::Spanner::Admin::Database::V1::CreateBackupMetadata.new( + progress: { + start_time: Time.now, + end_time: Time.now + 100, + progress_percent: 100 + }, + cancel_time: Time.now + 100 + ).to_proto + ), + done: true, + error: { code: 1, message: 'Backup creation cancelled by the user' } + ) + end + let(:database) { Google::Cloud::Spanner::Database.from_grpc database_grpc, spanner.service } + let(:expire_time) { Time.now + 36000 } + + it "create a database backup" do + mock = Minitest::Mock.new + create_req = { + database: database_path(instance_id, database_id), + expire_time: expire_time + } + create_res = Google::Gax::Operation.new( + job_grpc, + mock, + Google::Spanner::Admin::Database::V1::Backup, + Google::Spanner::Admin::Database::V1::CreateBackupMetadata + ) + mock.expect :create_backup, create_res, [ + instance_path(instance_id), + backup_id, + create_req + ] + mock.expect :get_operation, job_grpc_done, ["1234567890", Hash] + spanner.service.mocked_databases = mock + + job = database.create_backup backup_id, expire_time + + job.must_be_kind_of Google::Cloud::Spanner::Backup::Job + job.wont_be :done? + job.wont_be :error? + job.error.must_be :nil? + job.backup.must_be :nil? + job.start_time.must_be_kind_of Time + job.end_time.must_be :nil? + job.progress_percent.must_equal 0 + + job.reload! + backup = job.backup + backup.wont_be :nil? + backup.must_be_kind_of Google::Cloud::Spanner::Backup + job.start_time.must_be_kind_of Time + job.end_time.must_be_kind_of Time + job.cancel_time.must_be :nil? + job.progress_percent.must_equal 100 + + mock.verify + end + + it "cancel create database backup job" do + mock = Minitest::Mock.new + create_req = { + database: database_path(instance_id, database_id), + expire_time: expire_time + } + create_res = Google::Gax::Operation.new( + job_grpc, + mock, + Google::Spanner::Admin::Database::V1::Backup, + Google::Spanner::Admin::Database::V1::CreateBackupMetadata + ) + mock.expect :create_backup, create_res, [ + instance_path(instance_id), + backup_id, + create_req + ] + mock.expect :cancel_operation, nil , ["1234567890"] + mock.expect :get_operation, job_grpc_cancel, ["1234567890", Hash] + spanner.service.mocked_databases = mock + + job = database.create_backup backup_id, expire_time + + job.must_be_kind_of Google::Cloud::Spanner::Backup::Job + job.wont_be :done? + job.cancel.must_be_nil + + job.reload! + job.must_be :done? + job.start_time.must_be_kind_of Time + job.end_time.must_be_kind_of Time + job.cancel_time.must_be_kind_of Time + job.progress_percent.must_equal 100 + + mock.verify + end + + it "raise an error on create database backup for invalid expire time" do + stub = Object.new + + def stub.create_backup *args + raise Google::Cloud::InvalidArgumentError.new "invalid expire time" + end + + spanner.service.mocked_databases = stub + + proc { + database.create_backup backup_id, Time.now - 36000 + }.must_raise Google::Cloud::Error + end +end diff --git a/google-cloud-spanner/test/google/cloud/spanner/database/database_operations_test.rb b/google-cloud-spanner/test/google/cloud/spanner/database/database_operations_test.rb new file mode 100644 index 000000000000..c27133359fd2 --- /dev/null +++ b/google-cloud-spanner/test/google/cloud/spanner/database/database_operations_test.rb @@ -0,0 +1,239 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +require "helper" + +describe Google::Cloud::Spanner::Database, :database_operations, :mock_spanner do + let(:instance_id) { "my-instance-id" } + let(:database_id) { "my-database-id" } + let(:databases_grpc) { + Google::Spanner::Admin::Database::V1::Database.new database_hash( + instance_id: instance_id, database_id: database_id) + } + let(:database) { Google::Cloud::Spanner::Database.from_grpc databases_grpc, spanner.service } + let(:database_grpc) { Google::Spanner::Admin::Database::V1::Database.new database_hash } + let(:database_metadata_filter) { + format( + Google::Cloud::Spanner::Database::DATBASE_OPERATION_METADAT_FILTER_TEMPLATE, + database_id: database_id + ) + } + let(:job_name) { "1234567890" } + let(:job_hash) do + { + name: job_name, + metadata: { + type_url: "type.googleapis.com/google.spanner.admin.database.v1.CreateDatabaseMetadata", + value: Google::Spanner::Admin::Database::V1::CreateDatabaseMetadata.new.to_proto + } + } + end + let(:job_grpc) { Google::Longrunning::Operation.new job_hash } + let(:job_grpc_done) do + Google::Longrunning::Operation.new( + name:"1234567890", + metadata: Google::Protobuf::Any.new( + type_url: "google.spanner.admin.database.v1.CreateDatabaseMetadata", + value: Google::Spanner::Admin::Database::V1::CreateDatabaseMetadata.new.to_proto + ), + done: true, + response: Google::Protobuf::Any.new( + type_url: "type.googleapis.com/google.spanner.admin.database.v1.Database", + value: database_grpc.to_proto + ) + ) + end + let(:jobs_hash) do + 3.times.map { job_hash } + end + let(:first_page) do + h = { operations: jobs_hash } + h[:next_page_token] = "next_page_token" + Google::Spanner::Admin::Database::V1::ListDatabaseOperationsResponse.new h + end + let(:second_page) do + h = { operations: jobs_hash } + h[:next_page_token] = "second_page_token" + Google::Spanner::Admin::Database::V1::ListDatabaseOperationsResponse.new h + end + let(:last_page) do + h = { operations: jobs_hash } + h[:operations].pop + Google::Spanner::Admin::Database::V1::ListDatabaseOperationsResponse.new h + end + + it "list database operations" do + list_res = MockPagedEnumerable.new([first_page]) + + mock = Minitest::Mock.new + mock.expect :list_database_operations, list_res, [instance_path(instance_id), database_metadata_filter, page_size: nil] + 3.times do + mock.expect :get_operation, job_grpc_done, [job_name, Hash] + end + mock.expect :instance_variable_get, mock, ["@operations_client"] + database.service.mocked_databases = mock + + jobs = database.database_operations + jobs.size.must_equal 3 + + jobs.each do |job| + job.must_be_kind_of Google::Cloud::Spanner::Database::Job + job.wont_be :done? + job.wont_be :error? + job.error.must_be :nil? + job.database.must_be :nil? + job.reload! + + database = job.database + database.wont_be :nil? + database.must_be_kind_of Google::Cloud::Spanner::Database + end + + mock.verify + end + + it "paginates database operations with page size" do + list_res = MockPagedEnumerable.new([first_page]) + + mock = Minitest::Mock.new + mock.expect :list_database_operations, list_res, [instance_path(instance_id), database_metadata_filter, page_size: 3] + 3.times do + mock.expect :get_operation, job_grpc_done.dup, [job_name, Hash] + end + mock.expect :instance_variable_get, mock, ["@operations_client"] + database.service.mocked_databases = mock + + jobs = database.database_operations page_size: 3 + jobs.size.must_equal 3 + + jobs.each do |job| + job.must_be_kind_of Google::Cloud::Spanner::Database::Job + job.wont_be :done? + job.wont_be :error? + job.error.must_be :nil? + job.database.must_be :nil? + job.reload! + + database = job.database + database.wont_be :nil? + database.must_be_kind_of Google::Cloud::Spanner::Database + end + + mock.verify + end + + it "paginates database operations with next? and next" do + list_res = MockPagedEnumerable.new([first_page, last_page]) + mock = Minitest::Mock.new + mock.expect :list_database_operations, list_res, [instance_path(instance_id), database_metadata_filter, page_size: nil] + 2.times do + mock.expect :instance_variable_get, mock, ["@operations_client"] + end + database.service.mocked_databases = mock + + jobs = database.database_operations + + jobs.size.must_equal 3 + jobs.next?.must_equal true + jobs.next.size.must_equal 2 + jobs.next?.must_equal false + + mock.verify + end + + it "paginates database operations with all" do + list_res = MockPagedEnumerable.new([first_page, last_page]) + mock = Minitest::Mock.new + mock.expect :list_database_operations, list_res, [instance_path(instance_id), database_metadata_filter, page_size: nil] + 2.times do + mock.expect :instance_variable_get, mock, ["@operations_client"] + end + database.service.mocked_databases = mock + + jobs = database.database_operations.all.to_a + + mock.verify + + jobs.size.must_equal 5 + end + + it "paginates database operations with all and page size" do + list_res = MockPagedEnumerable.new([first_page, last_page]) + mock = Minitest::Mock.new + mock.expect :list_database_operations, list_res, [instance_path(instance_id), database_metadata_filter, page_size: 3] + 2.times do + mock.expect :instance_variable_get, mock, ["@operations_client"] + end + database.service.mocked_databases = mock + + jobs = database.database_operations(page_size: 3).all.to_a + + mock.verify + + jobs.size.must_equal 5 + end + + it "iterates database operations with all using Enumerator" do + list_res = MockPagedEnumerable.new([first_page, last_page]) + mock = Minitest::Mock.new + mock.expect :list_database_operations, list_res, [instance_path(instance_id), database_metadata_filter, page_size: nil] + 2.times do + mock.expect :instance_variable_get, mock, ["@operations_client"] + end + database.service.mocked_databases = mock + + jobs = database.database_operations.all.take(5) + + mock.verify + + jobs.size.must_equal 5 + end + + it "paginates database operations with filter" do + filter = format( + "(%s) AND (%s)", + filter: "done:true", database_filter: database_metadata_filter + ) + list_res = MockPagedEnumerable.new([first_page]) + mock = Minitest::Mock.new + mock.expect :list_database_operations, list_res, [instance_path(instance_id), filter, page_size: nil ] + mock.expect :instance_variable_get, mock, ["@operations_client"] + database.service.mocked_databases = mock + + jobs = database.database_operations filter: "done:true" + + mock.verify + + jobs.size.must_equal 3 + end + + it "paginates database operations with filter and page size" do + filter = format( + "(%s) AND (%s)", + filter: "done:true", database_filter: database_metadata_filter + ) + list_res = MockPagedEnumerable.new([first_page]) + mock = Minitest::Mock.new + mock.expect :list_database_operations, list_res, [instance_path(instance_id), filter, page_size: 3 ] + mock.expect :instance_variable_get, mock, ["@operations_client"] + database.service.mocked_databases = mock + + jobs = database.database_operations filter: "done:true", page_size: 3 + + mock.verify + + jobs.size.must_equal 3 + end +end diff --git a/google-cloud-spanner/test/google/cloud/spanner/database_test.rb b/google-cloud-spanner/test/google/cloud/spanner/database_test.rb index 351c2fce179d..84b35bbcb142 100644 --- a/google-cloud-spanner/test/google/cloud/spanner/database_test.rb +++ b/google-cloud-spanner/test/google/cloud/spanner/database_test.rb @@ -17,7 +17,20 @@ describe Google::Cloud::Spanner::Instance, :mock_spanner do let(:instance_id) { "my-instance-id" } let(:database_id) { "my-database-id" } - let(:database_grpc) { Google::Spanner::Admin::Database::V1::Database.new database_hash(instance_id: instance_id, database_id: database_id) } + let(:backup_id) { "my-backup-id" } + let(:source_database_id) { "my-backup-source-database-id" } + let(:restore_info) do + restore_info_hash source_type: 'BACKUP', backup_info: backup_info_hash( + instance_id: instance_id, + backup_id: backup_id, + create_time: Time.now, + source_database_id: source_database_id + ) + end + let(:database_grpc) do + Google::Spanner::Admin::Database::V1::Database.new \ + database_hash(instance_id: instance_id, database_id: database_id, restore_info: restore_info) + end let(:database) { Google::Cloud::Spanner::Database.from_grpc database_grpc, spanner.service } it "knows the identifiers" do @@ -29,5 +42,20 @@ database.state.must_equal :READY database.must_be :ready? database.wont_be :creating? + + restore_info = database.restore_info + restore_info.must_be_kind_of Google::Cloud::Spanner::Database::RestoreInfo + restore_info.source_type.must_equal :BACKUP + restore_info.must_be :source_backup? + + backup_info = restore_info.backup_info + backup_info.must_be_kind_of Google::Cloud::Spanner::Database::BackupInfo + backup_info.project_id.must_equal project + backup_info.instance_id.must_equal instance_id + backup_info.backup_id.must_equal backup_id + backup_info.source_database_project_id.must_equal project + backup_info.source_database_instance_id.must_equal instance_id + backup_info.source_database_id.must_equal source_database_id + backup_info.create_time.must_be_kind_of Time end end diff --git a/google-cloud-spanner/test/google/cloud/spanner/instance/backup_operations_test.rb b/google-cloud-spanner/test/google/cloud/spanner/instance/backup_operations_test.rb new file mode 100644 index 000000000000..b5b8befe6978 --- /dev/null +++ b/google-cloud-spanner/test/google/cloud/spanner/instance/backup_operations_test.rb @@ -0,0 +1,223 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +require "helper" + +describe Google::Cloud::Spanner::Instance, :backup_operations, :mock_spanner do + let(:instance_id) { "my-instance-id" } + let(:instance_grpc) { Google::Spanner::Admin::Instance::V1::Instance.new instance_hash(name: instance_id) } + let(:instance) { Google::Cloud::Spanner::Instance.from_grpc instance_grpc, spanner.service } + let(:backup_grpc) { Google::Spanner::Admin::Database::V1::Backup.new backup_hash } + let(:job_name) { "1234567890" } + let(:job_hash) do + { + name: job_name, + metadata: { + type_url: "type.googleapis.com/google.spanner.admin.database.v1.CreateBackupMetadata", + value: "" + } + } + end + let(:job_grpc) { Google::Longrunning::Operation.new job_hash } + let(:job_grpc_done) do + Google::Longrunning::Operation.new( + name:"1234567890", + metadata: Google::Protobuf::Any.new( + type_url: "google.spanner.admin.database.v1.CreateBackupMetadata", + value: Google::Spanner::Admin::Database::V1::CreateBackupMetadata.new.to_proto + ), + done: true, + response: Google::Protobuf::Any.new( + type_url: "type.googleapis.com/google.spanner.admin.database.v1.backup", + value: backup_grpc.to_proto + ) + ) + end + let(:jobs_hash) do + 3.times.map { job_hash } + end + let(:first_page) do + h = { operations: jobs_hash } + h[:next_page_token] = "next_page_token" + Google::Spanner::Admin::Database::V1::ListBackupOperationsResponse.new h + end + let(:second_page) do + h = { operations: jobs_hash } + h[:next_page_token] = "second_page_token" + Google::Spanner::Admin::Database::V1::ListBackupOperationsResponse.new h + end + let(:last_page) do + h = { operations: jobs_hash } + h[:operations].pop + Google::Spanner::Admin::Database::V1::ListBackupOperationsResponse.new h + end + + it "list backup operations" do + list_res = MockPagedEnumerable.new([first_page]) + + mock = Minitest::Mock.new + mock.expect :list_backup_operations, list_res, [instance_path(instance_id), nil, page_size: nil] + 3.times do + mock.expect :get_operation, job_grpc_done, [job_name, Hash] + end + mock.expect :instance_variable_get, mock, ["@operations_client"] + instance.service.mocked_databases = mock + + jobs = instance.backup_operations + jobs.size.must_equal 3 + + jobs.each do |job| + job.must_be_kind_of Google::Cloud::Spanner::Backup::Job + job.wont_be :done? + job.wont_be :error? + job.error.must_be :nil? + job.backup.must_be :nil? + job.reload! + + backup = job.backup + backup.wont_be :nil? + backup.must_be_kind_of Google::Cloud::Spanner::Backup + end + + mock.verify + end + + it "paginates backup operations with page size" do + list_res = MockPagedEnumerable.new([first_page]) + + mock = Minitest::Mock.new + mock.expect :list_backup_operations, list_res, [instance_path(instance_id), nil, page_size: 3] + 3.times do + mock.expect :get_operation, job_grpc_done, [job_name, Hash] + end + mock.expect :instance_variable_get, mock, ["@operations_client"] + instance.service.mocked_databases = mock + + jobs = instance.backup_operations page_size: 3 + jobs.size.must_equal 3 + + jobs.each do |job| + job.must_be_kind_of Google::Cloud::Spanner::Backup::Job + job.wont_be :done? + job.wont_be :error? + job.error.must_be :nil? + job.backup.must_be :nil? + job.reload! + + backup = job.backup + backup.wont_be :nil? + backup.must_be_kind_of Google::Cloud::Spanner::Backup + end + + mock.verify + end + + it "paginates backup operations with next? and next" do + list_res = MockPagedEnumerable.new([first_page, last_page]) + mock = Minitest::Mock.new + mock.expect :list_backup_operations, list_res, [instance_path(instance_id), nil, page_size: nil] + 2.times do + mock.expect :instance_variable_get, mock, ["@operations_client"] + end + instance.service.mocked_databases = mock + + jobs = instance.backup_operations + + jobs.size.must_equal 3 + jobs.next?.must_equal true + jobs.next.size.must_equal 2 + jobs.next?.must_equal false + + mock.verify + end + + it "paginates backup operations with all" do + list_res = MockPagedEnumerable.new([first_page, last_page]) + mock = Minitest::Mock.new + mock.expect :list_backup_operations, list_res, [instance_path(instance_id), nil, page_size: nil] + 2.times do + mock.expect :instance_variable_get, mock, ["@operations_client"] + end + instance.service.mocked_databases = mock + + jobs = instance.backup_operations.all.to_a + + mock.verify + + jobs.size.must_equal 5 + end + + it "paginates backup operations with all and page size" do + list_res = MockPagedEnumerable.new([first_page, last_page]) + mock = Minitest::Mock.new + mock.expect :list_backup_operations, list_res, [instance_path(instance_id), nil, page_size: 3] + 2.times do + mock.expect :instance_variable_get, mock, ["@operations_client"] + end + instance.service.mocked_databases = mock + + jobs = instance.backup_operations(page_size: 3).all.to_a + + mock.verify + + jobs.size.must_equal 5 + end + + it "iterates backup operations with all using Enumerator" do + list_res = MockPagedEnumerable.new([first_page, last_page]) + mock = Minitest::Mock.new + mock.expect :list_backup_operations, list_res, [instance_path(instance_id), nil, page_size: nil] + 2.times do + mock.expect :instance_variable_get, mock, ["@operations_client"] + end + instance.service.mocked_databases = mock + + jobs = instance.backup_operations.all.take(5) + + mock.verify + + jobs.size.must_equal 5 + end + + it "paginates backup operations with filter" do + filter = 'metadata.@type:CreateBackupMetadata' + list_res = MockPagedEnumerable.new([first_page]) + mock = Minitest::Mock.new + mock.expect :list_backup_operations, list_res, [instance_path(instance_id), filter, page_size: nil ] + mock.expect :instance_variable_get, mock, ["@operations_client"] + instance.service.mocked_databases = mock + + jobs = instance.backup_operations filter: filter + + mock.verify + + jobs.size.must_equal 3 + end + + it "paginates backup operations with filter and page size" do + filter = 'metadata.@type:CreateBackupMetadata' + list_res = MockPagedEnumerable.new([first_page]) + mock = Minitest::Mock.new + mock.expect :list_backup_operations, list_res, [instance_path(instance_id), filter, page_size: 3 ] + mock.expect :instance_variable_get, mock, ["@operations_client"] + instance.service.mocked_databases = mock + + jobs = instance.backup_operations filter: filter, page_size: 3 + + mock.verify + + jobs.size.must_equal 3 + end +end diff --git a/google-cloud-spanner/test/google/cloud/spanner/instance/backup_test.rb b/google-cloud-spanner/test/google/cloud/spanner/instance/backup_test.rb new file mode 100644 index 000000000000..d1238826731c --- /dev/null +++ b/google-cloud-spanner/test/google/cloud/spanner/instance/backup_test.rb @@ -0,0 +1,63 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +require "helper" + +describe Google::Cloud::Spanner::Instance, :backup, :mock_spanner do + let(:instance_id) { "my-instance-id" } + let(:database_id) { "my-database-id" } + let(:instance_grpc) { Google::Spanner::Admin::Instance::V1::Instance.new instance_hash(name: instance_id) } + let(:instance) { Google::Cloud::Spanner::Instance.from_grpc instance_grpc, spanner.service } + + it "gets a database backup" do + backup_id = "found-backup" + + get_res = Google::Spanner::Admin::Database::V1::Backup.new \ + backup_hash(instance_id: instance_id, database_id: database_id, backup_id: backup_id) + mock = Minitest::Mock.new + mock.expect :get_backup, get_res, [backup_path(instance_id, backup_id)] + instance.service.mocked_databases = mock + + backup = instance.backup backup_id + + mock.verify + + backup.project_id.must_equal project + backup.instance_id.must_equal instance_id + backup.database_id.must_equal database_id + backup.backup_id.must_equal backup_id + + backup.path.must_equal backup_path(instance_id, backup_id) + + backup.state.must_equal :READY + backup.must_be :ready? + backup.wont_be :creating? + end + + it "returns nil when getting a non-existent backup" do + not_found_backup_id = "not-found-backup" + + stub = Object.new + def stub.get_backup *args + gax_error = Google::Gax::GaxError.new "not found" + gax_error.instance_variable_set :@cause, GRPC::BadStatus.new(5, "not found") + raise gax_error + end + instance.service.mocked_databases = stub + + backup = instance.backup not_found_backup_id + backup.must_be :nil? + end +end diff --git a/google-cloud-spanner/test/google/cloud/spanner/instance/backups_test.rb b/google-cloud-spanner/test/google/cloud/spanner/instance/backups_test.rb new file mode 100644 index 000000000000..8d6fd2990b11 --- /dev/null +++ b/google-cloud-spanner/test/google/cloud/spanner/instance/backups_test.rb @@ -0,0 +1,161 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +require "helper" + +describe Google::Cloud::Spanner::Instance, :backups, :mock_spanner do + let(:instance_id) { "my-instance-id" } + let(:instance_grpc) { Google::Spanner::Admin::Instance::V1::Instance.new instance_hash(name: instance_id) } + let(:instance) { Google::Cloud::Spanner::Instance.from_grpc instance_grpc, spanner.service } + let(:first_page) do + h = backups_hash instance_id: instance_id + h[:next_page_token] = "next_page_token" + Google::Spanner::Admin::Database::V1::ListBackupsResponse.new h + end + let(:second_page) do + h = backups_hash instance_id: instance_id + h[:next_page_token] = "second_page_token" + Google::Spanner::Admin::Database::V1::ListBackupsResponse.new h + end + let(:last_page) do + h = backups_hash instance_id: instance_id + h[:backups].pop + Google::Spanner::Admin::Database::V1::ListBackupsResponse.new h + end + let(:next_page_options) { Google::Gax::CallOptions.new page_token: "next_page_token" } + + it "lists backups" do + get_res = MockPagedEnumerable.new([first_page]) + mock = Minitest::Mock.new + mock.expect :list_backups, get_res, [instance_path(instance_id), nil, page_size: nil] + instance.service.mocked_databases = mock + + backups = instance.backups + + mock.verify + + backups.size.must_equal 3 + end + + it "paginates backups with page size" do + get_res = MockPagedEnumerable.new([first_page]) + mock = Minitest::Mock.new + mock.expect :list_backups, get_res, [instance_path(instance_id), nil, page_size: 3] + instance.service.mocked_databases = mock + + backups = instance.backups page_size: 3 + + mock.verify + + backups.size.must_equal 3 + end + + it "paginates backups with next? and next" do + get_res = MockPagedEnumerable.new([first_page, last_page]) + mock = Minitest::Mock.new + mock.expect :list_backups, get_res, [instance_path(instance_id), nil, page_size: nil] + instance.service.mocked_databases = mock + + list = instance.backups + + mock.verify + + list.size.must_equal 3 + list.next?.must_equal true + list.next.size.must_equal 2 + list.next?.must_equal false + end + + it "paginates backups with next? and next and page size" do + get_res = MockPagedEnumerable.new([first_page, last_page]) + mock = Minitest::Mock.new + mock.expect :list_backups, get_res, [instance_path(instance_id), nil, page_size: 3] + instance.service.mocked_databases = mock + + list = instance.backups page_size: 3 + + mock.verify + + list.size.must_equal 3 + list.next?.must_equal true + list.next.size.must_equal 2 + list.next?.must_equal false + end + + it "paginates backups with all" do + get_res = MockPagedEnumerable.new([first_page, last_page]) + mock = Minitest::Mock.new + mock.expect :list_backups, get_res, [instance_path(instance_id), nil, page_size: nil] + instance.service.mocked_databases = mock + + backups = instance.backups.all.to_a + + mock.verify + + backups.size.must_equal 5 + end + + it "paginates backups with all and page size" do + get_res = MockPagedEnumerable.new([first_page, last_page]) + mock = Minitest::Mock.new + mock.expect :list_backups, get_res, [instance_path(instance_id), nil, page_size: 3] + instance.service.mocked_databases = mock + + backups = instance.backups(page_size: 3).all.to_a + + mock.verify + + backups.size.must_equal 5 + end + + it "iterates backups with all using Enumerator" do + get_res = MockPagedEnumerable.new([first_page, last_page]) + mock = Minitest::Mock.new + mock.expect :list_backups, get_res, [instance_path(instance_id), nil, page_size: nil] + instance.service.mocked_databases = mock + + backups = instance.backups.all.take(5) + + mock.verify + + backups.size.must_equal 5 + end + + it "paginates backups with filter" do + get_res = MockPagedEnumerable.new([first_page]) + mock = Minitest::Mock.new + mock.expect :list_backups, get_res, [instance_path(instance_id), "name:db1", page_size: nil] + instance.service.mocked_databases = mock + + backups = instance.backups filter: "name:db1" + + mock.verify + + backups.size.must_equal 3 + end + + it "paginates backups with filter and page size" do + get_res = MockPagedEnumerable.new([first_page]) + mock = Minitest::Mock.new + mock.expect :list_backups, get_res, [instance_path(instance_id), "name:db1", page_size: 3] + instance.service.mocked_databases = mock + + backups = instance.backups filter: "name:db1", page_size: 3 + + mock.verify + + backups.size.must_equal 3 + end +end diff --git a/google-cloud-spanner/test/google/cloud/spanner/instance/create_database_test.rb b/google-cloud-spanner/test/google/cloud/spanner/instance/create_database_test.rb index 39d3aaac3214..a08161fa14db 100644 --- a/google-cloud-spanner/test/google/cloud/spanner/instance/create_database_test.rb +++ b/google-cloud-spanner/test/google/cloud/spanner/instance/create_database_test.rb @@ -18,7 +18,6 @@ let(:instance_id) { "my-instance-id" } let(:instance_grpc) { Google::Spanner::Admin::Instance::V1::Instance.new instance_hash(name: instance_id) } let(:instance) { Google::Cloud::Spanner::Instance.from_grpc instance_grpc, spanner.service } - let(:job_grpc) do Google::Longrunning::Operation.new( name: "1234567890", @@ -29,13 +28,11 @@ ) end let(:database_grpc) do - Google::Spanner::Admin::Database::V1::Database.new \ + Google::Spanner::Admin::Database::V1::Database.new( name: "projects/bustling-kayak-91516/instances/my-new-instance", - config: "projects/my-project/instanceConfigs/regional-us-central1", - display_name: "My New Instance", - node_count: 1, state: :READY, - labels: {} + create_time: Time.now + ) end let(:job_grpc_done) do Google::Longrunning::Operation.new( @@ -46,8 +43,8 @@ ), done: true, response: Google::Protobuf::Any.new( - type_url: "type.googleapis.com/google.spanner.admin.database.v1.database", - value: Google::Spanner::Admin::Database::V1::CreateDatabaseMetadata.new.to_proto + type_url: "type.googleapis.com/google.spanner.admin.database.v1.Database", + value: database_grpc.to_proto ) ) end diff --git a/google-cloud-spanner/test/google/cloud/spanner/instance/database_operations_test.rb b/google-cloud-spanner/test/google/cloud/spanner/instance/database_operations_test.rb new file mode 100644 index 000000000000..2d88159283ac --- /dev/null +++ b/google-cloud-spanner/test/google/cloud/spanner/instance/database_operations_test.rb @@ -0,0 +1,223 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +require "helper" + +describe Google::Cloud::Spanner::Instance, :database_operations, :mock_spanner do + let(:instance_id) { "my-instance-id" } + let(:instance_grpc) { Google::Spanner::Admin::Instance::V1::Instance.new instance_hash(name: instance_id) } + let(:instance) { Google::Cloud::Spanner::Instance.from_grpc instance_grpc, spanner.service } + let(:database_grpc) { Google::Spanner::Admin::Database::V1::Database.new database_hash } + let(:job_name) { "1234567890" } + let(:job_hash) do + { + name: job_name, + metadata: { + type_url: "type.googleapis.com/google.spanner.admin.database.v1.CreateDatabaseMetadata", + value: Google::Spanner::Admin::Database::V1::CreateDatabaseMetadata.new.to_proto + } + } + end + let(:job_grpc) { Google::Longrunning::Operation.new job_hash } + let(:job_grpc_done) do + Google::Longrunning::Operation.new( + name:"1234567890", + metadata: Google::Protobuf::Any.new( + type_url: "google.spanner.admin.database.v1.CreateDatabaseMetadata", + value: Google::Spanner::Admin::Database::V1::CreateDatabaseMetadata.new.to_proto + ), + done: true, + response: Google::Protobuf::Any.new( + type_url: "type.googleapis.com/google.spanner.admin.database.v1.Database", + value: database_grpc.to_proto + ) + ) + end + let(:jobs_hash) do + 3.times.map { job_hash } + end + let(:first_page) do + h = { operations: jobs_hash } + h[:next_page_token] = "next_page_token" + Google::Spanner::Admin::Database::V1::ListDatabaseOperationsResponse.new h + end + let(:second_page) do + h = { operations: jobs_hash } + h[:next_page_token] = "second_page_token" + Google::Spanner::Admin::Database::V1::ListDatabaseOperationsResponse.new h + end + let(:last_page) do + h = { operations: jobs_hash } + h[:operations].pop + Google::Spanner::Admin::Database::V1::ListDatabaseOperationsResponse.new h + end + + it "list database operations" do + list_res = MockPagedEnumerable.new([first_page]) + + mock = Minitest::Mock.new + mock.expect :list_database_operations, list_res, [instance_path(instance_id), nil, page_size: nil] + 3.times do + mock.expect :get_operation, job_grpc_done, [job_name, Hash] + end + mock.expect :instance_variable_get, mock, ["@operations_client"] + instance.service.mocked_databases = mock + + jobs = instance.database_operations + jobs.size.must_equal 3 + + jobs.each do |job| + job.must_be_kind_of Google::Cloud::Spanner::Database::Job + job.wont_be :done? + job.wont_be :error? + job.error.must_be :nil? + job.database.must_be :nil? + job.reload! + + database = job.database + database.wont_be :nil? + database.must_be_kind_of Google::Cloud::Spanner::Database + end + + mock.verify + end + + it "paginates database operations with page size" do + list_res = MockPagedEnumerable.new([first_page]) + + mock = Minitest::Mock.new + mock.expect :list_database_operations, list_res, [instance_path(instance_id), nil, page_size: 3] + 3.times do + mock.expect :get_operation, job_grpc_done, [job_name, Hash] + end + mock.expect :instance_variable_get, mock, ["@operations_client"] + instance.service.mocked_databases = mock + + jobs = instance.database_operations page_size: 3 + jobs.size.must_equal 3 + + jobs.each do |job| + job.must_be_kind_of Google::Cloud::Spanner::Database::Job + job.wont_be :done? + job.wont_be :error? + job.error.must_be :nil? + job.database.must_be :nil? + job.reload! + + database = job.database + database.wont_be :nil? + database.must_be_kind_of Google::Cloud::Spanner::Database + end + + mock.verify + end + + it "paginates database operations with next? and next" do + list_res = MockPagedEnumerable.new([first_page, last_page]) + mock = Minitest::Mock.new + mock.expect :list_database_operations, list_res, [instance_path(instance_id), nil, page_size: nil] + 2.times do + mock.expect :instance_variable_get, mock, ["@operations_client"] + end + instance.service.mocked_databases = mock + + jobs = instance.database_operations + + jobs.size.must_equal 3 + jobs.next?.must_equal true + jobs.next.size.must_equal 2 + jobs.next?.must_equal false + + mock.verify + end + + it "paginates database operations with all" do + list_res = MockPagedEnumerable.new([first_page, last_page]) + mock = Minitest::Mock.new + mock.expect :list_database_operations, list_res, [instance_path(instance_id), nil, page_size: nil] + 2.times do + mock.expect :instance_variable_get, mock, ["@operations_client"] + end + instance.service.mocked_databases = mock + + jobs = instance.database_operations.all.to_a + + mock.verify + + jobs.size.must_equal 5 + end + + it "paginates database operations with all and page size" do + list_res = MockPagedEnumerable.new([first_page, last_page]) + mock = Minitest::Mock.new + mock.expect :list_database_operations, list_res, [instance_path(instance_id), nil, page_size: 3] + 2.times do + mock.expect :instance_variable_get, mock, ["@operations_client"] + end + instance.service.mocked_databases = mock + + jobs = instance.database_operations(page_size: 3).all.to_a + + mock.verify + + jobs.size.must_equal 5 + end + + it "iterates database operations with all using Enumerator" do + list_res = MockPagedEnumerable.new([first_page, last_page]) + mock = Minitest::Mock.new + mock.expect :list_database_operations, list_res, [instance_path(instance_id), nil, page_size: nil] + 2.times do + mock.expect :instance_variable_get, mock, ["@operations_client"] + end + instance.service.mocked_databases = mock + + jobs = instance.database_operations.all.take(5) + + mock.verify + + jobs.size.must_equal 5 + end + + it "paginates database operations with filter" do + filter = 'metadata.@type:CreateDatabaseMetadata' + list_res = MockPagedEnumerable.new([first_page]) + mock = Minitest::Mock.new + mock.expect :list_database_operations, list_res, [instance_path(instance_id), filter, page_size: nil ] + mock.expect :instance_variable_get, mock, ["@operations_client"] + instance.service.mocked_databases = mock + + jobs = instance.database_operations filter: filter + + mock.verify + + jobs.size.must_equal 3 + end + + it "paginates database operations with filter and page size" do + filter = 'metadata.@type:CreateDatabaseMetadata' + list_res = MockPagedEnumerable.new([first_page]) + mock = Minitest::Mock.new + mock.expect :list_database_operations, list_res, [instance_path(instance_id), filter, page_size: 3 ] + mock.expect :instance_variable_get, mock, ["@operations_client"] + instance.service.mocked_databases = mock + + jobs = instance.database_operations filter: filter, page_size: 3 + + mock.verify + + jobs.size.must_equal 3 + end +end diff --git a/google-cloud-spanner/test/helper.rb b/google-cloud-spanner/test/helper.rb index f49db2f87e36..e58b81552cd3 100644 --- a/google-cloud-spanner/test/helper.rb +++ b/google-cloud-spanner/test/helper.rb @@ -113,13 +113,51 @@ def databases_hash instance_id: "my-instance-id" { databases: 3.times.map { database_hash(instance_id: instance_id) } } end - def database_hash instance_id: "my-instance-id", database_id: "database-#{rand(9999)}", state: "READY" + def database_hash instance_id: "my-instance-id", database_id: "database-#{rand(9999)}", + state: "READY", restore_info: {} { name: "projects/#{project}/instances/#{instance_id}/databases/#{database_id}", - state: state + state: state, + restore_info: restore_info + } + end + + def restore_info_hash source_type: "BACKUP", backup_info: {} + { + source_type: source_type, + backup_info: backup_info + } + end + + def backup_info_hash instance_id: "my-instance-id", backup_id: "my-backup-id", + create_time: Time.now, source_database_id: "my-backup-source-database-id" + { + backup: "projects/#{project}/instances/#{instance_id}/backups/#{backup_id}", + create_time: create_time, + source_database: "projects/#{project}/instances/#{instance_id}/databases/#{source_database_id}" + } + end + + def backup_hash instance_id: "my-instance-id", database_id: "database-#{rand(9999)}", + backup_id: "backup-#{rand(9999)}", state: "READY", expire_time: Time.now + 36000, + create_time: Time.now, size_bytes: 1024, referencing_databases: ["db1"] + { + name: "projects/#{project}/instances/#{instance_id}/backups/#{backup_id}", + database: "projects/#{project}/instances/#{instance_id}/databases/#{database_id}", + state: state, + expire_time: expire_time, + create_time: create_time, + size_bytes: size_bytes, + referencing_databases: referencing_databases.map do |database| + "projects/#{project}/instances/#{instance_id}/databases/#{database}" + end } end + def backups_hash instance_id: "my-instance-id" + { backups: 3.times.map { backup_hash instance_id: instance_id } } + end + def project_path Google::Cloud::Spanner::Admin::Instance::V1::InstanceAdminClient.project_path project end @@ -146,6 +184,11 @@ def session_path instance_id, database_id, session_id project, instance_id, database_id, session_id) end + def backup_path instance, backup + Google::Cloud::Spanner::Admin::Database::V1::DatabaseAdminClient.backup_path( + project, instance, backup) + end + def paged_enum_struct response OpenStruct.new page: OpenStruct.new(response: response) end @@ -189,3 +232,23 @@ def inspect "<#{self.class}>" end end + +class MockPagedEnumerable + def initialize responses = [] + @responses = responses + @current_index = 0 + end + + def response + @responses[@current_index] + end + + def next_page? + response.next_page_token != "" + end + + def next_page + @current_index += 1 + response + end +end