grably 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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