percheron 0.6.4 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/.cane +4 -0
  3. data/.gitignore +1 -0
  4. data/.rubocop.yml +31 -0
  5. data/.travis.yml +4 -1
  6. data/Guardfile +3 -3
  7. data/README.md +19 -3
  8. data/Rakefile +24 -1
  9. data/bin/percheron +2 -20
  10. data/lib/percheron/actions/base.rb +0 -39
  11. data/lib/percheron/actions/build.rb +9 -7
  12. data/lib/percheron/actions/create.rb +64 -38
  13. data/lib/percheron/actions/exec.rb +40 -8
  14. data/lib/percheron/actions/exec_local.rb +4 -4
  15. data/lib/percheron/actions/logs.rb +44 -0
  16. data/lib/percheron/actions/purge.rb +15 -4
  17. data/lib/percheron/actions/recreate.rb +15 -65
  18. data/lib/percheron/actions/restart.rb +2 -1
  19. data/lib/percheron/actions/shell.rb +27 -0
  20. data/lib/percheron/actions/start.rb +10 -7
  21. data/lib/percheron/actions.rb +2 -1
  22. data/lib/percheron/commands/abstract.rb +20 -8
  23. data/lib/percheron/commands/build.rb +13 -0
  24. data/lib/percheron/commands/console.rb +53 -0
  25. data/lib/percheron/commands/create.rb +3 -3
  26. data/lib/percheron/commands/list.rb +7 -3
  27. data/lib/percheron/commands/logs.rb +16 -0
  28. data/lib/percheron/commands/main.rb +5 -2
  29. data/lib/percheron/commands/purge.rb +2 -2
  30. data/lib/percheron/commands/recreate.rb +3 -11
  31. data/lib/percheron/commands/restart.rb +2 -2
  32. data/lib/percheron/commands/shell.rb +16 -0
  33. data/lib/percheron/commands/start.rb +2 -2
  34. data/lib/percheron/commands/stop.rb +2 -2
  35. data/lib/percheron/commands.rb +3 -0
  36. data/lib/percheron/config.rb +64 -3
  37. data/lib/percheron/config_delegator.rb +0 -1
  38. data/lib/percheron/container.rb +95 -38
  39. data/lib/percheron/core_extensions.rb +1 -4
  40. data/lib/percheron/docker_connection.rb +5 -1
  41. data/lib/percheron/formatters/stack/table.rb +24 -12
  42. data/lib/percheron/logger.rb +21 -0
  43. data/lib/percheron/metastore.rb +1 -0
  44. data/lib/percheron/null_stack.rb +5 -0
  45. data/lib/percheron/oh_dear.rb +15 -7
  46. data/lib/percheron/stack.rb +77 -72
  47. data/lib/percheron/validators/config.rb +1 -1
  48. data/lib/percheron/validators/container.rb +31 -11
  49. data/lib/percheron/validators/stack.rb +3 -5
  50. data/lib/percheron/version.rb +1 -1
  51. data/lib/percheron.rb +2 -0
  52. data/percheron.gemspec +4 -1
  53. metadata +54 -4
  54. data/assets/percheron.png +0 -0
  55. data/lib/percheron/actions/rename.rb +0 -65
@@ -4,94 +4,44 @@ module Percheron
4
4
 
5
5
  include Base
6
6
 
7
- def initialize(container, force_recreate: false, delete: false)
7
+ def initialize(container, start: false)
8
8
  @container = container
9
- @force_recreate = force_recreate
10
- @delete = delete
9
+ @start = start
11
10
  end
12
11
 
13
12
  def execute!
14
13
  results = []
15
14
  if recreate?
16
15
  results << recreate!
16
+ results << start!
17
17
  else
18
- unless dockerfile_md5s_match?
19
- $logger.warn "Container '#{container.name}' MD5's do not match, consider recreating (bump the version!)"
20
- else
21
- $logger.info "Container '#{container.name}' does not need to be recreated"
22
- end
18
+ inform!
23
19
  end
24
20
  results.compact.empty? ? nil : container
25
21
  end
