kontena-cli 1.0.0.pre2 → 1.0.0.pre3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/VERSION +1 -1
  3. data/lib/kontena/callbacks/master/deploy/70_invite_self_after_deploy.rb +2 -1
  4. data/lib/kontena/cli/master/users/invite_command.rb +6 -2
  5. data/lib/kontena/cli/stack_command.rb +12 -18
  6. data/lib/kontena/cli/stacks/build_command.rb +3 -1
  7. data/lib/kontena/cli/stacks/common.rb +17 -0
  8. data/lib/kontena/cli/stacks/deploy_command.rb +11 -10
  9. data/lib/kontena/cli/stacks/install_command.rb +8 -5
  10. data/lib/kontena/cli/stacks/list_command.rb +3 -0
  11. data/lib/kontena/cli/stacks/logs_command.rb +6 -4
  12. data/lib/kontena/cli/stacks/monitor_command.rb +10 -9
  13. data/lib/kontena/cli/stacks/registry/pull_command.rb +28 -0
  14. data/lib/kontena/cli/stacks/registry/push_command.rb +22 -0
  15. data/lib/kontena/cli/stacks/registry/remove_command.rb +30 -0
  16. data/lib/kontena/cli/stacks/registry/search_command.rb +24 -0
  17. data/lib/kontena/cli/stacks/registry/show_command.rb +28 -0
  18. data/lib/kontena/cli/stacks/registry_command.rb +17 -0
  19. data/lib/kontena/cli/stacks/remove_command.rb +11 -9
  20. data/lib/kontena/cli/stacks/show_command.rb +11 -10
  21. data/lib/kontena/cli/stacks/upgrade_command.rb +8 -5
  22. data/lib/kontena/cli/stacks/yaml/custom_validators/extends_validator.rb +2 -1
  23. data/lib/kontena/cli/stacks/yaml/reader.rb +24 -6
  24. data/lib/kontena/command.rb +9 -3
  25. data/lib/kontena/stacks_cache.rb +35 -9
  26. data/lib/kontena/stacks_client.rb +17 -13
  27. data/spec/fixtures/stack-with-ifs.yml +51 -0
  28. data/spec/kontena/cli/master/users/invite_command_spec.rb +1 -2
  29. data/spec/kontena/cli/stacks/deploy_command_spec.rb +2 -2
  30. data/spec/kontena/cli/stacks/install_command_spec.rb +2 -2
  31. data/spec/kontena/cli/stacks/remove_command_spec.rb +2 -2
  32. data/spec/kontena/cli/stacks/show_command_spec.rb +2 -2
  33. data/spec/kontena/cli/stacks/upgrade_command_spec.rb +2 -4
  34. metadata +9 -4
  35. data/lib/kontena/cli/stacks/pull_command.rb +0 -12
  36. data/lib/kontena/cli/stacks/push_command.rb +0 -17
  37. data/lib/kontena/cli/stacks/search_command.rb +0 -17
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: bb909be3c98e6f195a842ad6291f01a061f5a99e
4
- data.tar.gz: e19fbfaeb34ae9b1f980bdcd9daf1f5a946a4339
3
+ metadata.gz: 4ba22a3516366cf0d4cb3d9b44e4460984a0cabf
4
+ data.tar.gz: 9e220dd6c8231906600723523ca8e59badc537ab
5
5
  SHA512:
6
- metadata.gz: 35adfcc4263f86a37f5d87c2ccc487c1db64e831092f1eeb9538575d7f01f11053001c52925d89f401806a8e7767cd5907893809bd497efa798a6acaa30b4705
7
- data.tar.gz: b32a33ee26a54ba74db995a020a95ca7f310f43da9bc144ab6147fdeafeec3d927765f82df3defb98e27854a8b240c6864eae1979608dd5160b0aab1aff47292
6
+ metadata.gz: 4088c0e1841b305ff61091f29adcae757045ecdf629840dd0988c83efc91e4b63d22e993c6766c3c36642c5e0fcdca3a48ef4a3e8c4ce405d73c0f1d6a9e9d9a
7
+ data.tar.gz: c15c7cee466cd4510f09616651d1ade116ba0b62e2c7dc8c6d415b630d88efbd88a665058dc2e474176dfbc1d846c454411cca16a80d0827c486d66bb10aa86d
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.0.0.pre2
1
+ 1.0.0.pre3
@@ -17,6 +17,7 @@ module Kontena
17
17
  if response && response.kind_of?(Hash) && response.has_key?('data') && response['data'].has_key?('attributes')
18
18
  user_data[:email] = response['data']['attributes']['email']
19
19
  user_data[:username] = response['data']['attributes']['username']
20
+ user_data[:id] = response['data']['id']
20
21
  user_data[:verified] = response['data']['attributes']['verified']
21
22
  @cloud_user_data = user_data
22
23
  end
@@ -32,7 +33,7 @@ module Kontena
32
33
 
33
34
  invite_response = nil
34
35
  spinner "Creating user #{cloud_user_data[:email]} into Kontena Master" do |spin|
35
- invite_response = Kontena.run("master users invite --return #{cloud_user_data[:email].shellescape}", returning: :result)
36
+ invite_response = Kontena.run("master users invite --external-id #{cloud_user_data[:id]} --return #{cloud_user_data[:email].shellescape}", returning: :result)
36
37
  unless invite_response.kind_of?(Hash) && invite_response.has_key?('invite_code')
37
38
  spin.fail
38
39
  end
@@ -9,6 +9,7 @@ module Kontena::Cli::Master::Users
9
9
 
10
10
  option ['-r', '--roles'], '[ROLES]', 'Comma separated list of roles to assign to the invited users'
11
11
  option ['-c', '--code'], :flag, 'Only output the invite code'
12
+ option '--external-id', '[EXTERNAL ID]', 'Assign external id to user', hidden: true
12
13
  option '--return', :flag, 'Return the code', hidden: true
13
14
 
14
15
  requires_current_master
@@ -20,10 +21,13 @@ module Kontena::Cli::Master::Users
20
21
  else
21
22
  roles = []
22
23
  end
23
-
24
+ external_id = nil
25
+ if email_list.size == 1 && self.external_id
26
+ external_id = self.external_id
27
+ end
24
28
  email_list.each do |email|
25
29
  begin
