grably 0.0.1

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 (51) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +202 -0
  3. data/README.md +2 -0
  4. data/exe/grably +2 -0
  5. data/lib/ext/class.rb +28 -0
  6. data/lib/grably.rb +83 -0
  7. data/lib/grably/cli.rb +46 -0
  8. data/lib/grably/core.rb +15 -0
  9. data/lib/grably/core/app/enchancer.rb +36 -0
  10. data/lib/grably/core/application.rb +8 -0
  11. data/lib/grably/core/colors.rb +86 -0
  12. data/lib/grably/core/commands.rb +23 -0
  13. data/lib/grably/core/commands/cp.rb +103 -0
  14. data/lib/grably/core/commands/digest.rb +12 -0
  15. data/lib/grably/core/commands/log.rb +19 -0
  16. data/lib/grably/core/commands/run.rb +85 -0
  17. data/lib/grably/core/commands/serialize.rb +16 -0
  18. data/lib/grably/core/configuration.rb +39 -0
  19. data/lib/grably/core/configuration/pretty_print.rb +22 -0
  20. data/lib/grably/core/digest.rb +93 -0
  21. data/lib/grably/core/dsl.rb +15 -0
  22. data/lib/grably/core/essentials.rb +49 -0
  23. data/lib/grably/core/module.rb +64 -0
  24. data/lib/grably/core/product.rb +301 -0
  25. data/lib/grably/core/task.rb +30 -0
  26. data/lib/grably/core/task/bucket.rb +29 -0
  27. data/lib/grably/core/task/enchancer.rb +50 -0
  28. data/lib/grably/core/task/expand.rb +15 -0
  29. data/lib/grably/core/task/jobs.rb +58 -0
  30. data/lib/grably/job.rb +28 -0
  31. data/lib/grably/job/class.rb +93 -0
  32. data/lib/grably/job/exceptions.rb +0 -0
  33. data/lib/grably/job/instance.rb +159 -0
  34. data/lib/grably/job/manifest.rb +67 -0
  35. data/lib/grably/jobs.rb +4 -0
  36. data/lib/grably/jobs/sync.rb +91 -0
  37. data/lib/grably/jobs/text.rb +4 -0
  38. data/lib/grably/jobs/text/erb.rb +40 -0
  39. data/lib/grably/jobs/text/json.rb +12 -0
  40. data/lib/grably/jobs/text/text.rb +21 -0
  41. data/lib/grably/jobs/text/yaml.rb +12 -0
  42. data/lib/grably/jobs/unzip.rb +1 -0
  43. data/lib/grably/jobs/upload.rb +1 -0
  44. data/lib/grably/jobs/zip.rb +2 -0
  45. data/lib/grably/jobs/zip/unzip.rb +24 -0
  46. data/lib/grably/jobs/zip/zip.rb +46 -0
  47. data/lib/grably/runner.rb +31 -0
  48. data/lib/grably/server.rb +83 -0
  49. data/lib/grably/utils/pretty_printer.rb +63 -0
  50. data/lib/grably/version.rb +12 -0
  51. metadata +164 -0
