kontena-cli 1.4.0.pre8 → 1.4.0.pre9

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e5fbe13d6c43974ce734828c192b42c582b56a04
4
- data.tar.gz: a2de0cbe2555ea75327028a9f1f6417f839a876f
3
+ metadata.gz: 0ee7784ef8718b8afec8e31001cb8386de5ae3f9
4
+ data.tar.gz: d5dca2cd45bcd062bce2935e81cfdf33bca6572e
5
5
  SHA512:
6
- metadata.gz: 9ed72921adf1a691ed03ea9ec1223800191136b686d89c0e1ca2af21306755e83cb9844c69db3831da566a1592d700d12c2126a99ae9f620c1817d0bf3ce6441
7
- data.tar.gz: f1fd2b0a344552704f2e3c1de967daed63e9cf66532791af1040aea5f19262b7b123b2ee1f19d1799525cb02556784192fa9329b2e68a00636cfebc01c31463d
6
+ metadata.gz: 35c6016071aa01ef2a0a85a2b7e088f76fb24d74bd16bd7ea9db99da9c6ffe78f5f9bb7fee7d7395e3080534b2b9f30e1b87b0bfdc112d3633bc38423e8cc11b
7
+ data.tar.gz: 25e2c16f93d615d012157460d3edf46d01f7fa73a90a67daea6ad028ebb16556a37262d2233d95d9036ef1721a17136a8df1ab70ebb55c514d6467eed9fbe779
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.4.0.pre8
1
+ 1.4.0.pre9
@@ -15,7 +15,7 @@ module Kontena::Cli::Certificate
15
15
  THREE_DAYS = 3 * 24 * 60 * 60
16
16
 
17
17
  def fields
18
- quiet? ? ['subject'] : {subject: 'subject', "expiration" => 'expires_in'}
18
+ quiet? ? ['subject'] : {subject: 'subject', "expiration" => 'expires_in', auto_renewable?: 'auto_renewable'}
19
19
  end
20
20
 
21
21
  def certificates
@@ -0,0 +1,23 @@
1
+ require_relative '../services/services_helper'
2
+
3
+ module Kontena::Cli::Certificate
4
+ class RemoveCommand < Kontena::Command
5
+ include Kontena::Cli::Common
6
+ include Kontena::Cli::GridOptions
7
+
8
+ parameter "SUBJECT", "Certificate subject"
9
+ option "--force", :flag, "Force remove", default: false, attribute_name: :forced
10
+
11
+ requires_current_master
12
+ requires_current_master_token
13
+ requires_current_grid
14
+
15
+ def execute
16
+ confirm_command(self.subject) unless forced?
17
+
18
+ spinner "Removing certificate for #{self.subject.colorize(:cyan)} from #{current_grid.colorize(:cyan)} grid " do
19
+ client.delete("certificates/#{current_grid}/#{self.subject}")
20
+ end
21
+ end
22
+ end
23
+ end
@@ -7,6 +7,7 @@ class Kontena::Cli::CertificateCommand < Kontena::Command
7
7
  subcommand "authorize", "Create DNS authorization for domain", load_subcommand('certificate/authorize_command')
8
8
  subcommand "request", "Request certificate for domain", load_subcommand('certificate/request_command')
9
9
  subcommand "get", "Get certificate for domain", load_subcommand('certificate/get_command')
10
+ subcommand ["remove", "rm"], "Remove certificate for domain", load_subcommand('certificate/remove_command')
10
11
 
11
12
 
12
13
  def execute
