nutkins 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 6aa69760600c24b0d1d482aa132627423d82ab80
4
+ data.tar.gz: 38fde0b125bbf671cf093bbe95c0d3b0d94e75c2
5
+ SHA512:
6
+ metadata.gz: d4d063d8d03c2f2930f7f019479dd2a67de7a2687e93193f4f27c2fcd513198b4f77e4521e1399ee950a07ea0c85052078ec7715fe47f1132a38a8125df304b0
7
+ data.tar.gz: 22475d2f4ba6076a2946085c34ff97afd7d74206fd616998c8c18d6877e4dfbb31dd95eab857a3857f5bbb4bf2f60445438934dbc5b9afb315c7c08ba36c3cb9
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2.4
4
+ before_install: gem install bundler -v 1.10.6
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in nutkins.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,10 @@
1
+ # Nutkins
2
+
3
+ [![build status](https://circleci.com/gh/ohjames/nutkins.png)](https://circleci.com/gh/ohjames/nutkins)
4
+
5
+ Nutkins is a tool to manage and test a CoreOS cluster:
6
+ * Config for each service is stored in a directory including an optional `config.yaml` and a `Dockerfile`.
7
+ * Easy to create and run test instances of any service in a single command.
8
+ * Docker images and containers are managed to avoid the difficulties using the raw `docker` command provides around removing images.
9
+ * A wrapper around [sistero](https://github.com/ohjames/sistero), a profile based cluster management tool is provided to make adding new instances to a cluster trivial (currently only digital ocean is supported).
10
+ * A wrapper around `fleetctl` to make upgrading services easy.
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "nutkins"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/bin/nutkins ADDED
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'nutkins'
5
+ require 'moister'
6
+ require 'ostruct'
7
+
8
+ module Nutkins::Command
9
+ def self.run args
10
+ global_config = nil
11
+ command = nil
12
+
13
+ Moister::SubcommandOptionParser.new do |op|
14
+ op.banner = 'usage: nutkins [global options] command [command options]'
15
+
16
+ op.for_all do |op|
17
+ op.on_tail '-h', '--help', 'show this help message' do
18
+ puts op
19
+ exit
20
+ end
21
+ end
22
+
23
+ op.on '-p', '--project dir', 'override path to project', 'project_dir'
24
+
25
+ op.subcommand 'build,b *names', 'build docker image from dockerfile'
26
+
27
+ op.subcommand 'create,c *names', 'create container from image' do |subop|
28
+ subop.on '-p', '--preserve', 'preserve existing container', 'preserve'
29
+ end
30
+
31
+ op.subcommand 'delete,d *names', 'delete container corresponding to image'
32
+ op.subcommand 'delete-all', 'delete containers corresponding to all images'
33
+
34
+ op.subcommand 'run,r name', 'run created container' do |subop|
35
+ subop.on '-r', '--reuse', 'reuse previously created container', 'reuse'
36
+ subop.on '-s', '--shell', 'add bash shell to console', 'shell'
37
+ end
38
+
39
+ op.subcommand 'exec,e name *cmd', 'execute a command in a running container'
40
+ op.subcommand 'build-secret,B path', 'build secret files/volumes'
41
+ op.subcommand 'extract-secrets,X [*names]', 'extract secret files/volumes'
42
+
43
+ parsed_cfg = op.parse(args).to_h
44
+
45
+ global_config = OpenStruct.new parsed_cfg[:config]
46
+ command = parsed_cfg[:command]
47
+ end
48
+
49
+ unless command
50
+ puts 'please supply a command, see --help'
51
+ exit 1
52
+ end
53
+
54
+ nutkins = Nutkins::CloudManager.new(project_dir: global_config.project_dir)
55
+ config = OpenStruct.new global_config[command]
56
+
57
+ case command
58
+ when 'build'
59
+ names = config.names
60
+ if names.empty?
61
+ nutkins.build
62
+ else
63
+ config.names.each &nutkins.method(:build)
64
+ end
65
+ when 'create'
66
+ config.names.each do |name|
67
+ nutkins.create name, preserve: config.preserve
68
+ end
69
+ when 'delete'
70
+ config.names.each &nutkins.method(:delete)
71
+ when 'delete-all'
72
+ nutkins.delete_all
73
+ when 'run'
74
+ nutkins.run config.name, reuse: config.reuse, shell: config.shell
75
+ when 'exec'
76
+ nutkins.exec config.name, config.cmd
77
+ when 'build-secret'
78
+ nutkins.build_secret config.path
79
+ when 'extract-secrets'
80
+ nutkins.extract_secrets config.names
81
+ end
82
+ rescue RuntimeError => e
83
+ puts e.to_s
84
+ end
85
+ end
86
+
87
+ Nutkins::Command::run ARGV
data/bin/setup ADDED
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
data/circle.yml ADDED
@@ -0,0 +1,3 @@
1
+ machine:
2
+ ruby:
3
+ version: 2.2.3
@@ -0,0 +1,17 @@
1
+ module Nutkins::Docker
2
+ def self.image_id_for_tag tag
3
+ regex = /^#{tag} +/
4
+ `docker images`.each_line do |line|
5
+ return line.split(' ')[2] if line =~ regex
6
+ end
7
+ nil
8
+ end
9
+
10
+ def self.container_id_for_tag tag
11
+ regex = /^[0-9a-f]+ +#{tag} +/
12
+ `docker ps -a`.each_line do |line|
13
+ return line.split(' ')[0] if line =~ regex
14
+ end
15
+ nil
16
+ end
17
+ end
@@ -0,0 +1,35 @@
1
+ module Nutkins::Download
2
+ def self.download_file url, output
3
+ orig_url = url
4
+ tries = 10
5
+ while (tries -= 1) >= 0
6
+ response = Net::HTTP.get_response(URI(url))
7
+ case response
8
+ when Net::HTTPRedirection
9
+ url = response["location"]
10
+ else
11
+ open(output, "wb") do |file|
12
+ file.write(response.body)
13
+ end
14
+ return
15
+ end
16
+ end
17
+
18
+ raise "could not download #{orig_url}"
19
+ end
20
+
21
+ def self.download_resources img_dir, resources
22
+ resources.each do |resource|
23
+ source = resource["source"]
24
+ dest = File.join(img_dir, resource["dest"])
25
+ unless File.exists? dest
26
+ FileUtils.mkdir_p File.dirname(dest)
27
+ print "downloading #{source}"
28
+ download_file source, dest
29
+ puts " - done"
30
+ mode = resource["mode"]
31
+ File.chmod(mode, dest) if mode
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,3 @@
1
+ module Nutkins
2
+ VERSION = "0.1.0"
3
+ end
data/lib/nutkins.rb ADDED
@@ -0,0 +1,207 @@
1
+ require "yaml"
2
+ require "ostruct"
3
+ require "net/http"
4
+ require "uri"
5
+ require "fileutils"
6
+ require "json"
7
+
8
+ module Nutkins ; end
9
+
10
+ require "nutkins/docker"
11
+ require "nutkins/download"
12
+ require "nutkins/version"
13
+
14
+ # Must be somedomain.net instead of somedomain.net/, otherwise, it will throw exception.
15
+ module Nutkins
16
+ CONFIG_FILE_NAME = 'nutkins.yaml'
17
+ IMG_CONFIG_FILE_NAME = 'nutkin.yaml'
18
+ VOLUMES_PATH = 'volumes'
19
+
20
+ class CloudManager
21
+ def initialize(project_dir: nil)
22
+ @project_root = project_dir || Dir.pwd
23
+ cfg_path = File.join(@project_root, CONFIG_FILE_NAME)
24
+ if File.exists? cfg_path
25
+ @config = OpenStruct.new(YAML.load_file cfg_path)
26
+ else
27
+ @config = OpenStruct.new
28
+ end
29
+ end
30
+
31
+ def build img_name = '.'
32
+ cfg = get_image_config img_name
33
+ img_dir = get_project_dir img_name
34
+ raise "directory `#{img_dir}' does not exist" unless Dir.exists? img_dir
35
+
36
+ if img_name == '.'
37
+ img_name = cfg["image"]
38
+ raise "nutkin.yaml requires image entry for `build .'" unless img_name
39
+ end
40
+
41
+ build_cfg = cfg["build"]
42
+ if build_cfg
43
+ # download each of the files in the resources section if it doesn't exist
44
+ resources = build_cfg["resources"]
45
+ Dowload.download_resources img_dir, resources if resources
46
+ end
47
+
48
+ tag = get_tag img_name
49
+ prev_image_id = Docker.image_id_for_tag tag
50
+
51
+ if run_docker "build", "-t", tag, img_dir
52
+ image_id = Docker.image_id_for_tag tag
53
+ if not prev_image_id.nil? and image_id != prev_image_id
54
+ puts "deleting previous image #{prev_image_id}"
55
+ run_docker "rmi", prev_image_id
56
+ end
57
+ else
58
+ raise "issue building docker image for #{img_name}"
59
+ end
60
+ end
61
+
62
+ def create img_name, preserve: false, docker_args: []
63
+ flags = []
64
+ cfg = get_image_config img_name
65
+ create_cfg = cfg["create"]
66
+ if create_cfg
67
+ (create_cfg["ports"] or []).each do |port|
68
+ flags.push '-p', "#{port}:#{port}"
69
+ end
70
+
71
+ img_dir = get_project_dir img_name
72
+ (create_cfg["volumes"] or []).each do |volume|
73
+ src, dest = volume.split ' -> '
74
+ src = File.absolute_path File.join(img_dir, VOLUMES_PATH, src)
75
+ flags.push '-v', "#{src}:#{dest}"
76
+ end
77
+ end
78
+
79
+ tag = get_tag img_name
80
+ prev_container_id = Docker.container_id_for_tag tag unless preserve
81
+ puts "creating new docker image"
82
+ unless run_docker "create", "-it", *flags, tag, *docker_args
83
+ raise "failed to create `#{img_name}' container"
84
+ end
85
+
86
+ unless preserve
87
+ container_id = Docker.container_id_for_tag tag
88
+ if not prev_container_id.nil? and container_id != prev_container_id
89
+ puts "deleting previous container #{prev_container_id}"
90
+ run_docker "rm", prev_container_id
91
+ end
92
+ end
93
+
94
+ puts "created `#{img_name}' container"
95
+ end
96
+
97
+ def run img_name, reuse: false, shell: false
98
+ cfg = get_image_config img_name
99
+ tag = get_tag img_name
100
+ create_args = []
101
+ if shell
102
+ raise '--shell and --reuse arguments are incompatible' if reuse
103
+
104
+ # TODO: test for smell-baron
105
+ create_args = JSON.parse(`docker inspect #{tag}`)[0]["Config"]["Cmd"]
106
+ create_args.unshift '/bin/bash', '---'
107
+ create_args.unshift '-f' unless create_args[0] == '-f'
108
+ # TODO: provide version that doesn't require smell-baron
109
+ end
110
+
111
+ id = reuse && Docker.container_id_for_tag(tag)
112
+ unless id
113
+ create img_name, docker_args: create_args
114
+ id = Docker.container_id_for_tag tag
115
+ raise "couldn't create container to run `#{img_name}'" unless id
116
+ end
117
+
118
+ Kernel.exec "docker", "start", "-ai", id
119
+ end
120
+
121
+ def delete img_name
122
+ puts "TODO: delete #{img_name}"
123
+ end
124
+
125
+ def delete_all
126
+ puts "TODO: delete_all"
127
+ end
128
+
129
+ def build_secret path
130
+ secret = path
131
+ path_is_dir = Dir.exists? path
132
+ if path_is_dir
133
+ secret += '.tar'
134
+ system "tar", "cf", secret, "-C", File.dirname(path), File.basename(path)
135
+ end
136
+
137
+ loop do
138
+ puts "enter passphrase for #{secret}"
139
+ break if system 'gpg', '-c', secret
140
+ end
141
+
142
+ File.unlink secret if path_is_dir
143
+ end
144
+
145
+ def extract_secrets img_names
146
+ with_current = img_names.empty?
147
+ img_names = get_image_names(img_names)
148
+ img_names.push '.' if with_current
149
+
150
+ img_names.each do |img_name|
151
+ get_secrets(img_name).each do |secret|
152
+ loop do
153
+ puts "enter passphrase for #{secret}"
154
+ break if system 'gpg', secret
155
+ end
156
+
157
+ secret = secret[0..-5]
158
+ if File.extname(secret) == '.tar'
159
+ system "tar", "xf", secret, "-C", File.dirname(secret)
160
+ File.unlink secret
161
+ end
162
+ end
163
+ end
164
+ end
165
+
166
+ def exec img_name, *cmd
167
+ puts "TODO: exec #{img_name}: #{cmd.join ' '}"
168
+ end
169
+
170
+ private
171
+ def get_image_config path
172
+ img_cfg_path = File.join get_project_dir(path), IMG_CONFIG_FILE_NAME
173
+ img_cfg = File.exists?(img_cfg_path) ? YAML.load_file(img_cfg_path) : {}
174
+ @repository = img_cfg['repository'] || @config.repository
175
+ img_cfg
176
+ end
177
+
178
+ def get_project_dir path
179
+ path == '.' ? @project_root : File.join(@project_root, path)
180
+ end
181
+
182
+ def get_tag tag
183
+ raise "command requires `repository' entry in nutkins.yaml or nutkin.yaml" if @repository.nil?
184
+ @repository + '/' + tag
185
+ end
186
+
187
+ def get_image_names img_names
188
+ if img_names.empty?
189
+ Dir.glob("#{@project_root}/*/Dockerfile").map do |path|
190
+ File.basename File.dirname(path)
191
+ end
192
+ else
193
+ img_names
194
+ end
195
+ end
196
+
197
+ # can supply img_name or . for project root
198
+ def get_secrets img_name
199
+ img_dir = get_project_dir img_name
200
+ Dir.glob("#{img_dir}/{volumes,secrets}/*.gpg")
201
+ end
202
+
203
+ def run_docker *args
204
+ system 'docker', *args
205
+ end
206
+ end
207
+ end
data/nutkins.gemspec ADDED
@@ -0,0 +1,35 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'nutkins/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "nutkins"
8
+ spec.version = Nutkins::VERSION
9
+ spec.authors = ["James Pike"]
10
+ spec.email = ["github@chilon.net"]
11
+
12
+ spec.summary = %q{CoreOS cluster management tool.}
13
+ spec.description = spec.summary
14
+ spec.homepage = "http://github.com/ohjames/nutkins"
15
+
16
+ # Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or
17
+ # delete this section to allow pushing this gem to any host.
18
+ if spec.respond_to?(:metadata)
19
+ spec.metadata['allowed_push_host'] = "https://rubygems.org"
20
+ else
21
+ raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
22
+ end
23
+
24
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
25
+ spec.bindir = "exe"
26
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
27
+ spec.require_paths = ["lib"]
28
+
29
+ spec.add_development_dependency "bundler", "~> 1.9"
30
+ spec.add_development_dependency "rake", "~> 10.0"
31
+ spec.add_development_dependency "rspec"
32
+
33
+ spec.add_dependency "sistero", "~> 0.4.4"
34
+ spec.add_dependency "moister", "~> 0.3.0"
35
+ end
metadata ADDED
@@ -0,0 +1,129 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: nutkins
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - James Pike
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-01-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.9'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.9'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: sistero
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 0.4.4
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 0.4.4
69
+ - !ruby/object:Gem::Dependency
70
+ name: moister
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 0.3.0
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 0.3.0
83
+ description: CoreOS cluster management tool.
84
+ email:
85
+ - github@chilon.net
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - ".gitignore"
91
+ - ".rspec"
92
+ - ".travis.yml"
93
+ - Gemfile
94
+ - README.md
95
+ - Rakefile
96
+ - bin/console
97
+ - bin/nutkins
98
+ - bin/setup
99
+ - circle.yml
100
+ - lib/nutkins.rb
101
+ - lib/nutkins/docker.rb
102
+ - lib/nutkins/download.rb
103
+ - lib/nutkins/version.rb
104
+ - nutkins.gemspec
105
+ homepage: http://github.com/ohjames/nutkins
106
+ licenses: []
107
+ metadata:
108
+ allowed_push_host: https://rubygems.org
109
+ post_install_message:
110
+ rdoc_options: []
111
+ require_paths:
112
+ - lib
113
+ required_ruby_version: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ required_rubygems_version: !ruby/object:Gem::Requirement
119
+ requirements:
120
+ - - ">="
121
+ - !ruby/object:Gem::Version
122
+ version: '0'
123
+ requirements: []
124
+ rubyforge_project:
125
+ rubygems_version: 2.5.1
126
+ signing_key:
127
+ specification_version: 4
128
+ summary: CoreOS cluster management tool.
129
+ test_files: []