kontena-cli 1.4.0.pre6 → 1.4.0.pre7

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 (181) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +1 -1
  3. data/VERSION +1 -1
  4. data/bin/kontena +1 -1
  5. data/kontena-cli.gemspec +3 -3
  6. data/lib/kontena/cli/certificate/authorize_command.rb +67 -6
  7. data/lib/kontena/cli/certificate/get_command.rb +7 -0
  8. data/lib/kontena/cli/certificate/list_command.rb +75 -0
  9. data/lib/kontena/cli/certificate/register_command.rb +13 -2
  10. data/lib/kontena/cli/certificate/request_command.rb +20 -0
  11. data/lib/kontena/cli/certificate/show_command.rb +19 -0
  12. data/lib/kontena/cli/certificate_command.rb +4 -1
  13. data/lib/kontena/cli/cloud/master/add_command.rb +1 -1
  14. data/lib/kontena/cli/common.rb +21 -33
  15. data/lib/kontena/cli/etcd/health_command.rb +21 -27
  16. data/lib/kontena/cli/helpers/exec_helper.rb +15 -6
  17. data/lib/kontena/cli/helpers/health_helper.rb +12 -0
  18. data/lib/kontena/cli/helpers/log_helper.rb +2 -2
  19. data/lib/kontena/cli/helpers/time_helper.rb +29 -0
  20. data/lib/kontena/cli/master/init_cloud_command.rb +19 -0
  21. data/lib/kontena/cli/master/list_command.rb +1 -1
  22. data/lib/kontena/cli/master/ssh_command.rb +3 -1
  23. data/lib/kontena/cli/master/use_command.rb +1 -2
  24. data/lib/kontena/cli/node_command.rb +1 -0
  25. data/lib/kontena/cli/nodes/health_command.rb +28 -13
  26. data/lib/kontena/cli/nodes/list_command.rb +19 -3
  27. data/lib/kontena/cli/nodes/show_command.rb +4 -2
  28. data/lib/kontena/cli/nodes/ssh_command.rb +5 -2
  29. data/lib/kontena/cli/nodes/update_command.rb +2 -0
  30. data/lib/kontena/cli/plugins/install_command.rb +11 -8
  31. data/lib/kontena/cli/plugins/list_command.rb +5 -3
  32. data/lib/kontena/cli/plugins/search_command.rb +4 -2
  33. data/lib/kontena/cli/plugins/show_command.rb +17 -0
  34. data/lib/kontena/cli/plugins/uninstall_command.rb +9 -13
  35. data/lib/kontena/cli/registry/create_command.rb +1 -1
  36. data/lib/kontena/cli/services/create_command.rb +6 -0
  37. data/lib/kontena/cli/services/services_helper.rb +33 -6
  38. data/lib/kontena/cli/services/update_command.rb +6 -0
  39. data/lib/kontena/cli/stacks/build_command.rb +3 -3
  40. data/lib/kontena/cli/stacks/common.rb +105 -90
  41. data/lib/kontena/cli/stacks/deploy_command.rb +7 -3
  42. data/lib/kontena/cli/stacks/install_command.rb +39 -6
  43. data/lib/kontena/cli/stacks/list_command.rb +36 -4
  44. data/lib/kontena/cli/stacks/logs_command.rb +9 -2
  45. data/lib/kontena/cli/stacks/registry/pull_command.rb +2 -2
  46. data/lib/kontena/cli/stacks/registry/push_command.rb +20 -9
  47. data/lib/kontena/cli/stacks/registry/remove_command.rb +4 -4
  48. data/lib/kontena/cli/stacks/registry/show_command.rb +4 -4
  49. data/lib/kontena/cli/stacks/remove_command.rb +27 -1
  50. data/lib/kontena/cli/stacks/service_generator.rb +12 -2
  51. data/lib/kontena/cli/stacks/show_command.rb +35 -5
  52. data/lib/kontena/cli/stacks/stack_name.rb +71 -0
  53. data/lib/kontena/cli/stacks/upgrade_command.rb +127 -14
  54. data/lib/kontena/cli/stacks/validate_command.rb +38 -10
  55. data/lib/kontena/cli/stacks/yaml/custom_validators/certificates_validator.rb +22 -0
  56. data/lib/kontena/cli/stacks/yaml/opto/prompt_resolver.rb +1 -2
  57. data/lib/kontena/cli/stacks/yaml/reader.rb +211 -185
  58. data/lib/kontena/cli/stacks/yaml/service_extender.rb +6 -12
  59. data/lib/kontena/cli/stacks/yaml/stack_file_loader.rb +97 -0
  60. data/lib/kontena/cli/stacks/yaml/stack_file_loader/file_loader.rb +41 -0
  61. data/lib/kontena/cli/stacks/yaml/stack_file_loader/registry_loader.rb +24 -0
  62. data/lib/kontena/cli/stacks/yaml/stack_file_loader/uri_loader.rb +23 -0
  63. data/lib/kontena/cli/stacks/yaml/validations.rb +16 -0
  64. data/lib/kontena/cli/stacks/yaml/validator_v3.rb +25 -8
  65. data/lib/kontena/client.rb +2 -2
  66. data/lib/kontena/command.rb +11 -0
  67. data/lib/kontena/main_command.rb +3 -1
  68. data/lib/kontena/plugin_manager.rb +11 -198
  69. data/lib/kontena/plugin_manager/cleaner.rb +33 -0
  70. data/lib/kontena/plugin_manager/common.rb +86 -0
  71. data/lib/kontena/plugin_manager/installer.rb +54 -0
  72. data/lib/kontena/plugin_manager/loader.rb +93 -0
  73. data/lib/kontena/plugin_manager/rubygems_client.rb +42 -23
  74. data/lib/kontena/plugin_manager/uninstaller.rb +34 -0
  75. data/lib/kontena/util.rb +24 -0
  76. data/lib/kontena_cli.rb +1 -0
  77. data/omnibus/config/projects/kontena.rb +7 -1
  78. data/omnibus/config/software/{kontena.rb → kontena-cli.rb} +2 -0
  79. data/spec/fixtures/api/node.json +2 -1
  80. data/spec/fixtures/stack-internal-extend.yml +6 -1
  81. data/spec/fixtures/stack-with-dependencies-dep-1-1.yml +8 -0
  82. data/spec/fixtures/stack-with-dependencies-dep-1.yml +17 -0
  83. data/spec/fixtures/stack-with-dependencies-dep-2.yml +8 -0
  84. data/spec/fixtures/stack-with-dependencies-dep-3.yml +5 -0
  85. data/spec/fixtures/stack-with-dependencies-dep_2-removed.yml +17 -0
  86. data/spec/fixtures/stack-with-dependencies-dep_3-added.yml +25 -0
  87. data/spec/fixtures/stack-with-dependencies.yml +22 -0
  88. data/spec/fixtures/stack-with-variables.yml +3 -0
  89. data/spec/kontena/cli/etcd/health_command_spec.rb +45 -33
  90. data/spec/kontena/cli/helpers/exec_helper_spec.rb +2 -1
  91. data/spec/kontena/cli/master/init_cloud_command_spec.rb +14 -0
  92. data/spec/kontena/cli/nodes/health_command_spec.rb +74 -10
  93. data/spec/kontena/cli/nodes/list_command_spec.rb +381 -232
  94. data/spec/kontena/cli/nodes/show_command_spec.rb +31 -0
  95. data/spec/kontena/cli/nodes/ssh_command_spec.rb +18 -3
  96. data/spec/kontena/cli/plugins/install_command_spec.rb +1 -1
  97. data/spec/kontena/cli/stacks/build_command_spec.rb +6 -12
  98. data/spec/kontena/cli/stacks/common_spec.rb +42 -69
  99. data/spec/kontena/cli/stacks/install_command_spec.rb +57 -31
  100. data/spec/kontena/cli/stacks/list_command_spec.rb +44 -0
  101. data/spec/kontena/cli/stacks/logs_command_spec.rb +12 -1
  102. data/spec/kontena/cli/stacks/remove_command_spec.rb +39 -0
  103. data/spec/kontena/cli/stacks/show_command_spec.rb +16 -0
  104. data/spec/kontena/cli/stacks/stack_name_spec.rb +21 -0
  105. data/spec/kontena/cli/stacks/upgrade_command_spec.rb +73 -56
  106. data/spec/kontena/cli/stacks/validate_command_spec.rb +81 -0
  107. data/spec/kontena/cli/stacks/yaml/custom_validators/affinities_validator_spec.rb +22 -0
  108. data/spec/kontena/cli/stacks/yaml/reader_spec.rb +173 -169
  109. data/spec/kontena/cli/stacks/yaml/service_extender_spec.rb +12 -3
  110. data/spec/kontena/cli/stacks/yaml/stack_file_loader/file_loader_spec.rb +47 -0
  111. data/spec/kontena/cli/stacks/yaml/stack_file_loader/registry_loader_spec.rb +53 -0
  112. data/spec/kontena/cli/stacks/yaml/stack_file_loader/uri_loader_spec.rb +53 -0
  113. data/spec/kontena/cli/stacks/yaml/stack_file_loader_spec.rb +104 -0
  114. data/spec/kontena/cli/stacks/yaml/validator_v3_spec.rb +19 -0
  115. data/spec/kontena/plugin_manager/cleaner_spec.rb +20 -0
  116. data/spec/kontena/plugin_manager/common_spec.rb +39 -0
  117. data/spec/kontena/plugin_manager/installer_spec.rb +50 -0
  118. data/spec/kontena/plugin_manager/loader_spec.rb +5 -0
  119. data/spec/kontena/plugin_manager/rubygems_client_spec.rb +11 -25
  120. data/spec/kontena/plugin_manager/uninstaller_spec.rb +19 -0
  121. data/spec/kontena/plugin_manager_spec.rb +7 -7
  122. metadata +64 -97
  123. data/lib/kontena/cli/app_command.rb +0 -22
  124. data/lib/kontena/cli/apps/build_command.rb +0 -28
  125. data/lib/kontena/cli/apps/common.rb +0 -172
  126. data/lib/kontena/cli/apps/config_command.rb +0 -25
  127. data/lib/kontena/cli/apps/deploy_command.rb +0 -137
  128. data/lib/kontena/cli/apps/docker_compose_generator.rb +0 -61
  129. data/lib/kontena/cli/apps/docker_helper.rb +0 -80
  130. data/lib/kontena/cli/apps/dockerfile_generator.rb +0 -16
  131. data/lib/kontena/cli/apps/init_command.rb +0 -89
  132. data/lib/kontena/cli/apps/kontena_yml_generator.rb +0 -105
  133. data/lib/kontena/cli/apps/list_command.rb +0 -59
  134. data/lib/kontena/cli/apps/logs_command.rb +0 -37
  135. data/lib/kontena/cli/apps/monitor_command.rb +0 -93
  136. data/lib/kontena/cli/apps/remove_command.rb +0 -74
  137. data/lib/kontena/cli/apps/restart_command.rb +0 -39
  138. data/lib/kontena/cli/apps/scale_command.rb +0 -33
  139. data/lib/kontena/cli/apps/service_generator.rb +0 -114
  140. data/lib/kontena/cli/apps/service_generator_v2.rb +0 -27
  141. data/lib/kontena/cli/apps/show_command.rb +0 -23
  142. data/lib/kontena/cli/apps/start_command.rb +0 -40
  143. data/lib/kontena/cli/apps/stop_command.rb +0 -40
  144. data/lib/kontena/cli/apps/yaml/custom_validators/affinities_validator.rb +0 -19
  145. data/lib/kontena/cli/apps/yaml/custom_validators/build_validator.rb +0 -22
  146. data/lib/kontena/cli/apps/yaml/custom_validators/extends_validator.rb +0 -20
  147. data/lib/kontena/cli/apps/yaml/custom_validators/hooks_validator.rb +0 -54
  148. data/lib/kontena/cli/apps/yaml/custom_validators/secrets_validator.rb +0 -22
  149. data/lib/kontena/cli/apps/yaml/reader.rb +0 -213
  150. data/lib/kontena/cli/apps/yaml/service_extender.rb +0 -77
  151. data/lib/kontena/cli/apps/yaml/validations.rb +0 -71
  152. data/lib/kontena/cli/apps/yaml/validator.rb +0 -38
  153. data/lib/kontena/cli/apps/yaml/validator_v2.rb +0 -53
  154. data/spec/fixtures/app.json +0 -42
  155. data/spec/fixtures/health.yml +0 -26
  156. data/spec/fixtures/kontena-build.yml +0 -16
  157. data/spec/fixtures/kontena-internal-extend.yml +0 -8
  158. data/spec/fixtures/kontena-invalid.yml +0 -4
  159. data/spec/fixtures/kontena-with-env-file.yml +0 -18
  160. data/spec/fixtures/kontena-with-variables.yml +0 -19
  161. data/spec/fixtures/kontena.yml +0 -17
  162. data/spec/fixtures/kontena_build_v2.yml +0 -26
  163. data/spec/fixtures/kontena_numeric_version.yml +0 -9
  164. data/spec/fixtures/kontena_v2.yml +0 -35
  165. data/spec/fixtures/mysql.yml +0 -3
  166. data/spec/fixtures/wordpress-scaled.yml +0 -3
  167. data/spec/fixtures/wordpress.yml +0 -2
  168. data/spec/kontena/cli/app/build_command_spec.rb +0 -55
  169. data/spec/kontena/cli/app/common_spec.rb +0 -110
  170. data/spec/kontena/cli/app/config_command_spec.rb +0 -78
  171. data/spec/kontena/cli/app/deploy_command_spec.rb +0 -217
  172. data/spec/kontena/cli/app/docker_helper_spec.rb +0 -155
  173. data/spec/kontena/cli/app/init_command_spec.rb +0 -109
  174. data/spec/kontena/cli/app/logs_command_spec.rb +0 -131
  175. data/spec/kontena/cli/app/scale_spec.rb +0 -51
  176. data/spec/kontena/cli/app/service_generator_spec.rb +0 -384
  177. data/spec/kontena/cli/app/service_generator_v2_spec.rb +0 -73
  178. data/spec/kontena/cli/app/yaml/reader_spec.rb +0 -457
  179. data/spec/kontena/cli/app/yaml/service_extender_spec.rb +0 -127
  180. data/spec/kontena/cli/app/yaml/validator_spec.rb +0 -380
  181. data/spec/kontena/cli/app/yaml/validator_v2_spec.rb +0 -301
