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..24c8cfd 100644 --- a/lib/datadog_backup/cli.rb +++ b/lib/datadog_backup/cli.rb @@ -6,96 +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)) - end + def diffs + $options[:resources].each do |resource| + resource.all.each do |resource_instance| + next if resource_instance.diff.nil? || resource_instance.diff.empty? - def getdiff(id) - result = definitive_resource_instance(id).diff(id) - case result - when '---' || '' || "\n" || '
' - nil - else - result - end - end - - # rubocop:disable Style/StringConcatenation - def format_diff_output(diff_output) - case 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.' + puts resource_instance.diff + end end 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) + $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! - puts(send(action.to_sym)) + case $options[:action] + when 'backup' + LOGGER.info('Starting backup.') + backup + when 'restore' + LOGGER.info('Starting 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 - def ask_to_restore(id, diff) + ## + # Interact with the user + + 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 @@ -103,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 @@ -118,14 +89,10 @@ def ask_to_restore(id, diff) end end - def matching_resource_instance(klass) - resource_instances.select { |resource_instance| resource_instance.instance_of?(klass) }.first - end - - def resource_instances - @resource_instances ||= resources.map do |resource| - resource.new(@options) - end + ## + # Finding the right resource instance to use. + def any_resource_instance + $options[:resources].first end end end diff --git a/lib/datadog_backup/client.rb b/lib/datadog_backup/client.rb new file mode 100644 index 0000000..58748d2 --- /dev/null +++ b/lib/datadog_backup/client.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true +require 'faraday' +require 'faraday/retry' + +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..9ff32ef 100644 --- a/lib/datadog_backup/dashboards.rb +++ b/lib/datadog_backup/dashboards.rb @@ -3,42 +3,26 @@ 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) + @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' + + def self.all + 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 #{futures.length} #{@api_resource_name}s in Datadog" - watcher = ::DatadogBackup::ThreadPool.watcher + 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' + @all = Concurrent::Promises.zip(*futures).value! end 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..be0e65d 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[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..9986842 100644 --- a/lib/datadog_backup/resources.rb +++ b/lib/datadog_backup/resources.rb @@ -2,175 +2,131 @@ require 'diffy' require 'deepsort' -require 'faraday' -require 'faraday/retry' 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 + 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(diff_format) - LOGGER.debug("Compared ID #{id} and found filesystem: #{filesystem} <=> current: #{current} == result: #{result}") + 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(format = $options[:output_format]) + case 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..6b2a6f5 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].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 defined? @body + super(api_resource_name: instance_resource_name) + else + 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 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..d3a946f 100644 --- a/spec/datadog_backup/dashboards_spec.rb +++ b/spec/datadog_backup/dashboards_spec.rb @@ -3,103 +3,139 @@ 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' => [FactoryBot.body(:dashboard)]}) + + allow_any_instance_of(DatadogBackup::Client).to receive(:get_body) + .with('/api/v1/dashboard/abc-123-def', {}, {}) + .and_return(FactoryBot.body(:dashboard)) 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 { described_class.new_resource } - dashboards.backup - expect(file).to have_received(:write).with(::JSON.pretty_generate(board_abc_123_def.deep_sort)) + it 'raises an ArgumentError' do + expect { subject }.to raise_error(ArgumentError) + end + 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([FactoryBot.body(:dashboard)]) } + 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) { build(:dashboard) } + + describe '#diff' do + subject(:diff) { abc.diff('text') } - it { is_expected.to eq({ a: :b }) } + before do + allow(abc).to receive(:body_from_backup) + .and_return({ 'id' => 'abc-123-def', 'title' => 'def' }) + end + + it { + expect(diff).to eq(<<~EODIFF + --- + id: abc-123-def + -title: abc + +title: def + EODIFF + .chomp) + } + end + + describe '#dump' do + context 'when mode is :json' do + subject(:json) { abc.dump(:json) } + + it { is_expected.to eq(JSON.pretty_generate(FactoryBot.body(:dashboard))) } + end + + context 'when mode is :yaml' do + subject(:yaml) { abc.dump(:yaml) } + + it { is_expected.to eq(FactoryBot.body(:dashboard).to_yaml) } + 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(FactoryBot.body(:dashboard)) } + 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', FactoryBot.body(:dashboard), {}) + .and_return(FactoryBot.body(:dashboard)) + 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', FactoryBot.body(:dashboard), {}) + .and_return(FactoryBot.body(:dashboard)) + 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..f483b6b 100644 --- a/spec/datadog_backup/monitors_spec.rb +++ b/spec/datadog_backup/monitors_spec.rb @@ -3,106 +3,140 @@ 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([FactoryBot.body(:monitor)]) + + allow_any_instance_of(DatadogBackup::Client).to receive(:get_body) + .with('/api/v1/monitor/12345', {}, {}) + .and_return(FactoryBot.body(: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: '12345', body: FactoryBot.body(:monitor)) } - 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: '12345') } + + it { is_expected.to be_a(described_class) } + end + + context 'with no id and with body' do + subject { described_class.new_resource(body: FactoryBot.body(:monitor)) } + + it { is_expected.to be_a(described_class) } + end + + context 'with no id and no body' do + subject { described_class.new_resource } + + it 'raises an ArgumentError' do + expect { subject }.to raise_error(ArgumentError) + end + 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([FactoryBot.body(:monitor)]) } + end + + describe '.get_by_id' do + subject { described_class.get_by_id('12345').id } + + it { is_expected.to eq('12345') } + 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) { build(:monitor) } + + describe '#diff' do + subject(:diff) { abc.diff('text') } + + before do + allow(abc).to receive(:body_from_backup) + .and_return({ 'id' => '12345', 'name' => 'Local Copy' }) + end + + it { + expect(diff).to eq(<<~EODIFF + --- + id: '12345' + -name: '12345' + +name: Local Copy + EODIFF + .chomp) + } + end + + describe '#dump' do + context 'when mode is :json' do + subject(:json) { abc.dump(:json) } + + it { is_expected.to eq(JSON.pretty_generate(FactoryBot.body(:monitor))) } end - expect(monitors.diff(123_455)).to eq '' + context 'when mode is :yaml' do + subject(:yaml) { abc.dump(:yaml) } - FileUtils.rm monitors.filename(123_455) + it { is_expected.to eq(FactoryBot.body(:monitor).to_yaml) } + end end - end - describe '#filename' do - subject { monitors.filename(123_455) } - it { is_expected.to eq("#{tempdir}/monitors/123455.json") } - end + describe '#myclass' do + subject { abc.myclass } + + 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(FactoryBot.body(: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', FactoryBot.body(: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/12345', FactoryBot.body(:monitor), {}) + .and_return(FactoryBot.body(:monitor)) + 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..3d9e0a4 --- /dev/null +++ b/spec/datadog_backup/resources/local_filesystem/class_methods_spec.rb @@ -0,0 +1,216 @@ +# 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") + 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 + 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", + "#{$options[:backup_dir]}/synthetics/mno-789-pqr.json", + "#{$options[:backup_dir]}/synthetics/stu-012-vwx.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', 'mno-789-pqr', 'stu-012-vwx') } + end + + describe '.all_file_ids_for_selected_resources' do + subject { resources.all_file_ids_for_selected_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 + + 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 + + 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 + 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::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 + 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 + + 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 + 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( + array_including( + "#{$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..2e0cf24 --- /dev/null +++ b/spec/datadog_backup/resources/local_filesystem_spec.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe DatadogBackup::Resources::LocalFilesystem 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 } + + 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..24bd74f 100644 --- a/spec/datadog_backup/synthetics_spec.rb +++ b/spec/datadog_backup/synthetics_spec.rb @@ -3,255 +3,159 @@ 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' => [ + FactoryBot.body(:synthetic_api), + FactoryBot.body(:synthetic_browser) + ] }) + + allow_any_instance_of(DatadogBackup::Client).to receive(:get_body) + .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/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/mno-789-pqr', {}, {}) + .and_raise(Faraday::ResourceNotFound) + + allow_any_instance_of(DatadogBackup::Client).to receive(:get_body) + .with('/api/v1/synthetics/tests/api/stu-456-vwx', {}, {}) + .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: 'mno-789-pqr', body: FactoryBot.body(:synthetic_api)) } - 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: 'mno-789-pqr') } - 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: FactoryBot.body(:synthetic_api)) } - 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 { described_class.new_resource } + + it 'raises an ArgumentError' do + expect { subject }.to raise_error(ArgumentError) + end + 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( + FactoryBot.body(:synthetic_api), + FactoryBot.body(:synthetic_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('mno-789-pqr').id } - it { is_expected.to eq browser_test } + it { is_expected.to eq('mno-789-pqr') } 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) { build(:synthetic_api) } - context 'when the remote is not found' do - subject(:invalid_diff) { synthetics.diff('invalid-id') } + describe '#diff' do + subject(:diff) { abc.diff('text') } 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' => 'mno-789-pqr', 'type' => 'api', 'title' => 'abc' }) end it { - expect(invalid_diff).to eq(%(---- {}\n+---\n+name: invalid-diff)) + expect(diff).to eq(<<~EODIFF + --- + public_id: mno-789-pqr + 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 + context 'when mode is :json' do + subject(:json) { abc.dump(:json) } - describe '#create' do - context 'when the type is api' do - subject(:create) { synthetics.create({ 'type' => 'api' }) } - - before do - stubs.post('/api/v1/synthetics/tests/api') { respond_with200({ 'public_id' => 'api-create-abc' }) } + it { is_expected.to eq(JSON.pretty_generate(FactoryBot.body(:synthetic_api))) } 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 + subject(:yaml) { abc.dump(:yaml) } - before do - stubs.post('/api/v1/synthetics/tests/browser') { respond_with200({ 'public_id' => 'browser-create-abc' }) } + it { is_expected.to eq(FactoryBot.body(:synthetic_api).to_yaml) } 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' }) } - before do - stubs.put('/api/v1/synthetics/tests/api/api-update-abc') { respond_with200({ 'public_id' => 'api-update-abc' }) } - end + describe '#myclass' do + subject { abc.myclass } - 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(FactoryBot.body(:synthetic_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', FactoryBot.body(:synthetic_api), {}) + .and_return(FactoryBot.body(:synthetic_api)) + 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/mno-789-pqr', FactoryBot.body(:synthetic_api), {}) + .and_return(FactoryBot.body(:synthetic_api)) + update end 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/spec_helper.rb b/spec/spec_helper.rb index 42288c8..b0c49ee 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -8,7 +8,12 @@ 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.key? 'DD_API_KEY' +ENV['DD_APP_KEY'] = 'abc123' unless ENV.key? 'DD_APP_KEY' + require 'tmpdir' +require 'factory_bot' require 'datadog_backup' SPEC_ROOT = __dir__ @@ -81,7 +86,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 +102,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