kontena-cli 1.0.0.pre1 → 1.0.0.pre2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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|