@@ -18,38 +18,151 @@ module Kontena::Cli::Stacks
18
18
  option '--[no-]deploy', :flag, 'Trigger deploy after upgrade', default: true
19
19
 
20
20
  option '--force', :flag, 'Force upgrade'
21
+ option '--skip-dependencies', :flag, "Do not install any stack dependencies"
22
+ option '--dry-run', :flag, "Do not perform any uploading", hidden: true
21
23
 
22
24
  requires_current_master
23
25
  requires_current_master_token
24
26
 
27
+ def normalize_local_data(stack_data, parent_name)
28
+ return nil if stack_data.nil? || stack_data.empty?
29
+
30
+ depends = stack_data.delete('depends') || []
31
+ normalized_data = {
32
+ parent_name => stack_data.merge(
33
+ :loader => loader_class.for(stack_data['stack'])
34
+ )
35
+ }
36
+
37
+ depends.each do |stack|
38
+ key = "#{parent_name}-#{stack['name']}"
39
+ normalized_data.merge!(normalize_local_data(stack.merge('parent_name' => parent_name), key))
40
+ end
41
+ normalized_data
42
+ end
43
+
44
+ def normalize_master_data(stack_name, raise_not_found = false)
45
+ begin
46
+ data = fetch_master_data(stack_name)
47
+ rescue Kontena::Errors::StandardError => ex
48
+ return nil if ex.status == 404 && !raise_not_found
49
+ raise ex
50
+ end
51
+ depends = data.delete('children') || []
52
+
53
+ normalized_data = { stack_name => data }
54
+
55
+ return normalized_data if skip_dependencies?
56
+
57
+ depends.each do |stack|
58
+ normalized_data.merge!(normalize_master_data(stack['name']))
59
+ end
60
+ normalized_data
61
+ end
62
+
63
+ def merge_data(local_data, remote_data)
64
+ merged = {}
65
+ unless local_data.nil? || local_data.empty?
66
+ local_data.each do |key, data|
67
+ merged[key] ||= {}
68
+ merged[key][:local] = data
69
+ end
70
+ end
71
+ unless remote_data.nil? || remote_data.empty?
72
+ remote_data.each do |key, data|
73
+ merged[key] ||= {}
74
+ merged[key][:remote] = data
75
+ end
76
+ end
77
+ merged
78
+ end
79
+
25
80
  def execute