@@ -0,0 +1,15 @@
1
+ module Grably
2
+ module Core
3
+ module TaskExtensions
4
+ # Add expand method to Task
5
+ module Expand
6
+ # Expands expression in task context
7
+ # @param expr [Object] expand expression
8
+ # @return Array<Product> result of expand in task context
9
+ def expand(expr)
10
+ Product.expand(expr, self)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,58 @@
1
+ module Grably
2
+ module Core
3
+ module TaskExtensions
4
+ # # Jobs
5
+ # @see Grably::Core::Job
6
+ module Jobs
7
+ def method_missing(meth, *args, &block)
8
+ job_class = find_job_class(meth.to_s)
9
+ if job_class
10
+ execute_job_with_args(args, job_class, meth)
11
+ else
12
+ super
13
+ end
14
+ end
15
+
16
+ def execute_job_with_args(args, job_class, meth)
17
+ working_dir = job_dir(task_dir, meth.to_s)
18
+ FileUtils.mkdir_p(working_dir)
19
+ job_class.new.run(self, working_dir, *args)
20
+ end
21
+
22
+ # Create working directory for instantiated job inside task directory
23
+ # @param [String] base_dir task working directory
24
+ # @param [String] job_name Grably::Job call name
25
+ # @return [String] job working directory
26
+ def job_dir(base_dir, job_name)
27
+ # All this flow is working under assumption that all task jobs called
28
+ # in same order. We store counter for each job in Task instance and
29
+ # it updated throug all task live time. Each time task instance is
30
+ # recreated we use frech (zero) counter
31
+ counter = (jobs[job_name] || -1) + 1
32
+ jobs[job_name] = counter
33
+ name = [job_name, counter.to_s.rjust(3, '0')].join('-')
34
+ File.join(base_dir, name)
35
+ end
36
+
37
+ def jobs
38
+ @jobs ||= {}
39
+ end
40
+
41
+ def respond_to_missing?(meth, include_private = false)
42
+ find_job_class(meth) || super
43
+ end
44
+
45
+ private
46
+
47
+ def find_job_class(name)
48
+ n = name.to_sym
49
+ all_classes = Grably::Job.jobs.flat_map do |c|
50
+ ObjectSpace.each_object(Class).select { |klass| klass <= c }
51
+ end
52
+
53
+ all_classes.find { |c| c.job_call_name == n }
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,28 @@
1
+ require_relative 'core/product'
2
+ require_relative 'core/digest'
3
+
4
+ require_relative 'job/exceptions'
5
+ require_relative 'job/manifest'
6
+ require_relative 'job/class'
7
+ require_relative 'job/instance'
8
+
9
+ module Grably
10
+ # TBD
11
+ module Job
12
+ # includes "Job::InstanceMethods" and extends "Job::ClassMethods"
13
+ # using the Job.included callback.
14
+ # @!parse include Job::InstanceMethods
15
+ # @!parse extend Job::ClassMethods
16
+ class << self
17
+ def included(receiver)
18
+ receiver.extend ClassMethods
19
+ receiver.send :include, InstanceMethods
20
+ jobs << receiver
21
+ end
22
+
23
+ def jobs
24
+ @jobs ||= []
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,93 @@
1
+ module Grably
2
+ module Job
3
+ module ClassMethods # :nodoc:
4
+ # Mark instance variable as source product container. When field contains
5
+ # source product job will track its state between launches. If product
6
+ # was changed since last run job is considered as changed and will be
7
+ # rebuild. This method also generates attribute_reader for given variable.
8
+ # @param name [Symbol] attribute name
9
+ # @param extras [Hash] extra arguments. Now unsued.
10
+ def src(name, extras = {})
11
+ class_exec do
12
+ attr_reader name
13
+ register_job_argument(name, :src, extras)
14
+ end
15
+ end
16
+
17
+ # Mark instance variable as multiple source product container. When field
18
+ # contains multiple src products job will track each product state
19
+ # between launches. If any of source products was changed since last run
20
+ # job is considered as changed and will be rebuild. This method also
21
+ # generates attribute_reader for given variable.
22
+ # @param name [Symbol] attribute name
23
+ # @param extras [Hash] extra arguments. Now unsued.
24
+ def srcs(name, extras = {})
25
+ class_exec do
26
+ attr_reader name
27
+ register_job_argument(name, :srcs, extras)
28
+ end
29
+ end
30
+
31
+ # Mark instance variable as incremental sources container. When field
32
+ # contains incremental sources it does not impact on job state. But job
33
+ # setup will generate state delta between two launches. This allows to
34
+ # decide if job should be rebuild completely or only changed files should
35
+ # be rebuild. This method generates two syntatic instance methods:
36
+ # * standart attribute accessor which contains current products
37
+ # * ! attribute_accessor (i.e. foo! ) which will return delta array in
38
+ # following format: [ modifications, additions, deletions ]
39
+ # @param name [Symbol] attribute name
40
+ # @param extras [Hash] extra arguments. Now unsued.
41
+ def srcs!(name, extras = {})
42
+ class_exec do
43
+ attr_reader name
44
+ register_job_argument(name, :isrcs, extras)
45
+ eval("def #{name}!; @deltas[:#{name}]; end") # rubocop:disable Security/Eval
46
+ end
47
+ end
48
+
49
+ # Mark instance variable as option container. When field contains option
50
+ # job will track its value between launches. If value was changed since
51
+ # last run job is considered as changed and will be rebuild. This method
52
+ # also generates attribute_reader for given variable.
53
+ # @param name [Symbol] attribute name
54
+ # @param extras [Hash] extra arguments. Now unsued.
55
+ def opt(name, extras = {})
56
+ class_exec do
57
+ attr_reader name
58
+ register_job_argument(name, :opt, extras)
59
+ end
60
+ end
61
+
62
+ def register_job_argument(name, type, extras)
63
+ job_args[name] = [type, extras]
64
+ end
65
+
66
+ def job_args
67
+ @job_args ||= {}
68
+ end
69
+
70
+ def job_call_name
71
+ return unless name # TODO: in runtime some anonymous sublcass instances
72
+ # can be created find out when and why
73
+ # job_call_name by user with call_as method. Yet it is not required
74
+ # if job_call_name empty we'll try to sytetize job name
75
+ @job_call_name ||= synthesize_job_name
76
+ end
77
+
78
+ def call_as(name)
79
+ @job_call_name = name
80
+ end
81
+
82
+ # Try to syntetize job name from class name
83
+ # * Strip all parent modules names
84
+ # * Cut off Job postfix from class name if any
85
+ # * convert class name to snake case
86
+ def synthesize_job_name
87
+ klass_name = name.scan(/[^:]+/).last
88
+ klass_name = klass_name[/(.+)Job/, 1] || klass_name
89
+ klass_name.scan(/[A-Z][a-z]+/).map(&:downcase).join('_').to_sym
90
+ end
91
+ end
92
+ end
93
+ end
File without changes
@@ -0,0 +1,159 @@
1
+ module Grably
2
+ module Job
3
+ module InstanceMethods # rubocop:disable Metrics/ModuleLength, Style/Documentation
4
+ def run(task, working_dir, *args) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
5
+ prepare(task, working_dir)
6
+ initialize_state(*args) if stateful?
7
+
8
+ if changed?
9
+ clean unless incremental?
10
+ log " * [#{self.class.job_call_name}] building"
11
+ result = @manifest.result = Grably::Core::Product.expand(build)
12
+ @manifest.dump
13
+ else
14
+ log " * [#{self.class.job_call_name}] uptodate"
15
+ result = @manifest.result
16
+ end
17
+
18
+ result
19
+ rescue StandardError => err
20
+ # If any error occured this will force rebuild on next run
21
+ @manifest.remove if @manifest
22
+ raise(err)
23
+ end
24
+
25
+ # When job is changed it need to be reevaluated so build method will be
26
+ # launched. If job remains unchanged previous result will be returned
27
+ def changed?
28
+ @changed
29
+ end
30
+
31
+ # Means that job has state which can be tracked between launches. If
32
+ # job is stateless it always changed.
33
+ def stateful?
34
+ @stateful
35
+ end
36
+
37
+ # If job contains incremental srcs it is incremental. Incremental job
38
+ # does not clean self state between launches. It should be managed by
39
+ # user.
40
+ def incremental?
41
+ @job_args.any? { |_name, desc| desc.first == :isrc }
42
+ end
43
+
44
+ def job_dir(path = nil)
45
+ path.nil? ? @job_dir : File.join(@job_dir, path)
46
+ end
47
+
48
+ def meta
49
+ @manifest.meta
50
+ end
51
+
52
+ # Celans job state by removing all files from working directory
53
+ def clean
54
+ FileUtils.rm_rf(Dir[File.join(@job_dir, '*')])
55
+ end
56
+
57
+ private
58
+
59
+ def prepare(task, working_dir)
60
+ @job_dir = working_dir # initialize job dir
61
+ @t = task
62
+ @job_args = job_args_lookup
63
+ # We need to track state only if job has arguments.
64
+ @stateful = !@job_args.empty?
65
+ # If job is stateful it is not changed by default. Only after stat will
66
+ # be initialized we can say if it changed. If job has no state we should
67
+ # always rebuild it. So changed != stateful
68
+ @changed = !@stateful
69
+ # In case if job has incremental sources we should keep deltas
70
+ # somewhere. Deltas are kept in hash where they stored by field name.
71
+ @deltas = {}
72
+ end
73
+
74
+ # Executes job state initialization by assigning values to instance
75
+ # variables
76
+ def initialize_state(*args)
77
+ # Load previous state
78
+ @manifest = Manifest.new(job_dir)
79
+ _loaded = @manifest.load # try to load manifest
80
+
81
+ if self.class.method_defined?(:setup)
82
+ # This means that class has user defined method for initialization so
83
+ # we should call this method for setup
84
+ setup(*args)
85
+ else
86
+ # If no setup method defined we will use synthetic setup which assumes
87
+ # that arguments is a Hash where keys is job argument names
88
+ synthetic_setup(*args)
89
+ end
90
+ expand_job_arguments
91
+ end
92
+
93
+ def synthetic_setup(args)
94
+ check_synthetic_arguments(args)
95
+ args.each { |k, v| instance_variable_set("@#{k}", v) }
96
+ end
97
+
98
+ def check_synthetic_arguments(args)
99
+ raise "Expected Hash got #{args.inspect}" unless args.is_a?(Hash)
100
+ job_args = @job_args.keys
101
+ extra = args.keys - job_args
102
+ raise "Unknown arguments: #{extra.join(', ')}" unless extra.empty?
103
+ missing = job_args - args.keys
104
+ raise "Missing arguments #{missing.join(', ')}" unless missing.empty?
105
+ end
106
+
107
+ # Walks through all job arguments and does proper expand operation for its
108
+ # value
109
+ def expand_job_arguments
110
+ @job_args.each do |name, desc|
111
+ type, _extras = desc
112
+ value = instance_variable_get("@#{name}")
113
+ update_argument(name, type, value)
114
+ end
115
+ end
116
+
117
+ def update_argument(name, type, value) # rubocop:disable Metrics/MethodLength
118
+ case type
119
+ when :src
120
+ # src expects single product
121
+ value = Product.expand(value, @t)
122
+ raise(ArgumentError, "Expected only one product for #{name}") unless value.length == 1
123
+ value = value.first
124
+ when :srcs, :isrcs
125
+ # src and isrcs expects multiple products
126
+ value = Product.expand(value, @t)
127
+ when :opt # rubocop:disable Lint/EmptyWhen
128
+ # do nothing, we do not expand opt values. keeping them as is
129
+ when nil
130
+ raise(ArgumentError, "#{name} not defined")
131
+ else
132
+ raise(ArgumentError, "Unknown type #{type} for #{name}")
133
+ end
134
+
135
+ @manifest.update(name, type, value, ->(*a) { on_job_arg_set(*a) })
136
+ instance_variable_set("@#{name}", value)
137
+ end
138
+
139
+ # rubocop:disable Metrics/ParameterLists
140
+ def on_job_arg_set(name, type, old_digest, old_val, new_digest, new_val)
141
+ if type == :isrcs
142
+ # isrcs never impact changed state. We just need to gather infomration
143
+ # about what actualy was changed
144
+ @deltas[name] = Core::Digest.diff_digests(old_digest, new_digest)
145
+ elsif old_val != new_val || old_digest != new_digest
146
+ @changed = true
147
+ end
148
+ end
149
+ # rubocop:enable Metrics/ParameterLists
150
+
151
+ def job_args_lookup(klass = self.class, args = {})
152
+ job_args_lookup(klass.superclass, args) if klass.superclass
153
+ args.update(klass.job_args) if klass.included_modules.include?(Grably::Job)
154
+
155
+ args
156
+ end
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,67 @@
1
+ require 'ostruct'
2
+ require 'fileutils'
3
+
4
+ module Grably
5
+ module Job
6
+ # Manifest file with job data. Manifest keeps information about previous run
7
+ # including passed arguments and output file digests
8
+ class Manifest
9
+ # Manifest file location relative to jobdir
10
+ MANIFEST = '.manifest'.freeze
11
+
12
+ attr_reader :manifest_file
13
+
14
+ def initialize(job_dir)
15
+ @job_dir = job_dir
16
+ @data = OpenStruct.new(src: {}, srcs: {}, opt: {}, result: nil, meta: {})
17
+ @manifest_file = File.join(job_dir, MANIFEST)
18
+ end
19
+
20
+ def update(name, type, value, update_hook) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
21
+ case type
22
+ when :src
23
+ old_value, old_digest = @data.src[name]
24
+ new_digest = Grably::Digest.digest(value).first
25
+ when :srcs
26
+ old_value, old_digest = @data.srcs[name]
27
+ new_digest = Grably::Digest.digest(*value)
28
+ when :opt
29
+ old_value, _old_digest = @data.opt[name]
30
+ old_digest = nil
31
+ new_digest = nil
32
+ else
33
+ raise ArgumentError, 'Invalid type: ' + type
34
+ end
35
+
36
+ @data.send(type)[name] = [value, new_digest]
37
+ update_hook.call(name, type, old_value, old_digest, value, new_digest)
38
+ [old_value, old_digest]
39
+ end
40
+
41
+ def result=(products)
42
+ digests = Grably::Digest.digest(*products)
43
+ @data.result = [products, digests]
44
+ end
45
+
46
+ def meta
47
+ @data.meta
48
+ end
49
+
50
+ def result
51
+ @data.result.first
52
+ end
53
+
54
+ def load
55
+ @data = load_obj(manifest_file) if File.exist?(manifest_file)
56
+ end
57
+
58
+ def dump
59
+ save_obj(manifest_file, @data)
60
+ end
61
+
62
+ def remove
63
+ FileUtils.rm_f(manifest_file)
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,4 @@
1
+ require_relative 'jobs/text'
2
+ require_relative 'jobs/zip'
3
+ require_relative 'jobs/sync'
4
+ require_relative 'jobs/upload'
@@ -0,0 +1,91 @@
1
+ module Grably # :nodoc:
2
+ # TBD
3
+ class SyncJob
4
+ include Grably::Job
5
+
6
+ PROTO_SSH = %r{^ssh://(.*)$}
7
+ SSH_HOST = /(.+?):(.+?)@(.+)/
8
+ DEFAULT_RSYNC_PARAMS = %w(-avz --progress --delete).freeze
9
+
10
+ srcs :files
11
+ opt :dst
12
+
13
+ opt :host
14
+ opt :proto
15
+ opt :no_partial
16
+ opt :ssh_key
17
+ opt :ssh_pass
18
+ opt :ssh_port
19
+
20
+ def setup(srcs, dst = nil, _p = {})
21
+ @files = srcs
22
+ @dst = dst || job_dir
23
+
24
+ @proto = :file
25
+
26
+ unpack_dst_hash(@dst) unless @dst.is_a?(String)
27
+ @dst.match(PROTO_SSH) do |m|
28
+ @proto = :ssh
29
+ @dst = m[1]
30
+ end
31
+ setup_ssh_opts if @proto == :ssh
32
+ end
33
+
34
+ def changed?
35
+ true
36
+ end
37
+
38
+ def build
39
+ trace "Syncing products to #{dst}"
40
+ cp_smart(files, dst, log: proto == :file)
41
+ ssh_sync if proto == :ssh
42
+ proto == :ssh ? [] : dst
43
+ end
44
+
45
+ private
46
+
47
+ def ssh_cmd
48
+ cmd = %w(ssh)
49
+ cmd += ['-i', ssh_key] if ssh_key
50
+ cmd += ['-p', ssh_port] if ssh_port
51
+ cmd = ['sshpass', '-p', ssh_pass] + cmd if ssh_pass
52
+ "'#{cmd.join(' ')}'"
53
+ end
54
+
55
+ def unpack_dst_hash(dst)
56
+ @proto = :ssh
57
+ @no_partial = dst[:no_partial]
58
+ @ssh_key = dst[:ssh_key]
59
+ @ssh_pass = dst[:ssh_pass]
60
+ @ssh_port = dst[:ssh_port]
61
+ @dst = dst[:host]
62
+ end
63
+
64
+ def ssh_sync
65
+ if files.empty?
66
+ warn 'Nothing to sync' if files.empty?
67
+ end
68
+
69
+ log "Syncing #{files.size} files to #{host}"
70
+ ['rsync', *rsync_params, @dst, '-e', ssh_cmd, host].run_log
71
+ end
72
+
73
+ def rsync_params
74
+ params = DEFAULT_RSYNC_PARAMS.dup
75
+ params << '--partial' unless no_partial
76
+
77
+ params
78
+ end
79
+
80
+ def setup_ssh_opts
81
+ @host = @dst
82
+ # '/' at the end will cause to sync internal file structure
83
+ @dst = job_dir('files') + '/'
84
+
85
+ @host.match(SSH_HOST) do |m|
86
+ @ssh_pass = m[2]
87
+ @host = [m[1], m[2]].join('@')
88
+ end
89
+ end
90
+ end
91
+ end