aptible-cli 0.6.9 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +1 -0
  3. data/README.md +1 -1
  4. data/aptible-cli.gemspec +6 -3
  5. data/bin/aptible +3 -1
  6. data/codecov.yml +12 -0
  7. data/lib/aptible/cli/agent.rb +3 -0
  8. data/lib/aptible/cli/helpers/app.rb +98 -33
  9. data/lib/aptible/cli/helpers/database.rb +3 -3
  10. data/lib/aptible/cli/subcommands/apps.rb +4 -3
  11. data/lib/aptible/cli/subcommands/backup.rb +55 -0
  12. data/lib/aptible/cli/subcommands/config.rb +2 -2
  13. data/lib/aptible/cli/subcommands/db.rb +11 -2
  14. data/lib/aptible/cli/subcommands/rebuild.rb +1 -1
  15. data/lib/aptible/cli/subcommands/restart.rb +1 -1
  16. data/lib/aptible/cli/version.rb +1 -1
  17. data/spec/aptible/cli/helpers/git_remote_handle_strategy_spec.rb +54 -0
  18. data/spec/aptible/cli/helpers/{handle_from_git_remote.rb → handle_from_git_remote_spec.rb} +0 -0
  19. data/spec/aptible/cli/helpers/options_handle_strategy_spec.rb +14 -0
  20. data/spec/aptible/cli/helpers/tunnel_spec.rb +0 -1
  21. data/spec/aptible/cli/subcommands/apps_spec.rb +141 -54
  22. data/spec/aptible/cli/subcommands/backup_spec.rb +115 -0
  23. data/spec/aptible/cli/subcommands/db_spec.rb +35 -61
  24. data/spec/aptible/cli/subcommands/domains_spec.rb +21 -38
  25. data/spec/aptible/cli/subcommands/logs_spec.rb +12 -17
  26. data/spec/aptible/cli/subcommands/ps_spec.rb +5 -12
  27. data/spec/fabricators/account_fabricator.rb +10 -0
  28. data/spec/fabricators/app_fabricator.rb +14 -0
  29. data/spec/fabricators/backup_fabricator.rb +10 -0
  30. data/spec/fabricators/database_fabricator.rb +15 -0
  31. data/spec/fabricators/operation_fabricator.rb +6 -0
  32. data/spec/fabricators/service_fabricator.rb +9 -0
  33. data/spec/fabricators/vhost_fabricator.rb +9 -0
  34. data/spec/spec_helper.rb +9 -1
  35. metadata +81 -17
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c58858b887b9868e017e93d7248f4e7d1f898dc2
4
- data.tar.gz: 7b2d52cc1b316205abb7d35c13722a32dccda29a
3
+ metadata.gz: 4b335a0f359338d92dff7ff3e0a267c0da5a6790
4
+ data.tar.gz: 28fa0a1b6c4129e361df287757fc8436f540fb9f
5
5
  SHA512:
6
- metadata.gz: 2c326bb7a9cc09e2689804e43b0fa00643eb759dbe838f572b0dc44df32caba6f6b76f1a14375e4aa6c1d5cac83457b8a37274c72f14ed843b6afcaa6acb2287
7
- data.tar.gz: 07bd87aa8c414600d845ce133c868ad1a0df02e9c6518f6202717eabd7f12d8d7b95df42cc5a1f4177bb4f315e6950fc4fa6e13b64059ce2f12f8b0b0a6bf2c3
6
+ metadata.gz: e0c6bb5308aa87d7696a6b4cd1ae85923429084b5bfa3c6882114cbc0932c001e60f4e3045f686bc4e7c5827874c9a53e2685bee2268210baee5479395c1fd99
7
+ data.tar.gz: 9604c082e3d8a4b47ab0bad5bd1686fcf2dd27cefa0404f2fffd2f685b8c93fb8b3b19a9348a74aba98c4debfe6bfd14d539b16fec2f1dc67dba93b319a2c667
data/Gemfile CHANGED
@@ -4,6 +4,7 @@ gem 'pry', github: 'fancyremarker/pry', branch: 'aptible'
4
4
 
5
5
  group :test do
6
6
  gem 'webmock'
7
+ gem 'codecov', require: false
7
8
  end
8
9
 
9
10
  # Specify your gem's dependencies in aptible-cli.gemspec