26
- master_data = spinner "Reading stack #{pastel.cyan(name)} metadata from Kontena Master" do |spin|
27
- read_stack || spin.fail!
81
+ set_env_variables(stack_name, current_grid)
82
+
83
+ local = spinner "Parsing #{pastel.cyan(source)}" do
84
+ normalize_local_data({'stack' => source, 'depends' => skip_dependencies? ? nil : loader.dependencies}, stack_name)
28
85
  end
29
86
 
30
- stack = stack_read_and_dump(filename, name: name, values: values, defaults: master_data['variables'])
87
+ remote = spinner "Reading stack #{pastel.cyan(stack_name)} from master" do
88
+ normalize_master_data(stack_name, true)
89
+ end
31
90
 
32
- unless force? || master_data['stack'] == stack['stack']
33
- confirm "Replacing stack #{Kontena.pastel.cyan(master_data['stack'])} on master with #{Kontena.pastel.cyan(stack['stack'])} from #{Kontena.pastel.yellow(filename)}. Are you sure?"
91
+ merged = merge_data(local, remote)
92
+
93
+ removes = merged.keys.select { |k| merged[k][:local].nil? }
94
+
95
+ unless removes.empty?
96
+ puts
97
+ puts "Stacks to be removed because they are no longer depended on:"
98
+ removes.each do |r|
99
+ puts pastel.yellow("- #{r}")
100
+ end
101
+ puts
102
+ unless force?
103
+ puts "#{pastel.red('Warning:')} This can not be undone, data will be lost."
104
+ end
105
+ confirm unless force?
106
+ removes.reverse_each do |removed_stack|
107
+ Kontena.run!('stack', 'remove', '--force', '--keep-dependencies', removed_stack)
108
+ merged.delete(removed_stack)
109
+ end
34
110
  end
35
111
 