@@ -0,0 +1,96 @@
1
+ require_relative 'yaml/stack_file_loader'
2
+
3
+ module Kontena::Cli::Stacks
4
+ class ChangeResolver
5
+
6
+ attr_reader :old_data, :new_data
7
+
8
+ # Creates a change analysis from two sets of stack data.
9
+ # The format is a flat hash of all related stacks.
10
+ #
11
+ # @param old_data [Hash]
12
+ # @param new_data [Hash]
13
+ def initialize(old_data, new_data)
14
+ @old_data = old_data
15
+ @new_data = new_data
16
+ analyze
17
+ end
18
+
19
+ # @return [Array<String>] an array of services that should be added
20
+ def added_services
21
+ @added_services ||= []
22
+ end
23
+
24
+ # @return [Array<String>] an array of services that should be removed
25
+ def removed_services
26
+ @removed_services ||= []
27
+ end
28
+
29
+ # @return [Array<String>] an array of services that should be upgraded
30
+ def upgraded_services
31
+ @upgraded_services ||= []
32
+ end
33
+
34
+ # @return [Array<String>] an array of stack installation names that should be removed
35
+ def removed_stacks
36
+ @removed_stacks ||= []
37
+ end
38
+
39
+ # @return [Array<String>] an array of stack installation names that should be removed
40
+ def added_stacks
41
+ @added_stacks ||= []
42
+ end
43
+
44
+ # @return [Array<String>] an array of stack installation names that should be upgraded
45
+ def upgraded_stacks
46
+ @upgraded_stacks ||= []
47
+ end
48
+
49
+ # @return [Hash] a hash of "installed-stack-name" => { :from => 'stackname', :to => 'new-stackname' }
50
+ def replaced_stacks
51
+ @replaced_stacks ||= {}
52
+ end
53
+
54
+ # @return [Array<String>] an array of installed stack names that should exist after upgrade
55
+ def remaining_stacks
56
+ @remaining_stacks ||= added_stacks + upgraded_stacks
57
+ end
58
+
59
+ def analyze
60
+ old_names = old_data.keys
61
+ new_names = new_data.keys
62
+
63
+ removed_stacks.concat(old_names - new_names)
64
+ added_stacks.concat(new_names - old_names)
65
+ upgraded_stacks.concat(new_names & old_names)
66
+
67
+ removed_stacks.each do |removed_stack|
68
+ removed_services.concat(
69
+ old_data[removed_stack][:stack_data]['services'].map { |svc| "#{removed_stack}/#{svc['name']}"}
70
+ )
71
+ end
72
+
73
+ added_stacks.each do |added_stack|
74
+ added_services.concat(
75
+ new_data[added_stack][:stack_data]['services'].map { |svc| "#{added_stack}/#{svc['name']}"}
76
+ )
77
+ end
78
+
79
+ upgraded_stacks.each do |upgraded_stack|
80
+ old_stack = old_data[upgraded_stack][:stack_data]['stack']
81
+ new_stack = new_data[upgraded_stack][:stack_data]['stack']
82
+
83
+ unless old_stack == new_stack
84
+ replaced_stacks[upgraded_stack] = { from: old_stack, to: new_stack }
85
+ end
86
+
87
+ old_services = old_data[upgraded_stack][:stack_data]['services'].map { |svc| "#{upgraded_stack}/#{svc['name']}" }
88
+ new_services = new_data[upgraded_stack][:stack_data]['services'].map { |svc| "#{upgraded_stack}/#{svc['name']}" }
89
+
90
+ removed_services.concat(old_services - new_services)
91
+ added_services.concat(new_services - old_services)
92
+ upgraded_services.concat(new_services & old_services)
93
+ end
94
+ end
95
+ end
96
+ end
@@ -44,7 +44,7 @@ module Kontena::Cli::Stacks
44
44
  return if dependencies.nil?
45
45
  dependencies.each do |dependency|
46
46
  target_name = "#{stack_name}-#{dependency['name']}"
47
- caret "Installing dependency #{pastel.cyan(dependency[:stack])} as #{pastel.cyan(target_name)}"
47
+ caret "Installing dependency #{pastel.cyan(dependency['stack'])} as #{pastel.cyan(target_name)}"
48
48
  cmd = ['stack', 'install', '-n', target_name, '--parent-name', stack_name]
49
49
 
50
50
  dependency['variables'].merge(dependency_values_from_options(dependency['name'])).each do |key, value|
@@ -76,6 +76,7 @@ module Kontena::Cli::Stacks
76
76
  end
