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.
- 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|
|