36
- spinner "Upgrading stack #{pastel.cyan(name)}" do |spin|
37
- update_stack(stack) || spin.fail!
112
+ unless force?
113
+ merged.each do |stackname, data|
114
+ next if data[:remote].nil?
115
+ unless data[:local][:loader].stack_name.stack_name == data[:remote]['stack']
116
+ confirm "Replacing stack #{pastel.cyan(data[:remote]['stack'])} on master with #{pastel.cyan(data[:local][:loader].stack_name.stack_name)}. Are you sure?"
117
+ end
118
+ end
38
119
  end
39
120
 
40
- Kontena.run!(['stack', 'deploy', name]) if deploy?
121
+ merged.reverse_each do |stackname, data|
122
+ set_env_variables(stackname, current_grid)
123
+ data[:local][:stack] = data[:local][:loader].reader.execute(
124
+ name: stackname,
125
+ values: (data.dig(:local, 'variables') || {}).merge(dependency_values_from_options(stackname)),
126
+ defaults: data.dig(:remote, 'variables'),
127
+ parent_name: data.dig(:local, 'parent_name')
128
+ )
129
+ hint_on_validation_notifications(data[:local][:loader].reader.notifications, data[:local][:loader].source)
130
+ abort_on_validation_errors(data[:local][:loader].reader.errors, data[:local][:loader].source)
131
+ end
132
+
133
+ merged.reverse_each do |stackname, data|
134
+ stack = data[:local][:stack]
135
+ if data[:remote]
136
+ spinner "Upgrading #{stack_name == stackname ? 'stack' : 'dependency'} #{pastel.cyan(stackname)}" do |spin|
137
+ update_stack(stackname, stack) || spin.fail!
138
+ end
139
+ else
140
+ cmd = ['stack', 'install', '--name', stackname]
141
+ cmd.concat ['--parent-name', stack['parent_name']] if stack['parent_name']
142
+ stack['variables'].merge(dependency_values_from_options(stackname)).each do |k, v|
143
+ cmd.concat ['-v', "#{k}=#{v}"]
144
+ end
145
+ cmd << '--no-deploy'
146
+ cmd << data[:local][:loader].source
147
+ caret "Installing new dependency #{cmd.last} as #{stackname}"
148
+ Kontena.run!(cmd)
149
+ end
150
+
151
+ Kontena.run!(['stack', 'deploy', stackname]) if deploy?
152
+ end
41
153
  end
42
154
 
43
- def update_stack(stack)
44
- client.put(stack_url, stack)
155
+ def update_stack(name, data)
156
+ return true if dry_run?
157
+ client.put(stack_url(name), data)
45
158
  end
46
159
 
47
- def stack_url
48
- @stack_url ||= "stacks/#{current_grid}/#{name}"
160
+ def stack_url(name)
161
+ "stacks/#{current_grid}/#{name}"
49
162
  end
50
163
 
51
- def read_stack
52
- client.get(stack_url)
164
+ def fetch_master_data(stack_name)
165
+ client.get(stack_url(stack_name))
53
166
  end
54
167
  end
55
168
  end
@@ -16,26 +16,54 @@ module Kontena::Cli::Stacks
16
16
  include Common::StackValuesFromOption
17
17
 
18
18
  option '--online', :flag, "Enable connections to current master", default: false
19
+ option '--dependency-tree', :flag, "Show dependency tree"
20
+ option '--[no-]dependencies', :flag, "Validate dependencies", default: true
21
+ option '--parent-name', '[PARENT_NAME]', "Set parent name", hidden: true
22
+
23
+ def validate_dependencies
24
+ dependencies = loader.dependencies
25
+ return if dependencies.nil?
26
+ dependencies.each do |dependency|
27
+ target_name = "#{stack_name}-#{dependency['name']}"
28
+ cmd = ['stack', 'validate']
29
+ cmd << '--online' if online?
30
+ cmd.concat ['--parent-name', stack_name]
31
+
32
+ dependency['variables'].merge(dependency_values_from_options(dependency['name'])).each do |key, value|
33
+ next if key == 'PARENT_STACK'
34
+ cmd.concat ['-v', "#{key}=#{value}"]
35
+ end
36
+ cmd << dependency['stack']
37
+ Kontena.run(cmd)
38
+ end
39
+ end
19
40
 
20
41
  def execute
21
42
  unless online?
22
43
  config.current_master = nil
23
- values ||= {}
24
- values.merge!('GRID' => 'validate')
44
+ set_env_variables(stack_name, 'validate', 'validate-platform')
25
45
  end
26
46
 
27
- reader = reader_from_yaml(filename, name: name, values: values)
28
- outcome = reader.execute
29
- hint_on_validation_notifications(outcome[:notifications]) unless outcome[:notifications].empty?
30
- abort_on_validation_errors(outcome[:errors]) unless outcome[:errors].empty?
47
+ if dependency_tree?
48
+ puts ::YAML.dump('name' => stack_name, 'stack' => source, 'depends' => stack['dependencies'])
49
+ exit 0
50
+ end
51
+
52
+ validate_dependencies if dependencies?
31
53
 
32
- dump_variables(reader) if values_to
54
+ hint_on_validation_notifications(reader.notifications, dependencies? ? loader.source : nil)
55
+ abort_on_validation_errors(reader.errors, dependencies? ? loader.source : nil)
56
+
57
+ dump_variables if values_to
33
58
 
34
59
  result = reader.fully_interpolated_yaml.merge(
35
- # simplest way to stringify keys in a hash
36
- 'variables' => JSON.parse(reader.variables.to_h(with_values: true, with_errors: true).to_json)
60
+ 'variables' => Kontena::Util.stringify_keys(reader.variables.to_h(with_values: true, with_errors: true))
37
61
  )
38
- puts ::YAML.dump(result)
62
+ if dependencies?
63
+ puts ::YAML.dump(result).sub(/\A---$/, "---\n# #{loader.source}")
64
+ else
65
+ puts ::YAML.dump(result)
66
+ end
39
67
  end
40
68
  end
41
69
  end
