kontena-cli 1.4.0.pre6 → 1.4.0.pre7

Sign up to get free protection for your applications and to get access to all the features.
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