26
22
 
27
23
  private
28
24
 
29
- attr_reader :container, :force_recreate, :delete
30
-
31
- alias_method :force_recreate?, :force_recreate
32
- alias_method :delete?, :delete
33
-
34
- def temporary_name
35
- '%s_wip' % container.name
36
- end
37
-
38
- def stored_dockerfile_md5
39
- container.dockerfile_md5 || container.current_dockerfile_md5
40
- end
41
-
42
- def temporary_container_exists?
43
- !!Docker::Container.get(temporary_name)
44
- rescue Docker::Error::NotFoundError
45
- false
46
- end
25
+ attr_reader :container, :start
26
+ alias_method :start?, :start
47
27
 
48
28
  def recreate?
49
- force_recreate? || (!dockerfile_md5s_match? && versions_mismatch? && container.auto_recreate?)
50
- end
51
-
52
- def versions_mismatch?
53
- container.version > container.built_version
29
+ !container.versions_match? || !container.dockerfile_md5s_match?
54
30
  end
55
31
 
56
- def dockerfile_md5s_match?
57
- stored_dockerfile_md5 == container.current_dockerfile_md5
32
+ def inform!
33
+ return nil unless container.dockerfile_md5s_match?
34
+ $logger.info "Container '#{container.name}' - No Dockerfile changes or version bump"
58
35
  end
59
36
 
60
37
  def recreate!
61
- $logger.debug "Container '#{container.name}' exists and will be recreated"
62
-
63
- unless temporary_container_exists?
64
- delete_container_and_image! if delete?
65
- create_container!
66
- rename_container!
67
- else
68
- $logger.debug "Not recreating '#{container.name}' container because temporary container '#{temporary_name}' already exists"
69
- end
70
- end
71
-
72
- def delete_container_and_image!
73
- delete_container!
74
- delete_image!
75
- end
76
-
77
- def delete_container!
78
- $logger.info "Deleting '#{container.name}' container"
79
- stop_containers!([ container ])
80
- container.docker_container.remove
81
- end
82
-
83
- def delete_image!
84
- $logger.info "Deleting '#{container.name}' image"
85
- container.image.remove
86
- end
87
-
88
- def create_container!
89
- opts = { create: { 'name' => temporary_name } }
90
- Create.new(container, recreate: true).execute!(opts)
38
+ $logger.debug "Container '#{container.name}' exists but will be recreated"
39
+ Purge.new(container).execute!
40
+ Create.new(container).execute!
91
41
  end
92
42
 
93
- def rename_container!
94
- Rename.new(container, temporary_name, container.name).execute!
43
+ def start!
44
+ Start.new(container).execute! if start?
95
45
  end
96
46
 
97
47
  end
@@ -24,7 +24,8 @@ module Percheron
24
24
  end
25
25
 
26
26
  def start!
27
- Start.new(container, dependant_containers: container.dependant_containers.values).execute!
27
+ opts = { dependant_containers: container.startable_dependant_containers.values }
28
+ Start.new(container, opts).execute!
28
29
  end
29
30
 
30
31
  end
@@ -0,0 +1,27 @@
1
+ module Percheron
2
+ module Actions
3
+ class Shell
4
+ include Base
5
+
6
+ DEFAULT_SHELL = '/bin/sh'
7
+
8
+ def initialize(container, shell: DEFAULT_SHELL)
9
+ @container = container
10
+ @shell = shell
11
+ end
12
+
13
+ def execute!
14
+ $logger.debug "Executing a bash shell on '#{container.name}' container"
15
+ exec!
16
+ end
17
+
18
+ private
19
+
20
+ attr_reader :container, :shell
21
+
22
+ def exec!
23
+ system('docker exec -ti %s %s' % [ container.full_name, shell ])
24
+ end
25
+ end
26
+ end
27
+ end
@@ -4,42 +4,45 @@ module Percheron
4
4
 
5
5
  include Base
6
6
 
7
- def initialize(container, dependant_containers: [], exec_scripts: true)
7
+ def initialize(container, dependant_containers: [], cmd: false, exec_scripts: true)
8
8
  @container = container
