diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..2c7a407 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,4 @@ +language: ruby +rvm: + - 2.2.2 +before_install: gem install bundler -v 1.10.6 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..ce9bee7 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,13 @@ +# Contributor Code of Conduct + +As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. + +We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion. + +Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. + +This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/) diff --git a/Gemfile b/Gemfile index 5ffe34e..fa75df1 100644 --- a/Gemfile +++ b/Gemfile @@ -1,4 +1,3 @@ source 'https://rubygems.org' -# Specify your gem's dependencies in doggy.gemspec gemspec diff --git a/Gemfile.lock b/Gemfile.lock index 53d79cd..d373b31 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,29 +1,44 @@ PATH remote: . specs: - doggy (0.2.0) - dogapi (~> 1.17) - ejson (~> 1.0) - thor (~> 0.19) - thread (~> 0.2) + doggy (2.0.0) + json (~> 1.8.3) + parallel (~> 1.6.1) + thor (~> 0.19.1) + virtus (~> 1.0.5) GEM remote: https://rubygems.org/ specs: - dogapi (1.20.0) - multi_json - ejson (1.0.0) - multi_json (1.11.2) + axiom-types (0.1.1) + descendants_tracker (~> 0.0.4) + ice_nine (~> 0.11.0) + thread_safe (~> 0.3, >= 0.3.1) + coercible (1.0.0) + descendants_tracker (~> 0.0.1) + descendants_tracker (0.0.4) + thread_safe (~> 0.3, >= 0.3.1) + equalizer (0.0.11) + ice_nine (0.11.1) + json (1.8.3) + minitest (5.8.0) + parallel (1.6.1) rake (10.4.2) thor (0.19.1) - thread (0.2.2) + thread_safe (0.3.5) + virtus (1.0.5) + axiom-types (~> 0.1) + coercible (~> 1.0) + descendants_tracker (~> 0.0, >= 0.0.3) + equalizer (~> 0.0, >= 0.0.9) PLATFORMS ruby DEPENDENCIES - bundler (~> 1.9) + bundler (~> 1.10) doggy! + minitest rake (~> 10.0) BUNDLED WITH diff --git a/Rakefile b/Rakefile index 3466dbf..d6c5113 100644 --- a/Rakefile +++ b/Rakefile @@ -1,9 +1,10 @@ require "bundler/gem_tasks" require "rake/testtask" -Rake::TestTask.new do |t| - t.libs << 'test' +Rake::TestTask.new(:test) do |t| + t.libs << "test" + t.libs << "lib" + t.test_files = FileList['test/**/*_test.rb'] end -desc "Run tests" task :default => :test diff --git a/bin/console b/bin/console index 16a519d..bcb4d49 100755 --- a/bin/console +++ b/bin/console @@ -2,5 +2,13 @@ require "bundler/setup" require "doggy" + +# You can add fixtures and/or initialization code here to make experimenting +# with your gem easier. You can also use a different console, if you like. + +# (If you use this, don't forget to add pry to your Gemfile!) +# require "pry" +# Pry.start + require "irb" IRB.start diff --git a/bin/setup b/bin/setup index 7913e05..b65ed50 100755 --- a/bin/setup +++ b/bin/setup @@ -3,3 +3,5 @@ set -euo pipefail IFS=$'\n\t' bundle install + +# Do any other automated setup that you need to do here diff --git a/doggy.gemspec b/doggy.gemspec index a17159b..fe49fb0 100644 --- a/doggy.gemspec +++ b/doggy.gemspec @@ -6,23 +6,33 @@ require 'doggy/version' Gem::Specification.new do |spec| spec.name = "doggy" spec.version = Doggy::VERSION - spec.authors = ["Vlad Gorodetsky"] - spec.email = ["v@gor.io"] + spec.authors = ["Vlad Gorodetsky", "Andre Medeiros"] + spec.email = ["v@gor.io", "me@andremedeiros.info"] spec.summary = %q{Syncs DataDog dashboards, alerts, screenboards, and monitors.} spec.description = %q{Syncs DataDog dashboards, alerts, screenboards, and monitors.} spec.homepage = "http://github.com/bai/doggy" spec.license = "MIT" + # Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or + # delete this section to allow pushing this gem to any host. + if spec.respond_to?(:metadata) + spec.metadata['allowed_push_host'] = "TODO: Set to 'http://mygemserver.com'" + else + raise "RubyGems 2.0 or newer is required to protect against public gem pushes." + end + spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } spec.bindir = "exe" spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } spec.require_paths = ["lib"] - spec.add_development_dependency "bundler", "~> 1.9" + spec.add_dependency "json", "~> 1.8.3" + spec.add_dependency "parallel", "~> 1.6.1" + spec.add_dependency "thor", "~> 0.19.1" + spec.add_dependency "virtus", "~> 1.0.5" + + spec.add_development_dependency "bundler", "~> 1.10" spec.add_development_dependency "rake", "~> 10.0" - spec.add_dependency "thor", "~> 0.19" - spec.add_dependency "dogapi", "~> 1.17" - spec.add_dependency "thread", "~> 0.2" - spec.add_dependency "ejson", "~> 1.0" + spec.add_development_dependency "minitest" end diff --git a/examples/my-monitor.rb b/examples/my-monitor.rb deleted file mode 100644 index 52b496d..0000000 --- a/examples/my-monitor.rb +++ /dev/null @@ -1,14 +0,0 @@ -created_at = Time.parse('2015-01-01 14:00:01').to_i * 1000 - -query <<-QUERY - sum(last_1m):sum:Engine.current_thrust.status{status:error}.as_count() < 50 -QUERY - -obj({ - created_at: created_at, - id: 100500, - message: "Houston, we have a problem @pagerduty-Houston", - name: "Engine thrust", - query: query, - type: "query alert" -}) diff --git a/exe/doggy b/exe/doggy index bbb8bfa..d67e14e 100755 --- a/exe/doggy +++ b/exe/doggy @@ -1,12 +1,4 @@ #!/usr/bin/env ruby -# Exit cleanly from an early interrupt -Signal.trap('INT') { exit 1 } - -require 'bundler/setup' -require 'doggy/friendly_errors' - -Doggy.with_friendly_errors do - require 'doggy/cli' - Doggy::CLI.start(ARGV, :debug => true) -end +require "doggy" +Doggy::CLI.start(ARGV) diff --git a/lib/doggy.rb b/lib/doggy.rb index 227d179..4ac8b81 100644 --- a/lib/doggy.rb +++ b/lib/doggy.rb @@ -1,103 +1,63 @@ -require 'fileutils' -require 'pathname' -require 'json' -require 'yaml' -require 'dogapi' - -require 'doggy/friendly_errors' - -require 'doggy/version' -require 'doggy/errors' -require 'doggy/shared_helpers' -require 'doggy/client' -require 'doggy/worker' -require 'doggy/definition' -require 'doggy/dsl' -require 'doggy/serializer/json' -require 'doggy/serializer/yaml' -require 'doggy/model/dash' -require 'doggy/model/monitor' -require 'doggy/model/screen' +require "pathname" +require "net/http" + +require "doggy/cli" +require "doggy/cli/edit" +require "doggy/cli/mute" +require "doggy/cli/pull" +require "doggy/cli/push" +require "doggy/cli/unmute" +require "doggy/model" +require "doggy/models/dashboard" +require "doggy/models/monitor" +require "doggy/models/screen" +require "doggy/version" module Doggy - DOG_SKIP_REGEX = /😱|:scream:/i.freeze - MANAGED_BY_DOGGY_REGEX = /🐶|\:dog\:/i.freeze - DEFAULT_SERIALIZER_CLASS = Doggy::Serializer::Json + DOG_SKIP_REGEX = /\xF0\x9F\x98\xB1|:scream:/i.freeze + MANAGED_BY_DOGGY_REGEX = /\xF0\x9F\x90\xB6|:dog:/i.freeze - class << self - # @option arguments [Constant] :serializer A specific serializer class to use, will be initialized by doggy and passed the object instance - def serializer(options = {}) - @serializer ||= options[:serializer] ? options[:serializer] : DEFAULT_SERIALIZER_CLASS - end + extend self - def client - Doggy::Client.new - end + def ui + (defined?(@ui) && @ui) || (self.ui = Thor::Shell::Color.new) + end - def objects_path - @objects_path ||= Pathname.new('objects').expand_path(SharedHelpers.find_root).expand_path.tap { |path| FileUtils.mkdir_p(path) } - end + def ui=(ui) + @ui = ui + end - def load_item(f) - item = case File.extname(f) - when '.yaml', '.yml' then Doggy::Serializer::Yaml.load(File.read(f)) - when '.json' then Doggy::Serializer::Json.load(File.read(f)) - when '.rb' then Doggy::Dsl.evaluate(f).obj - else raise InvalidItemType - end + def object_root + @object_root ||= Pathname.new('objects').expand_path(repo_root) + end - # Hackery to support legacy dash format - { - [ - determine_type(item), item['id'] || item['dash']['id'] - ] => item['dash'] ? item['dash'] : item - } - end + def repo_root + # TODO: Raise error when root can't be found + current_dir = Dir.pwd - def edit(id_or_filename) - if id_or_filename =~ /json|yml|yaml/ - item_from_filename = Doggy.load_item(Doggy.objects_path.join(id_or_filename)) - id = item_from_filename.keys[0][1] + while current_dir != '/' do + if File.exists?(File.join(current_dir, 'Gemfile')) then + return Pathname.new(current_dir) else - id = id_or_filename + current_dir = File.expand_path('../', current_dir) end - - object = (item_from_filename || all_local_items).detect { |(type, object_id), object| object_id.to_s == id.to_s } - if object && object[0] && object[0][0] && type = object[0][0].sub(/^[a-z\d]*/) { $&.capitalize } - Object.const_get("Doggy::#{type}").edit(id) - end - end - - def determine_type(item) - return 'dash' if item['graphs'] || item['dash'] - return 'monitor' if item['message'] - return 'screen' if item['board_title'] - raise InvalidItemType end + end - def emit_shipit_deployment - Doggy.client.dog.emit_event( - Dogapi::Event.new(ENV['REVISION'], msg_title: "ShipIt Deployment by #{ENV['USER']}", tags: %w(audit shipit), source_type_name: 'shipit') - ) - rescue => e - puts "Exception: #{e.message}" - end + def api_key + ENV['DATADOG_API_KEY'] || secrets['datadog_api_key'] + end - def current_sha - now = Time.now.to_i - month_ago = now - 3600 * 24 * 30 - events = Doggy.client.dog.stream(month_ago, now, tags: %w(audit shipit))[1]['events'] + def application_key + ENV['DATADOG_APP_KEY'] || secrets['datadog_app_key'] + end - events[0]['text'] # most recetly deployed SHA - rescue => e - puts "Exception: #{e.message}" - end +protected - def all_local_items - @all_local_items ||= Dir[Doggy.objects_path.join('**/*')].inject({}) do |memo, file| - next if File.directory?(file) - memo.merge!(load_item(file)) - end - end + def secrets + @secrets ||= begin + raw = File.read(repo_root.join('secrets.json')) + JSON.parse(raw) + end end -end +end # Doggy diff --git a/lib/doggy/cli.rb b/lib/doggy/cli.rb index 7582a83..e86f868 100644 --- a/lib/doggy/cli.rb +++ b/lib/doggy/cli.rb @@ -1,106 +1,64 @@ -require 'thor' -require 'doggy' +require "thor" module Doggy class CLI < Thor include Thor::Actions - def self.start(*) - super - rescue Exception => e - raise e - ensure - end - - def initialize(*args) - super - rescue UnknownArgumentError => e - raise Doggy::InvalidOption, e.message - ensure - self.options ||= {} - end - - check_unknown_options!(:except => [:config, :exec]) - stop_on_unknown_option! :exec - - desc "pull OBJECT_ID OBJECT_ID OBJECT_ID", "Pulls objects from DataDog" + desc "pull", "Pulls objects from Datadog" long_desc <<-D - Pull objects from DataDog. If pull is successful, Doggy exits with a status of 0. - If not, the error is displayed and Doggy exits status 1. + Pull objects from Datadog. All objects are pulled unless the type switches + are used. D - def pull(*ids) - require 'doggy/cli/pull' - Pull.new(options.dup, ids).run - end - desc "push [OBJECT_ID OBJECT_ID OBJECT_ID]", "Pushes objects to DataDog" - long_desc <<-D - Pushes objects to DataDog. If push is successful, Doggy exits with a status of 0. - If not, the error is displayed and Doggy exits status 1. - D - def push(*ids) - require 'doggy/cli/push' - Push.new(options.dup, ids).run - end + method_option "dashboards", type: :boolean, desc: 'Pull dashboards' + method_option "monitors", type: :boolean, desc: 'Pull monitors' + method_option "screens", type: :boolean, desc: 'Pull screens' - desc "edit OBJECT_ID", "Edit an existing object on DataDog" - long_desc <<-D - Opens default browser pointing to an object to edit it visually. After you finish, it will - display edit result. - D - def edit(id) - require 'doggy/cli/edit' - Edit.new(options.dup, id).run + def pull + CLI::Pull.new(options.dup).run end - desc "delete OBJECT_ID OBJECT_ID OBJECT_ID", "Deletes objects from DataDog" + desc "push", "Pushes objects to Datadog" long_desc <<-D - Deletes objects from DataDog. If delete is successful, Doggy exits with a status of 0. - If not, the error is displayed and Doggy exits status 1. + Pushes objects to Datadog. Any objects that aren't skipped and don't have + the marker in their title will get it as a result of a push. D - def delete(*ids) - require 'doggy/cli/delete' - Delete.new(options.dup, ids).run + + method_option "dashboards", type: :boolean, desc: 'Pull dashboards' + method_option "monitors", type: :boolean, desc: 'Pull monitors' + method_option "screens", type: :boolean, desc: 'Pull screens' + + def push + CLI::Push.new(options.dup).run end + desc "mute OBJECT_ID OBJECT_ID OBJECT_ID", "Mutes monitor on DataDog" long_desc <<-D - Mutes monitor on DataDog. If mute is successful, Doggy exits with a status of 0. - If not, the error is displayed and Doggy exits status 1. + Mutes monitors on Datadog. D + def mute(*ids) - require 'doggy/cli/mute' - Mute.new(options.dup, ids).run + CLI::Mute.new(options.dup, ids).run end desc "unmute OBJECT_ID OBJECT_ID OBJECT_ID", "Unmutes monitor on DataDog" long_desc <<-D - Deletes objects from DataDog. If delete is successful, Doggy exits with a status of 0. - If not, the error is displayed and Doggy exits status 1. + Unmutes monitors on datadog D + def unmute(*ids) - require 'doggy/cli/unmute' - Unmute.new(options.dup, ids).run + CLI::Unmute.new(options.dup, ids).run end - desc "sha", "Detects the most recent SHA deployed by ShipIt" + desc "edit OBJECT_ID", "Edits an object" long_desc <<-D - Scans DataDog event stream for shipit events what contain most recently deployed version - of DataDog properties. - If not, the error is displayed and Doggy exits status 1. + Edits an object D - def sha - require 'doggy/cli/sha' - Sha.new.run - end - desc "version", "Prints Doggy version" - long_desc <<-D - Prints Doggy version - D - def version - require 'doggy/cli/version' - Version.new.run + def edit(id) + CLI::Edit.new(options.dup, id).run end end end + diff --git a/lib/doggy/cli/delete.rb b/lib/doggy/cli/delete.rb deleted file mode 100644 index dfa4ee6..0000000 --- a/lib/doggy/cli/delete.rb +++ /dev/null @@ -1,21 +0,0 @@ -module Doggy - class CLI::Delete - attr_reader :options, :ids - - def initialize(options, ids) - @options = options - @ids = ids - end - - def run - begin - Doggy::Dash.delete(ids) - Doggy::Monitor.delete(ids) - Doggy::Screen.delete(ids) - rescue DoggyError - puts "Delete failed." - exit 1 - end - end - end -end diff --git a/lib/doggy/cli/edit.rb b/lib/doggy/cli/edit.rb index bf0244d..92cc59f 100644 --- a/lib/doggy/cli/edit.rb +++ b/lib/doggy/cli/edit.rb @@ -1,14 +1,41 @@ module Doggy class CLI::Edit - attr_reader :options, :id - - def initialize(options, id) + def initialize(options, param) @options = options - @id = id + @param = param end def run - Doggy.edit(id) + resource = resource_by_param + return Doggy.ui.error("Could not find resource with #{ @param }") unless resource + + Dir.chdir(File.dirname(resource.path)) do + system("open '#{ resource.human_edit_url }'") + while !Doggy.ui.yes?('Are you done editing?') do + Doggy.ui.say "run, rabbit run / dig that hole, forget the sun / and when at last the work is done / don't sit down / it's time to dig another one" + end + + new_resource = resource.class.find(resource.id) + new_resource.path = resource.path + new_resource.save_local + end + end + + private + + def resource_by_param + resources = Doggy::Models::Dashboard.all_local + resources += Doggy::Models::Monitor.all_local + resources += Doggy::Models::Screen.all_local + + if @param =~ /^[0-9]+$/ then + id = @param.to_i + return resources.find { |res| res.id == id } + else + full_path = File.expand_path(@param, Dir.pwd) + return resources.find { |res| res.path == full_path } + end end end end + diff --git a/lib/doggy/cli/mute.rb b/lib/doggy/cli/mute.rb index 0d47f89..8b6d406 100644 --- a/lib/doggy/cli/mute.rb +++ b/lib/doggy/cli/mute.rb @@ -1,19 +1,14 @@ module Doggy class CLI::Mute - attr_reader :options, :ids - def initialize(options, ids) @options = options - @ids = ids + @ids = ids end def run - begin - Doggy::Monitor.mute(ids) - rescue DoggyError - puts "Mute failed." - exit 1 - end + monitors = @ids.map { |id| Doggy::Models::Monitor.find(id) } + monitors.each(&:mute) end end end + diff --git a/lib/doggy/cli/pull.rb b/lib/doggy/cli/pull.rb index 0280b71..816a6ff 100644 --- a/lib/doggy/cli/pull.rb +++ b/lib/doggy/cli/pull.rb @@ -1,21 +1,29 @@ module Doggy class CLI::Pull - attr_reader :options, :ids - - def initialize(options, ids) + def initialize(options) @options = options - @ids = ids end def run - begin - Doggy::Dash.download(ids) - Doggy::Monitor.download(ids) - Doggy::Screen.download(ids) - rescue DoggyError - puts "Pull failed." - exit 1 - end + pull_resources('dashboards', Models::Dashboard) if should_pull?('dashboards') + pull_resources('monitors', Models::Monitor) if should_pull?('monitors') + pull_resources('screens', Models::Screen) if should_pull?('screens') + end + + private + + def should_pull?(resource) + @options.empty? || @options[resource] + end + + def pull_resources(name, klass) + Doggy.ui.say "Pulling #{ name }" + local_resources = klass.all_local + remote_resources = klass.all + + klass.assign_paths(remote_resources, local_resources) + remote_resources.each(&:save_local) end end end + diff --git a/lib/doggy/cli/push.rb b/lib/doggy/cli/push.rb index bcb9f1c..f1c0c1b 100644 --- a/lib/doggy/cli/push.rb +++ b/lib/doggy/cli/push.rb @@ -1,28 +1,26 @@ module Doggy class CLI::Push - attr_reader :options, :ids - - def initialize(options, ids) + def initialize(options) @options = options - @ids = ids end def run - begin - if ids.any? - Doggy::Dash.upload(ids) - Doggy::Monitor.upload(ids) - Doggy::Screen.upload(ids) - else - Doggy::Dash.upload_all - Doggy::Monitor.upload_all - Doggy::Screen.upload_all - Doggy.emit_shipit_deployment if ENV['SHIPIT'] - end - rescue DoggyError - puts "Push failed." - exit 1 - end + push_resources('dashboards', Models::Dashboard) if should_push?('dashboards') + push_resources('monitors', Models::Monitor) if should_push?('monitors') + push_resources('screens', Models::Screen) if should_push?('screens') + end + + private + + def should_push?(resource) + @options.empty? || @options[resource] + end + + def push_resources(name, klass) + Doggy.ui.say "Pushing #{ name }" + local_resources = klass.all_local + local_resources.each(&:save) end end end + diff --git a/lib/doggy/cli/sha.rb b/lib/doggy/cli/sha.rb deleted file mode 100644 index 2c87345..0000000 --- a/lib/doggy/cli/sha.rb +++ /dev/null @@ -1,12 +0,0 @@ -module Doggy - class CLI::Sha - def run - begin - print Doggy.current_sha - rescue DoggyError - puts "Could not fetch latest SHA from DataDog." - exit 1 - end - end - end -end diff --git a/lib/doggy/cli/unmute.rb b/lib/doggy/cli/unmute.rb index db2ce4d..41b79b1 100644 --- a/lib/doggy/cli/unmute.rb +++ b/lib/doggy/cli/unmute.rb @@ -1,19 +1,15 @@ module Doggy class CLI::Unmute - attr_reader :options, :ids - def initialize(options, ids) @options = options - @ids = ids + @ids = ids end def run - begin - Doggy::Monitor.unmute(ids) - rescue DoggyError - puts "Unmute failed." - exit 1 - end + monitors = @ids.map { |id| Doggy::Models::Monitor.find(id) } + monitors.each(&:unmute) end end end + + diff --git a/lib/doggy/cli/version.rb b/lib/doggy/cli/version.rb deleted file mode 100644 index 1c55fe5..0000000 --- a/lib/doggy/cli/version.rb +++ /dev/null @@ -1,7 +0,0 @@ -module Doggy - class CLI::Version - def run - print Doggy::VERSION - end - end -end diff --git a/lib/doggy/client.rb b/lib/doggy/client.rb deleted file mode 100644 index d02f795..0000000 --- a/lib/doggy/client.rb +++ /dev/null @@ -1,46 +0,0 @@ -class Dogapi::APIService - attr_reader :api_key, :application_key # as they are useless in the parent class -end - -module Doggy - class Client - def api_key - @api_key ||= ENV.fetch('DATADOG_API_KEY', ejson_config[:datadog_api_key]) - rescue => e - puts "[DogSync#api_key] Exception: #{e.message}" - raise - end - - def app_key - @app_key ||= ENV.fetch('DATADOG_APP_KEY', ejson_config[:datadog_app_key]) - rescue => e - puts "[DogSync#app_key] Exception: #{e.message}" - raise - end - - def dog - @dog ||= Dogapi::Client.new(api_key, app_key) - end - - def api_service - @api_service ||= Dogapi::APIService.new(api_key, app_key) - end - - def api_service_params - @api_service_params ||= { api_key: Doggy.client.api_service.api_key, application_key: Doggy.client.api_service.application_key } - end - - private - - def ejson_config - @ejson_config ||= begin - if File.exists?('secrets.json') - secrets = JSON.parse(File.read('secrets.json')) - { datadog_api_key: secrets['datadog_api_key'], datadog_app_key: secrets['datadog_app_key'] } - else - {} - end - end - end - end -end diff --git a/lib/doggy/definition.rb b/lib/doggy/definition.rb deleted file mode 100644 index 8c93a0b..0000000 --- a/lib/doggy/definition.rb +++ /dev/null @@ -1,9 +0,0 @@ -module Doggy - class Definition - attr_reader :obj - - def initialize(obj) - @obj = obj - end - end -end diff --git a/lib/doggy/dsl.rb b/lib/doggy/dsl.rb deleted file mode 100644 index 9dc8843..0000000 --- a/lib/doggy/dsl.rb +++ /dev/null @@ -1,73 +0,0 @@ -module Doggy - class Dsl - def self.evaluate(object_file) - builder = new - builder.eval_file(object_file) - builder.to_definition - end - - def initialize - @obj = {} - end - - def eval_file(object_file, contents = nil) - contents ||= File.open(object_file.to_s, "rb") { |f| f.read } - instance_eval(contents, object_file.to_s, 1) - rescue Exception => e - message = "There was an error " \ - "#{e.is_a?(ObjectFileEvalError) ? "evaluating" : "parsing"} " \ - "`#{File.basename object_file.to_s}`: #{e.message}" - - raise DSLError.new(message, object_file, e.backtrace, contents) - end - - def obj(structure) - @obj = structure - end - - # @return [Definition] the parsed object definition. - def to_definition - Definition.new(@obj) - end - - def method_missing(name, *args) - raise Doggy::ObjectFileError, "Undefined local variable or method `#{name}' for object file" - end - - private - - class DSLError < Doggy::ObjectFileError - # @return [String] the message that should be presented to the user. - attr_reader :message - - # @return [String] the path of the dsl file that raised the exception. - attr_reader :dsl_path - - # @return [Exception] the backtrace of the exception raised by the evaluation of the dsl file. - attr_reader :backtrace - - # @param [Exception] backtrace @see backtrace - # @param [String] dsl_path @see dsl_path - def initialize(message, dsl_path, backtrace, contents = nil) - @status_code = $!.respond_to?(:status_code) && $!.status_code - - @message = message - @dsl_path = dsl_path - @backtrace = backtrace - @contents = contents - end - - def status_code - @status_code || super - end - - # @return [String] the contents of the DSL that cause the exception to - # be raised. - def contents - @contents ||= begin - dsl_path && File.exist?(dsl_path) && File.read(dsl_path) - end - end - end - end -end diff --git a/lib/doggy/errors.rb b/lib/doggy/errors.rb deleted file mode 100644 index 814179a..0000000 --- a/lib/doggy/errors.rb +++ /dev/null @@ -1,12 +0,0 @@ -module Doggy - class DoggyError < StandardError - def self.status_code(code) - define_method(:status_code) { code } - end - end - - class ObjectFileError < DoggyError; status_code(12); end - class ObjectFileEvalError < DoggyError; status_code(11); end - class InvalidOption < DoggyError; status_code(15); end - class InvalidItemType < DoggyError; status_code(10); end -end diff --git a/lib/doggy/friendly_errors.rb b/lib/doggy/friendly_errors.rb deleted file mode 100644 index 228e232..0000000 --- a/lib/doggy/friendly_errors.rb +++ /dev/null @@ -1,76 +0,0 @@ -require 'cgi' -require 'thor' -require 'doggy' - -module Doggy - def self.with_friendly_errors - yield - rescue Doggy::Dsl::DSLError => e - puts e.message - exit e.status_code - rescue Doggy::DoggyError => e - puts e.message - puts e - exit e.status_code - rescue Thor::AmbiguousTaskError => e - puts e.message - exit 15 - rescue Thor::UndefinedTaskError => e - puts e.message - exit 15 - rescue Thor::Error => e - puts e.message - exit 1 - rescue Interrupt => e - puts "\nQuitting..." - puts e - exit 1 - rescue SystemExit => e - exit e.status - rescue Exception => e - request_issue_report_for(e) - exit 1 - end - - def self.request_issue_report_for(e) - puts <<-EOS.gsub(/^ {6}/, "") - --- ERROR REPORT TEMPLATE ------------------------------------------------------- - - What did you do? - - I ran the command `#{$PROGRAM_NAME} #{ARGV.join(" ")}` - - - What did you expect to happen? - - I expected Doggy to... - - - What happened instead? - - Instead, what actually happened was... - - - Error details - - #{e.class}: #{e.message} - #{e.backtrace.join("\n ")} - - --- TEMPLATE END ---------------------------------------------------------------- - - EOS - - puts "Unfortunately, an unexpected error occurred, and Doggy cannot continue." - - puts <<-EOS.gsub(/^ {6}/, "") - - First, try this link to see if there are any existing issue reports for this error: - #{issues_url(e)} - - If there aren't any reports for this error yet, please create copy and paste the report template above into a new issue. Don't forget to anonymize any private data! The new issue form is located at: - https://github.com/bai/doggy/issues/new - EOS - end - - def self.issues_url(exception) - "https://github.com/bai/doggy/search?q=" \ - "#{CGI.escape(exception.message.lines.first.chomp)}&type=Issues" - end -end diff --git a/lib/doggy/model.rb b/lib/doggy/model.rb new file mode 100644 index 0000000..dfb3445 --- /dev/null +++ b/lib/doggy/model.rb @@ -0,0 +1,155 @@ +require "json" +require "parallel" +require "uri" +require "virtus" + +module Doggy + class Model + include Virtus.model + + # This stores the path on disk. We don't define it as a model attribute so + # it doesn't get serialized. + attr_accessor :path + + # This stores whether the resource has been loaded locally or remotely. + attr_accessor :loading_source + + class << self + def root=(root) + @root = root.to_s + end + + def root + @root || nil + end + + def find(id) + attributes = request(:get, resource_url(id)) + resource = new(attributes) + + resource.loading_source = :remote + resource + end + + def assign_paths(remote_resources, local_resources) + remote_resources.each do |remote| + local = local_resources.find { |l| l.id == remote.id } + next unless local + + remote.path = local.path + end + end + + def all + collection = request(:get, resource_url) + if collection.is_a?(Hash) && collection.keys.length == 1 + collection = collection.values.first + end + + ids = collection + .map { |record| new(record) } + .select { |instance| instance.managed? } + .map { |instance| instance.id } + + Parallel.map(ids) { |id| find(id) } + end + + def all_local + @all_local ||= begin + # TODO: Add serializer support here + files = Dir[Doggy.object_root.join("**/*.json")] + resources = Parallel.map(files) do |file| + raw = File.read(file) + + begin + attributes = JSON.parse(raw) + rescue JSON::ParserError + Doggy.ui.error "Could not parse #{ file }." + next + end + + next unless infer_type(attributes) == self + + resource = new(attributes) + resource.path = file + resource.loading_source = :local + resource + end + + resources.compact + end + end + + def infer_type(attributes) + return Models::Dashboard if attributes['graphs'] + return Models::Monitor if attributes['message'] + return Models::Screen if attributes['board_title'] + end + + def request(method, url, body = nil) + uri = URI(url) + uri.query = "api_key=#{ Doggy.api_key }&application_key=#{ Doggy.application_key }" + + http = Net::HTTP.new(uri.host, uri.port) + http.use_ssl = (uri.scheme == 'https') + + request = case method + when :get then Net::HTTP::Get.new(uri.request_uri) + when :post then Net::HTTP::Post.new(uri.request_uri) + when :put then Net::HTTP::Put.new(uri.request_uri) + end + + request.content_type = 'application/json' + request.body = body if body + + response = http.request(request) + JSON.parse(response.body) + end + + protected + + def resource_url(id = nil) + raise NotImplementedError, "#resource_url has to be implemented." + end + end + + def initialize(attributes = nil) + root_key = self.class.root + + return super unless attributes && root_key + return super unless attributes[root_key].is_a?(Hash) + + attributes = attributes[root_key] + super(attributes) + end + + def save_local + @path ||= Doggy.object_root.join("#{ id }.json") + File.open(@path, 'w') { |f| f.write(JSON.pretty_generate(to_h)) } + end + + def save + ensure_managed_emoji! + + body = JSON.dump(to_h) + if !id then + attributes = request(:post, resource_url, body) + self.id = self.class.new(attributes).id + save_local + else + request(:put, resource_url(id), body) + end + end + + protected + + def resource_url(id = nil) + self.class.resource_url(id) + end + + def request(method, uri, body = nil) + self.class.request(method, uri, body) + end + end # Model +end # Doggy + diff --git a/lib/doggy/model/dash.rb b/lib/doggy/model/dash.rb deleted file mode 100644 index e09bfdb..0000000 --- a/lib/doggy/model/dash.rb +++ /dev/null @@ -1,93 +0,0 @@ -module Doggy - class Dash - def self.upload_all - objects = Doggy.all_local_items.find_all { |(type, id), object| type == 'dash' } - puts "Uploading #{objects.size} dashboards" - upload(objects.map { |(type, id), object| id }) - rescue => e - puts "Exception: #{e.message}" - end - - def self.download(ids) - Doggy::Worker.new(threads: Doggy::Worker::CONCURRENT_STREAMS) { |id| new(id: id).save }.call([*ids]) - end - - def self.upload(ids) - Doggy::Worker.new(threads: Doggy::Worker::CONCURRENT_STREAMS) { |id| new(id: id).push }.call([*ids]) - end - - def self.edit(id) - system %{open "https://app.datadoghq.com/dash/#{id}"} - if SharedHelpers.agree("Are you done?") - puts 'Here is the output of your edit:' - puts Doggy::Serializer::Json.dump(new(id: id).raw) - else - puts "run, rabbit run / dig that hole, forget the sun / and when at last the work is done / don't sit down / it's time to dig another one" - edit(id) - end - end - - def initialize(**options) - @id = options[:id] - @title = options[:title] || raw_local['title'] - @description = options[:description] || raw_local['description'] - @graphs = options[:graphs] || raw_local['graphs'] - @template_variables = options[:template_variables] || raw_local['template_variables'] - end - - def raw - @raw ||= begin - status, result = Doggy.client.dog.get_dashboard(@id) - result && result['dash'] && result['dash'].sort.to_h || {} - end - end - - def raw_local - return {} unless File.exists?(path) - @raw_local ||= begin - object = Doggy.serializer.load(File.read(path)) - object['dash'] ? object['dash'] : object - end - end - - def save - return if raw.nil? || raw.empty? # do not save if it's empty - return if raw['errors'] # do not save if there are any errors - return if raw['title'] =~ Doggy::DOG_SKIP_REGEX # do not save if it had skip tag in title - - File.write(path, Doggy.serializer.dump(raw)) - end - - def push - return unless File.exists?(path) - return if @title =~ Doggy::DOG_SKIP_REGEX - return unless Doggy.determine_type(raw_local) == 'dash' - - # Managed by doggy (TM) - @title = @title =~ MANAGED_BY_DOGGY_REGEX ? @title : @title + " 🐶" - - if @id - SharedHelpers.with_retry do - Doggy.client.dog.update_dashboard(@id, @title, @description, @graphs, @template_variables) - end - else - SharedHelpers.with_retry do - dash = Doggy.client.dog.create_dashboard(@title, @description, @graphs) - end - # FIXME: Remove duplication - @id = dash[1]['id'] - @graphs = dash[1]['graphs'] - end - end - - def delete - Doggy.client.dog.delete_dashboard(@id) - end - - private - - def path - "#{Doggy.objects_path}/#{@id}.json" - end - end -end diff --git a/lib/doggy/model/monitor.rb b/lib/doggy/model/monitor.rb deleted file mode 100644 index f7cf59f..0000000 --- a/lib/doggy/model/monitor.rb +++ /dev/null @@ -1,142 +0,0 @@ -module Doggy - class Monitor - def self.upload_all - objects = Doggy.all_local_items.find_all { |(type, id), object| type == 'monitor' } - puts "Uploading #{objects.size} monitors" - upload(objects.map { |(type, id), object| id }) - rescue => e - puts "Exception: #{e.message}" - end - - def self.download(ids) - Doggy::Worker.new(threads: Doggy::Worker::CONCURRENT_STREAMS) { |id| new(id: id).save }.call([*ids]) - end - - def self.upload(ids) - Doggy::Worker.new(threads: Doggy::Worker::CONCURRENT_STREAMS) { |id| new(id: id).push }.call([*ids]) - end - - def self.mute(ids) - Doggy::Worker.new(threads: Doggy::Worker::CONCURRENT_STREAMS) { |id| new(id: id).mute }.call([*ids]) - end - - def self.unmute(ids) - Doggy::Worker.new(threads: Doggy::Worker::CONCURRENT_STREAMS) { |id| new(id: id).unmute }.call([*ids]) - end - - def self.edit(id) - system %{open "https://app.datadoghq.com/monitors##{id}"} - if SharedHelpers.agree("Are you done?") - puts 'Here is the output of your edit:' - puts Doggy::Serializer::Json.dump(new(id: id).raw) - else - puts "run, rabbit run / dig that hole, forget the sun / and when at last the work is done / don't sit down / it's time to dig another one" - edit(id) - end - end - - def initialize(**options) - @id = options[:id] - @query = options[:query] - @silenced = options[:silenced] - @name = options[:name] - @timeout_h = options[:timeout_h] - @message = options[:message] - @notify_audit = options[:notify_audit] - @notify_no_data = options[:notify_no_data] - @renotify_interval = options[:renotify_interval] - @escalation_message = options[:escalation_message] - @no_data_timeframe = options[:no_data_timeframe] - @silenced_timeout_ts = options[:silenced_timeout_ts] - end - - def raw - @raw ||= begin - status, alert = Doggy.client.dog.get_monitor(@id) - - return if status != '200' - - alert.delete('state') # delete unnecessary state - alert.delete('overall_state') # delete unnecessary state - if alert['options'] - alert['options'].delete('silenced') # delete unnecessary state - alert['options'] = alert['options'].sort.to_h # sort option keys; DataDog response is not ordered - end - alert && alert.sort.to_h - end - end - - def raw_local - return unless File.exists?(path) - @raw_local ||= Doggy.serializer.load(File.read(path)) - end - - def save - return if raw.nil? || raw.empty? # do not save if it's empty - return if raw['errors'] # do not save if there are any errors - return if raw['name'] =~ Doggy::DOG_SKIP_REGEX # do not save if it had skip tag in title - - File.write(path, Doggy.serializer.dump(raw)) - end - - def mute - Doggy.client.dog.mute_monitor(@id) - end - - def unmute - Doggy.client.dog.unmute_monitor(@id) - end - - def push - @name ||= raw_local['name'] - - return if @name =~ Doggy::DOG_SKIP_REGEX - return unless Doggy.determine_type(raw_local) == 'monitor' - - # Managed by doggy (TM) - @name = @name =~ MANAGED_BY_DOGGY_REGEX ? @name : @name + " 🐶" - - if @id - return unless File.exists?(path) - - SharedHelpers.with_retry do - Doggy.client.dog.update_monitor(@id, @query || raw_local['query'], { - name: @name || raw_local['name'], - timeout_h: @timeout_h || raw_local['timeout_h'], - message: @message || raw_local['message'], - notify_audit: @notify_audit || raw_local['notify_audit'], - notify_no_data: @notify_no_data || raw_local['notify_no_data'], - renotify_interval: @renotify_interval || raw_local['renotify_interval'], - escalation_message: @escalation_message || raw_local['escalation_message'], - no_data_timeframe: @no_data_timeframe || raw_local['no_data_timeframe'], - silenced_timeout_ts: @silenced_timeout_ts || raw_local['silenced_timeout_ts'], - options: { - silenced: mute_state_for(@id), - }, - }) - end - else - SharedHelpers.with_retry do - result = Doggy.client.dog.monitor('metric alert', @query, name: @name) - end - @id = result[1]['id'] - end - end - - def delete - Doggy.client.dog.delete_alert(@id) - end - - private - - def path - "#{Doggy.objects_path}/#{@id}.json" - end - - def mute_state_for(id) - if remote_state = Doggy.all_local_items.detect { |key, value| key == [ 'monitor', id.to_i ] } - remote_state[1]['options']['silenced'] if remote_state[1]['options'] - end - end - end -end diff --git a/lib/doggy/model/screen.rb b/lib/doggy/model/screen.rb deleted file mode 100644 index 95b7c3e..0000000 --- a/lib/doggy/model/screen.rb +++ /dev/null @@ -1,80 +0,0 @@ -module Doggy - class Screen - def self.upload_all - objects = Doggy.all_local_items.find_all { |(type, id), object| type == 'screen' } - puts "Uploading #{objects.size} screens" - upload(objects.map { |(type, id), object| id }) - rescue => e - puts "Exception: #{e.message}" - end - - def self.download(ids) - Doggy::Worker.new(threads: Doggy::Worker::CONCURRENT_STREAMS) { |id| new(id: id).save }.call([*ids]) - end - - def self.upload(ids) - Doggy::Worker.new(threads: Doggy::Worker::CONCURRENT_STREAMS) { |id| new(id: id).push }.call([*ids]) - end - - def self.edit(id) - system %{open "https://app.datadoghq.com/screen/#{id}"} - if SharedHelpers.agree("Are you done?") - puts 'Here is the output of your edit:' - puts Doggy::Serializer::Json.dump(new(id: id).raw) - else - puts "run, rabbit run / dig that hole, forget the sun / and when at last the work is done / don't sit down / it's time to dig another one" - edit(id) - end - end - - def initialize(**options) - @id = options[:id] - @description = options[:description] || raw_local - end - - def raw - @raw ||= begin - status, result = Doggy.client.dog.get_screenboard(@id) - result && result.sort.to_h - end - end - - def raw_local - return {} unless File.exists?(path) - @raw_local ||= Doggy.serializer.load(File.read(path)) - end - - def save - return if raw['errors'] # do now download an item if it doesn't exist - return if raw['board_title'] =~ Doggy::DOG_SKIP_REGEX - return if raw.empty? - File.write(path, Doggy.serializer.dump(raw)) - end - - def push - return if @description =~ Doggy::DOG_SKIP_REGEX - return unless Doggy.determine_type(raw_local) == 'screen' - if @id - SharedHelpers.with_retry do - Doggy.client.dog.update_screenboard(@id, @description) - end - else - SharedHelpers.with_retry do - result = Doggy.client.dog.create_screenboard(@description) - end - @id = result[1]['id'] - @description = result[1] - end - end - - def delete - Doggy.client.dog.delete_screenboard(@id) - end - - private - - def path - "#{Doggy.objects_path}/#{@id}.json" - end - end -end diff --git a/lib/doggy/models/dashboard.rb b/lib/doggy/models/dashboard.rb new file mode 100644 index 0000000..d89315c --- /dev/null +++ b/lib/doggy/models/dashboard.rb @@ -0,0 +1,37 @@ +module Doggy + module Models + class Dashboard < Doggy::Model + self.root = 'dash' + + attribute :id, Integer + attribute :title, String + attribute :description, String + + attribute :graphs, Array[Hash] + attribute :template_variables, Array[Hash] + + def self.resource_url(id = nil) + "https://app.datadoghq.com/api/v1/dash".tap do |base_url| + base_url << "/#{ id }" if id + end + end + + def managed? + !(title =~ Doggy::DOG_SKIP_REGEX) + end + + def ensure_managed_emoji! + return unless managed? + self.title += " \xF0\x9F\x90\xB6" + end + + def human_url + "https://app.datadoghq.com/dash/#{ id }" + end + + # Dashboards don't have a direct edit URL + alias_method :human_edit_url, :human_url + end # Dashboard + end # Models +end # Doggy + diff --git a/lib/doggy/models/monitor.rb b/lib/doggy/models/monitor.rb new file mode 100644 index 0000000..efa3d77 --- /dev/null +++ b/lib/doggy/models/monitor.rb @@ -0,0 +1,82 @@ +module Doggy + module Models + class Monitor < Doggy::Model + class Options + include Virtus.model + attr_accessor :monitor + + attribute :silenced, Hash + attribute :notify_audit, Boolean + attribute :notify_no_data, Boolean + attribute :no_data_timeframe, Integer + attribute :timeout_h, Integer + attribute :escalation_message, String + + def to_h + return super unless monitor.id && monitor.loading_source == :local + + # Pull remote silenced state. If we don't send this value, Datadog + # assumes that we want to unmute the monitor. + remote_monitor = Monitor.find(monitor.id) + self.silenced = remote_monitor.options.silenced + super + end + end + + attribute :id, Integer + attribute :org_id, Integer + attribute :name, String + + attribute :message, String + attribute :query, String + attribute :options, Options + attribute :tags, Array[String] + attribute :type, String + attribute :multi, Boolean + + def self.resource_url(id = nil) + "https://app.datadoghq.com/api/v1/monitor".tap do |base_url| + base_url << "/#{ id }" if id + end + end + + def initialize(attributes = nil) + super(attributes) + + options.monitor = self + end + + def managed? + !(name =~ Doggy::DOG_SKIP_REGEX) + end + + def ensure_managed_emoji! + return unless managed? + self.name += " \xF0\x9F\x90\xB6" + end + + def mute + return unless id + request(:post, "#{ resource_url(id) }/mute") + end + + def unmute + return unless id + request(:post, "#{ resource_url(id) }/unmute") + end + + def human_url + "https://app.datadoghq.com/monitors##{ id }" + end + + def human_edit_url + "https://app.datadoghq.com/monitors##{ id }/edit" + end + + def to_h + super.merge(options: options.to_h) + end + end # Monitor + end # Models +end # Doggy + diff --git a/lib/doggy/models/screen.rb b/lib/doggy/models/screen.rb new file mode 100644 index 0000000..59e26fc --- /dev/null +++ b/lib/doggy/models/screen.rb @@ -0,0 +1,37 @@ +module Doggy + module Models + class Screen < Doggy::Model + attribute :id, Integer + attribute :board_title, String + + attribute :board_bgtype, String + attribute :templated, Boolean + attribute :widgets, Array[Hash] + attribute :height, String + attribute :width, String + + def self.resource_url(id = nil) + "https://app.datadoghq.com/api/v1/screen".tap do |base_url| + base_url << "/#{ id }" if id + end + end + + def managed? + !(board_title =~ Doggy::DOG_SKIP_REGEX) + end + + def ensure_managed_emoji! + return unless managed? + self.board_title += " \xF0\x9F\x90\xB6" + end + + def human_url + "https://app.datadoghq.com/screen/#{ id }" + end + + # Screens don't have a direct edit URL + alias_method :human_edit_url, :human_url + end # Screen + end # Models +end # Doggy + diff --git a/lib/doggy/serializer/json.rb b/lib/doggy/serializer/json.rb deleted file mode 100644 index 2f07fda..0000000 --- a/lib/doggy/serializer/json.rb +++ /dev/null @@ -1,15 +0,0 @@ -module Doggy - module Serializer - class Json - # De-serialize a Hash from JSON string - def self.load(string) - ::JSON.load(string) - end - - # Serialize a Hash to JSON string - def self.dump(object, options = {}) - ::JSON.pretty_generate(object, options) + "\n" - end - end - end -end diff --git a/lib/doggy/serializer/yaml.rb b/lib/doggy/serializer/yaml.rb deleted file mode 100644 index bf0258f..0000000 --- a/lib/doggy/serializer/yaml.rb +++ /dev/null @@ -1,15 +0,0 @@ -module Doggy - module Serializer - class Yaml - # De-serialize a Hash from YAML string - def self.load(string) - ::YAML.load(string) - end - - # Serialize a Hash to YAML string - def self.dump(object, options = {}) - ::YAML.dump(object, options) - end - end - end -end diff --git a/lib/doggy/shared_helpers.rb b/lib/doggy/shared_helpers.rb deleted file mode 100644 index e7c56f1..0000000 --- a/lib/doggy/shared_helpers.rb +++ /dev/null @@ -1,63 +0,0 @@ -module Doggy - module SharedHelpers - MAX_TRIES = 5 - - def self.strip_heredoc(string) - indent = string.scan(/^[ \t]*(?=\S)/).min.try(:size) || 0 - string.gsub(/^[ \t]{#{indent}}/, '') - end - - def self.with_retry(times: MAX_TRIES, reraise: false) - tries = 0 - while tries < times - begin - yield - break - rescue => e - error "Caught error! Attempt #{tries}..." - error "#{e.class.name}: #{e.message}" - error "#{e.backtrace.join("\n")}" - tries += 1 - - raise e if tries >= times && reraise - end - end - end - - def self.agree(prompt) - raise Error, "Not a tty" unless $stdin.tty? - - puts prompt + " (Y/N)" - line = $stdin.readline.chomp.upcase - puts - line == "Y" - end - - def self.error(msg) - puts "[ERROR] #{ msg }" - end - - def self.find_root - File.dirname(find_file("Gemfile")) - end - - def self.find_file(*names) - search_up(*names) do |filename| - return filename if File.file?(filename) - end - end - - def self.search_up(*names) - previous = nil - current = File.expand_path(Pathname.pwd) - - until !File.directory?(current) || current == previous - names.each do |name| - filename = File.join(current, name) - yield filename - end - current, previous = File.expand_path("..", current), current - end - end - end -end diff --git a/lib/doggy/version.rb b/lib/doggy/version.rb index 03e99ee..e06838e 100644 --- a/lib/doggy/version.rb +++ b/lib/doggy/version.rb @@ -1,3 +1,3 @@ module Doggy - VERSION = '0.2.2' + VERSION = "2.0.0" end diff --git a/lib/doggy/worker.rb b/lib/doggy/worker.rb deleted file mode 100644 index 4ed292f..0000000 --- a/lib/doggy/worker.rb +++ /dev/null @@ -1,31 +0,0 @@ -require 'thread' -require 'thread/pool' - -Thread.abort_on_exception = true - -module Doggy - class Worker - # Spawn 10 threads for HTTP requests. - CONCURRENT_STREAMS = 10 - - def initialize(options = {}, &runner) - @runner = runner - @threads = options.fetch(:threads) - end - - def call(jobs) - results = [] - pool = Thread::Pool.new(@threads) - tasks = jobs.map { |job| - pool.process { - results << [ job, @runner.call(job) ] - } - } - pool.shutdown - if task_with_errors = tasks.detect { |task| task.exception } - raise task_with_errors.exception - end - results - end - end -end diff --git a/test/doggy/model_test.rb b/test/doggy/model_test.rb new file mode 100644 index 0000000..f1549f4 --- /dev/null +++ b/test/doggy/model_test.rb @@ -0,0 +1,43 @@ +require_relative '../test_helper' + +class Doggy::ModelTest < Minitest::Test + class DummyModel < Doggy::Model + attribute :id, Integer + attribute :title, String + end + + class DummyModelWithRoot < DummyModel + self.root = :dash + end + + def test_mass_assignment + instance = DummyModel.new(id: 1, title: 'Some test') + + assert_equal 1, instance.id + assert_equal 'Some test', instance.title + end + + def test_value_coallescion + instance = DummyModel.new(id: '2', title: :a_symbol) + + assert_equal 2, instance.id + assert_equal 'a_symbol', instance.title + end + + def test_root + first_instance = DummyModelWithRoot.new({'dash' => {'id' => 1, 'title' => 'Pipeline'}}) + second_instance = DummyModelWithRoot.new(id: 2, title: 'RPMs') + + assert_equal 1, first_instance.id + assert_equal 'Pipeline', first_instance.title + + assert_equal 2, second_instance.id + assert_equal 'RPMs', second_instance.title + end + + def test_type_inferrence + assert_equal Doggy::Models::Dashboard, Doggy::Model.infer_type({'graphs' => []}) + assert_equal Doggy::Models::Monitor, Doggy::Model.infer_type({'message' => ''}) + assert_equal Doggy::Models::Screen, Doggy::Model.infer_type({'board_title' => ''}) + end +end diff --git a/test/doggy/models/dashboard_test.rb b/test/doggy/models/dashboard_test.rb new file mode 100644 index 0000000..42b923e --- /dev/null +++ b/test/doggy/models/dashboard_test.rb @@ -0,0 +1,36 @@ +require_relative '../../test_helper' + +class Doggy::Models::DashboardTest < Minitest::Test + def test_attribute_loading + fixture = load_fixture('dashboard.json') + dashboard = Doggy::Models::Dashboard.new(fixture) + + assert_equal 2473, dashboard.id + assert_equal 'My Dashboard', dashboard.title + assert_equal 'An informative dashboard.', dashboard.description + end + + def test_managed_flag + first_managed_dashboard = Doggy::Models::Dashboard.new(title: 'Managed') + second_managed_dashboard = Doggy::Models::Dashboard.new(title: 'Managed :dog:') + third_managed_dashboard = Doggy::Models::Dashboard.new(title: "Managed \xF0\x9F\x90\xB6") + + first_skipped_dashboard = Doggy::Models::Dashboard.new(title: 'Non-managed :scream:') + second_skipped_dashboard = Doggy::Models::Dashboard.new(title: "Non-managed \xF0\x9F\x98\xB1") + + assert first_managed_dashboard.managed? + assert second_managed_dashboard.managed? + assert third_managed_dashboard.managed? + + refute first_skipped_dashboard.managed? + refute second_skipped_dashboard.managed? + end + + def test_ensure_managed + managed_dashboard = Doggy::Models::Dashboard.new(title: 'Managed') + managed_dashboard.ensure_managed_emoji! + + assert_equal "Managed \xF0\x9F\x90\xB6", managed_dashboard.title + end +end + diff --git a/test/doggy_test.rb b/test/doggy_test.rb new file mode 100644 index 0000000..bd70250 --- /dev/null +++ b/test/doggy_test.rb @@ -0,0 +1,4 @@ +require 'test_helper' + +class DoggyTest < Minitest::Test +end diff --git a/test/fixtures/dashboard.json b/test/fixtures/dashboard.json new file mode 100644 index 0000000..b8ff161 --- /dev/null +++ b/test/fixtures/dashboard.json @@ -0,0 +1,23 @@ +{ + "dash": { + "description": "An informative dashboard.", + "graphs": [ + { + "definition": { + "events": [], + "requests": [ + { + "q": "avg:system.mem.free{*}" + } + ], + "viz": "timeseries" + }, + "title": "Average Memory Free" + } + ], + "id": 2473, + "title": "My Dashboard" + }, + "resource": "/api/v1/dash/2473", + "url": "/dash/dash/2473" +} diff --git a/test/test_doggy.rb b/test/test_doggy.rb deleted file mode 100644 index eccaab5..0000000 --- a/test/test_doggy.rb +++ /dev/null @@ -1,5 +0,0 @@ -require 'minitest/autorun' -require 'doggy' - -class DoggyTest < Minitest::Test -end diff --git a/test/test_helper.rb b/test/test_helper.rb new file mode 100644 index 0000000..2366c3c --- /dev/null +++ b/test/test_helper.rb @@ -0,0 +1,14 @@ +$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) +require 'json' + +require 'doggy' + +require 'minitest/pride' +require 'minitest/autorun' + +def load_fixture(fixture_path) + path = File.expand_path("fixtures/#{ fixture_path }", "#{ __FILE__ }/../") + raw = File.read(path) + + JSON.parse(raw) +end