configmonkey_cli 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 (35) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +87 -0
  6. data/Rakefile +1 -0
  7. data/VERSION +1 -0
  8. data/bin/configmonkey +9 -0
  9. data/bin/configmonkey.sh +14 -0
  10. data/configmonkey_cli.gemspec +27 -0
  11. data/lib/configmonkey_cli.rb +43 -0
  12. data/lib/configmonkey_cli/application.rb +159 -0
  13. data/lib/configmonkey_cli/application/colorize.rb +22 -0
  14. data/lib/configmonkey_cli/application/configuration.rb +38 -0
  15. data/lib/configmonkey_cli/application/configuration.tpl +0 -0
  16. data/lib/configmonkey_cli/application/core.rb +78 -0
  17. data/lib/configmonkey_cli/application/dispatch.rb +81 -0
  18. data/lib/configmonkey_cli/application/manifest.rb +316 -0
  19. data/lib/configmonkey_cli/application/manifest_actions/base.rb +90 -0
  20. data/lib/configmonkey_cli/application/manifest_actions/chmod.rb +40 -0
  21. data/lib/configmonkey_cli/application/manifest_actions/copy.rb +38 -0
  22. data/lib/configmonkey_cli/application/manifest_actions/custom.rb +52 -0
  23. data/lib/configmonkey_cli/application/manifest_actions/inplace.rb +28 -0
  24. data/lib/configmonkey_cli/application/manifest_actions/invoke.rb +45 -0
  25. data/lib/configmonkey_cli/application/manifest_actions/link.rb +46 -0
  26. data/lib/configmonkey_cli/application/manifest_actions/mkdir.rb +41 -0
  27. data/lib/configmonkey_cli/application/manifest_actions/remove.rb +46 -0
  28. data/lib/configmonkey_cli/application/manifest_actions/rsync.rb +112 -0
  29. data/lib/configmonkey_cli/application/manifest_actions/rtfm.rb +32 -0
  30. data/lib/configmonkey_cli/application/manifest_actions/sync_links.rb +60 -0
  31. data/lib/configmonkey_cli/application/manifest_actions/template.rb +38 -0
  32. data/lib/configmonkey_cli/application/output_helper.rb +30 -0
  33. data/lib/configmonkey_cli/helper.rb +62 -0
  34. data/lib/configmonkey_cli/version.rb +4 -0
  35. metadata +162 -0