77
77
 
78
78
  def tree_icon(row)
79
+ return '' unless $stdout.tty?
79
80
  parent = row['parent']
80
81
  children = row['children'] || []
81
82
  if parent.nil? && children.empty?
@@ -1,4 +1,7 @@
1
1
  require_relative 'common'
2
+ require_relative 'change_resolver'
3
+
4
+ require 'json'
2
5
 
3
6
  module Kontena::Cli::Stacks
4
7
  class UpgradeCommand < Kontena::Command
@@ -19,143 +22,207 @@ module Kontena::Cli::Stacks
19
22
 
20
23
  option '--force', :flag, 'Force upgrade'
21
24
  option '--skip-dependencies', :flag, "Do not install any stack dependencies"
22
- option '--dry-run', :flag, "Do not perform any uploading", hidden: true
25
+ option '--dry-run', :flag, "Simulate upgrade"
23
26
 
24
27
  requires_current_master
25
28
  requires_current_master_token
26
29
 
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
- }
30
+ # @return [Kontena::Cli::Stacks::ChangeResolver]
31
+ def execute
32
+ set_env_variables(stack_name, current_grid)
36
33
 
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))
34
+ old_data = spinner "Reading stack #{pastel.cyan(stack_name)} from master" do
35
+ gather_master_data(stack_name)
40
36
  end
41
- normalized_data
42
- end
43
37
 
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
38
+ new_data = spinner "Parsing #{pastel.cyan(source)}" do
39
+ loader.flat_dependencies(
40
+ stack_name,
41
+ variables: values_from_options
42
+ )
50
43
  end
51
- depends = data.delete('children') || []
52
44
 
53
- normalized_data = { stack_name => data }
45
+ changes = process_data(old_data, new_data)
46
+
47
+ display_report(changes)
48
+
49
+ return if dry_run?
50
+
51
+ get_confirmation(changes)
52
+
53
+ deployable_stacks = []
54
+ deployable_stacks.concat run_installs(changes)
55
+ deployable_stacks.concat run_upgrades(changes)
56
+
57
+ run_deploys(deployable_stacks) if deploy?
58
+
59
+ run_removes(changes.removed_stacks)
60
+
61
+ changes
62
+ end
54
63
 
55
- return normalized_data if skip_dependencies?
64
+ private
56
65
 
57
- depends.each do |stack|
58
- normalized_data.merge!(normalize_master_data(stack['name']))
66
+ # Recursively fetch master data in StackFileLoader#flat_dependencies format
67
+ # @return [Hash{string => Hash}] stackname => hash
68
+ def gather_master_data(stackname)
69
+ response = fetch_master_data(stackname)
70
+ children = response.delete('children') || []
71
+ result = { stackname => { stack_data: response } }
72
+ children.each do |child|
73
+ result.merge!(gather_master_data(child['name']))
59
74
  end
60
- normalized_data
75
+ result
61
76
  end
62
77
 
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
78
+ # Preprocess data and return a ChangeResolver
79
+ # @param old_data [Hash] data from master
80
+ # @param new_data [Hash] data from files
81
+ # @return [Kontena::Cli::Stacks::ChangeRsolver]
82
+ def process_data(old_data, new_data)
83
+ logger.debug { "Master stacks: #{old_data.keys.join(",")} YAML stacks: #{new_data.keys.join(",")}" }
84
+
85
+ new_data.reverse_each do |stackname, data|
86
+ reader = data[:loader].reader
87
+ set_env_variables(stackname, current_grid) # set envs for execution time
88
+ data[:stack_data] = reader.execute(
89
+ values: data[:variables],
90
+ defaults: old_data[stackname].nil? ? nil : old_data[stackname][:stack_data]['variables'],
91
+ parent_name: data[:parent_name],
92
+ name: data[:name]
93
+ )
94
+ hint_on_validation_notifications(reader.notifications, reader.loader.source)
95
+ abort_on_validation_errors(reader.errors, reader.loader.source)
70
96
  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
