kontena-cli 1.0.0.pre2 → 1.0.0.pre3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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
-