kontena-cli 1.4.0.pre8 → 1.4.0.pre9

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