26
- data = { email: email, response_type: 'invite' }
30
+ data = { email: email, external_id: external_id, response_type: 'invite' }
27
31
  response = client.post('/oauth2/authorize', data)
28
32
  if self.code?
29
33
  puts response['invite_code']
@@ -7,27 +7,21 @@ require_relative 'stacks/show_command'
7
7
  require_relative 'stacks/build_command'
8
8
  require_relative 'stacks/monitor_command'
9
9
  require_relative 'stacks/logs_command'
10
- require_relative 'stacks/push_command'
11
- require_relative 'stacks/pull_command'
12
- require_relative 'stacks/search_command'
13
- require_relative 'stacks/install_command'
10
+ require_relative 'stacks/registry_command'
14
11
 
15
12
  class Kontena::Cli::StackCommand < Kontena::Command
16
13
 
17
- subcommand "install", "Install a stack", Kontena::Cli::Stacks::InstallCommand
18
- subcommand "build", "Build stack file images", Kontena::Cli::Stacks::BuildCommand
19
- subcommand ["ls", "list"], "List stacks", Kontena::Cli::Stacks::ListCommand
20
- subcommand "show", "Show stack details", Kontena::Cli::Stacks::ShowCommand
21
- subcommand "upgrade", "Upgrade installed stack", Kontena::Cli::Stacks::UpgradeCommand
22
- subcommand "deploy", "Deploy stack", Kontena::Cli::Stacks::DeployCommand
23
- subcommand "logs", "Show logs from stack services", Kontena::Cli::Stacks::LogsCommand
24
- subcommand "monitor", "Monitor stack services", Kontena::Cli::Stacks::MonitorCommand
25
- subcommand ["remove","rm"], "Remove a deployed stack", Kontena::Cli::Stacks::RemoveCommand
26
- subcommand "push", "Push a stack to stacks registry", Kontena::Cli::Stacks::PushCommand
27
- subcommand ["pull", "get"], "Pull a stack from stacks registry", Kontena::Cli::Stacks::PullCommand
28
- subcommand "install", "Deploy a stack to Kontena Master", Kontena::Cli::Stacks::InstallCommand
29
- subcommand "search", "Search for stacks in stacks repository", Kontena::Cli::Stacks::SearchCommand
30
-
14
+ subcommand "install", "Install a stack to a grid", Kontena::Cli::Stacks::InstallCommand
15
+ subcommand ["ls", "list"], "List installed stacks in a grid", Kontena::Cli::Stacks::ListCommand
16
+ subcommand ["remove","rm"], "Remove a deployed stack from a grid", Kontena::Cli::Stacks::RemoveCommand
17
+ subcommand "show", "Show details about a stack in a grid", Kontena::Cli::Stacks::ShowCommand
18
+ subcommand "upgrade", "Upgrade a stack in a grid", Kontena::Cli::Stacks::UpgradeCommand
19
+ subcommand ["start", "deploy"], "Deploy an installed stack in a grid", Kontena::Cli::Stacks::DeployCommand
20
+ subcommand "logs", "Show logs from services in a stack", Kontena::Cli::Stacks::LogsCommand
21
+ subcommand "monitor", "Monitor services in a stack", Kontena::Cli::Stacks::MonitorCommand
22
+ subcommand "build", "Build images listed in a stack file and push them to an image registry", Kontena::Cli::Stacks::BuildCommand
23
+ subcommand "registry", "Stack registry related commands", Kontena::Cli::Stacks::RegistryCommand
24
+
31
25
  def execute
32
26
  end
33
27
  end
@@ -1,10 +1,12 @@
1
1
  require_relative 'common'
2
2
 
3
3
  module Kontena::Cli::Stacks
4
- class BuildCommand < Clamp::Command
4
+ class BuildCommand < Kontena::Command
5
5
  include Kontena::Cli::Common
6
6
  include Common
7
7
 
8
+ banner "Build images listed in a stack file and push them to your image registry"
9
+
8
10
  option ['-f', '--file'], 'FILE', 'Specify an alternate Kontena compose file', attribute_name: :filename, default: 'kontena.yml'
9
11
  option ['--no-cache'], :flag, 'Do not use cache when building the image', default: false
10
12
  option ['--no-push'], :flag, 'Do not push images to registry', default: false
@@ -7,6 +7,23 @@ module Kontena::Cli::Stacks
7
7
  module Common
8
8
  include Kontena::Cli::Services::ServicesHelper
9
9
 
10
+ module StackNameParam
11
+ attr_accessor :stack_version
12
+
13
+ def self.included(where)
14
+ where.parameter "STACK_NAME", "Stack name, for example user/stackname or user/stackname:version" do |name|
15
+ if name.include?(':')
16
+ name, @stack_version = name.split(':',2 )
17
+ end
18
+ name
19
+ end
20
+ end
21
+ end
22
+
23
+ def stack_name
24
+ @stack_name ||= self.name || stack_name_from_yaml(filename)
25
+ end
26
+
10
27
  def stack_from_yaml(filename)
11
28
  reader = Kontena::Cli::Stacks::YAML::Reader.new(filename)
12
29
  if reader.stack_name.nil?
@@ -6,33 +6,34 @@ module Kontena::Cli::Stacks
6
6
  include Kontena::Cli::GridOptions
7
7
  include Common
8
8
 
9
+ banner "Deploys all services of a stack that has been installed in a grid on Kontena Master"
10
+
9
11
  parameter "NAME", "Stack name"
10
12
 
11
- def execute
12
- require_api_url
13
- token = require_token
13
+ requires_current_master
14
+ requires_current_master_token
14
15
 
16
+ def execute
15
17
  deployment = nil
16
18
  spinner "Deploying stack #{pastel.cyan(name)}" do
17
- deployment = deploy_stack(token, name)
19
+ deployment = deploy_stack(name)
18
20
  deployment['service_deploys'].each do |service_deploy|
19
- wait_for_deploy_to_finish(token, service_deploy)
21
+ wait_for_deploy_to_finish(service_deploy)
20
22
  end
21
23
  end
22
24
  end
23
25
 
24
- def deploy_stack(token, name)
25
- client(token).post("stacks/#{current_grid}/#{name}/deploy", {})
26
+ def deploy_stack(name)
27
+ client.post("stacks/#{current_grid}/#{name}/deploy", {})
26
28
  end
