kontena-cli 1.5.0.pre5 → 1.5.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (30) hide show
  1. checksums.yaml +4 -4
  2. data/VERSION +1 -1
  3. data/lib/kontena/callbacks/master/deploy/05_before_deploy_configuration_wizard.rb +2 -2
  4. data/lib/kontena/cli/services/deploy_command.rb +1 -1
  5. data/lib/kontena/cli/stacks/deploy_command.rb +1 -1
  6. data/lib/kontena/cli/stacks/install_command.rb +11 -0
  7. data/lib/kontena/cli/stacks/registry/create_command.rb +24 -0
  8. data/lib/kontena/cli/stacks/registry/make_private_command.rb +24 -0
  9. data/lib/kontena/cli/stacks/registry/make_public_command.rb +24 -0
  10. data/lib/kontena/cli/stacks/registry/pull_command.rb +1 -1
  11. data/lib/kontena/cli/stacks/registry/push_command.rb +1 -2
  12. data/lib/kontena/cli/stacks/registry/remove_command.rb +1 -1
  13. data/lib/kontena/cli/stacks/registry/search_command.rb +23 -10
  14. data/lib/kontena/cli/stacks/registry/show_command.rb +44 -11
  15. data/lib/kontena/cli/stacks/registry_command.rb +3 -0
  16. data/lib/kontena/cli/stacks/show_command.rb +15 -1
  17. data/lib/kontena/cli/stacks/upgrade_command.rb +9 -0
  18. data/lib/kontena/cli/stacks/yaml/reader.rb +2 -0
  19. data/lib/kontena/cli/stacks/yaml/stack_file_loader/registry_loader.rb +1 -1
  20. data/lib/kontena/client.rb +8 -0
  21. data/lib/kontena/scripts/completer.rb +2 -2
  22. data/lib/kontena/stacks_cache.rb +22 -25
  23. data/lib/kontena/stacks_client.rb +133 -20
  24. data/omnibus/wrappers/sh/kontena +1 -1
  25. data/spec/fixtures/kontena_v3_with_metadata.yml +28 -0
  26. data/spec/kontena/cli/stacks/install_command_spec.rb +26 -1
  27. data/spec/kontena/cli/stacks/upgrade_command_spec.rb +8 -1
  28. data/spec/kontena/cli/stacks/yaml/reader_spec.rb +8 -8
  29. data/spec/kontena/cli/stacks/yaml/stack_file_loader/registry_loader_spec.rb +1 -1
  30. metadata +7 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0a7efe145812b07faaf7db786cfd9f5320f6790b1f687d87d0bf284dd75e13f2
4
- data.tar.gz: b69ab9f071460527d24bee50a0429b469728d3fce545c23fbc37faf96105cc5f
3
+ metadata.gz: b512497470a658b7be3dae937f44e65d8ebc673d867a20e7d5a04bf9dab2b89f
4
+ data.tar.gz: 8486842c3eba0ee7af57283f108e02fb610a3e83ceb30090e9309fb1eaa3b204
5
5
  SHA512:
6
- metadata.gz: f1132d851ba291392f713f01b5dd27f4ae9d98afe70e7fb2b3ef1fb00c944e73a73032b7afdcef7e1fedea499dd075cb1aa8936305f0b7e603a674b7adb218bb
7
- data.tar.gz: f7bfa592fdf9663608321c79bed54b0feabac4337f6426c3fb07d0a1bcd004cd098c12e29679e4a096d4f8dc1eefedd5a03459971bb9224c72fd7525566a9a37
6
+ metadata.gz: 41400fae5d6590041c57594981ab97aea90e0e5103c0a0b8ea6215c8d00187259c529786f7230742f347666a525fb1fadfbcf0e6951c6971975e12823a2ba057
7
+ data.tar.gz: f60a87d8531854e09bea64fd2fa895dff9c6de9c7805ad7497a007910b80ec449d55dd7bed0af81cfacb051ad86dcf65a61ac882ca6a60b1b26151aa566d8d0e
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.5.0.pre5
1
+ 1.5.0.rc1
@@ -83,13 +83,13 @@ module Kontena
83
83
  command.skip_auth_provider = false
84
84
  when :custom
85
85
  puts
86
- puts 'Learn how to configure custom user authentication provider after installation at: www.kontena.io/docs/using-kontena/authentication'
86
+ puts 'Learn how to configure custom user authentication provider after installation at: www.kontena.io/docs/advanced/authentication'
87
87
  puts
88
88
  command.cloud_master_id = nil
89
89
  command.skip_auth_provider = true
90
90
  when :none
91
91
  puts
92
- puts "You have selected to use Kontena Master in single user mode. You can configure an authentication provider later. For more information, see here: www.kontena.io/docs/using-kontena/authentication"
92
+ puts "You have selected to use Kontena Master in single user mode. You can configure an authentication provider later. For more information, see here: www.kontena.io/docs/advanced/authentication"
93
93
  puts
94
94
  command.cloud_master_id = nil
95
95
  command.skip_auth_provider = true
@@ -7,7 +7,7 @@ module Kontena::Cli::Services
7
7
  include ServicesHelper
8
8
 
9
9
  parameter "NAME", "Service name"
10
- option '--[no-]wait', :flag, 'Do not wait for service deployment', default: true
10
+ option '--[no-]wait', :flag, 'Wait for service deployment to finish', default: true
11
11
  option '--force', :flag, 'Force deploy even if service does not have any changes'
12
12
 
13
13
  def execute
@@ -10,7 +10,7 @@ module Kontena::Cli::Stacks
10
10
 
