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