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