From 200b0adb388294d4ab1d06dfce956179374c68da Mon Sep 17 00:00:00 2001 From: Jim Park Date: Tue, 30 Aug 2022 23:13:58 -0700 Subject: [PATCH 01/13] refactor: use generator methods, and class instance variables to separate duties between classes --- .rubocop.yml | 7 + datadog_backup.gemspec | 1 + lib/datadog_backup.rb | 12 +- lib/datadog_backup/cli.rb | 120 +++---- lib/datadog_backup/client.rb | 47 +++ lib/datadog_backup/dashboards.rb | 43 +-- lib/datadog_backup/local_filesystem.rb | 87 ----- lib/datadog_backup/monitors.rb | 36 +- lib/datadog_backup/options.rb | 35 -- lib/datadog_backup/resources.rb | 200 +++++------- lib/datadog_backup/resources/class_methods.rb | 60 ++++ .../resources/local_filesystem.rb | 51 +++ .../local_filesystem/class_methods.rb | 65 ++++ lib/datadog_backup/synthetics.rb | 86 ++--- lib/datadog_backup/thread_pool.rb | 7 +- spec/datadog_backup/cli_spec.rb | 138 -------- spec/datadog_backup/client_spec.rb | 4 + spec/datadog_backup/core_spec.rb | 156 --------- spec/datadog_backup/dashboards_spec.rb | 206 +++++++----- spec/datadog_backup/local_filesystem_spec.rb | 188 ----------- spec/datadog_backup/monitors_spec.rb | 195 ++++++----- .../local_filesystem/class_methods_spec.rb | 146 +++++++++ .../resources/local_filesystem_spec.rb | 48 +++ spec/datadog_backup/synthetics_spec.rb | 307 ++++++------------ spec/factories/resources.rb | 1 + spec/spec_helper.rb | 31 +- 26 files changed, 1006 insertions(+), 1271 deletions(-) create mode 100644 lib/datadog_backup/client.rb delete mode 100644 lib/datadog_backup/local_filesystem.rb delete mode 100644 lib/datadog_backup/options.rb create mode 100644 lib/datadog_backup/resources/class_methods.rb create mode 100644 lib/datadog_backup/resources/local_filesystem.rb create mode 100644 lib/datadog_backup/resources/local_filesystem/class_methods.rb create mode 100644 spec/datadog_backup/client_spec.rb delete mode 100644 spec/datadog_backup/core_spec.rb delete mode 100644 spec/datadog_backup/local_filesystem_spec.rb create mode 100644 spec/datadog_backup/resources/local_filesystem/class_methods_spec.rb create mode 100644 spec/datadog_backup/resources/local_filesystem_spec.rb create mode 100644 spec/factories/resources.rb diff --git a/.rubocop.yml b/.rubocop.yml index ae0bde2..c667fa7 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -15,6 +15,7 @@ AllCops: TargetRubyVersion: 2.7 NewCops: enable + Layout/LineLength: Enabled: false @@ -34,4 +35,10 @@ RSpec/MultipleMemoizedHelpers: Enabled: false RSpec/ExampleLength: + Enabled: false + +Style/ClassVars: + Enabled: false + +Style/GlobalVars: Enabled: false \ No newline at end of file diff --git a/datadog_backup.gemspec b/datadog_backup.gemspec index 62f5b36..e48ffe3 100644 --- a/datadog_backup.gemspec +++ b/datadog_backup.gemspec @@ -29,6 +29,7 @@ Gem::Specification.new do |spec| spec.add_dependency 'faraday-retry' spec.add_development_dependency 'bundler' + spec.add_development_dependency 'factory_bot' spec.add_development_dependency 'guard-rspec' spec.add_development_dependency 'pry' spec.add_development_dependency 'pry-byebug' diff --git a/lib/datadog_backup.rb b/lib/datadog_backup.rb index b458aea..049113f 100644 --- a/lib/datadog_backup.rb +++ b/lib/datadog_backup.rb @@ -2,16 +2,22 @@ require 'concurrent' -require_relative 'datadog_backup/local_filesystem' -require_relative 'datadog_backup/options' require_relative 'datadog_backup/cli' +require_relative 'datadog_backup/client' +require_relative 'datadog_backup/thread_pool' + +require_relative 'datadog_backup/resources/local_filesystem/class_methods' +require_relative 'datadog_backup/resources/local_filesystem' +require_relative 'datadog_backup/resources/class_methods' require_relative 'datadog_backup/resources' + require_relative 'datadog_backup/dashboards' require_relative 'datadog_backup/monitors' require_relative 'datadog_backup/synthetics' -require_relative 'datadog_backup/thread_pool' + require_relative 'datadog_backup/version' require_relative 'datadog_backup/deprecations' + DatadogBackup::Deprecations.check # DatadogBackup is a gem for backing up and restoring Datadog monitors and dashboards. diff --git a/lib/datadog_backup/cli.rb b/lib/datadog_backup/cli.rb index d6a041a..3271fe3 100644 --- a/lib/datadog_backup/cli.rb +++ b/lib/datadog_backup/cli.rb @@ -6,46 +6,67 @@ module DatadogBackup # CLI is the command line interface for the datadog_backup gem. class Cli - include ::DatadogBackup::Options - - def all_diff_futures - LOGGER.info("Starting diffs on #{::DatadogBackup::ThreadPool::TPOOL.max_length} threads") - any_resource_instance - .all_file_ids_for_selected_resources - .map do |file_id| - Concurrent::Promises.future_on(::DatadogBackup::ThreadPool::TPOOL, file_id) do |fid| - [fid, getdiff(fid)] - end - end - end - - def any_resource_instance - resource_instances.first + def initialize(options) + $options = options end def backup - resource_instances.each(&:purge) - resource_instances.each(&:backup) + $options[:resources].each(&:purge) + $options[:resources].each(&:backup_all) any_resource_instance.all_files end - def definitive_resource_instance(id) - matching_resource_instance(any_resource_instance.class_from_id(id)) + def restore + futures = all_diff_futures + watcher = ::DatadogBackup::ThreadPool.watcher + + futures.each do |future| + id, diff = *future.value! + next if diff.nil? || diff.empty? + + if $options[:force_restore] + definitive_resource_instance(id).restore(id) + else + ask_to_restore(id, diff) + end + end + watcher.join if watcher.status end - def getdiff(id) - result = definitive_resource_instance(id).diff(id) - case result - when '---' || '' || "\n" || '
' - nil + def run! + case $options[:action] + when 'backup' + LOGGER.info('Starting backup.') + puts backup + when 'restore' + LOGGER.info('Starting restore.') + puts restore else - result + fatal 'No action specified.' + end + rescue SystemExit, Interrupt + ::DatadogBackup::ThreadPool.shutdown + end + + private + + ## + # The diff methods + + def all_diff_futures + LOGGER.info("Starting diffs on #{::DatadogBackup::ThreadPool::TPOOL.max_length} threads") + any_resource_instance + .all_file_ids_for_selected_resources + .map do |file_id| + Concurrent::Promises.future_on(::DatadogBackup::ThreadPool::TPOOL, file_id) do |fid| + [fid, getdiff(fid)] + end end end # rubocop:disable Style/StringConcatenation def format_diff_output(diff_output) - case diff_format + case $options[:diff_format] when nil, :color diff_output.map do |id, diff| " ---\n id: #{id}\n#{diff}" @@ -64,34 +85,19 @@ def format_diff_output(diff_output) end # rubocop:enable Style/StringConcatenation - def initialize(options) - @options = options - end - - def restore - futures = all_diff_futures - watcher = ::DatadogBackup::ThreadPool.watcher - - futures.each do |future| - id, diff = *future.value! - next if diff.nil? || diff.empty? - - if @options[:force_restore] - definitive_resource_instance(id).restore(id) - else - ask_to_restore(id, diff) - end + def getdiff(id) + LOGGER.debug("Searching for diff for #{id}.") + result = definitive_resource_instance(id).get_by_id(id).diff + case result + when '---' || '' || "\n" || '
' + nil + else + result end - watcher.join if watcher.status - end - - def run! - puts(send(action.to_sym)) - rescue SystemExit, Interrupt - ::DatadogBackup::ThreadPool.shutdown end - private + ## + # Interact with the user def ask_to_restore(id, diff) puts '--------------------------------------------------------------------------------' @@ -118,14 +124,14 @@ def ask_to_restore(id, diff) end end - def matching_resource_instance(klass) - resource_instances.select { |resource_instance| resource_instance.instance_of?(klass) }.first + ## + # Finding the right resource instance to use. + def any_resource_instance + $options[:resources].first end - def resource_instances - @resource_instances ||= resources.map do |resource| - resource.new(@options) - end + def definitive_resource_instance(id) + any_resource_instance.class_from_id(id) end end end diff --git a/lib/datadog_backup/client.rb b/lib/datadog_backup/client.rb new file mode 100644 index 0000000..5f8a1e9 --- /dev/null +++ b/lib/datadog_backup/client.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +module DatadogBackup + class Client + RETRY_OPTIONS = { + max: 5, + interval: 0.05, + interval_randomness: 0.5, + backoff_factor: 2 + }.freeze + + def initialize + @client = Faraday.new( + url: ENV.fetch('DD_SITE_URL', 'https://api.datadoghq.com/'), + headers: { + 'DD-API-KEY' => ENV.fetch('DD_API_KEY'), + 'DD-APPLICATION-KEY' => ENV.fetch('DD_APP_KEY') + } + ) do |faraday| + faraday.request :json + faraday.request :retry, RETRY_OPTIONS + faraday.response(:logger, LOGGER, { headers: true, bodies: LOGGER.debug?, log_level: :debug }) do |logger| + logger.filter(/(DD-API-KEY:)([^&]+)/, '\1[REDACTED]') + logger.filter(/(DD-APPLICATION-KEY:)([^&]+)/, '\1[REDACTED]') + end + faraday.response :raise_error + faraday.response :json + faraday.adapter Faraday.default_adapter + end + end + + def get_body(path, params = {}, headers = {}) + response = @client.get(path, params, headers) + response.body + end + + def post_body(path, body, headers = {}) + response = @client.post(path, body, headers) + response.body + end + + def put_body(path, body, headers = {}) + response = @client.put(path, body, headers) + response.body + end + end +end diff --git a/lib/datadog_backup/dashboards.rb b/lib/datadog_backup/dashboards.rb index ee12632..0a3c829 100644 --- a/lib/datadog_backup/dashboards.rb +++ b/lib/datadog_backup/dashboards.rb @@ -3,42 +3,11 @@ module DatadogBackup # Dashboards specific overrides for backup and restore. class Dashboards < Resources - def all - get_all.fetch('dashboards') - end - - def backup - LOGGER.info("Starting diffs on #{::DatadogBackup::ThreadPool::TPOOL.max_length} threads") - futures = all.map do |dashboard| - Concurrent::Promises.future_on(::DatadogBackup::ThreadPool::TPOOL, dashboard) do |board| - id = board[id_keyname] - get_and_write_file(id) - end - end - - watcher = ::DatadogBackup::ThreadPool.watcher - watcher.join if watcher.status - - Concurrent::Promises.zip(*futures).value! - end - - def initialize(options) - super(options) - @banlist = %w[modified_at url].freeze - end - - private - - def api_version - 'v1' - end - - def api_resource_name - 'dashboard' - end - - def id_keyname - 'id' - end + @api_version = 'v1' + @api_resource_name = 'dashboard' + @id_keyname = 'id' + @banlist = %w[modified_at url].freeze + @api_service = DatadogBackup::Client.new + @dig_in_list_body = 'dashboards' end end diff --git a/lib/datadog_backup/local_filesystem.rb b/lib/datadog_backup/local_filesystem.rb deleted file mode 100644 index 9411928..0000000 --- a/lib/datadog_backup/local_filesystem.rb +++ /dev/null @@ -1,87 +0,0 @@ -# frozen_string_literal: true - -require 'fileutils' -require 'json' -require 'yaml' -require 'deepsort' - -module DatadogBackup - ## - # Meant to be mixed into DatadogBackup::Resources - # Relies on @options[:backup_dir] and @options[:output_format] - module LocalFilesystem - def all_files - ::Dir.glob(::File.join(backup_dir, '**', '*')).select { |f| ::File.file?(f) } - end - - def all_file_ids - all_files.map { |file| ::File.basename(file, '.*') } - end - - def all_file_ids_for_selected_resources - all_file_ids.select do |id| - resources.include? class_from_id(id) - end - end - - def class_from_id(id) - class_string = ::File.dirname(find_file_by_id(id)).split('/').last.capitalize - ::DatadogBackup.const_get(class_string) - end - - def dump(object) - case output_format - when :json - JSON.pretty_generate(object.deep_sort) - when :yaml - YAML.dump(object.deep_sort) - else - raise 'invalid output_format specified or not specified' - end - end - - def filename(id) - ::File.join(mydir, "#{id}.#{output_format}") - end - - def file_type(filepath) - ::File.extname(filepath).strip.downcase[1..].to_sym - end - - def find_file_by_id(id) - ::Dir.glob(::File.join(backup_dir, '**', "#{id}.*")).first - end - - def load_from_file(string, output_format) - case output_format - when :json - JSON.parse(string) - when :yaml - YAML.safe_load(string) - else - raise 'invalid output_format specified or not specified' - end - end - - def load_from_file_by_id(id) - filepath = find_file_by_id(id) - load_from_file(::File.read(filepath), file_type(filepath)) - end - - def mydir - ::File.join(backup_dir, myclass) - end - - def purge - ::FileUtils.rm(::Dir.glob(File.join(mydir, '*'))) - end - - def write_file(data, filename) - LOGGER.info "Backing up #{filename}" - file = ::File.open(filename, 'w') - file.write(data) - ensure - file.close - end - end -end diff --git a/lib/datadog_backup/monitors.rb b/lib/datadog_backup/monitors.rb index b097b81..7b9e262 100644 --- a/lib/datadog_backup/monitors.rb +++ b/lib/datadog_backup/monitors.rb @@ -3,35 +3,11 @@ module DatadogBackup # Monitor specific overrides for backup and restore. class Monitors < Resources - def all - get_all - end - - def backup - all.map do |monitor| - id = monitor['id'] - write_file(dump(get_by_id(id)), filename(id)) - end - end - - def get_by_id(id) - monitor = all.select { |m| m['id'].to_s == id.to_s }.first - monitor.nil? ? {} : except(monitor) - end - - def initialize(options) - super(options) - @banlist = %w[overall_state overall_state_modified matching_downtimes modified].freeze - end - - private - - def api_version - 'v1' - end - - def api_resource_name - 'monitor' - end + @api_version = 'v1' + @api_resource_name = 'monitor' + @id_keyname = 'id' + @banlist = %w[id matching_downtimes modified overall_state overall_state_modified].freeze + @api_service = DatadogBackup::Client.new + @dig_in_list_body = nil end end diff --git a/lib/datadog_backup/options.rb b/lib/datadog_backup/options.rb deleted file mode 100644 index 6280da2..0000000 --- a/lib/datadog_backup/options.rb +++ /dev/null @@ -1,35 +0,0 @@ -# frozen_string_literal: true - -module DatadogBackup - # Describes what the user wants to see done. - module Options - def action - @options[:action] - end - - def backup_dir - @options[:backup_dir] - end - - def concurrency_limit - @options[:concurrency_limit] | 2 - end - - def diff_format - @options[:diff_format] - end - - # Either :json or :yaml - def output_format - @options[:output_format] - end - - def resources - @options[:resources] - end - - def force_restore - @options[:force_restore] - end - end -end diff --git a/lib/datadog_backup/resources.rb b/lib/datadog_backup/resources.rb index 3b00668..defd4ab 100644 --- a/lib/datadog_backup/resources.rb +++ b/lib/datadog_backup/resources.rb @@ -9,168 +9,126 @@ module DatadogBackup # The default options for backing up and restores. # This base class is meant to be extended by specific resources, such as Dashboards, Monitors, and so on. class Resources - include ::DatadogBackup::LocalFilesystem - include ::DatadogBackup::Options - - RETRY_OPTIONS = { - max: 5, - interval: 0.05, - interval_randomness: 0.5, - backoff_factor: 2 - }.freeze - - def backup - raise 'subclass is expected to implement #backup' - end + include LocalFilesystem + + ## + # Class methods and variables + extend ClassMethods + + @api_version = nil + @api_resource_name = nil + @id_keyname = nil + @banlist = %w[].freeze + @api_service = DatadogBackup::Client.new + @dig_in_list_body = nil # What keys do I need to traverse to get the list of resources? + + ## + # Instance methods + + attr_reader :id, :body, :api_version, :api_resource_name, :id_keyname, :banlist, :api_service # Returns the diffy diff. # Optionally, supply an array of keys to remove from comparison - def diff(id) - current = except(get_by_id(id)).deep_sort.to_yaml - filesystem = except(load_from_file_by_id(id)).deep_sort.to_yaml - result = ::Diffy::Diff.new(current, filesystem, include_plus_and_minus_in_html: true).to_s(diff_format) - LOGGER.debug("Compared ID #{id} and found filesystem: #{filesystem} <=> current: #{current} == result: #{result}") + def diff + current = @body.to_yaml + filesystem = body_from_backup.to_yaml + result = ::Diffy::Diff.new(current, filesystem, include_plus_and_minus_in_html: true).to_s($options[:diff_format]) + LOGGER.debug("Compared ID #{@id} and found filesystem: #{filesystem} <=> current: #{current} == result: #{result}") result.chomp end - # Returns a hash with banlist elements removed - def except(hash) - hash.tap do # tap returns self - @banlist.each do |key| - hash.delete(key) # delete returns the value at the deleted key, hence the tap wrapper - end + def dump + case $options[:output_format] + when :json + JSON.pretty_generate(sanitized_body) + when :yaml + YAML.dump(sanitized_body) + else + raise 'invalid output_format specified or not specified' end end - # Fetch the specified resource from Datadog - def get(id) - params = {} - headers = {} - response = api_service.get("/api/#{api_version}/#{api_resource_name}/#{id}", params, headers) - body_with_2xx(response) + def myclass + self.class.myclass end - # Returns a list of all resources in Datadog - # Do not use directly, but use the child classes' #all method instead - def get_all - return @get_all if @get_all - + # Fetch the resource from Datadog + def get(api_resource_name: @api_resource_name) params = {} headers = {} - response = api_service.get("/api/#{api_version}/#{api_resource_name}", params, headers) - @get_all = body_with_2xx(response) - end - - # Download the resource from Datadog and write it to a file - def get_and_write_file(id) - body = get_by_id(id) - write_file(dump(body), filename(id)) - body - end - - # Fetch the specified resource from Datadog and remove the banlist elements - def get_by_id(id) - except(get(id)) + body = @api_service.get_body("/api/#{@api_version}/#{api_resource_name}/#{@id}", params, headers) + @body = sanitize(body) end - def initialize(options) - @options = options - @banlist = [] - ::FileUtils.mkdir_p(mydir) - end - - def myclass - self.class.to_s.split(':').last.downcase - end - - # Create a new resource in Datadog - def create(body) + def create(api_resource_name: @api_resource_name) headers = {} - response = api_service.post("/api/#{api_version}/#{api_resource_name}", body, headers) - body = body_with_2xx(response) - LOGGER.warn "Successfully created #{body.fetch(id_keyname)} in datadog." - LOGGER.info 'Invalidating cache' - @get_all = nil + body = @api_service.post_body("/api/#{@api_version}/#{api_resource_name}", @body, headers) + @id = body[@id_keyname] + LOGGER.warn "Successfully created #{@id} in datadog." + self.class.invalidate_cache body end - # Update an existing resource in Datadog - def update(id, body) + def update(api_resource_name: @api_resource_name) headers = {} - response = api_service.put("/api/#{api_version}/#{api_resource_name}/#{id}", body, headers) - body = body_with_2xx(response) - LOGGER.warn "Successfully restored #{id} to datadog." - LOGGER.info 'Invalidating cache' - @get_all = nil + body = @api_service.put_body("/api/#{@api_version}/#{api_resource_name}/#{@id}", @body, headers) + LOGGER.warn "Successfully restored #{@id} to datadog." + self.class.invalidate_cache body end - # If the resource exists in Datadog, update it. Otherwise, create it. - def restore(id) - body = load_from_file_by_id(id) + def restore + @body = body_from_backup begin - update(id, body) + update rescue RuntimeError => e raise e.message unless e.message.include?('update failed with error 404') - create_newly(id, body) + create_newly + ensure + @body end end - # Return the Faraday body from a response with a 2xx status code, otherwise raise an error - def body_with_2xx(response) - unless response.status.to_s =~ /^2/ - raise "#{caller_locations(1, - 1)[0].label} failed with error #{response.status}" - end - - response.body - end - private - def api_url - ENV.fetch('DD_SITE_URL', 'https://api.datadoghq.com/') + # Create a new resource in Datadog, then move the old file to the new resource's ID + def create_newly + delete_backup + create + backup end - def api_version - raise 'subclass is expected to implement #api_version' + # Returns a hash with @banlist elements removed + def except(hash) + outhash = hash.dup + @banlist.each do |key| + outhash.delete(key) # delete returns the value at the deleted key, hence the tap wrapper + end + outhash end - def api_resource_name - raise 'subclass is expected to implement #api_resource_name' - end + # If the `id` is nil, then we can only #create from the `body`. + # If the `id` is not nil, then we can #update or #restore. + def initialize(api_version:, api_resource_name:, id_keyname:, banlist:, api_service:, id: nil, body: nil) + raise ArgumentError, 'id and body cannot both be nil' if id.nil? && body.nil? - # Some resources have a different key for the id. - def id_keyname - 'id' + @api_version = api_version + @api_resource_name = api_resource_name + @id_keyname = id_keyname + @banlist = banlist + @api_service = api_service + + @id = id + @body = body ? sanitize(body) : get end - def api_service - @api_service ||= Faraday.new( - url: api_url, - headers: { - 'DD-API-KEY' => ENV.fetch('DD_API_KEY'), - 'DD-APPLICATION-KEY' => ENV.fetch('DD_APP_KEY') - } - ) do |faraday| - faraday.request :json - faraday.request :retry, RETRY_OPTIONS - faraday.response(:logger, LOGGER, { headers: true, bodies: LOGGER.debug?, log_level: :debug }) do |logger| - logger.filter(/(DD-API-KEY:)([^&]+)/, '\1[REDACTED]') - logger.filter(/(DD-APPLICATION-KEY:)([^&]+)/, '\1[REDACTED]') - end - faraday.response :raise_error - faraday.response :json - faraday.adapter Faraday.default_adapter - end + def sanitize(body) + except(body.deep_sort) end - # Create a new resource in Datadog, then move the old file to the new resource's ID - def create_newly(file_id, body) - new_id = create(body).fetch(id_keyname) - FileUtils.rm(find_file_by_id(file_id)) - get_and_write_file(new_id) + def sanitized_body + sanitize(@body) end end end diff --git a/lib/datadog_backup/resources/class_methods.rb b/lib/datadog_backup/resources/class_methods.rb new file mode 100644 index 0000000..e3c9ace --- /dev/null +++ b/lib/datadog_backup/resources/class_methods.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +module DatadogBackup + class Resources + module ClassMethods + def new_resource(id: nil, body: nil) + raise ArgumentError, 'id and body cannot both be nil' if id.nil? && body.nil? + + new( + id: id, + body: body, + api_version: @api_version, + api_resource_name: @api_resource_name, + id_keyname: @id_keyname, + banlist: @banlist, + api_service: @api_service + ) + end + + def all + @all ||= get_all.map do |resource| + new_resource(id: resource.fetch(@id_keyname), body: resource) + end + LOGGER.info "Found #{@all.length} #{@api_resource_name}s in Datadog" + @all + end + + # Returns a list of all resources in Datadog + # Do not use directly, but use the child classes' #all method instead + def get_all + return @get_all if @get_all + + LOGGER.info("#{myclass}: Fetching all #{@api_resource_name} from Datadog") + + params = {} + headers = {} + body = @api_service.get_body("/api/#{@api_version}/#{@api_resource_name}", params, headers) + @get_all = @dig_in_list_body ? body.fetch(*@dig_in_list_body) : body + end + + # Fetch the specified resource from Datadog and remove the @banlist elements + def get_by_id(id) + all.find { |resource| resource.id == id } + end + + def backup_all + all.map(&:backup) + end + + def invalidate_cache + LOGGER.info 'Invalidating cache' + @get_all = nil + end + + def myclass + to_s.split(':').last.downcase + end + end + end +end diff --git a/lib/datadog_backup/resources/local_filesystem.rb b/lib/datadog_backup/resources/local_filesystem.rb new file mode 100644 index 0000000..f171418 --- /dev/null +++ b/lib/datadog_backup/resources/local_filesystem.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require 'fileutils' +require 'json' +require 'yaml' +require 'deepsort' + +module DatadogBackup + class Resources + ## + # Meant to be mixed into DatadogBackup::Resources + # Relies on $options[:backup_dir] and $options[:output_format] + module LocalFilesystem + def self.included(base) + base.extend(ClassMethods) + end + + # Write to backup file + def backup + ::FileUtils.mkdir_p(mydir) + write_file(dump, filename) + end + + def delete_backup + ::FileUtils.rm(filename) + end + + def body_from_backup + sanitize(self.class.load_from_file_by_id(@id)) + end + + def filename + ::File.join(mydir, "#{@id}.#{$options[:output_format]}") + end + + private + + def mydir + self.class.mydir + end + + def write_file(data, filename) + LOGGER.info "Backing up #{filename}" + file = ::File.open(filename, 'w') + file.write(data) + ensure + file.close + end + end + end +end diff --git a/lib/datadog_backup/resources/local_filesystem/class_methods.rb b/lib/datadog_backup/resources/local_filesystem/class_methods.rb new file mode 100644 index 0000000..0500609 --- /dev/null +++ b/lib/datadog_backup/resources/local_filesystem/class_methods.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +module DatadogBackup + class Resources + module LocalFilesystem + module ClassMethods + def all_files + ::Dir.glob(::File.join($options[:backup_dir], '**', '*')).select { |f| ::File.file?(f) } + end + + def all_file_ids + all_files.map { |file| ::File.basename(file, '.*') } + end + + def all_file_ids_for_selected_resources + all_file_ids.select do |id| + $options[:resources].include? class_from_id(id) + end + end + + def class_from_id(id) + class_string = ::File.dirname(find_file_by_id(id)).split('/').last.capitalize + ::DatadogBackup.const_get(class_string) + end + + def find_file_by_id(id) + idx = all_file_ids.index(id.to_s) + return all_files[idx] if idx + + raise "Could not find file for #{id}" + end + + def load_from_file_by_id(id) + filepath = find_file_by_id(id) + load_from_file(::File.read(filepath), file_type(filepath)) + end + + def mydir + ::File.join($options[:backup_dir], myclass) + end + + def purge + ::FileUtils.rm(::Dir.glob(File.join(mydir, '*'))) + end + + private + + def load_from_file(string, output_format) + case output_format + when :json + JSON.parse(string) + when :yaml + YAML.safe_load(string) + else + raise 'invalid output_format specified or not specified' + end + end + + def file_type(filepath) + ::File.extname(filepath).strip.downcase[1..].to_sym + end + end + end + end +end diff --git a/lib/datadog_backup/synthetics.rb b/lib/datadog_backup/synthetics.rb index 3e2ead8..ce53b1d 100644 --- a/lib/datadog_backup/synthetics.rb +++ b/lib/datadog_backup/synthetics.rb @@ -3,66 +3,42 @@ module DatadogBackup # Synthetic specific overrides for backup and restore. class Synthetics < Resources - def all - get_all.fetch('tests') - end - - def backup - all.map do |synthetic| - id = synthetic[id_keyname] - get_and_write_file(id) + @api_version = 'v1' + @api_resource_name = 'synthetics/tests' # used for list, but #instance_resource_name is used for get, create, update + @id_keyname = 'public_id' + @banlist = %w[creator created_at modified_at monitor_id public_id].freeze + @api_service = DatadogBackup::Client.new + @dig_in_list_body = 'tests' + + def instance_resource_name + return 'synthetics/tests/browser' if @body.fetch('type') == 'browser' + return 'synthetics/tests/api' if @body.fetch('type') == 'api' + end + + def get + if @body.nil? + begin + breakloop = false + super(api_resource_name: 'synthetics/tests/api') + rescue Faraday::ResourceNotFound + if breakloop + raise 'Could not find resource' + else + breakloop = true + super(api_resource_name: 'synthetics/tests/browser') + end + end + else + super(api_resource_name: instance_resource_name) end end - def get_by_id(id) - synthetic = all.select { |s| s[id_keyname].to_s == id.to_s }.first - synthetic.nil? ? {} : except(synthetic) - end - - def initialize(options) - super(options) - @banlist = %w[creator created_at modified_at monitor_id public_id].freeze - end - - def create(body) - create_api_resource_name = api_resource_name(body) - headers = {} - response = api_service.post("/api/#{api_version}/#{create_api_resource_name}", body, headers) - resbody = body_with_2xx(response) - LOGGER.warn "Successfully created #{resbody.fetch(id_keyname)} in datadog." - LOGGER.info 'Invalidating cache' - @get_all = nil - resbody - end - - def update(id, body) - update_api_resource_name = api_resource_name(body) - headers = {} - response = api_service.put("/api/#{api_version}/#{update_api_resource_name}/#{id}", body, headers) - resbody = body_with_2xx(response) - LOGGER.warn "Successfully restored #{id} to datadog." - LOGGER.info 'Invalidating cache' - @get_all = nil - resbody - end - - private - - def api_version - 'v1' - end - - def api_resource_name(body = nil) - return 'synthetics/tests' if body.nil? - return 'synthetics/tests' if body['type'].nil? - return 'synthetics/tests/browser' if body['type'].to_s == 'browser' - return 'synthetics/tests/api' if body['type'].to_s == 'api' - - raise "Unknown type #{body['type']}" + def create + super(api_resource_name: instance_resource_name) end - def id_keyname - 'public_id' + def update + super(api_resource_name: instance_resource_name) end end end diff --git a/lib/datadog_backup/thread_pool.rb b/lib/datadog_backup/thread_pool.rb index bb70860..ebb385c 100644 --- a/lib/datadog_backup/thread_pool.rb +++ b/lib/datadog_backup/thread_pool.rb @@ -3,9 +3,12 @@ module DatadogBackup # Used by CLI and Dashboards to size thread pool according to available CPU resourcess. module ThreadPool + min_threads = Concurrent.processor_count + max_threads = Concurrent.processor_count * 2 + TPOOL = ::Concurrent::ThreadPoolExecutor.new( - min_threads: [2, Concurrent.processor_count].max, - max_threads: [2, Concurrent.processor_count].max * 2, + min_threads: min_threads, + max_threads: max_threads, fallback_policy: :abort ) diff --git a/spec/datadog_backup/cli_spec.rb b/spec/datadog_backup/cli_spec.rb index f713a1f..b733171 100644 --- a/spec/datadog_backup/cli_spec.rb +++ b/spec/datadog_backup/cli_spec.rb @@ -3,142 +3,4 @@ require 'spec_helper' describe DatadogBackup::Cli do - let(:stubs) { Faraday::Adapter::Test::Stubs.new } - let(:api_client_double) { Faraday.new { |f| f.adapter :test, stubs } } - let(:tempdir) { Dir.mktmpdir } - let(:options) do - { - action: 'backup', - backup_dir: tempdir, - diff_format: nil, - output_format: :json, - resources: [DatadogBackup::Dashboards] - } - end - let(:cli) { described_class.new(options) } - let(:dashboards) do - dashboards = DatadogBackup::Dashboards.new(options) - allow(dashboards).to receive(:api_service).and_return(api_client_double) - return dashboards - end - - before do - allow(cli).to receive(:resource_instances).and_return([dashboards]) - end - - describe '#backup' do - context 'when dashboards are deleted in datadog' do - let(:all_dashboards) do - respond_with200( - { - 'dashboards' => [ - { 'id' => 'stillthere' }, - { 'id' => 'alsostillthere' } - ] - } - ) - end - - before do - dashboards.write_file('{"text": "diff"}', "#{tempdir}/dashboards/stillthere.json") - dashboards.write_file('{"text": "diff"}', "#{tempdir}/dashboards/alsostillthere.json") - dashboards.write_file('{"text": "diff"}', "#{tempdir}/dashboards/deleted.json") - - stubs.get('/api/v1/dashboard') { all_dashboards } - stubs.get('/api/v1/dashboard/stillthere') { respond_with200({}) } - stubs.get('/api/v1/dashboard/alsostillthere') { respond_with200({}) } - end - - it 'deletes the file locally as well' do - cli.backup - expect { File.open("#{tempdir}/dashboards/deleted.json", 'r') }.to raise_error(Errno::ENOENT) - end - end - end - - describe '#restore' do - subject(:restore) { cli.restore } - let(:stdin) { class_double('STDIN') } - - after(:all) do - $stdin = STDIN - end - - before do - $stdin = stdin - dashboards.write_file('{"text": "diff"}', "#{tempdir}/dashboards/diffs1.json") - allow(dashboards).to receive(:get_by_id).and_return({ 'text' => 'diff2' }) - allow(dashboards).to receive(:write_file) - allow(dashboards).to receive(:update) - end - - example 'starts interactive restore' do - allow(stdin).to receive(:gets).and_return('q') - - expect { restore }.to( - output(/\(r\)estore to Datadog, overwrite local changes and \(d\)ownload, \(s\)kip, or \(q\)uit\?/).to_stdout - .and(raise_error(SystemExit)) - ) - end - - context 'when the user chooses to restore' do - before do - allow(stdin).to receive(:gets).and_return('r') - end - - example 'it restores from disk to server' do - restore - expect(dashboards).to have_received(:update).with('diffs1', { 'text' => 'diff' }) - end - end - - context 'when the user chooses to download' do - before do - allow(stdin).to receive(:gets).and_return('d') - end - - example 'it writes from server to disk' do - restore - expect(dashboards).to have_received(:write_file).with(%({\n "text": "diff2"\n}), "#{tempdir}/dashboards/diffs1.json") - end - end - - context 'when the user chooses to skip' do - before do - allow(stdin).to receive(:gets).and_return('s') - end - - example 'it does not write to disk' do - restore - expect(dashboards).not_to have_received(:write_file) - end - - example 'it does not update the server' do - restore - expect(dashboards).not_to have_received(:update) - end - end - - context 'when the user chooses to quit' do - before do - allow(stdin).to receive(:gets).and_return('q') - end - - example 'it exits' do - expect { restore }.to raise_error(SystemExit) - end - - example 'it does not write to disk' do - restore - rescue SystemExit - expect(dashboards).not_to have_received(:write_file) - end - - example 'it does not update the server' do - restore - rescue SystemExit - expect(dashboards).not_to have_received(:update) - end - end - end end diff --git a/spec/datadog_backup/client_spec.rb b/spec/datadog_backup/client_spec.rb new file mode 100644 index 0000000..de54438 --- /dev/null +++ b/spec/datadog_backup/client_spec.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +describe DatadogBackup::Client do +end diff --git a/spec/datadog_backup/core_spec.rb b/spec/datadog_backup/core_spec.rb deleted file mode 100644 index f79f213..0000000 --- a/spec/datadog_backup/core_spec.rb +++ /dev/null @@ -1,156 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -describe DatadogBackup::Resources do - let(:stubs) { Faraday::Adapter::Test::Stubs.new } - let(:api_client_double) { Faraday.new { |f| f.adapter :test, stubs } } - let(:tempdir) { Dir.mktmpdir } - let(:resources) do - resources = described_class.new( - action: 'backup', - backup_dir: tempdir, - diff_format: nil, - resources: [], - output_format: :json - ) - allow(resources).to receive(:api_service).and_return(api_client_double) - return resources - end - - describe '#diff' do - subject(:diff) { resources.diff('diff') } - - before do - allow(resources).to receive(:get_by_id).and_return({ 'text' => 'diff1', 'extra' => 'diff1' }) - resources.write_file('{"text": "diff2", "extra": "diff2"}', "#{tempdir}/resources/diff.json") - end - - it { - expect(diff).to eq(<<~EODIFF - --- - -extra: diff1 - -text: diff1 - +extra: diff2 - +text: diff2 - EODIFF - .chomp) - } - end - - describe '#except' do - subject { resources.except({ a: :b, b: :c }) } - - it { is_expected.to eq({ a: :b, b: :c }) } - end - - describe '#initialize' do - subject(:myresources) { resources } - - it 'makes the subdirectories' do - fileutils = class_double(FileUtils).as_stubbed_const - allow(fileutils).to receive(:mkdir_p) - myresources - expect(fileutils).to have_received(:mkdir_p).with("#{tempdir}/resources") - end - end - - describe '#myclass' do - subject { resources.myclass } - - it { is_expected.to eq 'resources' } - end - - describe '#create' do - subject(:create) { resources.create({ 'a' => 'b' }) } - - example 'it will post /api/v1/dashboard' do - allow(resources).to receive(:api_version).and_return('v1') - allow(resources).to receive(:api_resource_name).and_return('dashboard') - stubs.post('/api/v1/dashboard', { 'a' => 'b' }) { respond_with200({ 'id' => 'whatever-id-abc' }) } - create - stubs.verify_stubbed_calls - end - end - - describe '#update' do - subject(:update) { resources.update('abc-123-def', { 'a' => 'b' }) } - - example 'it puts /api/v1/dashboard' do - allow(resources).to receive(:api_version).and_return('v1') - allow(resources).to receive(:api_resource_name).and_return('dashboard') - stubs.put('/api/v1/dashboard/abc-123-def', { 'a' => 'b' }) { respond_with200({ 'id' => 'whatever-id-abc' }) } - update - stubs.verify_stubbed_calls - end - - context 'when the id is not found' do - before do - allow(resources).to receive(:api_version).and_return('v1') - allow(resources).to receive(:api_resource_name).and_return('dashboard') - stubs.put('/api/v1/dashboard/abc-123-def', { 'a' => 'b' }) { [404, {}, { 'id' => 'whatever-id-abc' }] } - end - - it 'raises an error' do - expect { update }.to raise_error(RuntimeError, 'update failed with error 404') - end - end - end - - describe '#restore' do - before do - allow(resources).to receive(:api_version).and_return('api-version-string') - allow(resources).to receive(:api_resource_name).and_return('api-resource-name-string') - stubs.get('/api/api-version-string/api-resource-name-string/abc-123-def') { respond_with200({ 'test' => 'ok' }) } - stubs.get('/api/api-version-string/api-resource-name-string/bad-123-id') do - [404, {}, { 'error' => 'blahblah_not_found' }] - end - allow(resources).to receive(:load_from_file_by_id).and_return({ 'load' => 'ok' }) - end - - context 'when id exists' do - subject(:restore) { resources.restore('abc-123-def') } - - example 'it calls out to update' do - allow(resources).to receive(:update) - restore - expect(resources).to have_received(:update).with('abc-123-def', { 'load' => 'ok' }) - end - end - - context 'when id does not exist on remote' do - subject(:restore_newly) { resources.restore('bad-123-id') } - - let(:fileutils) { class_double(FileUtils).as_stubbed_const } - - before do - allow(resources).to receive(:load_from_file_by_id).and_return({ 'load' => 'ok' }) - stubs.put('/api/api-version-string/api-resource-name-string/bad-123-id') do - [404, {}, { 'error' => 'id not found' }] - end - stubs.post('/api/api-version-string/api-resource-name-string', { 'load' => 'ok' }) do - respond_with200({ 'id' => 'my-new-id' }) - end - allow(fileutils).to receive(:rm) - allow(resources).to receive(:create).with({ 'load' => 'ok' }).and_return({ 'id' => 'my-new-id' }) - allow(resources).to receive(:get_and_write_file) - allow(resources).to receive(:find_file_by_id).with('bad-123-id').and_return('/path/to/bad-123-id.json') - end - - example 'it calls out to create' do - restore_newly - expect(resources).to have_received(:create).with({ 'load' => 'ok' }) - end - - example 'it saves the new file' do - restore_newly - expect(resources).to have_received(:get_and_write_file).with('my-new-id') - end - - example 'it deletes the old file' do - restore_newly - expect(fileutils).to have_received(:rm).with('/path/to/bad-123-id.json') - end - end - end -end diff --git a/spec/datadog_backup/dashboards_spec.rb b/spec/datadog_backup/dashboards_spec.rb index 8a8fe5d..6ce8ba2 100644 --- a/spec/datadog_backup/dashboards_spec.rb +++ b/spec/datadog_backup/dashboards_spec.rb @@ -3,103 +3,143 @@ require 'spec_helper' describe DatadogBackup::Dashboards do - let(:stubs) { Faraday::Adapter::Test::Stubs.new } - let(:api_client_double) { Faraday.new { |f| f.adapter :test, stubs } } - let(:tempdir) { Dir.mktmpdir } - let(:dashboards) do - dashboards = described_class.new( - action: 'backup', - backup_dir: tempdir, - output_format: :json, - resources: [] - ) - allow(dashboards).to receive(:api_service).and_return(api_client_double) - return dashboards - end - let(:dashboard_description) do - { - 'description' => 'bar', - 'id' => 'abc-123-def', - 'title' => 'foo' - } - end - let(:board_abc_123_def) do - { - 'graphs' => [ - { - 'definition' => { - 'viz' => 'timeseries', - 'requests' => [ - { - 'q' => 'min:foo.bar{a:b}', - 'stacked' => false - } - ] - }, - 'title' => 'example graph' - } - ], - 'description' => 'example dashboard', - 'title' => 'example dashboard' - } - end - let(:all_dashboards) { respond_with200({ 'dashboards' => [dashboard_description] }) } - let(:example_dashboard) { respond_with200(board_abc_123_def) } - before do - stubs.get('/api/v1/dashboard') { all_dashboards } - stubs.get('/api/v1/dashboard/abc-123-def') { example_dashboard } + allow_any_instance_of(DatadogBackup::Client).to receive(:get_body) + .with('/api/v1/dashboard', {}, {}) + .and_return({ 'dashboards' => [{ 'id' => 'abc-123-def' }] }) + + allow_any_instance_of(DatadogBackup::Client).to receive(:get_body) + .with('/api/v1/dashboard/abc-123-def', {}, {}) + .and_return({ 'id' => 'abc-123-def' }) end - describe '#backup' do - subject { dashboards.backup } + describe 'Class Methods' do + describe '.new_resource' do + context 'with id and body' do + subject { described_class.new_resource(id: 'abc-123-def', body: { id: 'abc-123-def' }) } + + it { is_expected.to be_a(described_class) } + end + + context 'with id and no body' do + subject { described_class.new_resource(id: 'abc-123-def') } + + it { is_expected.to be_a(described_class) } + end + + context 'with no id and with body' do + subject { described_class.new_resource(body: { id: 'abc-123-def' }) } + + it { is_expected.to be_a(described_class) } + end - it 'is expected to create a file' do - file = instance_double(File) - allow(File).to receive(:open).with(dashboards.filename('abc-123-def'), 'w').and_return(file) - allow(file).to receive(:write) - allow(file).to receive(:close) + context 'with no id and no body' do + subject { proc { described_class.new_resource } } - dashboards.backup - expect(file).to have_received(:write).with(::JSON.pretty_generate(board_abc_123_def.deep_sort)) + it { is_expected.to raise_error(ArgumentError) } + end end - end - describe '#filename' do - subject { dashboards.filename('abc-123-def') } + describe '.all' do + subject { described_class.all } - it { is_expected.to eq("#{tempdir}/dashboards/abc-123-def.json") } - end + it { is_expected.to be_a(Array) } + it { is_expected.to all(be_a(described_class)) } + end - describe '#get_by_id' do - subject { dashboards.get_by_id('abc-123-def') } + describe '.get_all' do + subject { described_class.get_all } - it { is_expected.to eq board_abc_123_def } - end + it { is_expected.to eq([{ 'id' => 'abc-123-def' }]) } + end - describe '#diff' do - it 'calls the api only once' do - dashboards.write_file('{"a":"b"}', dashboards.filename('abc-123-def')) - expect(dashboards.diff('abc-123-def')).to eq(<<~EODASH - --- - -description: example dashboard - -graphs: - -- definition: - - requests: - - - q: min:foo.bar{a:b} - - stacked: false - - viz: timeseries - - title: example graph - -title: example dashboard - +a: b - EODASH - .chomp) + describe '.get_by_id' do + subject { described_class.get_by_id('abc-123-def').id } + + it { is_expected.to eq('abc-123-def') } + end + + describe '.myclass' do + subject { described_class.myclass } + + it { is_expected.to eq('dashboards') } end end - describe '#except' do - subject { dashboards.except({ :a => :b, 'modified_at' => :c, 'url' => :d }) } + describe 'Instance Methods' do + subject(:abc) { described_class.new_resource(id: 'abc-123-def') } + + describe '#diff' do + subject(:diff) { abc.diff } - it { is_expected.to eq({ a: :b }) } + before do + allow(abc).to receive(:body_from_backup) + .and_return({ 'id' => 'abc-123-def', 'title' => 'abc' }) + $options[:diff_format] = 'text' + end + + it { + expect(diff).to eq(<<~EODIFF + --- + id: abc-123-def + +title: abc + EODIFF + .chomp) + } + end + + describe '#dump' do + subject(:dump) { abc.dump } + + context 'when mode is :json' do + before do + $options[:output_format] = :json + end + + it { is_expected.to eq(%({\n "id": "abc-123-def"\n})) } + end + + context 'when mode is :yaml' do + before do + $options[:output_format] = :yaml + end + + it { is_expected.to eq(%(---\nid: abc-123-def\n)) } + end + end + + describe '#myclass' do + subject { abc.myclass } + + it { is_expected.to eq('dashboards') } + end + + describe '#get' do + subject(:get) { abc.get } + + it { is_expected.to eq('id' => 'abc-123-def') } + end + + describe '#create' do + subject(:create) { abc.create } + + it 'posts to the API' do + expect_any_instance_of(DatadogBackup::Client).to receive(:post_body) + .with('/api/v1/dashboard', { 'id' => 'abc-123-def' }, {}) + .and_return({ 'id' => 'abc-123-def' }) + create + end + end + + describe '#update' do + subject(:update) { abc.update } + + it 'posts to the API' do + expect_any_instance_of(DatadogBackup::Client).to receive(:put_body) + .with('/api/v1/dashboard/abc-123-def', { 'id' => 'abc-123-def' }, {}) + .and_return({ 'id' => 'abc-123-def' }) + update + end + end end end diff --git a/spec/datadog_backup/local_filesystem_spec.rb b/spec/datadog_backup/local_filesystem_spec.rb deleted file mode 100644 index 01e9ebe..0000000 --- a/spec/datadog_backup/local_filesystem_spec.rb +++ /dev/null @@ -1,188 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -describe DatadogBackup::LocalFilesystem do - let(:tempdir) { Dir.mktmpdir } - let(:resources) do - DatadogBackup::Resources.new( - action: 'backup', - backup_dir: tempdir, - resources: [DatadogBackup::Dashboards], - output_format: :json - ) - end - let(:resources_yaml) do - DatadogBackup::Resources.new( - action: 'backup', - backup_dir: tempdir, - resources: [], - output_format: :yaml - ) - end - - describe '#all_files' do - subject { resources.all_files } - - before do - File.new("#{tempdir}/all_files.json", 'w') - end - - after do - FileUtils.rm "#{tempdir}/all_files.json" - end - - it { is_expected.to eq(["#{tempdir}/all_files.json"]) } - end - - describe '#all_file_ids_for_selected_resources' do - subject { resources.all_file_ids_for_selected_resources } - - before do - Dir.mkdir("#{tempdir}/dashboards") - Dir.mkdir("#{tempdir}/monitors") - File.new("#{tempdir}/dashboards/all_files.json", 'w') - File.new("#{tempdir}/monitors/12345.json", 'w') - end - - after do - FileUtils.rm "#{tempdir}/dashboards/all_files.json" - FileUtils.rm "#{tempdir}/monitors/12345.json" - end - - it { is_expected.to eq(['all_files']) } - end - - describe '#class_from_id' do - subject { resources.class_from_id('abc-123-def') } - - before do - resources.write_file('abc', "#{tempdir}/resources/abc-123-def.json") - end - - after do - FileUtils.rm "#{tempdir}/resources/abc-123-def.json" - end - - it { is_expected.to eq DatadogBackup::Resources } - end - - describe '#dump' do - context 'when mode is :json' do - subject { resources.dump({ a: :b }) } - - it { is_expected.to eq(%({\n "a": "b"\n})) } - end - - context 'when mode is :yaml' do - subject { resources_yaml.dump({ 'a' => 'b' }) } - - it { is_expected.to eq(%(---\na: b\n)) } - end - end - - describe '#filename' do - context 'when mode is :json' do - subject { resources.filename('abc-123-def') } - - it { is_expected.to eq("#{tempdir}/resources/abc-123-def.json") } - end - - context 'when mode is :yaml' do - subject { resources_yaml.filename('abc-123-def') } - - it { is_expected.to eq("#{tempdir}/resources/abc-123-def.yaml") } - end - end - - describe '#file_type' do - subject { resources.file_type("#{tempdir}/file_type.json") } - - before do - File.new("#{tempdir}/file_type.json", 'w') - end - - after do - FileUtils.rm "#{tempdir}/file_type.json" - end - - it { is_expected.to eq :json } - end - - describe '#find_file_by_id' do - subject { resources.find_file_by_id('find_file') } - - before do - File.new("#{tempdir}/find_file.json", 'w') - end - - after do - FileUtils.rm "#{tempdir}/find_file.json" - end - - it { is_expected.to eq "#{tempdir}/find_file.json" } - end - - describe '#load_from_file' do - context 'when mode is :json' do - subject { resources.load_from_file(%({\n "a": "b"\n}), :json) } - - it { is_expected.to eq('a' => 'b') } - end - - context 'when mode is :yaml' do - subject { resources.load_from_file(%(---\na: b\n), :yaml) } - - it { is_expected.to eq('a' => 'b') } - end - end - - describe '#load_from_file_by_id' do - context 'when the backup is in json but the mode is :yaml' do - subject { resources_yaml.load_from_file_by_id('abc-123-def') } - - before { resources.write_file(%({"a": "b"}), "#{tempdir}/resources/abc-123-def.json") } - - after { FileUtils.rm "#{tempdir}/resources/abc-123-def.json" } - - it { is_expected.to eq('a' => 'b') } - end - - context 'when the backup is in yaml but the mode is :json' do - subject { resources.load_from_file_by_id('abc-123-def') } - - before { resources.write_file(%(---\na: b), "#{tempdir}/resources/abc-123-def.yaml") } - - after { FileUtils.rm "#{tempdir}/resources/abc-123-def.yaml" } - - it { is_expected.to eq('a' => 'b') } - end - - context 'with Integer as parameter' do - subject { resources.load_from_file_by_id(12_345) } - - before { resources.write_file(%(---\na: b), "#{tempdir}/resources/12345.yaml") } - - after { FileUtils.rm "#{tempdir}/resources/12345.yaml" } - - it { is_expected.to eq('a' => 'b') } - end - end - - describe '#write_file' do - subject(:write_file) { resources.write_file('abc123', "#{tempdir}/resources/abc-123-def.json") } - - let(:file_like_object) { instance_double(File) } - - it 'writes a file to abc-123-def.json' do - allow(File).to receive(:open).and_call_original - allow(File).to receive(:open).with("#{tempdir}/resources/abc-123-def.json", 'w').and_return(file_like_object) - allow(file_like_object).to receive(:write) - allow(file_like_object).to receive(:close) - - write_file - - expect(file_like_object).to have_received(:write).with('abc123') - end - end -end diff --git a/spec/datadog_backup/monitors_spec.rb b/spec/datadog_backup/monitors_spec.rb index 090e057..05d070a 100644 --- a/spec/datadog_backup/monitors_spec.rb +++ b/spec/datadog_backup/monitors_spec.rb @@ -3,106 +3,143 @@ require 'spec_helper' describe DatadogBackup::Monitors do - let(:stubs) { Faraday::Adapter::Test::Stubs.new } - let(:api_client_double) { Faraday.new { |f| f.adapter :test, stubs } } - let(:tempdir) { Dir.mktmpdir } - let(:monitors) do - monitors = described_class.new( - action: 'backup', - backup_dir: tempdir, - output_format: :json, - resources: [] - ) - allow(monitors).to receive(:api_service).and_return(api_client_double) - return monitors - end - let(:monitor_description) do - { - 'query' => 'bar', - 'message' => 'foo', - 'id' => 123_455, - 'name' => 'foo', - 'overall_state' => 'OK', - 'overall_state_modified' => '2020-07-27T22:00:00+00:00' - } - end - let(:clean_monitor_description) do - { - 'id' => 123_455, - 'message' => 'foo', - 'name' => 'foo', - 'query' => 'bar' - } - end - let(:all_monitors) { respond_with200([monitor_description]) } - let(:example_monitor) { respond_with200(monitor_description) } - before do - stubs.get('/api/v1/monitor') { all_monitors } - stubs.get('/api/v1/dashboard/123455') { example_monitor } + allow_any_instance_of(DatadogBackup::Client).to receive(:get_body) + .with('/api/v1/monitor', {}, {}) + .and_return([{ 'id' => 'abc-123-def' }]) + + allow_any_instance_of(DatadogBackup::Client).to receive(:get_body) + .with('/api/v1/monitor/abc-123-def', {}, {}) + .and_return({ 'id' => 'abc-123-def', 'name' => 'Test Monitor' }) end - describe '#get_all' do - subject { monitors.get_all } + describe 'Class Methods' do + describe '.new_resource' do + context 'with id and body' do + subject { described_class.new_resource(id: 'abc-123-def', body: { id: 'abc-123-def' }) } - it { is_expected.to eq [monitor_description] } - end + it { is_expected.to be_a(described_class) } + end + + context 'with id and no body' do + subject { described_class.new_resource(id: 'abc-123-def') } + + it { is_expected.to be_a(described_class) } + end + + context 'with no id and with body' do + subject { described_class.new_resource(body: { id: 'abc-123-def' }) } + + it { is_expected.to be_a(described_class) } + end + + context 'with no id and no body' do + subject { proc { described_class.new_resource } } + + it { is_expected.to raise_error(ArgumentError) } + end + end + + describe '.all' do + subject { described_class.all } + + it { is_expected.to be_a(Array) } + it { is_expected.to all(be_a(described_class)) } + end + + describe '.get_all' do + subject { described_class.get_all } - describe '#backup' do - subject { monitors.backup } + it { is_expected.to eq([{ 'id' => 'abc-123-def' }]) } + end + + describe '.get_by_id' do + subject { described_class.get_by_id('abc-123-def').id } + + it { is_expected.to eq('abc-123-def') } + end - it 'is expected to create a file' do - file = instance_double(File) - allow(File).to receive(:open).with(monitors.filename(123_455), 'w').and_return(file) - allow(file).to receive(:write) - allow(file).to receive(:close) + describe '.myclass' do + subject { described_class.myclass } - monitors.backup - expect(file).to have_received(:write).with(::JSON.pretty_generate(clean_monitor_description)) + it { is_expected.to eq('monitors') } end end - describe '#diff and #except' do - example 'it ignores `overall_state` and `overall_state_modified`' do - monitors.write_file(monitors.dump(monitor_description), monitors.filename(123_455)) - stubs.get('/api/v1/dashboard/123455') do - respond_with200( - [ - { - 'query' => 'bar', - 'message' => 'foo', - 'id' => 123_455, - 'name' => 'foo', - 'overall_state' => 'ZZZZZZZZZZZZZZZZZZZZZZZZZZZ', - 'overall_state_modified' => '9999-07-27T22:55:55+00:00' - } - ] - ) + describe 'Instance Methods' do + subject(:abc) { described_class.new_resource(id: 'abc-123-def') } + + describe '#diff' do + subject(:diff) { abc.diff } + + before do + allow(abc).to receive(:body_from_backup) + .and_return({ 'name' => 'Local Copy' }) + $options[:diff_format] = 'text' end - expect(monitors.diff(123_455)).to eq '' + it { + expect(diff).to eq(<<~EODIFF + --- + -name: Test Monitor + +name: Local Copy + EODIFF + .chomp) + } + end + + describe '#dump' do + subject(:dump) { abc.dump } + + context 'when mode is :json' do + before do + $options[:output_format] = :json + end + + it { is_expected.to eq(%({\n "name": "Test Monitor"\n})) } + end - FileUtils.rm monitors.filename(123_455) + context 'when mode is :yaml' do + before do + $options[:output_format] = :yaml + end + + it { is_expected.to eq(%(---\nname: Test Monitor\n)) } + end end - end - describe '#filename' do - subject { monitors.filename(123_455) } + describe '#myclass' do + subject { abc.myclass } - it { is_expected.to eq("#{tempdir}/monitors/123455.json") } - end + it { is_expected.to eq('monitors') } + end - describe '#get_by_id' do - context 'when Integer' do - subject { monitors.get_by_id(123_455) } + describe '#get' do + subject(:get) { abc.get } - it { is_expected.to eq monitor_description } + it { is_expected.to eq('name' => 'Test Monitor') } end - context 'when String' do - subject { monitors.get_by_id('123455') } + describe '#create' do + subject(:create) { abc.create } - it { is_expected.to eq monitor_description } + it 'posts to the API' do + expect_any_instance_of(DatadogBackup::Client).to receive(:post_body) + .with('/api/v1/monitor', { 'name' => 'Test Monitor' }, {}) + .and_return({ 'id' => 'abc-999-def' }) + create + end + end + + describe '#update' do + subject(:update) { abc.update } + + it 'posts to the API' do + expect_any_instance_of(DatadogBackup::Client).to receive(:put_body) + .with('/api/v1/monitor/abc-123-def', { 'name' => 'Test Monitor' }, {}) + .and_return({ 'id' => 'abc-123-def' }) + update + end end end end diff --git a/spec/datadog_backup/resources/local_filesystem/class_methods_spec.rb b/spec/datadog_backup/resources/local_filesystem/class_methods_spec.rb new file mode 100644 index 0000000..ab1c9cc --- /dev/null +++ b/spec/datadog_backup/resources/local_filesystem/class_methods_spec.rb @@ -0,0 +1,146 @@ +# frozen_string_literal: true + +describe 'DatadogBackup' do + describe 'Resources' do + describe 'LocalFilesystem' do + describe 'ClassMethods' do + let(:resources) do + DatadogBackup::Resources + end + + before(:context) do + FileUtils.mkdir_p("#{$options[:backup_dir]}/dashboards") + FileUtils.mkdir_p("#{$options[:backup_dir]}/monitors") + FileUtils.mkdir_p("#{$options[:backup_dir]}/synthetics") + File.write("#{$options[:backup_dir]}/dashboards/abc-123-def.json", '{"id": "abc-123-def", "file_type": "json"}') + File.write("#{$options[:backup_dir]}/dashboards/ghi-456-jkl.yaml", "---\nid: ghi-456-jkl\nfile_type: yaml\n") + File.write("#{$options[:backup_dir]}/monitors/12345.json", '{"id":12345, "file_type": "json"}') + File.write("#{$options[:backup_dir]}/monitors/67890.yaml", "---\nid: 67890\nfile_type: yaml\n") + end + + after(:context) do + FileUtils.rm "#{$options[:backup_dir]}/dashboards/abc-123-def.json" + FileUtils.rm "#{$options[:backup_dir]}/dashboards/ghi-456-jkl.yaml" + FileUtils.rm "#{$options[:backup_dir]}/monitors/12345.json" + FileUtils.rm "#{$options[:backup_dir]}/monitors/67890.yaml" + end + + describe '.all_files' do + subject { resources.all_files } + + it { + expect(subject).to contain_exactly( + "#{$options[:backup_dir]}/dashboards/abc-123-def.json", + "#{$options[:backup_dir]}/dashboards/ghi-456-jkl.yaml", + "#{$options[:backup_dir]}/monitors/12345.json", + "#{$options[:backup_dir]}/monitors/67890.yaml" + ) + } + end + + describe '.all_file_ids' do + subject { resources.all_file_ids } + + it { is_expected.to contain_exactly('abc-123-def', 'ghi-456-jkl', '12345', '67890') } + end + + describe '.all_file_ids_for_selected_resources' do + subject { resources.all_file_ids_for_selected_resources } + + around do |example| + old_resources = $options[:resources] + + begin + $options[:resources] = [DatadogBackup::Dashboards] + example.run + ensure + $options[:resources] = old_resources + end + end + + specify do + expect(subject).to contain_exactly('abc-123-def', 'ghi-456-jkl') + end + end + + describe '.class_from_id' do + subject { resources.class_from_id('abc-123-def') } + + it { is_expected.to eq DatadogBackup::Dashboards } + end + + describe '.find_file_by_id' do + subject { resources.find_file_by_id('abc-123-def') } + + it { is_expected.to eq "#{$options[:backup_dir]}/dashboards/abc-123-def.json" } + end + + describe '.load_from_file_by_id' do + context 'when the mode is :yaml but the backup is json' do + subject { resources.load_from_file_by_id('abc-123-def') } + + around do |example| + old_resources = $options[:output_format] + + begin + $options[:output_format] = :yaml + example.run + ensure + $options[:output_format] = old_resources + end + end + + it { is_expected.to eq({ 'id' => 'abc-123-def', 'file_type' => 'json' }) } + end + + context 'when the mode is :json but the backup is yaml' do + subject { resources.load_from_file_by_id('ghi-456-jkl') } + + it { is_expected.to eq({ 'id' => 'ghi-456-jkl', 'file_type' => 'yaml' }) } + end + + context 'with Integer as parameter' do + subject { resources.load_from_file_by_id(12_345) } + + it { is_expected.to eq({ 'id' => 12_345, 'file_type' => 'json' }) } + end + end + + describe '.mydir' do + context 'when the resource is DatadogBackup::Dashboards' do + subject { DatadogBackup::Dashboards.mydir } + + it { is_expected.to eq "#{$options[:backup_dir]}/dashboards" } + end + + context 'when the resource is DatadogBackup::Monitors' do + subject { DatadogBackup::Monitors.mydir } + + it { is_expected.to eq "#{$options[:backup_dir]}/monitors" } + end + + context 'when the resource is DatadogBackup::Synthetics' do + subject { DatadogBackup::Synthetics.mydir } + + it { is_expected.to eq "#{$options[:backup_dir]}/synthetics" } + end + end + + describe '.purge' do + context 'when the resource is DatadogBackup::Dashboards' do + subject(:purge) { DatadogBackup::Dashboards.purge } + + specify do + allow(FileUtils).to receive(:rm) + purge + expect(FileUtils).to have_received(:rm).with([ + "#{$options[:backup_dir]}/dashboards/abc-123-def.json", + "#{$options[:backup_dir]}/dashboards/ghi-456-jkl.yaml" + ]) + end + end + end + end + end + end +end diff --git a/spec/datadog_backup/resources/local_filesystem_spec.rb b/spec/datadog_backup/resources/local_filesystem_spec.rb new file mode 100644 index 0000000..3da92b8 --- /dev/null +++ b/spec/datadog_backup/resources/local_filesystem_spec.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe DatadogBackup::Resources::LocalFilesystem do + let(:dashboard) do + DatadogBackup::Dashboards.new_resource(id: 'abc-123-def', body: { id: 'abc-123-def' }) + end + + describe '#backup' do + subject(:backup) { dashboard.backup } + + it 'writes a file' do + file = instance_double(File) + allow(File).to receive(:open).and_return(file) + allow(file).to receive(:write) + allow(file).to receive(:close) + backup + expect(file).to have_received(:write).with(%({\n "id": "abc-123-def"\n})) + end + end + + describe '#delete_backup' do + subject(:delete_backup) { dashboard.delete_backup } + + it 'deletes a file' do + allow(FileUtils).to receive(:rm) + delete_backup + expect(FileUtils).to have_received(:rm).with(dashboard.filename) + end + end + + describe '#body_from_backup' do + subject(:body_from_backup) { dashboard.body_from_backup } + + before do + allow(dashboard.class).to receive(:load_from_file_by_id).and_return({ 'id' => 'abc-123-def' }) + end + + it { is_expected.to eq({ 'id' => 'abc-123-def' }) } + end + + describe '#filename' do + subject(:filename) { dashboard.filename } + + it { is_expected.to eq("#{$options[:backup_dir]}/dashboards/abc-123-def.json") } + end +end diff --git a/spec/datadog_backup/synthetics_spec.rb b/spec/datadog_backup/synthetics_spec.rb index 8297d56..0923dea 100644 --- a/spec/datadog_backup/synthetics_spec.rb +++ b/spec/datadog_backup/synthetics_spec.rb @@ -3,255 +3,164 @@ require 'spec_helper' describe DatadogBackup::Synthetics do - let(:stubs) { Faraday::Adapter::Test::Stubs.new } - let(:api_client_double) { Faraday.new { |f| f.adapter :test, stubs } } - let(:tempdir) { Dir.mktmpdir } # TODO: delete afterward - let(:synthetics) do - synthetics = described_class.new( - action: 'backup', - backup_dir: tempdir, - output_format: :json, - resources: [] - ) - allow(synthetics).to receive(:api_service).and_return(api_client_double) - return synthetics - end - let(:api_test) do - { 'config' => { 'assertions' => [{ 'operator' => 'contains', 'property' => 'set-cookie', 'target' => '_user_id', 'type' => 'header' }, - { 'operator' => 'contains', 'target' => 'body message', 'type' => 'body' }, - { 'operator' => 'is', 'property' => 'content-type', 'target' => 'text/html; charset=utf-8', 'type' => 'header' }, - { 'operator' => 'is', 'target' => 200, 'type' => 'statusCode' }, - { 'operator' => 'lessThan', 'target' => 5000, 'type' => 'responseTime' }], - 'request' => { 'headers' => { 'User-Agent' => 'Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:65.0) Gecko/20100101 Firefox/65.0', - 'cookie' => '_a=12345; _example_session=abc123' }, - 'method' => 'GET', - 'url' => 'https://www.example.com/' } }, - 'creator' => { 'email' => 'user@example.com', 'handle' => 'user@example.com', 'name' => 'Hugh Zer' }, - 'locations' => ['aws:ap-northeast-1', 'aws:eu-central-1', 'aws:eu-west-2', 'aws:us-west-2'], - 'message' => 'TEST: This is a test', - 'monitor_id' => 12_345, - 'name' => 'TEST: This is a test', - 'options' => { 'follow_redirects' => true, - 'httpVersion' => 'http1', - 'min_failure_duration' => 120, - 'min_location_failed' => 2, - 'monitor_options' => { 'renotify_interval' => 0 }, - 'monitor_priority' => 1, - 'retry' => { 'count' => 1, 'interval' => 500 }, - 'tick_every' => 120 }, - 'public_id' => 'abc-123-def', - 'status' => 'live', - 'subtype' => 'http', - 'tags' => ['env:test'], - 'type' => 'api' } - end - let(:browser_test) do - { 'config' => { 'assertions' => [], - 'configVariables' => [], - 'request' => { 'headers' => {}, 'method' => 'GET', 'url' => 'https://www.example.com' }, - 'setCookie' => nil, - 'variables' => [] }, - 'creator' => { 'email' => 'user@example.com', - 'handle' => 'user@example.com', - 'name' => 'Hugh Zer' }, - 'locations' => ['aws:us-east-2'], - 'message' => 'Test message', - 'monitor_id' => 12_345, - 'name' => 'www.example.com', - 'options' => { 'ci' => { 'executionRule' => 'non_blocking' }, - 'device_ids' => ['chrome.laptop_large', 'chrome.mobile_small'], - 'disableCors' => false, - 'disableCsp' => false, - 'ignoreServerCertificateError' => false, - 'min_failure_duration' => 300, - 'min_location_failed' => 1, - 'monitor_options' => { 'renotify_interval' => 0 }, - 'noScreenshot' => false, - 'retry' => { 'count' => 0, 'interval' => 1000 }, - 'tick_every' => 900 }, - 'public_id' => '456-ghi-789', - 'status' => 'live', - 'tags' => ['env:test'], - 'type' => 'browser' } - end - let(:all_synthetics) { respond_with200({ 'tests' => [api_test, browser_test] }) } - let(:api_synthetic) { respond_with200(api_test) } - let(:browser_synthetic) { respond_with200(browser_test) } - before do - stubs.get('/api/v1/synthetics/tests') { all_synthetics } - stubs.get('/api/v1/synthetics/tests/api/abc-123-def') { api_synthetic } - stubs.get('/api/v1/synthetics/tests/browser/456-ghi-789') { browser_synthetic } + allow_any_instance_of(DatadogBackup::Client).to receive(:get_body) + .with('/api/v1/synthetics/tests', {}, {}) + .and_return({ 'tests' => [ + { 'public_id' => 'abc-123-def', 'type' => 'api' }, + { 'public_id' => 'ghi-456-jkl', 'type' => 'browser' } + ] }) + + allow_any_instance_of(DatadogBackup::Client).to receive(:get_body) + .with('/api/v1/synthetics/tests/api/abc-123-def', {}, {}) + .and_return({ 'public_id' => 'abc-123-def', 'type' => 'api' }) + + allow_any_instance_of(DatadogBackup::Client).to receive(:get_body) + .with('/api/v1/synthetics/tests/browser/ghi-456-jkl', {}, {}) + .and_return({ 'public_id' => 'ghi-456-jkl', 'type' => 'browser' }) + + # While searching for a test, datadog_backup will brute force try one before the other. + allow_any_instance_of(DatadogBackup::Client).to receive(:get_body) + .with('/api/v1/synthetics/tests/browser/abc-123-def', {}, {}) + .and_raise(Faraday::ResourceNotFound) + + allow_any_instance_of(DatadogBackup::Client).to receive(:get_body) + .with('/api/v1/synthetics/tests/api/ghi-456-jkl', {}, {}) + .and_raise(Faraday::ResourceNotFound) end - describe '#all' do - subject { synthetics.all } + describe 'Class Methods' do + describe '.new_resource' do + context 'with id and body' do + subject { described_class.new_resource(id: 'abc-123-def', body: { public_id: 'abc-123-def' }) } - it { is_expected.to contain_exactly(api_test, browser_test) } - end + it { is_expected.to be_a(described_class) } + end - describe '#backup' do - subject(:backup) { synthetics.backup } + context 'with id and no body' do + subject { described_class.new_resource(id: 'abc-123-def') } - let(:apifile) { instance_double(File) } - let(:browserfile) { instance_double(File) } + it { is_expected.to be_a(described_class) } + end - before do - allow(File).to receive(:open).with(synthetics.filename('abc-123-def'), 'w').and_return(apifile) - allow(File).to receive(:open).with(synthetics.filename('456-ghi-789'), 'w').and_return(browserfile) - allow(apifile).to receive(:write) - allow(apifile).to receive(:close) - allow(browserfile).to receive(:write) - allow(browserfile).to receive(:close) - end + context 'with no id and with body' do + subject { described_class.new_resource(body: { public_id: 'abc-123-def' }) } - it 'is expected to write the API test' do - backup - expect(apifile).to have_received(:write).with(::JSON.pretty_generate(api_test)) - end + it { is_expected.to be_a(described_class) } + end - it 'is expected to write the browser test' do - backup - expect(browserfile).to have_received(:write).with(::JSON.pretty_generate(browser_test)) + context 'with no id and no body' do + subject { proc { described_class.new_resource } } + + it { is_expected.to raise_error(ArgumentError) } + end end - end - describe '#filename' do - subject { synthetics.filename('abc-123-def') } + describe '.all' do + subject { described_class.all } - it { is_expected.to eq("#{tempdir}/synthetics/abc-123-def.json") } - end + it { is_expected.to be_a(Array) } + it { is_expected.to all(be_a(described_class)) } + end - describe '#get_by_id' do - context 'when the type is api' do - subject { synthetics.get_by_id('abc-123-def') } + describe '.get_all' do + subject { described_class.get_all } - it { is_expected.to eq api_test } + it { + expect(subject).to contain_exactly( + { 'public_id' => 'abc-123-def', 'type' => 'api' }, + { 'public_id' => 'ghi-456-jkl', 'type' => 'browser' } + ) + } end - context 'when the type is browser' do - subject { synthetics.get_by_id('456-ghi-789') } + describe '.get_by_id' do + subject { described_class.get_by_id('abc-123-def').id } - it { is_expected.to eq browser_test } + it { is_expected.to eq('abc-123-def') } end - end - describe '#diff' do # TODO: migrate to resources_spec.rb, since #diff is not defined here. - subject { synthetics.diff('abc-123-def') } + describe '.myclass' do + subject { described_class.myclass } - before do - synthetics.write_file(synthetics.dump(api_test), synthetics.filename('abc-123-def')) + it { is_expected.to eq('synthetics') } end + end - context 'when the test is identical' do - it { is_expected.to be_empty } - end + describe 'Instance Methods' do + subject(:abc) { described_class.new_resource(id: 'abc-123-def') } - context 'when the remote is not found' do - subject(:invalid_diff) { synthetics.diff('invalid-id') } + describe '#diff' do + subject(:diff) { abc.diff } before do - synthetics.write_file(synthetics.dump({ 'name' => 'invalid-diff' }), synthetics.filename('invalid-id')) + allow(abc).to receive(:body_from_backup) + .and_return({ 'public_id' => 'abc-123-def', 'type' => 'api', 'title' => 'abc' }) + $options[:diff_format] = 'text' end it { - expect(invalid_diff).to eq(%(---- {}\n+---\n+name: invalid-diff)) + expect(diff).to eq(<<~EODIFF + --- + +public_id: abc-123-def + type: api + +title: abc + EODIFF + .chomp) } end - context 'when there is a local update' do - before do - different_test = api_test.dup - different_test['message'] = 'Different message' - synthetics.write_file(synthetics.dump(different_test), synthetics.filename('abc-123-def')) - end - - it { is_expected.to include(%(-message: 'TEST: This is a test'\n+message: Different message)) } - end - end + describe '#dump' do + subject(:dump) { abc.dump } - describe '#create' do - context 'when the type is api' do - subject(:create) { synthetics.create({ 'type' => 'api' }) } + context 'when mode is :json' do + before do + $options[:output_format] = :json + end - before do - stubs.post('/api/v1/synthetics/tests/api') { respond_with200({ 'public_id' => 'api-create-abc' }) } + it { is_expected.to eq(%({\n "type": "api"\n})) } end - it { is_expected.to eq({ 'public_id' => 'api-create-abc' }) } - end - - context 'when the type is browser' do - subject(:create) { synthetics.create({ 'type' => 'browser' }) } + context 'when mode is :yaml' do + before do + $options[:output_format] = :yaml + end - before do - stubs.post('/api/v1/synthetics/tests/browser') { respond_with200({ 'public_id' => 'browser-create-abc' }) } + it { is_expected.to eq(%(---\ntype: api\n)) } end - - it { is_expected.to eq({ 'public_id' => 'browser-create-abc' }) } end - end - describe '#update' do - context 'when the type is api' do - subject(:update) { synthetics.update('api-update-abc', { 'type' => 'api' }) } + describe '#myclass' do + subject { abc.myclass } - before do - stubs.put('/api/v1/synthetics/tests/api/api-update-abc') { respond_with200({ 'public_id' => 'api-update-abc' }) } - end - - it { is_expected.to eq({ 'public_id' => 'api-update-abc' }) } + it { is_expected.to eq('synthetics') } end - context 'when the type is browser' do - subject(:update) { synthetics.update('browser-update-abc', { 'type' => 'browser' }) } - - before do - stubs.put('/api/v1/synthetics/tests/browser/browser-update-abc') { respond_with200({ 'public_id' => 'browser-update-abc' }) } - end + describe '#get' do + subject(:get) { abc.get } - it { is_expected.to eq({ 'public_id' => 'browser-update-abc' }) } + it { is_expected.to eq({ 'type' => 'api' }) } end - end - describe '#restore' do - context 'when the id exists' do - subject { synthetics.restore('abc-123-def') } + describe '#create' do + subject(:create) { abc.create } - before do - synthetics.write_file(synthetics.dump({ 'name' => 'restore-valid-id', 'type' => 'api' }), synthetics.filename('abc-123-def')) - stubs.put('/api/v1/synthetics/tests/api/abc-123-def') { respond_with200({ 'public_id' => 'abc-123-def', 'type' => 'api' }) } + it 'posts to the api endpoint' do + expect_any_instance_of(DatadogBackup::Client).to receive(:post_body) + .with('/api/v1/synthetics/tests/api', { 'type' => 'api' }, {}) + .and_return({ 'public_id' => 'abc-999-def' }) + create end - - it { is_expected.to eq({ 'public_id' => 'abc-123-def', 'type' => 'api' }) } end - context 'when the id does not exist' do - subject(:restore) { synthetics.restore('restore-invalid-id') } - - before do - synthetics.write_file(synthetics.dump({ 'name' => 'restore-invalid-id', 'type' => 'api' }), synthetics.filename('restore-invalid-id')) - stubs.put('/api/v1/synthetics/tests/api/restore-invalid-id') { [404, {}, ''] } - stubs.post('/api/v1/synthetics/tests/api') { respond_with200({ 'public_id' => 'restore-valid-id' }) } - allow(synthetics).to receive(:create).and_call_original - allow(synthetics).to receive(:all).and_return([api_test, browser_test, { 'public_id' => 'restore-valid-id', 'type' => 'api' }]) - end - - it { is_expected.to eq({ 'type' => 'api' }) } - - it 'calls create with the contents of the original file' do - restore - expect(synthetics).to have_received(:create).with({ 'name' => 'restore-invalid-id', 'type' => 'api' }) - end - - it 'deletes the original file' do - restore - expect(File.exist?(synthetics.filename('restore-invalid-id'))).to be false - end + describe '#update' do + subject(:update) { abc.update } - it 'creates a new file with the restored contents' do - restore - expect(File.exist?(synthetics.filename('restore-valid-id'))).to be true + it 'puts to the api endpoint' do + expect_any_instance_of(DatadogBackup::Client).to receive(:put_body) + .with('/api/v1/synthetics/tests/api/abc-123-def', { 'type' => 'api' }, {}) + .and_return({ 'public_id' => 'abc-123-def' }) + update end end end diff --git a/spec/factories/resources.rb b/spec/factories/resources.rb new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/spec/factories/resources.rb @@ -0,0 +1 @@ + diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 42288c8..4fe5f52 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -9,6 +9,7 @@ $stdout = File.new('/dev/null', 'w+') require 'tmpdir' +require 'factory_bot' require 'datadog_backup' SPEC_ROOT = __dir__ @@ -81,7 +82,7 @@ # Print the 10 slowest examples and example groups at the # end of the spec run, to help surface which specs are running # particularly slow. - config.profile_examples = 10 + config.profile_examples = 2 # Run specs in random order to surface order dependencies. If you find an # order dependency and want to debug it, you can fix the order by providing @@ -97,8 +98,36 @@ # Make RSpec available throughout the rspec unit test suite config.expose_dsl_globally = true + + config.include FactoryBot::Syntax::Methods + + config.before(:suite) do + FactoryBot.find_definitions + end end def respond_with200(body) [200, {}, body] end + +tempdir = Dir.mktmpdir +$options = { + action: nil, + backup_dir: tempdir, + diff_format: :color, + resources: [ + DatadogBackup::Dashboards, + DatadogBackup::Monitors, + DatadogBackup::Synthetics + ], + output_format: :json, + force_restore: false +} + +def cleanup + FileUtils.rm_rf($options[:backup_dir]) +end + +at_exit do + cleanup +end From 9c1a77a1f30f649d42fa370755177bf97a288491 Mon Sep 17 00:00:00 2001 From: Jim Park Date: Wed, 26 Oct 2022 11:19:09 -0700 Subject: [PATCH 02/13] supply default DD_API_KEY for testing --- spec/spec_helper.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 4fe5f52..474b25e 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -8,6 +8,10 @@ LOGGER.level = Logger::ERROR $stdout = File.new('/dev/null', 'w+') +# Mock DD API Key unless provided by environment. +ENV['DD_API_KEY'] = 'abc123' unless ENV.has_key? 'DD_API_KEY' +ENV['DD_APP_KEY'] = 'abc123' unless ENV.has_key? 'DD_APP_KEY' + require 'tmpdir' require 'factory_bot' require 'datadog_backup' From 7d4d7a6af9e68beaa1d6ca4bf057c66413089977 Mon Sep 17 00:00:00 2001 From: Jim Park Date: Wed, 26 Oct 2022 11:26:52 -0700 Subject: [PATCH 03/13] set format to json for consistency --- spec/datadog_backup/resources/local_filesystem_spec.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/spec/datadog_backup/resources/local_filesystem_spec.rb b/spec/datadog_backup/resources/local_filesystem_spec.rb index 3da92b8..2e0cf24 100644 --- a/spec/datadog_backup/resources/local_filesystem_spec.rb +++ b/spec/datadog_backup/resources/local_filesystem_spec.rb @@ -3,10 +3,14 @@ require 'spec_helper' describe DatadogBackup::Resources::LocalFilesystem do - let(:dashboard) do + subject(:dashboard) do DatadogBackup::Dashboards.new_resource(id: 'abc-123-def', body: { id: 'abc-123-def' }) end + before do + $options[:output_format] = :json + end + describe '#backup' do subject(:backup) { dashboard.backup } From 93b86244100605276aab725f3b31657a3d4486d7 Mon Sep 17 00:00:00 2001 From: Jim Park Date: Wed, 26 Oct 2022 11:58:42 -0700 Subject: [PATCH 04/13] refactor: cleanup deprecation warnings --- spec/datadog_backup/dashboards_spec.rb | 6 ++++-- spec/datadog_backup/monitors_spec.rb | 6 ++++-- .../resources/local_filesystem/class_methods_spec.rb | 10 ++++++---- spec/datadog_backup/synthetics_spec.rb | 6 ++++-- spec/spec_helper.rb | 4 ++-- 5 files changed, 20 insertions(+), 12 deletions(-) diff --git a/spec/datadog_backup/dashboards_spec.rb b/spec/datadog_backup/dashboards_spec.rb index 6ce8ba2..526c4a3 100644 --- a/spec/datadog_backup/dashboards_spec.rb +++ b/spec/datadog_backup/dashboards_spec.rb @@ -34,9 +34,11 @@ end context 'with no id and no body' do - subject { proc { described_class.new_resource } } + subject { described_class.new_resource } - it { is_expected.to raise_error(ArgumentError) } + it 'raises an ArgumentError' do + expect {subject}.to raise_error(ArgumentError) + end end end diff --git a/spec/datadog_backup/monitors_spec.rb b/spec/datadog_backup/monitors_spec.rb index 05d070a..a4a1b9c 100644 --- a/spec/datadog_backup/monitors_spec.rb +++ b/spec/datadog_backup/monitors_spec.rb @@ -34,9 +34,11 @@ end context 'with no id and no body' do - subject { proc { described_class.new_resource } } + subject { described_class.new_resource } - it { is_expected.to raise_error(ArgumentError) } + it 'raises an ArgumentError' do + expect {subject}.to raise_error(ArgumentError) + end end end diff --git a/spec/datadog_backup/resources/local_filesystem/class_methods_spec.rb b/spec/datadog_backup/resources/local_filesystem/class_methods_spec.rb index ab1c9cc..21afd24 100644 --- a/spec/datadog_backup/resources/local_filesystem/class_methods_spec.rb +++ b/spec/datadog_backup/resources/local_filesystem/class_methods_spec.rb @@ -133,10 +133,12 @@ specify do allow(FileUtils).to receive(:rm) purge - expect(FileUtils).to have_received(:rm).with([ - "#{$options[:backup_dir]}/dashboards/abc-123-def.json", - "#{$options[:backup_dir]}/dashboards/ghi-456-jkl.yaml" - ]) + expect(FileUtils).to have_received(:rm).with( + array_including( + "#{$options[:backup_dir]}/dashboards/abc-123-def.json", + "#{$options[:backup_dir]}/dashboards/ghi-456-jkl.yaml" + ) + ) end end end diff --git a/spec/datadog_backup/synthetics_spec.rb b/spec/datadog_backup/synthetics_spec.rb index 0923dea..721ab84 100644 --- a/spec/datadog_backup/synthetics_spec.rb +++ b/spec/datadog_backup/synthetics_spec.rb @@ -50,9 +50,11 @@ end context 'with no id and no body' do - subject { proc { described_class.new_resource } } + subject { described_class.new_resource } - it { is_expected.to raise_error(ArgumentError) } + it 'raises an ArgumentError' do + expect {subject}.to raise_error(ArgumentError) + end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 474b25e..b0c49ee 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -9,8 +9,8 @@ $stdout = File.new('/dev/null', 'w+') # Mock DD API Key unless provided by environment. -ENV['DD_API_KEY'] = 'abc123' unless ENV.has_key? 'DD_API_KEY' -ENV['DD_APP_KEY'] = 'abc123' unless ENV.has_key? 'DD_APP_KEY' +ENV['DD_API_KEY'] = 'abc123' unless ENV.key? 'DD_API_KEY' +ENV['DD_APP_KEY'] = 'abc123' unless ENV.key? 'DD_APP_KEY' require 'tmpdir' require 'factory_bot' From 77f3d472094e045123895921fbb2983496756f23 Mon Sep 17 00:00:00 2001 From: Jim Park Date: Wed, 26 Oct 2022 12:01:06 -0700 Subject: [PATCH 05/13] avoid "warning: instance variable @body not initialized" error --- lib/datadog_backup/synthetics.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/datadog_backup/synthetics.rb b/lib/datadog_backup/synthetics.rb index ce53b1d..337d425 100644 --- a/lib/datadog_backup/synthetics.rb +++ b/lib/datadog_backup/synthetics.rb @@ -16,7 +16,7 @@ def instance_resource_name end def get - if @body.nil? + if !defined? @body begin breakloop = false super(api_resource_name: 'synthetics/tests/api') From de7d0145b61b0a40e98d5404c3aff416b46c05f7 Mon Sep 17 00:00:00 2001 From: Jim Park Date: Wed, 26 Oct 2022 12:08:00 -0700 Subject: [PATCH 06/13] rubocop -A --- lib/datadog_backup/synthetics.rb | 6 +++--- spec/datadog_backup/dashboards_spec.rb | 2 +- spec/datadog_backup/monitors_spec.rb | 2 +- spec/datadog_backup/synthetics_spec.rb | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/datadog_backup/synthetics.rb b/lib/datadog_backup/synthetics.rb index 337d425..7428cae 100644 --- a/lib/datadog_backup/synthetics.rb +++ b/lib/datadog_backup/synthetics.rb @@ -16,7 +16,9 @@ def instance_resource_name end def get - if !defined? @body + if defined? @body + super(api_resource_name: instance_resource_name) + else begin breakloop = false super(api_resource_name: 'synthetics/tests/api') @@ -28,8 +30,6 @@ def get super(api_resource_name: 'synthetics/tests/browser') end end - else - super(api_resource_name: instance_resource_name) end end diff --git a/spec/datadog_backup/dashboards_spec.rb b/spec/datadog_backup/dashboards_spec.rb index 526c4a3..31fe84e 100644 --- a/spec/datadog_backup/dashboards_spec.rb +++ b/spec/datadog_backup/dashboards_spec.rb @@ -37,7 +37,7 @@ subject { described_class.new_resource } it 'raises an ArgumentError' do - expect {subject}.to raise_error(ArgumentError) + expect { subject }.to raise_error(ArgumentError) end end end diff --git a/spec/datadog_backup/monitors_spec.rb b/spec/datadog_backup/monitors_spec.rb index a4a1b9c..536415b 100644 --- a/spec/datadog_backup/monitors_spec.rb +++ b/spec/datadog_backup/monitors_spec.rb @@ -37,7 +37,7 @@ subject { described_class.new_resource } it 'raises an ArgumentError' do - expect {subject}.to raise_error(ArgumentError) + expect { subject }.to raise_error(ArgumentError) end end end diff --git a/spec/datadog_backup/synthetics_spec.rb b/spec/datadog_backup/synthetics_spec.rb index 721ab84..e653d77 100644 --- a/spec/datadog_backup/synthetics_spec.rb +++ b/spec/datadog_backup/synthetics_spec.rb @@ -53,7 +53,7 @@ subject { described_class.new_resource } it 'raises an ArgumentError' do - expect {subject}.to raise_error(ArgumentError) + expect { subject }.to raise_error(ArgumentError) end end end From 9df4d97b5400c41541aaee15e9fd504fea4ba7e3 Mon Sep 17 00:00:00 2001 From: Jim Park Date: Wed, 26 Oct 2022 12:37:53 -0700 Subject: [PATCH 07/13] fix: dashboards needs to do a get for each element in the list --- lib/datadog_backup/dashboards.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/datadog_backup/dashboards.rb b/lib/datadog_backup/dashboards.rb index 0a3c829..e598f34 100644 --- a/lib/datadog_backup/dashboards.rb +++ b/lib/datadog_backup/dashboards.rb @@ -9,5 +9,13 @@ class Dashboards < Resources @banlist = %w[modified_at url].freeze @api_service = DatadogBackup::Client.new @dig_in_list_body = 'dashboards' + + def self.all + @all ||= get_all.map do |resource| + new_resource(id: resource.fetch(@id_keyname)) + end + LOGGER.info "Found #{@all.length} #{@api_resource_name}s in Datadog" + @all + end end end From 27393e9948dbc60b618cf462e9894419c7609fa5 Mon Sep 17 00:00:00 2001 From: Jim Park Date: Wed, 26 Oct 2022 12:42:21 -0700 Subject: [PATCH 08/13] fix: include id for monitors --- lib/datadog_backup/monitors.rb | 2 +- spec/datadog_backup/monitors_spec.rb | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/datadog_backup/monitors.rb b/lib/datadog_backup/monitors.rb index 7b9e262..be0e65d 100644 --- a/lib/datadog_backup/monitors.rb +++ b/lib/datadog_backup/monitors.rb @@ -6,7 +6,7 @@ class Monitors < Resources @api_version = 'v1' @api_resource_name = 'monitor' @id_keyname = 'id' - @banlist = %w[id matching_downtimes modified overall_state overall_state_modified].freeze + @banlist = %w[matching_downtimes modified overall_state overall_state_modified].freeze @api_service = DatadogBackup::Client.new @dig_in_list_body = nil end diff --git a/spec/datadog_backup/monitors_spec.rb b/spec/datadog_backup/monitors_spec.rb index 536415b..21ead66 100644 --- a/spec/datadog_backup/monitors_spec.rb +++ b/spec/datadog_backup/monitors_spec.rb @@ -76,13 +76,14 @@ before do allow(abc).to receive(:body_from_backup) - .and_return({ 'name' => 'Local Copy' }) + .and_return({ 'id' => 'abc-123-def', 'name' => 'Local Copy' }) $options[:diff_format] = 'text' end it { expect(diff).to eq(<<~EODIFF --- + id: abc-123-def -name: Test Monitor +name: Local Copy EODIFF @@ -98,7 +99,7 @@ $options[:output_format] = :json end - it { is_expected.to eq(%({\n "name": "Test Monitor"\n})) } + it { is_expected.to eq(%({\n "id": "abc-123-def",\n "name": "Test Monitor"\n})) } end context 'when mode is :yaml' do @@ -106,7 +107,7 @@ $options[:output_format] = :yaml end - it { is_expected.to eq(%(---\nname: Test Monitor\n)) } + it { is_expected.to eq(%(---\nid: abc-123-def\nname: Test Monitor\n)) } end end @@ -119,7 +120,7 @@ describe '#get' do subject(:get) { abc.get } - it { is_expected.to eq('name' => 'Test Monitor') } + it { is_expected.to eq('id' => 'abc-123-def', 'name' => 'Test Monitor') } end describe '#create' do @@ -127,7 +128,7 @@ it 'posts to the API' do expect_any_instance_of(DatadogBackup::Client).to receive(:post_body) - .with('/api/v1/monitor', { 'name' => 'Test Monitor' }, {}) + .with('/api/v1/monitor', { 'id' => 'abc-123-def', 'name' => 'Test Monitor' }, {}) .and_return({ 'id' => 'abc-999-def' }) create end @@ -138,7 +139,7 @@ it 'posts to the API' do expect_any_instance_of(DatadogBackup::Client).to receive(:put_body) - .with('/api/v1/monitor/abc-123-def', { 'name' => 'Test Monitor' }, {}) + .with('/api/v1/monitor/abc-123-def', { 'id' => 'abc-123-def', 'name' => 'Test Monitor' }, {}) .and_return({ 'id' => 'abc-123-def' }) update end From 816949799bf7d6958242fc6c81cdb8f759b5bcb4 Mon Sep 17 00:00:00 2001 From: Jim Park Date: Wed, 26 Oct 2022 12:47:39 -0700 Subject: [PATCH 09/13] fix: restore multithreading for Dashboards.all --- lib/datadog_backup/dashboards.rb | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/datadog_backup/dashboards.rb b/lib/datadog_backup/dashboards.rb index e598f34..fa7ef1f 100644 --- a/lib/datadog_backup/dashboards.rb +++ b/lib/datadog_backup/dashboards.rb @@ -12,10 +12,15 @@ class Dashboards < Resources def self.all @all ||= get_all.map do |resource| - new_resource(id: resource.fetch(@id_keyname)) + Concurrent::Promises.future_on(DatadogBackup::ThreadPool::TPOOL, resource) do |r| + new_resource(id: r.fetch(@id_keyname)) + end end LOGGER.info "Found #{@all.length} #{@api_resource_name}s in Datadog" - @all + + watcher = DatadogBackup::ThreadPool.watcher + watcher.join if watcher.status + Concurrent::Promises.zip(*@all).value! end end end From a003595d9a3e889fd7794f83e39fbfd38b7d5183 Mon Sep 17 00:00:00 2001 From: Jim Park Date: Wed, 26 Oct 2022 12:54:34 -0700 Subject: [PATCH 10/13] fix: restore multithreading for Dashboards.all --- lib/datadog_backup/dashboards.rb | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/datadog_backup/dashboards.rb b/lib/datadog_backup/dashboards.rb index fa7ef1f..9ff32ef 100644 --- a/lib/datadog_backup/dashboards.rb +++ b/lib/datadog_backup/dashboards.rb @@ -11,16 +11,18 @@ class Dashboards < Resources @dig_in_list_body = 'dashboards' def self.all - @all ||= get_all.map do |resource| + return @all if @all + + futures = get_all.map do |resource| Concurrent::Promises.future_on(DatadogBackup::ThreadPool::TPOOL, resource) do |r| new_resource(id: r.fetch(@id_keyname)) end end - LOGGER.info "Found #{@all.length} #{@api_resource_name}s in Datadog" + LOGGER.info "Found #{futures.length} #{@api_resource_name}s in Datadog" watcher = DatadogBackup::ThreadPool.watcher watcher.join if watcher.status - Concurrent::Promises.zip(*@all).value! + @all = Concurrent::Promises.zip(*futures).value! end end end From 157dd43d7da5b8117b6d68e7d7ad7f88785f1255 Mon Sep 17 00:00:00 2001 From: Jim Park Date: Sun, 6 Nov 2022 19:48:15 -0800 Subject: [PATCH 11/13] update CLI to use new API --- lib/datadog_backup/cli.rb | 99 ++++++------------- .../local_filesystem/class_methods_spec.rb | 98 +++++++++++++++--- 2 files changed, 113 insertions(+), 84 deletions(-) diff --git a/lib/datadog_backup/cli.rb b/lib/datadog_backup/cli.rb index 3271fe3..24c8cfd 100644 --- a/lib/datadog_backup/cli.rb +++ b/lib/datadog_backup/cli.rb @@ -16,92 +16,57 @@ def backup any_resource_instance.all_files end - def restore - futures = all_diff_futures - watcher = ::DatadogBackup::ThreadPool.watcher + def diffs + $options[:resources].each do |resource| + resource.all.each do |resource_instance| + next if resource_instance.diff.nil? || resource_instance.diff.empty? - futures.each do |future| - id, diff = *future.value! - next if diff.nil? || diff.empty? + puts resource_instance.diff + end + end + end - if $options[:force_restore] - definitive_resource_instance(id).restore(id) - else - ask_to_restore(id, diff) + def restore + $options[:resources].each do |resource| + resource.all.each do |resource_instance| + next if resource_instance.diff.nil? || resource_instance.diff.empty? + + if $options[:force_restore] + resource_instance.restore + else + ask_to_restore(resource_instance) + end end end - watcher.join if watcher.status end def run! case $options[:action] when 'backup' LOGGER.info('Starting backup.') - puts backup + backup when 'restore' LOGGER.info('Starting restore.') - puts restore + restore + when 'diffs' + LOGGER.info('Starting diffs.') + diffs else fatal 'No action specified.' end + LOGGER.info('Done.') rescue SystemExit, Interrupt ::DatadogBackup::ThreadPool.shutdown end private - ## - # The diff methods - - def all_diff_futures - LOGGER.info("Starting diffs on #{::DatadogBackup::ThreadPool::TPOOL.max_length} threads") - any_resource_instance - .all_file_ids_for_selected_resources - .map do |file_id| - Concurrent::Promises.future_on(::DatadogBackup::ThreadPool::TPOOL, file_id) do |fid| - [fid, getdiff(fid)] - end - end - end - - # rubocop:disable Style/StringConcatenation - def format_diff_output(diff_output) - case $options[:diff_format] - when nil, :color - diff_output.map do |id, diff| - " ---\n id: #{id}\n#{diff}" - end.join("\n") - when :html - '' + - diff_output.map do |id, diff| - "