27
29
 
28
- # @param [String] token
29
30
  # @param [Hash] deployment
30
31
  # @return [Boolean]
31
- def wait_for_deploy_to_finish(token, deployment, timeout = 600)
32
+ def wait_for_deploy_to_finish(deployment, timeout = 600)
32
33
  deployed = false
33
34
  Timeout::timeout(timeout) do
34
35
  until deployed
35
- deployment = client(token).get("services/#{deployment['service_id']}/deploys/#{deployment['id']}")
36
+ deployment = client.get("services/#{deployment['service_id']}/deploys/#{deployment['id']}")
36
37
  deployed = true if deployment['finished_at']
37
38
  sleep 1
38
39
  end
@@ -6,25 +6,28 @@ module Kontena::Cli::Stacks
6
6
  include Kontena::Cli::GridOptions
7
7
  include Common
8
8
 
9
+ banner "Installs a stack to a grid on Kontena Master"
10
+
9
11
  parameter "[FILE]", "Kontena stack file", default: "kontena.yml", attribute_name: :filename
10
12
 
11
13
  option ['-n', '--name'], 'NAME', 'Define stack name (by default comes from stack file)'
12
14
  option '--deploy', :flag, 'Deploy after installation'
13
15
 
16
+ requires_current_master
17
+ requires_current_master_token
18
+
14
19
  def execute
15
- require_api_url
16
- token = require_token
17
20
  require_config_file(filename)
18
21
  stack = stack_from_yaml(filename)
19
22
  stack['name'] = name if name
20
23
  spinner "Creating stack #{pastel.cyan(stack['name'])} " do
21
- create_stack(token, stack)
24
+ create_stack(stack)
22
25
  end
23
26
  Kontena.run("stack deploy #{stack['name']}") if deploy?
24
27
  end
25
28
 
26
- def create_stack(token, stack)
27
- client(token).post("grids/#{current_grid}/stacks", stack)
29
+ def create_stack(stack)
30
+ client.post("grids/#{current_grid}/stacks", stack)
28
31
  end
29
32
  end
30
33
  end
@@ -6,7 +6,10 @@ module Kontena::Cli::Stacks
6
6
  include Kontena::Cli::GridOptions
7
7
  include Common
8
8
 
9
+ banner "Lists all installed stacks on a grid in Kontena Master"
10
+
9
11
  requires_current_master
12
+ requires_current_master_token
10
13
 
11
14
  def execute
12
15
  list_stacks
@@ -1,18 +1,20 @@
1
1
  module Kontena::Cli::Stacks
2
- class LogsCommand < Clamp::Command
2
+ class LogsCommand < Kontena::Command
3
3
  include Kontena::Cli::Common
4
4
  include Kontena::Cli::GridOptions
5
5
  include Kontena::Cli::Helpers::LogHelper
6
6
 
7
+ banner "Shows logs from services in a stack"
8
+
7
9
  parameter "NAME", "Stack name"
8
10
  option ["-t", "--tail"], :flag, "Tail (follow) logs", default: false
9
11
  option ["-l", "--lines"], "LINES", "How many lines to show", default: '100'
10
12
  option "--since", "SINCE", "Show logs since given timestamp"
11
13
 
12
- def execute
13
- require_api_url
14
- token = require_token
14
+ requires_current_master
15
+ requires_current_master_token
15
16
 
17
+ def execute
16
18
  query_params = {}
17
19
  query_params[:limit] = lines if lines
18
20
  query_params[:since] = since if since
@@ -1,33 +1,34 @@
1
1
  require_relative 'common'
2
2
 
3
3
  module Kontena::Cli::Stacks
4
- class MonitorCommand < Clamp::Command
4
+ class MonitorCommand < Kontena::Command
5
5
  include Kontena::Cli::Common
6
6
  include Kontena::Cli::GridOptions
7
7
  include Common
8
8
 
9
+ banner "Monitor services in a stack"
10
+
9
11
  parameter "NAME", "Stack name"
10
12
  parameter "[SERVICES] ...", "Stack services to monitor", attribute_name: 'selected_services'
11
13
 
12
- def execute
13
- require_api_url
14
- token = require_token
14
+ requires_current_master
15
+ requires_current_master_token
15
16
 
16
- response = client(token).get("grids/#{current_grid}/services?stack=#{name}")
17
+ def execute
18
+ response = client.get("grids/#{current_grid}/services?stack=#{name}")
17
19
  services = response['services']
18
20
  if selected_services.size > 0
19
21
  services.delete_if{ |s| !selected_services.include?(s['name'])}
20
22
  end
21
- show_monitor(token, services)
23
+ show_monitor(services)
22
24
  end
23
25
 
24
- # @param [String] token
25
26
  # @param [Array<Hash>]
26
- def show_monitor(token, services)
27
+ def show_monitor(services)
27
28
  loop do
28
29
  nodes = {}
29
30
  services.each do |service|
30
- result = client(token).get("services/#{service['id']}/containers") rescue nil
31
+ result = client.get("services/#{service['id']}/containers") rescue nil
31
32
  service['instances'] = 0
32
33
  if result
33
34
  service['instances'] = result['containers'].size