11
11
  parameter "NAME ...", "Stack name", attribute_name: :names
12
12
 
13
- option '--[no-]wait', :flag, 'Do not wait for service deployment', default: true
13
+ option '--[no-]wait', :flag, 'Wait for deployment to finish', default: true
14
14
 
15
15
  requires_current_master
16
16
  requires_current_master_token
@@ -20,6 +20,8 @@ module Kontena::Cli::Stacks
20
20
  option '--parent-name', '[PARENT_NAME]', "Set parent stack name", hidden: true
21
21
  option '--skip-dependencies', :flag, "Do not install any stack dependencies"
22
22
 
23
+ option "--force", :flag, "Force install", default: false, attribute_name: :forced
24
+
23
25
  requires_current_master
24
26
  requires_current_master_token
25
27
 
@@ -30,6 +32,15 @@ module Kontena::Cli::Stacks
30
32
 
31
33
  stack # runs validations
32
34
 
35
+ kontena_requirement = stack.dig('metadata', 'required_kontena_version')
36
+ unless kontena_requirement.nil?
37
+ master_version = Gem::Version.new(client.server_version)
38
+ unless Gem::Requirement.new(kontena_requirement).satisfied_by?(master_version)
39
+ puts "#{pastel.red("Warning: ")} Stack requires kontena version #{kontena_requirement} but Master version is #{master_version}"
40
+ confirm("Are you sure? You can skip this prompt by running this command with --force option") unless forced?
41
+ end
42
+ end
43
+
33
44
  hint_on_validation_notifications(reader.notifications)
34
45
  abort_on_validation_errors(reader.errors)
35
46
 
@@ -0,0 +1,24 @@
1
+ require_relative '../common'
2
+
3
+ module Kontena::Cli::Stacks::Registry
4
+ class CreateCommand < Kontena::Command
5
+ include Kontena::Cli::Common
6
+ include Kontena::Cli::Stacks::Common
7
+ include Kontena::Cli::Stacks::Common::RegistryNameParam
8
+
9
+ banner "Creates a new Stack to Kontena Stack Registry"
10
+
11
+ option '--private', :flag, "Create as private", attribute_name: :is_private
12
+
13
+ requires_current_account_token
14
+
15
+ def execute
16
+ exit_with_error "Can't create a stack with a version number" unless stack_name.version.nil?
17
+ spinner "Creating #{is_private? ? pastel.yellow('private') : 'public'} stack #{pastel.cyan(stack_name)} in Kontena Stack Registry" do
18
+ stacks_client.create(stack_name, is_private: is_private?)
19
+ end
20
+ end
21
+ end
22
+ end
23
+
24
+
@@ -0,0 +1,24 @@
1
+ require_relative '../common'
2
+
3
+ module Kontena::Cli::Stacks::Registry
4
+ class MakePrivateCommand < Kontena::Command
5
+ include Kontena::Cli::Common
6
+ include Kontena::Cli::Stacks::Common
7
+ include Kontena::Cli::Stacks::Common::RegistryNameParam
8
+
9
+ banner "Changes Stack visibility private in the Kontena Cloud Stack Registry"
10
+
11
+ option '--force', :flag, "Don't ask for confirmation"
12
+
13
+ requires_current_account_token
14
+
15
+ def execute
16
+ unless force?
17
+ confirm("Change stack #{pastel.cyan(stack_name)} visibility to private?")
18
+ end
19
+ spinner "Updating Stack #{pastel.cyan(stack_name)} visibility to private" do
20
+ stacks_client.make_private(stack_name)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,24 @@
1
+ require_relative '../common'
2
+
3
+ module Kontena::Cli::Stacks::Registry
4
+ class MakePublicCommand < Kontena::Command
5
+ include Kontena::Cli::Common
6
+ include Kontena::Cli::Stacks::Common
7
+ include Kontena::Cli::Stacks::Common::RegistryNameParam
8
+
9
+ banner "Changes Stack visibility private in the Kontena Cloud Stack Registry"
10
+
11
+ option '--force', :flag, "Don't ask for confirmation"
12
+
13
+ requires_current_account_token
14
+
15
+ def execute
16
+ unless force?
17
+ confirm("Change stack #{pastel.cyan(stack_name)} visibility to public?")
18
+ end
19
+ spinner "Updating Stack #{pastel.cyan(stack_name)} visibility to public" do
20
+ stacks_client.make_public(stack_name)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -14,7 +14,7 @@ module Kontena::Cli::Stacks::Registry
14
14
 
15
15
  def execute
16
16
  target = no_cache? ? stacks_client : Kontena::StacksCache
17
- content = target.pull(stack_name.stack_name, stack_name.version)
17
+ content = target.pull(stack_name)
18
18
  if return?
19
19
  return content
20
20
  elsif file
@@ -30,8 +30,7 @@ module Kontena::Cli::Stacks::Registry
30
30
  spinner("Pushing #{pastel.cyan(source)} to stack registry as #{loader.stack_name}") do
31
31
  unless dry_run?
32
32
  stacks_client.push(
33
- loader.stack_name.stack_name,
34
- loader.stack_name.version,
33
+ loader.stack_name,
35
34
  loader.content
36
35
  )
37
36
  end
@@ -23,7 +23,7 @@ module Kontena::Cli::Stacks::Registry
23
23
  end
24
24
  end
25
25
  spinner "Removing #{pastel.cyan(stack_name)} from the registry" do
26
- stacks_client.destroy(stack_name.stack_name, stack_name.version)
26
+ stacks_client.destroy(stack_name)
27
27
  end