9
9
  @dependant_containers = dependant_containers
10
+ @cmd = cmd
10
11
  @exec_scripts = exec_scripts
11
12
  end
12
13
 
13
14
  def execute!
14
15
  results = []
15
- results << create! unless container.exists?
16
+ results << create!
16
17
  unless container.running?
17
18
  results << start!
18
- results << execute_post_start_scripts! if exec_scripts?
19
+ results << execute_post_start_scripts!
19
20
  end
20
21
  results.compact.empty? ? nil : container
21
22
  end
22
23
 
23
24
  private
24
25
 
25
- attr_reader :container, :dependant_containers, :exec_scripts
26
+ attr_reader :container, :dependant_containers, :cmd, :exec_scripts
26
27
 
27
28
  def exec_scripts?
28
29
  !container.post_start_scripts.empty? && exec_scripts
29
30
  end
30
31
 
31
32
  def create!
32
- $logger.info "Creating '#{container.name}' container as it doesn't exist"
33
- Create.new(container).execute!
33
+ return nil if container.exists?
34
+ Create.new(container, cmd: cmd, exec_scripts: exec_scripts).execute!
34
35
  end
35
36
 
36
37
  def start!
38
+ return nil if !container.startable? || container.running?
37
39
  $logger.info "Starting '#{container.name}' container"
38
40
  container.docker_container.start!
39
41
  end
40
42
 
41
43
  def execute_post_start_scripts!
42
- Exec.new(container, dependant_containers, container.post_start_scripts, 'POST start').execute!
44
+ scripts = container.post_start_scripts
45
+ Exec.new(container, dependant_containers, scripts, 'POST start').execute! if exec_scripts?
43
46
  end
44
47
 
45
48
  end
@@ -4,11 +4,12 @@ require 'percheron/actions/start'
4
4
  require 'percheron/actions/restart'
5
5
  require 'percheron/actions/create'
6
6
  require 'percheron/actions/recreate'
7
- require 'percheron/actions/rename'
8
7
  require 'percheron/actions/build'
9
8
  require 'percheron/actions/purge'
10
9
  require 'percheron/actions/exec'
11
10
  require 'percheron/actions/exec_local'
11
+ require 'percheron/actions/shell'
12
+ require 'percheron/actions/logs'
12
13
 
13
14
  module Percheron
14
15
  module Actions
@@ -2,32 +2,44 @@ module Percheron
2
2
  module Commands
3
3
  class Abstract < Clamp::Command
4
4
 
5
- DEFAULT_CONFIG_FILE = '.percheron.yml'
5
+ option([ '-c', '--config_file' ], 'CONFIG', 'Config file',
6
+ default: Config::DEFAULT_CONFIG_FILE)
6
7
 
7
- option [ '-c', '--config_file' ], 'CONFIG', 'Configuration file', default: DEFAULT_CONFIG_FILE
8
-
9
- option '--version', :flag, 'show version' do
8
+ option('--version', :flag, 'show version') do
10
9
  puts Percheron::VERSION
11
10
  exit(0)
12
11
  end
13
12
 
14
13
  def self.default_parameters!
15
- parameter('STACK_NAME', 'stack name', required: false)
16
- parameter('CONTAINER_NAMES', 'container names', required: false, default: []) do |container_names|
14
+ parameter('STACK_NAME', 'stack name', required: true)
15
+ parameter('CONTAINER_NAMES', 'container names', default: [],
16
+ required: false) do |container_names|
17
17
  container_names.split(/,/)
18
18
  end
19
19
  end
20
20
 
21
+ def self.default_create_parameters!
22
+ default_parameters!
23
+ option('--start', :flag, 'Start container', default: false)
24
+ end
25
+
26
+ def execute
27
+ stack.valid?
28
+ rescue => e
29
+ signal_usage_error(e.message)
30
+ end
31
+
21
32
  def stack
33
+ return NullStack.new if stack_name.nil?
22
34
  Percheron::Stack.new(config, stack_name)