@@ -0,0 +1,28 @@
1
+ require_relative '../common'
2
+
3
+ module Kontena::Cli::Stacks::Registry
4
+ class PullCommand < Kontena::Command
5
+ include Kontena::Cli::Common
6
+ include Kontena::Cli::Stacks::Common
7
+ include Kontena::Cli::Stacks::Common::StackNameParam
8
+
9
+ banner "Pulls / downloads a stack from the stack registry"
10
+
11
+ option ['-F', '--file'], '[FILENAME]', "Write to file (default STDOUT)"
12
+ option '--no-cache', :flag, "Don't use local cache"
13
+ option '--return', :flag, 'Return the result', hidden: true
14
+
15
+ def execute
16
+ target = no_cache? ? stacks_client : Kontena::StacksCache
17
+ content = target.pull(stack_name, stack_version)
18
+ if return?
19
+ return content
20
+ elsif file
21
+ File.write(file, content)
22
+ puts pastel.green("Wrote #{content.bytesize} bytes to #{file}")
23
+ else
24
+ puts content
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,22 @@
1
+ require_relative '../common'
2
+
3
+ module Kontena::Cli::Stacks::Registry
4
+ class PushCommand < Kontena::Command
5
+ include Kontena::Cli::Common
6
+ include Kontena::Cli::Stacks::Common
7
+
8
+ banner "Pushes (uploads) a stack to the stack registry"
9
+
10
+ parameter "FILENAME", "Stack file path"
11
+
12
+ requires_current_account_token
13
+
14
+ def execute
15
+ file = Kontena::Cli::Stacks::YAML::Reader.new(filename, skip_variables: true, replace_missing: "filler")
16
+ name = "#{file.yaml['stack']}:#{file.yaml['version']}"
17
+ spinner("Pushing #{pastel.cyan(name)} to stacks registry") do
18
+ stacks_client.push(file.yaml['stack'], file.yaml['version'], file.raw_content)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,30 @@
1
+ require_relative '../common'
2
+
3
+ module Kontena::Cli::Stacks::Registry
4
+ class RemoveCommand < Kontena::Command
5
+ include Kontena::Cli::Common
6
+ include Kontena::Cli::Stacks::Common
7
+ include Kontena::Cli::Stacks::Common::StackNameParam
8
+
9
+ banner "Removes a stack (or version) from the stack registry. Use user/stack_name or user/stack_name:version."
10
+
11
+ option ['-f', '--force'], :flag, "Force delete"
12
+
13
+ requires_current_account_token
14
+
15
+ def execute
16
+ unless force?
17
+ if stack_version
18
+ puts "About to delete #{pastel.cyan("#{stack_name}:#{stack_version}")} from the stacks registry"
19
+ confirm
20
+ else
21
+ puts "About to delete an entire stack and all of its versions from the stacks registry"
22
+ confirm_command(stack_name)
23
+ end
24
+ end
25
+ spinner "Removing #{pastel.cyan(stack_name)} from the registry" do
26
+ stacks_client.destroy(stack_name, stack_version)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,24 @@
1
+ require_relative '../common'
2
+
3
+ module Kontena::Cli::Stacks::Registry
4
+ class SearchCommand < Kontena::Command
5
+ include Kontena::Cli::Common
6
+ include Kontena::Cli::Stacks::Common
7
+
8
+ banner "Search for stacks on the stack registry"
9
+
10
+ parameter "[QUERY]", "Query string"
11
+
12
+ def execute
13
+ results = stacks_client.search(query.to_s)
14
+ exit_with_error 'Nothing found' if results.empty?
15
+ titles = ['NAME', 'VERSION', 'DESCRIPTION']
16
+ columns = "%-40s %-10s %-40s"
17
+ puts columns % titles
18
+ results.each do |stack|
19
+ stack = ::YAML.load(stacks_client.show(stack['name'])) rescue nil
20
+ puts columns % [stack['stack'], stack['version'], stack['description'] || '-'] if stack
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,28 @@
1
+ require_relative '../common'
2
+
3
+ module Kontena::Cli::Stacks::Registry
4
+ class ShowCommand < Kontena::Command
5
+ include Kontena::Cli::Common
6
+ include Kontena::Cli::Stacks::Common
7
+ include Kontena::Cli::Stacks::Common::StackNameParam
8
+
9
+ banner "Shows information about a stack on the stacks registry"
10
+
11
+ option ['-v', '--versions'], :flag, "Only list available versions"
12
+
13
+ requires_current_account_token
14
+
15
+ def execute
16
+ stack = ::YAML.load(stacks_client.show(stack_name))
17
+ puts "#{stack['stack']}:"
18
+ puts " latest_version: #{stack['version']}"
19
+ puts " expose: #{stack['expose'] || '-'}"
20
+ puts " description: #{stack['description'] || '-'}"
21
+
22
+ puts " available_versions:"
23
+ stacks_client.versions(stack_name).map { |s| s['version']}.sort.reverse_each do |version|
24
+ puts " - #{version}"
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,17 @@
1
+ module Kontena::Cli::Stacks
2
+
3
+ require_relative 'registry/push_command'
4
+ require_relative 'registry/pull_command'
5
+ require_relative 'registry/search_command'
6
+ require_relative 'registry/show_command'
7
+ require_relative 'registry/remove_command'
8
+
9
+ class RegistryCommand < Kontena::Command
10
+
11
+ subcommand "push", "Push a stack into the stacks registry", Registry::PushCommand
12
+ subcommand "pull", "Pull a stack from the stacks registry", Registry::PullCommand
13
+ subcommand "search", "Search for stacks in the stacks registry", Registry::SearchCommand
14
+ subcommand "show", "Show info about a stack in the stacks registry", Registry::ShowCommand
15
+ subcommand ["remove", "rm"], "Remove a stack (or version) from the stacks registry", Registry::RemoveCommand
16
+ end
17
+ end
@@ -6,29 +6,31 @@ module Kontena::Cli::Stacks
6
6
  include Kontena::Cli::GridOptions
7
7
  include Common
8
8
 
9
+ banner "Removes a stack in a grid on Kontena Master"
10
+
9
11
  parameter "NAME", "Stack name"
10
12
  option "--force", :flag, "Force remove", default: false, attribute_name: :forced
11
13
 
12
- def execute
13
- require_api_url
14
- token = require_token
14
+ requires_current_master
15
+ requires_current_master_token
15
16
 
17
+ def execute
16
18
  confirm_command(name) unless forced?
17
19
  spinner "Removing stack #{pastel.cyan(name)} " do
18
- remove_stack(token, name)
19
- wait_stack_removal(token, name)
20
+ remove_stack(name)
21
+ wait_stack_removal(name)
20
22
  end
21
23
  end
22
24
 
23
- def remove_stack(token, name)
24
- client(token).delete("stacks/#{current_grid}/#{name}")
25
+ def remove_stack(name)
26
+ client.delete("stacks/#{current_grid}/#{name}")
25
27
  end
26
28
 
27
- def wait_stack_removal(token, name)
29
+ def wait_stack_removal(name)
28
30
  removed = false
29
31
  until removed == true
30
32
  begin
31
- client(token).get("stacks/#{current_grid}/#{name}")
33
+ client.get("stacks/#{current_grid}/#{name}")
32
34
  sleep 1
33
35
  rescue Kontena::Errors::StandardError => exc
34
36
  if exc.status == 404
