summaryrefslogtreecommitdiff
path: root/lib/gitlab/ci/cron_parser.rb
blob: 7334a112ccf2358de169fe8a15a505552d0fdc7d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# frozen_string_literal: true

module Gitlab
  module Ci
    class CronParser
      VALID_SYNTAX_SAMPLE_TIME_ZONE = 'UTC'
      VALID_SYNTAX_SAMPLE_CRON = '* * * * *'

      class << self
        def parse_natural(expression, cron_timezone = 'UTC')
          new(Fugit::Nat.parse(expression)&.original, cron_timezone)
        end

        # This method generates compatible expressions that can be
        # parsed by Fugit::Nat.parse to generate a cron line.
        # It takes start date of the cron and cadence in the following format:
        # cadence = {
        #   unit: 'day/week/month/year'
        #   duration: 1
        # }
        def parse_natural_with_timestamp(starts_at, cadence)
          case cadence[:unit]
          when 'day' # Currently supports only 'every 1 day'.
            "#{starts_at.min} #{starts_at.hour} * * *"
          when 'week' # Currently supports only 'every 1 week'.
            "#{starts_at.min} #{starts_at.hour} * * #{starts_at.wday}"
          when 'month'
            unless [1, 3, 6, 12].include?(cadence[:duration])
              raise NotImplementedError, "The cadence #{cadence} is not supported"
            end

            "#{starts_at.min} #{starts_at.hour} #{starts_at.mday} #{fall_in_months(cadence[:duration], starts_at)} *"
          when 'year' # Currently supports only 'every 1 year'.
            "#{starts_at.min} #{starts_at.hour} #{starts_at.mday} #{starts_at.month} *"
          else
            raise NotImplementedError, "The cadence unit #{cadence[:unit]} is not implemented"
          end
        end

        def fall_in_months(offset, start_date)
          (1..(12 / offset)).map { |i| start_date.next_month(offset * i).month }.join(',')
        end
      end

      def initialize(cron, cron_timezone = 'UTC')
        @cron = cron
        @cron_timezone = timezone_name(cron_timezone)
      end

      def next_time_from(time)
        cron_line.next_time(time).utc.in_time_zone(Time.zone) if cron_line.present?
      end

      def previous_time_from(time)
        cron_line.previous_time(time).utc.in_time_zone(Time.zone) if cron_line.present?
      end

      def cron_valid?
        try_parse_cron(@cron, VALID_SYNTAX_SAMPLE_TIME_ZONE).present?
      end

      def cron_timezone_valid?
        try_parse_cron(VALID_SYNTAX_SAMPLE_CRON, @cron_timezone).present?
      end

      def match?(time)
        cron_line.match?(time)
      end

      private

      def timezone_name(timezone)
        ActiveSupport::TimeZone.find_tzinfo(timezone).name
      rescue TZInfo::InvalidTimezoneIdentifier
        timezone
      end

      # NOTE:
      # cron_timezone can only accept timezones listed in TZInfo::Timezone.
      # Aliases of Timezones from ActiveSupport::TimeZone are NOT accepted,
      # because Fugit::Cron only supports TZInfo::Timezone.
      #
      # For example, those codes have the same effect.
      # Time.zone = 'Pacific Time (US & Canada)' (ActiveSupport::TimeZone)
      # Time.zone = 'America/Los_Angeles' (TZInfo::Timezone)
      #
      # However, try_parse_cron only accepts the latter format.
      # try_parse_cron('* * * * *', 'Pacific Time (US & Canada)') -> Doesn't work
      # try_parse_cron('* * * * *', 'America/Los_Angeles') -> Works
      # If you want to know more, please take a look
      # https://github.com/rails/rails/blob/master/activesupport/lib/active_support/values/time_zone.rb
      def try_parse_cron(cron, cron_timezone)
        Fugit::Cron.parse("#{cron} #{cron_timezone}")
      end

      def cron_line
        @cron_line ||= try_parse_cron(@cron, @cron_timezone)
      end
    end
  end
end