23
35
  end
24
36
 
25
37
  def default_config_file
26
- ENV.fetch('PERCHERON_CONFIG', DEFAULT_CONFIG_FILE)
38
+ ENV.fetch('PERCHERON_CONFIG', Config::DEFAULT_CONFIG_FILE)
27
39
  end
28
40
 
29
41
  def config
30
- @config ||= Percheron::Config.new(config_file)
42
+ @config ||= Percheron::Config.load!(config_file)
31
43
  rescue Errors::ConfigFileInvalid => e
32
44
  $logger.error e.message
33
45
  exit(1)
@@ -0,0 +1,13 @@
1
+ module Percheron
2
+ module Commands
3
+ class Build < Abstract
4
+
5
+ default_parameters!
6
+
7
+ def execute
8
+ super
9
+ stack.build!(container_names: container_names)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -2,9 +2,62 @@ module Percheron
2
2
  module Commands
3
3
  class Console < Abstract
4
4
 
5
+ parameter('STACK_NAME', 'stack name', required: false)
6
+
5
7
  def execute
8
+ super
9
+ require 'pry-byebug'
6
10
  pry
7
11
  end
12
+
13
+ private
14
+
15
+ def list
16
+ Stack.get(config, stack_name).each do |_, stack|
17
+ puts("\n", Percheron::Formatters::Stack::Table.new(stack).generate)
18
+ end
19
+ nil
20
+ end
21
+
22
+ def logs(container_name, follow: false)
23
+ stack.logs!(container_name, follow: follow)
24
+ nil
25
+ end
26
+
27
+ def shell(container_name)
28
+ stack.shell!(container_name)
29
+ nil
30
+ end
31
+
32
+ def purge(container_names)
33
+ stack.purge!(container_names: [ *container_names ])
34
+ nil
35
+ end
36
+
37
+ def create(container_names, start: false)
38
+ stack.create!(container_names: [ *container_names ], start: start)
39
+ nil
40
+ end
41
+
42
+ def recreate(container_names, start: false)
43
+ stack.create!(container_names: [ *container_names ], start: start)
44
+ nil
45
+ end
46
+
47
+ def start(container_names)
48
+ stack.start!(container_names: [ *container_names ])
49
+ nil
50
+ end
51
+
52
+ def stop(container_names)
53
+ stack.stop!(container_names: [ *container_names ])
54
+ nil
55
+ end
56
+
57
+ def restart(container_names)
58
+ stack.restart!(container_names: [ *container_names ])
59
+ nil
60
+ end
8
61
  end
9
62
  end
10
63
  end
@@ -2,11 +2,11 @@ module Percheron
2
2
  module Commands
3
3
  class Create < Abstract
4
4
 
5
- default_parameters!
5
+ default_create_parameters!
6
6
 
7
7
  def execute
8
- opts = { container_names: container_names }
9
- stack.create!(opts)
8
+ super
9
+ stack.create!(container_names: container_names, start: start?)
10
10
  end
11
11
  end
12
12
  end
@@ -5,9 +5,13 @@ module Percheron
5
5
  parameter('STACK_NAME', 'stack name', required: false)
6
6
 
7
7
  def execute
8
- Stack.get(config, stack_name).each do |stack_name, stack|
9
- puts
10
- puts Percheron::Formatters::Stack::Table.new(stack).generate
8
+ Stack.get(config, stack_name).each do |_, stack|
9
+ begin
10
+ stack.valid?
11
+ puts("\n", Percheron::Formatters::Stack::Table.new(stack).generate)
12
+ rescue Percheron::Errors::StackInvalid => e
13
+ signal_usage_error(e.message)
14
+ end
11
15
  end
12
16
  end
13
17
  end
@@ -0,0 +1,16 @@
1
+ module Percheron
2
+ module Commands
3
+ class Logs < Abstract
4
+
5
+ parameter('STACK_NAME', 'stack name', required: true)
6
+ parameter('CONTAINER_NAME', 'container name', required: true)
7
+
8
+ option('--follow', :flag, 'follow the logs', default: false)
9
+
10
+ def execute
11
+ super
12
+ stack.logs!(container_name, follow: follow?)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -1,14 +1,17 @@
1
1
  module Percheron
