diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d87d4be --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +*.gem +*.rbc +.bundle +.config +.yardoc +Gemfile.lock +InstalledFiles +_yardoc +coverage +doc/ +lib/bundler/man +pkg +rdoc +spec/reports +test/tmp +test/version_tmp +tmp diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..16f9cdb --- /dev/null +++ b/.rspec @@ -0,0 +1,2 @@ +--color +--format documentation diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..fa75df1 --- /dev/null +++ b/Gemfile @@ -0,0 +1,3 @@ +source 'https://rubygems.org' + +gemspec diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..69d0b70 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +Copyright (c) 2012 Gosha Arinich + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..6d08f90 --- /dev/null +++ b/README.md @@ -0,0 +1,149 @@ +# Ruroku + +The Ruby client for Heroku API, built on top of official `heroku.rb` +gem. + +## Installation + +Add this line to your application's Gemfile: + + gem 'ruroku' + +And then execute: + + $ bundle + +Or install it yourself as: + + $ gem install ruroku + +## Usage + +Start by initiating a connection with Heroku API: + + heroku = Ruroku::API.new api_key: YOUR_HEROKU_API_KEY + +(You can leave out `:api_key` if `ENV['HEROKU_API_KEY']` is set +instead.) + +Now you can interact with Heroku API using Ruroku. + +### Apps + +Each API object has apps associated with the Heroku account. You can +access an Array of all the associated apps with `#apps`: + + heroku.apps + # => [#, #, #] + + app = heroku.apps.first + +You then can get additional app info: + + app.id + app.name + app.stack + app.git_url + app.slug_size + app.repo_size + app.dynos + apps.workers + # and a few less interesting ones + +Maintenance mode can be turned on and off: + + app.maintenance! + app.no_maintenance! + +### Addons + +To get a list of addons used by a particular app: + + addons = app.addons + # => [#, #, #] + + addon = app.addons.first + +It's possible perform several actions on addon collections: + +#### Add an addon + + addons.add 'addon:plan' + +#### Remove an addon + + addons.delete 'addon-name' + +#### Upgrade an addon + + addons.upgrade 'addon:new-plan' + +Each addon object is associated with the application. You can delete +addons from the app by calling `#delete` method on the addon object as +well: + + addon.delete! + +### Collaborators + +List all app collaborators: + + collaborators = app.collaborators + +#### Add a collaborator + + collaborators.add 'email@me.com' + +#### Remove a collaborator + + collaborators.delete 'email@me.com' + +or + + collaborator.delete! + +### Config variables + +List all app config vars: + + app.config_vars + +### Domains + +Access domains used by the application: + + app.domains + +### Processes + +Get current application processes: + + app.collaborators + +### Releases + +List all app releases: + + app.releases + +## Mock + +For practice or testing you can also use a simulated Heroku: + + require 'ruroku' + + heroku = Ruroku::API.new api_key: API_KEY, mock: true + +After that commands should still behave the same, but they will only modify some local data instead of updating the state of things on Heroku. + +## Contributing + +1. Fork it +2. Create your feature branch (`git checkout -b my-new-feature`) +3. Commit your changes (`git commit -am 'Added some feature'`) +4. Push to the branch (`git push origin my-new-feature`) +5. Create new Pull Request + +## License + +Released under the MIT license. diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..f57ae68 --- /dev/null +++ b/Rakefile @@ -0,0 +1,2 @@ +#!/usr/bin/env rake +require "bundler/gem_tasks" diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..9fcc1d9 --- /dev/null +++ b/TODO.md @@ -0,0 +1,6 @@ +* Logs +* Stacks +* Keys +* User info +* Collection finder methods +* Specs diff --git a/lib/ruroku.rb b/lib/ruroku.rb new file mode 100644 index 0000000..dd9de1f --- /dev/null +++ b/lib/ruroku.rb @@ -0,0 +1,27 @@ +require "heroku-api" +require "time" +require "active_support/core_ext/numeric" +require "active_support/core_ext/string" + +require "ruroku/base" +require "ruroku/nested_base" +require "ruroku/resource_set" + +require "ruroku/api" +require "ruroku/app" +require "ruroku/addon" +require "ruroku/addon_set" +require "ruroku/collaborator" +require "ruroku/collaborator_set" +require "ruroku/config_var" +require "ruroku/config_var_set" +require "ruroku/domain" +require "ruroku/domain_set" +require "ruroku/process" +require "ruroku/process_set" +require "ruroku/release" +require "ruroku/release_set" +require "ruroku/version" + +module Ruroku +end diff --git a/lib/ruroku/addon.rb b/lib/ruroku/addon.rb new file mode 100644 index 0000000..82bfd35 --- /dev/null +++ b/lib/ruroku/addon.rb @@ -0,0 +1,15 @@ +module Ruroku + class Addon < NestedBase + attr_accessor :name, :description, :url, :state, :price, :beta + deletable :name + + # Public: Check if the addon is in beta. + def beta? + !!beta + end + + def price=(value) + @price = value + end + end +end diff --git a/lib/ruroku/addon_set.rb b/lib/ruroku/addon_set.rb new file mode 100644 index 0000000..eb8dfd4 --- /dev/null +++ b/lib/ruroku/addon_set.rb @@ -0,0 +1,14 @@ +module Ruroku + class AddonSet < ResourceSet + # Map API methods to collection methods. + # + # Examples + # + # addons.add 'addon-name' + # addons.upgrade 'addon-name' + # addon.delete 'addon-name' + map_api add: :post_addon, + upgrade: :put_addon, + delete: :delete_addon + end +end diff --git a/lib/ruroku/api.rb b/lib/ruroku/api.rb new file mode 100644 index 0000000..d577c11 --- /dev/null +++ b/lib/ruroku/api.rb @@ -0,0 +1,16 @@ +module Ruroku + class API + attr_accessor :heroku_api + + def initialize(options = {}) + self.heroku_api = Heroku::API.new options + end + + # Public: Get apps associated with current heroku account. + # + # Returns the Array[App]. + def apps + heroku_api.get_apps.body.map { |app| App.new heroku_api, app } + end + end +end diff --git a/lib/ruroku/app.rb b/lib/ruroku/app.rb new file mode 100644 index 0000000..9e587c5 --- /dev/null +++ b/lib/ruroku/app.rb @@ -0,0 +1,54 @@ +module Ruroku + class App < Base + attr_accessor :id, :name, :create_status, :created_at, :stack, :git_url, + :requested_stack, :repo_migrate_status, :slug_size, :repo_size, + :dynos, :workers + + # Public: Get addons associated with current app. + def addons + AddonSet.new self, *api.get_addons(name).body.map { |addon| Addon.new self, addon } + end + + # Public: Get collaborators associated with current app. + def collaborators + CollaboratorSet.new self, *api.get_collaborators(name).body.map { |collaborator| Collaborator.new self, collaborator } + end + + # Public: Get config vars associated with current app. + def config_vars + ConfigVarSet.new self, *api.get_config_vars(name).body.map { |key, value| ConfigVar.new self, key: key, value: value } + end + + # Public: Get processes associated with current app. + def processes + ProcessSet.new self, *api.get_ps(name).body.map { |ps| Process.new self, ps } + end + + # Public: Get releases associated with current app. + def releases + ReleaseSet.new self, *api.get_releases(name).body.map { |release| Release.new self, release } + end + + # Public: Turn the maintenance mode on. + def maintenance! + api.post_app_maintenance name, '1' + end + + # Public: Turn the maintenance mode off. + def no_maintenance! + api.post_app_maintenance name, '0' + end + + def created_at=(value) + @created_at = Time.parse value + end + + def slug_size=(value) + @slug_size = value.to_i.bytes + end + + def repo_size=(value) + @repo_size = value.to_i.bytes + end + end +end diff --git a/lib/ruroku/base.rb b/lib/ruroku/base.rb new file mode 100644 index 0000000..3d02ec3 --- /dev/null +++ b/lib/ruroku/base.rb @@ -0,0 +1,14 @@ +module Ruroku + class Base + attr_accessor :api + + def initialize(api, attributes = {}) + self.api = api + + attributes.each do |key, value| + method = "#{key}=" + send method, value if respond_to? method + end + end + end +end diff --git a/lib/ruroku/collaborator.rb b/lib/ruroku/collaborator.rb new file mode 100644 index 0000000..3b0e19b --- /dev/null +++ b/lib/ruroku/collaborator.rb @@ -0,0 +1,6 @@ +module Ruroku + class Collaborator < NestedBase + attr_accessor :email, :access + deletable :email + end +end diff --git a/lib/ruroku/collaborator_set.rb b/lib/ruroku/collaborator_set.rb new file mode 100644 index 0000000..d02c272 --- /dev/null +++ b/lib/ruroku/collaborator_set.rb @@ -0,0 +1,12 @@ +module Ruroku + class CollaboratorSet < ResourceSet + # Map API methods to collection methods. + # + # Examples + # + # collaborators.add 'collaborator-email' + # collaborators.delete 'collaborator-email' + map_api add: :post_collaborator, + delete: :delete_collaborator + end +end diff --git a/lib/ruroku/config_var.rb b/lib/ruroku/config_var.rb new file mode 100644 index 0000000..94d180a --- /dev/null +++ b/lib/ruroku/config_var.rb @@ -0,0 +1,15 @@ +module Ruroku + class ConfigVar < NestedBase + attr_accessor :key, :value + deletable :key + + def value=(new_value) + if @value.nil? + @value = new_value + else + api.put_config_vars app.name, key => new_value + @value = new_value + end + end + end +end diff --git a/lib/ruroku/config_var_set.rb b/lib/ruroku/config_var_set.rb new file mode 100644 index 0000000..7485cd6 --- /dev/null +++ b/lib/ruroku/config_var_set.rb @@ -0,0 +1,12 @@ +module Ruroku + class ConfigVarSet < ResourceSet + # Map API methods to collection methods. + # + # Examples + # + # config_vars.add 'KEY' => 'value' + # config_vars.delete 'KEY' => 'value' + map_api add: :post_config_vars, + delete: :delete_config_var + end +end diff --git a/lib/ruroku/domain.rb b/lib/ruroku/domain.rb new file mode 100644 index 0000000..315b5e3 --- /dev/null +++ b/lib/ruroku/domain.rb @@ -0,0 +1,14 @@ +module Ruroku + class Domain < NestedBase + attr_accessor :id, :domain, :base_domain, :default, :created_at, :updated_at + deletable :domain + + def created_at=(value) + @created_at = Time.parse value + end + + def updated_at=(value) + @updated_at = Time.parse value + end + end +end diff --git a/lib/ruroku/domain_set.rb b/lib/ruroku/domain_set.rb new file mode 100644 index 0000000..6306f6d --- /dev/null +++ b/lib/ruroku/domain_set.rb @@ -0,0 +1,12 @@ +module Ruroku + class DomainSet < ResourceSet + # Map API methods to collection methods. + # + # Examples + # + # domains.add 'domain.com' + # domains.delete 'domain.com' + map_api add: :post_domains, + delete: :delete_domain + end +end diff --git a/lib/ruroku/nested_base.rb b/lib/ruroku/nested_base.rb new file mode 100644 index 0000000..04b300f --- /dev/null +++ b/lib/ruroku/nested_base.rb @@ -0,0 +1,21 @@ +module Ruroku + class NestedBase < Base + attr_accessor :app, :api + + def initialize(app, attributes = {}) + self.app = app + + super app.api, attributes + end + + def self.deletable(resource_id) + resource_name = name.demodulize.underscore + + define_method :delete! do + api_method = "delete_#{resource_name}" + resource_id = send resource_id + api.send api_method, app.name, resource_id + end + end + end +end diff --git a/lib/ruroku/process.rb b/lib/ruroku/process.rb new file mode 100644 index 0000000..5cf8d27 --- /dev/null +++ b/lib/ruroku/process.rb @@ -0,0 +1,25 @@ +module Ruroku + class Process < NestedBase + attr_accessor :process, :type, :command, :upid, :slug, :action, + :pretty_state, :elapsed, :rendezvous_url, :attached, :transitioned_at + + # Public: Restart the process. + def restart + api.post_ps_restart app.name, 'ps' => process + end + + # Public: Stop the process. + def stop + api.post_ps_stop app.name, 'ps' => process + end + + # Public: Check if the process is attached. + def attached? + !!attached + end + + def transitioned_at=(value) + @transitioned_at = Time.parse value + end + end +end diff --git a/lib/ruroku/process_set.rb b/lib/ruroku/process_set.rb new file mode 100644 index 0000000..13800e2 --- /dev/null +++ b/lib/ruroku/process_set.rb @@ -0,0 +1,37 @@ +module Ruroku + class ProcessSet < ResourceSet + # Publc: Run the command. + # + # command - The String command. + def run(command) + api.post_ps app.name, command + end + + # Public: Restart all the processes. + def restart + api.post_ps_restart app.name + end + + # Public: Scale processes. + # + # type - The String process type. + # qty - The Integer process quantity. + def scale(type, qty) + api.ps_scale app.name, type, qty + end + + # Public: Stop the process(es). + # + # options - The Hash + # either :ps - The String process name + # or :type - The String process type + # + # Examples + # + # processes.stop 'ps' => 'web.1' + # processes.stop 'type' => 'web' + def stop(options) + api.post_ps_stop app.name, options + end + end +end diff --git a/lib/ruroku/release.rb b/lib/ruroku/release.rb new file mode 100644 index 0000000..1e78fe4 --- /dev/null +++ b/lib/ruroku/release.rb @@ -0,0 +1,10 @@ +module Ruroku + class Release < NestedBase + attr_accessor :name, :descr, :user, :commit, :env, :addons, :pstable, + :created_at + + def created_at=(value) + @created_at = Time.parse value + end + end +end diff --git a/lib/ruroku/release_set.rb b/lib/ruroku/release_set.rb new file mode 100644 index 0000000..2f792ec --- /dev/null +++ b/lib/ruroku/release_set.rb @@ -0,0 +1,10 @@ +module Ruroku + class ReleaseSet < ResourceSet + # Map API methods to collection methods. + # + # Examples + # + # releases.rollback 'v1' + map_api rollback: :post_release + end +end diff --git a/lib/ruroku/resource_set.rb b/lib/ruroku/resource_set.rb new file mode 100644 index 0000000..a0201c0 --- /dev/null +++ b/lib/ruroku/resource_set.rb @@ -0,0 +1,20 @@ +module Ruroku + class ResourceSet < Array + attr_accessor :app, :api + + def initialize(app, *args) + self.app = app + self.api = app.api + + super args + end + + def self.map_api(methods) + methods.each do |method_name, api_mapping| + define_method method_name do |resource_name| + api.send api_mapping, app.name, resource_name + end + end + end + end +end diff --git a/lib/ruroku/version.rb b/lib/ruroku/version.rb new file mode 100644 index 0000000..57ce407 --- /dev/null +++ b/lib/ruroku/version.rb @@ -0,0 +1,3 @@ +module Ruroku + VERSION = "0.0.1" +end diff --git a/ruroku.gemspec b/ruroku.gemspec new file mode 100644 index 0000000..a0872cc --- /dev/null +++ b/ruroku.gemspec @@ -0,0 +1,22 @@ +# -*- encoding: utf-8 -*- +require File.expand_path('../lib/ruroku/version', __FILE__) + +Gem::Specification.new do |gem| + gem.authors = ["Gosha Arinich"] + gem.email = ["me@goshakkk.name"] + gem.description = %q{Ruby client for the Heroku API} + gem.summary = %q{Ruby client for the Heroku API} + gem.homepage = "" + + gem.files = `git ls-files`.split($\) + gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } + gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) + gem.name = "ruroku" + gem.require_paths = ["lib"] + gem.version = Ruroku::VERSION + + gem.add_runtime_dependency 'heroku-api', '~> 0.2.4' + gem.add_runtime_dependency 'activesupport', '~> 3.2.5' + + gem.add_development_dependency 'rspec' +end