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.
- checksums.yaml +4 -4
- data/VERSION +1 -1
- data/kontena-cli.gemspec +1 -0
- data/lib/kontena/callbacks/master/deploy/50_authenticate_after_deploy.rb +6 -1
- data/lib/kontena/cli/apps/list_command.rb +2 -2
- data/lib/kontena/cli/apps/scale_command.rb +1 -1
- data/lib/kontena/cli/apps/service_generator.rb +1 -2
- data/lib/kontena/cli/apps/yaml/custom_validators/extends_validator.rb +3 -4
- data/lib/kontena/cli/config.rb +1 -0
- data/lib/kontena/cli/services/list_command.rb +2 -2
- data/lib/kontena/cli/services/monitor_command.rb +1 -1
- data/lib/kontena/cli/services/services_helper.rb +1 -1
- data/lib/kontena/cli/stack_command.rb +12 -5
- data/lib/kontena/cli/stacks/common.rb +13 -12
- data/lib/kontena/cli/stacks/list_command.rb +5 -6
- data/lib/kontena/cli/stacks/monitor_command.rb +1 -1
- data/lib/kontena/cli/stacks/pull_command.rb +12 -0
- data/lib/kontena/cli/stacks/push_command.rb +17 -0
- data/lib/kontena/cli/stacks/search_command.rb +17 -0
- data/lib/kontena/cli/stacks/service_generator.rb +1 -2
- data/lib/kontena/cli/stacks/show_command.rb +1 -1
- data/lib/kontena/cli/stacks/yaml/opto/prompt_resolver.rb +76 -0
- data/lib/kontena/cli/stacks/yaml/opto/vault_resolver.rb +10 -0
- data/lib/kontena/cli/stacks/yaml/opto/vault_setter.rb +12 -0
- data/lib/kontena/cli/stacks/yaml/reader.rb +143 -47
- data/lib/kontena/cli/stacks/yaml/service_extender.rb +26 -35
- data/lib/kontena/cli/stacks/yaml/validations.rb +2 -0
- data/lib/kontena/cli/vault/read_command.rb +3 -0
- data/lib/kontena/cli/vault/update_command.rb +1 -1
- data/lib/kontena/cli/vault/write_command.rb +3 -1
- data/lib/kontena/stacks_cache.rb +86 -0
- data/lib/kontena/stacks_client.rb +37 -0
- data/lib/kontena_cli.rb +1 -0
- data/spec/fixtures/stack-invalid.yml +5 -0
- data/spec/fixtures/stack-with-prompted-variables.yml +66 -0
- data/spec/fixtures/stack-with-variables.yml +37 -0
- data/spec/kontena/cli/app/deploy_command_spec.rb +2 -2
- data/spec/kontena/cli/stacks/list_command_spec.rb +2 -4
- data/spec/kontena/cli/stacks/yaml/reader_spec.rb +8 -10
- data/spec/kontena/cli/stacks/yaml/service_extender_spec.rb +4 -2
- metadata +28 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bb909be3c98e6f195a842ad6291f01a061f5a99e
|
4
|
+
data.tar.gz: e19fbfaeb34ae9b1f980bdcd9daf1f5a946a4339
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 35adfcc4263f86a37f5d87c2ccc487c1db64e831092f1eeb9538575d7f01f11053001c52925d89f401806a8e7767cd5907893809bd497efa798a6acaa30b4705
|
7
|
+
data.tar.gz: b32a33ee26a54ba74db995a020a95ca7f310f43da9bc144ab6147fdeafeec3d927765f82df3defb98e27854a8b240c6864eae1979608dd5160b0aab1aff47292
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
1.0.0.
|
1
|
+
1.0.0.pre2
|
data/kontena-cli.gemspec
CHANGED
@@ -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['
|
41
|
-
desired = service['
|
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['
|
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['
|
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
|
-
|
15
|
-
|
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
|
data/lib/kontena/cli/config.rb
CHANGED
@@ -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['
|
34
|
-
desired = service['
|
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['
|
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['
|
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
|
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
|
11
|
-
reader = Kontena::Cli::Stacks::YAML::Reader.new(filename
|
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.
|
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
|
-
|
10
|
-
require_api_url
|
11
|
-
token = require_token
|
9
|
+
requires_current_master
|
12
10
|
|
13
|
-
|
11
|
+
def execute
|
12
|
+
list_stacks
|
14
13
|
end
|
15
14
|
|
16
|
-
def list_stacks
|
17
|
-
response = client
|
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,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['
|
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['
|
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,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
|
-
|
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
|
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[:
|
27
|
-
result[:
|
28
|
-
result[:name]
|
29
|
-
result[:expose]
|
30
|
-
result[: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]
|
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
|
-
|
48
|
-
|
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
|
-
|
65
|
-
|
66
|
-
|
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.
|
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
|
-
|
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
|
129
|
-
|
130
|
-
|
131
|
-
|
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.
|
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.
|
253
|
+
if extend_config.kind_of?(Hash)
|
158
254
|
extend_config['service']
|
159
|
-
elsif extend_config.
|
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.
|
169
|
-
notifications.concat outcome[:notifications] unless notifications.any? { |item| item.
|
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'].
|
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'].
|
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'].
|
295
|
+
if options['build'].kind_of?(String)
|
200
296
|
options['build'] = File.expand_path(options['build'])
|
201
|
-
elsif context = options
|
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').
|
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|
|