decking 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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