2
2
  module Commands
3
3
  class Main < Abstract
4
- subcommand 'list', "List stacks and it's containers", List
4
+ subcommand %w(list status), "List stacks and it's containers", List
5
5
  subcommand 'console', 'Start a pry console session', Console
6
6
  subcommand 'start', 'Start a stack', Start
7
7
  subcommand 'stop', 'Stop a stack', Stop
8
8
  subcommand 'restart', 'Restart a stack', Restart
9
- subcommand 'create', 'Create a stack', Create
9
+ subcommand 'build', 'Build images for a stack', Build
10
+ subcommand 'create', 'Build images and create containers for a stack', Create
10
11
  subcommand 'recreate', 'Recreate a stack', Recreate
11
12
  subcommand 'purge', 'Purge a stack', Purge
13
+ subcommand 'shell', 'Shell into a container', Shell
14
+ subcommand 'logs', 'Show logs for a container', Logs
12
15
  end
13
16
  end
14
17
  end
@@ -5,8 +5,8 @@ module Percheron
5
5
  default_parameters!
6
6
 
7
7
  def execute
8
- opts = { container_names: container_names }
9
- stack.purge!(opts)
8
+ super
9
+ stack.purge!(container_names: container_names)
10
10
  end
11
11
  end
12
12
  end
@@ -2,19 +2,11 @@ module Percheron
2
2
  module Commands
3
3
  class Recreate < Abstract
4
4
 
5
- default_parameters!
6
-
7
- option "--force", :flag, 'Force recreation', default: false
8
- option "--delete", :flag, 'Delete container + image before recreation', default: false
5
+ default_create_parameters!
9
6
 
10
7
  def execute
11
- opts = {
12
- container_names: container_names,
13
- force_recreate: force?,
14
- delete: delete?
15
- }
16
-
17
- stack.recreate!(opts)
8
+ super
9
+ stack.recreate!(container_names: container_names, start: start?)
18
10
  end
19
11
  end
20
12
  end
@@ -5,8 +5,8 @@ module Percheron
5
5
  default_parameters!
6
6
 
7
7
  def execute
8
- opts = { container_names: container_names }
9
- stack.restart!(opts)
8
+ super
9
+ stack.restart!(container_names: container_names)
10
10
  end
11
11
  end
12
12
  end
@@ -0,0 +1,16 @@
1
+ module Percheron
2
+ module Commands
3
+ class Shell < Abstract
4
+
5
+ parameter('STACK_NAME', 'stack name', required: true)
6
+ parameter('CONTAINER_NAME', 'container name', required: true)
7
+
8
+ option('--shell', 'SHELL', 'Shell to use', default: Percheron::Actions::Shell::DEFAULT_SHELL)
9
+
10
+ def execute
11
+ super
12
+ stack.shell!(container_name, shell: shell)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -5,8 +5,8 @@ module Percheron
5
5
  default_parameters!
6
6
 
7
7
  def execute
8
- opts = { container_names: container_names }
9
- stack.start!(opts)
8
+ super
9
+ stack.start!(container_names: container_names)
10
10
  end
11
11
  end
12
12
  end
@@ -5,8 +5,8 @@ module Percheron
5
5
  default_parameters!
6
6
 
7
7
  def execute
8
- opts = { container_names: container_names }
9
- stack.stop!(opts)
8
+ super
9
+ stack.stop!(container_names: container_names)
10
10
  end
11
11
  end
12
12
  end
@@ -8,7 +8,10 @@ require 'percheron/commands/stop'
8
8
  require 'percheron/commands/purge'
9
9
  require 'percheron/commands/console'
10
10
  require 'percheron/commands/create'
11
+ require 'percheron/commands/build'
11
12
  require 'percheron/commands/recreate'
13
+ require 'percheron/commands/shell'
14
+ require 'percheron/commands/logs'
12
15
  require 'percheron/commands/main'