97
+
98
+ set_env_variables(stack_name, current_grid) # restore envs
99
+
100
+ spinner "Analyzing upgrade" do
101
+ Kontena::Cli::Stacks::ChangeResolver.new(old_data, new_data)
76
102
  end
77
- merged
78
103
  end
79
104
 
80
- def execute
81
- set_env_variables(stack_name, current_grid)
105
+ def display_report(changes)
106
+ if !dry_run? && changes.removed_stacks.empty? && changes.replaced_stacks.empty? && changes.upgraded_stacks.size == 1 && changes.removed_services.empty?
107
+ return
108
+ end
109
+
110
+ will = dry_run? ? "would" : "will"
111
+
112
+ puts "SERVICES:"
113
+ puts "-" * 40
82
114
 
83
- local = spinner "Parsing #{pastel.cyan(source)}" do
84
- normalize_local_data({'stack' => source, 'depends' => skip_dependencies? ? nil : loader.dependencies}, stack_name)
115
+ unless changes.removed_services.empty?
116
+ puts pastel.yellow("These services #{will} be removed from master:")
117
+ changes.removed_services.each { |svc| puts pastel.yellow(" - #{svc}") }
118
+ puts
85
119
  end
86
120
 
87
- remote = spinner "Reading stack #{pastel.cyan(stack_name)} from master" do
88
- normalize_master_data(stack_name, true)
121
+ unless changes.added_services.empty?
122
+ puts pastel.green("These new services #{will} be created to master:")
123
+ changes.added_services.each { |svc| puts pastel.green(" - #{svc}") }
124
+ puts
89
125
  end
90
126
 
91
- merged = merge_data(local, remote)
127
+ unless changes.upgraded_services.empty?
128
+ puts pastel.cyan("These services #{will} be upgraded:")
129
+ changes.upgraded_services.each do |svc|
130
+ puts pastel.cyan("- #{svc}")
131
+ end
132
+ puts
133
+ end
92
134
 
93
- removes = merged.keys.select { |k| merged[k][:local].nil? }
135
+ puts "STACKS:"
136
+ puts "-" * 40
94
137
 
95
- unless removes.empty?
138
+ unless changes.removed_stacks.empty?
139
+ puts pastel.red("These stacks #{will} be removed because they are no longer depended on:")
140
+ changes.removed_stacks.each { |stack| puts pastel.red("- #{stack}") }
96
141
  puts
97
- puts "Stacks to be removed because they are no longer depended on:"
98
- removes.each do |r|
99
- puts pastel.yellow("- #{r}")
142
+ end
143
+
144
+ unless changes.replaced_stacks.empty?
145
+ puts pastel.yellow("These stacks #{will} be replaced by other stacks:")
146
+ changes.replaced_stacks.each do |installed_name, data|
147
+ puts "- #{pastel.yellow(installed_name)} from #{pastel.cyan(data[:from])} to #{pastel.cyan(data[:to])}"
100
148
  end
101
149
  puts
102
- unless force?
150
+ end
151
+
152
+ unless changes.added_stacks.empty?
153
+ puts pastel.red("These new stack dependencies #{will} be installed:")
154
+ changes.added_stacks.each { |stack| puts pastel.red("- #{stack}") }
155
+ puts
156
+ end
157
+
158
+ unless changes.upgraded_stacks.empty?
159
+ puts pastel.cyan("These stacks #{will} be upgraded#{' and deployed' if deploy?}:")
160
+ changes.upgraded_stacks.each { |stack| puts pastel.cyan("- #{stack}") }
161
+ puts
162
+ end
163
+
164
+ puts
165
+ end
166
+
167
+ # requires heavier confirmation when something very dangerous is going to happen
168
+ def get_confirmation(changes)
169
+ unless force?
170
+ unless changes.removed_services.empty? && changes.removed_stacks.empty? && changes.replaced_stacks.empty?
103
171
  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)
172
+ confirm
109
173
  end
110
174
  end