---
id: #{id}
#{diff}" - end.join('
') + - '' - else - raise 'Unexpected diff_format.' - end - end - # rubocop:enable Style/StringConcatenation - - def getdiff(id) - LOGGER.debug("Searching for diff for #{id}.") - result = definitive_resource_instance(id).get_by_id(id).diff - case result - when '---' || '' || "\n" || '
' - nil - else - result - end - end - ## # Interact with the user - def ask_to_restore(id, diff) + def ask_to_restore(resource_instance) puts '--------------------------------------------------------------------------------' - puts format_diff_output([id, diff]) + puts resource_instance.diff puts '(r)estore to Datadog, overwrite local changes and (d)ownload, (s)kip, or (q)uit?' loop do response = $stdin.gets.chomp @@ -109,12 +74,12 @@ def ask_to_restore(id, diff) when 'q' exit when 'r' - puts "Restoring #{id} to Datadog." - definitive_resource_instance(id).restore(id) + puts "Restoring #{resource_instance.id} to Datadog." + resource_instance.restore break when 'd' - puts "Downloading #{id} from Datadog." - definitive_resource_instance(id).get_and_write_file(id) + puts "Downloading #{resource_instance.id} from Datadog." + resource_instance.backup break when 's' break @@ -129,9 +94,5 @@ def ask_to_restore(id, diff) def any_resource_instance $options[:resources].first end - - def definitive_resource_instance(id) - any_resource_instance.class_from_id(id) - end end end diff --git a/spec/datadog_backup/resources/local_filesystem/class_methods_spec.rb b/spec/datadog_backup/resources/local_filesystem/class_methods_spec.rb index 21afd24..3d9e0a4 100644 --- a/spec/datadog_backup/resources/local_filesystem/class_methods_spec.rb +++ b/spec/datadog_backup/resources/local_filesystem/class_methods_spec.rb @@ -16,6 +16,8 @@ File.write("#{$options[:backup_dir]}/dashboards/ghi-456-jkl.yaml", "---\nid: ghi-456-jkl\nfile_type: yaml\n") File.write("#{$options[:backup_dir]}/monitors/12345.json", '{"id":12345, "file_type": "json"}') File.write("#{$options[:backup_dir]}/monitors/67890.yaml", "---\nid: 67890\nfile_type: yaml\n") + File.write("#{$options[:backup_dir]}/synthetics/mno-789-pqr.json", '{"type": "api"}') + File.write("#{$options[:backup_dir]}/synthetics/stu-012-vwx.yaml", "---\ntype: browser\n") end after(:context) do @@ -33,7 +35,9 @@ "#{$options[:backup_dir]}/dashboards/abc-123-def.json", "#{$options[:backup_dir]}/dashboards/ghi-456-jkl.yaml", "#{$options[:backup_dir]}/monitors/12345.json", - "#{$options[:backup_dir]}/monitors/67890.yaml" + "#{$options[:backup_dir]}/monitors/67890.yaml", + "#{$options[:backup_dir]}/synthetics/mno-789-pqr.json", + "#{$options[:backup_dir]}/synthetics/stu-012-vwx.yaml" ) } end @@ -41,38 +45,102 @@ describe '.all_file_ids' do subject { resources.all_file_ids } - it { is_expected.to contain_exactly('abc-123-def', 'ghi-456-jkl', '12345', '67890') } + it { is_expected.to contain_exactly('abc-123-def', 'ghi-456-jkl', '12345', '67890', 'mno-789-pqr', 'stu-012-vwx') } end describe '.all_file_ids_for_selected_resources' do subject { resources.all_file_ids_for_selected_resources } - around do |example| - old_resources = $options[:resources] + context 'Dashboards' do + around do |example| + old_resources = $options[:resources] + + begin + $options[:resources] = [DatadogBackup::Dashboards] + example.run + ensure + $options[:resources] = old_resources + end + end + + specify do + expect(subject).to contain_exactly('abc-123-def', 'ghi-456-jkl') + end + end - begin - $options[:resources] = [DatadogBackup::Dashboards] - example.run - ensure - $options[:resources] = old_resources + context 'Monitors' do + around do |example| + old_resources = $options[:resources] + + begin + $options[:resources] = [DatadogBackup::Monitors] + example.run + ensure + $options[:resources] = old_resources + end + end + + specify do + expect(subject).to contain_exactly('12345', '67890') end end - specify do - expect(subject).to contain_exactly('abc-123-def', 'ghi-456-jkl') + context 'Synthetics' do + around do |example| + old_resources = $options[:resources] + + begin + $options[:resources] = [DatadogBackup::Synthetics] + example.run + ensure + $options[:resources] = old_resources + end + end + + specify do + expect(subject).to contain_exactly('mno-789-pqr', 'stu-012-vwx') + end end end describe '.class_from_id' do - subject { resources.class_from_id('abc-123-def') } + context 'Dashboards' do + subject { resources.class_from_id('abc-123-def') } + + it { is_expected.to eq DatadogBackup::Dashboards } + end + + context 'Monitors' do + subject { resources.class_from_id('12345') } - it { is_expected.to eq DatadogBackup::Dashboards } + it { is_expected.to eq DatadogBackup::Monitors } + end + + context 'Synthetics' do + subject { resources.class_from_id('mno-789-pqr') } + + it { is_expected.to eq DatadogBackup::Synthetics } + end end describe '.find_file_by_id' do - subject { resources.find_file_by_id('abc-123-def') } + context 'Dashboards' do + subject { resources.find_file_by_id('abc-123-def') } + + it { is_expected.to eq "#{$options[:backup_dir]}/dashboards/abc-123-def.json" } + end - it { is_expected.to eq "#{$options[:backup_dir]}/dashboards/abc-123-def.json" } + context 'Monitors' do + subject { resources.find_file_by_id('12345') } + + it { is_expected.to eq "#{$options[:backup_dir]}/monitors/12345.json" } + end + + context 'Synthetics' do + subject { resources.find_file_by_id('mno-789-pqr') } + + it { is_expected.to eq "#{$options[:backup_dir]}/synthetics/mno-789-pqr.json" } + end end describe '.load_from_file_by_id' do From 91d50d6ef5e4a197ad914a369aa651a21951e17c Mon Sep 17 00:00:00 2001 From: Jim Park Date: Sun, 6 Nov 2022 19:56:38 -0800 Subject: [PATCH 12/13] refactor: move faraday to client.rb --- lib/datadog_backup/client.rb | 2 ++ lib/datadog_backup/resources.rb | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/datadog_backup/client.rb b/lib/datadog_backup/client.rb index 5f8a1e9..58748d2 100644 --- a/lib/datadog_backup/client.rb +++ b/lib/datadog_backup/client.rb @@ -1,4 +1,6 @@ # frozen_string_literal: true +require 'faraday' +require 'faraday/retry' module DatadogBackup class Client diff --git a/lib/datadog_backup/resources.rb b/lib/datadog_backup/resources.rb index defd4ab..01c8e0a 100644 --- a/lib/datadog_backup/resources.rb +++ b/lib/datadog_backup/resources.rb @@ -2,8 +2,6 @@ require 'diffy' require 'deepsort' -require 'faraday' -require 'faraday/retry' module DatadogBackup # The default options for backing up and restores. From 9baaf3e90a349b8a8759498caa99b51c9e3acd6a Mon Sep 17 00:00:00 2001 From: Jim Park Date: Sun, 6 Nov 2022 22:49:17 -0800 Subject: [PATCH 13/13] chore: add FactoryBot to specs --- lib/datadog_backup/resources.rb | 8 +-- lib/datadog_backup/synthetics.rb | 2 +- spec/datadog_backup/dashboards_spec.rb | 40 +++++++-------- spec/datadog_backup/monitors_spec.rb | 52 +++++++++----------- spec/datadog_backup/synthetics_spec.rb | 63 +++++++++++------------- spec/factories.rb | 67 ++++++++++++++++++++++++++ spec/factories/resources.rb | 1 - 7 files changed, 140 insertions(+), 93 deletions(-) create mode 100644 spec/factories.rb delete mode 100644 spec/factories/resources.rb diff --git a/lib/datadog_backup/resources.rb b/lib/datadog_backup/resources.rb index 01c8e0a..9986842 100644 --- a/lib/datadog_backup/resources.rb +++ b/lib/datadog_backup/resources.rb @@ -27,16 +27,16 @@ class Resources # Returns the diffy diff. # Optionally, supply an array of keys to remove from comparison - def diff + def diff(diff_format = $options[:diff_format]) current = @body.to_yaml filesystem = body_from_backup.to_yaml - result = ::Diffy::Diff.new(current, filesystem, include_plus_and_minus_in_html: true).to_s($options[:diff_format]) + result = ::Diffy::Diff.new(current, filesystem, include_plus_and_minus_in_html: true).to_s(diff_format) LOGGER.debug("Compared ID #{@id} and found filesystem: #{filesystem} <=> current: #{current} == result: #{result}") result.chomp end - def dump - case $options[:output_format] + def dump(format = $options[:output_format]) + case format when :json JSON.pretty_generate(sanitized_body) when :yaml diff --git a/lib/datadog_backup/synthetics.rb b/lib/datadog_backup/synthetics.rb index 7428cae..6b2a6f5 100644 --- a/lib/datadog_backup/synthetics.rb +++ b/lib/datadog_backup/synthetics.rb @@ -6,7 +6,7 @@ class Synthetics < Resources @api_version = 'v1' @api_resource_name = 'synthetics/tests' # used for list, but #instance_resource_name is used for get, create, update @id_keyname = 'public_id' - @banlist = %w[creator created_at modified_at monitor_id public_id].freeze + @banlist = %w[creator created_at modified_at monitor_id].freeze @api_service = DatadogBackup::Client.new @dig_in_list_body = 'tests' diff --git a/spec/datadog_backup/dashboards_spec.rb b/spec/datadog_backup/dashboards_spec.rb index 31fe84e..d3a946f 100644 --- a/spec/datadog_backup/dashboards_spec.rb +++ b/spec/datadog_backup/dashboards_spec.rb @@ -6,11 +6,11 @@ before do allow_any_instance_of(DatadogBackup::Client).to receive(:get_body) .with('/api/v1/dashboard', {}, {}) - .and_return({ 'dashboards' => [{ 'id' => 'abc-123-def' }] }) + .and_return({ 'dashboards' => [FactoryBot.body(:dashboard)]}) allow_any_instance_of(DatadogBackup::Client).to receive(:get_body) .with('/api/v1/dashboard/abc-123-def', {}, {}) - .and_return({ 'id' => 'abc-123-def' }) + .and_return(FactoryBot.body(:dashboard)) end describe 'Class Methods' do @@ -52,7 +52,7 @@ describe '.get_all' do subject { described_class.get_all } - it { is_expected.to eq([{ 'id' => 'abc-123-def' }]) } + it { is_expected.to eq([FactoryBot.body(:dashboard)]) } end describe '.get_by_id' do @@ -69,44 +69,38 @@ end describe 'Instance Methods' do - subject(:abc) { described_class.new_resource(id: 'abc-123-def') } + subject(:abc) { build(:dashboard) } describe '#diff' do - subject(:diff) { abc.diff } + subject(:diff) { abc.diff('text') } before do allow(abc).to receive(:body_from_backup) - .and_return({ 'id' => 'abc-123-def', 'title' => 'abc' }) - $options[:diff_format] = 'text' + .and_return({ 'id' => 'abc-123-def', 'title' => 'def' }) end it { expect(diff).to eq(<<~EODIFF --- id: abc-123-def - +title: abc + -title: abc + +title: def EODIFF .chomp) } end describe '#dump' do - subject(:dump) { abc.dump } - context 'when mode is :json' do - before do - $options[:output_format] = :json - end + subject(:json) { abc.dump(:json) } - it { is_expected.to eq(%({\n "id": "abc-123-def"\n})) } + it { is_expected.to eq(JSON.pretty_generate(FactoryBot.body(:dashboard))) } end context 'when mode is :yaml' do - before do - $options[:output_format] = :yaml - end + subject(:yaml) { abc.dump(:yaml) } - it { is_expected.to eq(%(---\nid: abc-123-def\n)) } + it { is_expected.to eq(FactoryBot.body(:dashboard).to_yaml) } end end @@ -119,7 +113,7 @@ describe '#get' do subject(:get) { abc.get } - it { is_expected.to eq('id' => 'abc-123-def') } + it { is_expected.to eq(FactoryBot.body(:dashboard)) } end describe '#create' do @@ -127,8 +121,8 @@ it 'posts to the API' do expect_any_instance_of(DatadogBackup::Client).to receive(:post_body) - .with('/api/v1/dashboard', { 'id' => 'abc-123-def' }, {}) - .and_return({ 'id' => 'abc-123-def' }) + .with('/api/v1/dashboard', FactoryBot.body(:dashboard), {}) + .and_return(FactoryBot.body(:dashboard)) create end end @@ -138,8 +132,8 @@ it 'posts to the API' do expect_any_instance_of(DatadogBackup::Client).to receive(:put_body) - .with('/api/v1/dashboard/abc-123-def', { 'id' => 'abc-123-def' }, {}) - .and_return({ 'id' => 'abc-123-def' }) + .with('/api/v1/dashboard/abc-123-def', FactoryBot.body(:dashboard), {}) + .and_return(FactoryBot.body(:dashboard)) update end end diff --git a/spec/datadog_backup/monitors_spec.rb b/spec/datadog_backup/monitors_spec.rb index 21ead66..f483b6b 100644 --- a/spec/datadog_backup/monitors_spec.rb +++ b/spec/datadog_backup/monitors_spec.rb @@ -6,29 +6,29 @@ before do allow_any_instance_of(DatadogBackup::Client).to receive(:get_body) .with('/api/v1/monitor', {}, {}) - .and_return([{ 'id' => 'abc-123-def' }]) + .and_return([FactoryBot.body(:monitor)]) allow_any_instance_of(DatadogBackup::Client).to receive(:get_body) - .with('/api/v1/monitor/abc-123-def', {}, {}) - .and_return({ 'id' => 'abc-123-def', 'name' => 'Test Monitor' }) + .with('/api/v1/monitor/12345', {}, {}) + .and_return(FactoryBot.body(:monitor)) end describe 'Class Methods' do describe '.new_resource' do context 'with id and body' do - subject { described_class.new_resource(id: 'abc-123-def', body: { id: 'abc-123-def' }) } + subject { described_class.new_resource(id: '12345', body: FactoryBot.body(:monitor)) } it { is_expected.to be_a(described_class) } end context 'with id and no body' do - subject { described_class.new_resource(id: 'abc-123-def') } + subject { described_class.new_resource(id: '12345') } it { is_expected.to be_a(described_class) } end context 'with no id and with body' do - subject { described_class.new_resource(body: { id: 'abc-123-def' }) } + subject { described_class.new_resource(body: FactoryBot.body(:monitor)) } it { is_expected.to be_a(described_class) } end @@ -52,13 +52,13 @@ describe '.get_all' do subject { described_class.get_all } - it { is_expected.to eq([{ 'id' => 'abc-123-def' }]) } + it { is_expected.to eq([FactoryBot.body(:monitor)]) } end describe '.get_by_id' do - subject { described_class.get_by_id('abc-123-def').id } + subject { described_class.get_by_id('12345').id } - it { is_expected.to eq('abc-123-def') } + it { is_expected.to eq('12345') } end describe '.myclass' do @@ -69,22 +69,21 @@ end describe 'Instance Methods' do - subject(:abc) { described_class.new_resource(id: 'abc-123-def') } + subject(:abc) { build(:monitor) } describe '#diff' do - subject(:diff) { abc.diff } + subject(:diff) { abc.diff('text') } before do allow(abc).to receive(:body_from_backup) - .and_return({ 'id' => 'abc-123-def', 'name' => 'Local Copy' }) - $options[:diff_format] = 'text' + .and_return({ 'id' => '12345', 'name' => 'Local Copy' }) end it { expect(diff).to eq(<<~EODIFF --- - id: abc-123-def - -name: Test Monitor + id: '12345' + -name: '12345' +name: Local Copy EODIFF .chomp) @@ -92,25 +91,20 @@ end describe '#dump' do - subject(:dump) { abc.dump } - context 'when mode is :json' do - before do - $options[:output_format] = :json - end + subject(:json) { abc.dump(:json) } - it { is_expected.to eq(%({\n "id": "abc-123-def",\n "name": "Test Monitor"\n})) } + it { is_expected.to eq(JSON.pretty_generate(FactoryBot.body(:monitor))) } end context 'when mode is :yaml' do - before do - $options[:output_format] = :yaml - end + subject(:yaml) { abc.dump(:yaml) } - it { is_expected.to eq(%(---\nid: abc-123-def\nname: Test Monitor\n)) } + it { is_expected.to eq(FactoryBot.body(:monitor).to_yaml) } end end + describe '#myclass' do subject { abc.myclass } @@ -120,7 +114,7 @@ describe '#get' do subject(:get) { abc.get } - it { is_expected.to eq('id' => 'abc-123-def', 'name' => 'Test Monitor') } + it { is_expected.to eq(FactoryBot.body(:monitor)) } end describe '#create' do @@ -128,7 +122,7 @@ it 'posts to the API' do expect_any_instance_of(DatadogBackup::Client).to receive(:post_body) - .with('/api/v1/monitor', { 'id' => 'abc-123-def', 'name' => 'Test Monitor' }, {}) + .with('/api/v1/monitor', FactoryBot.body(:monitor) , {}) .and_return({ 'id' => 'abc-999-def' }) create end @@ -139,8 +133,8 @@ it 'posts to the API' do expect_any_instance_of(DatadogBackup::Client).to receive(:put_body) - .with('/api/v1/monitor/abc-123-def', { 'id' => 'abc-123-def', 'name' => 'Test Monitor' }, {}) - .and_return({ 'id' => 'abc-123-def' }) + .with('/api/v1/monitor/12345', FactoryBot.body(:monitor), {}) + .and_return(FactoryBot.body(:monitor)) update end end diff --git a/spec/datadog_backup/synthetics_spec.rb b/spec/datadog_backup/synthetics_spec.rb index e653d77..24bd74f 100644 --- a/spec/datadog_backup/synthetics_spec.rb +++ b/spec/datadog_backup/synthetics_spec.rb @@ -7,44 +7,44 @@ allow_any_instance_of(DatadogBackup::Client).to receive(:get_body) .with('/api/v1/synthetics/tests', {}, {}) .and_return({ 'tests' => [ - { 'public_id' => 'abc-123-def', 'type' => 'api' }, - { 'public_id' => 'ghi-456-jkl', 'type' => 'browser' } + FactoryBot.body(:synthetic_api), + FactoryBot.body(:synthetic_browser) ] }) allow_any_instance_of(DatadogBackup::Client).to receive(:get_body) - .with('/api/v1/synthetics/tests/api/abc-123-def', {}, {}) - .and_return({ 'public_id' => 'abc-123-def', 'type' => 'api' }) + .with('/api/v1/synthetics/tests/api/mno-789-pqr', {}, {}) + .and_return(FactoryBot.body(:synthetic_api)) allow_any_instance_of(DatadogBackup::Client).to receive(:get_body) - .with('/api/v1/synthetics/tests/browser/ghi-456-jkl', {}, {}) - .and_return({ 'public_id' => 'ghi-456-jkl', 'type' => 'browser' }) + .with('/api/v1/synthetics/tests/browser/stu-456-vwx', {}, {}) + .and_return(FactoryBot.body(:synthetic_browser)) # While searching for a test, datadog_backup will brute force try one before the other. allow_any_instance_of(DatadogBackup::Client).to receive(:get_body) - .with('/api/v1/synthetics/tests/browser/abc-123-def', {}, {}) + .with('/api/v1/synthetics/tests/browser/mno-789-pqr', {}, {}) .and_raise(Faraday::ResourceNotFound) allow_any_instance_of(DatadogBackup::Client).to receive(:get_body) - .with('/api/v1/synthetics/tests/api/ghi-456-jkl', {}, {}) + .with('/api/v1/synthetics/tests/api/stu-456-vwx', {}, {}) .and_raise(Faraday::ResourceNotFound) end describe 'Class Methods' do describe '.new_resource' do context 'with id and body' do - subject { described_class.new_resource(id: 'abc-123-def', body: { public_id: 'abc-123-def' }) } + subject { described_class.new_resource(id: 'mno-789-pqr', body: FactoryBot.body(:synthetic_api)) } it { is_expected.to be_a(described_class) } end context 'with id and no body' do - subject { described_class.new_resource(id: 'abc-123-def') } + subject { described_class.new_resource(id: 'mno-789-pqr') } it { is_expected.to be_a(described_class) } end context 'with no id and with body' do - subject { described_class.new_resource(body: { public_id: 'abc-123-def' }) } + subject { described_class.new_resource(body: FactoryBot.body(:synthetic_api)) } it { is_expected.to be_a(described_class) } end @@ -70,16 +70,16 @@ it { expect(subject).to contain_exactly( - { 'public_id' => 'abc-123-def', 'type' => 'api' }, - { 'public_id' => 'ghi-456-jkl', 'type' => 'browser' } + FactoryBot.body(:synthetic_api), + FactoryBot.body(:synthetic_browser) ) } end describe '.get_by_id' do - subject { described_class.get_by_id('abc-123-def').id } + subject { described_class.get_by_id('mno-789-pqr').id } - it { is_expected.to eq('abc-123-def') } + it { is_expected.to eq('mno-789-pqr') } end describe '.myclass' do @@ -90,21 +90,20 @@ end describe 'Instance Methods' do - subject(:abc) { described_class.new_resource(id: 'abc-123-def') } + subject(:abc) { build(:synthetic_api) } describe '#diff' do - subject(:diff) { abc.diff } + subject(:diff) { abc.diff('text') } before do allow(abc).to receive(:body_from_backup) - .and_return({ 'public_id' => 'abc-123-def', 'type' => 'api', 'title' => 'abc' }) - $options[:diff_format] = 'text' + .and_return({ 'public_id' => 'mno-789-pqr', 'type' => 'api', 'title' => 'abc' }) end it { expect(diff).to eq(<<~EODIFF --- - +public_id: abc-123-def + public_id: mno-789-pqr type: api +title: abc EODIFF @@ -113,22 +112,16 @@ end describe '#dump' do - subject(:dump) { abc.dump } - context 'when mode is :json' do - before do - $options[:output_format] = :json - end + subject(:json) { abc.dump(:json) } - it { is_expected.to eq(%({\n "type": "api"\n})) } + it { is_expected.to eq(JSON.pretty_generate(FactoryBot.body(:synthetic_api))) } end context 'when mode is :yaml' do - before do - $options[:output_format] = :yaml - end + subject(:yaml) { abc.dump(:yaml) } - it { is_expected.to eq(%(---\ntype: api\n)) } + it { is_expected.to eq(FactoryBot.body(:synthetic_api).to_yaml) } end end @@ -141,7 +134,7 @@ describe '#get' do subject(:get) { abc.get } - it { is_expected.to eq({ 'type' => 'api' }) } + it { is_expected.to eq(FactoryBot.body(:synthetic_api)) } end describe '#create' do @@ -149,8 +142,8 @@ it 'posts to the api endpoint' do expect_any_instance_of(DatadogBackup::Client).to receive(:post_body) - .with('/api/v1/synthetics/tests/api', { 'type' => 'api' }, {}) - .and_return({ 'public_id' => 'abc-999-def' }) + .with('/api/v1/synthetics/tests/api', FactoryBot.body(:synthetic_api), {}) + .and_return(FactoryBot.body(:synthetic_api)) create end end @@ -160,8 +153,8 @@ it 'puts to the api endpoint' do expect_any_instance_of(DatadogBackup::Client).to receive(:put_body) - .with('/api/v1/synthetics/tests/api/abc-123-def', { 'type' => 'api' }, {}) - .and_return({ 'public_id' => 'abc-123-def' }) + .with('/api/v1/synthetics/tests/api/mno-789-pqr', FactoryBot.body(:synthetic_api), {}) + .and_return(FactoryBot.body(:synthetic_api)) update end end diff --git a/spec/factories.rb b/spec/factories.rb new file mode 100644 index 0000000..e10d1d8 --- /dev/null +++ b/spec/factories.rb @@ -0,0 +1,67 @@ +class BodyStrategy + def initialize + @strategy = FactoryBot.strategy_by_name(:create).new + end + + #delegate :association, to: :@strategy + + def result(evaluation) + JSON.parse(@strategy.result(evaluation).dump(:json)) + end +end + +FactoryBot.register_strategy(:body, BodyStrategy) + +FactoryBot.define do + factory :dashboard, class: DatadogBackup::Dashboards do + id { 'abc-123-def' } + body { + { + 'id' => 'abc-123-def', + 'title' => 'abc' + } + } + + skip_create + initialize_with { DatadogBackup::Dashboards.new_resource(id: id, body: body) } + end + + factory :monitor, class: DatadogBackup::Monitors do + id { '12345' } + body { + { + 'id'=> '12345', + 'name' => '12345' + } + } + + skip_create + initialize_with { DatadogBackup::Monitors.new_resource(id: id, body: body) } + end + + factory :synthetic_api, class: DatadogBackup::Synthetics do + id { 'mno-789-pqr' } + body { + { + 'type' => 'api', + 'public_id' => 'mno-789-pqr', + } + } + + skip_create + initialize_with { DatadogBackup::Synthetics.new_resource(id: id, body: body) } + end + + factory :synthetic_browser, class: DatadogBackup::Synthetics do + id { 'stu-456-vwx' } + body { + { + 'type' => 'browser', + 'public_id' => 'stu-456-vwx', + } + } + + skip_create + initialize_with { DatadogBackup::Synthetics.new_resource(id: id, body: body) } + end +end diff --git a/spec/factories/resources.rb b/spec/factories/resources.rb deleted file mode 100644 index 8b13789..0000000 --- a/spec/factories/resources.rb +++ /dev/null @@ -1 +0,0 @@ -