kontena-cli 1.0.0.pre1 → 1.0.0.pre2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/VERSION +1 -1
  3. data/kontena-cli.gemspec +1 -0
  4. data/lib/kontena/callbacks/master/deploy/50_authenticate_after_deploy.rb +6 -1
  5. data/lib/kontena/cli/apps/list_command.rb +2 -2
  6. data/lib/kontena/cli/apps/scale_command.rb +1 -1
  7. data/lib/kontena/cli/apps/service_generator.rb +1 -2
  8. data/lib/kontena/cli/apps/yaml/custom_validators/extends_validator.rb +3 -4
  9. data/lib/kontena/cli/config.rb +1 -0
  10. data/lib/kontena/cli/services/list_command.rb +2 -2
  11. data/lib/kontena/cli/services/monitor_command.rb +1 -1
  12. data/lib/kontena/cli/services/services_helper.rb +1 -1
  13. data/lib/kontena/cli/stack_command.rb +12 -5
  14. data/lib/kontena/cli/stacks/common.rb +13 -12
  15. data/lib/kontena/cli/stacks/list_command.rb +5 -6
  16. data/lib/kontena/cli/stacks/monitor_command.rb +1 -1
  17. data/lib/kontena/cli/stacks/pull_command.rb +12 -0
  18. data/lib/kontena/cli/stacks/push_command.rb +17 -0
  19. data/lib/kontena/cli/stacks/search_command.rb +17 -0
  20. data/lib/kontena/cli/stacks/service_generator.rb +1 -2
  21. data/lib/kontena/cli/stacks/show_command.rb +1 -1
  22. data/lib/kontena/cli/stacks/yaml/opto/prompt_resolver.rb +76 -0
  23. data/lib/kontena/cli/stacks/yaml/opto/vault_resolver.rb +10 -0
  24. data/lib/kontena/cli/stacks/yaml/opto/vault_setter.rb +12 -0
  25. data/lib/kontena/cli/stacks/yaml/reader.rb +143 -47
  26. data/lib/kontena/cli/stacks/yaml/service_extender.rb +26 -35
  27. data/lib/kontena/cli/stacks/yaml/validations.rb +2 -0
  28. data/lib/kontena/cli/vault/read_command.rb +3 -0
  29. data/lib/kontena/cli/vault/update_command.rb +1 -1
  30. data/lib/kontena/cli/vault/write_command.rb +3 -1
  31. data/lib/kontena/stacks_cache.rb +86 -0
  32. data/lib/kontena/stacks_client.rb +37 -0
  33. data/lib/kontena_cli.rb +1 -0
  34. data/spec/fixtures/stack-invalid.yml +5 -0
  35. data/spec/fixtures/stack-with-prompted-variables.yml +66 -0
  36. data/spec/fixtures/stack-with-variables.yml +37 -0
  37. data/spec/kontena/cli/app/deploy_command_spec.rb +2 -2
  38. data/spec/kontena/cli/stacks/list_command_spec.rb +2 -4
  39. data/spec/kontena/cli/stacks/yaml/reader_spec.rb +8 -10
  40. data/spec/kontena/cli/stacks/yaml/service_extender_spec.rb +4 -2
  41. metadata +28 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 449b75212c33f152d3b6d1179301ad0da967b5a2
4
- data.tar.gz: a4190acf5dbd76ddd258afd7aafd7aa186d3e606
3
+ metadata.gz: bb909be3c98e6f195a842ad6291f01a061f5a99e
4
+ data.tar.gz: e19fbfaeb34ae9b1f980bdcd9daf1f5a946a4339
5
5
  SHA512:
6
- metadata.gz: 5689de9e2585507122dde4432c24c3d58bcaec6acf8ff93748f1611449e2c337a52ae608ecf4abf853fdfdee039b990baa7cab7bff870853cb4f5afe603cdbd4
7
- data.tar.gz: 5531cc8ff20f5477c1912f06fabf78c73cbe44531141fc60db08430857191605d59fff266122cba3d4c733cb2338498944abe117b0decacb9ebdbe6963dcfe87
6
+ metadata.gz: 35adfcc4263f86a37f5d87c2ccc487c1db64e831092f1eeb9538575d7f01f11053001c52925d89f401806a8e7767cd5907893809bd497efa798a6acaa30b4705
7
+ data.tar.gz: b32a33ee26a54ba74db995a020a95ca7f310f43da9bc144ab6147fdeafeec3d927765f82df3defb98e27854a8b240c6864eae1979608dd5160b0aab1aff47292
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.0.0.pre1
1
+ 1.0.0.pre2
data/kontena-cli.gemspec CHANGED
@@ -29,4 +29,5 @@ Gem::Specification.new do |spec|
29
29
  spec.add_runtime_dependency "launchy", "~> 2.4.3"