@@ -6,17 +6,19 @@ module Kontena::Cli::Stacks
6
6
  include Kontena::Cli::GridOptions
7
7
  include Common
8
8
 
9
+ banner "Show information and status of a stack in a grid on Kontena Master"
10
+
9
11
  parameter "NAME", "Stack name"
10
12
 
11
- def execute
12
- require_api_url
13
- token = require_token
13
+ requires_current_master
14
+ requires_current_master_token
14
15
 
15
- show_stack(token, name)
16
+ def execute
17
+ show_stack(name)
16
18
  end
17
19
 
18
- def show_stack(token, name)
19
- stack = client(token).get("stacks/#{current_grid}/#{name}")
20
+ def show_stack(name)
21
+ stack = client.get("stacks/#{current_grid}/#{name}")
20
22
 
21
23
  puts "#{stack['name']}:"
22
24
  puts " state: #{stack['state']}"
@@ -26,14 +28,13 @@ module Kontena::Cli::Stacks
26
28
  puts " expose: #{stack['expose'] || '-'}"
27
29
  puts " services:"
28
30
  stack['services'].each do |service|
29
- show_service(token, service['id'])
31
+ show_service(service['id'])
30
32
  end
31
33
  end
32
34
 
33
- # @param [String] token
34
35
  # @param [String] service_id
35
- def show_service(token, service_id)
36
- service = get_service(token, service_id)
36
+ def show_service(service_id)
37
+ service = get_service(service_id)
37
38
  pad = ' '.freeze
38
39
  puts "#{pad}#{service['name']}:"
39
40
  puts "#{pad} image: #{service['image']}"
@@ -6,23 +6,26 @@ module Kontena::Cli::Stacks
6
6
  include Kontena::Cli::GridOptions
7
7
  include Common
8
8
 
9
+ banner "Upgrades a stack in a grid on Kontena Master"
10
+
9
11
  parameter "NAME", "Stack name"
10
12
  parameter "[FILE]", "Kontena stack file", default: "kontena.yml"
11
13
  option '--deploy', :flag, 'Deploy after upgrade'
12
14
 
15
+ requires_current_master
16
+ requires_current_master_token
17
+
13
18
  def execute
14
- require_api_url
15
- token = require_token
16
19
  require_config_file(file)
17
20
  stack = stack_from_yaml(file)
18
21
  spinner "Upgrading stack #{pastel.cyan(name)} " do
19
- update_stack(token, stack)
22
+ update_stack(stack)
20
23
  end
21
24
  Kontena.run("stack deploy #{name}") if deploy?
22
25
  end
23
26
 
24
- def update_stack(token, stack)
25
- client(token).put("stacks/#{current_grid}/#{name}", stack)
27
+ def update_stack(stack)
28
+ client.put("stacks/#{current_grid}/#{name}", stack)
26
29
  end
27
30
  end
28
31
  end
@@ -12,7 +12,8 @@ module Kontena::Cli::Stacks::YAML::Validations::CustomValidators
12
12
  if value.is_a?(Hash)
13
13
  extends_validation = {
14
14
  'service' => 'string',
15
- 'file' => HashValidator.optional('string')
15
+ 'file' => HashValidator.optional('string'),
16
+ 'stack' => HashValidator.optional('string')
16
17
  }
17
18
  HashValidator.validator_for(extends_validation).validate(key, value, extends_validation, errors)
18
19
  end
@@ -4,10 +4,11 @@ module Kontena::Cli::Stacks
4
4
  module YAML
5
5
  class Reader
6
6
  include Kontena::Util
7
+ include Kontena::Cli::Common
7
8
 
8
9
  attr_reader :file, :raw_content, :result, :errors, :notifications, :variables, :yaml
9
10
 
10
- def initialize(file, skip_validation: false, skip_variables: false, replace_missing: nil)
11
+ def initialize(file, skip_validation: false, skip_variables: false, replace_missing: nil, from_registry: false)
11
12
  require 'yaml'
12
13
  require_relative 'service_extender'
13
14
  require_relative 'validator_v3'
@@ -17,7 +18,16 @@ module Kontena::Cli::Stacks
17
18
  require_relative 'opto/prompt_resolver'
18
19
 
19
20
  @file = file
20
- @raw_content = File.read(File.expand_path(file))
21
+ @from_registry = from_registry
22
+
23
+ if from_registry?
24
+ require 'shellwords'
25
+ @raw_content = Kontena::StacksCache.pull(file)
26
+ @registry = Kontena::StacksCache::RegistryClientFactory.new.stacks_client.api_url
27
+ else
28
+ @raw_content = File.read(File.expand_path(file))
29
+ end
30
+
21
31
  @errors = []
22
32
  @notifications = []
23
33
  @skip_validation = skip_validation
@@ -27,6 +37,10 @@ module Kontena::Cli::Stacks
27
37
  parse_yaml
28
38
  end
29
39
 
40
+ def from_registry?
41
+ @from_registry == true
42
+ end
43
+
30
44
  # @return [Opto::Group]
31
45
  def variables
32
46
  return @variables if @variables
@@ -49,10 +63,11 @@ module Kontena::Cli::Stacks
49
63
  # @return [Hash]
50
64
  def execute(service_name = nil)
51
65
  result = {}
52
- Dir.chdir(File.dirname(File.expand_path(file))) do
66
+ Dir.chdir(from_registry? ? Dir.pwd : File.dirname(File.expand_path(file))) do
53
67
  result[:stack] = yaml['stack']
54
68
  result[:version] = self.stack_version
55
69
  result[:name] = self.stack_name
70
+ result[:registry] = @registry if from_registry?
56
71
  result[:expose] = yaml['expose']
57
72
  result[:errors] = errors unless skip_validation?
58
73
  result[:notifications] = notifications
@@ -239,9 +254,12 @@ module Kontena::Cli::Stacks
239
254
  def extend_config(service_config)
240
255
  extended_service = extended_service(service_config['extends'])
241
256
  return unless extended_service
242
- filename = service_config['extends']['file']
257
+ filename = service_config['extends']['file']
258
+ stackname = service_config['extends']['stack']
243
259
  if filename
244
260
  parent_config = from_external_file(filename, extended_service)
261
+ elsif stackname
262
+ parent_config = from_external_file(stackname, extended_service, from_registry: true)
245
263
  else
