azuki 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (99) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +71 -0
  3. data/bin/azuki +17 -0
  4. data/data/cacert.pem +3988 -0
  5. data/lib/azuki.rb +17 -0
  6. data/lib/azuki/auth.rb +339 -0
  7. data/lib/azuki/cli.rb +38 -0
  8. data/lib/azuki/client.rb +764 -0
  9. data/lib/azuki/client/azuki_postgresql.rb +141 -0
  10. data/lib/azuki/client/cisaurus.rb +26 -0
  11. data/lib/azuki/client/pgbackups.rb +113 -0
  12. data/lib/azuki/client/rendezvous.rb +108 -0
  13. data/lib/azuki/client/ssl_endpoint.rb +25 -0
  14. data/lib/azuki/command.rb +294 -0
  15. data/lib/azuki/command/account.rb +23 -0
  16. data/lib/azuki/command/accounts.rb +34 -0
  17. data/lib/azuki/command/addons.rb +305 -0
  18. data/lib/azuki/command/apps.rb +393 -0
  19. data/lib/azuki/command/auth.rb +86 -0
  20. data/lib/azuki/command/base.rb +230 -0
  21. data/lib/azuki/command/certs.rb +209 -0
  22. data/lib/azuki/command/config.rb +137 -0
  23. data/lib/azuki/command/db.rb +218 -0
  24. data/lib/azuki/command/domains.rb +85 -0
  25. data/lib/azuki/command/drains.rb +46 -0
  26. data/lib/azuki/command/fork.rb +164 -0
  27. data/lib/azuki/command/git.rb +64 -0
  28. data/lib/azuki/command/help.rb +179 -0
  29. data/lib/azuki/command/keys.rb +115 -0
  30. data/lib/azuki/command/labs.rb +147 -0
  31. data/lib/azuki/command/logs.rb +45 -0
  32. data/lib/azuki/command/maintenance.rb +61 -0
  33. data/lib/azuki/command/pg.rb +269 -0
  34. data/lib/azuki/command/pgbackups.rb +329 -0
  35. data/lib/azuki/command/plugins.rb +110 -0
  36. data/lib/azuki/command/ps.rb +232 -0
  37. data/lib/azuki/command/regions.rb +22 -0
  38. data/lib/azuki/command/releases.rb +124 -0
  39. data/lib/azuki/command/run.rb +180 -0
  40. data/lib/azuki/command/sharing.rb +89 -0
  41. data/lib/azuki/command/ssl.rb +43 -0
  42. data/lib/azuki/command/stack.rb +62 -0
  43. data/lib/azuki/command/status.rb +51 -0
  44. data/lib/azuki/command/update.rb +47 -0
  45. data/lib/azuki/command/version.rb +23 -0
  46. data/lib/azuki/deprecated.rb +5 -0
  47. data/lib/azuki/deprecated/help.rb +38 -0
  48. data/lib/azuki/distribution.rb +9 -0
  49. data/lib/azuki/excon.rb +9 -0
  50. data/lib/azuki/helpers.rb +517 -0
  51. data/lib/azuki/helpers/azuki_postgresql.rb +165 -0
  52. data/lib/azuki/helpers/log_displayer.rb +70 -0
  53. data/lib/azuki/plugin.rb +163 -0
  54. data/lib/azuki/updater.rb +171 -0
  55. data/lib/azuki/version.rb +3 -0
  56. data/lib/vendor/azuki/okjson.rb +598 -0
  57. data/spec/azuki/auth_spec.rb +256 -0
  58. data/spec/azuki/client/azuki_postgresql_spec.rb +71 -0
  59. data/spec/azuki/client/pgbackups_spec.rb +43 -0
  60. data/spec/azuki/client/rendezvous_spec.rb +62 -0
  61. data/spec/azuki/client/ssl_endpoint_spec.rb +48 -0
  62. data/spec/azuki/client_spec.rb +564 -0
  63. data/spec/azuki/command/addons_spec.rb +601 -0
  64. data/spec/azuki/command/apps_spec.rb +351 -0
  65. data/spec/azuki/command/auth_spec.rb +38 -0
  66. data/spec/azuki/command/base_spec.rb +109 -0
  67. data/spec/azuki/command/certs_spec.rb +178 -0
  68. data/spec/azuki/command/config_spec.rb +144 -0
  69. data/spec/azuki/command/db_spec.rb +110 -0
  70. data/spec/azuki/command/domains_spec.rb +87 -0
  71. data/spec/azuki/command/drains_spec.rb +34 -0
  72. data/spec/azuki/command/fork_spec.rb +56 -0
  73. data/spec/azuki/command/git_spec.rb +144 -0
  74. data/spec/azuki/command/help_spec.rb +93 -0
  75. data/spec/azuki/command/keys_spec.rb +120 -0
  76. data/spec/azuki/command/labs_spec.rb +100 -0
  77. data/spec/azuki/command/logs_spec.rb +60 -0
  78. data/spec/azuki/command/maintenance_spec.rb +51 -0
  79. data/spec/azuki/command/pg_spec.rb +236 -0
  80. data/spec/azuki/command/pgbackups_spec.rb +307 -0
  81. data/spec/azuki/command/plugins_spec.rb +104 -0
  82. data/spec/azuki/command/ps_spec.rb +195 -0
  83. data/spec/azuki/command/releases_spec.rb +130 -0
  84. data/spec/azuki/command/run_spec.rb +83 -0
  85. data/spec/azuki/command/sharing_spec.rb +59 -0
  86. data/spec/azuki/command/stack_spec.rb +46 -0
  87. data/spec/azuki/command/status_spec.rb +48 -0
  88. data/spec/azuki/command/version_spec.rb +16 -0
  89. data/spec/azuki/command_spec.rb +211 -0
  90. data/spec/azuki/helpers/azuki_postgresql_spec.rb +155 -0
  91. data/spec/azuki/helpers_spec.rb +48 -0
  92. data/spec/azuki/plugin_spec.rb +172 -0
  93. data/spec/azuki/updater_spec.rb +44 -0
  94. data/spec/helper/legacy_help.rb +16 -0
  95. data/spec/spec.opts +1 -0
  96. data/spec/spec_helper.rb +224 -0
  97. data/spec/support/display_message_matcher.rb +49 -0
  98. data/spec/support/openssl_mock_helper.rb +8 -0
  99. metadata +211 -0