30
30
  spec.add_runtime_dependency "hash_validator", "~> 0.7.0"
31
31
  spec.add_runtime_dependency "retriable", "~> 2.1.0"
32
+ spec.add_runtime_dependency "opto", "~> 1.5"
32
33
  end
@@ -16,6 +16,11 @@ module Kontena
16
16
  return unless command.result.has_key?(:code)
17
17
  return unless command.result.has_key?(:name)
18
18
 
19
+ # If plugins generate self-signed cert, most of the upcoming callbacks will
20
+ # fail without this. This can be made bit more clever once all the plugins
21
+ # return the generated self-signed certificate.
22
+ ENV['SSL_IGNORE_ERRORS'] = 'true'
23
+
19
24
  # In case there already is a server with the same name add random characters to name
20
25
  if config.find_server(command.result[:name])
21
26
  command.result[:name] = "#{command.result[:name]}-#{SecureRandom.hex(2)}"
@@ -33,7 +38,7 @@ module Kontena
33
38
  ENV["DEBUG"] && puts("Trying to request / from #{new_master.url}")
34
39
  client = Kontena::Client.new(new_master.url, nil, ignore_ssl_errors: true)
35
40
  client.get('/')
36
- rescue
41
+ rescue
37
42
  ENV["DEBUG"] && puts("HTTPS test failed: #{$!} #{$!.message}")
38
43
  unless retried
39
44
  new_master.url = "http://#{command.result[:public_ip]}"
@@ -37,8 +37,8 @@ module Kontena::Cli::Apps
37
37
  ports = service['ports'].map{|p|
38
38
  "#{p['ip']}:#{p['node_port']}->#{p['container_port']}/#{p['protocol']}"
39
39
  }.join(", ")
40
- running = service['instances']['running']
41
- desired = service['container_count']
40
+ running = service['instance_counts']['running']
41
+ desired = service['instances']
42
42
  instances = "#{running} / #{desired}"
43
43
  vars = [name, service['image'], instances, state, service['state'], ports]
44
44
  else
@@ -19,7 +19,7 @@ module Kontena::Cli::Apps
19
19
  yml_service = services_from_yaml(filename, [service], service_prefix, true)
20
20
  if yml_service[service]
21
21
  options = yml_service[service]
22
- exit_with_error("Service has already instances defined in #{filename}. Please update #{filename} and deploy service instead") if options['container_count']
22
+ exit_with_error("Service has already instances defined in #{filename}. Please update #{filename} and deploy service instead") if options['instances']
23
23
  spinner "Scaling #{service.colorize(:cyan)} " do
24
24
  deployment = scale_service(require_token, prefixed_name(service), instances)
25
25
  wait_for_deploy_to_finish(token, deployment)
@@ -25,10 +25,9 @@ module Kontena::Cli::Apps
25
25
  # @return [Hash]
26
26
  def parse_data(options)
27
27
  data = {}
28
- data['container_count'] = options['instances']
28
+ data['instances'] = options['instances']
29
29
  data['image'] = parse_image(options['image'])
30
30
  data['env'] = options['environment'] if options['environment']
31
- data['container_count'] = options['instances']
32
31
  data['links'] = parse_links(options['links'] || [])
33
32
  data['external_links'] = parse_links(options['external_links'] || [])
34
33
  data['ports'] = parse_stringified_ports(options['ports'] || [])
@@ -10,10 +10,9 @@ module Kontena::Cli::Apps::YAML::Validations::CustomValidators
10
10
  return
11
11
  end
12
12
  if value.is_a?(Hash)
13
- extends_validation = {
14
- 'service' => 'string',
15
- 'file' => HashValidator.optional('string')
16
- }
13
+ extends_validation = { 'service' => 'string' }
14
+ extends_validation['file'] = HashValidator.optional('string') if value['file']
15
+ extends_validation['stack'] = HashValidator.optional('string') if value['stack']
17
16
  HashValidator.validator_for(extends_validation).validate(key, value, extends_validation, errors)
18
17
  end
19
18
  end