@@ -0,0 +1,22 @@
1
+ module Kontena::Cli::Stacks::YAML::Validations::CustomValidators
2
+ class CertificatesValidator < HashValidator::Validator::Base
3
+ def initialize
4
+ super('stacks_valid_certificates')
5
+ end
6
+
7
+ def validate(key, value, validations, errors)
8
+ unless value.is_a?(Array)
9
+ errors[key] = 'certificates must be array'
10
+ return
11
+ end
12
+ certificate_item_validation = {
13
+ 'subject' => 'string',
14
+ 'name' => 'string',
15
+ 'type' => 'string'
16
+ }
17
+ value.each do |certificate|
18
+ HashValidator.validator_for(certificate_item_validation).validate(key, certificate, certificate_item_validation, errors)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -1,5 +1,4 @@
1
- require 'kontena/cli/stacks/yaml/opto'
2
- require 'kontena/cli/common'
1
+ require_relative '../opto'
3
2
 
4
3
  module Kontena::Cli::Stacks
5
4
  module YAML
@@ -1,5 +1,10 @@
1
- require 'kontena/cli/common'
2
- require 'kontena/util'
1
+ require_relative 'stack_file_loader'
2
+ require_relative 'service_extender'
3
+ require_relative '../service_generator_v2'
4
+ require_relative 'validator_v3'
5
+ require 'opto'
6
+ require 'liquid'
7
+ require_relative 'opto'
3
8
 
4
9
  module Kontena::Cli::Stacks
5
10
  module YAML
@@ -8,156 +13,221 @@ module Kontena::Cli::Stacks
8
13
  module Setters; end
9
14
  end
10
15
 
11
- # Workaround for nil-valued variables in Liquid templates:
12
- # https://github.com/Shopify/liquid/issues/749
13
- # This is something that we can pass in to `Liquid::Template.render` that gets evaluated as nil.
14
- # If we pass in a nil value directly, then Liquid ignores it and considers the variable to be undefined.
15
16
  class LiquidNull
17
+ # Workaround for nil-valued variables in Liquid templates:
18
+ # https://github.com/Shopify/liquid/issues/749
19
+ # This is something that we can pass in to `Liquid::Template.render` that gets evaluated as nil.
20
+ # If we pass in a nil value directly, then Liquid ignores it and considers the variable to be undefined.
16
21
  def to_liquid
17
22
  nil
18
23
  end
19
24
  end
20
25
 
21
26
  class Reader
27
+ # The kontena Stack YAML reader
28
+
22
29
  include Kontena::Util
23
30
  include Kontena::Cli::Common
24
31
 
25
- attr_reader :file, :raw_content, :errors, :notifications, :defaults, :values, :registry
26
-
27
- def initialize(file, skip_validation: false, skip_variables: false, variables: nil, values: nil, defaults: nil)
28
- require_relative 'service_extender'
29
- require_relative 'validator_v3'
30
- require_relative 'opto'
31
- require 'liquid'
32
-
33
- @file = file
32
+ attr_reader :file, :loader, :errors, :notifications
34
33
 
35
- if from_registry?
36
- require 'shellwords'
37
- @raw_content = Kontena::StacksCache.pull(file)
38
- @registry = current_account.stacks_url
39
- elsif from_url?
40
- @raw_content = load_from_url(file)
41
- @registry = 'file://'
34
+ # @param stack_origin [String] a filename, pointer to registry or an URL
35
+ # @return [Reader]
36
+ def initialize(file)
37
+ if file.kind_of?(StackFileLoader)
38
+ @file = file.source
39
+ @loader = file
42
40
  else
43
- @raw_content = File.read(File.expand_path(file))
44
- @registry = 'file://'
41
+ @file = file
42
+ @loader = StackFileLoader.for(file)
45
43
  end
46
44
 
47
45
  @errors = []
48
46
  @notifications = []
49
- @skip_validation = skip_validation
50
- @skip_variables = skip_variables
51
- @variables = variables
52
- @values = values
53
- @defaults = defaults
54
47
  end
55
48
 
56
- def load_from_url(url)
57
- require 'open-uri'
58
- stream = open(url)
59
- stream.read
49
+ # @param without_defaults [TrueClass,FalseClass] strip the GRID, STACK, etc from response
50
+ # @param without_vault [TrueClass,FalseClass] strip out any values that are going to or coming from VAULT
51
+ # @return [Hash] a hash of key value pairs representing the values of stack variables
52
+ def variable_values(without_defaults: false, without_vault: false)
53
+ result = variables.to_h(values_only: true)
54
+ if without_defaults
55
+ result.delete_if { |k, _| default_envs.key?(k.to_s) || k.to_s == 'PARENT_STACK' }
56
+ end
57
+ if without_vault
58
+ result.delete_if { |k, _| variables.option(k).from.include?('vault') || variables.option(k).to.include?('vault') }
59
+ end
60
+ result
61
+ end
62
+
63
+ # Values that are set always when parsing stacks
64
+ # @return [Hash] a hash of key value pairs
65
+ def default_envs
66
+ @default_envs ||= {
67
+ 'GRID' => env['GRID'],
68
+ 'STACK' => env['STACK'],
69
+ 'PLATFORM' => env['PLATFORM'] || env['GRID']
70
+ }
60
71
  end
61
72
 
73
+ # Only uses the values from #default_envs to provide a hash from minimally interpolated
74
+ # YAML file. Useful for accessing some parts of the YAML without asking any questions.
75
+ #
76
+ # @return [Hash] minimally interpolated YAMl from the stack file.
62
77
  def internals_interpolated_yaml
63
78
  @internals_interpolated_yaml ||= ::YAML.safe_load(
64
79
  replace_dollar_dollars(
65
80
  interpolate(
66
81
  raw_content,
67
82
  use_opto: false,
68
- substitutions: {
69
- 'GRID' => env['GRID'],
70
- 'STACK' => env['STACK']
71
- },
83
+ substitutions: default_envs,
72
84
  warnings: false
73
85
  )
74
86
  )
75
- ) || {}
87
+ )
76
88
  rescue Psych::SyntaxError => ex
77
89
  raise ex, "Error while parsing #{file} : #{ex.message}"
78
90
  end
79
91
 
92
+ # Uses variable interpolation, prompts as needed, liquid interpolation
93
+ #
94
+ # @return [Hash] the most commplete stack parsing outcome
80
95
  def fully_interpolated_yaml
81
96
  return @fully_interpolated_yaml if @fully_interpolated_yaml