@@ -0,0 +1,164 @@
1
+ require "azuki/client/cisaurus"
2
+ require "azuki/command/base"
3
+
4
+ module Azuki::Command
5
+
6
+ # clone an existing app
7
+ #
8
+ class Fork < Base
9
+
10
+ # fork [NEWNAME]
11
+ #
12
+ # Fork an existing app -- copy config vars and Azuki Postgres data, and re-provision add-ons to a new app.
13
+ # New app name should not be an existing app. The new app will be created as part of the forking process.
14
+ #
15
+ # -s, --stack STACK # specify a stack
16
+ # --region REGION # HIDDEN: specify a region
17
+ #
18
+ def index
19
+
20
+ from = app
21
+ to = shift_argument || "#{from}-#{(rand*1000).to_i}"
22
+
23
+ from_info = api.get_app(from).body
24
+
25
+ to_info = action("Creating fork #{to}") do
26
+ api.post_app({
27
+ :name => to,
28
+ :region => options[:region] || from_info["region"],
29
+ :stack => options[:stack] || from_info["stack"],
30
+ :tier => from_info["tier"] == "legacy" ? "production" : from_info["tier"]
31
+ }).body
32
+ end
33
+
34
+ action("Copying slug") do
35
+ job_location = cisaurus_client.copy_slug(from, to)
36
+ loop do
37
+ break if cisaurus_client.job_done?(job_location)
38
+ print "."
39
+ sleep 1
40
+ end
41
+ end
42
+
43
+ from_config = api.get_config_vars(from).body
44
+ from_addons = api.get_addons(from).body
45
+
46
+ from_addons.each do |addon|
47
+ print "Adding #{addon["name"]}... "
48
+ begin
49
+ to_addon = api.post_addon(to, addon["name"]).body
50
+ puts "done"
51
+ rescue Azuki::API::Errors::RequestFailed => ex
52
+ puts "skipped (%s)" % json_decode(ex.response.body)["error"]
53
+ rescue Azuki::API::Errors::NotFound
54
+ puts "skipped (not found)"
55
+ end
56
+ if addon["name"] =~ /^azuki-postgresql:/
57
+ from_var_name = "#{addon["attachment_name"]}_URL"
58
+ from_attachment = to_addon["message"].match(/Attached as (\w+)_URL\n/)[1]
59
+ if from_config[from_var_name] == from_config["DATABASE_URL"]
60
+ from_config["DATABASE_URL"] = api.get_config_vars(to).body["#{from_attachment}_URL"]
61
+ end
62
+ from_config.delete(from_var_name)
63
+
64
+ plan = addon["name"].split(":").last
65
+ unless %w(dev basic).include? plan
66
+ wait_for_db to, to_addon
67
+ end
68
+
69
+ check_for_pgbackups! from
70
+ check_for_pgbackups! to
71
+ migrate_db addon, from, to_addon, to
72
+ end
73
+ end
74
+
75
+ to_config = api.get_config_vars(to).body
76
+
77
+ action("Copying config vars") do
78
+ diff = from_config.inject({}) do |ax, (key, val)|
79
+ ax[key] = val unless to_config[key]
80
+ ax
81
+ end
82
+ api.put_config_vars to, diff
83
+ end
84
+
85
+ puts "Fork complete, view it at #{to_info['web_url']}"
86
+ end
87
+
88
+ private
89
+
90
+ def cisaurus_client
91
+ cisaurus_url = ENV["CISAURUS_HOST"] || "https://cisaurus.azukiapp.com"
92
+ @cisaurus_client ||= Azuki::Client::Cisaurus.new(cisaurus_url)
93
+ end
94
+
95
+ def check_for_pgbackups!(app)
96
+ unless api.get_addons(app).body.detect { |addon| addon["name"] =~ /^pgbackups:/ }
97
+ action("Adding pgbackups:plus to #{app}") do
98
+ api.post_addon app, "pgbackups:plus"
99
+ end
100
+ end
101
+ end
102
+
103
+ def migrate_db(from_addon, from, to_addon, to)
104
+ transfer = nil
105
+
106
+ action("Creating database backup from #{from}") do
107
+ from_config = api.get_config_vars(from).body
108
+ from_attachment = from_addon["attachment_name"]
109
+ pgb = Azuki::Client::Pgbackups.new(from_config["PGBACKUPS_URL"])
110
+ transfer = pgb.create_transfer(from_config["#{from_attachment}_URL"], from_attachment, nil, "BACKUP", :expire => "true")
111
+ error transfer["errors"].values.flatten.join("\n") if transfer["errors"]
112
+ loop do
113
+ transfer = pgb.get_transfer(transfer["id"])
114
+ error transfer["errors"].values.flatten.join("\n") if transfer["errors"]
115
+ break if transfer["finished_at"]
116
+ sleep 1
117
+ print "."
118
+ end
119
+ print " "
120
+ end
121
+
122
+ action("Restoring database backup to #{to}") do
123
+ to_config = api.get_config_vars(to).body
124
+ to_attachment = to_addon["message"].match(/Attached as (\w+)_URL\n/)[1]
125
+ pgb = Azuki::Client::Pgbackups.new(to_config["PGBACKUPS_URL"])
126
+ transfer = pgb.create_transfer(transfer["public_url"], "EXTERNAL_BACKUP", to_config["#{to_attachment}_URL"], to_attachment)
127
+ error transfer["errors"].values.flatten.join("\n") if transfer["errors"]
128
+ loop do
129
+ transfer = pgb.get_transfer(transfer["id"])
130
+ error transfer["errors"].values.flatten.join("\n") if transfer["errors"]
131
+ break if transfer["finished_at"]
132
+ sleep 1
133
+ print "."
134
+ end
135
+ print " "
136
+ end
137
+ end
138
+
139
+ def pg_api(starter=false)
140
+ host = starter ? "postgres-starter-api.azukiapp.com" : "postgres-api.azukiapp.com"
141
+ RestClient::Resource.new "https://#{host}/client/v11/databases", Azuki::Auth.user, Azuki::Auth.password
142
+ end
143
+
144
+ def wait_for_db(app, attachment)
145
+ attachments = api.get_attachments(app).body.inject({}) { |ax,att| ax.update(att["name"] => att["resource"]["name"]) }
146
+ attachment_name = attachment["message"].match(/Attached as (\w+)_URL\n/)[1]
147
+ action("Waiting for database to be ready") do
148
+ loop do
149
+ begin
150
+ waiting = json_decode(pg_api["#{attachments[attachment_name]}/wait_status"].get.to_s)["waiting?"]
151
+ break unless waiting
152
+ print "."
153
+ sleep 5
154
+ rescue RestClient::ResourceNotFound
155
+ rescue Interrupt
156
+ exit 0
157
+ end
158
+ end
159
+ print " "
160
+ end
161
+ end
162
+
163
+ end
164
+ end
@@ -0,0 +1,64 @@
1
+ require "azuki/command/base"
2
+
3
+ # manage git for apps
4
+ #
5
+ class Azuki::Command::Git < Azuki::Command::Base
6
+
7
+ # git:clone APP [DIRECTORY]
8
+ #
9
+ # clones a azuki app to your local machine at DIRECTORY (defaults to app name)
10
+ #
11
+ # -r, --remote REMOTE # the git remote to create, default "azuki"
12
+ #
13
+ #Examples:
14
+ #
15
+ # $ azuki git:clone example
16
+ # Cloning from app 'example'...
17
+ # Cloning into 'example'...
18
+ # remote: Counting objects: 42, done.
19
+ # ...
20
+ #
21
+ def clone
22
+ remote = options[:remote] || "azuki"
23
+
24
+ name = options[:app] || shift_argument || error("Usage: azuki git:clone APP [DIRECTORY]")
25
+ directory = shift_argument
26
+ validate_arguments!
27
+
28
+ git_url = api.get_app(name).body["git_url"]
29
+
30
+ puts "Cloning from app '#{name}'..."
31
+ system "git clone -o #{remote} #{git_url} #{directory}".strip
32
+ end
33
+
34
+ alias_command "clone", "git:clone"
35
+
36
+ # git:remote [OPTIONS]
37
+ #
38
+ # adds a git remote to an app repo
39
+ #
40
+ # if OPTIONS are specified they will be passed to git remote add
41
+ #
42
+ # -r, --remote REMOTE # the git remote to create, default "azuki"
43
+ #
44
+ #Examples:
45
+ #
46
+ # $ azuki git:remote -a example
47
+ # Git remote azuki added
48
+ #
49
+ # $ azuki git:remote -a example
50
+ # ! Git remote azuki already exists
51
+ #
52
+ def remote
53
+ git_options = args.join(" ")
54
+ remote = options[:remote] || 'azuki'
55
+
56
+ if git('remote').split("\n").include?(remote)
57
+ error("Git remote #{remote} already exists")
58
+ else
59
+ app_data = api.get_app(app).body
60
+ create_git_remote(remote, app_data['git_url'])
61
+ end
62
+ end
63
+
64
+ end
@@ -0,0 +1,179 @@
1
+ require "azuki/command/base"
2
+ require "azuki/deprecated/help"
3
+
4
+ # list commands and display help
5
+ #
6
+ class Azuki::Command::Help < Azuki::Command::Base
7
+
8
+ PRIMARY_NAMESPACES = %w( auth apps ps run addons config releases domains logs sharing )
9
+
10
+ include Azuki::Deprecated::Help
11
+
12
+ # help [COMMAND]
13
+ #
14
+ # list available commands or display help for a specific command
15
+ #
16
+ #Examples:
17
+ #
18
+ # $ azuki help
19
+ # Usage: azuki COMMAND [--app APP] [command-specific-options]
20
+ #
21
+ # Primary help topics, type "azuki help TOPIC" for more details:
22
+ #
23
+ # addons # manage addon resources
24
+ # apps # manage apps (create, destroy)
25
+ # ...
26
+ #
27
+ # Additional topics:
28
+ #
29
+ # account # manage azuki account options
30
+ # accounts # manage multiple azuki accounts
31
+ # ...
32
+ #
33
+ # $ azuki help apps:create
34
+ # Usage: azuki apps:create [NAME]
35
+ #
36
+ # create a new app
37
+ #
38
+ # --addons ADDONS # a comma-delimited list of addons to install
39
+ # -b, --buildpack BUILDPACK # a buildpack url to use for this app
40
+ # -r, --remote REMOTE # the git remote to create, default "azuki"
41
+ # -s, --stack STACK # the stack on which to create the app
42
+ #
43
+ def index
44
+ if command = args.shift
45
+ help_for_command(command)
46
+ else
47
+ help_for_root
48
+ end
49
+ end
50
+
51
+ alias_command "-h", "help"
52
+ alias_command "--help", "help"
53
+
54
+ private
55
+
56
+ def commands_for_namespace(name)
57
+ Azuki::Command.commands.values.select do |command|
58
+ command[:namespace] == name && command[:command] != name
59
+ end
60
+ end
61
+
62
+ def namespaces
63
+ namespaces = Azuki::Command.namespaces
64
+ namespaces.delete("app")
65
+ namespaces
66
+ end
67
+
68
+ def commands
69
+ Azuki::Command.commands
70
+ end
71
+
72
+ def legacy_help_for_namespace(namespace)
73
+ instance = Azuki::Command::Help.groups.map do |group|
74
+ [ group.title, group.select { |c| c.first =~ /^#{namespace}/ }.length ]
75
+ end.sort_by { |l| l.last }.last
76
+ return nil unless instance
77
+ return nil if instance.last.zero?
78
+ instance.first
79
+ end
80
+
81
+ def legacy_help_for_command(command)
82
+ Azuki::Command::Help.groups.each do |group|
83
+ group.each do |cmd, description|
84
+ return description if cmd.split(" ").first == command
85
+ end
86
+ end
87
+ nil
88
+ end
89
+
90
+ def skip_namespace?(ns)
91
+ return true if ns[:description] =~ /DEPRECATED:/
92
+ return true if ns[:description] =~ /HIDDEN:/
93
+ false
94
+ end
95
+
96
+ def skip_command?(command)
97
+ return true if command[:help] =~ /DEPRECATED:/
98
+ return true if command[:help] =~ /^ HIDDEN:/
99
+ false
100
+ end
101
+
102
+ def primary_namespaces
103
+ PRIMARY_NAMESPACES.map { |name| namespaces[name] }.compact
104
+ end
105
+
106
+ def additional_namespaces
107
+ (namespaces.values - primary_namespaces)
108
+ end
109
+
110
+ def summary_for_namespaces(namespaces)
111
+ size = longest(namespaces.map { |n| n[:name] })
112
+ namespaces.sort_by {|namespace| namespace[:name]}.each do |namespace|
113
+ next if skip_namespace?(namespace)
114
+ name = namespace[:name]
115
+ namespace[:description] ||= legacy_help_for_namespace(name)
116
+ puts " %-#{size}s # %s" % [ name, namespace[:description] ]
117
+ end
118
+ end
119
+
120
+ def help_for_root
121
+ puts "Usage: azuki COMMAND [--app APP] [command-specific-options]"
122
+ puts
123
+ puts "Primary help topics, type \"azuki help TOPIC\" for more details:"
124
+ puts
125
+ summary_for_namespaces(primary_namespaces)
126
+ puts
127
+ puts "Additional topics:"
128
+ puts
129
+ summary_for_namespaces(additional_namespaces)
130
+ puts
131
+ end
132
+
133
+ def help_for_namespace(name)
134
+ namespace_commands = commands_for_namespace(name)
135
+
136
+ unless namespace_commands.empty?
137
+ size = longest(namespace_commands.map { |c| c[:banner] })
138
+ namespace_commands.sort_by { |c| c[:banner].to_s }.each do |command|
139
+ next if skip_command?(command)
140
+ command[:summary] ||= legacy_help_for_command(command[:command])
141
+ puts " %-#{size}s # %s" % [ command[:banner], command[:summary] ]
142
+ end
143
+ end
144
+ end
145
+
146
+ def help_for_command(name)
147
+ if command_alias = Azuki::Command.command_aliases[name]
148
+ display("Alias: #{name} redirects to #{command_alias}")
149
+ name = command_alias
150
+ end
151
+ if command = commands[name]
152
+ puts "Usage: azuki #{command[:banner]}"
153
+
154
+ if command[:help].strip.length > 0
155
+ help = command[:help].split("\n").reject do |line|
156
+ line =~ /HIDDEN/
157
+ end
158
+ puts help[1..-1].join("\n")
159
+ else
160
+ puts
161
+ puts " " + legacy_help_for_command(name).to_s
162
+ end
163
+ puts
164
+ end
165
+
166
+ namespace_commands = commands_for_namespace(name).reject do |command|
167
+ command[:help] =~ /DEPRECATED/
168
+ end
169
+
170
+ if !namespace_commands.empty?
171
+ puts "Additional commands, type \"azuki help COMMAND\" for more details:"
172
+ puts
173
+ help_for_namespace(name)
174
+ puts
175
+ elsif command.nil?
176
+ error "#{name} is not a azuki command. See `azuki help`."
177
+ end
178
+ end
179
+ end
@@ -0,0 +1,115 @@
1
+ require "azuki/command/base"
2
+
3
+ module Azuki::Command
4
+
5
+ # manage authentication keys
6
+ #
7
+ class Keys < Base
8
+
9
+ # keys
10
+ #
11
+ # display keys for the current user
12
+ #
13
+ # -l, --long # display extended information for each key
14
+ #
15
+ #Examples:
16
+ #
17
+ # $ azuki keys
18
+ # === email@example.com Keys
19
+ # ssh-rsa ABCDEFGHIJK...OPQRSTUV== email@example.com
20
+ #
21
+ # $ azuki keys --long
22
+ # === email@example.com Keys
23
+ # ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAp9AJD5QABmOcrkHm6SINuQkDefaR0MUrfgZ1Pxir3a4fM1fwa00dsUwbUaRuR7FEFD8n1E9WwDf8SwQTHtyZsJg09G9myNqUzkYXCmydN7oGr5IdVhRyv5ixcdiE0hj7dRnOJg2poSQ3Qi+Ka8SVJzF7nIw1YhuicHPSbNIFKi5s0D5a+nZb/E6MNGvhxoFCQX2IcNxaJMqhzy1ESwlixz45aT72mXYq0LIxTTpoTqma1HuKdRY8HxoREiivjmMQulYP+CxXFcMyV9kxTKIUZ/FXqlC6G5vSm3J4YScSatPOj9ID5HowpdlIx8F6y4p1/28r2tTl4CY40FFyoke4MQ== email@example.com
24
+ #
25
+ def index
26
+ validate_arguments!
27
+ keys = api.get_keys.body
28
+ if keys.length > 0
29
+ styled_header("#{Azuki::Auth.user} Keys")
30
+ keys = if options[:long]
31
+ keys.map {|key| key["contents"].strip}
32
+ else
33
+ keys.map {|key| format_key_for_display(key["contents"])}
34
+ end
35
+ styled_array(keys)
36
+ else
37
+ display("You have no keys.")
38
+ end
39
+ end
40
+
41
+ # keys:add [KEY]
42
+ #
43
+ # add a key for the current user
44
+ #
45
+ # if no KEY is specified, will try to find ~/.ssh/id_[rd]sa.pub
46
+ #
47
+ #Examples:
48
+ #
49
+ # $ azuki keys:add
50
+ # Could not find an existing public key.
51
+ # Would you like to generate one? [Yn] y
52
+ # Generating new SSH public key.
53
+ # Uploading SSH public key /.ssh/id_rsa.pub... done
54
+ #
55
+ # $ azuki keys:add /my/key.pub
56
+ # Uploading SSH public key /my/key.pub... done
57
+ #
58
+ def add
59
+ keyfile = shift_argument
60
+ validate_arguments!
61
+
62
+ if keyfile
63
+ Azuki::Auth.associate_key(keyfile)
64
+ else
65
+ # make sure we have credentials
66
+ Azuki::Auth.get_credentials
67
+ Azuki::Auth.associate_or_generate_ssh_key
68
+ end
69
+ end
70
+
71
+ # keys:remove KEY
72
+ #
73
+ # remove a key from the current user
74
+ #
75
+ #Examples:
76
+ #
77
+ # $ azuki keys:remove email@example.com
78
+ # Removing email@example.com SSH key... done
79
+ #
80
+ def remove
81
+ key = shift_argument
82
+ if key.nil? || key.empty?
83
+ error("Usage: azuki keys:remove KEY\nMust specify KEY to remove.")
84
+ end
85
+ validate_arguments!
86
+
87
+ action("Removing #{key} SSH key") do
88
+ api.delete_key(key)
89
+ end
90
+ end
91
+
92
+ # keys:clear
93
+ #
94
+ # remove all authentication keys from the current user
95
+ #
96
+ #Examples:
97
+ #
98
+ # $ azuki keys:clear
99
+ # Removing all SSH keys... done
100
+ #
101
+ def clear
102
+ validate_arguments!
103
+
104
+ action("Removing all SSH keys") do
105
+ api.delete_keys
106
+ end
107
+ end
108
+
109
+ protected
110
+ def format_key_for_display(key)
111
+ type, hex, local = key.strip.split(/\s/)
112
+ [type, hex[0,10] + '...' + hex[-10,10], local].join(' ')
113
+ end
114
+ end
115
+ end