175
+ end
111
176
 
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
177
+ def deployable_stacks
178
+ @deployable_stacks ||= []
179
+ end
180
+
181
+ def run_removes(removed_stacks)
182
+ removed_stacks.reverse_each do |removed_stack|
183
+ Kontena.run!('stack', 'remove', '--force', '--keep-dependencies', removed_stack)
184
+ end
185
+ end
186
+
187
+ # @return [Array] an array of stack names that have been installed, but not yet deployed
188
+ def run_installs(changes)
189
+ deployable_stacks = []
190
+ changes.added_stacks.reverse_each do |added_stack|
191
+ data = changes.new_data[added_stack]
192
+ cmd = ['stack', 'install', '--name', added_stack, '--no-deploy']
193
+ cmd.concat ['--parent-name', data[:parent_name]] if data[:parent_name]
194
+ data[:variables].each do |k,v|
195
+ cmd.concat ['-v', "#{k}=#{v}"]
118
196
  end
197
+ cmd << data[:loader].source
198
+ caret "Installing new dependency #{cmd.last} as #{added_stack}"
199
+ deployable_stacks << added_stack
200
+ Kontena.run!(cmd)
119
201
  end
202
+ deployable_stacks
203
+ end
120
204
 
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
-
143
- stack['variables'].merge(dependency_values_from_options(stackname)).each do |k, v|
144
- cmd.concat ['-v', "#{k}=#{v}"]
145
- end
146
-
147
- cmd << '--no-deploy'
148
- cmd << data[:local][:loader].source
149
- caret "Installing new dependency #{cmd.last} as #{stackname}"
150
- Kontena.run!(cmd)
205
+ # @return [Array] an array of stack names that have been upgraded, but not yet deployed
206
+ def run_upgrades(changes)
207
+ deployable_stacks = []
208
+ changes.upgraded_stacks.reverse_each do |upgraded_stack|
209
+ data = changes.new_data[upgraded_stack]
210
+ spinner "Upgrading #{stack_name == upgraded_stack ? 'stack' : 'dependency'} #{pastel.cyan(upgraded_stack)}" do |spin|
211
+ deployable_stacks << upgraded_stack
212
+ update_stack(upgraded_stack, data[:stack_data]) || spin.fail!
151
213
  end
214
+ end
215
+ deployable_stacks
216
+ end
152
217
 
153
- Kontena.run!(['stack', 'deploy', stackname]) if deploy?
218
+ # @param deployable_stacks [Array<String>] an array of stack names that should be deployed
219
+ def run_deploys(deployable_stacks)
220
+ deployable_stacks.each do |deployable_stack|
221
+ Kontena.run!(['stack', 'deploy', deployable_stack])
154
222
  end
155
223
  end
156
224
 
157
225
  def update_stack(name, data)
158
- return true if dry_run?
159
226
  client.put(stack_url(name), data)
160
227
  end
161
228
 
@@ -163,8 +230,8 @@ module Kontena::Cli::Stacks
163
230
  "stacks/#{current_grid}/#{name}"
164
231
  end
165
232
 
166
- def fetch_master_data(stack_name)
167
- client.get(stack_url(stack_name))
233
+ def fetch_master_data(stackname)
234
+ client.get(stack_url(stackname))
168
235
  end
169
236
  end
170
237
  end
@@ -30,7 +30,6 @@ module Kontena::Cli::Stacks
30
30
  cmd.concat ['--parent-name', stack_name]
31
31
 
32
32
  dependency['variables'].merge(dependency_values_from_options(dependency['name'])).each do |key, value|
33
- next if key == 'PARENT_STACK'
34
33
  cmd.concat ['-v', "#{key}=#{value}"]
35
34
  end
36
35
  cmd << dependency['stack']
@@ -58,8 +57,11 @@ module Kontena::Cli::Stacks
58
57
 
59
58
  dump_variables if values_to
60
59
 