@@ -0,0 +1,90 @@
1
+ module ConfigmonkeyCli
2
+ class Application
3
+ module ManifestAction
4
+ class Base
5
+ attr_reader :app, :manifest, :args, :opts, :thor
6
+
7
+ def initialize app, manifest, *args, &block
8
+ @app = app
9
+ @thor = manifest.thor
10
+ @args = []
11
+ @opts = {}
12
+ @manifest = manifest
13
+
14
+ init(*args, &block)
15
+ end
16
+
17
+ def to_s
18
+ "#<…::#{self.class.name.split("::").last(2).join("::")} @args=#{@args} @opts=#{@opts}>"
19
+ end
20
+ alias_method :inspect, :to_s
21
+
22
+ def args_and_opts hargs_and_opts = {}
23
+ args, opts = [], {}
24
+ hargs_and_opts.each do |k, v|
25
+ if k.is_a?(String)
26
+ args << k << v
27
+ elsif k.is_a?(Symbol)
28
+ opts[k] = v
29
+ else
30
+ raise "what the heck did you pass as a key?"
31
+ end
32
+ end
33
+ [args, opts]
34
+ end
35
+
36
+ def rel path
37
+ thor.relative_to_original_destination_root(path)
38
+ end
39
+
40
+ def expand_src src = ""
41
+ File.join(manifest.directory, src)
42
+ end
43
+
44
+ def expand_dst dst = ""
45
+ File.join(thor.destination_root, dst)
46
+ end
47
+
48
+ def exists? *args
49
+ if args[0].is_a?(Hash)
50
+ File.exists?(send(:"expand_#{args[0].keys[0]}", args[0].values[0]))
51
+ else
52
+ File.exists?(args[0])
53
+ end
54
+ end
55
+
56
+ def status name, *args
57
+ case args.length
58
+ when 0
59
+ raise ArgumentError("at least name and string is required")
60
+ when 1 # status :fake, rel(@destination)
61
+ thor.say_status name, args[0], :green
62
+ when 2 # status :fake, :green, rel(@destination)
63
+ thor.say_status name, args[1], args[0]
64
+ when 3 # status :fake, :green, rel(@destination), :red
65
+ thor.say_status name, thor.set_color(args[1], *args[2..-1]), args[0]
66
+ end
67
+ end
68
+
69
+ [:padded, :c, :say, :ask, :yes?, :no?].each do |meth|
70
+ define_method(meth) do |*args, &block|
71
+ manifest.send(meth, *args, &block)
72
+ end
73
+ end
74
+
75
+ def init *a, &b
76
+ end
77
+
78
+ def prepare
79
+ end
80
+
81
+ def simulate &b
82
+ app.warn self.inspect
83
+ end
84
+
85
+ def destructive &b
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,40 @@
1
+ module ConfigmonkeyCli
2
+ class Application
3
+ module ManifestAction
4
+ class Chmod < Base
5
+ def init path, mode, opts = {}
6
+ @opts = opts.reverse_merge({
7
+ #_p: true
8
+ })
9
+
10
+ @args = [path, mode]
11
+ end
12
+
13
+ def prepare
14
+ @path = expand_dst(@args[0])
15
+ @mode = @args[1]
16
+ end
17
+
18
+ def simulate
19
+ if thor.options[:pretend]
20
+ destructive
21
+ else
22
+ status :fake, :black, "#{@args[0]} (#{@args[1].to_s(8)})"
23
+ end
24
+ end
25
+
26
+ def destructive
27
+ if File.exist?(@path)
28
+ if @mode == File.stat(@path).mode - 0100000
29
+ status :identical, :blue, "#{@args[0]} (#{@args[1].to_s(8)})"
30
+ else
31
+ thor.chmod(@path, @mode)
32
+ end
33
+ else
34
+ status :noexist, :red, "#{@args[0]} (#{@args[1].to_s(8)})"
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,38 @@
1
+ module ConfigmonkeyCli
2
+ class Application
3
+ module ManifestAction
4
+ class Copy < Base
5
+ def init hargs_and_opts = {}
6
+ @args, @opts = args_and_opts(hargs_and_opts)
7
+ end
8
+
9
+ def prepare
10
+ @opts[:force] = app.opts[:default_yes]
11
+ @source = @args[0]
12
+ @destination = expand_dst(@args[1])
13
+ end
14
+
15
+ def simulate
16
+ if thor.options[:pretend]
17
+ destructive
18
+ else
19
+ status :fake, :black, rel(@destination)
20
+ end
21
+ end
22
+
23
+ def destructive
24
+ absolute_source = File.join(thor.source_paths[0], @source)
25
+ if FileTest.directory?(absolute_source)
26
+ thor.directory(@source, @destination, @opts)
27
+ else
28
+ thor.copy_file(@source, @destination, @opts)
29
+ if @opts[:chmod] && File.exist?(absolute_source) && File.exist?(@destination)
30
+ mode = @opts[:chmod] == true ? File.stat(absolute_source).mode - 0100000 : @opts[:chmod]
31
+ thor.chmod(@destination, mode) unless mode == File.stat(@destination).mode - 0100000
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,52 @@
1
+ module ConfigmonkeyCli
2
+ class Application
3
+ module ManifestAction
4
+ class Custom < Base
5
+ def init label = nil, &block
6
+ @label = label
7
+ block.call(Proxy.new(self)) if block
8
+ end
9
+
10
+ def inspect
11
+ @label ? super.gsub("Custom @args", "Custom @label=#{@label} @args") : super
12
+ end
13
+
14
+ def prepare
15
+ status :proc, :yellow, (@label || "unlabeled custom block"), :blue
16
+ end
17
+
18
+ def simulate
19
+ @args.each do |scope, block|
20
+ block.call(self, manifest) if scope == :always || scope == :simulate
21
+ end
22
+ end
23
+
24
+ def destructive
25
+ @args.each do |scope, block|
26
+ block.call(self, manifest) if scope == :always || scope == :destructive
27
+ end
28
+ end
29
+
30
+ class Proxy
31
+ attr_reader :action
32
+
33
+ def initialize action
34
+ @action = action
35
+ end
36
+
37
+ def always &block
38
+ @action.args << [:always, block]
39
+ end
40
+
41
+ def simulate &block
42
+ @action.args << [:simulate, block]
43
+ end
44
+
45
+ def destructive &block
46
+ @action.args << [:destructive, block]
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,28 @@
1
+ module ConfigmonkeyCli
2
+ class Application
3
+ module ManifestAction
4
+ class Inplace < Base
5
+ def init label = nil, &block
6
+ @label = label
7
+ @block = block
8
+ end
9
+
10
+ def inspect
11
+ @label ? super.gsub("Custom @args", "Custom @label=#{@label} @args") : super
12
+ end
13
+
14
+ def prepare
15
+ status :proc, :yellow, (@label || "unlabeled inplace block"), :blue
16
+ end
17
+
18
+ def simulate
19
+ destructive
20
+ end
21
+
22
+ def destructive
23
+ instance_exec(manifest, self, &@block)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,45 @@
1
+ module ConfigmonkeyCli
2
+ class Application
3
+ module ManifestAction
4
+ class Invoke < Base
5
+ def init command = nil, opts = {}, &command_builder
6
+ if command_builder
7
+ command = "#{command}" # reference copy
8
+ command = command_builder.call(command, opts)
9
+ end
10
+ @args = [command]
11
+ @opts = opts.reverse_merge(chomp: true, echo: true, fail: true)
12
+ end
13
+
14
+ def simulate
15
+ status :invoke, :yellow, @args[0]
16
+ end
17
+
18
+ def destructive
19
+ status :invoke, :yellow, @args[0]
20
+ code, res = exec(@args[0], @opts[:chomp])
21
+
22
+ if opts[:echo]
23
+ if code.exitstatus.zero?
24
+ say padded("#{c "[OK]", :green} #{res}", :black)
25
+ else
26
+ say padded("[#{code.exitstatus}] #{res}", :red)
27
+ raise "Invoked process exited with status #{code.exitstatus}: #{res}" if opts[:fail]
28
+ end
29
+ end
30
+ end
31
+
32
+ def exec cmd, chomp = true
33
+ app.debug "§invoking:#{cmd}"
34
+ _stdin, _stdouterr, _thread = Open3.popen2e(cmd)
35
+ _thread.join
36
+ res = _stdouterr.read
37
+ [_thread.value, chomp ? res.chomp : res]
38
+ ensure
39
+ _stdin.close rescue false
40
+ _stdouterr.close rescue false
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,46 @@
1
+ module ConfigmonkeyCli
2
+ class Application
3
+ module ManifestAction
4
+ class Link < Base
5
+ def init hargs_and_opts = {}
6
+ @args, @opts = args_and_opts(hargs_and_opts)
7
+ @opts = @opts.reverse_merge({
8
+ prefix: nil,
9
+ map: "d:d",
10
+ hard: false,
11
+ })
12
+ end
13
+
14
+ def prepare
15
+ @opts[:force] = app.opts[:default_yes]
16
+ @opts[:symbolic] = !@opts[:hard]
17
+ @source = @args[0]
18
+ @destination = @args[1]
19
+ map = @opts[:map].split(":")
20
+
21
+ # prefix source
22
+ @source = File.join(map[0]["d"] ? thor.destination_root : manifest.directory, @source)
23
+ @destination = File.join(map[1]["d"] ? thor.destination_root : manifest.directory, @destination)
24
+
25
+ # prefix target link
26
+ if(@opts[:prefix] && map[1].downcase["p"])
27
+ prefix = "cm--#{manifest.checksum(@opts[:prefix], soft: !map[1]["H"])}--"
28
+ @destination = File.join(File.dirname(@destination), "#{prefix}#{File.basename(@destination)}")
29
+ end
30
+ end
31
+
32
+ def simulate
33
+ if thor.options[:pretend]
34
+ destructive
35
+ else
36
+ status :fake, :black, rel(@destination)
37
+ end
38
+ end
39
+
40
+ def destructive
41
+ thor.create_link(@destination, @source, @opts)
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,41 @@
1
+ module ConfigmonkeyCli
2
+ class Application
3
+ module ManifestAction
4
+ class Mkdir < Base
5
+ def init directory, *sub_directories
6
+ @opts = sub_directories.extract_options!.reverse_merge({
7
+ #_p: true
8
+ })
9
+
10
+ # assemble directories
11
+ sub_directories.flatten!
12
+ if sub_directories.any?
13
+ @args = sub_directories.map {|d| File.join(directory, d) }
14
+ else
15
+ @args = [directory]
16
+ end
17
+ end
18
+
19
+ def prepare
20
+ @directories = @args.map{|dir| expand_dst(dir) }
21
+ end
22
+
23
+ def simulate
24
+ if thor.options[:pretend]
25
+ destructive
26
+ else
27
+ @directories.each do |dir|
28
+ status :fake, :black, @args[0]
29
+ end
30
+ end
31
+ end
32
+
33
+ def destructive
34
+ @directories.each do |dir|
35
+ thor.empty_directory(dir, @opts)
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,46 @@
1
+ module ConfigmonkeyCli
2
+ class Application
3
+ module ManifestAction
4
+ class Remove < Base
5
+ def init directory, *sub_directories
6
+ @opts = sub_directories.extract_options!.reverse_merge({
7
+ #_p: true
8
+ })
9
+
10
+ # assemble directories
11
+ sub_directories.flatten!
12
+ if sub_directories.any?
13
+ @args = sub_directories.map {|d| File.join(directory, d) }
14
+ else
15
+ @args = [directory]
16
+ end
17
+ end
18
+
19
+ def prepare
20
+ @opts[:force] = app.opts[:default_yes]
21
+ @directories = @args.map{|dir| expand_dst(dir) }
22
+ end
23
+
24
+ def simulate
25
+ if thor.options[:pretend]
26
+ destructive
27
+ else
28
+ @directories.each do |dir|
29
+ status :fake, :black, @args[0]
30
+ end
31
+ end
32
+ end
33
+
34
+ def destructive
35
+ @directories.each do |dir|
36
+ if FileTest.directory?(dir)
37
+ thor.remove_dir(dir, @opts)
38
+ else
39
+ thor.remove_file(dir, @opts)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,112 @@
1
+ module ConfigmonkeyCli
2
+ class Application
3
+ module ManifestAction
4
+ class Rsync < Base
5
+ def init hargs_and_opts = {}
6
+ @args, @opts = args_and_opts(hargs_and_opts)
7
+ @opts = @opts.reverse_merge({
8
+ binary: "rsync",
9
+ delete: true,
10
+ delay: true,
11
+ preview: false,
12
+ flags: [],
13
+ })
14
+ end
15
+
16
+ def prepare
17
+ @source = @args[0]
18
+ @destination = @args[1]
19
+ status :synchronize, :green, "#{@source} => #{@destination}"
20
+ end
21
+
22
+ def simulate
23
+ render_sync(do_sync(true))
24
+ end
25
+
26
+ def destructive
27
+ if @opts[:preview] && !(app.opts[:default_yes] || app.opts[:default_accept])
28
+ preview = do_sync(true)
29
+ render_sync(preview)
30
+ if preview.any?
31
+ if manifest.yes?("Apply changes?", default: @opts[:preview] == true || [:y, :yes].include?(@opts[:preview].to_sym))
32
+ render_sync do_sync
33
+ end
34
+ end
35
+ else
36
+ render_sync do_sync
37
+ end
38
+ end
39
+
40
+ # ----------------------------------------------
41
+
42
+ def str_flags force_dry = false
43
+ ([].tap{|f|
44
+ f << "--archive"
45
+ f << "--whole-file" # better I guess?
46
+ f << "--dry-run" if force_dry || app.opts[:simulation]
47
+ f << "--itemize-changes" # for parsing and display
48
+ f << "--delay-updates" if @opts[:delay]
49
+ if @opts[:delete]
50
+ f << (@opts[:delay] ? "--delete-#{@opts[:delay].is_a?(String) ? @opts[:delay] : "delay"}" : "--delete")
51
+ end
52
+ }.compact + @opts[:flags]).join(" ")
53
+ end
54
+
55
+ def rsync_command src, dst, force_dry = false
56
+ [
57
+ @opts[:binary],
58
+ str_flags(force_dry),
59
+ Shellwords.escape(expand_src(src)),
60
+ Shellwords.escape(expand_dst(dst))
61
+ ].join(" ")
62
+ end
63
+
64
+ def status_color_from_mode mode
65
+ if mode[1] == "f"
66
+ :white
67
+ elsif mode == "*deleting"
68
+ :red
69
+ elsif mode[1] == "d"
70
+ :blue
71
+ else
72
+ :white
73
+ end
74
+ end
75
+
76
+ def status_color_for_mode mode
77
+ if mode[0] == ">" || mode[0..1] == "cd"
78
+ :green
79
+ elsif mode == "*deleting"
80
+ :red
81
+ else
82
+ :yellow
83
+ end
84
+ end
85
+
86
+ def render_sync ary
87
+ ary.each do |mode, file|
88
+ status mode, status_color_for_mode(mode), file, status_color_from_mode(mode)
89
+ end
90
+ end
91
+
92
+ def do_sync force_dry = false
93
+ cmd = rsync_command(@source, @destination, force_dry)
94
+ code, res = exec(cmd)
95
+ raise "rsync exited with status #{code}: #{res}" unless code.exitstatus == 0
96
+ res.split("\n").map{|l| l.split(" ", 2) }
97
+ end
98
+
99
+ def exec cmd, chomp = true
100
+ app.debug "§invoking:#{cmd}"
101
+ _stdin, _stdouterr, _thread = Open3.popen2e(cmd)
102
+ _thread.join
103
+ res = _stdouterr.read
104
+ [_thread.value, chomp ? res.chomp : res]
105
+ ensure
106
+ _stdin.close rescue false
107
+ _stdouterr.close rescue false
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end