decking 0.0.2

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.
@@ -0,0 +1,105 @@
1
+ module Decking
2
+ module Helpers
3
+ extend self
4
+
5
+ CONSOLE_LENGTH=80
6
+
7
+ def run_with_progress(title, &block)
8
+ command = Thread.new(&block).tap{ |t| t.abort_on_exception = true}
9
+
10
+ progress = Thread.new do
11
+ opts = { title: title,
12
+ total: nil,
13
+ length: CONSOLE_LENGTH,
14
+ format: '%t%B',
15
+ progress_mark: ' ',
16
+ unknown_progress_animation_steps: ['.. .', '... ', ' ... ', ' ...', '. ..'] }
17
+ progressbar = ProgressBar.create opts
18
+
19
+ begin
20
+ loop do
21
+ progressbar.increment
22
+ sleep 0.5
23
+ end
24
+ rescue RuntimeError => e
25
+ if e.message == 'Shutdown'
26
+ progressbar.total = 100
27
+ progressbar.format '%t ' + "\u2713".green
28
+ progressbar.finish
29
+ else
30
+ raise RuntimeError e
31
+ end
32
+ end
33
+ end.tap {|t| t.abort_on_exception = true }
34
+
35
+ command.join
36
+ progress.raise 'Shutdown'
37
+ progress.join
38
+ finished = true
39
+ rescue Interrupt
40
+ clear_progressline
41
+ puts "I know you did't mean to do that... try again if you really do".yellow
42
+ rescue Exception => e
43
+ clear_progressline
44
+ puts e.class
45
+ puts e.message
46
+ puts e.backtrace.inspect
47
+ exit
48
+ ensure
49
+ begin
50
+ unless finished
51
+ command.join
52
+ progress.raise 'Shutdown'
53
+ progress.join
54
+ end
55
+ rescue Interrupt
56
+ puts "Caught second interrupt, exiting...".red
57
+ exit
58
+ rescue SystemExit
59
+ puts "Caught SystemExit. Exiting...".red
60
+ exit
61
+ end
62
+ end
63
+
64
+ def run_with_threads_multiplexed method, containers, *args
65
+ clear_progressline
66
+ threads = Array.new
67
+ containers.map do |name, container|
68
+ threads << Thread.new do
69
+ container.method(method).call(*args)
70
+ sleep 0.1
71
+ end
72
+ end
73
+ threads.map { |thread| thread.join }
74
+ rescue Interrupt
75
+ threads.map { |thread| thread.kill }
76
+ end
77
+
78
+ def clear_progressline
79
+ $stdout.print " " * CONSOLE_LENGTH + "\r"
80
+ #$stdout.print "\n"
81
+ end
82
+ end
83
+ end
84
+
85
+ class String
86
+ def black; "\033[30m#{self}\033[0m"; end
87
+ def red; "\033[31m#{self}\033[0m"; end
88
+ def green; "\033[32m#{self}\033[0m"; end
89
+ def yellow; "\033[33m#{self}\033[0m"; end
90
+ def brown; "\033[33m#{self}\033[0m"; end
91
+ def blue; "\033[34m#{self}\033[0m"; end
92
+ def magenta; "\033[35m#{self}\033[0m"; end
93
+ def cyan; "\033[36m#{self}\033[0m"; end
94
+ def gray; "\033[37m#{self}\033[0m"; end
95
+ def bg_black; "\033[40m#{self}\033[0m"; end
96
+ def bg_red; "\033[41m#{self}\033[0m"; end
97
+ def bg_green; "\033[42m#{self}\033[0m"; end
98
+ def bg_brown; "\033[43m#{self}\033[0m"; end
99
+ def bg_blue; "\033[44m#{self}\033[0m"; end
100
+ def bg_magenta; "\033[45m#{self}\033[0m"; end
101
+ def bg_cyan; "\033[46m#{self}\033[0m"; end
102
+ def bg_gray; "\033[47m#{self}\033[0m"; end
103
+ def bold; "\033[1m#{self}\033[22m"; end
104
+ def reverse_color; "\033[7m#{self}\033[27m"; end
105
+ end
File without changes
File without changes
@@ -0,0 +1,48 @@
1
+ module Decking
2
+ class Image
3
+ include Decking::Helpers
4
+ class << self
5
+ include Decking::Helpers
6
+ include Enumerable
7
+
8
+ #def delete_all ; map{|n, c| c.delete }; end
9
+ #def delete_all!; map{|n, c| c.delete! }; end
10
+
11
+ def images
12
+ @images ||= Hash.new
13
+ end
14
+
15
+ def instances
16
+ @instances ||= Hash.new
17
+ end
18
+
19
+ def add params
20
+ images.update params.name => params
21
+ self[params.name]
22
+ end
23
+
24
+ def [](name)
25
+ instances[name] ||= new(name, @images[name])
26
+ end
27
+
28
+ def each &block
29
+ @instances.each(&block)
30
+ end
31
+ end
32
+
33
+ attr_reader :name, :config
34
+
35
+ def initialize name, params
36
+ @name = name
37
+ @config = params
38
+ end
39
+
40
+ def method_missing method, *args, &block
41
+ if config.key? method
42
+ config[method]
43
+ else
44
+ super
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,166 @@
1
+ module Decking
2
+ module Parser
3
+ # Singleton Method: https://practicingruby.com/articles/ruby-and-the-singleton-pattern-dont-get-along
4
+ extend self
5
+
6
+ attr_accessor :config, :config_path
7
+
8
+ def config_file config_file
9
+ config_file ||= 'decking.yaml'
10
+
11
+ @config = Hashie::Mash.new(YAML.load_file(config_file))
12
+ @config_path = File.realpath(config_file)
13
+
14
+ confirm_requirements
15
+
16
+ end
17
+
18
+ def print
19
+ puts config.to_yaml
20
+ end
21
+
22
+ def parse cluster
23
+ parse_images
24
+ parse_containers
25
+ parse_clusters
26
+ parse_groups
27
+ merge_cluster_config cluster
28
+ end
29
+
30
+ private
31
+
32
+ def confirm_requirements
33
+ raise "No Containers Defined" unless config.containers?
34
+ raise "No Clusters Defined" unless config.clusters?
35
+ raise "No Images Defined" unless config.images?
36
+ end
37
+
38
+ def parse_images
39
+ config.images.each do |key, val|
40
+ if val.nil?
41
+ config.images[key] = Hashie::Mash.new
42
+ config.images[key].name = "#{key}:latest"
43
+ elsif val.is_a?(String)
44
+ config.images[key] = Hashie::Mash.new
45
+ config.images[key].name = val
46
+ end
47
+ config.images[key].tag ||= "latest"
48
+ end
49
+ end
50
+
51
+ def parse_containers
52
+ config.containers.each do |key, val|
53
+ config.containers[key] ||= Hashie::Mash.new
54
+ config.containers[key].links ||= Array.new
55
+ config.containers[key].binds ||= Array.new
56
+ config.containers[key].lxc_conf ||= Array.new
57
+ config.containers[key].domainname ||= ""
58
+ config.containers[key].command ||= ""
59
+ config.containers[key].entrypoint ||= nil
60
+ config.containers[key].memory ||= 0
61
+ config.containers[key].memory_swap ||= 0
62
+ config.containers[key].cpu_shares ||= 0
63
+ config.containers[key].cpu_set ||= ""
64
+ config.containers[key].attach_stdout ||= false
65
+ config.containers[key].attach_stderr ||= false
66
+ config.containers[key].attach_stdin ||= false
67
+ config.containers[key].tty ||= false
68
+ config.containers[key].open_stdin ||= false
69
+ config.containers[key].stdin_once ||= false
70
+ config.containers[key].volumes_from ||= Array.new
71
+ config.containers[key].image ||= key
72
+ config.containers[key].port ||= Array.new
73
+ config.containers[key].aliases ||= Array.new
74
+ config.containers[key].data ||= false
75
+ config.containers[key].hostname ||= key
76
+ config.containers[key].links.each_with_index do |v, idx|
77
+ config.containers[key].links[idx] = resolve_dependency v unless v.instance_of? Hash
78
+ end
79
+ config.containers[key].volumes_from.each_with_index do |v, idx|
80
+ unless config.containers.key? v
81
+ raise "'volumes_from' dependency '" + v + "' of container '" + key + "' does not exist"
82
+ end
83
+ end
84
+ end
85
+ end
86
+
87
+ def parse_clusters
88
+ config.clusters.each do |key, val|
89
+ if config.clusters[key].instance_of? Array
90
+ cont_ar = config.clusters[key]
91
+ config.clusters[key] = Hashie::Mash.new
92
+ config.clusters[key].containers = cont_ar
93
+ end
94
+ if (config.clusters[key].key? 'group') && (!config.groups.key?(config.clusters[key].group))
95
+ raise "Cluster '" + key + "' references invalid group '" + config.clusters[key].group
96
+ end
97
+ if (!config.clusters[key].key? 'group') && (config.groups.key? key)
98
+ config.clusters[key].group = key
99
+ end
100
+
101
+ raise "Cluster '" + key + "' is empty" unless config.clusters[key].key? "containers"
102
+ raise "Cluster '" + key + "' containers should be an Array" unless config.clusters[key].containers.instance_of? Array
103
+ end
104
+ end
105
+
106
+ def parse_groups
107
+ config.groups.each do |key, val|
108
+ config.groups[key] = Hashie::Mash.new if config.groups[key].nil?
109
+ config.groups[key].options = Hashie::Mash.new unless config.groups[key].key? 'options'
110
+ config.groups[key].containers = Hashie::Mash.new unless config.groups[key].key? 'containers'
111
+ config.groups[key].containers.each do |c_key, c_val|
112
+ if config.groups[key].containers[c_key].key? 'links'
113
+ config.groups[key].containers[c_key].links.each_with_index do |v, idx|
114
+ config.groups[key].containers[c_key].links[idx] = resolve_dependency v
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
120
+
121
+ def merge_cluster_config cluster
122
+ raise "Cluster '" + cluster + "' doesn't exist" unless config.clusters.key? cluster
123
+ c = Hashie::Mash.new
124
+ c.containers = Hash.new
125
+
126
+ # Merge primary container configs
127
+ config.clusters[cluster].containers.each_with_index do |key, idx|
128
+ c.containers[key] = config.containers[key]
129
+ end
130
+
131
+ c.containers.each do |k, v|
132
+ # Merge Global Overrides
133
+ c.containers[k] = c.containers[k].deep_merge(config.global) if config.key? 'global'
134
+ # Merge Group Overrides
135
+ c.containers[k] = c.containers[k].deep_merge(config.groups[config.clusters[cluster].group].options)
136
+ # Merge Group Container Overrides
137
+ if config.groups[config.clusters[cluster].group].containers.key? k
138
+ c.containers[k] = c.containers[k].deep_merge(config.groups[config.clusters[cluster].group].containers[k])
139
+ end
140
+ c.containers[k].name = k + '.' + cluster
141
+ c.containers[k].env.CONTAINER_NAME = k + '.' + cluster
142
+ c.containers[k].domainname = cluster + '.' + config.global.domainname if config.key?('global') && config.global.key?('domainname')
143
+ end
144
+ images = self.config.images
145
+ group = self.config.clusters[cluster].group
146
+ self.config = c
147
+ self.config.images = images
148
+ self.config.cluster = cluster
149
+ self.config.group = group
150
+ self.config
151
+ end
152
+
153
+ def resolve_dependency dep
154
+ ret = Hash.new
155
+ spl = dep.split ':'
156
+ ret["dep"] = spl[0]
157
+ unless spl[1].nil?
158
+ ret["alias"] = spl[1]
159
+ else
160
+ ret["alias"] = spl[0]
161
+ end
162
+ ret
163
+ end
164
+
165
+ end
166
+ end
@@ -0,0 +1,3 @@
1
+ module Decking
2
+ VERSION = "0.0.2"
3
+ end
data/lib/decking.rb ADDED
@@ -0,0 +1,25 @@
1
+ require 'hashie'
2
+ require 'docker'
3
+ Docker.validate_version!
4
+ require "ruby-progressbar"
5
+ require 'thread'
6
+ require 'yaml'
7
+
8
+ require 'log4r'
9
+ require 'log4r/formatter/patternformatter'
10
+ require 'log4r/outputter/syslogoutputter'
11
+ require 'syslog'
12
+ include Syslog::Constants
13
+
14
+ Log4r::Logger.global.level = Log4r::ALL
15
+ Log4r::StdoutOutputter.new('stdout', formatter: Log4r::PatternFormatter.new( pattern: '%d (%C) %l: %m', date_pattern: '%FT%T%:z' ))
16
+ Log4r::SyslogOutputter.new('decking', logopt: LOG_CONS | LOG_PID , facility: LOG_USER, formatter: Log4r::PatternFormatter.new( date_method: 'usec', pattern: '(%C} %l: %m'))
17
+ Log4r::Logger.new('decking')
18
+ Log4r::Logger['decking'].add('decking')
19
+ Log4r::Logger['decking'].add('stdout')
20
+ Log4r::Logger['decking'].debug "Initialized #{__FILE__}"
21
+
22
+ require_relative "decking/version"
23
+ require_relative "decking/helpers"
24
+ require_relative "decking/parser"
25
+ require_relative "decking/containers"
@@ -0,0 +1,60 @@
1
+ require_relative 'spec_helper'
2
+
3
+ describe Decking::Parser do
4
+ before(:each) { Decking::Parser.config_file('spec/resources/decking.yaml') }
5
+
6
+ describe '#parse' do
7
+
8
+ it 'sets image value to key when blank' do
9
+ Decking::Parser.parse 'qa'
10
+ expect(Decking::Parser.config.images.blank.name).to eq("blank:latest")
11
+ expect(Decking::Parser.config.images.base.name).to eq("fail")
12
+ expect(Decking::Parser.config.images.repos.name).to eq("repos:v1.02")
13
+ expect(Decking::Parser.config.images["eds-webapp"].name).to eq("eds-webapp:latest")
14
+ end
15
+
16
+ it 'sets image key in containers to container name when missing' do
17
+ Decking::Parser.parse 'qa'
18
+ expect(Decking::Parser.config.containers.blank_container_image.image).to eq("blank_container_image")
19
+ end
20
+
21
+ it 'resolves links into container and alias' do
22
+ Decking::Parser.parse 'qa'
23
+ expect(Decking::Parser.config.containers.repos.links[0]).to eq({"dep" => "elasticsearch", "alias" => "es"})
24
+ expect(Decking::Parser.config.containers.repos.links[1]).to eq({"dep" => "config", "alias" => "config"})
25
+ end
26
+
27
+ it 'ensures that volumes_from links exist' do
28
+ Decking::Parser.config.containers.repos.volumes_from = ["not-exists"]
29
+ expect{Decking::Parser.parse 'qa'}.to raise_error(RuntimeError)
30
+ end
31
+
32
+ it 'raises an error when a cluster group does not exist' do
33
+ Decking::Parser.config.clusters.no_group = Hashie::Mash.new
34
+ Decking::Parser.config.clusters.no_group.group = "no_group"
35
+ expect{Decking::Parser.parse 'qa'}.to raise_error(RuntimeError)
36
+ end
37
+
38
+ it 'resolves links into container and alias' do
39
+ Decking::Parser.parse 'qa-mod'
40
+ expect(Decking::Parser.config.containers["webapp-admin"].links[0]).to eq({"dep" => "elasticsearch", "alias" => "elasticsearch"})
41
+ end
42
+
43
+ it 'appropriately handles overrides' do
44
+ Decking::Parser.parse 'qa'
45
+ expect(Decking::Parser.config.containers.keys).to eq(['blank_container_image', 'repos', 'config', 'webapp-main', 'webapp-admin'])
46
+ expect(Decking::Parser.config.containers['webapp-admin'].env.WEBAPP).to eq('admin')
47
+ expect(Decking::Parser.config.containers['webapp-admin'].env.ENVIRONMENT).to eq('qa')
48
+ expect(Decking::Parser.config.containers['webapp-admin'].env.TEST_VAR).to eq('test')
49
+ expect(Decking::Parser.config.containers['webapp-admin'].env.GITHUB_REPO).to eq('test')
50
+ expect(Decking::Parser.config.containers['webapp-admin'].env.AWS_ACCESS_KEY).to eq('key')
51
+ expect(Decking::Parser.config.containers['webapp-admin'].env.AWS_SECRET_ACCESS_KEY).to eq('secret2')
52
+ expect(Decking::Parser.config.containers['webapp-admin'].image).to eq('webapp')
53
+ expect(Decking::Parser.config.containers['webapp-admin'].volumes_from).to eq(['repos','config'])
54
+ expect(Decking::Parser.config.containers['webapp-admin'].port).to eq(['82:80'])
55
+ expect(Decking::Parser.config.group).to eq('qa')
56
+ end
57
+ end
58
+
59
+ end
60
+
@@ -0,0 +1,62 @@
1
+ # vim: set foldmethod=indent
2
+ ---
3
+
4
+ images:
5
+ ubuntu:
6
+ eds-webapp:
7
+ name: test
8
+
9
+ containers:
10
+ ubuntu-hello-world:
11
+ image: ubuntu
12
+ port:
13
+ - 82:80
14
+ command: /bin/sh -c "while true; do echo Hello world; sleep 0.5; done"
15
+ ubuntu-hello-frank:
16
+ image: ubuntu
17
+ command: /bin/sh -c "while true; do echo Hello frank; sleep 0.5; done"
18
+ ubuntu-hello-josh:
19
+ image: ubuntu
20
+ command: /bin/sh -c "while true; do echo Hello josh; sleep 0.5; done"
21
+ ubuntu-hello-stderr:
22
+ image: ubuntu
23
+ command: /bin/sh -c "while true; do echo error will robinson >&2; sleep 0.5; done"
24
+ hello-chris:
25
+ image: ubuntu
26
+ command: /bin/sh -c "while true; do echo 'Hello Chris!!'; sleep 0.5; done"
27
+ hello-brandon:
28
+ image: ubuntu
29
+ command: /bin/sh -c "while true; do echo 'Hello Brandon!!'; sleep 0.5; done"
30
+
31
+ clusters:
32
+ container-tests:
33
+ - hello-chris
34
+ - hello-brandon
35
+ - ubuntu-hello-stderr
36
+
37
+ groups:
38
+ container-tests:
39
+ options:
40
+ env:
41
+ OPTIONS_ENV: false
42
+ OPTIONS_ENV_OVERRIDE: 'this will not be the value'
43
+ GLOBAL_OVERRIDE_AWS_REGION: 'us-east-1'
44
+ containers:
45
+ hello-brandon:
46
+ env:
47
+ OPTIONS_ENV_OVERRIDE: 'this is the real value'
48
+ ubuntu:
49
+ port:
50
+ - 83:81
51
+ - 82:80
52
+ env:
53
+ CONTAINERS_OPTS_ENV: false
54
+ OPTIONS_ENV_OVERRIDE: 'this is the real value'
55
+
56
+
57
+ global:
58
+ env:
59
+ AWS_ACCESS_KEY: key
60
+ AWS_SECRET_ACCESS_KEY: secret
61
+ GLOBAL_OVERRIDE_AWS_REGION: 'us-midwest-7'
62
+ domainname: qa.randywallace.com
@@ -0,0 +1,109 @@
1
+ # vim: set foldmethod=indent
2
+ ---
3
+
4
+ images:
5
+ base: fail
6
+ config:
7
+ repos:
8
+ name: repos:v1.02
9
+ eds-webapp:
10
+ elasticsearch:
11
+ tag: 1.5.0
12
+ blank:
13
+
14
+ containers:
15
+ ubuntu:
16
+ port:
17
+ - 82:80
18
+ command: "/bin/sh -c 'while true; do echo Hello world; sleep 1; done'"
19
+ blank_container_image:
20
+ repos:
21
+ image: repos
22
+ data: true
23
+ links:
24
+ - elasticsearch:es
25
+ - config
26
+ config:
27
+ image: config
28
+ data: true
29
+ elasticsearch:
30
+ image: elasticsearch
31
+ port:
32
+ - 9200:9200
33
+ volumes_from:
34
+ - repos
35
+ webapp-main:
36
+ image: webapp
37
+ volumes_from:
38
+ - repos
39
+ - config
40
+ port:
41
+ - 80:80
42
+ extra: webapp-main
43
+ webapp-admin:
44
+ image: webapp
45
+ volumes_from:
46
+ - repos
47
+ - config
48
+ port:
49
+ - 81:80
50
+ extra: webapp-admin
51
+
52
+ clusters:
53
+ qa:
54
+ - blank_container_image
55
+ - repos
56
+ - config
57
+ - webapp-main
58
+ - webapp-admin
59
+ qa-mod:
60
+ - repos
61
+ - config
62
+ - elasticsearch
63
+ - webapp-admin
64
+ container-tests:
65
+ - ubuntu
66
+
67
+ groups:
68
+ qa:
69
+ options:
70
+ env:
71
+ ENVIRONMENT: qa
72
+ TAG: v1.0.0
73
+ GITHUB_ACCOUNT: randywallace
74
+ GITHUB_REPO: test
75
+ GITHUB_BRANCH: master
76
+ GITHUB_TOKEN: token
77
+ TEST_VAR: test
78
+ containers:
79
+ webapp-admin:
80
+ env:
81
+ WEBAPP: admin
82
+ AWS_SECRET_ACCESS_KEY: secret2
83
+ port:
84
+ - 82:80
85
+ qa-mod:
86
+ options:
87
+ env:
88
+ ENVIRONMENT: qa-admin
89
+ TAG: v1.0.0
90
+ WEBAPP: original
91
+ containers:
92
+ webapp-admin:
93
+ port:
94
+ - 82:80
95
+ links:
96
+ - "elasticsearch:elasticsearch"
97
+ env:
98
+ WEBAPP: replace
99
+ container-tests:
100
+ options:
101
+ env:
102
+ ENVIRONMENT: qa-admin
103
+ TAG: v1.0.0
104
+
105
+ global:
106
+ env:
107
+ AWS_ACCESS_KEY: key
108
+ AWS_SECRET_ACCESS_KEY: secret
109
+ domainname: qa.randywallace.com
@@ -0,0 +1,2 @@
1
+ require 'pry'
2
+ require 'decking'
File without changes