28
28
  end
29
29
  end
@@ -4,25 +4,38 @@ module Kontena::Cli::Stacks::Registry
4
4
  class SearchCommand < Kontena::Command
5
5
  include Kontena::Cli::Common
6
6
  include Kontena::Cli::Stacks::Common
7
+ include Kontena::Cli::TableGenerator::Helper
7
8
 
8
9
  banner "Search for stacks on the stack registry"
9
10
 
10
11
  parameter "[QUERY]", "Query string"
11
12
 
13
+ option ['--[no-]pre'], :flag, "Include pre-release versions", default: true
14
+ option ['--[no-]private'], :flag, "Include private stacks", default: true, attribute_name: :priv
15
+
16
+ option ['--tag', '-t'], '[TAG]', "Search by tags", multivalued: true
17
+
12
18
  option ['-q', '--quiet'], :flag, "Output the identifying column only"
13
19
 
20
+ def fields
21
+ quiet? ? ['name'] : %w(name version pulls description)
22
+ end
23
+
14
24
  def execute
15
- results = stacks_client.search(query.to_s)
16
- if quiet?
17
- puts results.map { |s| s['stack'] }.join("\n")
18
- exit 0
19
- end
25
+ results = stacks_client.search(query.to_s, tags: tag_list, include_prerelease: pre?, include_private: priv?)
20
26
  exit_with_error 'Nothing found' if results.empty?
21
- titles = ['NAME', 'VERSION', 'DESCRIPTION']
22
- columns = "%-40s %-10s %-40s"
23
- puts columns % titles
24
- results.each do |stack|
25
- puts columns % [stack['stack'], stack['version'] || '?', stack['description'] || '-'] if stack
27
+ print_table(results.map { |r| r['attributes'] }) do |row|
28
+ next if quiet?
29
+ row['name'] = '%s/%s' % [row['organization-id'], row['name']]
30
+ row['name'] = pastel.yellow(row['name']) if row['is-private']
31
+ if row['latest-version'] && row['latest-version']['version']
32
+ row['version'] = row['latest-version']['version']
33
+ row['description'] = row['latest-version']['description']
34
+ else
35
+ row['version'] = '?'
36
+ end
37
+
38
+ row['description'] = '-' if row['description'].to_s.empty?
26
39
  end
27
40
  end
28
41
  end
@@ -13,19 +13,52 @@ module Kontena::Cli::Stacks::Registry
13
13
  requires_current_account_token
14
14
 
15
15
  def execute
16
- require 'semantic'
17
- unless versions?
18
- stack = ::YAML.safe_load(stacks_client.show(stack_name.stack_name, stack_name.version), [], [], true)
19
- puts "#{stack['stack']}:"
20
- puts " #{"latest_" unless stack_name.version}version: #{stack['version']}"
21
- puts " expose: #{stack['expose'] || '-'}"
22
- puts " description: #{stack['description'] || '-'}"
16
+ versions = stacks_client.versions(stack_name)
23
17
 
