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,15 +7,16 @@ 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
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)
......@@ -29,7 +30,7 @@ describe Gitlab::Ci::CronParser 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)
......@@ -40,7 +41,7 @@ describe Gitlab::Ci::CronParser 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])
......@@ -54,7 +55,7 @@ describe Gitlab::Ci::CronParser 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])
......@@ -77,7 +78,7 @@ describe Gitlab::Ci::CronParser 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
......@@ -112,7 +113,7 @@ describe Gitlab::Ci::CronParser 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
......@@ -130,12 +131,24 @@ describe Gitlab::Ci::CronParser do
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_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
it 'converts time in server time zone' do
......@@ -162,43 +175,72 @@ describe Gitlab::Ci::CronParser do
# 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.year).to eq(year)
expect(subject.month).to eq(12)
expect(subject.day).to eq(1)
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_timezone) { 'invalid_cron_timezone' }
it { is_expected.to be_nil }
end
context 'when cron syntax is quoted' do
shared_examples_for 'when cron syntax is quoted' do
let(:cron) { "'0 * * * *'" }
let(:cron_timezone) { 'UTC' }
it { expect(subject).to be_nil }
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_timezone) { 'UTC' }
it { expect(subject).to be_nil }
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_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