data/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [![Gem Version](https://badge.fury.io/rb/aptible-cli.png)](https://rubygems.org/gems/aptible-cli)
4
4
  [![Build Status](https://travis-ci.org/aptible/aptible-cli.png?branch=master)](https://travis-ci.org/aptible/aptible-cli)
5
5
  [![Dependency Status](https://gemnasium.com/aptible/aptible-cli.png)](https://gemnasium.com/aptible/aptible-cli)
6
-
6
+ [![codecov](https://codecov.io/gh/aptible/aptible-cli/branch/master/graph/badge.svg)](https://codecov.io/gh/aptible/aptible-cli)
7
7
  [![Roadmap](https://badge.waffle.io/aptible/aptible-cli.svg?label=ready&title=roadmap)](http://waffle.io/aptible/aptible-cli)
8
8
 
9
9
  Command-line interface for Aptible services.
data/aptible-cli.gemspec CHANGED
@@ -20,11 +20,13 @@ Gem::Specification.new do |spec|
20
20
  spec.test_files = spec.files.grep(%r{spec/})
21
21
  spec.require_paths = ['lib']
22
22
 
23
- spec.add_dependency 'aptible-api', '>= 0.9.5'
24
- spec.add_dependency 'aptible-auth', '>= 0.11.3'
25
- spec.add_dependency 'thor', '>= 0.19.0'
23
+ spec.add_dependency 'aptible-api', '~> 0.9.7'
24
+ spec.add_dependency 'aptible-auth', '~> 0.11.7'
25
+ spec.add_dependency 'aptible-resource', '~> 0.3.6'
26
+ spec.add_dependency 'thor', '~> 0.19.1'
26
27
  spec.add_dependency 'git'
27
28
  spec.add_dependency 'term-ansicolor'
29
+ spec.add_dependency 'chronic_duration', '~> 0.10.6'
28
30
 
29
31
  spec.add_development_dependency 'bundler', '~> 1.3'
30
32
  spec.add_development_dependency 'aptible-tasks', '>= 0.2.0'
@@ -32,4 +34,5 @@ Gem::Specification.new do |spec|
32
34
  spec.add_development_dependency 'rspec', '~> 2.0'
33
35
  spec.add_development_dependency 'pry'
34
36
  spec.add_development_dependency 'climate_control'
37
+ spec.add_development_dependency 'fabrication', '~> 2.15.2'
35
38
  end
data/bin/aptible CHANGED
@@ -12,6 +12,8 @@ begin
12
12
  rescue HyperResource::ClientError => e
13
13
  if e.body['error'] == 'invalid_token'
14
14
  STDERR.puts 'API authentication error: please run aptible login'
15
- exit 1
15
+ else
16
+ STDERR.puts "An error occurred: #{e.body['message']}"
16
17
  end
18
+ exit 1
17
19
  end
data/codecov.yml ADDED
@@ -0,0 +1,12 @@
1
+ # Only report coverage, don't set a failing status on branches or PRs
2
+ coverage:
3
+ status:
4
+ project:
5
+ default:
6
+ target: 0
7
+ patch:
8
+ default:
9
+ target: 0
10
+ changes:
11
+ default:
12
+ target: 0
@@ -1,6 +1,7 @@
1
1
  require 'aptible/auth'
2
2
  require 'thor'
3
3
  require 'json'
4
+ require 'chronic_duration'
4
5
 
5
6
  require_relative 'helpers/token'
6
7
  require_relative 'helpers/operation'
@@ -19,6 +20,7 @@ require_relative 'subcommands/ps'
19
20
  require_relative 'subcommands/rebuild'
20
21
  require_relative 'subcommands/restart'
21
22
  require_relative 'subcommands/ssh'
23
+ require_relative 'subcommands/backup'
22
24
 
23
25
  module Aptible
24
26
  module CLI
@@ -37,6 +39,7 @@ module Aptible
37
39
  include Subcommands::Rebuild
38
40
  include Subcommands::Restart
39
41
  include Subcommands::SSH
42
+ include Subcommands::Backup
40
43
 
41
44
  # Forward return codes on failures.
42
45
  def self.exit_on_failure?
@@ -8,6 +8,18 @@ module Aptible
8
8
  include Helpers::Token
9
9
  include Helpers::Environment
10
10
 
11
+ module ClassMethods
12
+ def app_options
13
+ option :app
14
+ option :environment
15
+ option :remote, aliases: '-r'
16
+ end
17
+ end
18
+
19
+ def self.included(base)
20
+ base.extend ClassMethods
21
+ end
22
+
11
23
  class HandleFromGitRemote
12
24
  PATTERN = %r{
13
25
  :((?<environment_handle>[0-9a-z\-_\.]+?)/)?
@@ -20,61 +32,114 @@ module Aptible
20
32
  end
21
33
  end
22
34
 
23
- def self.included(base)
24
- base.extend ClassMethods
35
+ class OptionsHandleStrategy
36
+ attr_reader :app_handle, :env_handle
37
+
38
+ def initialize(options)
39
+ @app_handle = options[:app]
40
+ @env_handle = options[:environment]
41
+ end
42
+
43
+ def usable?
44
+ !app_handle.nil?
45
+ end
46
+
47
+ def explain
48
+ '(options provided via CLI arguments)'
49
+ end
25
50
  end
26
51
 
27
- module ClassMethods
28
- def app_options
29
- option :app
30
- option :environment
31
- option :remote, aliases: '-r'
52
+ class GitRemoteHandleStrategy
53
+ def initialize(options)
54
+ @remote_name = options[:remote] || ENV['APTIBLE_REMOTE'] ||
55
+ 'aptible'
56
+ @repo_dir = Dir.pwd
57
+ end
58
+
59
+ def app_handle
60
+ handles_from_remote[:app_handle]
61
+ end
62
+
63
+ def env_handle
64
+ handles_from_remote[:environment_handle]
65
+ end
66
+
67
+ def usable?
68
+ !app_handle.nil? && !env_handle.nil?
69
+ end
70
+
71
+ def explain
72
+ "(options derived from git remote #{@remote_name})"
73
+ end
74
+
75
+ private
76
+
77
+ def handles_from_remote
78
+ @handles_from_remote ||= \
79
+ begin
80
+ git = Git.open(@repo_dir)
81
+ remote_url = git.remote(@remote_name).url || ''
82
+ HandleFromGitRemote.parse(remote_url)
83
+ rescue StandardError
84
+ # TODO: Consider being more specific here (ArgumentError?)
85
+ {}
86
+ end
32
87
  end
33
88
  end
34
89
 
35
90
  def ensure_app(options = {})
36
- remote = options[:remote] || ENV['APTIBLE_REMOTE'] || 'aptible'
37
- app_handle = options[:app] || handles_from_remote(remote)[:app_handle]
38
- environment_handle = options[:environment] ||
39
- handles_from_remote(remote)[:environment_handle]
40
-
41
- unless app_handle
42
- fail Thor::Error, <<-ERR.gsub(/\s+/, ' ').strip
43
- Could not find app in current working directory, please specify
44
- with --app
45
- ERR
91
+ s = handle_strategies.map { |cls| cls.new(options) }.find(&:usable?)
92
+
93
+ if s.nil?
94
+ err = 'Could not find app in current working directory, please ' \
95
+ 'specify with --app'
96
+ fail Thor::Error, err
46
97
  end
47
98
 
48
- environment = environment_from_handle(environment_handle)
49
- if environment_handle && !environment
50
- fail Thor::Error, "Could not find environment #{environment_handle}"
99
+ environment = nil
100
+ if s.env_handle
101
+ environment = environment_from_handle(s.env_handle)
102
+ if environment.nil?
103
+ err_bits = ['Could not find environment', s.env_handle]
104
+ err_bits << s.explain
105
+ fail Thor::Error, err_bits.join(' ')
106
+ end
51
107
  end
52
- apps = apps_from_handle(app_handle, environment)
108
+
109
+ apps = apps_from_handle(s.app_handle, environment)
110
+
53
111
  case apps.count
54
112
  when 1
55
113
  return apps.first
56
114
  when 0
57
- fail Thor::Error, "Could not find app #{app_handle}"
115
+ err_bits = ['Could not find app', s.app_handle]
116
+ if environment
117
+ err_bits << 'in environment'
118
+ err_bits << environment.handle
119
+ else
120
+ err_bits << 'in any environment'
121
+ end
122
+ err_bits << s.explain
123
+ fail Thor::Error, err_bits.join(' ')
58
124
  else
59
- fail Thor::Error, 'Multiple apps exist, please specify environment'
125
+ err = "Multiple apps named #{s.app_handle} exist, please specify " \
126
+ 'with --environment'
127
+ fail Thor::Error, err
60
128
  end
61
129
  end
62
130
 
63
131
  def apps_from_handle(handle, environment)
64
132
  if environment
65
- apps = environment.apps
133
+ environment.apps
66
134
  else
67
- apps = Aptible::Api::App.all(token: fetch_token)
68
- end
69
- apps.select { |a| a.handle == handle }
135
+ Aptible::Api::App.all(token: fetch_token)
136
+ end.select { |a| a.handle == handle }
70
137
  end
71
138
 
72
- def handles_from_remote(remote_name)
73
- git = Git.open(Dir.pwd)
74
- aptible_remote = git.remote(remote_name).url || ''
75
- HandleFromGitRemote.parse(aptible_remote)
76
- rescue
77
- {}
139
+ private
140
+
141
+ def handle_strategies
142
+ [OptionsHandleStrategy, GitRemoteHandleStrategy]
78
143
  end
79
144
  end
80
145
  end
@@ -24,8 +24,8 @@ module Aptible
24
24
  when 0
25
25
  fail Thor::Error, "Could not find database #{db_handle}"
26
26
  else
27
- fail Thor::Error,
28
- 'Multiple databases exist, please specify environment'
27
+ err = 'Multiple databases exist, please specify with --environment'
28
+ fail Thor::Error, err
29
29
  end
30
30
  end
31
31
 
@@ -45,7 +45,7 @@ module Aptible
45
45
  end
46
46
 
47
47
  def clone_database(source, dest_handle)
48
- op = source.create_operation(type: 'clone', handle: dest_handle)
48
+ op = source.create_operation!(type: 'clone', handle: dest_handle)
49
49
  poll_for_success(op)
50
50
 
51
51
  databases_from_handle(dest_handle, source.account).first
@@ -30,6 +30,7 @@ module Aptible
30
30
  fail Thor::Error, app.errors.full_messages.first
31
31
  else
32
32
  say "App #{handle} created!"
33
+ say "Git remote: #{app.git_repo}"
33
34
  end
34
35
  end
35
36
 
@@ -57,9 +58,9 @@ module Aptible
57
58
  "exist for app #{app.handle}. Valid " \
58
59
  "types: #{valid_types}."
59
60
  end
60
- op = service.create_operation(type: 'scale',
61
- container_count: num,
62
- container_size: options[:size])
61
+ op = service.create_operation!(type: 'scale',
62
+ container_count: num,
63
+ container_size: options[:size])
63
64
  attach_to_operation_logs(op)
64
65
  end
65
66
 
@@ -0,0 +1,55 @@
1
+ module Aptible
2
+ module CLI
3
+ module Subcommands
4
+ module Backup
5
+ def self.included(thor)
6
+ thor.class_eval do
7
+ include Helpers::Token
8
+ include Helpers::Database
9
+
10
+ desc 'backup:restore [--handle HANDLE] [--size SIZE_GB]',
11
+ 'Restore a backup'
12
+ option :handle
13
+ option :size, type: :numeric
14
+ define_method 'backup:restore' do |backup_id|
15
+ backup = Aptible::Api::Backup.find(backup_id, token: fetch_token)
16
+ fail Thor::Error, "Backup ##{backup_id} not found" if backup.nil?
17
+ handle = options[:handle]
18
+ unless handle
19
+ ts_suffix = backup.created_at.getgm.strftime '%Y-%m-%d-%H-%M-%S'
20
+ handle = "#{backup.database.handle}-at-#{ts_suffix}"
21
+ end
22
+
23
+ opts = {
24
+ type: 'restore',
25
+ handle: handle,
26
+ disk_size: options[:size]
27
+ }.delete_if { |_, v| v.nil? }
28
+
29
+ operation = backup.create_operation!(opts)
30
+ say "Restoring backup into #{handle}"
31
+ attach_to_operation_logs(operation)
32
+ end
33
+
34
+ desc 'backup:list DB_HANDLE', 'List backups for a database'
35
+ option :environment
36
+ option :max_age,
37
+ default: '1mo',
38
+ desc: 'Limit backups returned (example usage: 1w, 1y, etc.)'
39
+ define_method 'backup:list' do |handle|
40
+ age = ChronicDuration.parse(options[:max_age])
41
+ fail Thor::Error, "Invalid age: #{options[:max_age]}" if age.nil?
42
+ min_created_at = Time.now - age
43
+
44
+ database = ensure_database(options.merge(db: handle))
45
+ database.each_backup do |backup|
46
+ break if backup.created_at < min_created_at
47
+ say "#{backup.id}: #{backup.created_at}, #{backup.aws_region}"
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -24,7 +24,7 @@ module Aptible
24
24
  # FIXME: define_method - ?! Seriously, WTF Thor.
25
25
  app = ensure_app(options)
26
26
  env = Hash[args.map { |arg| arg.split('=', 2) }]
27
- operation = app.create_operation(type: 'configure', env: env)
27
+ operation = app.create_operation!(type: 'configure', env: env)
28
28
  puts 'Updating configuration and restarting app...'
29
29
  attach_to_operation_logs(operation)
30
30
  end
@@ -41,7 +41,7 @@ module Aptible
41
41
  # FIXME: define_method - ?! Seriously, WTF Thor.
42
42
  app = ensure_app(options)
43
43
  env = Hash[args.map { |arg| [arg, ''] }]
44
- operation = app.create_operation(type: 'configure', env: env)
44
+ operation = app.create_operation!(type: 'configure', env: env)
45
45
  puts 'Updating configuration and restarting app...'
46
46
  attach_to_operation_logs(operation)
47
47
  end
@@ -32,8 +32,8 @@ module Aptible
32
32
  if database.errors.any?
33
33
  fail Thor::Error, database.errors.full_messages.first
34
34
  else
35
- op = database.create_operation(type: 'provision',
36
- disk_size: options[:size])
35
+ op = database.create_operation!(type: 'provision',
36
+ disk_size: options[:size])
37
37
  attach_to_operation_logs(op)
38
38
  say database.reload.connection_url
39
39
  end
@@ -106,6 +106,15 @@ module Aptible
106
106
  say "Deprovisioning #{database.handle}..."
107
107
  database.create_operation!(type: 'deprovision')
108
108
  end
109
+
110
+ desc 'db:backup HANDLE', 'Backup a database'
111
+ option :environment
112
+ define_method 'db:backup' do |handle|
113
+ database = ensure_database(options.merge(db: handle))
114
+ say "Backing up #{database.handle}..."
115
+ op = database.create_operation!(type: 'backup')
116
+ attach_to_operation_logs(op)
117
+ end
109
118
  end
110
119
  end
111
120
  end
@@ -11,7 +11,7 @@ module Aptible
11
11
  app_options
12
12
  def rebuild
13
13
  app = ensure_app(options)
14
- operation = app.create_operation(type: 'rebuild')
14
+ operation = app.create_operation!(type: 'rebuild')
15
15
  puts 'Rebuilding app...'
16
16
  attach_to_operation_logs(operation)
17
17
  end
@@ -11,7 +11,7 @@ module Aptible
11
11
  app_options
12
12
  def restart
13
13
  app = ensure_app(options)
14
- operation = app.create_operation(type: 'restart')
14
+ operation = app.create_operation!(type: 'restart')
15
15
  puts 'Restarting app...'
16
16
  attach_to_operation_logs(operation)
17
17
  end
@@ -1,5 +1,5 @@
1
1
  module Aptible
2
2
  module CLI
3
- VERSION = '0.6.9'
3
+ VERSION = '0.7.0'
4
4
  end
5
5
  end
@@ -0,0 +1,54 @@
1
+ require 'spec_helper'
2
+
3
+ describe Aptible::CLI::Helpers::App::GitRemoteHandleStrategy do
4
+ let!(:work_dir) { Dir.mktmpdir }
5
+ after { FileUtils.remove_entry work_dir }
6
+ around { |example| Dir.chdir(work_dir) { example.run } }
7
+
8
+ context 'with git repo' do
9
+ before { `git init` }
10
+
11
+ context 'with aptible remote' do
12
+ before do
13
+ `git remote add aptible git@beta.aptible.com:some-env/some-app.git`
14
+ `git remote add prod git@beta.aptible.com:prod-env/prod-app.git`
15
+ end
16
+
17
+ it 'defaults to the Aptible remote' do
18
+ s = described_class.new({})
19
+ expect(s.app_handle).to eq('some-app')
20
+ expect(s.env_handle).to eq('some-env')
21
+ expect(s.usable?).to be_truthy
22
+ end
23
+
24
+ it 'allows explicitly passing a remote' do
25
+ s = described_class.new(remote: 'prod')
26
+ expect(s.app_handle).to eq('prod-app')
27
+ expect(s.env_handle).to eq('prod-env')
28
+ expect(s.usable?).to be_truthy
29
+ end
30
+
31
+ it 'accepts a remote from the environment' do
32
+ ClimateControl.modify APTIBLE_REMOTE: 'prod' do
33
+ s = described_class.new(remote: 'prod')
34
+ expect(s.app_handle).to eq('prod-app')
35
+ end
36
+ end
37
+
38
+ it 'is not usable when the remote does not exist' do
39
+ s = described_class.new(remote: 'foobar')
40
+ expect(s.usable?).to be_falsey
41
+ end
42
+
43
+ it 'outputs the remote when explaining' do
44
+ s = described_class.new(remote: 'prod')
45
+ expect(s.explain).to match(/derived from git remote prod/)
46
+ end
47
+ end
48
+ end
49
+
50
+ it 'is not usable outside of a git repo' do
51
+ s = described_class.new({})
52
+ expect(s.usable?).to be_falsey
53
+ end
54
+ end