82
- vars = variables.to_h(values_only: true)
83
97
  @fully_interpolated_yaml = ::YAML.safe_load(
84
98
  replace_dollar_dollars(
85
99
  interpolate(
86
100
  interpolate_liquid(
87
101
  raw_content,
88
- vars
102
+ variable_values
89
103
  ),
90
104
  use_opto: true,
91
105
  raise_on_unknown: true
92
106
  )
93
107
  )
94
- ) || {}
108
+ )
95
109
  rescue Psych::SyntaxError => ex
96
110
  raise ex, "Error while parsing #{file} : #{ex.message}"
97
111
  end
98
112
 
113
+ # The YAML file raw content
114
+ def raw_content
115
+ loader.content
116
+ end
117
+
118
+ # @return [Hash] with zero interpolation/processing. Will mostly fail
99
119
  def raw_yaml
100
- @raw_yaml ||= ::YAML.safe_load(raw_content) || {}
120
+ loader.yaml
121
+ end
122
+
123
+ # Creates an opto option definition compatible hash from the #default_envs hash
124
+ # @return [Hash]
125
+ def default_envs_to_options
126
+ default_envs.each_with_object({}) { |env, obj| obj[env[0]] = { type: :string, value: env[1] } }
101
127
  end
102
128
 
129
+ # Accessor to the Opto variable handler
103
130
  # @return [Opto::Group]
104
131
  def variables