246
264
  raise ("Service '#{extended_service}' not found in #{file}") unless services.has_key?(extended_service)
247
265
  parent_config = process_config(services[extended_service])
@@ -259,8 +277,8 @@ module Kontena::Cli::Stacks
259
277
  end
260
278
  end
261
279
 
262
- def from_external_file(filename, service_name)
263
- outcome = Reader.new(filename, skip_validation: @skip_validation, skip_variables: true, replace_missing: @replace_missing).execute(service_name)
280
+ def from_external_file(filename, service_name, from_registry: false)
281
+ outcome = Reader.new(filename, skip_validation: @skip_validation, skip_variables: true, replace_missing: @replace_missing, from_registry: from_registry).execute(service_name)
264
282
  errors.concat outcome[:errors] unless errors.any? { |item| item.has_key?(filename) }
265
283
  notifications.concat outcome[:notifications] unless notifications.any? { |item| item.has_key?(filename) }
266
284
  outcome[:services]
@@ -91,17 +91,23 @@ class Kontena::Command < Clamp::Command
91
91
  end
92
92
 
93
93
  def self.requires_current_master
94
- banner "#{Kontena.pastel.green("Requires current master")}: This command requires that you have selected a current master using 'kontena master login' or 'kontena master use'. You can also use the environment variable KONTENA_URL to specify the master address or KONTENA_MASTER=master_name to override the current_master setting."
94
+ unless Kontena::Cli::Config.current_master
95
+ banner "#{Kontena.pastel.green("Requires current master")}: This command requires that you have selected a current master using 'kontena master login' or 'kontena master use'. You can also use the environment variable KONTENA_URL to specify the master address or KONTENA_MASTER=master_name to override the current_master setting."
96
+ end
95
97
  @requires_current_master = true
96
98
  end
97
99
 
98
100
  def self.requires_current_grid
99
- banner "#{Kontena.pastel.green("Requires current grid")}: This command requires that you have selected a grid as the current grid using 'kontena grid use' or by setting KONTENA_GRID environment variable."
101
+ unless Kontena::Cli::Config.current_grid
102
+ banner "#{Kontena.pastel.green("Requires current grid")}: This command requires that you have selected a grid as the current grid using 'kontena grid use' or by setting KONTENA_GRID environment variable."
103
+ end
100
104
  @requires_current_grid = true
101
105
  end
102
106
 
103
107
  def self.requires_current_account_token
104
- banner "#{Kontena.pastel.green("Requires account authentication")}: This command requires that you have authenticated to Kontena Cloud using 'kontena cloud auth'"
108
+ unless Kontena::Cli::Config.current_account && Kontena::Cli::Config.current_account.token && Kontena::Cli::Config.current_account.token.access_token
109
+ banner "#{Kontena.pastel.green("Requires account authentication")}: This command requires that you have authenticated to Kontena Cloud using 'kontena cloud auth'"
110
+ end
105
111
  @requires_current_account_token = true
106
112
  end
107
113
 
@@ -1,13 +1,14 @@
1
1
  require_relative 'stacks_client'
2
2
  require_relative 'cli/common'
3
3
  require_relative 'cli/stacks/common'
4
+ require 'yaml'
4
5
 
5
6
  module Kontena
6
7
  class StacksCache
7
8
  class CachedStack
8
9
 
9
- attr_reader :stack
10
- attr_reader :version
10
+ attr_accessor :stack
11
+ attr_accessor :version
11
12
 
12
13
  def initialize(stack, version = nil)
13
14
  unless version
@@ -15,7 +16,6 @@ module Kontena
15
16
  end
16
17
  @stack = stack
17
18
  @version = version
18
- raise ArgumentError, "Stack name and version required" unless @stack && @version
19
19
  end
20
20
 
21
21
  def read
@@ -27,6 +27,11 @@ module Kontena
27
27
  end
28
28
 
29
29
  def write(content)
30
+ raise ArgumentError, "Stack name and version required" unless @stack && @version
31
+ unless File.directory?(File.dirname(path))
32
+ require 'fileutils'
33
+ FileUtils.mkdir_p(File.dirname(path))
34
+ end
30
35
  File.write(path, content)
31
36
  end
32
37
 
@@ -35,14 +40,14 @@ module Kontena
35
40
  end
36
41
 
37
42
  def cached?
43
+ return false unless version
38
44
  File.exist?(path)
39
45
  end
40
46
 
41
47
  def path
42
- return @path if @path
43
- @path = File.expand_path(File.join(base_path, stack, version))
44
- raise "Path traversal attempted" unless @path.start_with?(base_path)
45
- @path
48
+ path = File.expand_path(File.join(base_path, "#{stack}-#{version}.yml"))
49
+ raise "Path traversal attempted" unless path.start_with?(base_path)
50
+ path
46
51
  end
47
52
 
48
53
  private
@@ -58,13 +63,34 @@ module Kontena
58
63
  end
59
64
 
60
65
  class << self
61
- def get(stack, version = nil)
66
+ def pull(stack, version = nil)
62
67
  cache(stack, version).read
63
68
  end
64
69
 
70
+ def dputs(msg)
71
+ ENV["DEBUG"] && puts(msg)
72
+ end
73
+
65
74
  def cache(stack, version = nil)
66
75
  stack = CachedStack.new(stack, version)
67
- stack.write(client.pull(stack.stack, stack.version)) unless stack.cached?
76
+ if stack.cached?
77
+ dputs "Reading from cache: #{stack.path}"
78
+ else
79
+ dputs "Retrieving #{stack.stack}:#{stack.version} from registry"
80
+ content = client.pull(stack.stack, stack.version)
81
+ yaml = ::YAML.load(content)
82
+ new_stack = CachedStack.new(yaml['stack'], yaml['version'])
83
+ if new_stack.cached?
84
+ dputs "Already cached"
85
+ stack = new_stack
86
+ else
87
+ stack.stack = yaml['stack']
88
+ stack.version = yaml['version']
89
+ dputs "Writing #{stack.path}"
90
+ stack.write(content)
91
+ dputs "#{stack.stack}:#{stack.version} cached to #{stack.path}"
92
+ end
93
+ end
68
94
  stack
69
95
  end
70
96
 
@@ -7,31 +7,35 @@ module Kontena
7
7
  ACCEPT_YAML = { 'Accept' => 'application/yaml' }