24
- puts " available_versions:"
25
- end
18
+ if versions?
19
+ stacks_client.versions(stack_name).each do |version|
20
+ puts version['attributes']['version']
21
+ end
22
+ else
23
+ data = stacks_client.show(stack_name).dig('data', 'attributes')
24
+ puts "#{data['organization-id']}/#{data['name']}:"
25
+ puts " description: #{data.dig('latest-version', 'description') || '-'}"
26
+ puts " latest_version: #{data.dig('latest-version', 'version') || '-'}"
27
+ puts " created_at: #{data.dig('created-at')}"
28
+ puts " pulls: #{data.dig('pulls')}"
29
+ puts " private: #{data.dig('is-private')}"
30
+ meta = data.dig('latest-version', 'meta')
31
+ if meta
32
+ puts " meta:"
33
+ readme = meta.delete('readme')
34
+ meta_lines = YAML.dump(meta).split(/[\r\n]/)
35
+ meta_lines.shift
36
+ meta_lines.each do |meta_line|
37
+ puts " %s" % meta_line
38
+ end
39
+ if readme
40
+ if readme =~ /^http\S+$/
41
+ puts " readme: readme"
42
+ else
43
+ puts " readme: |"
44
+ readme.gsub!(/(\S{#{70}})(?=\S)/, '\1 ')
45
+ readme.gsub!(/(.{1,#{70}})(?:\s+|$)/, "\\1\n")
46
+ readme.gsub!(/^/, ' ')
47
+ puts readme
48
+ end
49
+ end
50
+ else
51
+ puts " meta: -"
52
+ end
26
53
 
27
- stacks_client.versions(stack_name.stack_name).reject {|s| s['version'].nil? || s['version'].empty?}.map { |s| Semantic::Version.new(s['version'])}.sort.reverse_each do |version|
28
- puts versions? ? version : " - #{version}"
54
+ if versions.empty?
55
+ puts " versions: -"
56
+ else
57
+ puts " versions:"
58
+ versions.each do |version|
59
+ puts " - #{version['attributes']['version']} (#{version['attributes']['created-at']})"
60
+ end
61
+ end
29
62
  end
30
63
  end
31
64
  end
@@ -5,5 +5,8 @@ module Kontena::Cli::Stacks
5
5
  subcommand ["search"], "Search for stacks in the stacks registry", load_subcommand('stacks/registry/search_command')
6
6
  subcommand "show", "Show info about a stack in the stacks registry", load_subcommand('stacks/registry/show_command')
7
7
  subcommand ["remove", "rm"], "Remove a stack (or version) from the stacks registry", load_subcommand('stacks/registry/remove_command')
8
+ subcommand "create", "Create a stack in the registry", load_subcommand('stacks/registry/create_command')
9
+ subcommand "make-private", "Change Stack visibility to private", load_subcommand('stacks/registry/make_private_command')
10
+ subcommand "make-public", "Change Stack visibility to public", load_subcommand('stacks/registry/make_public_command')
8
11
  end
9
12
  end
@@ -26,6 +26,10 @@ module Kontena::Cli::Stacks
26
26
  @variables ||= stack['variables'] || {}
27
27
  end
28
28
 
29
+ def metadata
30
+ @metadata ||= stack['metadata'] || {}
31
+ end
32
+
29
33
  def stack
30
34
  @stack ||= client.get("stacks/#{current_grid}/#{name}")
31
35
  end
@@ -41,7 +45,7 @@ module Kontena::Cli::Stacks
41
45
  def write_variables
42
46
  File.write(values_to, variable_yaml)
43
47
  end
44
-
48
+
45
49
  def show_stack
46
50
  puts "#{stack['name']}:"
47
51
  puts " created: #{stack['created_at']}"
@@ -51,10 +55,19 @@ module Kontena::Cli::Stacks
51
55
  puts " version: #{stack['version']}"
52
56
  puts " revision: #{stack['revision']}"
53
57
  puts " expose: #{stack['expose'] || '-'}"
58
+
54
59
  puts " variables:#{' -' if variables.empty?}"
55
60
  variables.each do |var, val|
56
61
  puts " #{var}: #{val}"
57
62
  end
63
+
64
+ puts " metadata:#{' -' if metadata.empty?}"
65
+ unless metadata.empty?
66
+ output_lines = ::YAML.dump(metadata).split(/[\r\n]/)
67
+ output_lines.shift # get rid of "---"
68
+ puts output_lines.map { |line| ' %s' % line }.join("\n")
69
+ end
70
+
58
71
  puts " parent: #{stack['parent'] ? stack['parent']['name'] : '-'}"
59
72
  if stack['children'] && !stack['children'].empty?
60
73
  puts " children:"
@@ -62,6 +75,7 @@ module Kontena::Cli::Stacks
62
75
  puts " - #{child['name']}"
63
76
  end
64
77
  end
78
+
65
79
  puts " services:"
66
80
  stack['services'].each do |service|
67
81
  show_service(service['id'])
@@ -33,6 +33,15 @@ module Kontena::Cli::Stacks
33
33
  gather_master_data(stack_name)
34
34
  end
35
35
 
36
+ kontena_requirement = loader.yaml.dig('meta', 'required_kontena_version')
37
+ unless kontena_requirement.nil?
38
+ master_version = Gem::Version.new(client.server_version)
39
+ unless Gem::Requirement.new(kontena_requirement).satisfied_by?(master_version)
40
+ puts "#{pastel.red("Warning: ")} Stack requires kontena version #{kontena_requirement} but Master version is #{master_version}"
41
+ confirm("Are you sure? You can skip this prompt by running this command with --force option") unless force?
42
+ end
43
+ end
44
+
36
45
  new_data = spinner "Parsing #{pastel.cyan(source)}" do
37
46
  loader.flat_dependencies(
38
47
  stack_name,
@@ -213,7 +213,9 @@ module Kontena::Cli::Stacks
213
213
  result['dependencies'] = dependencies
214
214
  result['source'] = raw_content
215
215
  result['variables'] = variable_values(without_defaults: true, without_vault: true)
216
+ result['metadata'] = raw_yaml['meta'] || {}
216
217
  end
218
+
217
219
  if parent_name
218
220
  result['parent'] = { 'name' => parent_name }
219
221
  else
@@ -6,7 +6,7 @@ module Kontena::Cli::Stacks
6
6
  end
7
7
 
8
8
  def read_content
9
- Kontena::StacksCache.pull(source)
9
+ Kontena::StacksCache.pull(Kontena::Cli::Stacks::StackName.new(source))
10
10
  end
11
11
 
12
12
  def origin
@@ -542,6 +542,14 @@ module Kontena
542
542
 
543
543
  if data.is_a?(Hash) && data.has_key?('error') && data['error'].is_a?(Hash)
544
544
  raise Kontena::Errors::StandardErrorHash.new(response.status, response.reason_phrase, data['error'])
545
+ elsif data.is_a?(Hash) && data.has_key?('errors') && data['errors'].is_a?(Array) && data['errors'].all? { |e| e.is_a?(Hash) }
546
+ error_with_status = data['errors'].find { |error| error.key?('status') }
547
+ if error_with_status
548
+ status = error_with_status['status']
549
+ else
550
+ status = response.status
551
+ end
552
+ raise Kontena::Errors::StandardErrorHash.new(status, response.reason_phrase, data)
545
553
  elsif data.is_a?(Hash) && data.has_key?('error')
546
554
  raise Kontena::Errors::StandardError.new(response.status, data['error'] + request_path)
547
555
  elsif data.is_a?(String) && !data.empty?
@@ -292,11 +292,11 @@ begin
292
292
  logs monitor show registry inspect)
293
293
  if words[1]
294
294
  if words[1] == 'registry' || words[1] == 'reg'
295
- registry_sub_commands = %(push pull search show remove)
295
+ registry_sub_commands = %(push pull search show remove make-public make-private create)
296
296
  if words[2]
297
297
  if words[2] == 'push'
298
298
  completion.push helper.yml_files(words[3])
299
- elsif %w(pull search show remove rm).include?(words[2]) && words[4].nil?
299
+ elsif %w(pull search show remove rm make-public make-private).include?(words[2]) && words[4].nil?
300
300
  completion.push helper.registry_stacks(words[3].to_s)
301
301
  else
302
302
  completion.push registry_sub_commands
@@ -4,15 +4,10 @@ module Kontena
4
4
  class StacksCache
5
5
  class CachedStack
6
6
 
7
- attr_accessor :stack
8
- attr_accessor :version
7
+ attr_reader :stack_name
9
8
 
10
- def initialize(stack, version = nil)
11
- unless version
12
- stack, version = stack.split(':', 2)
13
- end
14
- @stack = stack
15
- @version = version
9
+ def initialize(stack_name)
10
+ @stack_name = stack_name
16
11
  end
17
12
 
18
13
  def read
@@ -24,7 +19,8 @@ module Kontena
24
19
  end
25
20
 
26
21
  def write(content)
27
- raise ArgumentError, "Stack name and version required" unless @stack && @version
22
+ puts "WHATHAT??? #{stack_name.inspect} #{stack_name.version} #{stack_name.stack_name}"
23
+ raise ArgumentError, "Stack name and version required" unless stack_name.stack_name && stack_name.version
28
24
  unless File.directory?(File.dirname(path))
29
25
  require 'fileutils'
30
26
  FileUtils.mkdir_p(File.dirname(path))
@@ -37,12 +33,12 @@ module Kontena
37
33
  end
38
34
 
39
35
  def cached?
40
- return false unless version
36
+ return false unless stack_name.version
41
37
  File.exist?(path)
42
38
  end
43
39
 
44
40
  def path
45
- path = File.expand_path(File.join(base_path, "#{stack}-#{version}.yml"))
41
+ path = File.expand_path(File.join(base_path, "#{stack_name.stack_name}-#{stack_name.version}.yml"))
46
42
  raise "Path traversal attempted" unless path.start_with?(base_path)
47
43
  path
48
44
  end
@@ -62,39 +58,40 @@ module Kontena
62
58
  end
63
59
 
64
60
  class << self
65
- def pull(stack, version = nil)
66
- cache(stack, version).read
61
+ def pull(stack_name)
62
+ cache(stack_name).read
67
63
  end
68
64
 
69
65
  def dputs(msg)
70
66
  Kontena.logger.debug { msg }
71
67
  end
72
68
 
73
- def cache(stack, version = nil)
74
- stack = CachedStack.new(stack, version)
69
+ def cache(stack_name)
70
+ stack = CachedStack.new(stack_name)
75
71
  if stack.cached?
76
72
  dputs "Reading from cache: #{stack.path}"
77
73
  else
78
- dputs "Retrieving #{stack.stack}:#{stack.version} from registry"
79
- content = client.pull(stack.stack, stack.version)
80
- yaml = ::YAML.safe_load(content, [], [], true, stack.stack)
81
- new_stack = CachedStack.new(yaml['stack'], yaml['version'])
74
+ dputs "Retrieving #{stack.stack_name} from registry"
75
+ content = client.pull(stack_name)
76
+ yaml = ::YAML.safe_load(content, [], [], true, stack.stack_name.to_s)
77
+ new_stack_name = Kontena::Cli::Stacks::StackName.new(yaml['stack'], yaml['version'])
78
+ puts new_stack_name.inspect
79
+ new_stack = CachedStack.new(new_stack_name)
82
80
  if new_stack.cached?
83
81
  dputs "Already cached"
84
82
  stack = new_stack
85
83
  else
86
- stack.stack = yaml['stack']
87
- stack.version = yaml['version']
88
84
  dputs "Writing #{stack.path}"
89
- stack.write(content)
90
- dputs "#{stack.stack}:#{stack.version} cached to #{stack.path}"
85
+ new_stack.write(content)
86
+ dputs "#{new_stack.stack_name} cached to #{new_stack.path}"
87
+ stack = new_stack
91
88
  end
92
89
  end
93
90
  stack
94
91
  end
95
92
 
96
- def registry_url(stack, version = nil)
97
- client.full_uri(stack, version)
93
+ def registry_url(stack_name)
94
+ client.full_uri(stack_name)
98
95
  end
99
96
 
100
97
  def client
@@ -3,9 +3,11 @@ require 'kontena/client'
3
3
  module Kontena
4
4
  class StacksClient < Client
5
5
 
6
- ACCEPT_JSON = { 'Accept' => 'application/json' }
7
- ACCEPT_YAML = { 'Accept' => 'application/yaml' }
8
- CT_YAML = { 'Content-Type' => 'application/yaml' }
6
+ ACCEPT_JSON = { 'Accept' => 'application/json' }
7
+ ACCEPT_YAML = { 'Accept' => 'application/yaml' }
8
+ ACCEPT_JSONAPI = { 'Accept' => 'application/vnd.api+json' }
9
+ CT_YAML = { 'Content-Type' => 'application/yaml' }
10
+ CT_JSONAPI = { 'Content-Type' => 'application/vnd.api+json' }
9
11
 
10
12
  def raise_unless_token
11
13
  unless token && token['access_token']
@@ -20,45 +22,156 @@ module Kontena
20
22
  end
21
23
  end
22
24
 
23
- def full_uri(stack_name, version = nil)
24
- URI.join(api_url, path_to(stack_name, version)).to_s
25
+ def full_uri(stack_name)
26
+ URI.join(api_url, path_to_version(stack_name)).to_s
25
27
  end
26
28
 
27
- def path_to(stack_name, version = nil)
28
- version ? "/stack/#{stack_name}/version/#{version}" : "/stack/#{stack_name}"
29
+ def path_to_version(stack_name)
30
+ path_to_stack(stack_name) + "/stack-versions/%s" % (stack_name.version || 'latest')
29
31
  end
30
32
 
31
- def push(stack_name, version, data)
33
+ def path_to_stack(stack_name)
34
+ "/v2/organizations/%s/stacks/%s" % [stack_name.user, stack_name.stack]
35
+ end
36
+
37
+ def push(stack_name, data)
32
38
  raise_unless_token
33
- post('/stack/', data, {}, CT_YAML, true)
39
+ post(
40
+ '/v2/stack-files',
41
+ {
42
+ 'data' => {
43
+ 'type' => 'stack-files',
44
+ 'attributes' => { 'content' => data }
45
+ }
46
+ },
47
+ {},
48
+ CT_JSONAPI,
49
+ true
50
+ )
34
51
  end
35
52
 
36
- def show(stack_name, stack_version = nil)
53
+ def show(stack_name, include_prerelease: true)
37
54
  raise_unless_read_token
38
- get("#{path_to(stack_name, stack_version)}", nil, ACCEPT_JSON, options[:read_requires_token])
55
+ result = get("#{path_to_stack(stack_name)}", { 'include' => 'latest-version', 'include-prerelease' => include_prerelease }, ACCEPT_JSONAPI)
56
+ if result['included']
57
+ latest = result['included'].find { |i| i['type'] == 'stack-versions' }
58
+ return result unless latest
59
+ result['data']['attributes']['latest-version'] = {}
60
+ result['data']['attributes']['latest-version']['version'] = latest['attributes']['version']
61
+ result['data']['attributes']['latest-version']['description'] = latest['attributes']['description']
62
+ result['data']['attributes']['latest-version']['meta'] = latest['meta']
63
+ end
64
+ result
39
65
  end
40
66
 
41
- def versions(stack_name)
67
+ def versions(stack_name, include_prerelease: true, include_deleted: false)
42
68
  raise_unless_read_token
43
- get("#{path_to(stack_name, nil)}/versions", nil, ACCEPT_JSON, options[:read_requires_token])['versions']
69
+ get("#{path_to_stack(stack_name)}/stack-versions", { 'include-prerelease' => include_prerelease, 'include-deleted' => include_deleted}, ACCEPT_JSONAPI).dig('data')
44
70
  end
45
71
 
46
- def pull(stack_name, version = nil)
72
+ def pull(stack_name)
47
73
  raise_unless_read_token
48
- get(path_to(stack_name, version), nil, ACCEPT_YAML, options[:read_requires_token])
74
+ get(path_to_version(stack_name) + '/yaml', nil, ACCEPT_YAML)
49
75
  rescue StandardError => ex
50
- ex.message << " : #{path_to(stack_name, version)}"
76
+ ex.message << " : #{path_to_version(stack_name)}"
51
77
  raise ex, ex.message
52
78
  end
53
79
 
54
- def search(query)
80
+ def search(query, tags: [], include_prerelease: true, include_private: true, include_versionless: true)
55
81
  raise_unless_read_token
56
- get('/search', { q: query }, ACCEPT_JSON, options[:read_requires_token])['stacks']
82
+ if tags.empty?
83
+ result = get('/v2/stacks', { 'query' => query, 'include' => 'latest-version', 'include-prerelease' => include_prerelease, 'include-private' => include_private, 'include-versionless' => include_versionless }, ACCEPT_JSONAPI)
84
+ else
85
+ result = get('/v2/tags/%s/stacks' % tags.join(','), { 'query' => query, 'include' => 'latest-version', 'include-prerelease' => include_prerelease, 'include-private' => include_private }, ACCEPT_JSONAPI)
86
+ end
87
+
88
+ data = result.dig('data')
89
+ included = result.dig('included')
90
+ if included
91
+ data.each do |row|
92
+ name = '%s/%s' % [row.fetch('attributes', {}).fetch('organization-id'), row.fetch('attributes', {}).fetch('name')]
93
+ next if name.nil?
94
+ included_version = included.find { |i| i.fetch('attributes', {}).fetch('name') == name }
95
+ if included_version
96
+ row['attributes']['latest-version'] = {}
97
+ row['attributes']['latest-version']['version'] = included_version['attributes']['version']
98
+ row['attributes']['latest-version']['description'] = included_version['attributes']['description']
99
+ end
100
+ end
101
+ end
102
+ data
103
+ end
104
+
105
+ def destroy(stack_name)
106
+ raise_unless_token
107
+ if stack_name.version
108
+ id = stack_version_id(stack_name)
109
+ if id.nil?
110
+ raise Kontena::Errors::StandardError.new(404, 'Not found')
111
+ end
112
+ delete('/v2/stack-versions/%s' % id, nil, {}, ACCEPT_JSONAPI)
113
+ else
114
+ id = stack_id(stack_name)
115
+ if id.nil?
116
+ raise Kontena::Errors::StandardError.new(404, 'Not found')
117
+ end
118
+ delete('/v2/stacks/%s' % id, nil, {}, ACCEPT_JSONAPI)
119
+ end
120
+ end
121
+
122
+ def make_private(stack_name)
123
+ change_visibility(stack_name, is_private: true)
57
124
  end
58
125
 
59
- def destroy(stack_name, version = nil)
126
+ def make_public(stack_name)
127
+ change_visibility(stack_name, is_private: false)
128
+ end
129
+
130
+ def create(stack_name, is_private: true)
131
+ post(
132
+ '/v2/stacks',
133
+ stack_data(stack_name, is_private: is_private),
134
+ {},
135
+ CT_JSONAPI.merge(ACCEPT_JSONAPI)
136
+ )
137
+ end
138
+
139
+ private
140
+
141
+ def stack_id(stack_name)
142
+ show(stack_name).dig('data', 'id')
143
+ end
144
+
145
+ def stack_version_id(stack_name)
146
+ version = versions(stack_name, include_prerelease: true).find { |v| v.dig('attributes', 'version') == stack_name.version }
147
+ if version
148
+ version['id']
149
+ else
150
+ nil
151
+ end
152
+ end
153
+
154
+ def change_visibility(stack_name, is_private: true)
60
155
  raise_unless_token
61
- delete(path_to(stack_name, version), {})
156
+ put(
157
+ '/v2/stacks/%s' % stack_id(stack_name),
158
+ stack_data(stack_name, is_private: is_private),
159
+ {},
160
+ CT_JSONAPI.merge(ACCEPT_JSONAPI)
161
+ )
162
+ end
163
+
164
+ def stack_data(stack_name, is_private: true)
165
+ {
166
+ data: {
167
+ type: 'stacks',
168
+ attributes: {
169
+ 'name' => stack_name.stack,
170
+ 'organization-id' => stack_name.user,
171
+ 'is-private' => is_private
172
+ }
173
+ }
174
+ }
62
175
  end
63
176
  end
64
177
  end
@@ -1,6 +1,6 @@
1
1
  #!/bin/sh
2
2
 
3
- export PATH=/opt/kontena/embedded/bin:$PATH
3
+ export PATH="/opt/kontena/embedded/bin:$PATH"
4
4
  export GEM_HOME=/opt/kontena/embedded/lib/ruby/gems/2.5.0
5
5
  export GEM_PATH=$GEM_HOME
6
6
 
@@ -0,0 +1,28 @@
1
+ stack: user/stackname
2
+ version: 0.1.1
3
+ services:
4
+ wordpress:
5
+ extends:
6
+ file: docker-compose_v2.yml
7
+ service: wordpress
8
+ stateful: true
9
+ environment:
10
+ WORDPRESS_DB_PASSWORD: ${STACK}_secret
11
+ instances: 2
12
+ deploy:
13
+ strategy: ha
14
+ mysql:
15
+ extends:
16
+ file: docker-compose_v2.yml
17
+ service: mysql
18
+ stateful: true
19
+ environment:
20
+ - MYSQL_ROOT_PASSWORD=${STACK}_secret
21
+ meta:
22
+ tags:
23
+ - tag1
24
+ - tag2
25
+ readme: |
26
+ Text goes
27
+ here
28
+ required_kontena_version: ">= 0.5.0"
@@ -10,6 +10,7 @@ describe Kontena::Cli::Stacks::InstallCommand do
10
10
 
11
11
  before(:each) do
12
12
  ENV['STACK'] = nil
13
+ allow(client).to receive(:server_version).and_return(Kontena::Cli::VERSION)
13
14
  end
14
15
 
15
16
  describe '#execute' do
@@ -25,7 +26,8 @@ describe Kontena::Cli::Stacks::InstallCommand do
25
26
  'dependencies' => nil,
26
27
  'source' => /stack:/,
27
28
  'parent' => nil,
28
- 'expose' => nil
29
+ 'expose' => nil,
30
+ 'metadata' => {}
29
31
  }
30
32
  end
31
33
 
@@ -85,6 +87,29 @@ describe Kontena::Cli::Stacks::InstallCommand do
85
87
  subject.run(['-n', 'deptest', '--no-deploy', '-v', 'dep_1.dep_1.dep_var=1', fixture_path('stack-with-dependencies.yml')])
86
88
  end
87
89
  end
90
+
91
+ context 'with a stack including metadata' do
92
+ let(:stack_meta_expectation) do
93
+ stack_expectation.merge(
94
+ 'metadata' => hash_including(
95
+ 'tags' => array_including('tag1', 'tag2'),
96
+ 'readme' => /Text goes/,
97
+ 'required_kontena_version' => ">= 0.5.0"
98
+ )
99
+ )
100
+ end
101
+
102
+ it 'includes the metadata in the json sent to master' do
103
+ expect(client).to receive(:post).with('grids/test-grid/stacks', hash_including(stack_meta_expectation))
104
+ subject.run(['--no-deploy', fixture_path('kontena_v3_with_metadata.yml')])
105
+ end
106
+
107
+ it 'requires force if master version does not match metadata required_kontena_version' do
108
+ expect(client).to receive(:server_version).and_return('0.2.0')
109
+ expect(subject).to receive(:confirm).and_call_original
110
+ expect{subject.run(['--no-deploy', fixture_path('kontena_v3_with_metadata.yml')])}.to exit_with_error.and output(/version/).to_stdout
111
+ end
112
+ end
88
113
  end
89
114
 
90
115
  context 'variable value input' do
@@ -11,6 +11,7 @@ describe Kontena::Cli::Stacks::UpgradeCommand do
11
11
 
12
12
  before(:each) do
13
13
  ENV['STACK'] = nil
14
+ allow(client).to receive(:server_version).and_return(Kontena::Cli::VERSION)
14
15
  end
15
16
 
16
17
  describe '#execute' do
@@ -69,6 +70,13 @@ describe Kontena::Cli::Stacks::UpgradeCommand do
69
70
  subject.run(['--force', 'stack-a', fixture_path('kontena_v3.yml')])
70
71
  end
71
72
 
73
+ it 'requires force if master version does not match metadata required_kontena_version' do
74
+ expect(client).to receive(:get).with('stacks/test-grid/stack-a').and_return(stack_response)
75
+ expect(client).to receive(:server_version).and_return('0.2.0')
76
+ expect(subject).to receive(:confirm).and_call_original
77
+ expect{subject.run(['--no-deploy', 'stack-a', fixture_path('kontena_v3_with_metadata.yml')])}.to exit_with_error.and output(/version/).to_stdout
78
+ end
79
+
72
80
  context '--no-deploy option' do
73
81
  it 'does not trigger deploy' do
74
82
  expect(client).to receive(:get).with('stacks/test-grid/stack-a').and_return(stack_response)
@@ -79,7 +87,6 @@ describe Kontena::Cli::Stacks::UpgradeCommand do
79
87
  subject.run(['--no-deploy', '--force', 'stack-a', fixture_path('kontena_v3.yml')])
80
88
  end
81
89
  end
82
-
83
90
  context 'with a stack including dependencies' do
84
91
 
85
92
  let(:expectation) {{ 'name' => 'deptest', 'stack' => 'user/depstack1' }}
@@ -61,8 +61,7 @@ describe Kontena::Cli::Stacks::YAML::Reader do
61
61
 
62
62
  it 'returns result hash' do
63
63
  result = subject.execute
64
- expect(result).to be_kind_of(Hash)
65
- %w(
64
+ top_level_fields = %w(
66
65
  stack
67
66
  version
68
67
  name
@@ -74,9 +73,9 @@ describe Kontena::Cli::Stacks::YAML::Reader do
74
73
  source
75
74
  variables
76
75
  parent
77
- ).each do |k|
78
- expect(result.key?(k)).to be_truthy
79
- end
76
+ metadata
77
+ )
78
+ expect(result).to match hash_including(*top_level_fields)
80
79
  end
81
80
 
82
81
  context 'when extending services' do
@@ -178,7 +177,9 @@ describe Kontena::Cli::Stacks::YAML::Reader do
178
177
  end
179
178
 
180
179
  it 'extends services from a registry stack' do
181
- expect(Kontena::StacksCache).to receive(:pull).at_least(:once).with('registrystack/compose:1.0.0').and_return(File.read(fixture_path('docker-compose_v2.yml')))
180
+ expect(Kontena::StacksCache).to receive(:pull).at_least(:once) do |stackname|
181
+ expect(stackname.to_s).to eq 'registrystack/compose:1.0.0'
182
+ end.and_return(File.read(fixture_path('docker-compose_v2.yml')))
182
183
  expect(subject.execute['services']).to match array_including(
183
184
  hash_including(
184
185
  "instances"=>2,
@@ -332,8 +333,7 @@ describe Kontena::Cli::Stacks::YAML::Reader do
332
333
 
333
334
  it 'discards empty lines' do
334
335
  result = env_file
335
- result << '
336
- '
336
+ result << " \n \n"
337
337
  allow(File).to receive(:readlines).with('.env').and_return(result)
338
338
  variables = subject.send(:read_env_file, '.env')
339
339
  expect(variables).to eq([
@@ -23,7 +23,7 @@ describe Kontena::Cli::Stacks::YAML::RegistryLoader do
23
23
  let(:subject) { described_class.new('user/stack') }
24
24
 
25
25
  before do
26
- allow(Kontena::StacksCache).to receive(:pull).with('user/stack').and_return(
26
+ allow(Kontena::StacksCache).to receive(:pull).with(duck_type(:stack_name)).and_return(
27
27
  fixture('kontena_v3.yml')
28
28
  )
29
29
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kontena-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.0.pre5
4
+ version: 1.5.0.rc1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kontena, Inc
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-02-20 00:00:00.000000000 Z
11
+ date: 2018-02-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -407,6 +407,9 @@ files:
407
407
  - lib/kontena/cli/stacks/list_command.rb
408
408
  - lib/kontena/cli/stacks/logs_command.rb
409
409
  - lib/kontena/cli/stacks/monitor_command.rb
410
+ - lib/kontena/cli/stacks/registry/create_command.rb
411
+ - lib/kontena/cli/stacks/registry/make_private_command.rb
412
+ - lib/kontena/cli/stacks/registry/make_public_command.rb
410
413
  - lib/kontena/cli/stacks/registry/pull_command.rb
411
414
  - lib/kontena/cli/stacks/registry/push_command.rb
412
415
  - lib/kontena/cli/stacks/registry/remove_command.rb
@@ -535,6 +538,7 @@ files:
535
538
  - spec/fixtures/kontena_build_v3.yml
536
539
  - spec/fixtures/kontena_v3.yml
537
540
  - spec/fixtures/kontena_v3_with_compose_variables.yml
541
+ - spec/fixtures/kontena_v3_with_metadata.yml
538
542
  - spec/fixtures/kontena_v3_with_registry_extends.yml
539
543
  - spec/fixtures/stack-internal-extend.yml
540
544
  - spec/fixtures/stack-invalid.yml
@@ -718,6 +722,7 @@ test_files:
718
722
  - spec/fixtures/kontena_build_v3.yml
719
723
  - spec/fixtures/kontena_v3.yml
720
724
  - spec/fixtures/kontena_v3_with_compose_variables.yml
725
+ - spec/fixtures/kontena_v3_with_metadata.yml
721
726
  - spec/fixtures/kontena_v3_with_registry_extends.yml
722
727
  - spec/fixtures/stack-internal-extend.yml
723
728
  - spec/fixtures/stack-invalid.yml