61
- result = reader.fully_interpolated_yaml.merge(
62
- 'variables' => Kontena::Util.stringify_keys(reader.variables.to_h(with_values: true, with_errors: true))
60
+ result = stack.reject { |k, _| k == 'source' }
61
+ result.merge!(
62
+ 'variables' => Kontena::Util.stringify_keys(
63
+ reader.variable_values(without_defaults: true, without_vault: true, with_errors: true)
64
+ )
63
65
  )
64
66
  if dependencies?
65
67
  puts ::YAML.dump(result).sub(/\A---$/, "---\n# #{loader.source}")
@@ -49,8 +49,8 @@ module Kontena::Cli::Stacks
49
49
  # @param without_defaults [TrueClass,FalseClass] strip the GRID, STACK, etc from response
50
50
  # @param without_vault [TrueClass,FalseClass] strip out any values that are going to or coming from VAULT
51
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)
52
+ def variable_values(without_defaults: false, without_vault: false, with_errors: false)
53
+ result = variables.to_h(values_only: true, with_errors: with_errors)
54
54
  if without_defaults
55
55
  result.delete_if { |k, _| default_envs.key?(k.to_s) || k.to_s == 'PARENT_STACK' }
56
56
  end
@@ -43,6 +43,11 @@ module Kontena::Cli::Stacks
43
43
  @parent = parent
44
44
  end
45
45
 
46
+ # @return [String] a stripped down version of inspect without all the yaml source
47
+ def inspect
48
+ "#<#{self.class.name}:#{object_id} @source=#{source.inspect} @parent=#{parent.nil? ? 'nil' : parent.source}>"
49
+ end
50
+
46
51
  # @return [Hash] a hash parsed from the YAML content
47
52
  def yaml
48
53
  @yaml ||= ::YAML.safe_load(content, [], [], true, source)
@@ -75,21 +80,71 @@ module Kontena::Cli::Stacks
75
80
  # @return [Array<Hash>] an array of hashes ('name', 'stack', 'variables', and 'depends')
76
81
  def dependencies(recurse: true)
77
82
  return @dependencies if @dependencies
78
- depends = yaml['depends']
79
83
  if depends.nil? || depends.empty?
80
84
  @dependencies = nil
81
85
  else
82
86
  @dependencies = depends.map do |name, dependency|
83
- reader = StackFileLoader.for(dependency['stack'], self)
84
- deps = { 'name' => name, 'stack' => reader.source, 'variables' => dependency.fetch('variables', Hash.new) }
87
+ loader = StackFileLoader.for(dependency['stack'], self)
88
+ deps = { 'name' => name, 'stack' => loader.source, 'variables' => dependency.fetch('variables', Hash.new) }
85
89
  if recurse
86
- child_deps = reader.dependencies
90
+ child_deps = loader.dependencies
87
91
  deps['depends'] = child_deps unless child_deps.nil?
88
92
  end
89
93
  deps
90
94
  end
91
95
  end
92
96
  end
97
+
98
+ def to_h
99
+ {
100
+ 'stack' => stack_name.stack_name,
101
+ :loader => self,
102
+ }
103
+ end
104
+
105
+ # Returns a non nested hash of all dependencies.
106
+ # Processes :variables hash and moves the related variables to children
107
+ #
108
+ # @param basename [String] installed stack name
109
+ # @param opts [Hash] extra data such as variable lists
110
+ # @return [Hash] { installation_name => { 'name' => installation-name, 'stack' => stack_name, :loader => self }, child_install_name => { ... } }
111
+ def flat_dependencies(basename, opts = {})
112
+ opt_variables = opts[:variables] || {}
113
+
114
+ result = {
115
+ basename => self.to_h.merge(opts).merge(
116
+ name: basename,
117
+ variables: opt_variables.reject { |k, _| k.include?('.') }
118
+ )
119
+ }
120
+
121
+ depends.each do |as_name, data|
122
+ variables = {}
123
+
124
+ opt_variables.select { |k, _| k.start_with?(as_name + '.') }.each do |k,v|
125
+ variables[k.split('.', 2).last] = v
126
+ end
127
+
128
+ data['variables'] ||= {}
129
+
130
+ loader = StackFileLoader.for(data['stack'], self)
131
+ result.merge!(
132
+ loader.flat_dependencies(
133
+ basename + '-' + as_name,
134
+ variables: data['variables'].merge(variables),
135
+ parent_name: basename
136
+ )
137
+ )
138
+ end
139
+
140
+ result
141
+ end
142
+
143
+ private
144
+
145
+ def depends
146
+ yaml['depends'] || {}
147
+ end
93
148
  end