8
8
  CT_YAML = { 'Content-Type' => 'application/yaml' }
9
9
 
10
- def path_to(repo_name, version = nil)
11
- version ? "/stack/#{repo_name}/version/#{version}" : "/stack/#{repo_name}"
10
+ def path_to(stack_name, version = nil)
11
+ version ? "/stack/#{stack_name}/version/#{version}" : "/stack/#{stack_name}"
12
12
  end
13
13
 
14
- def push(repo_name, version, data)
14
+ def push(stack_name, version, data)
15
15
  post('/stack/', data, {}, CT_YAML)
16
16
  end
17
17
 
18
- def pull(repo_name, version = nil)
19
- get(path_to(repo_name, version), {}, ACCEPT_YAML)
18
+ def show(stack_name)
19
+ get("#{path_to(stack_name, nil)}", {}, ACCEPT_JSON)
20
+ end
21
+
22
+ def versions(stack_name)
23
+ get("#{path_to(stack_name, nil)}/versions", {}, ACCEPT_JSON)['versions']
24
+ end
25
+
26
+ def pull(stack_name, version = nil)
27
+ get(path_to(stack_name, version), {}, ACCEPT_YAML)
20
28
  rescue StandardError => ex
21
- ex.message << " : #{path_to(repo_name, version)}"
29
+ ex.message << " : #{path_to(stack_name, version)}"
22
30
  raise ex, ex.message
23
31
  end
24
32
 
25
33
  def search(query)
26
- get('/search', { q: query }, {}, ACCEPT_JSON)
27
- end
28
-
29
- def versions(repo_name)
30
- get("#{path_to(repo_name)}/versions", {}, ACCEPT_JSON)
34
+ get('/search', { q: query }, {}, ACCEPT_JSON)['stacks']
31
35
  end
32
36
 
33
- def destroy(repo_name, version = nil)
34
- delete(path_to(repo_name, version))
37
+ def destroy(stack_name, version = nil)
38
+ delete(path_to(stack_name, version), {})
35
39
  end
36
40
  end
37
41
  end
@@ -0,0 +1,51 @@
1
+ stack: user/stackname
2
+ version: 0.1.1
3
+ variables:
4
+ db:
5
+ type: enum
6
+ required: true
7
+ options:
8
+ - value: mysql
9
+ label: MySQL
10
+ description: Regular MySQL
11
+ - value: galera
12
+ label: Galera cluster
13
+ description: A mega super galera cluster
14
+ from: prompt
15
+ GALERA_NODES:
16
+ type: integer
17
+ min: 1
18
+ from:
19
+ prompt: Number of Galera nodes
20
+ only_if:
21
+ db: galera
22
+ no_wp:
23
+ type: boolean
24
+ as: boolean # default boolean output is string
25
+ from:
26
+ prompt: Skip wordpress?
27
+ services:
28
+ wordpress:
29
+ skip_if: no_wp
30
+ extends:
31
+ file: docker-compose_v2.yml
32
+ service: wordpress
33
+ image: wordpress
34
+ stateful: true
35
+ deploy:
36
+ strategy: ha
37
+ mysql:
38
+ only_if:
39
+ db: mysql
40
+ extends:
41
+ file: docker-compose_v2.yml
42
+ service: mysql
43
+ image: mysql
44
+ galera:
45
+ only_if:
46
+ db: galera
47
+ extends:
48
+ file: docker-compose_v2.yml
49
+ service: mysql
50
+ image: galera
51
+ instances: $GALERA_NODES
@@ -12,8 +12,7 @@ describe Kontena::Cli::Master::Users::InviteCommand do
12
12
 
13
13
  describe "#invite" do
14
14
  it 'makes invitation request for all given users' do
15
- expect(client).to receive(:post).with("/oauth2/authorize", {email: 'john@example.org', response_type: "invite"}).once
16
- expect(client).to receive(:post).with("/oauth2/authorize", {email: 'jane@example.org', response_type: "invite"}).once
15
+ expect(client).to receive(:post).with("/oauth2/authorize", {email: 'john@example.org', external_id: nil, response_type: "invite"}).once
17
16
 
18
17
  subject.run(['john@example.org', 'jane@example.org'])
19
18
  end
@@ -7,12 +7,12 @@ describe Kontena::Cli::Stacks::DeployCommand do
7
7
 
8
8
  describe '#execute' do
9
9
  it 'requires api url' do
10
- expect(subject).to receive(:require_api_url).once
10
+ expect(described_class.requires_current_master?).to be_truthy
11
11
  subject.run(['test-stack'])
12
12
  end
13
13
 
14
14
  it 'requires token' do
15
- expect(subject).to receive(:require_token).and_return(token)
15
+ expect(described_class.requires_current_master_token?).to be_truthy
16
16
  subject.run(['test-stack'])
17
17
  end
18
18
 
@@ -21,13 +21,13 @@ describe Kontena::Cli::Stacks::InstallCommand do
21
21
 
22
22
  it 'requires api url' do
23
23
  allow(subject).to receive(:stack_from_yaml).with('kontena.yml').and_return(stack)
24
- expect(subject).to receive(:require_api_url).once
24
+ expect(described_class.requires_current_master?).to be_truthy
25
25
  subject.run([])
26
26
  end
27
27
 
28
28
  it 'requires token' do
29
29
  allow(subject).to receive(:stack_from_yaml).with('kontena.yml').and_return(stack)
30
- expect(subject).to receive(:require_token).and_return(token)
30
+ expect(described_class.requires_current_master_token?).to be_truthy
31
31
  subject.run([])
32
32
  end
33
33
 
@@ -9,14 +9,14 @@ describe Kontena::Cli::Stacks::RemoveCommand do
9
9
  it 'requires api url' do
10
10
  allow(subject).to receive(:forced?).and_return(true)
11
11
  allow(subject).to receive(:wait_stack_removal)
12
- expect(subject).to receive(:require_api_url).once
12
+ expect(described_class.requires_current_master?).to be_truthy
13
13
  subject.run(['test-stack'])
14
14
  end
15
15
 
16
16
  it 'requires token' do
17
17
  allow(subject).to receive(:forced?).and_return(true)
18
18
  allow(subject).to receive(:wait_stack_removal)
19
- expect(subject).to receive(:require_token).and_return(token)
19
+ expect(described_class.requires_current_master_token?).to be_truthy
20
20
  subject.run(['test-stack'])
