pmgmt 1.0.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 (5) hide show
  1. checksums.yaml +7 -0
  2. data/lib/dockerhelper.rb +47 -0
  3. data/lib/pmgmt.rb +218 -0
  4. data/lib/syncfiles.rb +205 -0
  5. metadata +46 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 3e30d7e78a8a61ae27c505f47f1821c2ee991ac91264abfed389651626823e9a
4
+ data.tar.gz: 151734accba09a0511a88d78380894de6c4af6fad2ad0613c701441eb89963de
5
+ SHA512:
6
+ metadata.gz: 764187c3dcb66e45fd17b6e111d8230f06e4fa622f75690f9cf3a43639f75777392b1d8a37cbb59d6b13e854da3e2411e5c3e3e2fe5d6c1c5ad28fe20c1cbfb2
7
+ data.tar.gz: 97a9f031cd58f3ba87e97104cef2be3945a27c13815a55be444507cf9184f7285a3e6c011ee3e44d62fd0a7d5d586f31cd0ff817ed03373509f00ef9043f4fea
@@ -0,0 +1,47 @@
1
+ module PmgmtLib
2
+ class DockerHelper
3
+ attr :c
4
+
5
+ def initialize(common)
6
+ @c = common
7
+ end
8
+
9
+ def in_docker?()
10
+ File.exist?("/.dockerenv")
11
+ end
12
+
13
+ def requires_docker()
14
+ status = c.run %W{which docker}
15
+ unless status.success?
16
+ c.error "docker not installed."
17
+ STDERR.puts "Installation instructions:"
18
+ STDERR.puts "\n https://www.docker.com/community-edition\n\n"
19
+ exit 1
20
+ end
21
+ status = c.run %W{docker info}
22
+ unless status.success?
23
+ c.error "`docker info` command failed."
24
+ STDERR.puts "This is usually a permissions problem. Try allowing your user to run docker\n"
25
+ STDERR.puts "without sudo:"
26
+ STDERR.puts "\n$ sudo usermod -aG docker #{ENV["USER"]}\n\n"
27
+ c.error "Note: You will need to log-in to a new shell before this change will take effect.\n"
28
+ exit 1
29
+ end
30
+ end
31
+
32
+ def image_exists?(name)
33
+ requires_docker
34
+ fmt = "{{.Repository}}:{{.Tag}}"
35
+ c.capture_stdout(%W{docker images --format #{fmt}}).include?(name)
36
+ end
37
+
38
+ def ensure_image(name)
39
+ requires_docker
40
+ if not image_exists?(name)
41
+ c.error "Missing docker image \"#{name}\". Pulling..."
42
+ c.run_inline(%W{docker pull #{name}})
43
+ c.status "Image \"#{name}\" pulled."
44
+ end
45
+ end
46
+ end
47
+ end
data/lib/pmgmt.rb ADDED
@@ -0,0 +1,218 @@
1
+ require "open3"
2
+ require "ostruct"
3
+ require "yaml"
4
+
5
+ require_relative "dockerhelper"
6
+ require_relative "syncfiles"
7
+
8
+ class Pmgmt
9
+ @@commands = []
10
+
11
+ def self.load_scripts(scripts_dir)
12
+ if !File.directory?(scripts_dir)
13
+ self.new.error "Cannot load scripts. Not a directory: #{scripts_dir}"
14
+ exit 1
15
+ end
16
+ Dir.foreach(scripts_dir) do |item|
17
+ if item =~ /[.]rb$/
18
+ require "#{scripts_dir}/#{item}"
19
+ end
20
+ end
21
+ end
22
+
23
+ def self.register_command(command)
24
+ invocation = command[:invocation]
25
+ fn = command[:fn]
26
+ if fn.nil?
27
+ self.new.error "No :fn key defined for command #{invocation}"
28
+ exit 1
29
+ end
30
+ if fn.is_a?(Symbol)
31
+ unless Object.private_method_defined?(fn)
32
+ self.new.error "Function #{fn.to_s} is not defined for #{invocation}."
33
+ exit 1
34
+ end
35
+ else
36
+ # TODO(dmohs): Deprecation warning.
37
+ unless fn.is_a?(Proc)
38
+ self.new.error ":fn key for #{invocation} does not define a Proc or Symbol"
39
+ exit 1
40
+ end
41
+ end
42
+
43
+ @@commands.push(command)
44
+ end
45
+
46
+ def self.commands()
47
+ @@commands
48
+ end
49
+
50
+ def self.handle_or_die(args)
51
+ if args.length == 0 or args[0] == "--help"
52
+ self.new.print_usage
53
+ exit 0
54
+ end
55
+
56
+ if args[0] == "--cmplt" # Shell completion argument name inspired by vault
57
+ # Form of args: --cmplt <index-of-current-argument> ./project.rb arg arg arg
58
+ index = args[1].to_i
59
+ word = args[2 + index]
60
+ puts @@commands.select{ |x| x[:invocation].start_with?(word) }
61
+ .map{ |x| x[:invocation]}.join("\n")
62
+ exit 0
63
+ end
64
+
65
+ command = args.first
66
+ handler = @@commands.select{ |x| x[:invocation] == command }.first
67
+ if handler.nil?
68
+ error "#{command} command not found."
69
+ exit 1
70
+ end
71
+
72
+ fn = handler[:fn]
73
+ if fn.is_a?(Symbol)
74
+ method(fn).call(*args)
75
+ else
76
+ handler[:fn].call(*args.drop(1))
77
+ end
78
+ end
79
+
80
+ attr :docker
81
+ attr :sf
82
+
83
+ def initialize()
84
+ @docker = PmgmtLib::DockerHelper.new(self)
85
+ @sf = PmgmtLib::SyncFiles.new(self)
86
+ end
87
+
88
+ def print_usage()
89
+ STDERR.puts "\nUsage: #{$PROGRAM_NAME} <command> <options>\n\n"
90
+ if !@@commands.empty?
91
+ STDERR.puts "COMMANDS\n\n"
92
+ @@commands.each do |command|
93
+ STDERR.puts bold_term_text(command[:invocation])
94
+ STDERR.puts command[:description] || "[No description provided.]"
95
+ STDERR.puts
96
+ end
97
+ else
98
+ STDERR.puts " >> No commands defined.\n\n"
99
+ end
100
+ end
101
+
102
+ def load_env()
103
+ if not File.exists?("project.yaml")
104
+ error "Missing project.yaml"
105
+ exit 1
106
+ end
107
+ OpenStruct.new YAML.load(File.read("project.yaml"))
108
+ end
109
+
110
+ def red_term_text(text)
111
+ "\033[0;31m#{text}\033[0m"
112
+ end
113
+
114
+ def blue_term_text(text)
115
+ "\033[0;36m#{text}\033[0m"
116
+ end
117
+
118
+ def yellow_term_text(text)
119
+ "\033[0;33m#{text}\033[0m"
120
+ end
121
+
122
+ def bold_term_text(text)
123
+ "\033[1m#{text}\033[0m"
124
+ end
125
+
126
+ def status(text)
127
+ STDERR.puts blue_term_text(text)
128
+ end
129
+
130
+ def warning(text)
131
+ STDERR.puts yellow_term_text(text)
132
+ end
133
+
134
+ def error(text)
135
+ STDERR.puts red_term_text(text)
136
+ end
137
+
138
+ def put_command(cmd, redact=nil)
139
+ if cmd.is_a?(String)
140
+ command_string = "+ #{cmd}"
141
+ else
142
+ command_string = "+ #{cmd.join(" ")}"
143
+ end
144
+ command_to_echo = command_string.clone
145
+ if redact
146
+ command_to_echo.sub! redact, "*" * redact.length
147
+ end
148
+ STDERR.puts command_to_echo
149
+ end
150
+
151
+ # Pass err=nil to suppress stderr.
152
+ def capture_stdout(cmd, err = STDERR)
153
+ if err.nil?
154
+ err = "/dev/null"
155
+ end
156
+ output, _ = Open3.capture2(*cmd, :err => err)
157
+ output
158
+ end
159
+
160
+ def run_inline(cmd, redact=nil)
161
+ put_command(cmd, redact)
162
+
163
+ # `system`, by design (?!), hides stderr when the command fails.
164
+ if ENV["PROJECTRB_USE_SYSTEM"] == "true"
165
+ if not system(*cmd)
166
+ exit $?.exitstatus
167
+ end
168
+ else
169
+ pid = spawn(*cmd)
170
+ Process.wait pid
171
+ if $?.exited?
172
+ if !$?.success?
173
+ exit $?.exitstatus
174
+ end
175
+ else
176
+ error "Command exited abnormally."
177
+ exit 1
178
+ end
179
+ end
180
+ end
181
+
182
+ def run_inline_swallowing_interrupt(cmd)
183
+ begin
184
+ run_inline cmd
185
+ rescue Interrupt
186
+ end
187
+ end
188
+
189
+ def run_or_fail(cmd, redact=nil)
190
+ put_command(cmd, redact)
191
+ Open3.popen3(*cmd) do |i, o, e, t|
192
+ i.close
193
+ if not t.value.success?
194
+ STDERR.write red_term_text(e.read)
195
+ exit t.value.exitstatus
196
+ end
197
+ end
198
+ end
199
+
200
+ def run(cmd)
201
+ Open3.popen3(*cmd) do |i, o, e, t|
202
+ i.close
203
+ t.value
204
+ end
205
+ end
206
+
207
+ def pipe(*cmds)
208
+ s = cmds.map { |x| x.join(" ") }
209
+ s = s.join(" | ")
210
+ STDERR.puts "+ #{s}"
211
+ Open3.pipeline(*cmds).each do |status|
212
+ unless status.success?
213
+ error "Piped command failed"
214
+ exit 1
215
+ end
216
+ end
217
+ end
218
+ end
data/lib/syncfiles.rb ADDED
@@ -0,0 +1,205 @@
1
+ require "open3"
2
+
3
+ module PmgmtLib
4
+ class SyncFiles
5
+ attr :c
6
+
7
+ def initialize(common)
8
+ @c = common
9
+ end
10
+
11
+ def is_fswatch_installed()
12
+ status = c.run %W{which fswatch}
13
+ return status.success?
14
+ end
15
+
16
+ def src_vol_name()
17
+ env = c.load_env
18
+ "#{env.namespace}-src"
19
+ end
20
+
21
+ def output_vol_name()
22
+ env = c.load_env
23
+ "#{env.namespace}-out"
24
+ end
25
+
26
+ def get_src_volume_mount()
27
+ if is_fswatch_installed
28
+ %W{-v #{src_vol_name}:/w}
29
+ else
30
+ %W{-v #{ENV["PWD"]}:/w}
31
+ end
32
+ end
33
+
34
+ def get_volume_mounts()
35
+ env = c.load_env
36
+ if env.static_file_dest
37
+ get_src_volume_mount + %W{-v #{output_vol_name}:/w/#{env.static_file_dest}}
38
+ else
39
+ get_src_volume_mount
40
+ end
41
+ end
42
+
43
+ def log_file_name()
44
+ ".rsync.log"
45
+ end
46
+
47
+ def log_message(s)
48
+ File.open(log_file_name, "a") do |file|
49
+ file.write s
50
+ end
51
+ end
52
+
53
+ def get_dest_path(src, dst)
54
+ if dst.nil?
55
+ dst = src
56
+ end
57
+ dst = dst.split(/\//).reverse.drop(1).reverse.join("/")
58
+ if not dst.empty?
59
+ dst = "/w/#{dst}"
60
+ else
61
+ dst = "/w"
62
+ end
63
+ end
64
+
65
+ def start_rsync_container()
66
+ env = c.load_env
67
+ c.docker.requires_docker
68
+ c.docker.ensure_image("tjamet/rsync")
69
+ cmd = %W{
70
+ docker run -d
71
+ --name #{env.namespace}-rsync
72
+ -v #{src_vol_name}:/w
73
+ }
74
+ if env.static_file_dest
75
+ cmd += %W{-v #{output_vol_name}:/w/#{env.static_file_dest}}
76
+ end
77
+ c.run_inline cmd + %W{-e DAEMON=docker tjamet/rsync}
78
+ end
79
+
80
+ def stop_rsync_container()
81
+ env = c.load_env
82
+ c.run_inline %W{docker rm -f #{env.namespace}-rsync}
83
+ end
84
+
85
+ def rsync_path(src, dst, log)
86
+ env = c.load_env
87
+ dst = get_dest_path(src, dst)
88
+ rsync_remote_shell = "docker exec -i"
89
+ cmd = %W{
90
+ rsync --blocking-io -azlv --delete -e #{rsync_remote_shell}
91
+ #{src}
92
+ #{env.namespace}-rsync:#{dst}
93
+ }
94
+ if log
95
+ Open3.popen3(*cmd) do |i, o, e, t|
96
+ i.close
97
+ if not t.value.success?
98
+ c.error e.read
99
+ exit t.value.exitstatus
100
+ end
101
+ log_message o.read
102
+ end
103
+ else
104
+ c.run_inline cmd
105
+ end
106
+ end
107
+
108
+ def link_static_file(src, dst)
109
+ cmd = %W{docker run --rm -w /w} + get_volume_mounts + %W{alpine ln -snf /w/#{src} /w/#{dst}}
110
+ c.run_inline cmd
111
+ end
112
+
113
+ def link_static_files()
114
+ env = c.load_env
115
+ threads = []
116
+ foreach_static_file do |path, entry|
117
+ threads << Thread.new do
118
+ link_static_file "#{path}/#{entry}", "#{env.static_file_dest}/#{entry}"
119
+ end
120
+ end
121
+ threads.each do |t|
122
+ t.join
123
+ end
124
+ end
125
+
126
+ def foreach_static_file()
127
+ env = c.load_env
128
+ Dir.foreach(env.static_file_src) do |entry|
129
+ unless [".", ".."].include?(entry)
130
+ yield env.static_file_src, entry
131
+ end
132
+ end
133
+ end
134
+
135
+ def watch_path(src, dst)
136
+ Open3.popen3(*%W{fswatch -o #{src}}) do |stdin, stdout, stderr, thread|
137
+ Thread.current["pid"] = thread.pid
138
+ stdin.close
139
+ stdout.each_line do |_|
140
+ rsync_path src, dst, true
141
+ end
142
+ end
143
+ end
144
+
145
+ def perform_initial_sync()
146
+ env = c.load_env
147
+ env.source_file_paths.each do |src_path|
148
+ # Copying using tar ensures the destination directories will be created.
149
+ c.pipe(
150
+ %W{env COPYFILE_DISABLE=1 tar -c #{src_path}},
151
+ %W{docker cp - #{env.namespace}-rsync:/w}
152
+ )
153
+ rsync_path src_path, nil, false
154
+ end
155
+ if env.static_file_src
156
+ c.pipe(
157
+ %W{env COPYFILE_DISABLE=1 tar -c #{env.static_file_src}},
158
+ %W{docker cp - #{env.namespace}-rsync:/w}
159
+ )
160
+ rsync_path env.static_file_src, nil, false
161
+ end
162
+ end
163
+
164
+ def start_watching_sync()
165
+ env = c.load_env
166
+ File.open(log_file_name, "w") {} # Create and truncate if exists.
167
+ paths_to_watch = env.source_file_paths
168
+ if env.static_file_src
169
+ paths_to_watch += [env.static_file_src]
170
+ end
171
+ paths_to_watch.each do |src_path|
172
+ thread = Thread.new { watch_path src_path, nil }
173
+ at_exit {
174
+ Process.kill("HUP", thread["pid"])
175
+ thread.join
176
+ }
177
+ end
178
+ end
179
+
180
+ def maybe_start_file_syncing()
181
+ system_name, _ = Open3.capture2("uname")
182
+ system_name.chomp!
183
+ env = c.load_env
184
+ if env.static_file_src
185
+ c.status "Linking static files..."
186
+ link_static_files
187
+ end
188
+ fswatch_installed = is_fswatch_installed
189
+ if system_name == "Darwin" and not fswatch_installed
190
+ c.error "fswatch is not installed."
191
+ STDERR.puts "File syncing will be extremely slow due to a performance problem in docker.\n" \
192
+ "Installing fswatch is highly recommended. Try:\n\n$ brew install fswatch\n\n"
193
+ end
194
+ if fswatch_installed
195
+ c.status "Starting rsync container..."
196
+ at_exit { stop_rsync_container }
197
+ start_rsync_container
198
+ c.status "Performing initial file sync..."
199
+ perform_initial_sync
200
+ start_watching_sync
201
+ c.status "Watching source files. See log at #{log_file_name}."
202
+ end
203
+ end
204
+ end
205
+ end
metadata ADDED
@@ -0,0 +1,46 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pmgmt
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - David Mohs
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-07-10 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: A library to make Ruby your preferred scripting language for dev scripts.
14
+ email: davidmohs@gmail.com
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - lib/dockerhelper.rb
20
+ - lib/pmgmt.rb
21
+ - lib/syncfiles.rb
22
+ homepage: http://rubygems.org/gems/pmgmt
23
+ licenses:
24
+ - MIT
25
+ metadata: {}
26
+ post_install_message:
27
+ rdoc_options: []
28
+ require_paths:
29
+ - lib
30
+ required_ruby_version: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: '0'
35
+ required_rubygems_version: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ requirements: []
41
+ rubyforge_project:
42
+ rubygems_version: 2.7.6
43
+ signing_key:
44
+ specification_version: 4
45
+ summary: Project management scripting library.
46
+ test_files: []