xronor 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,27 @@
1
+ module Xronor
2
+ class DSL
3
+ class Default
4
+ DEFAULT_TIMEZONE = "UTC"
5
+
6
+ def initialize(&block)
7
+ @result = OpenStruct.new(
8
+ cron_timezone: DEFAULT_TIMEZONE,
9
+ prefix: "",
10
+ timezone: DEFAULT_TIMEZONE,
11
+ )
12
+
13
+ instance_eval(&block)
14
+ end
15
+
16
+ %i(cron_timezone prefix timezone).each do |key|
17
+ define_method(key) do |arg|
18
+ @result.send("#{key}=", arg)
19
+ end
20
+ end
21
+
22
+ def result
23
+ @result
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,66 @@
1
+ module Xronor
2
+ class DSL
3
+ class Job
4
+ include Xronor::DSL::Checker
5
+
6
+ def initialize(frequency, options, &block)
7
+ @frequency = frequency
8
+ @options = options
9
+
10
+ schedule = case frequency
11
+ when String # cron expression
12
+ frequency
13
+ when Symbol, Numeric # DSL (:hour, 1.min, 1.hour, ...)
14
+ Xronor::DSL::ScheduleConverter.convert(frequency, options)
15
+ else
16
+ raise ArgumentError, "Invalid frequency #{frequency}"
17
+ end
18
+
19
+ @result = OpenStruct.new(
20
+ description: nil,
21
+ name: "",
22
+ schedule: schedule,
23
+ command: "",
24
+ )
25
+
26
+ instance_eval(&block)
27
+ end
28
+
29
+ %i(description name).each do |key|
30
+ define_method(key) do |arg|
31
+ @result.send("#{key}=", arg)
32
+ end
33
+ end
34
+
35
+ def process_template(template, options)
36
+ template.gsub(/:\w+/) do |key|
37
+ before_and_after = [$`[-1..-1], $'[0..0]]
38
+ option = options[key.sub(':', '').to_sym] || key
39
+
40
+ if before_and_after.all? { |c| c == "'" }
41
+ escape_single_quotes(option)
42
+ elsif before_and_after.all? { |c| c == '"' }
43
+ escape_double_quotes(option)
44
+ else
45
+ option
46
+ end
47
+ end.gsub(/\s+/m, " ").strip
48
+ end
49
+
50
+ def result
51
+ required(:name, @result.name)
52
+ @result
53
+ end
54
+
55
+ private
56
+
57
+ def escape_single_quotes(str)
58
+ str.gsub(/'/) { "'\\''" }
59
+ end
60
+
61
+ def escape_double_quotes(str)
62
+ str.gsub(/"/) { '\"' }
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,75 @@
1
+ # This file is derived from Whenever, Copyright (c) 2017 Javan Makhmali
2
+ # Original code:
3
+ # https://github.com/javan/whenever/blob/1dcb91484e6f1ee91c9272daccbe84111754102b/lib/whenever/numeric_seconds.rb
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person
6
+ # obtaining a copy of this software and associated documentation
7
+ # files (the "Software"), to deal in the Software without
8
+ # restriction, including without limitation the rights to use,
9
+ # copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ # copies of the Software, and to permit persons to whom the
11
+ # Software is furnished to do so, subject to the following
12
+ # conditions:
13
+ #
14
+ # The above copyright notice and this permission notice shall be
15
+ # included in all copies or substantial portions of the Software.
16
+ #
17
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
19
+ # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
21
+ # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
22
+ # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23
+ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
24
+ # OTHER DEALINGS IN THE SOFTWARE.
25
+ #
26
+ # Modifications are Copyright 2017 Daisuke Fujita. License under the MIT License.
27
+
28
+ module Xronor
29
+ class DSL
30
+ class NumericSeconds
31
+ def self.seconds(number, units)
32
+ self.new(number).send(units)
33
+ end
34
+
35
+ def initialize(number)
36
+ @number = number.to_i
37
+ end
38
+
39
+ def seconds
40
+ @number
41
+ end
42
+ alias :second :seconds
43
+
44
+ def minutes
45
+ @number * 60
46
+ end
47
+ alias :minute :minutes
48
+
49
+ def hours
50
+ @number * 60 * 60
51
+ end
52
+ alias :hour :hours
53
+
54
+ def days
55
+ @number * 60 * 60 * 24
56
+ end
57
+ alias :day :days
58
+
59
+ def weeks
60
+ @number * 60 * 60 * 24 * 7
61
+ end
62
+ alias :week :weeks
63
+
64
+ def months
65
+ @number * 60 * 60 * 24 * 30
66
+ end
67
+ alias :month :months
68
+
69
+ def years
70
+ @number * 60 * 60 * 24 * 365 + 60 * 60 * 6 # consider leap year
71
+ end
72
+ alias :year :years
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,147 @@
1
+ # Part of logic in this file is derived from Whenever, Copyright (c) 2017 Javan Makhmali
2
+ # Original code:
3
+ # https://github.com/javan/whenever/blob/1dcb91484e6f1ee91c9272daccbe84111754102b/lib/whenever/cron.rb
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person
6
+ # obtaining a copy of this software and associated documentation
7
+ # files (the "Software"), to deal in the Software without
8
+ # restriction, including without limitation the rights to use,
9
+ # copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ # copies of the Software, and to permit persons to whom the
11
+ # Software is furnished to do so, subject to the following
12
+ # conditions:
13
+ #
14
+ # The above copyright notice and this permission notice shall be
15
+ # included in all copies or substantial portions of the Software.
16
+ #
17
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
19
+ # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
21
+ # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
22
+ # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23
+ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
24
+ # OTHER DEALINGS IN THE SOFTWARE.
25
+ #
26
+ # Modifications are Copyright 2017 Daisuke Fujita. License under the MIT License.
27
+
28
+ module Xronor
29
+ class DSL
30
+ class ScheduleConverter
31
+ WEEKDAYS = %i(sunday monday tuesday wednesday thursday friday saturday)
32
+
33
+ class << self
34
+ def convert(frequency, options)
35
+ self.new(frequency, options).convert
36
+ end
37
+ end
38
+
39
+ def initialize(frequency, options)
40
+ @frequency = frequency
41
+ @options = options
42
+ end
43
+
44
+ def convert
45
+ cron_at, dow_diff = case @options[:at]
46
+ when String
47
+ parse_and_convert_time
48
+ when Numeric
49
+ [@options[:at], 0]
50
+ else
51
+ [0, 0]
52
+ end
53
+
54
+ if WEEKDAYS.include?(@frequency) # :sunday, :monday, ..., :saturday
55
+ return cron_weekly(cron_at, dow_diff)
56
+ end
57
+
58
+ shortcut = case @frequency
59
+ when Numeric
60
+ @frequency
61
+ when :minute
62
+ Xronor::DSL.seconds(1, :minute)
63
+ when :hour
64
+ Xronor::DSL.seconds(1, :hour)
65
+ when :day
66
+ Xronor::DSL.seconds(1, :day)
67
+ end
68
+
69
+ raise ArgumentError, "Invalid frequency #{@frequency}" if !shortcut.is_a?(Numeric)
70
+
71
+ cron(shortcut, cron_at)
72
+ end
73
+
74
+ private
75
+
76
+ def comma_separated_timing(frequency, max, start = 0)
77
+ return start if frequency.nil? || frequency == "" || frequency == 0
78
+ return "*" if frequency == 1
79
+ return frequency if frequency > (max * 0.5) .ceil
80
+
81
+ original_start = start
82
+
83
+ start += frequency unless (max + 1).modulo(frequency).zero? || start > 0
84
+ output = (start..max).step(frequency).to_a
85
+
86
+ max_occurances = (max.to_f / (frequency.to_f)).round
87
+ max_occurances += 1 if original_start.zero?
88
+
89
+ output[0, max_occurances].join(',')
90
+ end
91
+
92
+ def cron(shortcut, cron_at)
93
+ digits = ["*", "*", "*", "*", "*"]
94
+
95
+ case shortcut
96
+ when Xronor::DSL.seconds(0, :second)...Xronor::DSL.seconds(1, :minute)
97
+ raise ArgumentError, "Time must be in minutes or higher"
98
+ when Xronor::DSL.seconds(1, :minute)...Xronor::DSL.seconds(1, :hour)
99
+ min_frequency = shortcut / 60
100
+ digits[0] = comma_separated_timing(min_frequency, 59, cron_at.is_a?(Time) ? cron_at.min : 0)
101
+ when Xronor::DSL.seconds(1, :hour)...Xronor::DSL.seconds(1, :day)
102
+ hour_frequency = (shortcut / 60 / 60).round
103
+ digits[0] = cron_at.is_a?(Time) ? cron_at.min : cron_at
104
+ digits[1] = comma_separated_timing(hour_frequency, 23, cron_at.is_a?(Time) ? cron_at.hour : 0)
105
+ when Xronor::DSL.seconds(1, :day)...Xronor::DSL.seconds(1, :month)
106
+ day_frequency = (shortcut / 24 / 60 / 60).round
107
+ digits[0] = cron_at.is_a?(Time) ? cron_at.min : 0
108
+ digits[1] = cron_at.is_a?(Time) ? cron_at.hour : cron_at
109
+ digits[2] = comma_separated_timing(day_frequency, 31, 1)
110
+ end
111
+
112
+ digits.join(" ")
113
+ end
114
+
115
+ def cron_weekly(cron_at, dow_diff)
116
+ dow = WEEKDAYS.index(@frequency)
117
+ dow += dow_diff
118
+
119
+ case dow
120
+ when -1 # Sunday -> Saturday
121
+ dow = 6
122
+ when 7 # Saturday -> Sunday
123
+ dow = 0
124
+ end
125
+
126
+ [
127
+ cron_at.is_a?(Time) ? cron_at.min : "0",
128
+ cron_at.is_a?(Time) ? cron_at.hour : "0",
129
+ "*",
130
+ "*",
131
+ dow,
132
+ ].join(" ")
133
+ end
134
+
135
+ def parse_and_convert_time
136
+ original_time_class = Chronic.time_class
137
+ Time.zone = @options[:timezone]
138
+ Chronic.time_class = Time.zone
139
+ local_at = Chronic.parse(@options[:at])
140
+ cron_at = local_at.in_time_zone(@options[:cron_timezone])
141
+ Chronic.time_class = original_time_class
142
+
143
+ return cron_at, (cron_at.wday - local_at.wday)
144
+ end
145
+ end
146
+ end
147
+ end
data/lib/xronor/dsl.rb ADDED
@@ -0,0 +1,63 @@
1
+ module Xronor
2
+ class DSL
3
+ DEFAULT_PREFIX = "scheduler-"
4
+ DEFAULT_TIMEZONE = "UTC"
5
+ DEFAULT_CRON_TIMEZONE = "UTC"
6
+ DEFAULT_JOB_TEMPLATE = ":job"
7
+
8
+ class DuplicatedError < StandardError; end
9
+
10
+ class << self
11
+ def eval(body)
12
+ self.new(body)
13
+ end
14
+
15
+ def seconds(number, units)
16
+ Xronor::DSL::NumericSeconds.seconds(number, units)
17
+ end
18
+ end
19
+
20
+ def initialize(body)
21
+ @result = OpenStruct.new(
22
+ jobs: {},
23
+ options: {
24
+ prefix: DEFAULT_PREFIX,
25
+ timezone: DEFAULT_TIMEZONE,
26
+ cron_timezone: DEFAULT_CRON_TIMEZONE,
27
+ job_template: DEFAULT_JOB_TEMPLATE,
28
+ },
29
+ )
30
+
31
+ instance_eval(body)
32
+ end
33
+
34
+ def default(&block)
35
+ @result.options.merge!(Xronor::DSL::Default.new(&block).result.to_h)
36
+ end
37
+
38
+ def every(frequency, options = {}, &block)
39
+ job = Xronor::DSL::Job.new(frequency, @result.options.merge(options), &block).result
40
+ raise Xronor::DSL::DuplicatedError, "Job \"#{job.name}\" already exists" if @result.jobs[job.name]
41
+ @result.jobs[job.name] = job
42
+ end
43
+
44
+ def job_template(template)
45
+ @result.options[:job_template] = template
46
+ end
47
+
48
+ def job_type(name, template)
49
+ Xronor::DSL::Job.class_eval do
50
+ define_method(name) do |task, *args|
51
+ options = { task: task }
52
+ options.merge!(args[0]) if args[0].is_a? Hash
53
+ job = process_template(template, options)
54
+ @result.command = process_template(@options[:job_template], options.merge({ job: job }))
55
+ end
56
+ end
57
+ end
58
+
59
+ def result
60
+ @result
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,77 @@
1
+ module Xronor
2
+ module Generator
3
+ class CloudWatchEvents
4
+ class << self
5
+ def generate(filename, options)
6
+ jobs = Xronor::Parser.parse(filename)
7
+ function_arn = lambda.retrieve_function_arn(options[:function])
8
+
9
+ current_jobs = cwe.list_jobs(options[:prefix])
10
+ add_jobs, delete_jobs = compare_jobs(options[:prefix], current_jobs, jobs)
11
+
12
+ added_rule_arns = add_jobs.map do |job|
13
+ if options[:dry_run]
14
+ puts "[DRYRUN] #{job.name} will be registered to CloudWatch Events"
15
+ else
16
+ arn = cwe.register_job(
17
+ job,
18
+ options[:prefix],
19
+ options[:cluster],
20
+ options[:task_definition],
21
+ options[:container],
22
+ function_arn,
23
+ )
24
+ puts "Registered #{arn}"
25
+ arn
26
+ end
27
+ end
28
+
29
+ if options[:dry_run]
30
+ else
31
+ dynamodb.sync_rule_arns(options[:table], added_rule_arns, [])
32
+ end
33
+
34
+ delete_jobs.each do |job|
35
+ if options[:dry_run]
36
+ puts "[DRYRUN] #{job} will be deregistered from CloudWatch Events"
37
+ else
38
+ cwe.deregister_job(job)
39
+ puts "Deregistered #{job}"
40
+ end
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def compare_jobs(prefix, current_jobs, next_jobs)
47
+ add_jobs, delete_jobs = [], []
48
+ next_job_names = next_jobs.map do |job|
49
+ job.cloud_watch_rule_name(prefix)
50
+ end
51
+
52
+ next_jobs.each do |job|
53
+ add_jobs << job unless current_jobs.include?(job.cloud_watch_rule_name(prefix))
54
+ end
55
+
56
+ current_jobs.each do |job|
57
+ delete_jobs << job unless next_job_names.include?(job)
58
+ end
59
+
60
+ return add_jobs, delete_jobs
61
+ end
62
+
63
+ def cwe
64
+ @cwe ||= Xronor::AWS::CloudWatchEvents.new
65
+ end
66
+
67
+ def dynamodb
68
+ @dynamodb ||= Xronor::AWS::DynamoDB.new
69
+ end
70
+
71
+ def lambda
72
+ @lambda ||= Xronor::AWS::Lambda.new
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,18 @@
1
+ module Xronor
2
+ module Generator
3
+ class Crontab
4
+ class << self
5
+ def generate(filename, options)
6
+ jobs = Xronor::Parser.parse(filename)
7
+
8
+ jobs.map do |job|
9
+ <<-EOS
10
+ # #{job.name} - #{job.description}
11
+ #{[job.schedule, job.command].join(" ")}
12
+ EOS
13
+ end.join("\n")
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,24 @@
1
+ module Xronor
2
+ module Generator
3
+ class ERB
4
+ class << self
5
+ def generate_all_in_one(filename, options)
6
+ @jobs = Xronor::Parser.parse(filename)
7
+ erb = open(options[:template]).read
8
+ ::ERB.new(erb, nil, "-").result(binding)
9
+ end
10
+
11
+ def generate_per_job(filename, options)
12
+ jobs = Xronor::Parser.parse(filename)
13
+ erb = open(options[:template]).read
14
+
15
+ jobs.inject({}) do |result, job|
16
+ @job = job
17
+ result[job.name.gsub(/[^\.\-_A-Za-z0-9]/, "_").downcase] = ::ERB.new(erb, nil, "-").result(binding)
18
+ result
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
data/lib/xronor/job.rb ADDED
@@ -0,0 +1,44 @@
1
+ module Xronor
2
+ class Job
3
+ DOM_INDEX = 2
4
+ DOW_INDEX = 4
5
+
6
+ def initialize(name, description, schedule, command)
7
+ @name = name
8
+ @description = description
9
+ @schedule = schedule
10
+ @command = command
11
+ end
12
+
13
+ attr_reader :command, :description, :name, :schedule
14
+
15
+ def cloud_watch_schedule
16
+ cron_fields = @schedule.split(" ")
17
+
18
+ if cron_fields[DOM_INDEX] == "*" && cron_fields[DOW_INDEX] == "*"
19
+ cron_fields[DOW_INDEX] = "?"
20
+ else
21
+ cron_fields[DOM_INDEX] = "?" if cron_fields[DOM_INDEX] == "*"
22
+
23
+ if cron_fields[DOW_INDEX] == "*"
24
+ cron_fields[DOW_INDEX] = "?"
25
+ else
26
+ cron_fields[DOW_INDEX] = cron_fields[DOW_INDEX].to_i + 1
27
+ end
28
+ end
29
+
30
+ cron_fields << "*" # Year
31
+ "cron(#{cron_fields.join(" ")})"
32
+ end
33
+
34
+ def cloud_watch_rule_name(prefix)
35
+ "#{prefix}#{@name}-#{hashcode}".gsub(/[^\.\-_A-Za-z0-9]/, "-").downcase
36
+ end
37
+
38
+ private
39
+
40
+ def hashcode
41
+ @hashcode ||= OpenSSL::Digest::SHA256.hexdigest("#{@name}\t#{@description}\t#{@schedule}\t#{@command}")[0..12]
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,13 @@
1
+ module Xronor
2
+ class Parser
3
+ def self.parse(filename)
4
+ body = open(filename).read
5
+ result = Xronor::DSL.eval(body).result
6
+
7
+ result.jobs.values.map do |job|
8
+ job.description ||= job.name
9
+ Xronor::Job.new(job.name, job.description, job.schedule, job.command)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,3 @@
1
+ module Xronor
2
+ VERSION = "0.1.0"
3
+ end
data/lib/xronor.rb ADDED
@@ -0,0 +1,30 @@
1
+ require "aws-sdk-core"
2
+ require "active_support/core_ext/time"
3
+ require "chronic"
4
+ require "erb"
5
+ require "openssl"
6
+ require "optparse"
7
+ require "shellwords"
8
+ require "thor"
9
+
10
+ require "xronor/aws/cloud_watch_events"
11
+ require "xronor/aws/dynamo_db"
12
+ require "xronor/aws/lambda"
13
+ require "xronor/cli"
14
+ require "xronor/core_ext/numeric"
15
+ require "xronor/dsl"
16
+ require "xronor/dsl/checker"
17
+ require "xronor/dsl/default"
18
+ require "xronor/dsl/job"
19
+ require "xronor/dsl/numeric_seconds"
20
+ require "xronor/dsl/schedule_converter"
21
+ require "xronor/generator/crontab"
22
+ require "xronor/generator/cloud_watch_events"
23
+ require "xronor/generator/erb"
24
+ require "xronor/job"
25
+ require "xronor/parser"
26
+ require "xronor/version"
27
+
28
+ module Xronor
29
+
30
+ end
data/xronor.gemspec ADDED
@@ -0,0 +1,32 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'xronor/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "xronor"
8
+ spec.version = Xronor::VERSION
9
+ spec.authors = ["Daisuke Fujita"]
10
+ spec.email = ["dtanshi45@gmail.com"]
11
+
12
+ spec.summary = %q{Whenever DSL -> CloudWatch Events Schedule Rule & ECS Task Parameter}
13
+ spec.description = %q{Convert Whenever DSL to CloudWatch Events Schedule Rule to invoke job on ECS}
14
+ spec.homepage = "https://github.com/dtan4/xronor"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
18
+ f.match(%r{^(test|spec|features)/})
19
+ end
20
+ spec.bindir = "exe"
21
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
+ spec.require_paths = ["lib"]
23
+
24
+ spec.add_dependency "activesupport", "~> 5.0.2"
25
+ spec.add_dependency "aws-sdk", "~> 2.8.7"
26
+ spec.add_dependency "chronic", "~> 0.10"
27
+ spec.add_dependency "thor", "~> 0.19"
28
+
29
+ spec.add_development_dependency "bundler", "~> 1.14"
30
+ spec.add_development_dependency "rake", "~> 10.0"
31
+ spec.add_development_dependency "rspec", "~> 3.0"
32
+ end