21
21
  end
22
22
 
@@ -8,13 +8,13 @@ describe Kontena::Cli::Stacks::ShowCommand do
8
8
  describe '#execute' do
9
9
  it 'requires api url' do
10
10
  allow(subject).to receive(:forced?).and_return(true)
11
- expect(subject).to receive(:require_api_url).once
11
+ expect(described_class.requires_current_master?).to be_truthy
12
12
  subject.run(['test-stack'])
13
13
  end
14
14
 
15
15
  it 'requires token' do
16
16
  allow(subject).to receive(:forced?).and_return(true)
17
- expect(subject).to receive(:require_token).and_return(token)
17
+ expect(described_class.requires_current_master_token?).to be_truthy
18
18
  subject.run(['test-stack'])
19
19
  end
20
20
 
@@ -17,26 +17,24 @@ describe Kontena::Cli::Stacks::UpgradeCommand do
17
17
  it 'requires api url' do
18
18
  allow(subject).to receive(:require_config_file).and_return(true)
19
19
  allow(subject).to receive(:stack_from_yaml).with('./path/to/kontena.yml').and_return(stack)
20
- expect(subject).to receive(:require_api_url).once
20
+ expect(described_class.requires_current_master?).to be_truthy
21
21
  subject.run(['stack-name', './path/to/kontena.yml'])
22
22
  end
23
23
 
24
24
  it 'requires token' do
25
25
  allow(subject).to receive(:require_config_file).and_return(true)
26
26
  allow(subject).to receive(:stack_from_yaml).with('./path/to/kontena.yml').and_return(stack)
27
- expect(subject).to receive(:require_token).and_return(token)
27
+ expect(described_class.requires_current_master_token?).to be_truthy
28
28
  subject.run(['stack-name', './path/to/kontena.yml'])
29
29
  end
30
30
 
31
31
  it 'requires stack file' do
32
32
  allow(subject).to receive(:stack_from_yaml).with('./path/to/kontena.yml').and_return(stack)
33
- allow(subject).to receive(:require_token).and_return(token)
34
33
  expect(subject).to receive(:require_config_file).with('./path/to/kontena.yml').and_return(true)
35
34
  subject.run(['stack-name', './path/to/kontena.yml'])
36
35
  end
37
36
 
38
37
  it 'uses kontena.yml as default stack file' do
39
- allow(subject).to receive(:require_token).and_return(token)
40
38
  expect(subject).to receive(:require_config_file).with('kontena.yml').and_return(true)
41
39
  expect(subject).to receive(:stack_from_yaml).with('kontena.yml').and_return(stack)
42
40
  subject.run(['stack-name'])
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kontena-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0.pre2
4
+ version: 1.0.0.pre3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kontena, Inc
@@ -370,10 +370,13 @@ files:
370
370
  - lib/kontena/cli/stacks/list_command.rb
371
371
  - lib/kontena/cli/stacks/logs_command.rb
372
372
  - lib/kontena/cli/stacks/monitor_command.rb
373
- - lib/kontena/cli/stacks/pull_command.rb
374
- - lib/kontena/cli/stacks/push_command.rb
373
+ - lib/kontena/cli/stacks/registry/pull_command.rb
374
+ - lib/kontena/cli/stacks/registry/push_command.rb
375
+ - lib/kontena/cli/stacks/registry/remove_command.rb
376
+ - lib/kontena/cli/stacks/registry/search_command.rb
377
+ - lib/kontena/cli/stacks/registry/show_command.rb
378
+ - lib/kontena/cli/stacks/registry_command.rb
375
379
  - lib/kontena/cli/stacks/remove_command.rb
376
- - lib/kontena/cli/stacks/search_command.rb
377
380
  - lib/kontena/cli/stacks/service_generator.rb
378
381
  - lib/kontena/cli/stacks/service_generator_v2.rb
379
382
  - lib/kontena/cli/stacks/show_command.rb
@@ -442,6 +445,7 @@ files:
442
445
  - spec/fixtures/stack-internal-extend.yml
443
446
  - spec/fixtures/stack-invalid.yml
444
447
  - spec/fixtures/stack-with-env-file.yml
448
+ - spec/fixtures/stack-with-ifs.yml
445
449
  - spec/fixtures/stack-with-prompted-variables.yml
446
450
  - spec/fixtures/stack-with-variables.yml
447
451
  - spec/fixtures/wordpress-scaled.yml
@@ -556,6 +560,7 @@ test_files:
556
560
  - spec/fixtures/stack-internal-extend.yml
557
561
  - spec/fixtures/stack-invalid.yml
558
562
  - spec/fixtures/stack-with-env-file.yml
563
+ - spec/fixtures/stack-with-ifs.yml
559
564
  - spec/fixtures/stack-with-prompted-variables.yml
560
565
  - spec/fixtures/stack-with-variables.yml
561
566
  - spec/fixtures/wordpress-scaled.yml
@@ -1,12 +0,0 @@
1
- require_relative 'common'
2
-
3
- module Kontena::Cli::Stacks
4
- class PullCommand < Kontena::Command
5
- include Kontena::Cli::Common
6
- include Common
7
-
8
- def execute
9
- end
10
- end
11
- end
12
-
@@ -1,17 +0,0 @@
1
- require_relative 'common'
2
-
3
- module Kontena::Cli::Stacks
4
- class PushCommand < Kontena::Command
5
- include Kontena::Cli::Common
6
- include Common
7
-
8
- parameter "FILENAME", "Stack file path"
9
-
10
- requires_current_account_token
11
-
12
- def execute
13
- file = YAML::Reader.new(self.filename, skip_variables: true, replace_missing: "filler")
14
- stacks_client.push(file.yaml['stack'], file.yaml['version'], file.raw_content)
15
- end
16
- end
17
- end
@@ -1,17 +0,0 @@
1
- require_relative 'common'
2
-
3
- module Kontena::Cli::Stacks
4
- class SearchCommand < Kontena::Command
5
- include Kontena::Cli::Common
6
- include Common
7
-
8
- parameter '[QUERY]', "Query string"
9
-
10
- requires_current_account_token
11
-
12
- def execute
13
- puts stacks_client.search(query).inspect
14
- end
15
- end
16
- end
17
-