@@ -122,6 +122,7 @@ module Kontena
122
122
  {
123
123
  name: 'kontena',
124
124
  url: 'https://cloud-api.kontena.io',
125
+ stacks_url: 'https://stacks.kontena.io',
125
126
  token_endpoint: 'https://cloud-api.kontena.io/oauth2/token',
126
127
  authorization_endpoint: 'https://cloud.kontena.io/login/oauth/authorize',
127
128
  userinfo_endpoint: 'https://cloud-api.kontena.io/user',
@@ -30,8 +30,8 @@ module Kontena::Cli::Services
30
30
 
31
31
  def print_service_row(service)
32
32
  stateful = service['stateful'] ? 'yes' : 'no'
33
- running = service['instances']['running']
34
- desired = service['container_count']
33
+ running = service['instance_counts']['running']
34
+ desired = service['instances']
35
35
  instances = "#{running} / #{desired}"
36
36
  ports = service['ports'].map{|p|
37
37
  "#{p['ip']}:#{p['node_port']}->#{p['container_port']}/#{p['protocol']}"
@@ -22,7 +22,7 @@ module Kontena::Cli::Services
22
22
  nodes[container['node']['name']] << container
23
23
  end
24
24
  clear_terminal
25
- puts "service: #{name} (#{result['containers'].size}/#{service['container_count']} instances)"
25
+ puts "service: #{name} (#{result['containers'].size}/#{service['instances']} instances)"
26
26
  puts "strategy: #{service['strategy']}"
27
27
  puts "status: #{service['state']}"
28
28
  puts "stateful: #{service['stateful'] == true ? 'yes' : 'no' }"
@@ -48,7 +48,7 @@ module Kontena
48
48
  puts " image: #{service['image']}"
49
49
  puts " revision: #{service['revision']}"
50
50
  puts " stateful: #{service['stateful'] == true ? 'yes' : 'no' }"
51
- puts " scaling: #{service['container_count'] }"
51
+ puts " scaling: #{service['instances'] }"
52
52
  puts " strategy: #{service['strategy']}"
53
53
  puts " deploy_opts:"
54
54
  puts " min_health: #{service['deploy_opts']['min_health']}"
@@ -7,6 +7,10 @@ require_relative 'stacks/show_command'
7
7
  require_relative 'stacks/build_command'
8
8
  require_relative 'stacks/monitor_command'
9
9
  require_relative 'stacks/logs_command'
10
+ require_relative 'stacks/push_command'
11
+ require_relative 'stacks/pull_command'
12
+ require_relative 'stacks/search_command'
13
+ require_relative 'stacks/install_command'
10
14
 
11
15
  class Kontena::Cli::StackCommand < Kontena::Command
12
16
 
@@ -16,11 +20,14 @@ class Kontena::Cli::StackCommand < Kontena::Command
16
20
  subcommand "show", "Show stack details", Kontena::Cli::Stacks::ShowCommand
17
21
  subcommand "upgrade", "Upgrade installed stack", Kontena::Cli::Stacks::UpgradeCommand
18
22
  subcommand "deploy", "Deploy stack", Kontena::Cli::Stacks::DeployCommand
19
- subcommand "logs", "Show stack logs from stack services", Kontena::Cli::Stacks::LogsCommand
20
- subcommand "monitor", "Monitor stack", Kontena::Cli::Stacks::MonitorCommand
21
- subcommand ["remove","rm"], "Remove stack", Kontena::Cli::Stacks::RemoveCommand
22
-
23
+ subcommand "logs", "Show logs from stack services", Kontena::Cli::Stacks::LogsCommand
24
+ subcommand "monitor", "Monitor stack services", Kontena::Cli::Stacks::MonitorCommand
25
+ subcommand ["remove","rm"], "Remove a deployed stack", Kontena::Cli::Stacks::RemoveCommand
26
+ subcommand "push", "Push a stack to stacks registry", Kontena::Cli::Stacks::PushCommand
27
+ subcommand ["pull", "get"], "Pull a stack from stacks registry", Kontena::Cli::Stacks::PullCommand
28
+ subcommand "install", "Deploy a stack to Kontena Master", Kontena::Cli::Stacks::InstallCommand
29
+ subcommand "search", "Search for stacks in stacks repository", Kontena::Cli::Stacks::SearchCommand
30
+
23
31
  def execute
24
-
25
32
  end
26
33
  end
@@ -1,19 +1,19 @@
1
- require 'yaml'
2
1
  require_relative 'yaml/reader'
3
2
  require_relative '../services/services_helper'
4
3
  require_relative 'service_generator_v2'
4
+ require_relative '../../stacks_client'
5
5
 
6
6
  module Kontena::Cli::Stacks
7
7
  module Common
8
8
  include Kontena::Cli::Services::ServicesHelper
9
9
 
10
- def stack_from_yaml(filename, skip_validation = false)
11
- reader = Kontena::Cli::Stacks::YAML::Reader.new(filename, skip_validation)
10
+ def stack_from_yaml(filename)
11
+ reader = Kontena::Cli::Stacks::YAML::Reader.new(filename)
12
12
  if reader.stack_name.nil?
13
- exit_with_error "Stack MUST have name in YAML! Aborting."
13
+ exit_with_error "Stack MUST have stack name in YAML top level field 'stack'! Aborting."
14
14
  end
15
15
  set_env_variables(self.name || reader.stack_name, current_grid)
16
- reader.reload
16
+ #reader.reload
17
17
  outcome = reader.execute
18
18
 
19
19
  hint_on_validation_notifications(outcome[:notifications]) if outcome[:notifications].size > 0
@@ -24,7 +24,7 @@ module Kontena::Cli::Stacks
24
24
  'stack' => outcome[:stack],
25
25
  'expose' => outcome[:expose],
26
26
  'version' => outcome[:version],
27
- 'source' => reader.raw,
27
+ 'source' => reader.raw_content,
28
28
  'registry' => 'file://',
29
29
  'services' => kontena_services
30
30
  }
@@ -52,12 +52,6 @@ module Kontena::Cli::Stacks
52
52
  services
53
53
  end
54
54
 
55
- def read_yaml(filename, skip_validation = false)
56
- reader = Kontena::Cli::Stacks::YAML::Reader.new(filename)
57
- outcome = reader.execute
58
- outcome
59
- end
60
-
61
55
  def set_env_variables(stack, grid)
62
56
  ENV['STACK'] = stack
63
57
  ENV['GRID'] = grid
@@ -98,5 +92,12 @@ module Kontena::Cli::Stacks
98
92
  display_notifications(errors, :red)
99
93
  abort
100
94
  end
95
+
96
+ def stacks_client
97
+ return @stacks_client if @stacks_client
98
+ Kontena.run('cloud login') unless cloud_auth?
99
+ config.reset_instance
100
+ @stacks_client = Kontena::StacksClient.new(kontena_account.stacks_url, kontena_account.token)
101
+ end
101
102
  end
102
103
  end
@@ -6,15 +6,14 @@ module Kontena::Cli::Stacks
6
6
  include Kontena::Cli::GridOptions
7
7
  include Common
8
8
 
9
- def execute
10
- require_api_url
11
- token = require_token
9
+ requires_current_master
12
10
 
13
- list_stacks(token)
11
+ def execute
12
+ list_stacks
14
13
  end
15
14
 
16
- def list_stacks(token)
17
- response = client(token).get("grids/#{current_grid}/stacks")
15
+ def list_stacks
16
+ response = client.get("grids/#{current_grid}/stacks")
18
17
 
19
18
  titles = ['NAME', 'VERSION', 'SERVICES', 'STATE', 'EXPOSED PORTS']
20
19
  puts "%-60s %-10s %-10s %-10s %-50s" % titles
@@ -15,7 +15,7 @@ module Kontena::Cli::Stacks
15
15
 
16
16
  response = client(token).get("grids/#{current_grid}/services?stack=#{name}")
17
17
  services = response['services']
18
- if selected_services
18
+ if selected_services.size > 0
19
19
  services.delete_if{ |s| !selected_services.include?(s['name'])}
20
20
  end
21
21
  show_monitor(token, services)
@@ -0,0 +1,12 @@
1
+ require_relative 'common'
2
+
3
+ module Kontena::Cli::Stacks
4
+ class PullCommand < Kontena::Command
5
+ include Kontena::Cli::Common
6
+ include Common
7
+
8
+ def execute
9
+ end
10
+ end
11
+ end
12
+
@@ -0,0 +1,17 @@
1
+ require_relative 'common'
2
+
3
+ module Kontena::Cli::Stacks
4
+ class PushCommand < Kontena::Command
5
+ include Kontena::Cli::Common
6
+ include Common
7
+
8
+ parameter "FILENAME", "Stack file path"
9
+
10
+ requires_current_account_token
11
+
12
+ def execute
13
+ file = YAML::Reader.new(self.filename, skip_variables: true, replace_missing: "filler")
14
+ stacks_client.push(file.yaml['stack'], file.yaml['version'], file.raw_content)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ require_relative 'common'
2
+
3
+ module Kontena::Cli::Stacks
4
+ class SearchCommand < Kontena::Command
5
+ include Kontena::Cli::Common
6
+ include Common
7
+
8
+ parameter '[QUERY]', "Query string"
9
+
10
+ requires_current_account_token
11
+
12
+ def execute
13
+ puts stacks_client.search(query).inspect
14
+ end
15
+ end
16
+ end
17
+
@@ -25,10 +25,9 @@ module Kontena::Cli::Stacks
25
25
  # @return [Hash]
26
26
  def parse_data(options)
27
27
  data = {}
28
- data['container_count'] = options['instances']
28
+ data['instances'] = options['instances']
29
29
  data['image'] = parse_image(options['image'])
30
30
  data['env'] = options['environment'] if options['environment']
31
- data['container_count'] = options['instances']
32
31
  data['links'] = parse_links(options['links'] || [])
33
32
  data['external_links'] = parse_links(options['external_links'] || [])
34
33
  data['ports'] = parse_stringified_ports(options['ports'] || [])
@@ -45,7 +45,7 @@ module Kontena::Cli::Stacks
45
45
  end
46
46
  puts "#{pad} revision: #{service['revision']}"
47
47
  puts "#{pad} stateful: #{service['stateful'] == true ? 'yes' : 'no' }"
48
- puts "#{pad} scaling: #{service['container_count'] }"
48
+ puts "#{pad} scaling: #{service['instances'] }"
49
49
  puts "#{pad} strategy: #{service['strategy']}"
50
50
  puts "#{pad} deploy_opts:"
51
51
  puts "#{pad} min_health: #{service['deploy_opts']['min_health']}"
@@ -0,0 +1,76 @@
1
+ if RUBY_VERSION < '2.1'
2
+ require 'opto/extensions/hash_string_or_symbol_key'
3
+ using Opto::Extension::HashStringOrSymbolKey
4
+ end
5
+
6
+ module Kontena::Cli::Stacks
7
+ module YAML
8
+ class Prompt < Opto::Resolver
9
+ include Kontena::Cli::Common
10
+
11
+ using Opto::Extension::HashStringOrSymbolKey unless RUBY_VERSION < '2.1'
12
+
13
+ def enum?
14
+ option.type == 'enum'
15
+ end
16
+
17
+ def boolean?
18
+ option.type == 'boolean'
19
+ end
20
+
21
+ def prompt_word
22
+ return "Select" if enum?
23
+ return "Enable" if boolean?
24
+ "Enter"
25
+ end
26
+
27
+ def question_text
28
+ (!hint.nil? && hint != option.name) ? "#{hint} :" : "#{prompt_word} #{option.label || option.name} :"
29
+ end
30
+
31
+ def enum_can_be_other?
32
+ enum? && option.handler.options[:can_be_other] ? true : false
33
+ end
34
+
35
+ def enum
36
+ opts = option.handler.options[:options]
37
+ opts << { label: '(Other)', value: nil, description: '(Other)' } if enum_can_be_other?
38
+
39
+ answer = prompt.select(question_text) do |menu|
40
+ menu.enum ':' # makes it show numbers before values, you can press the number to select.
41
+ menu.default(opts.index {|opt| opt[:value] == option.default }.to_i + 1) if option.default
42
+ opts.each do |opt|
43
+ menu.choice opt[:label], opt[:value]
44
+ end
45
+ end
46
+
47
+ if answer.nil? && enum_can_be_other?
48
+ ask
49
+ else
50
+ answer
51
+ end
52
+ end
53
+
54
+ def bool
55
+ prompt.yes?(question_text, default: option.default == false ? false : true)
56
+ end
57
+
58
+ def ask
59
+ prompt.ask(question_text, default: option.default)
60
+ end
61
+
62
+
63
+ def resolve
64
+ return nil if option.skip?
65
+ if enum?
66
+ enum
67
+ elsif boolean?
68
+ bool
69
+ else
70
+ ask
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+
@@ -0,0 +1,10 @@
1
+ module Kontena::Cli::Stacks
2
+ module YAML
3
+ class Opto::Resolvers::Vault < Opto::Resolver
4
+ def resolve
5
+ require 'shellwords'
6
+ Kontena.run("vault read --return #{hint.shellescape}", returning: :result)
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,12 @@
1
+ module Kontena::Cli::Stacks
2
+ module YAML
3
+ class Opto::Setters::Vault < Opto::Setter
4
+ def set(value)
5
+ require 'shellwords'
6
+ ENV["DEBUG"] && puts("Setting to vault: #{hint}")
7
+ Kontena.run("vault write --silent #{hint} #{value.to_s.shellescape}")
8
+ end
9
+ end
10
+ end
11
+ end
12
+
@@ -1,35 +1,63 @@
1
- require 'yaml'
2
- require_relative 'service_extender'
3
- require_relative 'validator_v3'
4
1
  require_relative '../../../util'
5
2
 
6
3
  module Kontena::Cli::Stacks
7
4
  module YAML
8
5
  class Reader
9
6
  include Kontena::Util
10
- attr_reader :yaml, :file, :errors, :notifications
11
7
 
12
- def initialize(file, skip_validation = false)
8
+ attr_reader :file, :raw_content, :result, :errors, :notifications, :variables, :yaml
9
+
10
+ def initialize(file, skip_validation: false, skip_variables: false, replace_missing: nil)
11
+ require 'yaml'
12
+ require_relative 'service_extender'
13
+ require_relative 'validator_v3'
14
+ require 'opto'
15
+ require_relative 'opto/vault_setter'
16
+ require_relative 'opto/vault_resolver'
17
+ require_relative 'opto/prompt_resolver'
18
+
13
19
  @file = file
20
+ @raw_content = File.read(File.expand_path(file))
14
21
  @errors = []
15
22
  @notifications = []
16
- @skip_validation = skip_validation
23
+ @skip_validation = skip_validation
24
+ @skip_variables = skip_variables
25
+ @replace_missing = replace_missing
26
+ parse_variables unless skip_variables
17
27
  parse_yaml
18
28
  end
19
29
 
30
+ # @return [Opto::Group]
31
+ def variables
32
+ return @variables if @variables
33
+ yaml = ::YAML.load(interpolate(raw_content, 'filler'))
34
+ if yaml && yaml.has_key?('variables')
35
+ @variables = Opto::Group.new(yaml['variables'], defaults: { from: :env, to: :env })
36
+ else
37
+ @variables = Opto::Group.new(defaults: { from: :env, to: :env })
38
+ end
39
+ @variables
40
+ end
41
+
42
+ def parse_variables
43
+ raise RuntimeError, "Variable validation failed: #{variables.errors.inspect}" unless variables.valid?
44
+ variables.run
45
+ end
46
+
20
47
  ##
21
48
  # @param [String] service_name
22
49
  # @return [Hash]
23
50
  def execute(service_name = nil)
24
51
  result = {}
25
52
  Dir.chdir(File.dirname(File.expand_path(file))) do
26
- result[:version] = yaml['version'] || '1'
27
- result[:stack] = yaml['stack']
28
- result[:name] = self.stack_name
29
- result[:expose] = yaml['expose']
30
- result[:errors] = errors
53
+ result[:stack] = yaml['stack']
54
+ result[:version] = self.stack_version
55
+ result[:name] = self.stack_name
56
+ result[:expose] = yaml['expose']
57
+ result[:errors] = errors unless skip_validation?
31
58
  result[:notifications] = notifications
32
- result[:services] = parse_services(service_name) unless errors.count > 0
59
+ result[:services] = parse_services(service_name) unless errors.count > 0
60
+ result[:variables] = variables.to_h(values_only: true).reject { |k,_| variables.option(k).to.has_key?(:vault) } unless skip_variables?
33
61
  end
34
62
  result
35
63
  end
@@ -37,38 +65,37 @@ module Kontena::Cli::Stacks
37
65
  def reload
38
66
  @errors = []
39
67
  @notifications = []
68
+ @variables = nil
69
+ parse_variables unless skip_variables?
40
70
  parse_yaml
41
71
  end
42
72
 
43
73
  def stack_name
44
- yaml['stack'].split('/').last if yaml['stack']
74
+ yaml['stack'].split('/').last.split(':').first if yaml['stack']
45
75
  end
46
76
 
47
- # @return [String]
48
- def raw
49
- read_content
77
+ def stack_version
78
+ yaml['version'] || yaml['stack'].to_s[/:(.*)/, 1] || '1'
50
79
  end
51
80
 
52
81
  private
53
82
 
83
+ # A hash such as { "${MYSQL_IMAGE}" => "MYSQL_IMAGE } where the key is the
84
+ # string to be substituted and value is the pure name part
85
+ # @return [Hash]
86
+ def yaml_substitutables
87
+ @content_variables ||= raw_content.scan(/((?<!\$)\$(?!\$)\{?(\w+)\}?)/m)
88
+ end
89
+
54
90
  def parse_yaml
55
91
  load_yaml
56
92
  validate unless skip_validation?
57
93
  end
58
94
 
59
- def read_content
60
- @content ||= File.read(File.expand_path(file))
61
- end
62
-
63
95
  def load_yaml
64
- content = read_content.dup
65
- interpolate(content)
66
- replace_dollar_dollars(content)
67
- begin
68
- @yaml = ::YAML.load(content)
69
- rescue Psych::SyntaxError => e
70
- raise "Error while parsing #{file}".colorize(:red)+ " "+e.message
71
- end
96
+ @yaml = ::YAML.load(replace_dollar_dollars(interpolate(raw_content)))
97
+ rescue Psych::SyntaxError => e
98
+ raise "Error while parsing #{file}".colorize(:red)+ " "+e.message
72
99
  end
73
100
 
74
101
  # @return [Array] array of validation errors
@@ -82,6 +109,10 @@ module Kontena::Cli::Stacks
82
109
  @skip_validation == true
83
110
  end
84
111
 
112
+ def skip_variables?
113
+ @skip_variables == true
114
+ end
115
+
85
116
  def store_failures(data)
86
117
  errors << { file => data[:errors] } unless data[:errors].empty?
87
118
  notifications << { file => data[:notifications] } unless data[:notifications].empty?
@@ -99,21 +130,75 @@ module Kontena::Cli::Stacks
99
130
  if service_name.nil?
100
131
  services.each do |name, config|
101
132
  services[name] = process_config(config)
133
+ if process_service?(config)
134
+ services[name].delete('only_if')
135
+ services[name].delete('skip_if')
136
+ else
137
+ services.delete(name)
138
+ end
102
139
  end
103
140
  services
104
141
  else
105
- raise ("Service '#{service_name}' not found in #{file}") unless services.key?(service_name)
142
+ raise ("Service '#{service_name}' not found in #{file}") unless services.has_key?(service_name)
106
143
  process_config(services[service_name])
107
144
  end
108
145
  end
109
146
 
147
+ def process_service?(config)
148
+ return true unless config['skip_if'] || config['only_if']
149
+ return true if skip_variables? || variables.empty?
150
+
151
+ skip_lambdas = normalize_ifs(config['skip_if'])
152
+ only_lambdas = normalize_ifs(config['only_if'])
153
+
154
+ if skip_lambdas
155
+ return false if skip_lambdas.any? { |s| s.call }
156
+ end
157
+
158
+ if only_lambdas
159
+ return false unless only_lambdas.all? { |s| s.call }
160
+ end
161
+
162
+ true
163
+ end
164
+
165
+ # Generates an array of lambdas that return true if a condition is true
166
+ # Possible syntaxes:
167
+ # @example
168
+ # normalize_ifs( 'wp' ) # lambdas return true if variable wp is not null or false or 'false'
169
+ # normalize_ifs( wp: 1 ) # lambdas return true if value of wp is 1
170
+ # normalize_ifs( [:wp, :ws] ) # lambdas return true if wp and ws are not not null or false or 'false'
171
+ # normalize_ifs( wp: 1, ws: 1) # lambdas return true if wp and ws are 1
172
+ # normalize_ifs(nil) # returns nil
173
+ def normalize_ifs(ifs)
174
+ case ifs
175
+ when NilClass
176
+ nil
177
+ when Array
178
+ ifs.map do |iff|
179
+ lambda { val = variables.value_of(iff.to_s); !val.nil? && !val.kind_of?(FalseClass) && val != 'false' }
180
+ end
181
+ when Hash
182
+ ifs.each_with_object([]) do |(k, v), arr|
183
+ arr << lambda { variables.value_of(k.to_s) == v }
184
+ end
185
+ when String, Symbol
186
+ [lambda { val = variables.value_of(ifs.to_s); !val.nil? && !val.kind_of?(FalseClass) && val != 'false' }]
187
+ else
188
+ raise TypeError, "Invalid syntax for if: #{ifs.inspect}"
189
+ end
190
+ end
191
+
110
192
  # @param [Hash] service_config
111
193
  def process_config(service_config)
112
194
  normalize_env_vars(service_config)
113
195
  merge_env_vars(service_config)
114
196
  expand_build_context(service_config)
115
197
  normalize_build_args(service_config)
116
- service_config = extend_config(service_config) if service_config.key?('extends')
198
+ if service_config.has_key?('extends')
199
+ service_config = extend_config(service_config)
200
+ service_config.delete('extends')
201
+ end
117
202
  service_config
118
203
  end
119
204
 
@@ -124,18 +209,29 @@ module Kontena::Cli::Stacks
124
209
 
125
210
  ##
126
211
  # @param [String] text - content of YAML file
127
- def interpolate(text)
128
- text.gsub!(/(?<!\$)\$(?!\$)\{?\w+\}?/) do |v| # searches $VAR and ${VAR} and not $$VAR
129
- var = v.tr('${}', '')
130
- puts "The #{var} is not set. Substituting an empty string." if !ENV.key?(var) && !skip_validation?
131
- ENV[var] # replace with equivalent ENV variables
212
+ def interpolate(text, filler = nil)
213
+ text.gsub(/(?<!\$)\$(?!\$)\{?\w+\}?/) do |v| # searches $VAR and ${VAR} and not $$VAR
214
+ if filler
215
+ filler
216
+ elsif @replace_missing
217
+ @replace_missing
218
+ else
219
+ var = v.tr('${}', '')
220
+ val = ENV[var]
221
+ if val
222
+ val
223
+ else
224
+ puts "Value for #{var} is not set. Substituting with an empty string." unless skip_validation?
225
+ ''
226
+ end
227
+ end
132
228
  end
133
229
  end
134
230
 
135
231
  ##
136
232
  # @param [String] text - content of yaml file
137
233
  def replace_dollar_dollars(text)
138
- text.gsub!('$$', '$')
234
+ text.gsub('$$', '$')
139
235
  end
140
236
 
141
237
  # @param [Hash] service_config
@@ -147,16 +243,16 @@ module Kontena::Cli::Stacks
147
243
  if filename
148
244
  parent_config = from_external_file(filename, extended_service)
149
245
  else
150
- raise ("Service '#{extended_service}' not found in #{file}") unless services.key?(extended_service)
246
+ raise ("Service '#{extended_service}' not found in #{file}") unless services.has_key?(extended_service)
151
247
  parent_config = process_config(services[extended_service])
152
248
  end
153
249
  ServiceExtender.new(service_config).extend_from(parent_config)
154
250
  end
155
251
 
156
252
  def extended_service(extend_config)
157
- if extend_config.is_a?(Hash)
253
+ if extend_config.kind_of?(Hash)
158
254
  extend_config['service']
159
- elsif extend_config.is_a?(String)
255
+ elsif extend_config.kind_of?(String)
160
256
  extend_config
161
257
  else
162
258
  nil
@@ -164,15 +260,15 @@ module Kontena::Cli::Stacks
164
260
  end
165
261
 
166
262
  def from_external_file(filename, service_name)
167
- outcome = Reader.new(filename, @skip_validation).execute(service_name)
168
- errors.concat outcome[:errors] unless errors.any? { |item| item.key?(filename) }
169
- notifications.concat outcome[:notifications] unless notifications.any? { |item| item.key?(filename) }
263
+ outcome = Reader.new(filename, skip_validation: @skip_validation, skip_variables: true, replace_missing: @replace_missing).execute(service_name)
264
+ errors.concat outcome[:errors] unless errors.any? { |item| item.has_key?(filename) }
265
+ notifications.concat outcome[:notifications] unless notifications.any? { |item| item.has_key?(filename) }
170
266
  outcome[:services]
171
267
  end
172
268
 
173
269
  # @param [Hash] options - service config
174
270
  def normalize_env_vars(options)
175
- if options['environment'].is_a?(Hash)
271
+ if options['environment'].kind_of?(Hash)
176
272
  options['environment'] = options['environment'].map { |k, v| "#{k}=#{v}" }
177
273
  end
178
274
  end
@@ -181,7 +277,7 @@ module Kontena::Cli::Stacks
181
277
  def merge_env_vars(options)
182
278
  return options['environment'] unless options['env_file']
183
279
 
184
- options['env_file'] = [options['env_file']] if options['env_file'].is_a?(String)
280
+ options['env_file'] = [options['env_file']] if options['env_file'].kind_of?(String)
185
281
  options['environment'] = [] unless options['environment']
186
282
  options['env_file'].each do |env_file|
187
283
  options['environment'].concat(read_env_file(env_file))
@@ -196,16 +292,16 @@ module Kontena::Cli::Stacks
196
292
  end
197
293
 
198
294
  def expand_build_context(options)
199
- if options['build'].is_a?(String)
295
+ if options['build'].kind_of?(String)
200
296
  options['build'] = File.expand_path(options['build'])
201
- elsif context = options.dig('build', 'context')
297
+ elsif context = safe_dig(options, 'build', 'context')
202
298
  options['build']['context'] = File.expand_path(context)
203
299
  end
204
300
  end
205
301
 
206
302
  # @param [Hash] options - service config
207
303
  def normalize_build_args(options)
208
- if safe_dig(options, 'build', 'args').is_a?(Array)
304
+ if safe_dig(options, 'build', 'args').kind_of?(Array)
209
305
  args = options['build']['args'].dup
210
306
  options['build']['args'] = {}
211
307
  args.each do |arg|