percheron 0.6.4 → 0.7.0

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