13
16
 
14
17
  module Percheron
@@ -3,19 +3,20 @@ require 'yaml'
3
3
  module Percheron
4
4
  class Config
5
5
 
6
+ DEFAULT_CONFIG_FILE = '.percheron.yml'
7
+
6
8
  extend Forwardable
7
9
 
8
10
  def_delegators :contents, :docker
9
11
 
10
12
  def initialize(file)
11
13
  @file = Pathname.new(file).expand_path
12
- valid?
13
14
  docker_setup!
14
15
  self
15
16
  end
16
17
 
17
18
  def stacks
18
- contents.stacks.to_hash_by_key(:name)
19
+ process_stacks!
19
20
  end
20
21
 
21
22
  def file_base_path
@@ -26,10 +27,71 @@ module Percheron
26
27
  Validators::Config.new(file).valid?
27
28
  end
28
29
 
30
+ def self.load!(config_file = DEFAULT_CONFIG_FILE)
31
+ new(config_file)
32
+ end
33
+
29
34
  private
30
35
 
31
36
  attr_reader :file
32
37
 
38
+ # rubocop:disable Metrics/MethodLength
39
+ def process_stacks! # FIXME: bugs here :(
40
+ stacks_by_name = contents.stacks.to_hash_by_key(:name)
41
+ scanned = scan_container_configs(stacks_by_name)
42
+ stacks_by_name.each do |_, stack|
43
+ stack_containers = stack.containers.each_with_object({}) do |container_config, all|
44
+ if scanned[container_config.name]
45
+ merge(all, container_config, scanned)
46
+ else
47
+ replace_scanned(all, container_config, scanned)
48
+ end
49
+ end
50
+ stack.containers = stack_containers
51
+ end
52
+ end
53
+ # rubocop:enable Metrics/MethodLength
54
+
55
+ def merge(all, container_config, scanned) # FIXME: poor name
56
+ all.merge!(expand_container_config(container_config, scanned[container_config.name]))
57
+ end
58
+
59
+ def replace_scanned(all, container_config, scanned) # FIXME: poor name
60
+ match = container_config.fetch(:dependant_container_names, [])
61
+ unless (match & scanned.keys).empty?
62
+ container_config.dependant_container_names = match.map { |v| scanned[v] }.flatten
63
+ end
64
+ all[container_config.name] = container_config
65
+ end
66
+
67
+ def scan_container_configs(stacks_by_name) # FIXME
68
+ all = {}
69
+ stacks_by_name.each do |_, stack|
70
+ stack.containers.each do |container_config|
71
+ next if container_config.fetch(:instances, 1) == 1
72
+ all[container_config.name] = 1.upto(container_config.instances).map do |number|
73
+ "#{container_config.name}#{number}"
74
+ end
75
+ end
76
+ end
77
+ all
78
+ end
79
+
80
+ def expand_container_config(container_config, new_container_names) # FIXME
81
+ new_container_names.each_with_object({}) do |new_name, all|
82
+ temp_container_config = container_config.dup
83
+ temp_container_config.delete(:instances)
84
+ temp_container_config.pseudo_name = container_config.name
85
+ temp_container_config.name = new_name
86
+ all[new_name] = eval_container_config(temp_container_config)
87
+ end
88
+ end
89
+
90
+ def eval_container_config(container_config)
91
+ template = Liquid::Template.parse(container_config.to_h.to_yaml.to_s)
92
+ YAML.load(template.render(container_config))
93
+ end
94
+
33
95
  def contents
34
96
  Hashie::Mash.new(YAML.load_file(file))
35
97
  end
@@ -37,6 +99,5 @@ module Percheron
37
99
  def docker_setup!
38
100
  Percheron::DockerConnection.new(self).setup!
39
101
  end
40
-
41
102
  end
42
103
  end
@@ -1,6 +1,5 @@
1
1
  module Percheron
2
2
  module ConfigDelegator
3
-
4
3
  def def_config_item_with_default(config, default, *symbols)
5
4
  symbols.each do |symbol|
6
5
  define_method(symbol) do