nutkins 0.1.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.
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: []