kontena-cli 1.5.0.pre5 → 1.5.0.rc1

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 (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