105
- return @variables if @variables
106
- @variables = ::Opto::Group.new(
107
- (internals_interpolated_yaml['variables'] || {}).merge('STACK' => { type: :string, value: env['STACK']}, 'GRID' => {type: :string, value: env['GRID']}),
108
- defaults: {
109
- from: :env,
110
- to: :env
111
- }
132
+ @variables ||= ::Opto::Group.new(
133
+ internals_interpolated_yaml.fetch('variables', {}).merge(default_envs_to_options),
134
+ defaults: { from: :env }
112
135
  )
113
- if defaults
114
- defaults.each do |key, val|
115
- var = @variables.option(key)
116
- var.default = val if var
117
- end
136
+ end
137
+
138
+ # Accepts a hash of variable_name => variable_value pairs and sets the values as variable default values
139
+ # Used when previous answers are read from master and passed as default values for upgrade.
140
+ # @param defaults [Hash] { 'variable_name' => 'variable_value' }
141
+ def set_variable_defaults(defaults)
142
+ defaults.each do |key, val|
143
+ var = variables.option(key.to_s)
144
+ var.default = val if var
118
145
  end
146
+ end
119
147
 
120
- if values
121
- values.each do |key, val|
122
- var = @variables.option(key)
123
- var.set(val) if var
124
- end
148
+ # Set values from a hash to values of the variables.
149
+ # Used when variable values are read from a file or command line parameters or dependency variable injection
150
+ # @param [Hash] a hash of variable_name => variable_value pairs
151
+ def set_variable_values(values)
152
+ values.each do |key, val|
153
+ var = variables.option(key.to_s)
154
+ var.set(val) if var
125
155
  end
126
- @variables
127
156
  end
128
157
 
129
- # @param [String] service_name
130
- # @return [Hash]
131
- def execute(service_name = nil)
132
- process_variables unless skip_variables?
133
- validate unless skip_validation?
134
-
135
- result = {}
136
- Dir.chdir(from_file? ? File.dirname(File.expand_path(file)) : Dir.pwd) do
137
- result[:stack] = raw_yaml['stack']
138
- result[:version] = self.stack_version
139
- result[:name] = self.stack_name
140
- result[:registry] = registry
141
- result[:expose] = fully_interpolated_yaml['expose']
142
- result[:errors] = errors unless skip_validation?
143
- result[:notifications] = notifications
144
- result[:services] = errors.count.zero? ? parse_services(service_name) : {}
145
- unless skip_variables?
146
- result[:variables] = variables.to_h(values_only: true).reject do |k,_|
147
- k == 'GRID' || k == 'STACK' || variables.option(k).to.has_key?(:vault) || variables.option(k).from.has_key?(:vault)
148
- end
149
- end
150
- result[:volumes] = errors.count.zero? ? parse_volumes : {}
158
+ # Creates a set of variables using the 'depends' section. The variable name is the name of the dependency
159
+ # and the variable value is the generated child stack name. For example,.have something like:
160
+ # depends:
161
+ # redis:
162
+ # stack: foo/redis
163
+ # you will get a new variable called "redis" and its value will be "this-stack-name-redis".
164
+ # This variable can be used to interpolate for example a hostname to some environment variable:
165
+ # environment:
166
+ # - "REDIS_HOST=redis.${REDIS}"
167
+ def create_dependency_variables(dependencies, name)
168
+ return if dependencies.nil?
169
+ dependencies.each do |options|
170
+ variables.build_option(name: options['name'].to_s, type: :string, value: "#{name}-#{options['name']}")
171
+ create_dependency_variables(options['depends'], "#{name}.#{options['name']}")
151
172
  end
152
- result
153
173
  end
154
174
 
175
+ # If this stack is a part of a dependency chain and has a parent, the variable $PARENT_STACK will
176
+ # interpolate to the name of the parent stack.
177
+ def create_parent_variable(parent_name)
178
+ variables.build_option(name: 'PARENT_STACK', type: :string, value: parent_name)
179
+ end
180
+
181
+ # @return [Boolean] did this stack come from a local file?
182
+ def from_file?
183
+ loader.origin == 'file'
184
+ end
185
+
186
+ # @param [String] service_name (set when using extends)
187
+ # @param name [String] override stackname (default is to parse it from the YAML, but if you set it through -n it needs to be overriden)
188
+ # @param parent_name [String] parent stack name
189
+ # @param skip_validation [Boolean] skip running validations
190
+ # @param values [Hash] force-set variable values using variable_name => variable_value key pairs
191
+ # @param defaults [Hash] set variable defaults from variable_name => variable_value key pairs
192
+ # @return [Hash]
193
+ def execute(service_name = nil, name: loader.stack_name.stack, parent_name: nil, skip_validation: false, values: nil, defaults: nil)
194
+ set_variable_defaults(defaults) if defaults
195
+ set_variable_values(values) if values
196
+ create_dependency_variables(dependencies, name)
197
+ create_parent_variable(parent_name) if parent_name
155
198
 
156
- def process_variables
157
199
  variables.run
158
- raise RuntimeError, "Variable validation failed: #{variables.errors.inspect}" unless variables.valid?
200
+ raise RuntimeError, "Variable validation failed: #{variables.errors.inspect} in #{file}" unless variables.valid? || skip_validation
201
+
202
+ validate unless skip_validation
203
+
204
+ {}.tap do |result|
205
+ Dir.chdir(from_file? ? File.dirname(File.expand_path(file)) : Dir.pwd) do
206
+ result['stack'] = raw_yaml['stack']
207
+ result['version'] = loader.stack_name.version || '0.0.1'
208
+ result['name'] = name
209
+ result['registry'] = loader.registry
210
+ result['expose'] = fully_interpolated_yaml['expose']
211
+ result['services'] = errors.empty? ? parse_services(service_name) : {}
212
+ result['volumes'] = errors.empty? ? parse_volumes : {}
213
+ result['dependencies'] = dependencies
214
+ result['source'] = raw_content
215
+ result['variables'] = variable_values(without_defaults: true, without_vault: true)
216
+ result['parent_name'] = parent_name
217
+ end
218
+ end
159
219
  end
160
220
 
221
+ # Returns an array of hashes containing the dependency tree starting from this file
222
+ # @return [Array<Hash>]]
223
+ def dependencies
224
+ @dependencies ||= loader.dependencies
225
+ end
226
+
227
+ # Interpolate any Liquid templating in the YAML content
228
+ # @param content [String] file content
229
+ # @param vars [Hash] key-value pairs
230
+ # @return [String]
161
231
  # @raise [Liquid::Error]
162
232
  def interpolate_liquid(content, vars)
163
233
  Liquid::Template.error_mode = :strict
@@ -169,41 +239,13 @@ module Kontena::Cli::Stacks
169
239
  template.render!(vars, strict_variables: true, strict_filters: true)
170
240
  end
171
241
 
172
- def stack_name
173
- @stack_name ||= parse_stack_name(raw_yaml['stack'].to_s)[:stack]
174
- end
175
-
176
- def stack_version
177
- @stack_version ||= raw_yaml['version'] || parse_stack_name(raw_yaml['stack'].to_s)[:version] || '0.0.1'
178
- end
179
-
180
- # @return [Array] array of validation errors
242
+ # @return [Array<Hash>] array of validation errors
181
243
  def validate
182
244
  result = validator.validate(fully_interpolated_yaml)
183
245
  store_failures(result)
184
246
  result
185
247
  end
186
248
 
187
- def skip_validation?
188
- !!@skip_validation
189
- end
190
-
191
- def skip_variables?
192
- !!@skip_variables
193
- end
194
-
195
- def from_registry?
196
- file =~ /\A[a-zA-Z0-9\_\.\-]+\/[a-zA-Z0-9\_\.\-]+(?::.*)?\z/ && !File.exist?(file)
197
- end
198
-
199
- def from_url?
200
- file =~ /\A(?:http|https|ftp):\/\//
201
- end
202
-
203
- def from_file?
204
- !from_registry? && !from_url?
205
- end
206
-
207
249
  # @return [Kontena::Cli::Stacks::YAML::ValidatorV3]
208
250
  def validator
209
251
  @validator ||= YAML::ValidatorV3.new
@@ -214,12 +256,12 @@ module Kontena::Cli::Stacks
214
256
  if process_hash?(config)
215
257
  volumes[name].delete('only_if')
216
258
  volumes[name].delete('skip_if')
217
- volumes[name] = process_volume(config)
259
+ volumes[name] = process_volume(name, config)
218
260
  else
219
261
  volumes.delete(name)
220
262
  end
221
263
  end
222
- volumes
264
+ volumes.map { |name, vol| vol.merge('name' => name) }
223
265
  end
224
266
 
225
267
  ##
@@ -228,7 +270,7 @@ module Kontena::Cli::Stacks
228
270
  def parse_services(service_name = nil)
229
271
  if service_name.nil?
230
272
  services.each do |name, config|
231
- services[name] = process_config(config)
273
+ services[name] = process_config(config, name)
232
274
  if process_hash?(config)
233
275
  services[name].delete('only_if')
234
276
  services[name].delete('skip_if')
@@ -236,10 +278,10 @@ module Kontena::Cli::Stacks
236
278
  services.delete(name)
237
279
  end
238
280
  end
239
- services
281
+ services.map { |name, svc| svc.merge('name' => name) }
240
282
  else
241
- raise ("Service '#{service_name}' not found in #{file}") unless services.has_key?(service_name)
242
- process_config(services[service_name])
283
+ raise ("Service '#{service_name}' not found in #{file}") unless services.key?(service_name)
284
+ process_config(services[service_name], service_name)
243
285
  end
244
286
  end
245
287
 
@@ -249,7 +291,6 @@ module Kontena::Cli::Stacks
249
291
  # @return [Boolean]
250
292
  def process_hash?(hash)
251
293
  return true unless hash['skip_if'] || hash['only_if']
252
- return true if skip_variables? || variables.empty?
253
294
 
254
295
  skip_lambdas = normalize_ifs(hash['skip_if'])
255
296
  only_lambdas = normalize_ifs(hash['only_if'])
@@ -266,36 +307,49 @@ module Kontena::Cli::Stacks
266
307
  end
267
308
 
268
309
  # @param [Hash] service_config
269
- def process_config(service_config)
310
+ def process_config(service_config, name=nil)
270
311
  normalize_env_vars(service_config)
271
312
  merge_env_vars(service_config)
272
313
  expand_build_context(service_config)
273
314
  normalize_build_args(service_config)
274
- if service_config.has_key?('extends')
315
+ if service_config.key?('extends')
275
316
  service_config = extend_config(service_config)
276
317
  service_config.delete('extends')
277
318
  end
278
- service_config
319
+ if name
320
+ exit_with_error("Image is missing for #{name}. Aborting.") unless service_config['image'] # why isn't this a validation?
321
+ ServiceGeneratorV2.new(service_config).generate.merge('name' => name)
322
+ else
323
+ ServiceGeneratorV2.new(service_config).generate
324
+ end
279
325
  end
280
326
 
281
- def process_volume(volume_config)
327
+ def process_volume(name, volume_config)
328
+ return [] if volume_config.nil? || volume_config.empty?
329
+ if volume_config['external'].is_a?(TrueClass)
330
+ volume_config['external'] = name
331
+ elsif volume_config['external']['name']
332
+ volume_config['external'] = volume_config['external']['name']
333
+ end
334
+ volume_config['name'] = name
282
335
  volume_config
283
336
  end
284
337
 
285
338
  def volumes
286
- @volumes ||= fully_interpolated_yaml['volumes'] || {}
339
+ @volumes ||= fully_interpolated_yaml.fetch('volumes', {})
287
340
  end
288
341
 
289
342
  # @return [Hash] - services from YAML file
290
343
  def services
291
- @services ||= fully_interpolated_yaml['services'] || {}
344
+ @services ||= fully_interpolated_yaml.fetch('services', {})
292
345
  end
293
346
 
294
347
  def from_external_file(filename, service_name)
295
- outcome = Reader.new(filename, skip_validation: skip_validation?, skip_variables: true, variables: variables, defaults: defaults, values: values).execute(service_name)
296
- errors.concat outcome[:errors] unless errors.any? { |item| item.has_key?(filename) }
297
- notifications.concat outcome[:notifications] unless notifications.any? { |item| item.has_key?(filename) }
298
- outcome[:services]
348
+ external_reader = Reader.new(filename)
349
+ outcome = external_reader.execute(service_name)
350
+ errors.concat external_reader.errors unless errors.any? { |item| item.key?(filename) }
351
+ notifications.concat external_reader.notifications unless notifications.any? { |item| item.key?(filename) }
352
+ outcome['services']
299
353
  end
300
354
 
301
355
  private
@@ -314,8 +368,9 @@ module Kontena::Cli::Stacks
314
368
  if use_opto
315
369
  opt = variables.option(var)
316
370
  if opt.nil?
317
- if variables.find { |opt| opt.to[:env][var] }
318
- val = env[var]
371
+ to_env = variables.find { |opt| Array(opt.to[:env]).include?(var) }
372
+ if to_env
373
+ val = to_env.value
319
374
  else
320
375
  raise RuntimeError, "Undeclared variable '#{var}' in #{file}:#{line_num} -- #{row}" if raise_on_unknown
321
376
  end
@@ -348,7 +403,7 @@ module Kontena::Cli::Stacks
348
403
  # @example
349
404
  # normalize_ifs( 'wp' ) # lambdas return true if variable wp is not null or false or 'false'
350
405
  # normalize_ifs( wp: 1 ) # lambdas return true if value of wp is 1
351
- # normalize_ifs( [:wp, :ws] ) # lambdas return true if wp and ws are not not null or false or 'false'
406
+ # normalize_ifs( ['wp, :ws'] ) # lambdas return true if wp and ws are not not null or false or 'false'
352
407
  # normalize_ifs( wp: 1, ws: 1) # lambdas return true if wp and ws are 1
353
408
  # normalize_ifs(nil) # returns nil
354
409
  def normalize_ifs(ifs)
@@ -373,34 +428,27 @@ module Kontena::Cli::Stacks
373
428
  # @param [Hash] service_config
374
429
  # @return [Hash] updated service config
375
430
  def extend_config(service_config)
376
- extended_service = extended_service(service_config['extends'])
377
- return unless extended_service
378
- filename = service_config['extends']['file']
379
- stackname = service_config['extends']['stack']
380
- if filename
381
- parent_config = from_external_file(filename, extended_service)
382
- elsif stackname
383
- parent_config = from_external_file(stackname, extended_service)
431
+ extends = service_config['extends']
432
+ case extends
433
+ when NilClass
434
+ return
435
+ when String
436
+ raise ("Service '#{extends}' not found in #{file}") unless services.key?(extends)
437
+ parent_config = process_config(services[extends])
438
+ when Hash
439
+ target = extends['file'] || extends['stack']
440
+ parent_config = from_external_file(target, extends['service'])
384
441
  else
385
- raise ("Service '#{extended_service}' not found in #{file}") unless services.has_key?(extended_service)
386
- parent_config = process_config(services[extended_service])
442
+ raise TypeError, "Extends must be a hash or string"
387
443
  end
388
444
  ServiceExtender.new(service_config).extend_from(parent_config)
389
445
  end
390
446
 
391
- def extended_service(extend_config)
392
- if extend_config.kind_of?(Hash)
393
- extend_config['service']
394
- elsif extend_config.kind_of?(String)
395
- extend_config
396
- else
397
- nil
398
- end
399
- end
400
-
401
447
  def store_failures(data)
402
- errors << { file => data[:errors] } unless data[:errors].empty?
403
- notifications << { file => data[:notifications] } unless data[:notifications].empty?
448
+ data['errors'] = data[:errors] unless data['errors']
449
+ data['notifications'] = data[:notifications] unless data['notifications']
450
+ errors << { file => data['errors'] || data[:errors] } unless data['errors'].empty?
451
+ notifications << { file => data['notifications'] } unless data['notifications'].empty?
404
452
  end
405
453
 
406
454
 
@@ -439,37 +487,15 @@ module Kontena::Cli::Stacks
439
487
 
440
488
  # @param [Hash] options - service config
441
489
  def normalize_build_args(options)
442
- if safe_dig(options, 'build', 'args').kind_of?(Array)
443
- args = options['build']['args'].dup
444
- options['build']['args'] = {}
445
- args.each do |arg|
446
- k,v = arg.split('=')
447
- options['build']['args'][k] = v
448
- end
449
- end
450
- end
451
-
452
- # Takes a stack name such as user/foo:1.0.0 and breaks it into components
453
- # @param [String] stack_name
454
- # @return [Hash] a hash with :user, :stack and :version
455
- def parse_stack_name(stack_name)
456
- return {} if stack_name.nil?
457
- return {} if stack_name.empty?
458
- name, version = stack_name.split(':', 2)
459
- if name.include?('/')
460
- user, stack = name.split('/', 2)
461
- else
462
- user = nil
463
- stack = name
464
- end
465
- {
466
- user: user,
467
- stack: stack,
468
- version: version
469
- }
490
+ build = options['build']
491
+ return unless build.kind_of?(Hash)
492
+ args = build['args']
493
+ return unless args
494
+ return unless args.kind_of?(Array)
495
+ build.delete('args')
496
+ build['args'] = args.map { |arg| arg.split('=', 2) }.to_h
470
497
  end
471
498
 
472
-
473
499
  def env
474
500
  ENV
475
501
  end