94
149
  end
95
150
  end
@@ -51,13 +51,13 @@ describe Kontena::Cli::Stacks::UpgradeCommand do
51
51
  it 'sends stack to master' do
52
52
  expect(client).to receive(:get).with('stacks/test-grid/stack-a').and_return(stack_response)
53
53
  expect(client).to receive(:put).with('stacks/test-grid/stack-a', hash_including(stack_expectation.merge('name' => 'stack-a'))).and_return(true)
54
- subject.run(['--no-deploy', 'stack-a', fixture_path('kontena_v3.yml')])
54
+ subject.run(['--no-deploy', '--force', 'stack-a', fixture_path('kontena_v3.yml')])
55
55
  end
56
56
 
57
57
  it 'requires confirmation when master stack is different than input stack' do
58
58
  expect(client).to receive(:get).with('stacks/test-grid/stack-b').and_return(stack_response.merge('stack' => 'foo/otherstack'))
59
- expect(subject).to receive(:confirm).with(/Replacing stack foo\/otherstack on master with user\/stackname/).and_call_original
60
- expect{subject.run(['stack-b', fixture_path('kontena_v3.yml')])}.to exit_with_error
59
+ expect(subject).to receive(:confirm).and_call_original
60
+ expect{subject.run(['stack-b', fixture_path('kontena_v3.yml')])}.to exit_with_error.and output(/- stack-b from foo\/otherstack to user\/stackname/).to_stdout
61
61
  end
62
62
 
63
63
  it 'triggers deploy by default' do
@@ -66,7 +66,7 @@ describe Kontena::Cli::Stacks::UpgradeCommand do
66
66
  'stacks/test-grid/stack-a', anything
67
67
  ).and_return({})
68
68
  expect(Kontena).to receive(:run!).with(['stack', 'deploy', 'stack-a']).once
69
- subject.run(['stack-a', fixture_path('kontena_v3.yml')])
69
+ subject.run(['--force', 'stack-a', fixture_path('kontena_v3.yml')])
70
70
  end
71
71
 
72
72
  context '--no-deploy option' do
@@ -83,13 +83,13 @@ describe Kontena::Cli::Stacks::UpgradeCommand do
83
83
  context 'with a stack including dependencies' do
84
84
 
85
85
  let(:expectation) {{ 'name' => 'deptest', 'stack' => 'user/depstack1' }}
86
- let(:expectation_1) {{ 'name' => 'deptest-dep_1', 'stack' => 'user/depstack1child1' }}
86
+ let(:expectation_1) {{ 'name' => 'deptest-dep_1', 'stack' => 'user/depstack1child1'}}
87
87
  let(:expectation_1_1) {{ 'name' => 'deptest-dep_1-dep_1', 'stack' => 'user/depstack1child1child1', 'services' => array_including(hash_including('image' => 'image:2')) }}
88
88
  let(:expectation_2) {{ 'name' => 'deptest-dep_2', 'stack' => 'user/depstack1child2', 'services' => array_including(hash_including('image' => 'image:1')), 'variables' => hash_including('dep_var' => 1) }}
89
89
 
