aptible-cli 0.6.9 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
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