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 ...@@ -537,6 +537,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
...@@ -592,6 +593,18 @@ module Ci ...@@ -592,6 +593,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,198 +7,240 @@ describe Gitlab::Ci::CronParser do ...@@ -7,198 +7,240 @@ 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)
expect(subject.hour).to eq(4) expect(subject.hour).to eq(4)
expect(subject.day).to eq(5) expect(subject.day).to eq(5)
expect(subject.month).to eq(6) expect(subject.month).to eq(6)
end
end end
end
context 'when specific day of week' do context 'when specific day of week' 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)
end
end end
end
context 'when slash used' do context 'when slash used' 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])
expect(subject.hour).to be_in([0, 6, 12, 18]) expect(subject.hour).to be_in([0, 6, 12, 18])
expect(subject.day).to be_in([1, 11, 21, 31]) expect(subject.day).to be_in([1, 11, 21, 31])
expect(subject.month).to be_in([1, 11]) expect(subject.month).to be_in([1, 11])
end
end end
end
context 'when range used' do context 'when range used' 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])
expect(subject.day).to be_in((1..5).to_a) expect(subject.day).to be_in((1..5).to_a)
end
end end
end
context 'when cron_timezone is TZInfo format' do context 'when cron_timezone is TZInfo format' do
before do before do
allow(Time).to receive(:zone) allow(Time).to receive(:zone)
.and_return(ActiveSupport::TimeZone['UTC']) .and_return(ActiveSupport::TimeZone['UTC'])
end end
let(:hour_in_utc) do let(:hour_in_utc) do
ActiveSupport::TimeZone[cron_timezone] ActiveSupport::TimeZone[cron_timezone]
.now.change(hour: 0).in_time_zone('UTC').hour .now.change(hour: 0).in_time_zone('UTC').hour
end end
context 'when cron_timezone is US/Pacific' do context 'when cron_timezone is US/Pacific' 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
Timecop.freeze(Time.utc(2017, 1, 1)) do Timecop.freeze(Time.utc(2017, 1, 1)) do
expect(subject.hour).to eq(hour_in_utc) expect(subject.hour).to eq(hour_in_utc)
end
end end
end end
end
context 'when PDT (Pacific Daylight Time)' do context 'when PDT (Pacific Daylight Time)' do
it 'converts time in server time zone' do it 'converts time in server time zone' do
Timecop.freeze(Time.utc(2017, 6, 1)) do Timecop.freeze(Time.utc(2017, 6, 1)) do
expect(subject.hour).to eq(hour_in_utc) expect(subject.hour).to eq(hour_in_utc)
end
end end
end end
end end
end end
end
context 'when cron_timezone is ActiveSupport::TimeZone format' do context 'when cron_timezone is ActiveSupport::TimeZone format' do
before do before do
allow(Time).to receive(:zone) allow(Time).to receive(:zone)
.and_return(ActiveSupport::TimeZone['UTC']) .and_return(ActiveSupport::TimeZone['UTC'])
end end
let(:hour_in_utc) do let(:hour_in_utc) do
ActiveSupport::TimeZone[cron_timezone] ActiveSupport::TimeZone[cron_timezone]
.now.change(hour: 0).in_time_zone('UTC').hour .now.change(hour: 0).in_time_zone('UTC').hour
end end
context 'when cron_timezone is Berlin' do context 'when cron_timezone is Berlin' 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
Timecop.freeze(Time.utc(2017, 1, 1)) do Timecop.freeze(Time.utc(2017, 1, 1)) do
expect(subject.hour).to eq(hour_in_utc) expect(subject.hour).to eq(hour_in_utc)
end
end end
end end
end
context 'when CEST (Central European Summer Time)' do context 'when CEST (Central European Summer Time)' do
it 'converts time in server time zone' do it 'converts time in server time zone' do
Timecop.freeze(Time.utc(2017, 6, 1)) do Timecop.freeze(Time.utc(2017, 6, 1)) do
expect(subject.hour).to eq(hour_in_utc) expect(subject.hour).to eq(hour_in_utc)
end
end 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
context 'when EST (Eastern Standard Time)' do let(:hour_in_utc) do
it 'converts time in server time zone' do ActiveSupport::TimeZone[cron_timezone]
Timecop.freeze(Time.utc(2017, 1, 1)) do .now.change(hour: 0).in_time_zone('UTC').hour
expect(subject.hour).to eq(hour_in_utc) end
end
end
end
context 'when EDT (Eastern Daylight Time)' do it_behaves_like returns_time_for_epoch
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
context 'when time crosses a Daylight Savings boundary' do context 'when EST (Eastern Standard Time)' do
let(:cron) { '* 0 1 12 *'} it 'converts time in server time zone' do
Timecop.freeze(Time.utc(2017, 1, 1)) do
# Note this previously only failed if the time zone is set expect(subject.hour).to eq(hour_in_utc)
# 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
end end
end end
end end
context 'when cron and cron_timezone are invalid' do context 'when EDT (Eastern Daylight Time)' do
let(:cron) { 'invalid_cron' } it 'converts time in server time zone' do
let(:cron_timezone) { 'invalid_cron_timezone' } 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
end
context 'when cron syntax is quoted' do shared_examples_for 'when cron and cron_timezone are invalid' do
let(:cron) { "'0 * * * *'" } let(:cron) { 'invalid_cron' }
let(:cron_timezone) { 'UTC' } let(:cron_timezone) { 'invalid_cron_timezone' }
it { expect(subject).to be_nil } it { is_expected.to be_nil }
end end
context 'when cron syntax is rufus-scheduler syntax' do shared_examples_for 'when cron syntax is quoted' do
let(:cron) { 'every 3h' } 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 is scheduled to a non existent day' do shared_examples_for 'when cron syntax is rufus-scheduler syntax' do
let(:cron) { '0 12 31 2 *' } 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
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 end
describe '#cron_valid?' do describe '#cron_valid?' do
......
...@@ -2892,6 +2892,19 @@ describe Ci::Build do ...@@ -2892,6 +2892,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