Commit 98176cb2 authored by Lin Jen-Shin's avatar Lin Jen-Shin

Merge branch '24295-freeze-period-service' into 'master'

Add Freeze Period Status

See merge request gitlab-org/gitlab!29244
parents 3d3be7eb 407b0aa2
......@@ -537,6 +537,7 @@ module Ci
.concat(job_variables)
.concat(environment_changed_page_variables)
.concat(persisted_environment_variables)
.concat(deploy_freeze_variables)
.to_runner_variables
end
end
......@@ -592,6 +593,18 @@ module Ci
end
end
def deploy_freeze_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
break variables unless freeze_period?
variables.append(key: 'CI_DEPLOY_FREEZE', value: 'true')
end
end
def freeze_period?
Ci::FreezePeriodStatus.new(project: project).execute
end
def features
{ trace_sections: true }
end
......
# frozen_string_literal: true
module Ci
class FreezePeriodStatus
attr_reader :project
def initialize(project:)
@project = project
end
def execute
project.freeze_periods.any? { |period| within_freeze_period?(period) }
end
def within_freeze_period?(period)
# previous_freeze_end, ..., previous_freeze_start, ..., NOW, ..., next_freeze_end, ..., next_freeze_start
# Current time is within a freeze period if
# it falls between a previous freeze start and next freeze end
start_freeze = Gitlab::Ci::CronParser.new(period.freeze_start, period.cron_timezone)
end_freeze = Gitlab::Ci::CronParser.new(period.freeze_end, period.cron_timezone)
previous_freeze_start = previous_time(start_freeze)
previous_freeze_end = previous_time(end_freeze)
next_freeze_start = next_time(start_freeze)
next_freeze_end = next_time(end_freeze)
previous_freeze_end < previous_freeze_start &&
previous_freeze_start <= time_zone_now &&
time_zone_now <= next_freeze_end &&
next_freeze_end < next_freeze_start
end
private
def previous_time(cron_parser)
cron_parser.previous_time_from(time_zone_now)
end
def next_time(cron_parser)
cron_parser.next_time_from(time_zone_now)
end
def time_zone_now
@time_zone_now ||= Time.zone.now
end
end
end
---
title: Add freeze periods via CI_DEPLOY_FREEZE variable
merge_request: 29244
author:
type: added
......@@ -12,8 +12,11 @@ module Gitlab
end
def next_time_from(time)
@cron_line ||= try_parse_cron(@cron, @cron_timezone)
@cron_line.next_time(time).utc.in_time_zone(Time.zone) if @cron_line.present?
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?
......@@ -49,6 +52,10 @@ module Gitlab
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
......@@ -7,198 +7,240 @@ describe Gitlab::Ci::CronParser do
it { is_expected.to be > Time.now }
end
describe '#next_time_from' do
subject { described_class.new(cron, cron_timezone).next_time_from(Time.now) }
shared_examples_for "returns time in the past" do
it { is_expected.to be < Time.now }
end
context 'when cron and cron_timezone are valid' do
context 'when specific time' do
let(:cron) { '3 4 5 6 *' }
let(:cron_timezone) { 'UTC' }
shared_examples_for 'when cron and cron_timezone are valid' do |returns_time_for_epoch|
context 'when specific time' do
let(:cron) { '3 4 5 6 *' }
let(:cron_timezone) { 'UTC' }
it_behaves_like "returns time in the future"
it_behaves_like returns_time_for_epoch
it 'returns exact time' do
expect(subject.min).to eq(3)
expect(subject.hour).to eq(4)
expect(subject.day).to eq(5)
expect(subject.month).to eq(6)
end
it 'returns exact time' do
expect(subject.min).to eq(3)
expect(subject.hour).to eq(4)
expect(subject.day).to eq(5)
expect(subject.month).to eq(6)
end
end
context 'when specific day of week' do
let(:cron) { '* * * * 0' }
let(:cron_timezone) { 'UTC' }
context 'when specific day of week' do
let(:cron) { '* * * * 0' }
let(:cron_timezone) { 'UTC' }
it_behaves_like "returns time in the future"
it_behaves_like returns_time_for_epoch
it 'returns exact day of week' do
expect(subject.wday).to eq(0)
end
it 'returns exact day of week' do
expect(subject.wday).to eq(0)
end
end
context 'when slash used' do
let(:cron) { '*/10 */6 */10 */10 *' }
let(:cron_timezone) { 'UTC' }
context 'when slash used' do
let(:cron) { '*/10 */6 */10 */10 *' }
let(:cron_timezone) { 'UTC' }
it_behaves_like "returns time in the future"
it_behaves_like returns_time_for_epoch
it 'returns specific time' do
expect(subject.min).to be_in([0, 10, 20, 30, 40, 50])
expect(subject.hour).to be_in([0, 6, 12, 18])
expect(subject.day).to be_in([1, 11, 21, 31])
expect(subject.month).to be_in([1, 11])
end
it 'returns specific time' do
expect(subject.min).to be_in([0, 10, 20, 30, 40, 50])
expect(subject.hour).to be_in([0, 6, 12, 18])
expect(subject.day).to be_in([1, 11, 21, 31])
expect(subject.month).to be_in([1, 11])
end
end
context 'when range used' do
let(:cron) { '0,20,40 * 1-5 * *' }
let(:cron_timezone) { 'UTC' }
context 'when range used' do
let(:cron) { '0,20,40 * 1-5 * *' }
let(:cron_timezone) { 'UTC' }
it_behaves_like "returns time in the future"
it_behaves_like returns_time_for_epoch
it 'returns specific time' do
expect(subject.min).to be_in([0, 20, 40])
expect(subject.day).to be_in((1..5).to_a)
end
it 'returns specific time' do
expect(subject.min).to be_in([0, 20, 40])
expect(subject.day).to be_in((1..5).to_a)
end
end
context 'when cron_timezone is TZInfo format' do
before do
allow(Time).to receive(:zone)
.and_return(ActiveSupport::TimeZone['UTC'])
end
context 'when cron_timezone is TZInfo format' do
before do
allow(Time).to receive(:zone)
.and_return(ActiveSupport::TimeZone['UTC'])
end
let(:hour_in_utc) do
ActiveSupport::TimeZone[cron_timezone]
.now.change(hour: 0).in_time_zone('UTC').hour
end
let(:hour_in_utc) do
ActiveSupport::TimeZone[cron_timezone]
.now.change(hour: 0).in_time_zone('UTC').hour
end
context 'when cron_timezone is US/Pacific' do
let(:cron) { '* 0 * * *' }
let(:cron_timezone) { 'US/Pacific' }
context 'when cron_timezone is US/Pacific' do
let(:cron) { '* 0 * * *' }
let(:cron_timezone) { 'US/Pacific' }
it_behaves_like "returns time in the future"
it_behaves_like returns_time_for_epoch
context 'when PST (Pacific Standard Time)' do
it 'converts time in server time zone' do
Timecop.freeze(Time.utc(2017, 1, 1)) do
expect(subject.hour).to eq(hour_in_utc)
end
context 'when PST (Pacific Standard Time)' do
it 'converts time in server time zone' do
Timecop.freeze(Time.utc(2017, 1, 1)) do
expect(subject.hour).to eq(hour_in_utc)
end
end
end
context 'when PDT (Pacific Daylight Time)' do
it 'converts time in server time zone' do
Timecop.freeze(Time.utc(2017, 6, 1)) do
expect(subject.hour).to eq(hour_in_utc)
end
context 'when PDT (Pacific Daylight Time)' do
it 'converts time in server time zone' do
Timecop.freeze(Time.utc(2017, 6, 1)) do
expect(subject.hour).to eq(hour_in_utc)
end
end
end
end
end
context 'when cron_timezone is ActiveSupport::TimeZone format' do
before do
allow(Time).to receive(:zone)
.and_return(ActiveSupport::TimeZone['UTC'])
end
context 'when cron_timezone is ActiveSupport::TimeZone format' do
before do
allow(Time).to receive(:zone)
.and_return(ActiveSupport::TimeZone['UTC'])
end
let(:hour_in_utc) do
ActiveSupport::TimeZone[cron_timezone]
.now.change(hour: 0).in_time_zone('UTC').hour
end
let(:hour_in_utc) do
ActiveSupport::TimeZone[cron_timezone]
.now.change(hour: 0).in_time_zone('UTC').hour
end
context 'when cron_timezone is Berlin' do
let(:cron) { '* 0 * * *' }
let(:cron_timezone) { 'Berlin' }
context 'when cron_timezone is Berlin' do
let(:cron) { '* 0 * * *' }
let(:cron_timezone) { 'Berlin' }
it_behaves_like "returns time in the future"
it_behaves_like returns_time_for_epoch
context 'when CET (Central European Time)' do
it 'converts time in server time zone' do
Timecop.freeze(Time.utc(2017, 1, 1)) do
expect(subject.hour).to eq(hour_in_utc)
end
context 'when CET (Central European Time)' do
it 'converts time in server time zone' do
Timecop.freeze(Time.utc(2017, 1, 1)) do
expect(subject.hour).to eq(hour_in_utc)
end
end
end
context 'when CEST (Central European Summer Time)' do
it 'converts time in server time zone' do
Timecop.freeze(Time.utc(2017, 6, 1)) do
expect(subject.hour).to eq(hour_in_utc)
end
context 'when CEST (Central European Summer Time)' do
it 'converts time in server time zone' do
Timecop.freeze(Time.utc(2017, 6, 1)) do
expect(subject.hour).to eq(hour_in_utc)
end
end
end
end
end
end
context 'when cron_timezone is Eastern Time (US & Canada)' do
let(:cron) { '* 0 * * *' }
let(:cron_timezone) { 'Eastern Time (US & Canada)' }
shared_examples_for 'when cron_timezone is Eastern Time (US & Canada)' do |returns_time_for_epoch, year|
let(:cron) { '* 0 * * *' }
let(:cron_timezone) { 'Eastern Time (US & Canada)' }
it_behaves_like "returns time in the future"
before do
allow(Time).to receive(:zone)
.and_return(ActiveSupport::TimeZone['UTC'])
end
context 'when EST (Eastern Standard Time)' do
it 'converts time in server time zone' do
Timecop.freeze(Time.utc(2017, 1, 1)) do
expect(subject.hour).to eq(hour_in_utc)
end
end
end
let(:hour_in_utc) do
ActiveSupport::TimeZone[cron_timezone]
.now.change(hour: 0).in_time_zone('UTC').hour
end
context 'when EDT (Eastern Daylight Time)' do
it 'converts time in server time zone' do
Timecop.freeze(Time.utc(2017, 6, 1)) do
expect(subject.hour).to eq(hour_in_utc)
end
end
end
it_behaves_like returns_time_for_epoch
context 'when time crosses a Daylight Savings boundary' do
let(:cron) { '* 0 1 12 *'}
# Note this previously only failed if the time zone is set
# to a zone that observes Daylight Savings
# (e.g. America/Chicago) at the start of the test. Stubbing
# TZ doesn't appear to be enough.
it 'generates day without TZInfo::AmbiguousTime error' do
Timecop.freeze(Time.utc(2020, 1, 1)) do
expect(subject.year).to eq(2020)
expect(subject.month).to eq(12)
expect(subject.day).to eq(1)
end
end
end
context 'when EST (Eastern Standard Time)' do
it 'converts time in server time zone' do
Timecop.freeze(Time.utc(2017, 1, 1)) do
expect(subject.hour).to eq(hour_in_utc)
end
end
end
context 'when cron and cron_timezone are invalid' do
let(:cron) { 'invalid_cron' }
let(:cron_timezone) { 'invalid_cron_timezone' }
context 'when EDT (Eastern Daylight Time)' do
it 'converts time in server time zone' do
Timecop.freeze(Time.utc(2017, 6, 1)) do
expect(subject.hour).to eq(hour_in_utc)
end
end
end
it { is_expected.to be_nil }
context 'when time crosses a Daylight Savings boundary' do
let(:cron) { '* 0 1 12 *'}
# Note this previously only failed if the time zone is set
# to a zone that observes Daylight Savings
# (e.g. America/Chicago) at the start of the test. Stubbing
# TZ doesn't appear to be enough.
it 'generates day without TZInfo::AmbiguousTime error' do
Timecop.freeze(Time.utc(2020, 1, 1)) do
expect(subject.year).to eq(year)
expect(subject.month).to eq(12)
expect(subject.day).to eq(1)
end
end
end
end
context 'when cron syntax is quoted' do
let(:cron) { "'0 * * * *'" }
let(:cron_timezone) { 'UTC' }
shared_examples_for 'when cron and cron_timezone are invalid' do
let(:cron) { 'invalid_cron' }
let(:cron_timezone) { 'invalid_cron_timezone' }
it { expect(subject).to be_nil }
end
it { is_expected.to be_nil }
end
context 'when cron syntax is rufus-scheduler syntax' do
let(:cron) { 'every 3h' }
let(:cron_timezone) { 'UTC' }
shared_examples_for 'when cron syntax is quoted' do
let(:cron) { "'0 * * * *'" }
let(:cron_timezone) { 'UTC' }
it { expect(subject).to be_nil }
end
it { expect(subject).to be_nil }
end
context 'when cron is scheduled to a non existent day' do
let(:cron) { '0 12 31 2 *' }
let(:cron_timezone) { 'UTC' }
shared_examples_for 'when cron syntax is rufus-scheduler syntax' do
let(:cron) { 'every 3h' }
let(:cron_timezone) { 'UTC' }
it { expect(subject).to be_nil }
end
it { expect(subject).to be_nil }
end
shared_examples_for 'when cron is scheduled to a non existent day' do
let(:cron) { '0 12 31 2 *' }
let(:cron_timezone) { 'UTC' }
it { expect(subject).to be_nil }
end
describe '#next_time_from' do
subject { described_class.new(cron, cron_timezone).next_time_from(Time.now) }
it_behaves_like 'when cron and cron_timezone are valid', 'returns time in the future'
it_behaves_like 'when cron_timezone is Eastern Time (US & Canada)', 'returns time in the future', 2020
it_behaves_like 'when cron and cron_timezone are invalid'
it_behaves_like 'when cron syntax is quoted'
it_behaves_like 'when cron syntax is rufus-scheduler syntax'
it_behaves_like 'when cron is scheduled to a non existent day'
end
describe '#previous_time_from' do
subject { described_class.new(cron, cron_timezone).previous_time_from(Time.now) }
it_behaves_like 'when cron and cron_timezone are valid', 'returns time in the past'
it_behaves_like 'when cron_timezone is Eastern Time (US & Canada)', 'returns time in the past', 2019
it_behaves_like 'when cron and cron_timezone are invalid'
it_behaves_like 'when cron syntax is quoted'
it_behaves_like 'when cron syntax is rufus-scheduler syntax'
it_behaves_like 'when cron is scheduled to a non existent day'
end
describe '#cron_valid?' do
......
......@@ -2892,6 +2892,19 @@ describe Ci::Build do
it { is_expected.to include(deployment_variable) }
end
context 'when build has a freeze period' do
let(:freeze_variable) { { key: 'CI_DEPLOY_FREEZE', value: 'true', masked: false, public: true } }
before do
expect_next_instance_of(Ci::FreezePeriodStatus) do |freeze_period|
expect(freeze_period).to receive(:execute)
.and_return(true)
end
end
it { is_expected.to include(freeze_variable) }
end
context 'when project has default CI config path' do
let(:ci_config_path) { { key: 'CI_CONFIG_PATH', value: '.gitlab-ci.yml', public: true, masked: false } }
......
......@@ -24,7 +24,7 @@ RSpec.describe Ci::FreezePeriod, type: :model do
expect(freeze_period).not_to be_valid
end
it 'does not allow non-cron strings' do
it 'does not allow an invalid timezone' do
freeze_period = build(:ci_freeze_period, cron_timezone: 'invalid')
expect(freeze_period).not_to be_valid
......
# frozen_string_literal: true
require 'spec_helper'
describe Ci::FreezePeriodStatus do
let(:project) { create :project }
# '0 23 * * 5' == "At 23:00 on Friday."", '0 7 * * 1' == "At 07:00 on Monday.""
let(:friday_2300) { '0 23 * * 5' }
let(:monday_0700) { '0 7 * * 1' }
subject { described_class.new(project: project).execute }
shared_examples 'within freeze period' do |time|
it 'is frozen' do
Timecop.freeze(time) do
expect(subject).to be_truthy
end
end
end
shared_examples 'outside freeze period' do |time|
it 'is not frozen' do
Timecop.freeze(time) do
expect(subject).to be_falsy
end
end
end
describe 'single freeze period' do
let!(:freeze_period) { create(:ci_freeze_period, project: project, freeze_start: friday_2300, freeze_end: monday_0700) }
it_behaves_like 'outside freeze period', Time.utc(2020, 4, 10, 22, 59)
it_behaves_like 'within freeze period', Time.utc(2020, 4, 10, 23, 01)
it_behaves_like 'within freeze period', Time.utc(2020, 4, 13, 6, 59)
it_behaves_like 'outside freeze period', Time.utc(2020, 4, 13, 7, 1)
end
describe 'multiple freeze periods' do
# '30 23 * * 5' == "At 23:30 on Friday."", '0 8 * * 1' == "At 08:00 on Monday.""
let(:friday_2330) { '30 23 * * 5' }
let(:monday_0800) { '0 8 * * 1' }
let!(:freeze_period_1) { create(:ci_freeze_period, project: project, freeze_start: friday_2300, freeze_end: monday_0700) }
let!(:freeze_period_2) { create(:ci_freeze_period, project: project, freeze_start: friday_2330, freeze_end: monday_0800) }
it_behaves_like 'outside freeze period', Time.utc(2020, 4, 10, 22, 59)
it_behaves_like 'within freeze period', Time.utc(2020, 4, 10, 23, 29)
it_behaves_like 'within freeze period', Time.utc(2020, 4, 11, 10, 0)
it_behaves_like 'within freeze period', Time.utc(2020, 4, 10, 23, 1)
it_behaves_like 'within freeze period', Time.utc(2020, 4, 13, 6, 59)
it_behaves_like 'within freeze period', Time.utc(2020, 4, 13, 7, 59)
it_behaves_like 'outside freeze period', Time.utc(2020, 4, 13, 8, 1)
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment