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
@@ -15,63 +15,54 @@ module Kontena::Cli::Stacks
|
|
15
15
|
# @param [Hash] from
|
16
16
|
# @return [Hash]
|
17
17
|
def extend_from(from)
|
18
|
-
service_config['environment'] = extend_env_vars(
|
19
|
-
|
20
|
-
|
21
|
-
)
|
22
|
-
service_config['secrets'] = extend_secrets(
|
23
|
-
from['secrets'],
|
24
|
-
service_config['secrets']
|
25
|
-
)
|
26
|
-
build_args = extend_build_args(safe_dig(from, 'build', 'args'), safe_dig(service_config, 'build', 'args'))
|
18
|
+
service_config['environment'] = extend_env_vars(from['environment'], service_config['environment'])
|
19
|
+
service_config['secrets'] = extend_secrets( from['secrets'], service_config['secrets'])
|
20
|
+
build_args = extend_build_args(safe_dig(from, 'build', 'args'), safe_dig(service_config, 'build', 'args'))
|
27
21
|
unless build_args.empty?
|
28
|
-
service_config['build']
|
22
|
+
service_config['build'] ||= {}
|
29
23
|
service_config['build']['args'] = build_args
|
30
24
|
end
|
31
|
-
|
32
25
|
from.merge(service_config)
|
33
26
|
end
|
34
27
|
|
35
28
|
private
|
36
29
|
|
30
|
+
# Takes two arrays of "key=value" pairs and merges them. Keys in "from"-array
|
31
|
+
# will not overwrite keys that already exist in "to"-array.
|
32
|
+
#
|
37
33
|
# @param [Array] from
|
38
34
|
# @param [Array] to
|
39
35
|
# @return [Array]
|
40
36
|
def extend_env_vars(from, to)
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
end
|
48
|
-
end
|
49
|
-
env_vars
|
37
|
+
to ||= []
|
38
|
+
from ||= []
|
39
|
+
to_hash = Hash[*to.flat_map {|t| t.split('=') }]
|
40
|
+
from_hash = Hash[*from.flat_map {|f| f.split('=') }]
|
41
|
+
|
42
|
+
from_hash.merge(to_hash).map {|k,v| "#{k}=#{v}"}
|
50
43
|
end
|
51
44
|
|
45
|
+
# Takes two arrays of hashes containing { 'secret' => 'str', 'type' => 'str', 'name' => 'str' }
|
46
|
+
# and merges them. 'secret' is the primary key, secrets found in "to" are not overwritten.
|
47
|
+
#
|
52
48
|
# @param [Array] from
|
53
49
|
# @param [Array] to
|
54
50
|
# @return [Array]
|
55
51
|
def extend_secrets(from, to)
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
end
|
62
|
-
end
|
52
|
+
from ||= []
|
53
|
+
to ||= []
|
54
|
+
uniq_from = []
|
55
|
+
from.each do |from_hash|
|
56
|
+
uniq_from << from_hash unless to.find {|to_hash| from_hash['secret'] == to_hash['secret'] }
|
63
57
|
end
|
64
|
-
|
58
|
+
to + uniq_from
|
65
59
|
end
|
66
60
|
|
61
|
+
# Basic merge of two hashes, "to" is dominant.
|
67
62
|
def extend_build_args(from, to)
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
args[k] = v unless args.has_key?(k)
|
72
|
-
end
|
73
|
-
end
|
74
|
-
args
|
63
|
+
from ||= {}
|
64
|
+
to ||= {}
|
65
|
+
from.merge(to)
|
75
66
|
end
|
76
67
|
end
|
77
68
|
end
|
@@ -43,6 +43,8 @@ module Kontena::Cli::Stacks::YAML
|
|
43
43
|
'volumes_from' => optional('array'),
|
44
44
|
'secrets' => optional('stacks_valid_secrets'),
|
45
45
|
'hooks' => optional('stacks_valid_hooks'),
|
46
|
+
'only_if' => optional(-> (value) { value.is_a?(String) || value.is_a?(Hash) || value.is_a?(Array) }),
|
47
|
+
'skip_if' => optional(-> (value) { value.is_a?(String) || value.is_a?(Hash) || value.is_a?(Array) }),
|
46
48
|
'deploy' => optional({
|
47
49
|
'strategy' => optional(%w(ha daemon random)),
|
48
50
|
'wait_for_port' => optional('integer'),
|
@@ -5,12 +5,15 @@ module Kontena::Cli::Vault
|
|
5
5
|
|
6
6
|
parameter "NAME", "Secret name"
|
7
7
|
|
8
|
+
option '--return', :flag, 'Return the value', hidden: true
|
9
|
+
|
8
10
|
def execute
|
9
11
|
require_api_url
|
10
12
|
require_current_grid
|
11
13
|
|
12
14
|
token = require_token
|
13
15
|
result = client(token).get("secrets/#{current_grid}/#{name}")
|
16
|
+
return result['value'] if self.return?
|
14
17
|
puts "#{result['name']}:"
|
15
18
|
puts " created_at: #{result['created_at']}"
|
16
19
|
puts " value: #{result['value']}"
|
@@ -22,7 +22,7 @@ module Kontena::Cli::Vault
|
|
22
22
|
upsert: upsert?
|
23
23
|
}
|
24
24
|
spinner "Updating #{name.colorize(:cyan)} value in the vault " do
|
25
|
-
client(token).put("
|
25
|
+
client(token).put("secrets/#{current_grid}/#{name}", data)
|
26
26
|
end
|
27
27
|
end
|
28
28
|
end
|
@@ -6,6 +6,8 @@ module Kontena::Cli::Vault
|
|
6
6
|
parameter 'NAME', 'Secret name'
|
7
7
|
parameter '[VALUE]', 'Secret value'
|
8
8
|
|
9
|
+
option '--silent', :flag, "Reduce output verbosity"
|
10
|
+
|
9
11
|
def execute
|
10
12
|
require_api_url
|
11
13
|
require_current_grid
|
@@ -20,7 +22,7 @@ module Kontena::Cli::Vault
|
|
20
22
|
name: name,
|
21
23
|
value: secret
|
22
24
|
}
|
23
|
-
|
25
|
+
vspinner "Writing #{name.colorize(:cyan)} to the vault " do
|
24
26
|
client(token).post("grids/#{current_grid}/secrets", data)
|
25
27
|
end
|
26
28
|
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require_relative 'stacks_client'
|
2
|
+
require_relative 'cli/common'
|
3
|
+
require_relative 'cli/stacks/common'
|
4
|
+
|
5
|
+
module Kontena
|
6
|
+
class StacksCache
|
7
|
+
class CachedStack
|
8
|
+
|
9
|
+
attr_reader :stack
|
10
|
+
attr_reader :version
|
11
|
+
|
12
|
+
def initialize(stack, version = nil)
|
13
|
+
unless version
|
14
|
+
stack, version = stack.split(':', 2)
|
15
|
+
end
|
16
|
+
@stack = stack
|
17
|
+
@version = version
|
18
|
+
raise ArgumentError, "Stack name and version required" unless @stack && @version
|
19
|
+
end
|
20
|
+
|
21
|
+
def read
|
22
|
+
File.read(path)
|
23
|
+
end
|
24
|
+
|
25
|
+
def load
|
26
|
+
YAML.load(read)
|
27
|
+
end
|
28
|
+
|
29
|
+
def write(content)
|
30
|
+
File.write(path, content)
|
31
|
+
end
|
32
|
+
|
33
|
+
def delete
|
34
|
+
File.unlink(path)
|
35
|
+
end
|
36
|
+
|
37
|
+
def cached?
|
38
|
+
File.exist?(path)
|
39
|
+
end
|
40
|
+
|
41
|
+
def path
|
42
|
+
return @path if @path
|
43
|
+
@path = File.expand_path(File.join(base_path, stack, version))
|
44
|
+
raise "Path traversal attempted" unless @path.start_with?(base_path)
|
45
|
+
@path
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def base_path
|
51
|
+
Kontena::StacksCache.base_path
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
class RegistryClientFactory
|
56
|
+
include Kontena::Cli::Common
|
57
|
+
include Kontena::Cli::Stacks::Common
|
58
|
+
end
|
59
|
+
|
60
|
+
class << self
|
61
|
+
def get(stack, version = nil)
|
62
|
+
cache(stack, version).read
|
63
|
+
end
|
64
|
+
|
65
|
+
def cache(stack, version = nil)
|
66
|
+
stack = CachedStack.new(stack, version)
|
67
|
+
stack.write(client.pull(stack.stack, stack.version)) unless stack.cached?
|
68
|
+
stack
|
69
|
+
end
|
70
|
+
|
71
|
+
def client
|
72
|
+
@client ||= RegistryClientFactory.new.stacks_client
|
73
|
+
end
|
74
|
+
|
75
|
+
def base_path
|
76
|
+
return @base_path if @base_path
|
77
|
+
@base_path = File.join(Dir.home, '.kontena/cache/stacks')
|
78
|
+
unless File.directory?(@base_path)
|
79
|
+
require 'fileutils'
|
80
|
+
FileUtils.mkdir_p(@base_path)
|
81
|
+
end
|
82
|
+
@base_path
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require_relative 'client'
|
2
|
+
|
3
|
+
module Kontena
|
4
|
+
class StacksClient < Client
|
5
|
+
|
6
|
+
ACCEPT_JSON = { 'Accept' => 'application/json' }
|
7
|
+
ACCEPT_YAML = { 'Accept' => 'application/yaml' }
|
8
|
+
CT_YAML = { 'Content-Type' => 'application/yaml' }
|
9
|
+
|
10
|
+
def path_to(repo_name, version = nil)
|
11
|
+
version ? "/stack/#{repo_name}/version/#{version}" : "/stack/#{repo_name}"
|
12
|
+
end
|
13
|
+
|
14
|
+
def push(repo_name, version, data)
|
15
|
+
post('/stack/', data, {}, CT_YAML)
|
16
|
+
end
|
17
|
+
|
18
|
+
def pull(repo_name, version = nil)
|
19
|
+
get(path_to(repo_name, version), {}, ACCEPT_YAML)
|
20
|
+
rescue StandardError => ex
|
21
|
+
ex.message << " : #{path_to(repo_name, version)}"
|
22
|
+
raise ex, ex.message
|
23
|
+
end
|
24
|
+
|
25
|
+
def search(query)
|
26
|
+
get('/search', { q: query }, {}, ACCEPT_JSON)
|
27
|
+
end
|
28
|
+
|
29
|
+
def versions(repo_name)
|
30
|
+
get("#{path_to(repo_name)}/versions", {}, ACCEPT_JSON)
|
31
|
+
end
|
32
|
+
|
33
|
+
def destroy(repo_name, version = nil)
|
34
|
+
delete(path_to(repo_name, version))
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/lib/kontena_cli.rb
CHANGED
@@ -65,6 +65,7 @@ require_relative 'kontena/cli/version'
|
|
65
65
|
require_relative 'kontena/cli/common'
|
66
66
|
require_relative 'kontena/command'
|
67
67
|
require_relative 'kontena/client'
|
68
|
+
require_relative 'kontena/stacks_cache'
|
68
69
|
require_relative 'kontena/plugin_manager'
|
69
70
|
require_relative 'kontena/main_command'
|
70
71
|
require_relative 'kontena/cli/spinner'
|
@@ -0,0 +1,66 @@
|
|
1
|
+
stack: user/stackname
|
2
|
+
version: 0.1.1
|
3
|
+
variables:
|
4
|
+
enum_test:
|
5
|
+
type: enum
|
6
|
+
options:
|
7
|
+
- Foo
|
8
|
+
- Bar
|
9
|
+
- Baz
|
10
|
+
from: prompt
|
11
|
+
wp_pass:
|
12
|
+
type: string # has a callback that writes the value to vault
|
13
|
+
required: true
|
14
|
+
min_length: 10
|
15
|
+
empty_is_nil: true
|
16
|
+
from:
|
17
|
+
env: WORDPRESS_DB_PASSWORD # first try from local env
|
18
|
+
prompt: Enter a password for the wordpress instance # then try from prompt
|
19
|
+
random_string: # if prompt returned nil, generate a random string
|
20
|
+
length: 10
|
21
|
+
charset: ascii_printable
|
22
|
+
to:
|
23
|
+
env: WP_PASS
|
24
|
+
test_var:
|
25
|
+
type: string
|
26
|
+
from:
|
27
|
+
random_string:
|
28
|
+
length: 16
|
29
|
+
charset: hex
|
30
|
+
to:
|
31
|
+
env: test_var
|
32
|
+
TEST_ENV_VAR: # the default from/to is to set/read env of the option name
|
33
|
+
type: :string
|
34
|
+
services:
|
35
|
+
wordpress:
|
36
|
+
extends:
|
37
|
+
file: docker-compose_v2.yml
|
38
|
+
service: wordpress
|
39
|
+
image: wordpress:$TAG
|
40
|
+
stateful: true
|
41
|
+
environment:
|
42
|
+
- WORDPRESS_DB_PASSWORD=${STACK}_secret
|
43
|
+
secrets:
|
44
|
+
- secret: WP_ADMIN_PASSWORD
|
45
|
+
name: WORDPRESS_PASSWORD
|
46
|
+
type: env
|
47
|
+
- secret: FOO
|
48
|
+
name: FOOFOO
|
49
|
+
type: env
|
50
|
+
instances: 2
|
51
|
+
deploy:
|
52
|
+
strategy: ha
|
53
|
+
mysql:
|
54
|
+
extends:
|
55
|
+
file: docker-compose_v2.yml
|
56
|
+
service: mysql
|
57
|
+
image: ${MYSQL_IMAGE}
|
58
|
+
stateful: true
|
59
|
+
secrets:
|
60
|
+
- secret: WP_MYSQL_ROOT_PW
|
61
|
+
name: MYSQL_PASSWORD
|
62
|
+
type: env
|
63
|
+
environment:
|
64
|
+
- INTERNAL_VAR=$$INTERNAL_VAR
|
65
|
+
- RANDOM_VAR=${test_var}
|
66
|
+
- TEST_VAR=$TEST_ENV_VAR
|
@@ -1,5 +1,29 @@
|
|
1
1
|
stack: user/stackname
|
2
2
|
version: 0.1.1
|
3
|
+
variables:
|
4
|
+
wp_pass:
|
5
|
+
type: string # has a callback that writes the value to vault
|
6
|
+
required: true
|
7
|
+
min_length: 10
|
8
|
+
empty_is_nil: true
|
9
|
+
from:
|
10
|
+
env: WORDPRESS_DB_PASSWORD # first try from local env
|
11
|
+
random_string: # if prompt returned nil, generate a random string
|
12
|
+
length: 10
|
13
|
+
charset: ascii_printable
|
14
|
+
to:
|
15
|
+
env: WP_PASS # put it to WP_PASS env variable
|
16
|
+
test_var:
|
17
|
+
type: string
|
18
|
+
from:
|
19
|
+
random_string:
|
20
|
+
length: 16
|
21
|
+
charset: hex
|
22
|
+
to:
|
23
|
+
env: test_var
|
24
|
+
TEST_ENV_VAR: # the default from/to is to set/read env of the option name
|
25
|
+
type: :string
|
26
|
+
|
3
27
|
services:
|
4
28
|
wordpress:
|
5
29
|
extends:
|
@@ -9,6 +33,13 @@ services:
|
|
9
33
|
stateful: true
|
10
34
|
environment:
|
11
35
|
- WORDPRESS_DB_PASSWORD=${STACK}_secret
|
36
|
+
secrets:
|
37
|
+
- secret: WP_ADMIN_PASSWORD
|
38
|
+
name: WORDPRESS_PASSWORD
|
39
|
+
type: env
|
40
|
+
- secret: FOO
|
41
|
+
name: FOOFOO
|
42
|
+
type: env
|
12
43
|
instances: 2
|
13
44
|
deploy:
|
14
45
|
strategy: ha
|
@@ -18,5 +49,11 @@ services:
|
|
18
49
|
service: mysql
|
19
50
|
image: ${MYSQL_IMAGE}
|
20
51
|
stateful: true
|
52
|
+
secrets:
|
53
|
+
- secret: WP_MYSQL_ROOT_PW
|
54
|
+
name: MYSQL_PASSWORD
|
55
|
+
type: env
|
21
56
|
environment:
|
22
57
|
- INTERNAL_VAR=$$INTERNAL_VAR
|
58
|
+
- RANDOM_VAR=${test_var}
|
59
|
+
- TEST_VAR=$TEST_ENV_VAR
|
@@ -171,7 +171,7 @@ describe Kontena::Cli::Apps::DeployCommand do
|
|
171
171
|
'name' => 'kontena-test-mysql',
|
172
172
|
'image' => 'mysql:5.6',
|
173
173
|
'env' => ['MYSQL_ROOT_PASSWORD=kontena-test_secret'],
|
174
|
-
'
|
174
|
+
'instances' => nil,
|
175
175
|
'stateful' => true,
|
176
176
|
}
|
177
177
|
expect(subject).to receive(:create_service).with(duck_type(:access_token), '1', hash_including(data))
|
@@ -186,7 +186,7 @@ describe Kontena::Cli::Apps::DeployCommand do
|
|
186
186
|
'name' => 'kontena-test-wordpress',
|
187
187
|
'image' => 'wordpress:4.1',
|
188
188
|
'env' => ['WORDPRESS_DB_PASSWORD=kontena-test_secret'],
|
189
|
-
'
|
189
|
+
'instances' => 2,
|
190
190
|
'stateful' => true,
|
191
191
|
'strategy' => 'ha',
|
192
192
|
'links' => [{ 'name' => 'kontena-test-mysql', 'alias' => 'mysql' }],
|
@@ -7,13 +7,11 @@ describe Kontena::Cli::Stacks::ListCommand do
|
|
7
7
|
|
8
8
|
describe '#execute' do
|
9
9
|
it 'requires api url' do
|
10
|
-
expect(subject).to
|
11
|
-
subject.run([])
|
10
|
+
expect(subject.class.requires_current_master).to be_truthy
|
12
11
|
end
|
13
12
|
|
14
13
|
it 'requires token' do
|
15
|
-
expect(subject).to
|
16
|
-
subject.run([])
|
14
|
+
expect(subject.class.requires_current_master_token).to be_truthy
|
17
15
|
end
|
18
16
|
|
19
17
|
it 'fetches stacks from master' do
|