Commit 407b0aa2 authored by Sean Carroll's avatar Sean Carroll Committed by Lin Jen-Shin
parent 23346d01
...@@ -533,6 +533,7 @@ module Ci ...@@ -533,6 +533,7 @@ module Ci
.concat(job_variables) .concat(job_variables)
.concat(environment_changed_page_variables) .concat(environment_changed_page_variables)
.concat(persisted_environment_variables) .concat(persisted_environment_variables)
.concat(deploy_freeze_variables)
.to_runner_variables .to_runner_variables
end end
end end
...@@ -588,6 +589,18 @@ module Ci ...@@ -588,6 +589,18 @@ module Ci
end end
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 def features
{ trace_sections: true } { trace_sections: true }
end 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 ...@@ -12,8 +12,11 @@ module Gitlab
end end
def next_time_from(time) 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 end
def cron_valid? def cron_valid?
...@@ -49,6 +52,10 @@ module Gitlab ...@@ -49,6 +52,10 @@ module Gitlab
def try_parse_cron(cron, cron_timezone) def try_parse_cron(cron, cron_timezone)
Fugit::Cron.parse("#{cron} #{cron_timezone}") Fugit::Cron.parse("#{cron} #{cron_timezone}")
end end
def cron_line
@cron_line ||= try_parse_cron(@cron, @cron_timezone)
end
end end
end end
end end
...@@ -7,15 +7,16 @@ describe Gitlab::Ci::CronParser do ...@@ -7,15 +7,16 @@ describe Gitlab::Ci::CronParser do
it { is_expected.to be > Time.now } it { is_expected.to be > Time.now }
end end
describe '#next_time_from' do shared_examples_for "returns time in the past" do
subject { described_class.new(cron, cron_timezone).next_time_from(Time.now) } it { is_expected.to be < Time.now }
end
context 'when cron and cron_timezone are valid' do shared_examples_for 'when cron and cron_timezone are valid' do |returns_time_for_epoch|
context 'when specific time' do context 'when specific time' do
let(:cron) { '3 4 5 6 *' } let(:cron) { '3 4 5 6 *' }
let(:cron_timezone) { 'UTC' } let(:cron_timezone) { 'UTC' }
it_behaves_like "returns time in the future" it_behaves_like returns_time_for_epoch
it 'returns exact time' do it 'returns exact time' do
expect(subject.min).to eq(3) expect(subject.min).to eq(3)
...@@ -29,7 +30,7 @@ describe Gitlab::Ci::CronParser do ...@@ -29,7 +30,7 @@ describe Gitlab::Ci::CronParser do
let(:cron) { '* * * * 0' } let(:cron) { '* * * * 0' }
let(:cron_timezone) { 'UTC' } 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 it 'returns exact day of week' do
expect(subject.wday).to eq(0) expect(subject.wday).to eq(0)
...@@ -40,7 +41,7 @@ describe Gitlab::Ci::CronParser do ...@@ -40,7 +41,7 @@ describe Gitlab::Ci::CronParser do
let(:cron) { '*/10 */6 */10 */10 *' } let(:cron) { '*/10 */6 */10 */10 *' }
let(:cron_timezone) { 'UTC' } let(:cron_timezone) { 'UTC' }
it_behaves_like "returns time in the future" it_behaves_like returns_time_for_epoch
it 'returns specific time' do it 'returns specific time' do
expect(subject.min).to be_in([0, 10, 20, 30, 40, 50]) expect(subject.min).to be_in([0, 10, 20, 30, 40, 50])
...@@ -54,7 +55,7 @@ describe Gitlab::Ci::CronParser do ...@@ -54,7 +55,7 @@ describe Gitlab::Ci::CronParser do
let(:cron) { '0,20,40 * 1-5 * *' } let(:cron) { '0,20,40 * 1-5 * *' }
let(:cron_timezone) { 'UTC' } let(:cron_timezone) { 'UTC' }
it_behaves_like "returns time in the future" it_behaves_like returns_time_for_epoch
it 'returns specific time' do it 'returns specific time' do
expect(subject.min).to be_in([0, 20, 40]) expect(subject.min).to be_in([0, 20, 40])
...@@ -77,7 +78,7 @@ describe Gitlab::Ci::CronParser do ...@@ -77,7 +78,7 @@ describe Gitlab::Ci::CronParser do
let(:cron) { '* 0 * * *' } let(:cron) { '* 0 * * *' }
let(:cron_timezone) { 'US/Pacific' } 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 context 'when PST (Pacific Standard Time)' do
it 'converts time in server time zone' do it 'converts time in server time zone' do
...@@ -112,7 +113,7 @@ describe Gitlab::Ci::CronParser do ...@@ -112,7 +113,7 @@ describe Gitlab::Ci::CronParser do
let(:cron) { '* 0 * * *' } let(:cron) { '* 0 * * *' }
let(:cron_timezone) { 'Berlin' } 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 context 'when CET (Central European Time)' do
it 'converts time in server time zone' do it 'converts time in server time zone' do
...@@ -130,12 +131,24 @@ describe Gitlab::Ci::CronParser do ...@@ -130,12 +131,24 @@ describe Gitlab::Ci::CronParser do
end end
end end
end end
end
end
context 'when cron_timezone is Eastern Time (US & Canada)' do shared_examples_for 'when cron_timezone is Eastern Time (US & Canada)' do |returns_time_for_epoch, year|
let(:cron) { '* 0 * * *' } let(:cron) { '* 0 * * *' }
let(:cron_timezone) { 'Eastern Time (US & Canada)' } 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
let(:hour_in_utc) do
ActiveSupport::TimeZone[cron_timezone]
.now.change(hour: 0).in_time_zone('UTC').hour
end
it_behaves_like returns_time_for_epoch
context 'when EST (Eastern Standard Time)' do context 'when EST (Eastern Standard Time)' do
it 'converts time in server time zone' do it 'converts time in server time zone' do
...@@ -162,43 +175,72 @@ describe Gitlab::Ci::CronParser do ...@@ -162,43 +175,72 @@ describe Gitlab::Ci::CronParser do
# TZ doesn't appear to be enough. # TZ doesn't appear to be enough.
it 'generates day without TZInfo::AmbiguousTime error' do it 'generates day without TZInfo::AmbiguousTime error' do
Timecop.freeze(Time.utc(2020, 1, 1)) do Timecop.freeze(Time.utc(2020, 1, 1)) do
expect(subject.year).to eq(2020) expect(subject.year).to eq(year)
expect(subject.month).to eq(12) expect(subject.month).to eq(12)
expect(subject.day).to eq(1) expect(subject.day).to eq(1)
end end
end end
end end
end end
end
end
context 'when cron and cron_timezone are invalid' do shared_examples_for 'when cron and cron_timezone are invalid' do
let(:cron) { 'invalid_cron' } let(:cron) { 'invalid_cron' }
let(:cron_timezone) { 'invalid_cron_timezone' } let(:cron_timezone) { 'invalid_cron_timezone' }
it { is_expected.to be_nil } it { is_expected.to be_nil }
end end
context 'when cron syntax is quoted' do shared_examples_for 'when cron syntax is quoted' do
let(:cron) { "'0 * * * *'" } let(:cron) { "'0 * * * *'" }
let(:cron_timezone) { 'UTC' } let(:cron_timezone) { 'UTC' }
it { expect(subject).to be_nil } it { expect(subject).to be_nil }
end end
context 'when cron syntax is rufus-scheduler syntax' do shared_examples_for 'when cron syntax is rufus-scheduler syntax' do
let(:cron) { 'every 3h' } let(:cron) { 'every 3h' }
let(:cron_timezone) { 'UTC' } let(:cron_timezone) { 'UTC' }
it { expect(subject).to be_nil } it { expect(subject).to be_nil }
end end
context 'when cron is scheduled to a non existent day' do shared_examples_for 'when cron is scheduled to a non existent day' do
let(:cron) { '0 12 31 2 *' } let(:cron) { '0 12 31 2 *' }
let(:cron_timezone) { 'UTC' } let(:cron_timezone) { 'UTC' }
it { expect(subject).to be_nil } it { expect(subject).to be_nil }
end 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 end
describe '#cron_valid?' do describe '#cron_valid?' do
......
...@@ -2884,6 +2884,19 @@ describe Ci::Build do ...@@ -2884,6 +2884,19 @@ describe Ci::Build do
it { is_expected.to include(deployment_variable) } it { is_expected.to include(deployment_variable) }
end 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 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 } } 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 ...@@ -24,7 +24,7 @@ RSpec.describe Ci::FreezePeriod, type: :model do
expect(freeze_period).not_to be_valid expect(freeze_period).not_to be_valid
end 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') freeze_period = build(:ci_freeze_period, cron_timezone: 'invalid')
expect(freeze_period).not_to be_valid 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