90
- let(:response) { expectation.merge('parent' => nil, 'children' => [{'name' => 'deptest-dep_1'}, {'name' => 'deptest-dep_2'}]) }
91
- let(:response_1) { expectation_1.merge('parent' => { 'name' => 'deptest' }, 'children' => [{'name' => 'deptest-dep_1-dep_1'}]) }
92
- let(:response_1_1) { expectation_1_1.merge('parent' => { 'name' => 'deptest-dep_1' }, 'children' => []) }
90
+ let(:response) { expectation.merge('parent' => nil, 'children' => [{'name' => 'deptest-dep_1'}, {'name' => 'deptest-dep_2'}], 'services' => []) }
91
+ let(:response_1) { expectation_1.merge('parent' => { 'name' => 'deptest' }, 'children' => [{'name' => 'deptest-dep_1-dep_1'}], 'services' => []) }
92
+ let(:response_1_1) { expectation_1_1.merge('parent' => { 'name' => 'deptest-dep_1' }, 'children' => [], 'services' => []) }
93
93
  let(:response_2) { expectation_2.merge('parent' => { 'name' => 'deptest' }, 'children' => [], 'variables' => {}, 'services' => []) }
94
94
 
95
95
  before do
@@ -104,7 +104,7 @@ describe Kontena::Cli::Stacks::UpgradeCommand do
104
104
  expect(client).to receive(:put).with('stacks/test-grid/deptest-dep_1-dep_1', hash_including(expectation_1_1)).and_return(true)
105
105
  expect(client).to receive(:put).with('stacks/test-grid/deptest-dep_1', hash_including(expectation_1)).and_return(true)
106
106
  expect(client).to receive(:put).with('stacks/test-grid/deptest', hash_including(expectation)).and_return(true)
107
- subject.run(['--no-deploy', '-v', 'dep_2.dep_var=1', 'deptest', fixture_path('stack-with-dependencies.yml')])
107
+ subject.run(['--force', '--no-deploy', '-v', 'dep_2.dep_var=1', 'deptest', fixture_path('stack-with-dependencies.yml')])
108
108
  end
109
109
 
110
110
  context 'when a dependency has been removed' do
@@ -117,8 +117,8 @@ describe Kontena::Cli::Stacks::UpgradeCommand do
117
117
  context 'when a dependency has been added' do
118
118
  it 'installs any new stacks in the dependency chain' do
119
119
  allow(client).to receive(:put).and_return(true)
120
- expect(Kontena).to receive(:run!).with(["stack", "install", "--name", "deptest-dep_3", "--parent-name", "deptest", "--no-deploy", fixture_path('stack-with-dependencies-dep-3.yml')]).and_return(true)
121
- subject.run(['--no-deploy', '-v', 'dep_2.dep_var=1', 'deptest', fixture_path('stack-with-dependencies-dep_3-added.yml')])
120
+ expect(Kontena).to receive(:run!).with(["stack", "install", "--name", "deptest-dep_3", "--no-deploy", "--parent-name", "deptest", fixture_path('stack-with-dependencies-dep-3.yml')]).and_return(true)
121
+ subject.run(['--no-deploy', '--force', '-v', 'dep_2.dep_var=1', 'deptest', fixture_path('stack-with-dependencies-dep_3-added.yml')])
122
122
  end
123
123
  end
124
124
  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.4.0.pre8
4
+ version: 1.4.0.pre9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kontena, Inc
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-09-21 00:00:00.000000000 Z
11
+ date: 2017-09-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -257,6 +257,7 @@ files:
257
257
  - lib/kontena/cli/certificate/get_command.rb
258
258
  - lib/kontena/cli/certificate/list_command.rb
259
259
  - lib/kontena/cli/certificate/register_command.rb
260
+ - lib/kontena/cli/certificate/remove_command.rb
260
261
  - lib/kontena/cli/certificate/request_command.rb
261
262
  - lib/kontena/cli/certificate/show_command.rb
262
263
  - lib/kontena/cli/certificate_command.rb
@@ -408,6 +409,7 @@ files:
408
409
  - lib/kontena/cli/spinner.rb
409
410
  - lib/kontena/cli/stack_command.rb
410
411
  - lib/kontena/cli/stacks/build_command.rb
412
+ - lib/kontena/cli/stacks/change_resolver.rb
411
413
  - lib/kontena/cli/stacks/common.rb
412
414
  - lib/kontena/cli/stacks/deploy_command.rb
413
415
  - lib/kontena/cli/stacks/events_command.rb