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.
- checksums.yaml +4 -4
- data/Gemfile +1 -0
- data/README.md +1 -1
- data/aptible-cli.gemspec +6 -3
- data/bin/aptible +3 -1
- data/codecov.yml +12 -0
- data/lib/aptible/cli/agent.rb +3 -0
- data/lib/aptible/cli/helpers/app.rb +98 -33
- data/lib/aptible/cli/helpers/database.rb +3 -3
- data/lib/aptible/cli/subcommands/apps.rb +4 -3
- data/lib/aptible/cli/subcommands/backup.rb +55 -0
- data/lib/aptible/cli/subcommands/config.rb +2 -2
- data/lib/aptible/cli/subcommands/db.rb +11 -2
- data/lib/aptible/cli/subcommands/rebuild.rb +1 -1
- data/lib/aptible/cli/subcommands/restart.rb +1 -1
- data/lib/aptible/cli/version.rb +1 -1
- data/spec/aptible/cli/helpers/git_remote_handle_strategy_spec.rb +54 -0
- data/spec/aptible/cli/helpers/{handle_from_git_remote.rb → handle_from_git_remote_spec.rb} +0 -0
- data/spec/aptible/cli/helpers/options_handle_strategy_spec.rb +14 -0
- data/spec/aptible/cli/helpers/tunnel_spec.rb +0 -1
- data/spec/aptible/cli/subcommands/apps_spec.rb +141 -54
- data/spec/aptible/cli/subcommands/backup_spec.rb +115 -0
- data/spec/aptible/cli/subcommands/db_spec.rb +35 -61
- data/spec/aptible/cli/subcommands/domains_spec.rb +21 -38
- data/spec/aptible/cli/subcommands/logs_spec.rb +12 -17
- data/spec/aptible/cli/subcommands/ps_spec.rb +5 -12
- data/spec/fabricators/account_fabricator.rb +10 -0
- data/spec/fabricators/app_fabricator.rb +14 -0
- data/spec/fabricators/backup_fabricator.rb +10 -0
- data/spec/fabricators/database_fabricator.rb +15 -0
- data/spec/fabricators/operation_fabricator.rb +6 -0
- data/spec/fabricators/service_fabricator.rb +9 -0
- data/spec/fabricators/vhost_fabricator.rb +9 -0
- data/spec/spec_helper.rb +9 -1
- metadata +81 -17
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4b335a0f359338d92dff7ff3e0a267c0da5a6790
|
4
|
+
data.tar.gz: 28fa0a1b6c4129e361df287757fc8436f540fb9f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e0c6bb5308aa87d7696a6b4cd1ae85923429084b5bfa3c6882114cbc0932c001e60f4e3045f686bc4e7c5827874c9a53e2685bee2268210baee5479395c1fd99
|
7
|
+
data.tar.gz: 9604c082e3d8a4b47ab0bad5bd1686fcf2dd27cefa0404f2fffd2f685b8c93fb8b3b19a9348a74aba98c4debfe6bfd14d539b16fec2f1dc67dba93b319a2c667
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
[](https://rubygems.org/gems/aptible-cli)
|
4
4
|
[](https://travis-ci.org/aptible/aptible-cli)
|
5
5
|
[](https://gemnasium.com/aptible/aptible-cli)
|
6
|
-
|
6
|
+
[](https://codecov.io/gh/aptible/aptible-cli)
|
7
7
|
[](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', '
|
24
|
-
spec.add_dependency 'aptible-auth', '
|
25
|
-
spec.add_dependency '
|
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
data/codecov.yml
ADDED
data/lib/aptible/cli/agent.rb
CHANGED
@@ -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
|
-
|
24
|
-
|
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
|
-
|
28
|
-
def
|
29
|
-
|
30
|
-
|
31
|
-
|
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
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
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 =
|
49
|
-
if
|
50
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
133
|
+
environment.apps
|
66
134
|
else
|
67
|
-
|
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
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
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
|
-
|
28
|
-
|
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
|
-
|
62
|
-
|
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
|
-
|
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
|
data/lib/aptible/cli/version.rb
CHANGED
@@ -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
|