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
@@ -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
- from['environment'],
20
- service_config['environment']
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'] = {} unless 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
- env_vars = to || []
42
- if from
43
- from.each do |env|
44
- env_vars << env unless to && to.find do |key|
45
- key.split('=').first == env.split('=').first
46
- end
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
- secrets = to || []
57
- if from
58
- from.each do |from_secret|
59
- secrets << from_secret unless to && to.any? do |to_secret|
60
- to_secret['secret'] == from_secret['secret']
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
- secrets
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
- args = to || {}
69
- if from
70
- from.each do |k,v|
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("grids/#{current_grid}/secrets/#{name}", data)
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
- spinner "Writing #{name.colorize(:cyan)} to the vault " do
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,5 @@
1
+ wordpress:
2
+ stateful: 'true'
3
+ environment:
4
+ WORDPRESS_DB_PASSWORD: ${STACK}_secret
5
+
@@ -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
- 'container_count' => nil,
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
- 'container_count' => 2,
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 receive(:require_api_url).once
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 receive(:require_token).and_return(token)
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