From fab7c085e8834f83e675e19d0e49e66e1bf65293 Mon Sep 17 00:00:00 2001 From: Sven Fuchs Date: Wed, 13 Mar 2019 16:53:36 +0100 Subject: [PATCH 01/81] pass :keep_netrc preference to the worker --- lib/travis/scheduler/record/organization.rb | 4 ++++ lib/travis/scheduler/record/user.rb | 4 ++++ lib/travis/scheduler/serialize/worker.rb | 3 ++- lib/travis/scheduler/serialize/worker/repo.rb | 4 ++++ .../travis/scheduler/serialize/worker_spec.rb | 22 +++++++++++++++++-- 5 files changed, 34 insertions(+), 3 deletions(-) diff --git a/lib/travis/scheduler/record/organization.rb b/lib/travis/scheduler/record/organization.rb index 3e5eaf90..c262ed63 100644 --- a/lib/travis/scheduler/record/organization.rb +++ b/lib/travis/scheduler/record/organization.rb @@ -47,6 +47,10 @@ def default_worker_timeout end end + def keep_netrc? + preferences.key?('keep_netrc') ? preferences['keep_netrc'] : true + end + def uid "org:#{id}" end diff --git a/lib/travis/scheduler/record/user.rb b/lib/travis/scheduler/record/user.rb index 8dce146a..56e0d9f6 100644 --- a/lib/travis/scheduler/record/user.rb +++ b/lib/travis/scheduler/record/user.rb @@ -52,6 +52,10 @@ def default_worker_timeout end end + def keep_netrc? + preferences.key?('keep_netrc') ? preferences['keep_netrc'] : true + end + def uid "user:#{id}" end diff --git a/lib/travis/scheduler/serialize/worker.rb b/lib/travis/scheduler/serialize/worker.rb index c2107d28..403c440e 100644 --- a/lib/travis/scheduler/serialize/worker.rb +++ b/lib/travis/scheduler/serialize/worker.rb @@ -26,7 +26,8 @@ def data timeouts: repo.timeouts, cache_settings: cache_settings, enterprise: !!config[:enterprise], - prefer_https: !!config[:prefer_https] + prefer_https: !!config[:prefer_https], + keep_netrc: repo.keep_netrc? } data[:trace] = true if job.trace? data[:warmer] = true if job.warmer? diff --git a/lib/travis/scheduler/serialize/worker/repo.rb b/lib/travis/scheduler/serialize/worker/repo.rb index fc635159..577c4b10 100644 --- a/lib/travis/scheduler/serialize/worker/repo.rb +++ b/lib/travis/scheduler/serialize/worker/repo.rb @@ -38,6 +38,10 @@ def installation_id repo.installation&.github_id if repo.managed_by_app? && repo.private end + def keep_netrc? + repo.owner&.keep_netrc? + end + private # If the repo does not have a custom timeout, look to the repo's diff --git a/spec/travis/scheduler/serialize/worker_spec.rb b/spec/travis/scheduler/serialize/worker_spec.rb index 97d4f199..97558f67 100644 --- a/spec/travis/scheduler/serialize/worker_spec.rb +++ b/spec/travis/scheduler/serialize/worker_spec.rb @@ -98,7 +98,8 @@ def encrypted(value) }, cache_settings: s3, prefer_https: false, - enterprise: false + enterprise: false, + keep_netrc: true ) end @@ -267,7 +268,8 @@ def encrypted(value) }, cache_settings: s3, prefer_https: false, - enterprise: false + enterprise: false, + keep_netrc: true ) end @@ -334,5 +336,21 @@ def encrypted(value) end end end + + describe 'keep_netrc' do + describe 'defaults to true' do + it { expect(data[:keep_netrc]).to be true } + end + + describe 'preference set to true' do + before { repo.owner.update_attributes(preferences: { keep_netrc: true }) } + it { expect(data[:keep_netrc]).to be true } + end + + describe 'preference set to false' do + before { repo.owner.update_attributes(preferences: { keep_netrc: false }) } + it { expect(data[:keep_netrc]).to be false } + end + end end From 4a382f9349c308d78f289d4762145a3259e01a1e Mon Sep 17 00:00:00 2001 From: Dominic Jodoin Date: Fri, 15 Mar 2019 16:29:56 -0400 Subject: [PATCH 02/81] Added preferences method --- lib/travis/scheduler/record/organization.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/travis/scheduler/record/organization.rb b/lib/travis/scheduler/record/organization.rb index c262ed63..ca9c592b 100644 --- a/lib/travis/scheduler/record/organization.rb +++ b/lib/travis/scheduler/record/organization.rb @@ -47,6 +47,10 @@ def default_worker_timeout end end + def preferences + super || {} + end + def keep_netrc? preferences.key?('keep_netrc') ? preferences['keep_netrc'] : true end From 248945abeb83e9b20c8f8d6e93afe8e113a358ba Mon Sep 17 00:00:00 2001 From: Dominic Jodoin Date: Fri, 15 Mar 2019 16:38:23 -0400 Subject: [PATCH 03/81] Added preferences method to User class too. --- lib/travis/scheduler/record/user.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/travis/scheduler/record/user.rb b/lib/travis/scheduler/record/user.rb index 56e0d9f6..9202c7a1 100644 --- a/lib/travis/scheduler/record/user.rb +++ b/lib/travis/scheduler/record/user.rb @@ -52,6 +52,10 @@ def default_worker_timeout end end + def preferences + super || {} + end + def keep_netrc? preferences.key?('keep_netrc') ? preferences['keep_netrc'] : true end From 455b3f0041f511d05e8c268869f76857dfc81e96 Mon Sep 17 00:00:00 2001 From: Sven Fuchs Date: Thu, 23 May 2019 14:41:13 +0200 Subject: [PATCH 04/81] use config import for db setup --- .travis.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index ffd7ca94..c8bd02d0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,9 @@ language: ruby dist: trusty rvm: 2.4.2 +import: + - travis-ci/build-configs/db-setup.yml + cache: bundler: true @@ -20,6 +23,4 @@ env: before_install: gem install bundler -before_script: bundle exec rake db:create --trace - script: bundle exec rspec spec From e8f725def0a3842a5909f6dde010d6d9140bdbe2 Mon Sep 17 00:00:00 2001 From: Sven Fuchs Date: Thu, 23 May 2019 14:50:17 +0200 Subject: [PATCH 05/81] Update .travis.yml --- .travis.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index c8bd02d0..02309b3d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,9 +12,6 @@ services: - redis - rabbitmq -addons: - postgresql: 9.6 - env: matrix: - RAKE_TASK=spec From f106d618d829b186d9a3460e65161c65934553f9 Mon Sep 17 00:00:00 2001 From: Piotr Sarnacki Date: Wed, 29 May 2019 10:42:15 +0200 Subject: [PATCH 06/81] Don't include migrated jobs when calculating concurrency When we migrate a build history it's possible that some jobs are still running on .org. We shouldn't count them towards concurrency limits because they're technically running on .org --- lib/travis/scheduler/jobs/state.rb | 17 +++++++++++++++-- spec/travis/scheduler/jobs_spec.rb | 18 +++++++++++++++++- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/lib/travis/scheduler/jobs/state.rb b/lib/travis/scheduler/jobs/state.rb index 36ed8e75..9c66c6ae 100644 --- a/lib/travis/scheduler/jobs/state.rb +++ b/lib/travis/scheduler/jobs/state.rb @@ -8,7 +8,7 @@ class State < Struct.new(:context, :owners) include Helper::Context, Helper::Metrics ATTRS = { - running: %i(repository_id private queue), + running: %i(repository_id private queue org_id restarted_at), by_build: %i(id state stage_number) } @@ -43,7 +43,20 @@ def count_running_by_queue(name) private def read_running - Job.by_owners(owners.all).running.select(*ATTRS[:running]).to_a + collection = Job.by_owners(owners.all).running.select(*ATTRS[:running]).includes(:repository).to_a + if Travis.config.com? + collection = collection.find_all { |job| + # I think it's fine to filter running jobs after querying. usually + # it's not a good idea to do it in Ruby rather than SQL but jobs + # that might be rejected here will be rather rare - it's only for + # the purpose of not running migrated jobs. Doing it in SQL on the + # other hand would likely need an additional index and a join with + # repositories + # + !job.org_id || (job.restarted_at && job.restarted_at > job.repository.migrated_at) + } + end + collection end time :read_queueable, key: 'scheduler.running_jobs' diff --git a/spec/travis/scheduler/jobs_spec.rb b/spec/travis/scheduler/jobs_spec.rb index 32287130..b4844f6b 100644 --- a/spec/travis/scheduler/jobs_spec.rb +++ b/spec/travis/scheduler/jobs_spec.rb @@ -1,6 +1,6 @@ describe Travis::Scheduler::Jobs::Select do let(:org) { FactoryGirl.create(:org, login: 'travis-ci') } - let(:repo) { FactoryGirl.create(:repo, owner: user) } + let(:repo) { FactoryGirl.create(:repo, owner: user, migrated_at: 1.hour.ago) } let(:build) { FactoryGirl.create(:build) } let(:user) { FactoryGirl.create(:user, login: 'svenfuchs') } let(:owners) { Travis::Owners.group({ owner_type: 'User', owner_id: user.id }, config.to_h) } @@ -68,6 +68,22 @@ def subscribe(plan, owner = self.user) it { expect(reports).to include 'user svenfuchs public capacity: running=1 max=3 selected=2' } it { expect(reports).to include 'user svenfuchs boost capacity: running=1 max=2 selected=1' } it { expect(reports).to include 'user svenfuchs: queueable=4 running=2 selected=3 total_waiting=1 waiting_for_concurrency=1' } + + describe 'with migrated jobs' do + # a job that was migrated from org, shouldn't count towards running jobs + before { create_jobs(1, private: true, state: :started, org_id: 10) } + # a job that was migrated from org, but restarted after migration, should + # be counted towards running jobs + before { create_jobs(1, private: true, state: :started, org_id: 11, restarted_at: Time.now) } + + it { expect(selected.size).to eq 2 } + it { expect(reports).to include 'user svenfuchs capacities: public max=3, boost max=2' } + it { expect(reports).to include 'user svenfuchs public capacity: running=1 max=3 selected=2' } + it { expect(reports).to include 'user svenfuchs boost capacity: running=2 max=2 selected=0' } + it { expect(reports).to include 'user svenfuchs: queueable=4 running=3 selected=2 total_waiting=2 waiting_for_concurrency=2' } + + + end end describe 'with no queueable jobs' do From f88746ad38f4eff470155b658325a1416497075c Mon Sep 17 00:00:00 2001 From: Piotr Sarnacki Date: Wed, 29 May 2019 11:08:05 +0200 Subject: [PATCH 07/81] Don't enqueue jobs that are migrated from .org When we migrate build history some of the builds may be in the created state. We don't want to enqueue them as then they would be running on both platforms. --- lib/travis/scheduler/jobs/state.rb | 10 +++++++++- spec/travis/scheduler/jobs_spec.rb | 8 +++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/lib/travis/scheduler/jobs/state.rb b/lib/travis/scheduler/jobs/state.rb index 9c66c6ae..fed26599 100644 --- a/lib/travis/scheduler/jobs/state.rb +++ b/lib/travis/scheduler/jobs/state.rb @@ -61,7 +61,15 @@ def read_running time :read_queueable, key: 'scheduler.running_jobs' def read_queueable - Job.by_owners(owners.all).queueable.to_a + collection = Job.by_owners(owners.all).queueable.to_a + if Travis.config.com? + collection = collection.find_all { |job| + # we don't want to start migrated jobs if they are in the + # queueable state + !job.org_id || (job.restarted_at && job.restarted_at > job.repository.migrated_at) + } + end + collection end time :read_queueable, key: 'scheduler.queueable_jobs' diff --git a/spec/travis/scheduler/jobs_spec.rb b/spec/travis/scheduler/jobs_spec.rb index b4844f6b..d913085f 100644 --- a/spec/travis/scheduler/jobs_spec.rb +++ b/spec/travis/scheduler/jobs_spec.rb @@ -75,14 +75,16 @@ def subscribe(plan, owner = self.user) # a job that was migrated from org, but restarted after migration, should # be counted towards running jobs before { create_jobs(1, private: true, state: :started, org_id: 11, restarted_at: Time.now) } + # a job that was migrated as queueable, shouldn't be queued + before { create_jobs(1, private: true, state: :created, org_id: 12) } + # a job that was migrated and restarted + before { create_jobs(1, private: true, state: :created, org_id: 13, restarted_at: Time.now) } it { expect(selected.size).to eq 2 } it { expect(reports).to include 'user svenfuchs capacities: public max=3, boost max=2' } it { expect(reports).to include 'user svenfuchs public capacity: running=1 max=3 selected=2' } it { expect(reports).to include 'user svenfuchs boost capacity: running=2 max=2 selected=0' } - it { expect(reports).to include 'user svenfuchs: queueable=4 running=3 selected=2 total_waiting=2 waiting_for_concurrency=2' } - - + it { expect(reports).to include 'user svenfuchs: queueable=5 running=3 selected=2 total_waiting=3 waiting_for_concurrency=3' } end end From 6d765e07d5ad84f5ba7231a35a36590b4c177bb1 Mon Sep 17 00:00:00 2001 From: Hiro Asari Date: Wed, 19 Jun 2019 11:28:03 -0400 Subject: [PATCH 08/81] Put workspace_settings on payload --- lib/travis/scheduler/serialize/worker.rb | 7 +++++++ spec/travis/scheduler/serialize/worker_spec.rb | 5 +++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/travis/scheduler/serialize/worker.rb b/lib/travis/scheduler/serialize/worker.rb index 403c440e..9f1c22dd 100644 --- a/lib/travis/scheduler/serialize/worker.rb +++ b/lib/travis/scheduler/serialize/worker.rb @@ -25,6 +25,7 @@ def data ssh_key: ssh_key.data, timeouts: repo.timeouts, cache_settings: cache_settings, + workspace_settings: workspace_settings, enterprise: !!config[:enterprise], prefer_https: !!config[:prefer_https], keep_netrc: repo.keep_netrc? @@ -139,6 +140,12 @@ def cache_config config[:cache_settings] || {} end + def workspace_settings + if (ws_config = config[:workspace_settings] || {}) && ws_config.key?(job.queue) + config[:workspace_settings][job.queue].to_h + end + end + def format_date(date) date && date.strftime('%Y-%m-%dT%H:%M:%SZ') end diff --git a/spec/travis/scheduler/serialize/worker_spec.rb b/spec/travis/scheduler/serialize/worker_spec.rb index 97558f67..d3ee76ea 100644 --- a/spec/travis/scheduler/serialize/worker_spec.rb +++ b/spec/travis/scheduler/serialize/worker_spec.rb @@ -13,7 +13,7 @@ def encrypted(value) let(:repo) { FactoryGirl.create(:repo, default_branch: 'branch') } let(:owner) { repo.owner } let(:data) { described_class.new(job, config).data } - let(:config) { { cache_settings: { 'builds.gce' => s3 }, github: { source_host: 'github.com', api_url: 'https://api.github.com' }, vm_configs: {} } } + let(:config) { { cache_settings: { 'builds.gce' => s3 }, workspace_settings: { 'builds.gce' => s3 }, github: { source_host: 'github.com', api_url: 'https://api.github.com' }, vm_configs: {} } } let(:s3) { { access_key_id: 'ACCESS_KEY_ID', secret_access_key: 'SECRET_ACCESS_KEY', bucket_name: 'bucket' } } let(:event) { 'push' } let(:ref) { 'refs/tags/v1.2.3' } @@ -97,6 +97,7 @@ def encrypted(value) log_silence: 20 * 60 }, cache_settings: s3, + workspace_settings: s3, prefer_https: false, enterprise: false, keep_netrc: true @@ -267,6 +268,7 @@ def encrypted(value) log_silence: 20 * 60 }, cache_settings: s3, + workspace_settings: s3, prefer_https: false, enterprise: false, keep_netrc: true @@ -353,4 +355,3 @@ def encrypted(value) end end end - From ed7692ef087662a04f63826fba4332c9a4e00659 Mon Sep 17 00:00:00 2001 From: Hiro Asari Date: Wed, 19 Jun 2019 15:18:38 -0400 Subject: [PATCH 09/81] Fix workspace_settings ws_config is a Hashr, which doesn't have `#key?` --- lib/travis/scheduler/serialize/worker.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/travis/scheduler/serialize/worker.rb b/lib/travis/scheduler/serialize/worker.rb index 9f1c22dd..b22ff4c1 100644 --- a/lib/travis/scheduler/serialize/worker.rb +++ b/lib/travis/scheduler/serialize/worker.rb @@ -141,7 +141,7 @@ def cache_config end def workspace_settings - if (ws_config = config[:workspace_settings] || {}) && ws_config.key?(job.queue) + if (ws_config = config[:workspace_settings] || {}) && ws_config[job.queue] config[:workspace_settings][job.queue].to_h end end From 4a1c6449acf79e07a53a142463717534edca082a Mon Sep 17 00:00:00 2001 From: Piotr Milcarz Date: Fri, 28 Jun 2019 10:52:27 +0200 Subject: [PATCH 10/81] Environment variables can be restricted to specified branch --- lib/travis/scheduler/serialize/worker/job.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/travis/scheduler/serialize/worker/job.rb b/lib/travis/scheduler/serialize/worker/job.rb index 522f3495..eb08d52b 100644 --- a/lib/travis/scheduler/serialize/worker/job.rb +++ b/lib/travis/scheduler/serialize/worker/job.rb @@ -62,7 +62,7 @@ def warmer? private def env_var(var) - { name: var.name, value: var.value.decrypt, public: var.public } + { name: var.name, value: var.value.decrypt, public: var.public, branch: var.branch.to_s } end def has_secure_vars?(key) From e9ffa093d3ac765eb8a70d8e88d62023c0a365a8 Mon Sep 17 00:00:00 2001 From: Piotr Milcarz Date: Fri, 28 Jun 2019 11:14:03 +0200 Subject: [PATCH 11/81] Env model extended --- lib/travis/scheduler/record/repository/settings.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/travis/scheduler/record/repository/settings.rb b/lib/travis/scheduler/record/repository/settings.rb index c75cd22f..239b9597 100644 --- a/lib/travis/scheduler/record/repository/settings.rb +++ b/lib/travis/scheduler/record/repository/settings.rb @@ -14,6 +14,7 @@ class EnvVar < Travis::Settings::Model attribute :name, String attribute :value, Travis::Settings::EncryptedValue attribute :public, Boolean, default: false + attribute :branch, String attribute :repository_id, Integer validates :name, presence: true From c2c73c8aa5400ffe3bd653817fe5e6a33532ecc5 Mon Sep 17 00:00:00 2001 From: Piotr Milcarz Date: Fri, 28 Jun 2019 11:26:54 +0200 Subject: [PATCH 12/81] Tests fixes --- spec/travis/scheduler/serialize/worker_spec.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/travis/scheduler/serialize/worker_spec.rb b/spec/travis/scheduler/serialize/worker_spec.rb index d3ee76ea..9a226757 100644 --- a/spec/travis/scheduler/serialize/worker_spec.rb +++ b/spec/travis/scheduler/serialize/worker_spec.rb @@ -24,7 +24,7 @@ def encrypted(value) let(:settings) do Repository::Settings.load({ env_vars: [ - { name: 'FOO', value: encrypted('foo') }, + { name: 'FOO', value: encrypted('foo'), branch: 'foo-(dev)' }, { name: 'BAR', value: encrypted('bar'), public: true } ], timeout_hard_limit: 180, @@ -47,8 +47,8 @@ def encrypted(value) name: 'jobname', }, env_vars: [ - { name: 'FOO', value: 'foo', public: false }, - { name: 'BAR', value: 'bar', public: true } + { name: 'FOO', value: 'foo', public: false, branch: 'foo-(dev)' }, + { name: 'BAR', value: 'bar', public: true , branch: '' } ], job: { id: job.id, @@ -216,7 +216,7 @@ def encrypted(value) name: 'jobname', }, env_vars: [ - { name: 'BAR', value: 'bar', public: true } + { name: 'BAR', value: 'bar', public: true, branch: '' } ], job: { id: job.id, From 88f46097b0b211dc7420fc8fa419c122c760cc3d Mon Sep 17 00:00:00 2001 From: Piotr Milcarz Date: Fri, 28 Jun 2019 14:55:22 +0200 Subject: [PATCH 13/81] Return type improvement --- lib/travis/scheduler/serialize/worker/job.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/travis/scheduler/serialize/worker/job.rb b/lib/travis/scheduler/serialize/worker/job.rb index eb08d52b..9a097c78 100644 --- a/lib/travis/scheduler/serialize/worker/job.rb +++ b/lib/travis/scheduler/serialize/worker/job.rb @@ -62,7 +62,7 @@ def warmer? private def env_var(var) - { name: var.name, value: var.value.decrypt, public: var.public, branch: var.branch.to_s } + { name: var.name, value: var.value.decrypt, public: var.public, branch: var.branch } end def has_secure_vars?(key) From 2936ca81cff4be9f91ff218d6cc13333b2abb0ab Mon Sep 17 00:00:00 2001 From: Piotr Milcarz Date: Fri, 28 Jun 2019 15:01:50 +0200 Subject: [PATCH 14/81] Test fixed --- spec/travis/scheduler/serialize/worker_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/travis/scheduler/serialize/worker_spec.rb b/spec/travis/scheduler/serialize/worker_spec.rb index 9a226757..d354777c 100644 --- a/spec/travis/scheduler/serialize/worker_spec.rb +++ b/spec/travis/scheduler/serialize/worker_spec.rb @@ -48,7 +48,7 @@ def encrypted(value) }, env_vars: [ { name: 'FOO', value: 'foo', public: false, branch: 'foo-(dev)' }, - { name: 'BAR', value: 'bar', public: true , branch: '' } + { name: 'BAR', value: 'bar', public: true } ], job: { id: job.id, @@ -216,7 +216,7 @@ def encrypted(value) name: 'jobname', }, env_vars: [ - { name: 'BAR', value: 'bar', public: true, branch: '' } + { name: 'BAR', value: 'bar', public: true, branch: nil } ], job: { id: job.id, From 0b795d7e182a8378e970822a0563cb22fb1e5cf5 Mon Sep 17 00:00:00 2001 From: Piotr Milcarz Date: Fri, 28 Jun 2019 15:06:10 +0200 Subject: [PATCH 15/81] Test fix --- spec/travis/scheduler/serialize/worker_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/travis/scheduler/serialize/worker_spec.rb b/spec/travis/scheduler/serialize/worker_spec.rb index d354777c..fbc0218b 100644 --- a/spec/travis/scheduler/serialize/worker_spec.rb +++ b/spec/travis/scheduler/serialize/worker_spec.rb @@ -48,7 +48,7 @@ def encrypted(value) }, env_vars: [ { name: 'FOO', value: 'foo', public: false, branch: 'foo-(dev)' }, - { name: 'BAR', value: 'bar', public: true } + { name: 'BAR', value: 'bar', public: true, branch: nil } ], job: { id: job.id, From e51a0b05d3b97e15d71cfc30312bafdbb02406a7 Mon Sep 17 00:00:00 2001 From: Hiro Asari Date: Mon, 1 Jul 2019 09:58:01 -0400 Subject: [PATCH 16/81] Rename workspace_settings to workspace *_settings is redundant. --- lib/travis/scheduler/serialize/worker.rb | 8 ++++---- spec/travis/scheduler/serialize/worker_spec.rb | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/travis/scheduler/serialize/worker.rb b/lib/travis/scheduler/serialize/worker.rb index b22ff4c1..5b04db8b 100644 --- a/lib/travis/scheduler/serialize/worker.rb +++ b/lib/travis/scheduler/serialize/worker.rb @@ -25,7 +25,7 @@ def data ssh_key: ssh_key.data, timeouts: repo.timeouts, cache_settings: cache_settings, - workspace_settings: workspace_settings, + workspace: workspace, enterprise: !!config[:enterprise], prefer_https: !!config[:prefer_https], keep_netrc: repo.keep_netrc? @@ -140,9 +140,9 @@ def cache_config config[:cache_settings] || {} end - def workspace_settings - if (ws_config = config[:workspace_settings] || {}) && ws_config[job.queue] - config[:workspace_settings][job.queue].to_h + def workspace + if (ws_config = config[:workspace] || {}) && ws_config[job.queue] + config[:workspace][job.queue].to_h end end diff --git a/spec/travis/scheduler/serialize/worker_spec.rb b/spec/travis/scheduler/serialize/worker_spec.rb index fbc0218b..d2c09527 100644 --- a/spec/travis/scheduler/serialize/worker_spec.rb +++ b/spec/travis/scheduler/serialize/worker_spec.rb @@ -13,7 +13,7 @@ def encrypted(value) let(:repo) { FactoryGirl.create(:repo, default_branch: 'branch') } let(:owner) { repo.owner } let(:data) { described_class.new(job, config).data } - let(:config) { { cache_settings: { 'builds.gce' => s3 }, workspace_settings: { 'builds.gce' => s3 }, github: { source_host: 'github.com', api_url: 'https://api.github.com' }, vm_configs: {} } } + let(:config) { { cache_settings: { 'builds.gce' => s3 }, workspace: { 'builds.gce' => s3 }, github: { source_host: 'github.com', api_url: 'https://api.github.com' }, vm_configs: {} } } let(:s3) { { access_key_id: 'ACCESS_KEY_ID', secret_access_key: 'SECRET_ACCESS_KEY', bucket_name: 'bucket' } } let(:event) { 'push' } let(:ref) { 'refs/tags/v1.2.3' } @@ -97,7 +97,7 @@ def encrypted(value) log_silence: 20 * 60 }, cache_settings: s3, - workspace_settings: s3, + workspace: s3, prefer_https: false, enterprise: false, keep_netrc: true @@ -268,7 +268,7 @@ def encrypted(value) log_silence: 20 * 60 }, cache_settings: s3, - workspace_settings: s3, + workspace: s3, prefer_https: false, enterprise: false, keep_netrc: true From f64bddce6eacdd2eeed851058741574bc60cd3aa Mon Sep 17 00:00:00 2001 From: Piotr Sarnacki Date: Tue, 2 Jul 2019 06:17:06 +0200 Subject: [PATCH 17/81] Don't count migrated jobs towards concurrency and don't enqueue them (#190) I've already attempted it to fix the problem in 01139a4, but I only fixed one code path, which is currently not used, so the changes didn't go into effect. This commit applies the changes to the other code path as well. The reason we need to do it is because when we migrate jobs from .org to .com they might still be in a state that is considered by gatekeeper. So they might be started but they're running on .org not .com, thus we shouldn't count them as running on .com. The same with new jobs - we don't want to start them as they would start on .org anyway. I decided to filter these jobs in code instead of in the database because the number of jobs that we fetch from the database is relatively small and if I had to do it in the DB I would have to always join jobs with repositories, which actually might be slower. --- lib/travis/scheduler/jobs/state.rb | 28 ++++------------------ lib/travis/scheduler/limit/jobs.rb | 6 ++++- lib/travis/scheduler/limit/state.rb | 15 ++++++++---- lib/travis/support/filter_migrated_jobs.rb | 11 +++++++++ spec/travis/scheduler/limit/com_spec.rb | 21 +++++++++++++++- 5 files changed, 52 insertions(+), 29 deletions(-) create mode 100644 lib/travis/support/filter_migrated_jobs.rb diff --git a/lib/travis/scheduler/jobs/state.rb b/lib/travis/scheduler/jobs/state.rb index fed26599..9a357725 100644 --- a/lib/travis/scheduler/jobs/state.rb +++ b/lib/travis/scheduler/jobs/state.rb @@ -1,11 +1,13 @@ require 'travis/scheduler/helper/context' require 'travis/scheduler/helper/metrics' +require 'travis/support/filter_migrated_jobs' module Travis module Scheduler module Jobs class State < Struct.new(:context, :owners) include Helper::Context, Helper::Metrics + include FilterMigratedJobs ATTRS = { running: %i(repository_id private queue org_id restarted_at), @@ -43,33 +45,13 @@ def count_running_by_queue(name) private def read_running - collection = Job.by_owners(owners.all).running.select(*ATTRS[:running]).includes(:repository).to_a - if Travis.config.com? - collection = collection.find_all { |job| - # I think it's fine to filter running jobs after querying. usually - # it's not a good idea to do it in Ruby rather than SQL but jobs - # that might be rejected here will be rather rare - it's only for - # the purpose of not running migrated jobs. Doing it in SQL on the - # other hand would likely need an additional index and a join with - # repositories - # - !job.org_id || (job.restarted_at && job.restarted_at > job.repository.migrated_at) - } - end - collection + result = Job.by_owners(owners.all).running.select(*ATTRS[:running]).includes(:repository).to_a + filter_migrated_jobs(result) end time :read_queueable, key: 'scheduler.running_jobs' def read_queueable - collection = Job.by_owners(owners.all).queueable.to_a - if Travis.config.com? - collection = collection.find_all { |job| - # we don't want to start migrated jobs if they are in the - # queueable state - !job.org_id || (job.restarted_at && job.restarted_at > job.repository.migrated_at) - } - end - collection + filter_migrated_jobs(Job.by_owners(owners.all).queueable.to_a) end time :read_queueable, key: 'scheduler.queueable_jobs' diff --git a/lib/travis/scheduler/limit/jobs.rb b/lib/travis/scheduler/limit/jobs.rb index b3fdaa3d..d01b6e2f 100644 --- a/lib/travis/scheduler/limit/jobs.rb +++ b/lib/travis/scheduler/limit/jobs.rb @@ -1,5 +1,6 @@ require 'travis/scheduler/helper/context' require 'travis/scheduler/helper/metrics' +require 'travis/support/filter_migrated_jobs' module Travis module Scheduler @@ -20,6 +21,7 @@ class Jobs < Struct.new(:context, :owners) require 'travis/scheduler/limit/state' include Helper::Context, Helper::Metrics + include FilterMigratedJobs LIMITS = [ByOwner, ByRepo, ByQueue, ByStage] attr_reader :waiting_by_owner @@ -125,7 +127,9 @@ def waiting end def queueable - @queueable ||= Job.by_owners(owners.all).queueable.to_a + @queueable ||= begin + filter_migrated_jobs(Job.by_owners(owners.all).queueable.to_a) + end end time :queueable, key: 'scheduler.queueable_jobs' diff --git a/lib/travis/scheduler/limit/state.rb b/lib/travis/scheduler/limit/state.rb index e65084d0..d677c797 100644 --- a/lib/travis/scheduler/limit/state.rb +++ b/lib/travis/scheduler/limit/state.rb @@ -1,8 +1,11 @@ +require 'travis/support/filter_migrated_jobs' + module Travis module Scheduler module Limit class State BOOST = 'scheduler.owner.limit.%s' + include FilterMigratedJobs attr_reader :owners, :config @@ -14,19 +17,19 @@ def initialize(owners, config = {}) end def running_by_owners - @count[:owners] ||= running_jobs_by_owners.count + @count[:owners] ||= filter_running_jobs(running_jobs_by_owners).count end def running_by_owners_public - @count[:public] ||= running_jobs_by_owners.where(private: false).count + @count[:public] ||= filter_running_jobs(running_jobs_by_owners.where(private: false)).count end def running_by_repo(id) - @count[:repo][id] ||= Job.by_repo(id).running.count + @count[:repo][id] ||= filter_running_jobs(Job.by_repo(id).running).count end def running_by_queue(queue) - @count[:queue][queue] ||= Job.by_owners(owners.all).by_queue(queue).running.count + @count[:queue][queue] ||= filter_running_jobs(Job.by_owners(owners.all).by_queue(queue).running).count end def boost_for(login) @@ -38,6 +41,10 @@ def boost_for(login) def running_jobs_by_owners @running_jobs_by_owners ||= Job.by_owners(owners.all).running end + + def filter_running_jobs(jobs) + filter_migrated_jobs(jobs) + end end end end diff --git a/lib/travis/support/filter_migrated_jobs.rb b/lib/travis/support/filter_migrated_jobs.rb new file mode 100644 index 00000000..c974f0f0 --- /dev/null +++ b/lib/travis/support/filter_migrated_jobs.rb @@ -0,0 +1,11 @@ +module Travis::FilterMigratedJobs + def filter_migrated_jobs(jobs) + if Travis.config.com? + jobs.find_all { |job| + !job.org_id || (job.restarted_at && job.restarted_at > job.repository.migrated_at) + } + else + jobs + end + end +end diff --git a/spec/travis/scheduler/limit/com_spec.rb b/spec/travis/scheduler/limit/com_spec.rb index 57fd361a..8eee8024 100644 --- a/spec/travis/scheduler/limit/com_spec.rb +++ b/spec/travis/scheduler/limit/com_spec.rb @@ -1,6 +1,6 @@ describe Travis::Scheduler::Limit::Jobs, 'com (github apps)' do let(:org) { FactoryGirl.create(:org, login: 'travis-ci') } - let(:repo) { FactoryGirl.create(:repo, owner: owner) } + let(:repo) { FactoryGirl.create(:repo, owner: owner, migrated_at: 1.hour.ago) } let(:build) { FactoryGirl.create(:build) } let!(:owner) { FactoryGirl.create(:user, login: 'svenfuchs') } let(:owners) { Travis::Owners.group(data, config.to_h) } @@ -115,6 +115,25 @@ def subscription(plan, owner = self.owner) it { expect(report).to include('user svenfuchs: total: 4, running: 2, queueable: 3') } it { expect(limit.waiting_by_owner).to eq 1 } end + + describe 'with migrated jobs' do + before { create_jobs(1, state: :started, private: false) } + before { create_jobs(1, state: :created, private: false) } + + # a job that was migrated from org, shouldn't count towards running jobs + before { create_jobs(1, private: false, state: :started, org_id: 10) } + # a job that was migrated from org, but restarted after migration, should + # be counted towards running jobs + before { create_jobs(1, private: false, state: :started, org_id: 11, restarted_at: Time.now) } + # a job that was migrated as queueable, shouldn't be queued + before { create_jobs(1, private: false, state: :created, org_id: 12) } + # a job that was migrated and restarted + before { create_jobs(1, private: false, state: :created, org_id: 13, restarted_at: Time.now) } + before { run } + + it { expect(selected).to eq 2 } + it { expect(report).to include('user svenfuchs: total: 2, running: 2, queueable: 2') } + end end describe 'with a custom config limit unlimited' do From f071b95c97fb07a92458b14b09db2fd973f4a93a Mon Sep 17 00:00:00 2001 From: Sven Fuchs Date: Wed, 18 Sep 2019 22:43:39 +0200 Subject: [PATCH 18/81] allow env vars to be hashes --- .../serialize/worker/config/decrypt.rb | 33 ++++++++++-- .../serialize/worker/config/normalize.rb | 8 +-- .../scheduler/serialize/worker/config_spec.rb | 53 +++++++------------ 3 files changed, 49 insertions(+), 45 deletions(-) diff --git a/lib/travis/scheduler/serialize/worker/config/decrypt.rb b/lib/travis/scheduler/serialize/worker/config/decrypt.rb index 6ae8b3c9..d1609d77 100644 --- a/lib/travis/scheduler/serialize/worker/config/decrypt.rb +++ b/lib/travis/scheduler/serialize/worker/config/decrypt.rb @@ -20,7 +20,7 @@ def secure_env? end def process_env(env) - env = secure_env? ? decrypt_env(env) : remove_encrypted_env(env) + env = secure_env? ? decrypt_env(env) : to_vars(remove_encrypted_env(env)) env.compact end @@ -30,8 +30,24 @@ def remove_encrypted_env(env) end end - def decrypt_env(env) - env.map { |var| decrypt_var(var) } + def decrypt_env(vars) + case vars + when Array + vars.map { |var| decrypt_env(var) }.flatten + when Hash + vars.key?(:secure) ? decrypt_var(vars) : decrypt_hash(vars) + when String + decrypt_var(vars) + end + end + + def decrypt_hash(vars) + vars.map do |key, value| + secure = false + value = decryptor.decrypt(value) { |value| secure = value } + var = "#{key}=#{value}" + secure ? "SECURE #{var}" : var + end end def decrypt_var(var) @@ -41,6 +57,17 @@ def decrypt_var(var) rescue {} end + + def to_vars(env) + case env + when Array + env.map { |var| to_vars(var) }.flatten + when Hash + env.map { |key, value| [key, value].join('=') } + when String + env + end + end end end end diff --git a/lib/travis/scheduler/serialize/worker/config/normalize.rb b/lib/travis/scheduler/serialize/worker/config/normalize.rb index 4e6c8d06..28392696 100644 --- a/lib/travis/scheduler/serialize/worker/config/normalize.rb +++ b/lib/travis/scheduler/serialize/worker/config/normalize.rb @@ -61,13 +61,7 @@ def normalize_envs end def normalize_env(env) - [env].flatten.compact.map do |line| - if line.is_a?(Hash) && !line.key?(:secure) - line.map { |k, v| "#{k}=#{v}" }.join(' ') - else - line - end - end + [env].flatten.compact end def normalize_deploy diff --git a/spec/travis/scheduler/serialize/worker/config_spec.rb b/spec/travis/scheduler/serialize/worker/config_spec.rb index 6b61e627..aec1cfb0 100644 --- a/spec/travis/scheduler/serialize/worker/config_spec.rb +++ b/spec/travis/scheduler/serialize/worker/config_spec.rb @@ -12,8 +12,9 @@ def encrypt(string) let(:config) { { env: env, global_env: env } } let(:env) { [{ secure: 'invalid' }] } + before { subject } + it do - subject expect(config).to eql( env: [{ secure: 'invalid' }], global_env: [{ secure: 'invalid' }] @@ -21,12 +22,14 @@ def encrypt(string) end end - describe 'regular vars remain untouched' do + describe 'string vars' do let(:config) { { rvm: '1.8.7', env: 'FOO=foo', global_env: 'BAR=bar' } } + it { should eql(rvm: '1.8.7', env: ['FOO=foo'], global_env: ['BAR=bar']) } + end - it do - should eql(rvm: '1.8.7', env: ['FOO=foo'], global_env: ['BAR=bar']) - end + describe 'hash vars' do + let(:config) { { rvm: '1.8.7', env: { FOO: 'foo' }, global_env: { BAR: 'bar' } } } + it { should eql(rvm: '1.8.7', env: ['FOO=foo'], global_env: ['BAR=bar']) } end describe 'with a nil env' do @@ -45,40 +48,28 @@ def encrypt(string) include_examples :common - describe 'decrypts env vars' do + describe 'decrypts env string vars' do let(:config) { { env: env, global_env: env } } let(:env) { [encrypt('FOO=foo')] } + it { should eql env: ['SECURE FOO=foo'], global_env: ['SECURE FOO=foo'] } + end - it do - should eql( - env: ['SECURE FOO=foo'], - global_env: ['SECURE FOO=foo'] - ) - end + describe 'decrypts env hash vars' do + let(:config) { { env: env, global_env: env } } + let(:env) { [FOO: encrypt('foo')] } + it { should eql env: ['SECURE FOO=foo'], global_env: ['SECURE FOO=foo'] } end describe 'can mix secure and normal env vars' do let(:config) { { env: env, global_env: env } } let(:env) { [encrypt('FOO=foo'), 'BAR=bar'] } - - it do - should eql( - env: ['SECURE FOO=foo', 'BAR=bar'], - global_env: ['SECURE FOO=foo', 'BAR=bar'] - ) - end + it { should eql env: ['SECURE FOO=foo', 'BAR=bar'], global_env: ['SECURE FOO=foo', 'BAR=bar'] } end describe 'normalizes env vars which are hashes to strings' do let(:config) { { env: env, global_env: env } } let(:env) { [{ FOO: 'foo', BAR: 'bar' }, encrypt('BAZ=baz')] } - - it do - should eql( - env: ["FOO=foo BAR=bar", "SECURE BAZ=baz"], - global_env: ["FOO=foo BAR=bar", "SECURE BAZ=baz"] - ) - end + it { should eql env: ['FOO=foo', 'BAR=bar', 'SECURE BAZ=baz'], global_env: ['FOO=foo', 'BAR=bar', 'SECURE BAZ=baz'] } end end @@ -90,14 +81,7 @@ def encrypt(string) describe 'removes secure env vars' do let(:config) { { rvm: '1.8.7', env: env, global_env: env } } let(:env) { ['FOO=foo', 'BAR=bar', encrypt('BAZ=baz')] } - - it do - should eql( - rvm: '1.8.7', - env: ["FOO=foo", 'BAR=bar'], - global_env: ["FOO=foo", 'BAR=bar'] - ) - end + it { should eql rvm: '1.8.7', env: ["FOO=foo", 'BAR=bar'], global_env: ["FOO=foo", 'BAR=bar'] } end end @@ -202,5 +186,4 @@ def encrypt(string) end end end - end From 6f0b2a15eeb8c99118cdd5f19cd2ed5b410dce50 Mon Sep 17 00:00:00 2001 From: Sven Fuchs Date: Thu, 19 Sep 2019 21:52:15 +0200 Subject: [PATCH 19/81] do not decrypt values that are not a String or Hash --- lib/travis/scheduler/serialize/worker/config/decrypt.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/travis/scheduler/serialize/worker/config/decrypt.rb b/lib/travis/scheduler/serialize/worker/config/decrypt.rb index d1609d77..160bc039 100644 --- a/lib/travis/scheduler/serialize/worker/config/decrypt.rb +++ b/lib/travis/scheduler/serialize/worker/config/decrypt.rb @@ -44,7 +44,7 @@ def decrypt_env(vars) def decrypt_hash(vars) vars.map do |key, value| secure = false - value = decryptor.decrypt(value) { |value| secure = value } + value = decryptor.decrypt(value) { |value| secure = value } if decrypt?(value) var = "#{key}=#{value}" secure ? "SECURE #{var}" : var end @@ -58,6 +58,10 @@ def decrypt_var(var) {} end + def decrypt?(value) + value.is_a?(Hash) || value.is_a?(String) + end + def to_vars(env) case env when Array From 4e2bad2d065302bec33474fd971bbb8cb9b67fcd Mon Sep 17 00:00:00 2001 From: Sven Fuchs Date: Fri, 20 Sep 2019 17:41:43 +0200 Subject: [PATCH 20/81] Revert "use config import for db setup" This reverts commit 455b3f0041f511d05e8c268869f76857dfc81e96. --- .travis.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 02309b3d..3aa98a06 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,9 +2,6 @@ language: ruby dist: trusty rvm: 2.4.2 -import: - - travis-ci/build-configs/db-setup.yml - cache: bundler: true @@ -20,4 +17,6 @@ env: before_install: gem install bundler +before_script: bundle exec rake db:create --trace + script: bundle exec rspec spec From 059fc53ee1b34c250b4f132fb3eff87899fcb1fe Mon Sep 17 00:00:00 2001 From: Sven Fuchs Date: Fri, 20 Sep 2019 17:41:53 +0200 Subject: [PATCH 21/81] Revert "Update .travis.yml" This reverts commit e8f725def0a3842a5909f6dde010d6d9140bdbe2. --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index 3aa98a06..ffd7ca94 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,6 +9,9 @@ services: - redis - rabbitmq +addons: + postgresql: 9.6 + env: matrix: - RAKE_TASK=spec From 219e895742291ff1f23a04d033600c70218a152b Mon Sep 17 00:00:00 2001 From: Pavel Dotsulenko Date: Tue, 1 Oct 2019 16:44:20 +0300 Subject: [PATCH 22/81] Merge pull request #196 from travis-ci/pd-oss-only-arm Allow ARM builds for Open Source only --- lib/travis/queue/matcher.rb | 4 ++++ spec/travis/queue_spec.rb | 24 +++++++++++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/lib/travis/queue/matcher.rb b/lib/travis/queue/matcher.rb index 9936172b..d7c4ee83 100644 --- a/lib/travis/queue/matcher.rb +++ b/lib/travis/queue/matcher.rb @@ -8,6 +8,8 @@ class Matcher < Struct.new(:job, :config, :logger) unknown_matchers: 'unknown matchers used for queue %s: %s (repo=%s)"' } + OSS_ONLY_ARCH = %w(arm64) + def matches?(attrs) check_unknown_matchers(attrs.keys) matches = matches_for(attrs) @@ -69,6 +71,8 @@ def resources end def arch + return nil if job.private? && OSS_ONLY_ARCH.include?(job.config[:arch]) + job.config[:arch] end diff --git a/spec/travis/queue_spec.rb b/spec/travis/queue_spec.rb index 5f05056a..39ada040 100644 --- a/spec/travis/queue_spec.rb +++ b/spec/travis/queue_spec.rb @@ -28,6 +28,7 @@ { queue: 'builds.mac_beta', osx_image: 'beta' }, { queue: 'builds.new-foo', language: 'foo', percentage: percent }, { queue: 'builds.old-foo', language: 'foo' }, + { queue: 'builds.arm64-lxd', arch: 'arm64' }, { queue: 'builds.power', arch: 'ppc64le' }, ] end @@ -151,7 +152,28 @@ describe 'arch: ppc64le' do let(:config) { { arch: 'ppc64le' } } - it { expect(queue).to eq 'builds.power' } + + context 'when repo is public' do + it { expect(queue).to eq 'builds.power' } + end + + context 'when repo is private' do + let(:job) { FactoryGirl.build(:job, config: config, owner: owner, repository: repo, private: true) } + it { expect(queue).to eq 'builds.power' } + end + end + + describe 'arch: arm64' do + let(:config) { { arch: 'arm64' } } + + context 'when repo is public' do + it { expect(queue).to eq 'builds.arm64-lxd' } + end + + context 'when repo is private' do + let(:job) { FactoryGirl.build(:job, config: config, owner: owner, repository: repo, private: true) } + it { expect(queue).to eq 'builds.default' } + end end end From 41f93e4d6a90e90024cdeb4b6e0a2671830b1c89 Mon Sep 17 00:00:00 2001 From: Pavel Dotsulenko Date: Tue, 8 Oct 2019 14:15:22 +0100 Subject: [PATCH 23/81] Merge pull request #197 from travis-ci/pd-ibm-oss-only Allow IBM power builds for Open Source only --- lib/travis/queue/matcher.rb | 2 +- spec/travis/queue_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/travis/queue/matcher.rb b/lib/travis/queue/matcher.rb index d7c4ee83..d558cd5b 100644 --- a/lib/travis/queue/matcher.rb +++ b/lib/travis/queue/matcher.rb @@ -8,7 +8,7 @@ class Matcher < Struct.new(:job, :config, :logger) unknown_matchers: 'unknown matchers used for queue %s: %s (repo=%s)"' } - OSS_ONLY_ARCH = %w(arm64) + OSS_ONLY_ARCH = %w(arm64 ppc64le) def matches?(attrs) check_unknown_matchers(attrs.keys) diff --git a/spec/travis/queue_spec.rb b/spec/travis/queue_spec.rb index 39ada040..f045b1ee 100644 --- a/spec/travis/queue_spec.rb +++ b/spec/travis/queue_spec.rb @@ -159,7 +159,7 @@ context 'when repo is private' do let(:job) { FactoryGirl.build(:job, config: config, owner: owner, repository: repo, private: true) } - it { expect(queue).to eq 'builds.power' } + it { expect(queue).to eq 'builds.default' } end end From 899bd0202b72776014055c6e41bf29cbac8548d9 Mon Sep 17 00:00:00 2001 From: Pavel Dotsulenko Date: Thu, 31 Oct 2019 19:04:42 +0200 Subject: [PATCH 24/81] Allow IBM z builds for Open Source only --- lib/travis/queue/matcher.rb | 2 +- spec/travis/queue_spec.rb | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/travis/queue/matcher.rb b/lib/travis/queue/matcher.rb index d558cd5b..d0d36765 100644 --- a/lib/travis/queue/matcher.rb +++ b/lib/travis/queue/matcher.rb @@ -8,7 +8,7 @@ class Matcher < Struct.new(:job, :config, :logger) unknown_matchers: 'unknown matchers used for queue %s: %s (repo=%s)"' } - OSS_ONLY_ARCH = %w(arm64 ppc64le) + OSS_ONLY_ARCH = %w(arm64 ppc64le s390x) def matches?(attrs) check_unknown_matchers(attrs.keys) diff --git a/spec/travis/queue_spec.rb b/spec/travis/queue_spec.rb index f045b1ee..dd412a82 100644 --- a/spec/travis/queue_spec.rb +++ b/spec/travis/queue_spec.rb @@ -30,6 +30,7 @@ { queue: 'builds.old-foo', language: 'foo' }, { queue: 'builds.arm64-lxd', arch: 'arm64' }, { queue: 'builds.power', arch: 'ppc64le' }, + { queue: 'builds.z', arch: 's390x' }, ] end @@ -150,6 +151,19 @@ it { expect(queue).to eq 'builds.default' } end + describe 'arch: s390x' do + let(:config) { { arch: 's390x' } } + + context 'when repo is public' do + it { expect(queue).to eq 'builds.z' } + end + + context 'when repo is private' do + let(:job) { FactoryGirl.build(:job, config: config, owner: owner, repository: repo, private: true) } + it { expect(queue).to eq 'builds.default' } + end + end + describe 'arch: ppc64le' do let(:config) { { arch: 'ppc64le' } } From 5f0a5704da6ee3df96728ae7936ba073748941ce Mon Sep 17 00:00:00 2001 From: Pavel Dotsulenko Date: Wed, 6 Nov 2019 13:20:46 +0200 Subject: [PATCH 25/81] Revert "Allow IBM z builds for Open Source only" --- lib/travis/queue/matcher.rb | 2 +- spec/travis/queue_spec.rb | 14 -------------- 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/lib/travis/queue/matcher.rb b/lib/travis/queue/matcher.rb index d0d36765..d558cd5b 100644 --- a/lib/travis/queue/matcher.rb +++ b/lib/travis/queue/matcher.rb @@ -8,7 +8,7 @@ class Matcher < Struct.new(:job, :config, :logger) unknown_matchers: 'unknown matchers used for queue %s: %s (repo=%s)"' } - OSS_ONLY_ARCH = %w(arm64 ppc64le s390x) + OSS_ONLY_ARCH = %w(arm64 ppc64le) def matches?(attrs) check_unknown_matchers(attrs.keys) diff --git a/spec/travis/queue_spec.rb b/spec/travis/queue_spec.rb index dd412a82..f045b1ee 100644 --- a/spec/travis/queue_spec.rb +++ b/spec/travis/queue_spec.rb @@ -30,7 +30,6 @@ { queue: 'builds.old-foo', language: 'foo' }, { queue: 'builds.arm64-lxd', arch: 'arm64' }, { queue: 'builds.power', arch: 'ppc64le' }, - { queue: 'builds.z', arch: 's390x' }, ] end @@ -151,19 +150,6 @@ it { expect(queue).to eq 'builds.default' } end - describe 'arch: s390x' do - let(:config) { { arch: 's390x' } } - - context 'when repo is public' do - it { expect(queue).to eq 'builds.z' } - end - - context 'when repo is private' do - let(:job) { FactoryGirl.build(:job, config: config, owner: owner, repository: repo, private: true) } - it { expect(queue).to eq 'builds.default' } - end - end - describe 'arch: ppc64le' do let(:config) { { arch: 'ppc64le' } } From 8768efb023ad67ea182a7a901092e85533bfec50 Mon Sep 17 00:00:00 2001 From: Pavel Dotsulenko Date: Wed, 6 Nov 2019 13:44:22 +0200 Subject: [PATCH 26/81] Merge pull request #199 from travis-ci/pd-limit-z-queues-public Allow IBM z builds for Open Source only --- lib/travis/queue/matcher.rb | 2 +- spec/travis/queue_spec.rb | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/travis/queue/matcher.rb b/lib/travis/queue/matcher.rb index d558cd5b..d0d36765 100644 --- a/lib/travis/queue/matcher.rb +++ b/lib/travis/queue/matcher.rb @@ -8,7 +8,7 @@ class Matcher < Struct.new(:job, :config, :logger) unknown_matchers: 'unknown matchers used for queue %s: %s (repo=%s)"' } - OSS_ONLY_ARCH = %w(arm64 ppc64le) + OSS_ONLY_ARCH = %w(arm64 ppc64le s390x) def matches?(attrs) check_unknown_matchers(attrs.keys) diff --git a/spec/travis/queue_spec.rb b/spec/travis/queue_spec.rb index f045b1ee..dd412a82 100644 --- a/spec/travis/queue_spec.rb +++ b/spec/travis/queue_spec.rb @@ -30,6 +30,7 @@ { queue: 'builds.old-foo', language: 'foo' }, { queue: 'builds.arm64-lxd', arch: 'arm64' }, { queue: 'builds.power', arch: 'ppc64le' }, + { queue: 'builds.z', arch: 's390x' }, ] end @@ -150,6 +151,19 @@ it { expect(queue).to eq 'builds.default' } end + describe 'arch: s390x' do + let(:config) { { arch: 's390x' } } + + context 'when repo is public' do + it { expect(queue).to eq 'builds.z' } + end + + context 'when repo is private' do + let(:job) { FactoryGirl.build(:job, config: config, owner: owner, repository: repo, private: true) } + it { expect(queue).to eq 'builds.default' } + end + end + describe 'arch: ppc64le' do let(:config) { { arch: 'ppc64le' } } From e5af52e6155cd44e906944922a4a24336771f53a Mon Sep 17 00:00:00 2001 From: Sven Fuchs Date: Thu, 7 Nov 2019 18:37:06 +0100 Subject: [PATCH 27/81] output a more useful message when decryption failed --- lib/travis/support/secure_config.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/travis/support/secure_config.rb b/lib/travis/support/secure_config.rb index ecf3667e..dab7edba 100644 --- a/lib/travis/support/secure_config.rb +++ b/lib/travis/support/secure_config.rb @@ -61,8 +61,7 @@ def decrypt_value(value) Travis.logger.error("Can not decrypt secure config value: #{value.inspect[0..10]} using key: #{key.inspect[0..10]}") end rescue OpenSSL::PKey::RSAError => e - # Travis.logger.error("Error during decrypting secure config value: #{e.message}") - value + '[unable to decrypt]' end def secure_key?(key) From afde6c886fcad73df46f99949985c3d83b46f20c Mon Sep 17 00:00:00 2001 From: Sven Fuchs Date: Mon, 18 Nov 2019 15:32:25 +0100 Subject: [PATCH 28/81] remove all scheduler/limit codepath --- lib/travis/scheduler/jobs/limit/stages.rb | 2 + lib/travis/scheduler/limit/by_owner.rb | 102 --- lib/travis/scheduler/limit/by_queue.rb | 56 -- lib/travis/scheduler/limit/by_repo.rb | 45 -- lib/travis/scheduler/limit/by_stage.rb | 59 -- lib/travis/scheduler/limit/jobs.rb | 142 ---- lib/travis/scheduler/limit/state.rb | 51 -- .../scheduler/service/enqueue_owners.rb | 14 +- spec/travis/scheduler/limit/com_spec.rb | 710 ------------------ spec/travis/scheduler/limit/org_spec.rb | 193 ----- .../scheduler/service/enqueue_owners_spec.rb | 50 +- spec/travis/scheduler/service/event_spec.rb | 8 +- 12 files changed, 22 insertions(+), 1410 deletions(-) delete mode 100644 lib/travis/scheduler/limit/by_owner.rb delete mode 100644 lib/travis/scheduler/limit/by_queue.rb delete mode 100644 lib/travis/scheduler/limit/by_repo.rb delete mode 100644 lib/travis/scheduler/limit/by_stage.rb delete mode 100644 lib/travis/scheduler/limit/jobs.rb delete mode 100644 lib/travis/scheduler/limit/state.rb delete mode 100644 spec/travis/scheduler/limit/com_spec.rb delete mode 100644 spec/travis/scheduler/limit/org_spec.rb diff --git a/lib/travis/scheduler/jobs/limit/stages.rb b/lib/travis/scheduler/jobs/limit/stages.rb index 6630473e..26001cf1 100644 --- a/lib/travis/scheduler/jobs/limit/stages.rb +++ b/lib/travis/scheduler/jobs/limit/stages.rb @@ -1,3 +1,5 @@ +require 'travis/scheduler/model/stages' + module Travis module Scheduler module Jobs diff --git a/lib/travis/scheduler/limit/by_owner.rb b/lib/travis/scheduler/limit/by_owner.rb deleted file mode 100644 index 3fe87273..00000000 --- a/lib/travis/scheduler/limit/by_owner.rb +++ /dev/null @@ -1,102 +0,0 @@ -require 'travis/scheduler/helper/context' -require 'travis/scheduler/helper/logging' -require 'travis/scheduler/model/trial' - -module Travis - module Scheduler - module Limit - class ByOwner < Struct.new(:context, :reports, :owners, :job, :selected, :state, :config) - include Helper::Context - - KEYS = [:by_boost, :by_config, :by_plan, :by_trial, :default] - - def enqueue? - unlimited || current < max || !com? && throw(:result, :limited) - end - - private - - def current - without_public(state.running_by_owners + selected.size) - end - - def max - KEYS.each do |key| - value = send(key) - break report(key, value) if value && value > 0 - end - end - - def unlimited - report :unlimited, true if unlimited? - end - - def by_boost - owners.logins.map { |login| state.boost_for(login) }.max - end - - def by_config - owners.logins.map { |login| config_for(login) }.max - end - - def by_plan - with_public(owners.max_jobs) - end - - def by_trial - max_trial if max_trial && trial.active? - end - - def unlimited? - owners.logins.any? { |login| config_for(login) == -1 } - end - - def config_for(login) - with_public(config[:limit][:by_owner][login].to_i) - end - - def max_trial - with_public(config[:limit][:trial]) - end - - def default - with_public(config[:limit][:default] || 5) - end - - def with_public(max) - max = max + config[:limit][:public].to_i if max.to_i > 0 && job.public? && com? - max - end - - def without_public(count) - count = count - running_and_selected_public_jobs_upto_config_limit if !job.public? && com? - count - end - - def running_and_selected_public_jobs_upto_config_limit - count = state.running_by_owners_public + selected.select(&:public?).size - count = [count, config[:limit][:public].to_i].min if config[:limit][:public] - count - end - - def com? - config.site == 'com' - end - - def trial - @trial ||= Model::Trial.new(owners, context.redis) - end - - def report(key, value) - key = key.to_s.sub('by_', '').to_sym - name = [job.owner.is_a?(User) ? 'user' : 'org', job.owner.login].join(' ') - args = [name, key, value] - args << owners.subscribed_owners.join(', ') if key == :plan - msg = MSGS[:"max_#{key}"] || MSGS[:max] - reports << msg % args - value - end - end - end - end -end diff --git a/lib/travis/scheduler/limit/by_queue.rb b/lib/travis/scheduler/limit/by_queue.rb deleted file mode 100644 index 86d08758..00000000 --- a/lib/travis/scheduler/limit/by_queue.rb +++ /dev/null @@ -1,56 +0,0 @@ -require 'travis/scheduler/helper/context' -require 'travis/scheduler/helper/logging' - -module Travis - module Scheduler - module Limit - class ByQueue < Struct.new(:context, :reports, :owners, :job, :selected, :state, :_) - include Helper::Context - - def enqueue? - return true unless enabled? - return true unless queue == ENV['BY_QUEUE_NAME'] - result = current < max - report(max) if result - result - end - - private - - def enabled? - config[owners.key] || ENV['BY_QUEUE_DEFAULT'] - end - - def current - state.running_by_queue(job.queue) + selected.select { |j| j.queue == queue }.size - end - - def max - config.fetch(owners.key, default).to_i - end - - def queue - job.queue ||= Queue.new(job, context.config, nil).select - end - - def repo - job.repository - end - - def report(value) - reports << MSGS[:max] % [owners.to_s, "queue #{job.queue}", value] - value - end - - def default - ENV['BY_QUEUE_DEFAULT'].to_i - end - - # TODO make this a repo setting at some point? - def config - @config ||= ENV['BY_QUEUE_LIMIT'].to_s.split(',').map { |pair| pair.split('=') }.to_h - end - end - end - end -end diff --git a/lib/travis/scheduler/limit/by_repo.rb b/lib/travis/scheduler/limit/by_repo.rb deleted file mode 100644 index 7b36127b..00000000 --- a/lib/travis/scheduler/limit/by_repo.rb +++ /dev/null @@ -1,45 +0,0 @@ -require 'travis/scheduler/helper/context' -require 'travis/scheduler/helper/logging' - -module Travis - module Scheduler - module Limit - class ByRepo < Struct.new(:context, :reports, :owners, :job, :selected, :state, :config) - include Helper::Context - - def enqueue? - unlimited? || by_settings - end - - private - - def unlimited? - max == 0 - end - - def by_settings - result = current < max - report :repo_settings, max unless result - result - end - - def current - state.running_by_repo(repo.id) + selected.select { |j| j.repository_id == repo.id }.size - end - - def max - repo.settings.maximum_number_of_builds.to_i - end - - def repo - job.repository - end - - def report(key, value) - reports << MSGS[:max] % ["repo #{repo.slug}", key.to_s.sub('by_', ''), value] - value - end - end - end - end -end diff --git a/lib/travis/scheduler/limit/by_stage.rb b/lib/travis/scheduler/limit/by_stage.rb deleted file mode 100644 index 215d7891..00000000 --- a/lib/travis/scheduler/limit/by_stage.rb +++ /dev/null @@ -1,59 +0,0 @@ -require 'travis/scheduler/helper/context' -require 'travis/scheduler/model/stages' - -module Travis - module Scheduler - module Limit - class ByStage < Struct.new(:context, :reports, :owners, :job, :queued, :state, :config) - include Helper::Context - - def enqueue? - return true unless job.stage_number - !!report if queueable? - end - - private - - def queueable? - queueable.map { |attrs| attrs[:id] }.include?(job.id) - end - - def queueable - @queueable ||= Stages.build(jobs).startable - end - - ATTRS = [:id, :state, :stage_number] - KEYS = [:id, :state, :stage] - - def jobs - @jobs ||= begin - # TODO would it make sense to cache these on `state`? - jobs = Job.where(source_id: job.source_id) - sort(jobs).map { |job| attrs(job) } - end - end - - def attrs(job) - { - id: job.id, - stage: job.stage_number, - state: job.finished? ? :finished : :created - } - end - - def sort(jobs) - num = ->(job) { job.stage_number.split('.').map(&:to_i) } - jobs.sort { |lft, rgt| num.(lft) <=> num.(rgt) } - end - - def stages - jobs.map { |job| job[:stage] } - end - - def report - reports << MSGS[:max_stage] % ["build id=#{job.source_id} repo=#{job.repository.slug}", job.stage.number, queueable.size] - end - end - end - end -end diff --git a/lib/travis/scheduler/limit/jobs.rb b/lib/travis/scheduler/limit/jobs.rb deleted file mode 100644 index d01b6e2f..00000000 --- a/lib/travis/scheduler/limit/jobs.rb +++ /dev/null @@ -1,142 +0,0 @@ -require 'travis/scheduler/helper/context' -require 'travis/scheduler/helper/metrics' -require 'travis/support/filter_migrated_jobs' - -module Travis - module Scheduler - module Limit - MSGS = { - max: 'max jobs for %s by %s: %s', - max_plan: 'max jobs for %s by %s: %s (%s)', - max_stage: 'jobs for %s limited at stage: %s (queueable: %s)', - summary: '%s: total: %s, running: %s, queueable: %s', - stats: 'jobs waiting for %s: %s' - } - - class Jobs < Struct.new(:context, :owners) - require 'travis/scheduler/limit/by_owner' - require 'travis/scheduler/limit/by_queue' - require 'travis/scheduler/limit/by_repo' - require 'travis/scheduler/limit/by_stage' - require 'travis/scheduler/limit/state' - - include Helper::Context, Helper::Metrics - include FilterMigratedJobs - - LIMITS = [ByOwner, ByRepo, ByQueue, ByStage] - attr_reader :waiting_by_owner - - def run - unleak_queueables if ENV['UNLEAK_QUEUEABLE_JOBS'] - @waiting_by_owner = 0 - check_all - report summary - report stats if waiting.any? - honeycomb - end - - def reports - @reports ||= [] - end - - def selected - @selected ||= [] - end - - private - - def unleak_queueables - Queueable.connection.execute <<~sql - DELETE FROM queueable_jobs - WHERE id IN ( - SELECT queueable_jobs.id - FROM queueable_jobs - JOIN jobs ON queueable_jobs.job_id = jobs.id - WHERE jobs.state <> 'created' AND #{Job.owned_by(owners.all).to_sql} - ) - sql - rescue => e - puts e.message - end - time :unleak_queueables, key: 'scheduler.unleak_queueables' - - # We run each queueable job through a series of limits and select it - # only if all limits have allowed the job through by returning true. - # I.e. if any limit returns false then the given job will not be - # selected for queueing. - def check_all - queueable.each_with_index do |job, index| - case check(job) - when :limited - # The rest of the jobs will definitely be waiting for - # concurrency, regardless of other limits that might apply - @waiting_by_owner += queueable.length - index - break - when true - selected << job - end - end - end - - def set_queue(job) - inline :set_queue, job - end - - def check(job) - catch(:result) { enqueue?(job) } - end - - def enqueue?(job) - limits = limits_for(job).map(&:enqueue?) - if !limits[0] - @waiting_by_owner += 1 - end - limits.inject(&:&) - end - - def limits_for(job) - LIMITS.map { |limit| limit.new(context, reports, owners, job, selected, state, config) } - end - - def summary - MSGS[:summary] % [owners.to_s, queueable.size, state.running_by_owners, selected.size] - end - - def stats - jobs = waiting.group_by(&:repository) - stats = jobs.map { |repo, jobs| [repo.slug, jobs.size].join('=') } - MSGS[:stats] % [owners.key, stats.join(', ')] - end - - def honeycomb - Travis::Honeycomb.context.add('scheduler.stats', { - running: state.running_by_owners, - enqueued: selected.size, - waiting: waiting.size, - waiting_for_concurrency: @waiting_by_owner, - concurrent: selected.size + state.running_by_owners, - }) - end - - def report(*reports) - self.reports.concat(reports).uniq! - end - - def waiting - queueable - selected - end - - def queueable - @queueable ||= begin - filter_migrated_jobs(Job.by_owners(owners.all).queueable.to_a) - end - end - time :queueable, key: 'scheduler.queueable_jobs' - - def state - @state ||= State.new(owners, config) - end - end - end - end -end diff --git a/lib/travis/scheduler/limit/state.rb b/lib/travis/scheduler/limit/state.rb deleted file mode 100644 index d677c797..00000000 --- a/lib/travis/scheduler/limit/state.rb +++ /dev/null @@ -1,51 +0,0 @@ -require 'travis/support/filter_migrated_jobs' - -module Travis - module Scheduler - module Limit - class State - BOOST = 'scheduler.owner.limit.%s' - include FilterMigratedJobs - - attr_reader :owners, :config - - def initialize(owners, config = {}) - @owners = owners - @config = config - @count = { repo: {}, queue: {} } - @boosts = {} - end - - def running_by_owners - @count[:owners] ||= filter_running_jobs(running_jobs_by_owners).count - end - - def running_by_owners_public - @count[:public] ||= filter_running_jobs(running_jobs_by_owners.where(private: false)).count - end - - def running_by_repo(id) - @count[:repo][id] ||= filter_running_jobs(Job.by_repo(id).running).count - end - - def running_by_queue(queue) - @count[:queue][queue] ||= filter_running_jobs(Job.by_owners(owners.all).by_queue(queue).running).count - end - - def boost_for(login) - @boosts[login] ||= Scheduler.redis.get(BOOST % login).to_i - end - - private - - def running_jobs_by_owners - @running_jobs_by_owners ||= Job.by_owners(owners.all).running - end - - def filter_running_jobs(jobs) - filter_migrated_jobs(jobs) - end - end - end - end -end diff --git a/lib/travis/scheduler/service/enqueue_owners.rb b/lib/travis/scheduler/service/enqueue_owners.rb index 8ab1aeb7..c016d82a 100644 --- a/lib/travis/scheduler/service/enqueue_owners.rb +++ b/lib/travis/scheduler/service/enqueue_owners.rb @@ -1,5 +1,3 @@ -require 'travis/rollout' -require 'travis/scheduler/limit/jobs' require 'travis/owners' module Travis @@ -44,20 +42,10 @@ def enqueue time :enqueue def limit - if jobs? - Jobs::Select.new(context, owners) - else - Limit::Jobs.new(context, owners) - end + Jobs::Select.new(context, owners) end memoize :limit - def jobs? - owners.any? do |owner| - Rollout.matches?(:jobs, uid: owner.uid, owner: owner.login) - end - end - def owners @owners ||= Owners.group(data, config.to_h) end diff --git a/spec/travis/scheduler/limit/com_spec.rb b/spec/travis/scheduler/limit/com_spec.rb deleted file mode 100644 index 8eee8024..00000000 --- a/spec/travis/scheduler/limit/com_spec.rb +++ /dev/null @@ -1,710 +0,0 @@ -describe Travis::Scheduler::Limit::Jobs, 'com (github apps)' do - let(:org) { FactoryGirl.create(:org, login: 'travis-ci') } - let(:repo) { FactoryGirl.create(:repo, owner: owner, migrated_at: 1.hour.ago) } - let(:build) { FactoryGirl.create(:build) } - let!(:owner) { FactoryGirl.create(:user, login: 'svenfuchs') } - let(:owners) { Travis::Owners.group(data, config.to_h) } - let(:context) { Travis::Scheduler.context } - let(:redis) { context.redis } - let(:config) { context.config } - let(:data) { { owner_type: 'User', owner_id: owner.id } } - let(:limit) { described_class.new(context, owners) } - let(:report) { limit.reports } - let(:selected) { limit.selected.size } - let(:waiting) { limit.waiting_by_owner } - let(:run) { limit.run; nil } - - env USE_QUEUEABLE_JOBS: true - - before { config.limit.trial = nil } - before { config.limit.default = 1 } - before { config.plans = { one: 1, two: 2, four: 4, seven: 7, ten: 10 } } - before { config.site = 'com' } - - def create_jobs(count, attrs = {}) - defaults = { - repository: repo, - owner: owner, - source: build, - state: :created, - queueable: true, - private: false - } - 1.upto(count) { FactoryGirl.create(:job, defaults.merge(attrs)) } - end - - def subscription(plan, owner = self.owner) - FactoryGirl.create(:subscription, selected_plan: plan, valid_to: Time.now.utc, owner_type: owner.class.name, owner_id: owner.id) - end - - before { config.limit.public = 3 } - - describe 'with a boost limit 2' do - before { redis.set("scheduler.owner.limit.#{owner.login}", 2) } - - describe 'with private jobs only' do - before { create_jobs(4, private: true) } - before { run } - - it { expect(selected).to eq 2 } - it { expect(report).to include('max jobs for user svenfuchs by boost: 2') } - it { expect(report).to include('user svenfuchs: total: 4, running: 0, queueable: 2') } - it { expect(report).to include('jobs waiting for svenfuchs: svenfuchs/gem-release=2') } - it { expect(waiting).to eq 2 } - end - - describe 'with public jobs only' do - before { create_jobs(4, private: false) } - before { run } - - it { expect(selected).to eq 2 } - it { expect(report).to include('max jobs for user svenfuchs by boost: 2') } - it { expect(report).to include('user svenfuchs: total: 4, running: 0, queueable: 2') } - it { expect(report).to include('jobs waiting for svenfuchs: svenfuchs/gem-release=2') } - it { expect(waiting).to eq 2 } - end - - describe 'for mixed public and private jobs' do - before { create_jobs(1, private: true) } - before { create_jobs(1, private: false) } - before { run } - - it { expect(selected).to eq 2 } - it { expect(report).to include('max jobs for user svenfuchs by boost: 2') } - it { expect(report).to include('user svenfuchs: total: 2, running: 0, queueable: 2') } - it { expect(waiting).to eq 0 } - end - end - - describe 'with a two jobs plan' do - before { subscription(:two) } - - describe 'with private jobs only' do - before { create_jobs(1, state: :started, private: true) } - before { create_jobs(2, state: :created, private: true) } - before { run } - - it { expect(selected).to eq 1 } - it { expect(report).to include('max jobs for user svenfuchs by plan: 2 (svenfuchs)') } - it { expect(report).to include('user svenfuchs: total: 2, running: 1, queueable: 1') } - it { expect(limit.waiting_by_owner).to eq 1 } - end - - describe 'with public jobs only' do - before { create_jobs(1, state: :started, private: false) } - before { create_jobs(2, state: :created, private: false) } - before { subscription(:two) } - before { run } - - it { expect(selected).to eq 2 } - it { expect(report).to include('max jobs for user svenfuchs by plan: 5 (svenfuchs)') } # TODO fix log output? - it { expect(report).to include('user svenfuchs: total: 2, running: 1, queueable: 2') } - it { expect(limit.waiting_by_owner).to eq 0 } - end - - describe 'for mixed public and private jobs' do - before { create_jobs(1, state: :started, private: true) } - before { create_jobs(1, state: :started, private: false) } - before { create_jobs(2, state: :created, private: true) } - before { create_jobs(2, state: :created, private: false) } - before { subscription(:two) } - before { run } - - it { expect(selected).to eq 3 } - it { expect(report).to include('max jobs for user svenfuchs by plan: 2 (svenfuchs)') } - it { expect(report).to include('user svenfuchs: total: 4, running: 2, queueable: 3') } - it { expect(limit.waiting_by_owner).to eq 1 } - end - - describe 'with migrated jobs' do - before { create_jobs(1, state: :started, private: false) } - before { create_jobs(1, state: :created, private: false) } - - # a job that was migrated from org, shouldn't count towards running jobs - before { create_jobs(1, private: false, state: :started, org_id: 10) } - # a job that was migrated from org, but restarted after migration, should - # be counted towards running jobs - before { create_jobs(1, private: false, state: :started, org_id: 11, restarted_at: Time.now) } - # a job that was migrated as queueable, shouldn't be queued - before { create_jobs(1, private: false, state: :created, org_id: 12) } - # a job that was migrated and restarted - before { create_jobs(1, private: false, state: :created, org_id: 13, restarted_at: Time.now) } - before { run } - - it { expect(selected).to eq 2 } - it { expect(report).to include('user svenfuchs: total: 2, running: 2, queueable: 2') } - end - end - - describe 'with a custom config limit unlimited' do - before { config.limit.by_owner[owner.login] = -1 } - - describe 'with private jobs only' do - before { create_jobs(3, private: true) } - before { run } - - it { expect(selected).to eq 3 } - it { expect(report).to include('max jobs for user svenfuchs by unlimited: true') } - it { expect(report).to include('user svenfuchs: total: 3, running: 0, queueable: 3') } - it { expect(limit.waiting_by_owner).to eq 0 } - end - - describe 'with public jobs only' do - before { create_jobs(3, private: true) } - before { run } - - it { expect(selected).to eq 3 } - it { expect(report).to include('max jobs for user svenfuchs by unlimited: true') } - it { expect(report).to include('user svenfuchs: total: 3, running: 0, queueable: 3') } - it { expect(limit.waiting_by_owner).to eq 0 } - end - - describe 'for mixed public and private jobs' do - before { create_jobs(3, private: true) } - before { run } - - it { expect(selected).to eq 3 } - it { expect(report).to include('max jobs for user svenfuchs by unlimited: true') } - it { expect(report).to include('user svenfuchs: total: 3, running: 0, queueable: 3') } - it { expect(limit.waiting_by_owner).to eq 0 } - end - end - - describe 'with a custom config limit 1' do - before { config.limit.by_owner[owner.login] = 1 } - - describe 'with private jobs only' do - before { create_jobs(3, private: true) } - before { run } - - it { expect(selected).to eq 1 } - it { expect(report).to include('max jobs for user svenfuchs by config: 1') } - it { expect(report).to include('user svenfuchs: total: 3, running: 0, queueable: 1') } - it { expect(limit.waiting_by_owner).to eq 2 } - end - - describe 'with public jobs only' do - before { create_jobs(3, private: true) } - before { run } - - it { expect(selected).to eq 1 } - it { expect(report).to include('max jobs for user svenfuchs by config: 1') } - it { expect(report).to include('user svenfuchs: total: 3, running: 0, queueable: 1') } - it { expect(limit.waiting_by_owner).to eq 2 } - end - - describe 'for mixed public and private jobs' do - before { create_jobs(2, private: true) } - before { create_jobs(2, private: false) } - before { run } - - it { expect(selected).to eq 3 } # this definitely weird, but probably not used at all - it { expect(report).to include('max jobs for user svenfuchs by config: 1') } - it { expect(report).to include('user svenfuchs: total: 4, running: 0, queueable: 3') } - it { expect(limit.waiting_by_owner).to eq 1 } - end - end - - describe 'with a trial' do - before { config.limit.trial = 2 } - before { context.redis.set("trial:#{owner.login}", 5) } - - describe 'with private jobs only' do - before { create_jobs(3, private: true) } - before { run } - - it { expect(selected).to eq 2 } - it { expect(report).to include('max jobs for user svenfuchs by trial: 2') } - it { expect(report).to include('user svenfuchs: total: 3, running: 0, queueable: 2') } - it { expect(limit.waiting_by_owner).to eq 1 } - end - - describe 'with public jobs only' do - before { create_jobs(3, private: false) } - before { run } - - it { expect(selected).to eq 3 } - it { expect(report).to include('max jobs for user svenfuchs by trial: 5') } # TODO fix log output - it { expect(report).to include('user svenfuchs: total: 3, running: 0, queueable: 3') } - it { expect(limit.waiting_by_owner).to eq 0 } - end - - describe 'for mixed public and private jobs' do - before { create_jobs(3, private: true) } - before { create_jobs(3, private: false) } - before { run } - - it { expect(selected).to eq 5 } - it { expect(report).to include('max jobs for user svenfuchs by trial: 2') } - it { expect(report).to include('user svenfuchs: total: 6, running: 0, queueable: 5') } - it { expect(limit.waiting_by_owner).to eq 1 } - end - end - - describe 'with a default limit 1' do - before { config.limit.default = 1 } - - describe 'with private jobs only' do - before { create_jobs(3, private: true) } - before { run } - - it { expect(selected).to eq 1 } - it { expect(report).to include('max jobs for user svenfuchs by default: 1') } - it { expect(report).to include('user svenfuchs: total: 3, running: 0, queueable: 1') } - it { expect(limit.waiting_by_owner).to eq 2 } - end - - describe 'with public jobs only' do - before { create_jobs(3, private: false) } - before { run } - - it { expect(selected).to eq 3 } - it { expect(report).to include('max jobs for user svenfuchs by default: 4') } - it { expect(report).to include('user svenfuchs: total: 3, running: 0, queueable: 3') } - it { expect(limit.waiting_by_owner).to eq 0 } - end - - describe 'for mixed public and private jobs' do - before { create_jobs(3, private: true) } - before { create_jobs(3, private: false) } - before { run } - - it { expect(selected).to eq 4 } - it { expect(report).to include('max jobs for user svenfuchs by default: 1') } - it { expect(report).to include('user svenfuchs: total: 6, running: 0, queueable: 4') } - it { expect(limit.waiting_by_owner).to eq 2 } - end - end - - describe 'with a default limit 5 and a repo settings limit 2' do - before { config.limit.default = 5 } - before { repo.settings.update_attributes!(maximum_number_of_builds: 2) } - - describe 'with private jobs only' do - before { create_jobs(3, private: true) } - before { run } - - it { expect(selected).to eq 2 } - it { expect(report).to include('max jobs for repo svenfuchs/gem-release by repo_settings: 2') } - it { expect(report).to include('user svenfuchs: total: 3, running: 0, queueable: 2') } - it { expect(limit.waiting_by_owner).to eq 0 } - end - - describe 'with public jobs only' do - before { create_jobs(3, private: false) } - before { run } - - it { expect(selected).to eq 2 } - it { expect(report).to include('max jobs for repo svenfuchs/gem-release by repo_settings: 2') } - it { expect(report).to include('user svenfuchs: total: 3, running: 0, queueable: 2') } - it { expect(limit.waiting_by_owner).to eq 0 } - end - - describe 'for mixed public and private jobs' do - before { create_jobs(3, private: true) } - before { create_jobs(3, private: false) } - before { run } - - it { expect(selected).to eq 2 } - it { expect(report).to include('max jobs for repo svenfuchs/gem-release by repo_settings: 2') } - it { expect(report).to include('user svenfuchs: total: 6, running: 0, queueable: 2') } - it { expect(limit.waiting_by_owner).to eq 0 } - end - end - - describe 'with a default limit 1, a four jobs plan, and a repo setting of 3' do - before { subscription(:four) } - before { repo.settings.update_attributes!(maximum_number_of_builds: 3) } - - describe 'with private jobs only' do - before { create_jobs(1, state: :started, private: true) } - before { create_jobs(4, state: :created, private: true) } - before { run } - - it { expect(selected).to eq 2 } - it { expect(report).to include('max jobs for user svenfuchs by plan: 4 (svenfuchs)') } - it { expect(report).to include('max jobs for repo svenfuchs/gem-release by repo_settings: 3') } - it { expect(report).to include('user svenfuchs: total: 4, running: 1, queueable: 2') } - it { expect(limit.waiting_by_owner).to eq 0 } # TODO eh??? should be 2, no? - end - - describe 'with public jobs only' do - before { create_jobs(1, state: :started, private: false) } - before { create_jobs(4, state: :created, private: false) } - before { run } - - it { expect(selected).to eq 2 } - it { expect(report).to include('max jobs for user svenfuchs by plan: 7 (svenfuchs)') } # TODO fix log output? - it { expect(report).to include('max jobs for repo svenfuchs/gem-release by repo_settings: 3') } - it { expect(report).to include('user svenfuchs: total: 4, running: 1, queueable: 2') } - it { expect(limit.waiting_by_owner).to eq 0 } - end - - describe 'for mixed public and private jobs' do - before { create_jobs(1, state: :started, private: true) } - before { create_jobs(1, state: :started, private: false) } - before { create_jobs(3, state: :created, private: true) } - before { create_jobs(3, state: :created, private: false) } - before { run } - - it { expect(selected).to eq 1 } - it { expect(report).to include('max jobs for user svenfuchs by plan: 4 (svenfuchs)') } - it { expect(report).to include('max jobs for repo svenfuchs/gem-release by repo_settings: 3') } - it { expect(report).to include('user svenfuchs: total: 6, running: 2, queueable: 1') } - it { expect(limit.waiting_by_owner).to eq 0 } # TODO eh??? - end - end - - describe 'with no by_queue config being given and a two jobs plan' do - describe 'with private jobs only' do - before { create_jobs(3, private: true, queue: 'builds.osx') } - before { create_jobs(1, private: true, queue: 'builds.docker') } - before { subscription(:two) } - before { run } - - it { expect(selected).to eq 2 } - it { expect(report).to include('user svenfuchs: total: 4, running: 0, queueable: 2') } - end - - describe 'with public jobs only' do - before { create_jobs(3, private: false, queue: 'builds.osx') } - before { create_jobs(1, private: false, queue: 'builds.docker') } - before { run } - - it { expect(selected).to eq 4 } - it { expect(report).to include('user svenfuchs: total: 4, running: 0, queueable: 4') } - end - - describe 'for mixed public and private jobs' do - before { create_jobs(1, private: true, queue: 'builds.osx') } - before { create_jobs(1, private: true, queue: 'builds.docker') } - before { create_jobs(1, private: false, queue: 'builds.osx') } - before { create_jobs(1, private: false, queue: 'builds.docker') } - before { run } - - it { expect(selected).to eq 3 } - it { expect(report).to include('user svenfuchs: total: 4, running: 0, queueable: 3') } - end - end - - describe 'delegated accounts' do - describe 'with private jobs only' do - let(:carla) { FactoryGirl.create(:user, login: 'carla') } - - before { create_jobs(1, owner: owner, state: :started, queueable: false, private: true) } - before { create_jobs(1, owner: org, state: :started, queueable: false, private: true) } - before { create_jobs(3, owner: owner, state: :created, queueable: true, private: true) } - before { create_jobs(3, owner: org, state: :created, queueable: true, private: true) } - - before { config.limit.delegate = { owner.login => org.login, carla.login => org.login } } - - describe 'with one subscription' do - before { subscription(:seven, org) } - before { run } - - it { expect(selected).to eq 5 } - it { expect(limit.selected.map(&:owner).map(&:login)).to eq ['svenfuchs'] * 3 + ['travis-ci'] * 2 } - it { expect(report).to include('max jobs for user svenfuchs by plan: 7 (travis-ci)') } - it { expect(report).to include('user svenfuchs, user carla, org travis-ci: total: 6, running: 2, queueable: 5') } - it { expect(limit.waiting_by_owner).to eq 1 } - end - - describe 'with multiple subscriptions' do - before { subscription(:one, owner) } - before { subscription(:seven, org) } - before { run } - - it { expect(selected).to eq 6 } - it { expect(limit.selected.map(&:owner).map(&:login)).to eq ['svenfuchs'] * 3 + ['travis-ci'] * 3 } - it { expect(report).to include('max jobs for user svenfuchs by plan: 8 (svenfuchs, travis-ci)') } - it { expect(report).to include('user svenfuchs, user carla, org travis-ci: total: 6, running: 2, queueable: 6') } - end - end - - describe 'with public jobs only' do - let(:carla) { FactoryGirl.create(:user, login: 'carla') } - - before { create_jobs(1, owner: owner, state: :started, queueable: false, private: false) } - before { create_jobs(1, owner: org, state: :started, queueable: false, private: false) } - before { create_jobs(3, owner: owner, state: :created, queueable: true, private: false) } - before { create_jobs(3, owner: org, state: :created, queueable: true, private: false) } - - before { config.limit.delegate = { owner.login => org.login, carla.login => org.login } } - - describe 'with one subscription' do - before { subscription(:four, org) } - before { run } - - it { expect(selected).to eq 5 } - it { expect(limit.selected.map(&:owner).map(&:login)).to eq ['svenfuchs'] * 3 + ['travis-ci'] * 2 } - it { expect(report).to include('max jobs for user svenfuchs by plan: 7 (travis-ci)') } - it { expect(report).to include('user svenfuchs, user carla, org travis-ci: total: 6, running: 2, queueable: 5') } - it { expect(limit.waiting_by_owner).to eq 1 } - end - - describe 'with multiple subscriptions' do - before { subscription(:one, owner) } - before { subscription(:four, org) } - before { run } - - it { expect(selected).to eq 6 } - it { expect(limit.selected.map(&:owner).map(&:login)).to eq ['svenfuchs'] * 3 + ['travis-ci'] * 3 } - it { expect(report).to include('max jobs for user svenfuchs by plan: 8 (svenfuchs, travis-ci)') } # TODO fix log output - it { expect(report).to include('user svenfuchs, user carla, org travis-ci: total: 6, running: 2, queueable: 6') } - end - end - - describe 'for mixed public and private jobs' do - let(:carla) { FactoryGirl.create(:user, login: 'carla') } - - before { create_jobs(1, owner: owner, state: :started, queueable: false, private: true) } - before { create_jobs(1, owner: org, state: :started, queueable: false, private: true) } - before { create_jobs(3, owner: owner, state: :created, queueable: true, private: true) } - before { create_jobs(3, owner: org, state: :created, queueable: true, private: true) } - - before { config.limit.delegate = { owner.login => org.login, carla.login => org.login } } - - describe 'with one subscription' do - before { subscription(:seven, org) } - before { run } - - it { expect(selected).to eq 5 } - it { expect(limit.selected.map(&:owner).map(&:login)).to eq ['svenfuchs'] * 3 + ['travis-ci'] * 2 } - it { expect(report).to include('max jobs for user svenfuchs by plan: 7 (travis-ci)') } - it { expect(report).to include('user svenfuchs, user carla, org travis-ci: total: 6, running: 2, queueable: 5') } - it { expect(limit.waiting_by_owner).to eq 1 } - end - - describe 'with multiple subscriptions' do - before { subscription(:one, owner) } - before { subscription(:seven, org) } - before { run } - - it { expect(selected).to eq 6 } - it { expect(limit.selected.map(&:owner).map(&:login)).to eq ['svenfuchs'] * 3 + ['travis-ci'] * 3 } - it { expect(report).to include('max jobs for user svenfuchs by plan: 8 (svenfuchs, travis-ci)') } - it { expect(report).to include('user svenfuchs, user carla, org travis-ci: total: 6, running: 2, queueable: 6') } - end - end - end - - describe 'stages' do - describe 'with private jobs only' do - let(:one) { FactoryGirl.create(:stage, number: 1) } - let(:two) { FactoryGirl.create(:stage, number: 2) } - let(:three) { FactoryGirl.create(:stage, number: 3) } - - before { create_jobs(1, owner: owner, state: :created, private: true, stage: one, stage_number: '1.1') } - before { create_jobs(1, owner: owner, state: :created, private: true, stage: one, stage_number: '1.2') } - before { create_jobs(1, owner: owner, state: :created, private: true, stage: two, stage_number: '2.1') } - before { create_jobs(1, owner: owner, state: :created, private: true, stage: three, stage_number: '10.1') } - before { config.limit.default = 5 } - - describe 'queueing' do - before { run } - it { expect(selected).to eq 2 } - it { expect(report).to include("jobs for build id=#{build.id} repo=#{repo.slug} limited at stage: 1 (queueable: 2)") } - end - - describe 'ordering' do - before { one.jobs.update_all(state: :passed) } - before { Queueable.where(job_id: one.jobs.pluck(:id)).delete_all } - before { run } - it { expect(limit.selected.first.stage_number).to eq '2.1' } - end - end - - describe 'with public jobs only' do - let(:one) { FactoryGirl.create(:stage, number: 1) } - let(:two) { FactoryGirl.create(:stage, number: 2) } - let(:three) { FactoryGirl.create(:stage, number: 3) } - - before { create_jobs(1, owner: owner, state: :created, private: false, stage: one, stage_number: '1.1') } - before { create_jobs(1, owner: owner, state: :created, private: false, stage: one, stage_number: '1.2') } - before { create_jobs(1, owner: owner, state: :created, private: false, stage: two, stage_number: '2.1') } - before { create_jobs(1, owner: owner, state: :created, private: false, stage: three, stage_number: '10.1') } - before { config.limit.default = 5 } - - describe 'queueing' do - before { run } - it { expect(selected).to eq 2 } - it { expect(report).to include("jobs for build id=#{build.id} repo=#{repo.slug} limited at stage: 1 (queueable: 2)") } - end - - describe 'ordering' do - before { one.jobs.update_all(state: :passed) } - before { Queueable.where(job_id: one.jobs.pluck(:id)).delete_all } - before { run } - it { expect(limit.selected.first.stage_number).to eq '2.1' } - end - end - - describe 'for mixed public and private jobs' do - let(:one) { FactoryGirl.create(:stage, number: 1) } - let(:two) { FactoryGirl.create(:stage, number: 2) } - let(:three) { FactoryGirl.create(:stage, number: 3) } - - before { create_jobs(1, owner: owner, state: :created, private: true, stage: one, stage_number: '1.1') } - before { create_jobs(1, owner: owner, state: :created, private: false, stage: one, stage_number: '1.2') } - before { create_jobs(1, owner: owner, state: :created, private: true, stage: two, stage_number: '2.1') } - before { create_jobs(1, owner: owner, state: :created, private: false, stage: three, stage_number: '10.1') } - before { config.limit.default = 5 } - - describe 'queueing' do - before { run } - it { expect(selected).to eq 2 } - it { expect(report).to include("jobs for build id=#{build.id} repo=#{repo.slug} limited at stage: 1 (queueable: 2)") } - end - - describe 'ordering' do - before { one.jobs.update_all(state: :passed) } - before { Queueable.where(job_id: one.jobs.pluck(:id)).delete_all } - before { run } - it { expect(limit.selected.first.stage_number).to eq '2.1' } - end - end - end - - describe 'with a by_queue limit of 2 for the owner' do - describe 'with private jobs only' do - env BY_QUEUE_NAME: 'builds.osx' - env BY_QUEUE_LIMIT: 'svenfuchs=2' - - before { create_jobs(9, private: true, queue: 'builds.osx') } - before { create_jobs(1, private: true, queue: 'builds.docker') } - before { config.limit.default = 99 } - before { run } - - it { expect(selected).to eq 3 } - it { expect(report).to include('max jobs for user svenfuchs by queue builds.osx: 2') } - it { expect(report).to include('user svenfuchs: total: 10, running: 0, queueable: 3') } - end - - describe 'with public jobs only' do - env BY_QUEUE_NAME: 'builds.osx' - env BY_QUEUE_LIMIT: 'svenfuchs=2' - - before { create_jobs(9, private: false, queue: 'builds.osx') } - before { create_jobs(1, private: false, queue: 'builds.docker') } - before { config.limit.default = 99 } - before { run } - - it { expect(selected).to eq 3 } - it { expect(report).to include('max jobs for user svenfuchs by queue builds.osx: 2') } - it { expect(report).to include('user svenfuchs: total: 10, running: 0, queueable: 3') } - end - - describe 'for mixed public and private jobs' do - env BY_QUEUE_NAME: 'builds.osx' - env BY_QUEUE_LIMIT: 'svenfuchs=2' - - before { create_jobs(4, private: true, queue: 'builds.osx') } - before { create_jobs(1, private: true, queue: 'builds.docker') } - before { create_jobs(4, private: false, queue: 'builds.osx') } - before { create_jobs(1, private: false, queue: 'builds.docker') } - before { config.limit.default = 99 } - before { run } - - it { expect(selected).to eq 4 } - it { expect(report).to include('max jobs for user svenfuchs by queue builds.osx: 2') } - it { expect(report).to include('user svenfuchs: total: 10, running: 0, queueable: 4') } - end - end - - describe 'with a by_queue limit of 2 for the owner and a repo limit of 3 on another repo' do - describe 'with private jobs only' do - env BY_QUEUE_NAME: 'builds.osx' - env BY_QUEUE_LIMIT: 'svenfuchs=2' - env BY_QUEUE_DEFAULT: 2 - - let(:other) { FactoryGirl.create(:repo, github_id: 2) } - before { create_jobs(9, private: true, repository: repo, queue: 'builds.osx') } - before { create_jobs(5, private: true, repository: other, queue: 'builds.docker') } - before { config.limit.default = 99 } - before { other.settings.update_attributes!(maximum_number_of_builds: 3) } - before { run } - - it { expect(selected).to eq 5 } - it { expect(report).to include('max jobs for user svenfuchs by queue builds.osx: 2') } - it { expect(report).to include('user svenfuchs: total: 14, running: 0, queueable: 5') } - end - - describe 'with public jobs only' do - env BY_QUEUE_NAME: 'builds.osx' - env BY_QUEUE_LIMIT: 'svenfuchs=2' - env BY_QUEUE_DEFAULT: 2 - - let(:other) { FactoryGirl.create(:repo, github_id: 2) } - before { create_jobs(9, private: false, repository: repo, queue: 'builds.osx') } - before { create_jobs(5, private: false, repository: other, queue: 'builds.docker') } - before { config.limit.default = 99 } - before { other.settings.update_attributes!(maximum_number_of_builds: 3) } - before { run } - - it { expect(selected).to eq 5 } - it { expect(report).to include('max jobs for user svenfuchs by queue builds.osx: 2') } - it { expect(report).to include('user svenfuchs: total: 14, running: 0, queueable: 5') } - end - - describe 'for mixed public and private jobs' do - env BY_QUEUE_NAME: 'builds.osx' - env BY_QUEUE_LIMIT: 'svenfuchs=2' - env BY_QUEUE_DEFAULT: 2 - - let(:other) { FactoryGirl.create(:repo, github_id: 2) } - before { create_jobs(4, private: true, repository: repo, queue: 'builds.osx') } - before { create_jobs(2, private: true, repository: other, queue: 'builds.docker') } - before { create_jobs(4, private: false, repository: repo, queue: 'builds.osx') } - before { create_jobs(2, private: false, repository: other, queue: 'builds.docker') } - before { config.limit.default = 99 } - before { other.settings.update_attributes!(maximum_number_of_builds: 3) } - before { run } - - it { expect(selected).to eq 5 } - it { expect(report).to include('max jobs for user svenfuchs by queue builds.osx: 2') } - it { expect(report).to include('user svenfuchs: total: 12, running: 0, queueable: 5') } - end - end - - describe 'with a by_queue limit for the owner and jobs created for a different queue' do - env BY_QUEUE_NAME: 'builds.osx' - env BY_QUEUE_LIMIT: 'svenfuchs=2' - before { config.limit.default = 3 } - - describe 'with private jobs only' do - before { create_jobs(4, queue: 'builds.docker', private: true) } - before { create_jobs(1, queue: 'builds.osx', private: true) } - before { run } - - it { expect(selected).to eq 3 } - it { expect(report).to include('max jobs for user svenfuchs by default: 3') } - it { expect(report).to include('user svenfuchs: total: 5, running: 0, queueable: 3') } - it { expect(limit.waiting_by_owner).to eq 2 } - end - - describe 'with public jobs only' do - before { create_jobs(4, queue: 'builds.docker', private: true) } - before { create_jobs(1, queue: 'builds.osx', private: true) } - before { run } - - it { expect(selected).to eq 3 } - it { expect(report).to include('max jobs for user svenfuchs by default: 3') } - it { expect(report).to include('user svenfuchs: total: 5, running: 0, queueable: 3') } - it { expect(limit.waiting_by_owner).to eq 2 } - end - - describe 'for mixed public and private jobs' do - before { create_jobs(2, queue: 'builds.docker', private: true) } - before { create_jobs(1, queue: 'builds.osx', private: true) } - before { create_jobs(2, queue: 'builds.docker', private: false) } - before { create_jobs(1, queue: 'builds.osx', private: false) } - before { run } - - # TODO this is totally off and probably breaking expectations once in use - it { expect(selected).to eq 6 } - it { expect(report).to include('max jobs for user svenfuchs by default: 6') } - it { expect(report).to include('user svenfuchs: total: 6, running: 0, queueable: 6') } - it { expect(limit.waiting_by_owner).to eq 0 } - end - end -end diff --git a/spec/travis/scheduler/limit/org_spec.rb b/spec/travis/scheduler/limit/org_spec.rb deleted file mode 100644 index 79405d77..00000000 --- a/spec/travis/scheduler/limit/org_spec.rb +++ /dev/null @@ -1,193 +0,0 @@ -describe Travis::Scheduler::Limit::Jobs, 'org' do - let(:org) { FactoryGirl.create(:org, login: 'travis-ci') } - let(:repo) { FactoryGirl.create(:repo, owner: owner) } - let(:build) { FactoryGirl.create(:build) } - let!(:owner) { FactoryGirl.create(:user, login: 'svenfuchs') } - let(:owners) { Travis::Owners.group(data, config.to_h) } - let(:context) { Travis::Scheduler.context } - let(:redis) { context.redis } - let(:config) { context.config } - let(:data) { { owner_type: 'User', owner_id: owner.id } } - let(:limit) { described_class.new(context, owners) } - let(:report) { limit.reports } - let(:selected) { limit.selected.size } - let(:waiting) { limit.waiting_by_owner } - let(:run) { limit.run; nil } - - env USE_QUEUEABLE_JOBS: true - - before { config.limit.trial = nil } - before { config.limit.default = 5 } - before { config.plans = { one: 1, two: 2, four: 4, seven: 7, ten: 10 } } - before { config.site = 'org' } - - def create_jobs(count, attrs = {}) - defaults = { - repository: repo, - owner: owner, - source: build, - state: :created, - queueable: true, - private: false - } - 1.upto(count) { FactoryGirl.create(:job, defaults.merge(attrs)) } - end - - def subscription(plan) - FactoryGirl.create(:subscription, selected_plan: plan, valid_to: Time.now.utc, owner_type: owner.class.name, owner_id: owner.id) - end - - before { config.limit.public = 3 } - - describe 'with a boost limit 2' do - before { redis.set("scheduler.owner.limit.#{owner.login}", 2) } - before { create_jobs(4) } - before { run } - - it { expect(selected).to eq 2 } - it { expect(report).to include('max jobs for user svenfuchs by boost: 2') } - it { expect(report).to include('user svenfuchs: total: 4, running: 0, queueable: 2') } - it { expect(report).to include('jobs waiting for svenfuchs: svenfuchs/gem-release=2') } - it { expect(waiting).to eq 2 } - end - - describe 'with a custom config limit unlimited' do - before { config.limit.by_owner[owner.login] = -1 } - before { create_jobs(3) } - before { run } - - it { expect(selected).to eq 3 } - it { expect(report).to include('max jobs for user svenfuchs by unlimited: true') } - it { expect(report).to include('user svenfuchs: total: 3, running: 0, queueable: 3') } - it { expect(limit.waiting_by_owner).to eq 0 } - end - - describe 'with a custom config limit 1' do - before { config.limit.by_owner[owner.login] = 1 } - before { create_jobs(3) } - before { run } - - it { expect(selected).to eq 1 } - it { expect(report).to include('max jobs for user svenfuchs by config: 1') } - it { expect(report).to include('user svenfuchs: total: 3, running: 0, queueable: 1') } - it { expect(limit.waiting_by_owner).to eq 2 } - end - - describe 'with a default limit 1' do - before { config.limit.default = 1 } - before { create_jobs(3) } - before { run } - - it { expect(selected).to eq 1 } - it { expect(report).to include('max jobs for user svenfuchs by default: 1') } - it { expect(report).to include('user svenfuchs: total: 3, running: 0, queueable: 1') } - it { expect(limit.waiting_by_owner).to eq 2 } - end - - describe 'with a default limit 5 and a repo settings limit 2' do - before { config.limit.default = 5 } - before { repo.settings.update_attributes!(maximum_number_of_builds: 2) } - before { create_jobs(3) } - before { run } - - it { expect(selected).to eq 2 } - it { expect(report).to include('max jobs for repo svenfuchs/gem-release by repo_settings: 2') } - it { expect(report).to include('user svenfuchs: total: 3, running: 0, queueable: 2') } - it { expect(limit.waiting_by_owner).to eq 0 } - end - - describe 'with a default limit 5, and a repo setting of 3' do - before { config.limit.default = 5 } - before { repo.settings.update_attributes!(maximum_number_of_builds: 3) } - before { create_jobs(1, state: :started) } - before { create_jobs(4, state: :created) } - before { run } - - it { expect(selected).to eq 2 } - it { expect(report).to include('max jobs for repo svenfuchs/gem-release by repo_settings: 3') } - it { expect(report).to include('user svenfuchs: total: 4, running: 1, queueable: 2') } - it { expect(limit.waiting_by_owner).to eq 0 } - end - - describe 'with no by_queue config being given and a two jobs plan' do - before { create_jobs(3, queue: 'builds.osx') } - before { create_jobs(1, queue: 'builds.docker') } - before { run } - - it { expect(selected).to eq 4 } - it { expect(report).to include('user svenfuchs: total: 4, running: 0, queueable: 4') } - end - - describe 'stages' do - describe 'with public jobs only' do - let(:one) { FactoryGirl.create(:stage, number: 1) } - let(:two) { FactoryGirl.create(:stage, number: 2) } - let(:three) { FactoryGirl.create(:stage, number: 3) } - - before { create_jobs(1, owner: owner, state: :created, stage: one, stage_number: '1.1') } - before { create_jobs(1, owner: owner, state: :created, stage: one, stage_number: '1.2') } - before { create_jobs(1, owner: owner, state: :created, stage: two, stage_number: '2.1') } - before { create_jobs(1, owner: owner, state: :created, stage: three, stage_number: '10.1') } - before { config.limit.default = 5 } - - describe 'queueing' do - before { run } - it { expect(selected).to eq 2 } - it { expect(report).to include("jobs for build id=#{build.id} repo=#{repo.slug} limited at stage: 1 (queueable: 2)") } - end - - describe 'ordering' do - before { one.jobs.update_all(state: :passed) } - before { Queueable.where(job_id: one.jobs.pluck(:id)).delete_all } - before { run } - it { expect(limit.selected.first.stage_number).to eq '2.1' } - end - end - end - - describe 'with a by_queue limit of 2 for the owner' do - env BY_QUEUE_NAME: 'builds.osx' - env BY_QUEUE_LIMIT: 'svenfuchs=2' - - before { create_jobs(9, queue: 'builds.osx') } - before { create_jobs(1, queue: 'builds.docker') } - before { config.limit.default = 99 } - before { run } - - it { expect(selected).to eq 3 } - it { expect(report).to include('max jobs for user svenfuchs by queue builds.osx: 2') } - it { expect(report).to include('user svenfuchs: total: 10, running: 0, queueable: 3') } - end - - describe 'with a by_queue limit of 2 for the owner and a repo limit of 3 on another repo' do - env BY_QUEUE_NAME: 'builds.osx' - env BY_QUEUE_LIMIT: 'svenfuchs=2' - env BY_QUEUE_DEFAULT: 2 - - let(:other) { FactoryGirl.create(:repo, github_id: 2) } - before { create_jobs(9, repository: repo, queue: 'builds.osx') } - before { create_jobs(5, repository: other, queue: 'builds.docker') } - before { config.limit.default = 99 } - before { other.settings.update_attributes!(maximum_number_of_builds: 3) } - before { run } - - it { expect(selected).to eq 5 } - it { expect(report).to include('max jobs for user svenfuchs by queue builds.osx: 2') } - it { expect(report).to include('user svenfuchs: total: 14, running: 0, queueable: 5') } - end - - describe 'with a by_queue limit for the owner and jobs created for a different queue' do - env BY_QUEUE_NAME: 'builds.osx' - env BY_QUEUE_LIMIT: 'svenfuchs=2' - - before { create_jobs(9, queue: 'builds.docker') } - before { create_jobs(1, queue: 'builds.osx') } - before { config.limit.default = 5 } - before { run } - - it { expect(selected).to eq 5 } - it { expect(report).to include('max jobs for user svenfuchs by default: 5') } - it { expect(report).to include('user svenfuchs: total: 10, running: 0, queueable: 5') } - it { expect(limit.waiting_by_owner).to eq 5 } - end -end diff --git a/spec/travis/scheduler/service/enqueue_owners_spec.rb b/spec/travis/scheduler/service/enqueue_owners_spec.rb index 92ee12f0..e9b64e85 100644 --- a/spec/travis/scheduler/service/enqueue_owners_spec.rb +++ b/spec/travis/scheduler/service/enqueue_owners_spec.rb @@ -10,41 +10,21 @@ before { Travis::JobBoard.stubs(:post) } - describe 'legacy' do - before { 1.upto(2) { FactoryGirl.create(:job, commit: commit, repository: repo, owner: owner, private: true, state: :created, queue: 'builds.gce', config: {}) } } - before { config.limit.delegate = { owner.login => org.login } } - before { config.limit.default = 1 } - before { service.run } - - it { expect(Job.order(:id).map(&:state)).to eq %w[queued created] } - it { expect(Job.order(:id).map { |job| !!job.queueable }).to eq [false, true] } - - it { expect(log).to include "I 1234 Locking scheduler.owners-svenfuchs:travis-ci with: redis, ttl: 150s" } - it { expect(log).to include "I 1234 Evaluating jobs for owner group: user svenfuchs, org travis-ci" } - it { expect(log).to include "I 1234 enqueueing job #{job.id} (svenfuchs/gem-release)" } - it { expect(log).to include "I 1234 max jobs for user svenfuchs by default: 1" } - it { expect(log).to include "I 1234 user svenfuchs, org travis-ci: total: 2, running: 0, queueable: 1" } - it { expect(log).to include "I 1234 Publishing worker payload for job=#{job.id} queue=builds.gce" } - end - - describe 'with jobs enabled' do - env ROLLOUT: :jobs, ROLLOUT_JOBS_OWNERS: 'travis-ci' - before { 1.upto(2) { FactoryGirl.create(:job, commit: commit, repository: repo, owner: owner, private: true, state: :created, queue: 'builds.gce', config: {}) } } - before { config.limit.delegate = { owner.login => org.login } } - before { config.limit.by_owner = { org.login => 1 } } - before { service.run } - - it { expect(Job.order(:id).map(&:state)).to eq %w[queued created] } - it { expect(Job.order(:id).map { |job| !!job.queueable }).to eq [false, true] } - - it { expect(log).to include "I 1234 Locking scheduler.owners-svenfuchs:travis-ci with: redis, ttl: 150s" } - it { expect(log).to include "I 1234 Evaluating jobs for owner group: user svenfuchs, org travis-ci" } - it { expect(log).to include 'I 1234 user svenfuchs, org travis-ci config capacity: running=0 max=1 selected=1' } - it { expect(log).to include 'I 1234 repo svenfuchs/gem-release: queueable=2 running=0 selected=1 waiting=1' } - it { expect(log).to include 'I 1234 user svenfuchs, org travis-ci: queueable=2 running=0 selected=1 total_waiting=1 waiting_for_concurrency=1' } - it { expect(log).to include "I 1234 enqueueing job #{job.id} (svenfuchs/gem-release)" } - it { expect(log).to include "I 1234 Publishing worker payload for job=#{job.id} queue=builds.gce" } - end + before { 1.upto(2) { FactoryGirl.create(:job, commit: commit, repository: repo, owner: owner, private: true, state: :created, queue: 'builds.gce', config: {}) } } + before { config.limit.delegate = { owner.login => org.login } } + before { config.limit.by_owner = { org.login => 1 } } + before { service.run } + + it { expect(Job.order(:id).map(&:state)).to eq %w[queued created] } + it { expect(Job.order(:id).map { |job| !!job.queueable }).to eq [false, true] } + + it { expect(log).to include "I 1234 Locking scheduler.owners-svenfuchs:travis-ci with: redis, ttl: 150s" } + it { expect(log).to include "I 1234 Evaluating jobs for owner group: user svenfuchs, org travis-ci" } + it { expect(log).to include 'I 1234 user svenfuchs, org travis-ci config capacity: running=0 max=1 selected=1' } + it { expect(log).to include 'I 1234 repo svenfuchs/gem-release: queueable=2 running=0 selected=1 waiting=1' } + it { expect(log).to include 'I 1234 user svenfuchs, org travis-ci: queueable=2 running=0 selected=1 total_waiting=1 waiting_for_concurrency=1' } + it { expect(log).to include "I 1234 enqueueing job #{job.id} (svenfuchs/gem-release)" } + it { expect(log).to include "I 1234 Publishing worker payload for job=#{job.id} queue=builds.gce" } describe 'with invalid owner data' do let(:data) { { owner_type: nil, owner_id: 0 } } diff --git a/spec/travis/scheduler/service/event_spec.rb b/spec/travis/scheduler/service/event_spec.rb index 0ea56b2f..774150ab 100644 --- a/spec/travis/scheduler/service/event_spec.rb +++ b/spec/travis/scheduler/service/event_spec.rb @@ -12,15 +12,15 @@ context do before { Travis::JobBoard.stubs(:post) } before { config.limit.delegate = { owner.login => org.login } } - before { config.limit.default = 1 } + before { config.limit.by_owner = { org.login => 1 } } before { service.run } it { expect(Job.first.state).to eq 'queued' } - it { expect(log).to include "Evaluating jobs for owner group: user svenfuchs, org travis-ci" } + it { expect(log).to include 'Evaluating jobs for owner group: user svenfuchs, org travis-ci' } it { expect(log).to include "enqueueing job #{Job.first.id} (svenfuchs/gem-release)" } - it { expect(log).to include "max jobs for user svenfuchs by default: 1" } - it { expect(log).to include "user svenfuchs, org travis-ci: total: 1, running: 0, queueable: 1" } + it { expect(log).to include 'user svenfuchs, org travis-ci capacities: public max=5, config max=1' } + it { expect(log).to include 'user svenfuchs, org travis-ci: queueable=1 running=0 selected=1 total_waiting=0 waiting_for_concurrency=0' } end describe 'owner group already locked' do From 83cb0b03c8d8e8d2628877e48fe5f536e4f3a47b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damian=20Szyma=C5=84ski?= Date: Wed, 27 Nov 2019 11:05:26 +0100 Subject: [PATCH 29/81] Merge pull request #201 from travis-ci/default-cache Return default cache and workspace settings --- lib/travis/scheduler/serialize/worker.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/travis/scheduler/serialize/worker.rb b/lib/travis/scheduler/serialize/worker.rb index 5b04db8b..c944fd6e 100644 --- a/lib/travis/scheduler/serialize/worker.rb +++ b/lib/travis/scheduler/serialize/worker.rb @@ -133,7 +133,11 @@ def source_host end def cache_settings - cache_config[job.queue].to_h if cache_config[job.queue] + if cache_config[job.queue] + cache_config[job.queue].to_h + elsif cache_config['default'] + cache_config['default'].to_h + end end def cache_config @@ -143,6 +147,8 @@ def cache_config def workspace if (ws_config = config[:workspace] || {}) && ws_config[job.queue] config[:workspace][job.queue].to_h + elsif (ws_config = config[:workspace] || {}) && ws_config['default'] + config[:workspace]['default'].to_h end end From 72b0fb6109936836fb5084dc15ad6f0543ad983e Mon Sep 17 00:00:00 2001 From: Sven Fuchs Date: Fri, 29 Nov 2019 16:15:17 +0100 Subject: [PATCH 30/81] Revert "Use OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING when encrypting" This reverts commit f4db705c10e9cbea4d344ecb11374baa3071a8be. --- lib/travis/scheduler/record/ssl_key.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/travis/scheduler/record/ssl_key.rb b/lib/travis/scheduler/record/ssl_key.rb index 81f26114..798eb6be 100644 --- a/lib/travis/scheduler/record/ssl_key.rb +++ b/lib/travis/scheduler/record/ssl_key.rb @@ -9,12 +9,10 @@ def encode(string) end def encrypt(string) - key.public_encrypt(string, OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING) + key.public_encrypt(string) end def decrypt(string) - key.private_decrypt(string, OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING) - rescue OpenSSL::PKey::RSAError key.private_decrypt(string) end From 7a52accd6936094ae8f0e8c2ba8b87d802534ede Mon Sep 17 00:00:00 2001 From: Sven Fuchs Date: Fri, 7 Feb 2020 16:22:45 +0100 Subject: [PATCH 31/81] add warning --- lib/travis/scheduler/jobs/report.rb | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/lib/travis/scheduler/jobs/report.rb b/lib/travis/scheduler/jobs/report.rb index 905c3330..bee72509 100644 --- a/lib/travis/scheduler/jobs/report.rb +++ b/lib/travis/scheduler/jobs/report.rb @@ -10,16 +10,19 @@ module Jobs class Report < Struct.new(:owners, :state, :reports) include Helper::Memoize + WARN_QUEUE_SIZE = 10_000 + MSGS = { default: '%s capacity for %s: total=%s running=%s accepted=%s', queue: 'limited by queue %s for %s: max=%s rejected=%s accepted=%s', repo: 'limited by repo settings on %s: max=%s rejected=%s accepted=%s', stages: 'limited by stage repo=%s build_id=%s: rejected=%s accepted=%s', - summary: '%s: queueable=%s running=%s accepted=%s waiting=%s' + summary: '%s: queueable=%s running=%s accepted=%s waiting=%s', + excess: '%s excessive queue size %s' } def msgs - capacities.msgs + limits.msgs + by_repo.msgs + [totals.msg] + capacities.msgs + limits.msgs + by_repo.msgs + [totals.msg] + warnings end def metrics @@ -51,6 +54,16 @@ def totals end memoize :totals + def warnings + msgs = [] + msgs << MSGS[:warn] % [owners.to_s, queue_size] if queue_size > WARN_QUEUE_SIZE + msgs + end + + def queue_size + state.count_queueable + end + def data_for(type) reports.select { |report| report[:type] == type } end From e0a5134666b5dc59adce52d6a68512dfd2781c7f Mon Sep 17 00:00:00 2001 From: Sven Fuchs Date: Fri, 7 Feb 2020 19:16:11 +0100 Subject: [PATCH 32/81] allow setting queue size as env var --- lib/travis/scheduler/jobs/report.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/travis/scheduler/jobs/report.rb b/lib/travis/scheduler/jobs/report.rb index bee72509..44f10e06 100644 --- a/lib/travis/scheduler/jobs/report.rb +++ b/lib/travis/scheduler/jobs/report.rb @@ -10,7 +10,7 @@ module Jobs class Report < Struct.new(:owners, :state, :reports) include Helper::Memoize - WARN_QUEUE_SIZE = 10_000 + WARN_QUEUE_SIZE = ENV.fetch('WARN_QUEUE_SIZE', 10_000).to_i MSGS = { default: '%s capacity for %s: total=%s running=%s accepted=%s', From 3772b1d8999b23b1be6d405456a07125c2b966ae Mon Sep 17 00:00:00 2001 From: Sven Fuchs Date: Fri, 14 Feb 2020 15:43:48 +0100 Subject: [PATCH 33/81] fix msg key --- lib/travis/scheduler/jobs/report.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/travis/scheduler/jobs/report.rb b/lib/travis/scheduler/jobs/report.rb index 44f10e06..c1e7e51d 100644 --- a/lib/travis/scheduler/jobs/report.rb +++ b/lib/travis/scheduler/jobs/report.rb @@ -56,7 +56,7 @@ def totals def warnings msgs = [] - msgs << MSGS[:warn] % [owners.to_s, queue_size] if queue_size > WARN_QUEUE_SIZE + msgs << MSGS[:excess] % [owners.to_s, queue_size] if queue_size > WARN_QUEUE_SIZE msgs end From 702876cc88d7073201001a6b6b25b8d47f3e474d Mon Sep 17 00:00:00 2001 From: Sven Fuchs Date: Mon, 17 Feb 2020 21:50:42 +0100 Subject: [PATCH 34/81] pass secrets to build --- lib/travis/scheduler/serialize/worker.rb | 3 +- .../scheduler/serialize/worker/config.rb | 18 ++++++++++++ lib/travis/scheduler/serialize/worker/job.rb | 10 +++++++ .../scheduler/serialize/worker/job_spec.rb | 29 +++++++++++++++++-- .../travis/scheduler/serialize/worker_spec.rb | 6 ++-- 5 files changed, 60 insertions(+), 6 deletions(-) diff --git a/lib/travis/scheduler/serialize/worker.rb b/lib/travis/scheduler/serialize/worker.rb index 4db172f4..7ad7857a 100644 --- a/lib/travis/scheduler/serialize/worker.rb +++ b/lib/travis/scheduler/serialize/worker.rb @@ -29,7 +29,8 @@ def data workspace: workspace, enterprise: !!config[:enterprise], prefer_https: !!config[:prefer_https], - keep_netrc: repo.keep_netrc? + keep_netrc: repo.keep_netrc?, + secrets: job.secrets } data[:trace] = true if job.trace? data[:warmer] = true if job.warmer? diff --git a/lib/travis/scheduler/serialize/worker/config.rb b/lib/travis/scheduler/serialize/worker/config.rb index ac469ac1..b4fe9f96 100644 --- a/lib/travis/scheduler/serialize/worker/config.rb +++ b/lib/travis/scheduler/serialize/worker/config.rb @@ -18,6 +18,24 @@ def decrypt(config, decryptor, options) config = Normalize.new(config, options).jwt_sanitize config end + + def secrets(config) + secrets = [] + walk(config) { |obj| secrets << obj[:secure] if obj.key?(:secure) } + secrets + end + + def walk(obj, &block) + case obj + when Hash + block.call(obj) + obj.each { |key, obj| [key, walk(obj, &block)] }.to_h + when Array + obj.each { |obj| walk(obj, &block) } + else + obj + end + end end end end diff --git a/lib/travis/scheduler/serialize/worker/job.rb b/lib/travis/scheduler/serialize/worker/job.rb index 9a097c78..9e75a44d 100644 --- a/lib/travis/scheduler/serialize/worker/job.rb +++ b/lib/travis/scheduler/serialize/worker/job.rb @@ -44,6 +44,16 @@ def decrypted_config Config.decrypt(job.config, secure, full_addons: secure_env?, secure_env: secure_env?) end + def secrets + secrets = Config.secrets(job.config) + secrets.map { |str| decrypt(str) }.compact + end + + def decrypt(str) + repository.key.decrypt(Base64.decode64(str)) + rescue OpenSSL::PKey::RSAError => e + end + def vm_config # we'll want to see out what kinds of vm_config sets we have and # then decide how to best map what to where. at this point that diff --git a/spec/travis/scheduler/serialize/worker/job_spec.rb b/spec/travis/scheduler/serialize/worker/job_spec.rb index 1113e0c3..6fc030a0 100644 --- a/spec/travis/scheduler/serialize/worker/job_spec.rb +++ b/spec/travis/scheduler/serialize/worker/job_spec.rb @@ -1,8 +1,8 @@ describe Travis::Scheduler::Serialize::Worker::Job do let(:request) { Request.new } let(:build) { Build.new(request: request) } - let(:repository) { Repository.new } - let(:job) { Job.new(source: build, config: config, repository: repository) } + let(:repo) { FactoryGirl.create(:repository) } + let(:job) { Job.new(source: build, config: config, repository: repo) } let(:config) { {} } subject { described_class.new(job) } @@ -66,11 +66,34 @@ end context "when repository settings define a secure var" do - before { repository.settings.stubs(:has_secure_vars?).returns(true) } + before { repo.settings.stubs(:has_secure_vars?).returns(true) } it { expect(subject.secure_env_removed?).to eq(true) } end end + end + end + describe 'secrets' do + let(:config) do + { + env: { + global: [ + { secure: Base64.encode64(repo.key.encrypt('one')) }, + { secure: Base64.encode64(repo.key.encrypt('two')) }, + 'FOO=foo' + ] + }, + deploy: [ + { + provider: 's3', + secret_access_token: { + secure: Base64.encode64(repo.key.encrypt('three')) + } + } + ] + } end + + it { expect(subject.secrets).to eq %w(one two three) } end end diff --git a/spec/travis/scheduler/serialize/worker_spec.rb b/spec/travis/scheduler/serialize/worker_spec.rb index b074d339..19974b33 100644 --- a/spec/travis/scheduler/serialize/worker_spec.rb +++ b/spec/travis/scheduler/serialize/worker_spec.rb @@ -102,7 +102,8 @@ def encrypted(value) workspace: s3, prefer_https: false, enterprise: false, - keep_netrc: true + keep_netrc: true, + secrets: [] ) end @@ -275,7 +276,8 @@ def encrypted(value) workspace: s3, prefer_https: false, enterprise: false, - keep_netrc: true + keep_netrc: true, + secrets: [] ) end From 901291fafaac75b7003f2d0646a5cea744235b5f Mon Sep 17 00:00:00 2001 From: Sven Fuchs Date: Tue, 3 Mar 2020 16:18:33 +0100 Subject: [PATCH 35/81] do not try to decrypt non-string values --- lib/travis/scheduler/serialize/worker/job.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/travis/scheduler/serialize/worker/job.rb b/lib/travis/scheduler/serialize/worker/job.rb index 9e75a44d..cb2de19f 100644 --- a/lib/travis/scheduler/serialize/worker/job.rb +++ b/lib/travis/scheduler/serialize/worker/job.rb @@ -50,7 +50,7 @@ def secrets end def decrypt(str) - repository.key.decrypt(Base64.decode64(str)) + repository.key.decrypt(Base64.decode64(str)) if str.is_a?(String) rescue OpenSSL::PKey::RSAError => e end From 2d0c0972fe7ef90f35aaf7116f596f0df8a2c6c7 Mon Sep 17 00:00:00 2001 From: Piotr Milcarz Date: Wed, 11 Mar 2020 13:07:04 +0100 Subject: [PATCH 36/81] incomming change --- spec/travis/scheduler/serialize/worker_spec.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/spec/travis/scheduler/serialize/worker_spec.rb b/spec/travis/scheduler/serialize/worker_spec.rb index 1c8f2ecb..3e2ddeef 100644 --- a/spec/travis/scheduler/serialize/worker_spec.rb +++ b/spec/travis/scheduler/serialize/worker_spec.rb @@ -1,3 +1,4 @@ + require 'travis/scheduler/serialize/worker' describe Travis::Scheduler::Serialize::Worker do @@ -276,10 +277,7 @@ def encrypted(value) workspace: s3, prefer_https: false, enterprise: false, -<<<<<<< HEAD -======= keep_netrc: true, ->>>>>>> 901291fafaac75b7003f2d0646a5cea744235b5f secrets: [] ) end From 027b7b1fc153ce051f8df92708222823a0b8ba14 Mon Sep 17 00:00:00 2001 From: Sven Fuchs Date: Tue, 17 Mar 2020 21:05:02 +0100 Subject: [PATCH 37/81] bump rake --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 658fec40..832a0393 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -160,7 +160,7 @@ GEM rack (2.1.1) rack-protection (2.0.1) rack - rake (11.3.0) + rake (13.0.1) redis (3.3.3) redis-namespace (1.5.2) redis (~> 3.0, >= 3.0.4) From 7816288f881e03fd8ee899a686585ce311f5e47b Mon Sep 17 00:00:00 2001 From: Sven Fuchs Date: Tue, 17 Mar 2020 21:01:28 +0100 Subject: [PATCH 38/81] let capacities add up, no matter their type --- lib/travis/scheduler/jobs/capacities.rb | 26 ++++++---------- spec/travis/scheduler/jobs_spec.rb | 41 +++++++++++++++++++++++-- 2 files changed, 47 insertions(+), 20 deletions(-) diff --git a/lib/travis/scheduler/jobs/capacities.rb b/lib/travis/scheduler/jobs/capacities.rb index e8a7c6c3..87a0fece 100644 --- a/lib/travis/scheduler/jobs/capacities.rb +++ b/lib/travis/scheduler/jobs/capacities.rb @@ -13,48 +13,40 @@ module Jobs class Capacities < Struct.new(:context, :owners, :state) include Helper::Memoize - ANY = %i(boost config plan education trial) + ALL = %i(public boost config plan education trial) # TODO warn if no applicable :any capacity can be found def initialize(*) super - reduce(public, any) + reduce(*all) end def accept(job) - public.accept?(job) || any.try(:accept?, job) + all.detect { |capacity| capacity.accept?(job) } end def reports - active.map(&:reports).flatten + all.map(&:reports).flatten end memoize :reports def accepted - active.map(&:accepted).inject(&:+) + all.map(&:accepted).inject(&:+) end def exhausted? - active.all?(&:exhausted?) + all.all?(&:exhausted?) end memoize :exhausted def msg - "#{owners.to_s} capacities: #{active.map(&:to_s).join(', ')}" + "#{owners.to_s} capacities: #{all.map(&:to_s).join(', ')}" end private - def active - [public, any].compact - end - - def public - @public ||= build(:public) - end - - def any - @any ||= ANY.map { |name| build(name) }.detect(&:applicable?) + def all + @all ||= ALL.map { |name| build(name) }.select(&:applicable?) end def reduce(*all) diff --git a/spec/travis/scheduler/jobs_spec.rb b/spec/travis/scheduler/jobs_spec.rb index d913085f..0c52c444 100644 --- a/spec/travis/scheduler/jobs_spec.rb +++ b/spec/travis/scheduler/jobs_spec.rb @@ -277,7 +277,7 @@ def subscribe(plan, owner = self.user) end end - describe 'with a boost of 4, and a two jobs plan, only the boost is being used' do + describe 'with a boost of 4, and a two jobs plan, these add up' do before { subscribe(:two) } before { redis.set("scheduler.owner.limit.#{user.login}", 4) } @@ -285,9 +285,44 @@ def subscribe(plan, owner = self.user) before { create_jobs(1, private: true, state: :started) } before { create_jobs(5, private: true) } - it { expect(selected.size).to eq 3 } + it { expect(selected.size).to eq 5 } it { expect(reports).to include 'user svenfuchs boost capacity: running=1 max=4 selected=3' } - it { expect(reports).to include 'user svenfuchs: queueable=5 running=1 selected=3 total_waiting=2 waiting_for_concurrency=2' } + it { expect(reports).to include 'user svenfuchs: queueable=5 running=1 selected=5 total_waiting=0 waiting_for_concurrency=0' } + end + + describe 'with public jobs only' do + before { create_jobs(1, private: false, state: :started) } + before { create_jobs(5, private: false) } + + it { expect(selected.size).to eq 5 } + it { expect(reports).to include 'user svenfuchs public capacity: running=1 max=3 selected=2' } + it { expect(reports).to include 'user svenfuchs boost capacity: running=0 max=4 selected=3' } + it { expect(reports).to include 'user svenfuchs: queueable=5 running=1 selected=5 total_waiting=0 waiting_for_concurrency=0' } + end + + describe 'for mixed public and private jobs' do + before { create_jobs(1, private: true, state: :started) } + before { create_jobs(1, private: false, state: :started) } + before { create_jobs(2, private: false) + create_jobs(2, private: true) } + + it { expect(selected.size).to eq 4 } + it { expect(reports).to include 'user svenfuchs public capacity: running=1 max=3 selected=2' } + it { expect(reports).to include 'user svenfuchs boost capacity: running=1 max=4 selected=2' } + it { expect(reports).to include 'user svenfuchs: queueable=4 running=2 selected=4 total_waiting=0 waiting_for_concurrency=0' } + end + end + + describe 'with a boost of 4, and a config of 2, these add up' do + before { redis.set("scheduler.owner.limit.#{user.login}", 4) } + before { config[:limit][:by_owner][user.login] = 2 } + + describe 'with private jobs only' do + before { create_jobs(1, private: true, state: :started) } + before { create_jobs(5, private: true) } + + it { expect(selected.size).to eq 5 } + it { expect(reports).to include 'user svenfuchs boost capacity: running=1 max=4 selected=3' } + it { expect(reports).to include 'user svenfuchs: queueable=5 running=1 selected=5 total_waiting=0 waiting_for_concurrency=0' } end describe 'with public jobs only' do From ed060369372cbd902315fad79dbd71be186a06b2 Mon Sep 17 00:00:00 2001 From: gabriel-arc Date: Thu, 2 Apr 2020 16:38:13 +0200 Subject: [PATCH 39/81] unconditional build accept for enterprise --- lib/travis/scheduler/jobs/capacities.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/travis/scheduler/jobs/capacities.rb b/lib/travis/scheduler/jobs/capacities.rb index 87a0fece..6e34c5d2 100644 --- a/lib/travis/scheduler/jobs/capacities.rb +++ b/lib/travis/scheduler/jobs/capacities.rb @@ -22,7 +22,7 @@ def initialize(*) end def accept(job) - all.detect { |capacity| capacity.accept?(job) } + enterprise? ? true : all.detect { |capacity| capacity.accept?(job) } end def reports @@ -58,6 +58,10 @@ def reduce(*all) def build(name) Capacity.const_get(name.to_s.camelize).new(context, owners, self) end + + def enterprise? + !!context.config[:enterprise] + end end end end From 63814d710d73f30cbb9216859d242b4a008537f4 Mon Sep 17 00:00:00 2001 From: Eugene Shubin <51701929+eugene-travis@users.noreply.github.com> Date: Fri, 17 Apr 2020 13:54:18 +0300 Subject: [PATCH 40/81] empty commit to trigger a build From 28a5cb4dbeba53915d7634d9587b9a793d6bac01 Mon Sep 17 00:00:00 2001 From: Tahsin Hasan <51903216+Tahsin-travis-ci@users.noreply.github.com> Date: Fri, 17 Apr 2020 18:12:55 +0600 Subject: [PATCH 41/81] ruby encoder added (#13) Co-authored-by: Tahsin Hasan --- Dockerfile | 40 +++++++++++++++----- Makefile | 30 ++++++++++++++- bin/te-encode | 55 ++++++++++++++++++++++++++++ rgloader/loader.rb | 23 ++++++++++++ rgloader/rgloader.linux.x86_64.so | Bin 0 -> 87165 bytes rgloader/rgloader25.linux.x86_64.so | Bin 0 -> 91060 bytes rgloader/rgloader26.linux.x86_64.so | Bin 0 -> 91054 bytes 7 files changed, 137 insertions(+), 11 deletions(-) create mode 100644 bin/te-encode create mode 100644 rgloader/loader.rb create mode 100644 rgloader/rgloader.linux.x86_64.so create mode 100644 rgloader/rgloader25.linux.x86_64.so create mode 100644 rgloader/rgloader26.linux.x86_64.so diff --git a/Dockerfile b/Dockerfile index 469a7afc..c970be17 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,34 +1,54 @@ -FROM ruby:2.6.5-slim +# Defining platform type +ARG PLATFORM_TYPE=hosted -LABEL maintainer Travis CI GmbH +# Building the hosted base image +FROM ruby:2.6.5-slim as builder-hosted -# packages required for bundle install RUN ( \ apt-get update ; \ - apt-get install -y --no-install-recommends git make gcc g++ libpq-dev libjemalloc-dev \ + apt-get install -y --no-install-recommends gettext-base git make gcc g++ libpq-dev libjemalloc-dev openssh-server \ && rm -rf /var/lib/apt/lists/* \ ) +RUN mkdir -p /app +COPY . /app + +# Building the enterprise base image +FROM builder-hosted as builder-enterprise + +ARG RUBYENCODER_PROJECT_ID +ARG RUBYENCODER_PROJECT_KEY +ARG SSH_KEY +RUN ( \ + if test $RUBYENCODER_PROJECT_ID; then \ + chmod +x /app/bin/te-encode && \ + ./app/bin/te-encode && \ + rm -rf /root/.ssh/id_rsa; \ + fi; \ +) +FROM builder-${PLATFORM_TYPE} +LABEL maintainer Travis CI GmbH + +RUN ( \ + apt-get update ; \ + apt-get install -y --no-install-recommends gettext-base git make g++ libpq-dev \ + && rm -rf /var/lib/apt/lists/* \ +) ENV LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2 +RUN gem i bundler --no-document -v=2.1.4 # throw errors if Gemfile has been modified since Gemfile.lock RUN bundle config --global frozen 1 RUN bundle config set deployment 'true' -RUN mkdir -p /app WORKDIR /app COPY Gemfile /app COPY Gemfile.lock /app -RUN gem install bundler -v '2.1.4' - ARG bundle_gems__contribsys__com RUN bundle config https://gems.contribsys.com/ $bundle_gems__contribsys__com \ && bundle install \ && bundle config --delete https://gems.contribsys.com/ -RUN gem install --user-install executable-hooks - -COPY . /app CMD bundle exec bin/sidekiq-pgbouncer ${SIDEKIQ_CONCURRENCY:-5} ${SIDEKIQ_QUEUE:-scheduler} diff --git a/Makefile b/Makefile index d95431ab..10649df5 100644 --- a/Makefile +++ b/Makefile @@ -27,11 +27,39 @@ ifndef $$BUNDLE_GEMS__CONTRIBSYS__COM BUNDLE_GEMS__CONTRIBSYS__COM ?= $$BUNDLE_GEMS__CONTRIBSYS__COM endif +ifndef $$PLATFORM_TYPE + PLATFORM_TYPE ?= $$PLATFORM_TYPE +endif + +ifndef $$RUBYENCODER_PROJECT_ID + RUBYENCODER_PROJECT_ID ?= $$RUBYENCODER_PROJECT_ID +endif + +ifndef $$RUBYENCODER_PROJECT_KEY + RUBYENCODER_PROJECT_KEY ?= $$RUBYENCODER_PROJECT_KEY +endif + +ifeq ($(PLATFORM_TYPE), enterprise) + + ifeq ($(RUBYENCODER_PROJECT_ID),) + $(error RUBYENCODER_PROJECT_ID not set correctly.) + endif + + ifeq ($(RUBYENCODER_PROJECT_KEY),) + $(error RUBYENCODER_PROJECT_KEY not set correctly.) + endif + + BUILD_ARGUMENTS = --build-arg RUBYENCODER_PROJECT_ID="$(RUBYENCODER_PROJECT_ID)" --build-arg RUBYENCODER_PROJECT_KEY="$(RUBYENCODER_PROJECT_KEY)" --build-arg SSH_KEY="$$(cat ~/.ssh/id_rsa)" +else + PLATFORM_TYPE = hosted + BUILD_ARGUMENTS = +endif + DOCKER ?= docker .PHONY: docker-build docker-build: - $(DOCKER) build --build-arg bundle_gems__contribsys__com=$(BUNDLE_GEMS__CONTRIBSYS__COM) -t $(DOCKER_DEST) . + DOCKER_BUILDKIT=1 $(DOCKER) build --progress=plain --build-arg PLATFORM_TYPE="$(PLATFORM_TYPE)" $(BUILD_ARGUMENTS) --build-arg bundle_gems__contribsys__com=$(BUNDLE_GEMS__CONTRIBSYS__COM) -t $(DOCKER_DEST) . .PHONY: docker-login docker-login: diff --git a/bin/te-encode b/bin/te-encode new file mode 100644 index 00000000..0793a731 --- /dev/null +++ b/bin/te-encode @@ -0,0 +1,55 @@ +#!/bin/bash + +set -e + +function usage() { + echo "Encodes the provided files via a RubyEncoder instance running on the te-encode.travis-ci.com server" + echo + echo "This script assumes you have set up the following ENV vars:" + echo " - RUBYENCODER_PROJECT_ID" + echo " - RUBYENCODER_PROJECT_KEY" + + exit 1 +} + +echo +echo "Starting run for `basename "$0"` ..." +echo + +if [[ -z $RUBYENCODER_PROJECT_ID ]]; then + echo "ERROR: $RUBYENCODER_PROJECT_ID not set, exiting ..." + usage +fi + +if [[ -z $RUBYENCODER_PROJECT_KEY ]]; then + echo "ERROR: $RUBYENCODER_PROJECT_KEY not set, exiting ..." + usage +fi + +mkdir -p /root/.ssh +chmod 0700 /root/.ssh +echo "$SSH_KEY" > /root/.ssh/id_rsa +chmod 400 /root/.ssh/id_rsa + +shopt -s extglob globstar nullglob +paths=$(find /app/{script,lib}/**/*.rb -type f 2>/dev/null) +shopt -u extglob globstar nullglob + +ssh_target=root@te-encode.travis-ci.com +ssh_opts="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=error" +ssh_cmd=' + dir=$(mktemp -d) + trap "rm -r $dir" EXIT + cd $dir + tar xzm + rubyencoder -b- --ruby 2.5 --projid="'$RUBYENCODER_PROJECT_ID'" --projkey="'$RUBYENCODER_PROJECT_KEY'" $(find . -type f) 1>&2 + if [ $? -eq 0 ]; then tar cz *; else exit 1; fi +' + +echo "Encoding $(echo "$paths" | wc -w) files on ${ssh_target#*@}" +tar cz $paths | ssh $ssh_target $ssh_opts "$ssh_cmd" | tar xzm +rc=$? + +echo +echo "`basename "$0"` completed with rc=$rc" +exit $rc \ No newline at end of file diff --git a/rgloader/loader.rb b/rgloader/loader.rb new file mode 100644 index 00000000..6f99b135 --- /dev/null +++ b/rgloader/loader.rb @@ -0,0 +1,23 @@ +# RubyEncoder v1.0 loader +_v = RUBY_VERSION.scan(/^\d+\.\d+\.\d+/)[0].delete('.') +_v = '' if _v.to_i < 190 +_p = RUBY_PLATFORM.scan(/([A-Za-z0-9_]+)-([A-Za-z_]+)/)[0] +_d = File.expand_path(File.dirname(__FILE__)) +_f = [_d + '/rgloader' + _v + '.' + _p[1] + '.' + _p[0], + _d + '/rgloader' + _v + '.' + _p[1], + _d + '/rgloader' + _v[0..1] + '.' + _p[1] + '.' + _p[0], + _d + '/rgloader' + _v[0..1] + '.' + _p[1]] + +_fl = false +for x in _f do + begin + require x + _fl = true + break + rescue LoadError + end +end + +if not _fl then + raise LoadError, "The RubyEncoder loader is not installed. Please visit the http://www.rubyencoder.com/loaders/ RubyEncoder site to download the required loader for '"+_p[1]+"' and unpack it into '"+_d+"' directory to run this protected script." +end \ No newline at end of file diff --git a/rgloader/rgloader.linux.x86_64.so b/rgloader/rgloader.linux.x86_64.so new file mode 100644 index 0000000000000000000000000000000000000000..8de31447970179c2bc46881691b646c3f067c6d7 GIT binary patch literal 87165 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 Jb@Sor{|Dj1j5q)Q literal 0 HcmV?d00001 diff --git a/rgloader/rgloader25.linux.x86_64.so b/rgloader/rgloader25.linux.x86_64.so new file mode 100644 index 0000000000000000000000000000000000000000..f8a848150060169dfc54d2fa635e90bc77de0471 GIT binary patch literal 91060 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 literal 0 HcmV?d00001 diff --git a/rgloader/rgloader26.linux.x86_64.so b/rgloader/rgloader26.linux.x86_64.so new file mode 100644 index 0000000000000000000000000000000000000000..e3bd70a57853d782762b095c291bcc8d72a1c750 GIT binary patch literal 91054 zcmdRXdq7lW`u}iKG-}3MQCUtk7M2xg7NoW_I_SZ$sI;tX0TF~kAQ%$MY;3?dO|#Ku zH@C|!)$V4tTU4%Vb`iXQ+KudHT1i@YMl?xVB}@5zKJR%4U(Y(f-}k%!{Z1X;_xZfH z=Y8JidEU!8XS^>nds1vnj4g7-*{-sYYQM!q+%lo~@?&}2wscz`o7;Al?F^AOLFPG8 zFz$S_TqLyFoFW_1KDga7-F^N!0*{1FyC7{j1I;i}E*2!q70YtPvRou=Fq^YQ!bDj- z9wF}tZVg>BUnFE+UOC_SW-~tRbhBb>xPT1diiDAJ*P$Hw{GVPbpMy~-4)^41rrTVJ z6~p@ae|zmc;nv9y-1yzY>)i`iv(ELnKgFGj`+eM>;6Bmyi~_Qt5BGA3U4igU8E4Rh zdx@lr5Dt*>8xYpweo4}iU@;=E;=UR8ZSL0rbdj@U~?h4$$#a)U!7xx9YAHsbr z?%Qxr#QitiH{pI6_ld5LB{&aZvy2ZzI7G%zM|c(P-%Fao2;Adv=i@#P_rA0qs-j5`qK z;jY3Rz|Ct7?n`h#jQe)n8*%e`9(OwKr%B*?7IzZvM@itCjQj5*X0x4x@JievN&h#& z$tNfz-OzCI?)%tqJ`_ocWWkXR&Wk3?kH z7vTonLlu0Kj6W&E1cZ$;?vx>eM`ZjXglEcl3c|n2ID^ZTxEehT#CjD#d@AnW;EuyUV@lHmg-9*HiEM3iufj6bMgCyH(c=*B$}H^(3mH)ZPZ_jgYikdy34>`v1A z*dE-Sck%{@zi+eunbZN-+2ht_Wh{x>hr_L z!nLx#dvNoj&*DY>@#DT9H~m8$?!~xy)#Hxv`#Z#GE05ylc+>a)0XKDsm*M6`9}=|1|7ac0c}PY0^=j-LI|(d3`j_dGt|Kk(kdt{?y8 zf9IyVvX|HV{p`_;T2}6iLYLC%jCKr zK6GUMt*Os1n+~l#_w|wW*~?>|xufE{?1rC?-uTSQgzCCIogeQzrFQ3^^Xq?^{=~f3 zm;Z3n+-u!Ul8<&qy{N%B5mDio0^ThHuLO-7Jm;39lYRPNH?nznPe_qaytM9mB(hJLc6EZ*DQ#WVw^1%;`K5O5^C$m<^F5a{I*O z?}qNVk9HWpZ#v_%&)?kn=z_;yU*LQFYsb@p(T{%8)%U)*_d`>czjFCmqvM`>t^XGn zZvV0?z2(j=cg`3({;dg|CZb+(nEAVNj!&Kmg?Ahe392ma#Q5LgNKTZ0Sd{#410Q&T z`p=KjZgHOz=RYG#{;9Di=BFUx#CSsdiR*VnsowzaC+cq;oWzOn6~Ir_-<6QtiSW~+ z@VPijJu%?Nc>;M3kAlAvrJir2^!LLk_<<&r$e4EefCcQQ8ee8J83cz=`_R5e2^}3ZK_T!QY93e;;KWol){nj$)5bLH|$0 ze@2x2e~lu~o+x%y4|_b(IQ}k5J!7KCZF7|Veip^vPsKQ%i`#l7MXCRA6n)_Qbt3tU zj>7+KQTn?iir@Qpl=|!KOc_5&ygtd+#3adJPMyrMal1u!hdBH`ClD{ z|EHqJ`OYZ$Pl_TRTavm&pS}hiN>osihPDe>DND_$bXcumn2)>DpZK86nenxS)qqX zHup}G<~Z^43i%}28t7ngp}4rNM!N%Sr`i0Mo+D^kjjdSXjS^2mKY48t{YtVma-)Uo zOpFt+-6+U<(l0UV0o&qwNVeGRCMc7@FEm;C~J;(8x^F@INqNl%mbI@xZ+D<<%o#6Lkj18`E=e7VU#{Ufj2 zq@0`AnKbQ+*Fx08{Qfa!{)pUehP|`hM%k}?GrKrrA@2OG2}JhiH-aSIw9~|=Vf=VG zWV;P=1I={?uba?5<(zDhbD3;6Rc_25HnZD?OS$DVnLrZ8mDinUhy3TIAJ2bZ2#|P% z#CdkhYqZeMBwK3Or2hmyc+qzbw4H2QvR9D3fBu1XSx>!`PedOU%JItKp#rYmW?*wm z{+&{8PKlovk-wC~2eREC5mIhBv(1dlWd2>EU#Hm;E%K=W|CC$DD6_wur+Hl>?WO4( zldhNecM(08hoos{vhDZi$iNfqXCIOLH^>9o2SD?>SjxZs9h2S*n%7&hUtLlF5k0?G z=5Lnc7#a7uGQXqMEPt;V*y7PI%AtLi3DiowRrag7*aU6|&FhfFQ+WuD%R54bvi{U{ zCcW4UZ1>21bx3|nCB9$E$Fau*CP}&Ryh-qLn~6`B`BQ{_on~`b#$_P-%W-i^Jfd&k zNIhwg@&|k3N|5}E2LnLHN_>QrgEK7iN&ID5k7J1m6iWOQiFd6vfl(5FQP$r+#{?q$ z50|)GRxnrgcbW7jO;SH0E^+-?`iI#T`^uAi`a8{h4Kn{WIqr=-(84th<$0Yf^XC~R zE#1BC7MVXs=AU9F+uoMr($H!Gt3mU6SK{K340ObPC&_vo7JX}$^6^W#Mf$ZLILFH- z=Y>>JJI)2cr^EJRPoA`Q3EOr``Q%8u7%vmnOMP?fGJ%y6e_!fDw?$7TNIA6cHZu;A z`9GHRH_G|tI*IGZFYJBvIRf#^@j4>&=gIt$abJLR>aD-PtU#KF?Pb}o204zhdfSIm zJ`GYne?+~!JgA55x+VY8?b@D~{cYZC=9B7T+ZvI>_a@GL8m}AWcsXA%=|Z%}>si@u zo~q4$EC|MF1Fm3uTW(QFSqZ>`ntVp{OH1x9wB=5@EBE@sIVDxT!pe!I`BhbgRkqTSSp}C= zm0xnXt*E@Bu*_CeS(rcDR#Z@0UWEd43Vp@%O3I4LZPjI^C1rQmsw(r#kUX!l#8+r5 zDk`n6Dz;V5DJ{>RU0BK7xpVUiic880b4zC1DrbS6%G|QTdAS9M*ea`M&Cjht1BJ&I zDJUU%4ExB8X1`}cBxQxOZI(Pjz?8PH zytI5?VI_y&H@~7VLg$v27aT8j3{-CZ?Aev*Gt|Hwo4Jr5^UcC2%r^%vzarmORRL-G z#HbLj;GoYgEGXxgLvE&2it>H=r8clo3N50v^L@6tv&ZBXmNBDkR!P~Y;=-EadAO^v zvZSa4IVl%tOjcP5dM?#z)a7Kv>~HSe^4ZnYgovtHX=>nqWEU#Ix|I}EFe|_GxRk=o znu5X#OTGfy2g}VVz^Ixw6Ok8T=nuL!JGY`z2-X}gj9x);Y2jUkrM9_+a|`BH*s7`u z^2;E(Ic5f^e(tRNs=~`JL&qqWD%vITQXP*^vCV}L3-jmZ(kyc!IawmNpg14HYZk|_ zmd)wSX&NnCu9$DDsGe0?QjmK`;ruEw9DrsOL6=H=3{kvteuXc0R&`MkwWXl4VDzZo zTB%57<)$*wUUN}XVHuPj31zI@R2OPH^(Dd#QYjjYIDO7+G&Y+fE|JRexe;WJsTD9$ z8?6OHj*egeQ32~vu~AafdRc(!z~V84k|*}iR#Y~ZqVLs8X=5gz(6TBjfiNW~SXuR4 zU;Zr9uN37;mR=gRNj$WE+>jPW^Nc3b3@`qB{id@D=nElsw^MU1v=+0nlcSkt=vqY zcq1xMine-fLIrzsm@}R^ag6Ro#T3Mbz*lasdo5u{*lsB-1T%H>6<<_0c=6_dw;Tp{vV`IVI=aK_abopPU?C!uv{lXVwlVaAzbN`Y*k;cC%nLHXQ@ z(!!cZqm{xD!?Yp_J9|{s{JE^52<9nF0&0|7T$qp2Vlp=;j8V)YZAO?1NQTG^Z-c=p z#o(DOa{eeOD=4jo>tiYD-Z{F3voT+rM#vN~9M$k3MdVkGp#n-ZK)OqJ$p0KTu> zv|QmsUyQ<0y?!XYk7nN~HOfq*~K4nr^N~ol~z*mZ;RY^@1<`QAjV4woCKIVzs z8oCjB0Cdn7Sr}AM&A^zs8uL~9%CRV8VAdnd%U4h#LM#+8Xuk4%*b`!$?FAR+JVAac z3k&YTVy_HbkQ7`@o0yY(edY~0*Isi&X704CshQVKpJqdqB2f&hSXi)&RW@^lr2wXM zh0*4kS#k?!V`+f?%z_lf`qT!=&$dm@&YCbWchn`Ldt;+68Dp-FBY&}I?tPPnabB?k zF{ua^!6S4e6a{)~h~zTEV>L(WG!fR%@@gFs+Dwb2m}n%;Y|DK6>{yD49;?xeMe>^U z*>DK*|Koq`MGWMWfsp4b>AZC$hkx0d}_-+ zGkc09e`1%3ThBMsB_7%DMd~S*xMr!RQQ|okeCdy7{WC0h(|!}rv*0-gOuX2FcS*d$ zf;aqR=C85fZn@#Cx8Pk8Z?NDs2hDnxSa9u-i8oqsN4JSDwcs;;Ht{A4ZaZw^8!UK- z#M>=+(=TTJEf&1yR}=5B;CV+(+_2!zqbA;E!RJYR=(gbX5|5mNM8k{v>;BQO3+k&@B+?INd7>I~`K9zW)1@Dl!!-DUY zc(MimN#afm-Xrl83*LXQIWDOd{8WjjTk!KF?zZ5UN?fzx=@QSe;BJY}u;5oqJkNsP zDDh$oo-gqV3x21>Yb^KziTf@10}`*d;EzhY!Gixu;!7;}-zDB?!Bg{TO{tV;Ik#3Y{4re?zG_dNIb=Y*GW9pf8yHYKd1^@EavwW5M$z?ziA260f)5 zRT6Ko;0qNPMXU|Et8CEch!DUunVLmUy!T-zf183;wah4GX?i;$0Se zr^LH0c&Ef|@_AKc-SU&f6D|0!5_eeegk)2Hk}dct5_ekgQzf2a!OxO-ss%q^;^`Lr zQi;1Q_~jDUEcked=UDJeiO;a$*GfFkf?qet>~FCJzeVP+u;6(Tud(2BB<{E1Z068Vml5#Qhe$@9Cy})m!k0oExIxOQPV7QShZv z@TMsE$|!hq6nsMzygdrOB?{gV1vf1CKq%9^c zyuKI3xuJp&RPba4AFSX`1s|f|DGH9aVtTJs1yAlp@!hS0N8SNpvRlDlR`P2K{+NR2 zDEM*(pP}IVRxfhpDfq<^5Mi-`tB2qf3jUyyzed6R3hq~M`D&j?tyl0BO8y1~kG!kM z|L};P)!{QU!lV!J8EPdIevp;Hwq9S;6^TVdUDN;4ei$gzXCcx`J;}aQW(v zNbOMYN0t1Bf`6jmT?#H=F%qfW3hq<#Cyy}opYH=jo)a+Hso-mr{3!~~Z!aTPs)EZ` zg+x-if;U7mBXldcd?i?CYPy#Pj@W?w*OsG)s z$Qqh>je@rkke6SVQ@KgnNDtNkrn;e^oZUsMA$*(E+ zc?zDR;4TH9q2R+6JWs*TSMXv5zd*q&6g)-2YZQEhg8LQxLItl^@QW0@LBU5V_!0$Q zso;$Y{-lC0Rq#s`yh*_?Rq&Mxo~q!@3ZACm8x(w$g10O9Xa(P*;Fl?Qhk`dMxS`;W zD|nZJtMh5Mf?uxWw_S8h|F2N+L zx0=c63jVB;->u-|6kJpARSKS?;Num1hJs(E;CTxE8wD>`@TCe~q2O)>uTgN1g8LPG zf`Zp8_(TP7P;jq;FH!JJ1#eXFNeaGH!6z$tlY$2oe5HbG3f`>XSqi>E!PhHzyMnJ% z@GS~HMZr51{AvX^6g*qOyA*t?f_E$UH41JUc})MWRq#Xw&rxuPf?ucL$qN3af;$y_ znS!S%_y-D}s^IS_c)EiBRl(f~zF5IE1;0VTa}@jm1)rhd(-b^U!KW*Dv4Y>I;1vpf zkAl}I_)QA#SMV7MUa#OcD|mx~-=g446#P~NZ&dJ^3cggqZ&UCl1^=yruT=0{1#edH zJO$sN;Q0#PuHds2e2an?D0qj0->2Y)g3ng)E(I@C@NNY!QgGYF$Mk=Wf+s5YTMF(_ z@Q{KhD|nNFI~BZE!BZ5xSiw^j{22vLSMU-AcPseq3a%;m9SWYK;H3&aL&5J?@H_?o zlY$p3_*@0AQ1BK7uTk*l6x^@iWeQ%e;IArpgMz=J;7b&|T)`U^yh6d3D)^lW-lX7_ z3cgaos}#Ih!K)Q~gM#0s;Oz?jyn=60@OcW}q2M(NZYcPC1=j+55{(c0*=)w`{qbk| z)A*^Sz`De>J!AHN?zHs`{|SHX!`z6G7Md6C=)pC7H|ZEu6>b;w4$`rtn+3g zL2o7bu(tSw#1-*)NU(ywVevNb@={!L%BMruF;T%CfM;d*wh24UF zlJrTWQw9Af=_JxlK|e^ETfDGC(D#!*g|to3cayf0?m7x!cqM5E=?+2PPI>_8c0tb~ zJ&<&>pl>0~EnB!r(ASe5OuA9fQ%DaX-5}@*q)#R77xa~+Pa|C+=uxDTN#_arLekvI zg>wXbF6lE!y9Ip)=`%^E3VI;vp`@LH?oawG(hfn#lIE5xY!me1&p>ml74AC1{_iL4 zB;6tC-K6OZ!|j6JLHbVYn@A5Q-5}^Sq|Ybq7xXI97m%(H z^lPM3NaqQ98R-$Ea|Hby=?h7_1^p!Hi%6#m`ccv&NjnAoAnA)qI|O|{=}So41bsK@ zOG$VAD#o95D(MbE-%dJ>bi1Huk>=JV+$`u@NOQ{)ZW8qMq`6fIHwtnHiZr(t;XFZKNSa%UaE_qQCC#lw*e&QYNRK6*D(Hcv$B}jlxVbcLWtk>->h&J*;7q&by`a|C@Z>AOk01$_qTdq}4WdLZcqq@9B9Px@Zc z4nfC~UP#&|=))g_<`O#Gbx4dqX+P->LGLELh;+N4caXlHbhDthk`9n=67*)$wWJ#b zy@_-k=>|csA-$NiU(l;aKR~)d(65oMC!HtgWu$e|If8zU^n;|`f_{>8kaVh`A0_<| zX{VqcBpo8{5cK_|e@EIT=(|ZjOuFl!7=O|Yq&ozCJLyMAw+ng}=|@R73;Gt)kCAQ? z^!214C*3IMDWrc-xQeu8v`phuBjLOM^-7n1%X=^R0yOZrLDZqQuU zX~Bf??>lW;U9&H?r(G=T8Y0(p|M=pqPAvBH?>y6PsGg+-#^)ig&6lJF$M4(hv>B^l z1zPB=vF|!<*`eVh5wJd#$sVy&YxyNk3v|Y48C$BH_F?eqTA(>b(-Y1@x~FIGI%FM) zDAFsxYfU)%h0_*ut7qoMwMF(}Q~~_9E*jjm2@Qe)-xDDsEKI}muLd!|jl0liGs+Pp3Yt7IfuL_(!52Psr1q0Q<8@4aMfQDri|pkyT@J)HBDNm)PTU_A*=M5< zc^yUe;w?q?!cTzxgnM0)J%7KMr(}(pukhm{`_We5JBsWjTY*h;Ie{Q&`F99WOF7Wl zNX`2awXvpeaCf3sv{bkWrGV$}L0+^T$zK8v^1m?YqiktIk-h4RL;x)jBHknxdR>tk)Okvq0H(a67z*MCB}KzAaw>I_ZaVwM%Pu!7$HKSe2` z$;YUK!SOCpyw&i@;=Xm7{)5uE-p2OB(~)f27rSD1DzGO-3y$f;a!u2ZXmtmDqhUUm z!DO^xy5T#}%TD!JbRCR<+ftVk)upU0>K!i(?&olJ?iF#yor1GgSnm_4v-36C-af-% zLU7D{(NesNO%-eUH>+qb6nG80g0B0XSlZWDwlqdxQymKIhzlH!_0<6%Y_GisLo_bQ zUR%YX3Ups)uPtFL=m_i>7C4+>ue*h0XaPj7dtHgfjsM0FYQP%6-&nz4tnm>*O(%c* zutW*pjpU*R?L|82>g5&5RuKGvDFzn4EuL zs%>MYX?B92OsK^WB-$8vcH2pxfKHJrmEk*=`k)x{?t_w;1b*{nB~Ffni>F-u?X`;8V#_~JFRUbjuNd$*9O`AE)&5+HN0 z5@rWyWBl|r*^wa(??ip}VK(Dk4s>w*mkyP(d7!AyZ)!0$TWrJ4F((a83zL(?vXszuC1aM;V97T6RMc7qG! zB9p&yCVy0J)ObGm`zQJHOtY_O&Mw#mAHHZ$hm*tbxXaBjG?Y*#L*EZuXOzG8DW*OX}g=$^=J#{VgkjsYp-{Buk z-_-^fF*+1P;}K0oR0B7Oh_h5GL(mR{#-o&C-8SDO6rCFLt??)XlNERmv*mW?NkN`$ z{Vy&j^WI3kL~|ro#oAa)>uTY-l0+@d)mMPQj`dD7Gv+cZZZv%dtUj&T_!0vt#yyzu zBa?%U@HiSu#Qy{nmZ7JZUx%3X5inw^e`!m21RyU=^qVm0YoUv)fV3sN2!dBz!VA5z z=`wFy!nHC6J=B6@wYnZ(lJN^QsV$C$w7}X#Eu*t~C-ie9vND6i*eWt4oF>br^%yIK zDx2#8bZdmkw-%h@&_bScEm-07UuBy=0YnABM6KnUI4!Qfma(DoyoQyOisH`#I~=}F zEJD(?j26n_uCKLVj#CQ`tleC*6ZY@>KnvD5!U+(t^ly>*f&Se1isB8P^}{-FOINcV z{W9wJ_4L#=R}VJbs4^?ae(OIN2|x7oERFGfrF~>{ZY?T0-d~vW59&`b?ltTG-EsA& z99O^MrjOPC*S+A2E_NGS?3KdBek5J&M!Hzv+Hpy~)pV`CH+tP_^~;J&jq8HR{aL!p zFChtY{Cy9aVJDV1^q151e`}$Um)@Z1zkp#3-@YoRaT8Qk`c?ailTd$f@PqY(FuYj! zeKl_nrj*26Z^IOa@}8SkH)BLRH})>fv{tVx4gMGHoBr2cJ0b>Fgfb$he)R(tJFNX-t-b~)4bW`(@2xT%;N`e=H*i&Q@`OE8win1eApW62Hn6@y?B zu6i2NuC(UB`WPb%;sp=qUTI%(Lktr8B2f#DbHQ3JF_VTfDHch4u+PZWdyq2NOi5u% z98xwTMGInrLRvQl8x>x}w0NYgkZBIupP6D(+)9Iw$YzHG|LrpK5E(OZ&`xusbKZ+UCsc)RE)*4d~^@e80 z4e)~PBWP95qW5a6CXX&Fxw-6`(QskeVy&$^w3V|`N0p7vD4W$f(E;uz!d%8&$|@^a zl z2PE7pmG{9c%LLF=^+*f4)fp{HOoi!&-&^d{Sgc z_*~Z4Yka2v^xOnRxezm_Vu6W9YbPak*1XkDTXt}K7Xql}smKKbd;`XaFfLm^Y`nvP z367b}3~=b7G5q%i(l%!6`;F&C(#3!IkEGp3y+|7VNN>^+@WN6%jJXtS@ZxKFlX{RO zTik72%hb^LT434wQL&yh&NKwJkX3BX)(;x}O`u)^KNvsn5;-20!1sn>0#5;uYtkQ# zPlyE*LU1!^;V~p>!Bpey-9(4HV4_R0)oe@n1OD>T^jM<|vNh&2Q)tL*-0A8G0VKb1 z2GY}-(++C-=LfWxP~pF^fiRqAyIxdo`~)Qip2>#vgh?oCd}BgP26Q!{Q)pJfA$`sE zuS5zO{TXf#MbAY;dII;?#x6JqW4~GaJhON_QjJ^=V`%(P0j)K1#HfxB3r4rnm3&~z zDh?TpQD){3A7}Gxjq}XRZyn40teN>UoXr>sX66qNXXb+xfxZ>q;34K0{@vUudUDZD zxClkf`NDHkT63?eE!wkm2rGEYwS@u=j=ze#5v*gD0M5pO=y8#}+||uwEZ?3c9UAjT zG>at^DPD-OfQ+&XFXL4RB=~ocD`@6eJsWr+;d8VSQ}U_f0xaHMY12Y)iTXqFv2n7+ z;E;d31>wdTN7-d;sbJ?-i0W;ac#JI68=BG6(is;T`qinZQQt;?GmtXicGv65vDc1I z0jK&_sz96Zz;{T3E6PdR7U<~fOL#p|8?jc4o9W8&>br&G`r4QXiHfBgml4}ND0L1h z%nn|BH&_7!>@T~83nvt)U4-c}oFT$mm!Banf1#Sa62cWV6_eVyGGXE z{w=$?z?Ea1kHW?`Q1{g*p)fQ6)5Zi&8_4w}(!fO>_hZ3_1e@8xLI;vEM_~K#kPBxA zHm|sK7s(KFW?D}g_Zmi`u!LGy9bt&^e@$kD7C^}XPtPwlRVlB`T&z(jS z)4^jS!th@FK@I73o9%TsqX0%Y1>4HHCs?>{uMb7{8E0Z#J)wm$#(r8q`Co6Zy*7c> zV;*cXo&vMppwE?>S$EK07sBFQ3oLMHHhWz@;#2D&sZ@ffs!OQR0#~uV1{0$Zvl9Y* zTnLa-FJ{-@{~k*s)E4i}@VORX;o!ATLc|kHa(UX~U0~RzXS>{9_{mDBSBBT+w%5*J z{Si5$a0T~qSc@C0g;V50-CjEynT#@U08!2G=&f}d?X^9e7DMB^?nBaJ@ImOFy*A@) z*e(W^qRfWj36X`y>_8?Ba3f$-?s^-vA_)u!y5sG&j|~7h~3IuiGS!oqFx;R%C{D&VbI?pn!e^;TJ?1Mun|2Dg2mO5A`4g zMUa&S9$p||H}j>7e0P}nycnjGOb*f-lW$Cz#xM*YmaE1CFs1M^;6)8y$~u#ReuVij z`Wl<~Hv|&C4~Q2V_8FOV+w8T4ppZHByvJRBv740+r#nNL$ z{YXxQe-}!Cc!hBi%{VX?+ve>~n=KP;CX&s%t53liB=p-K=}OyJnQ7`!&#%wLKU)>@ zaqSLIyww;X&A_ecUki_!woUNMliLb#X2XU+g9D@j@*EvCJvqk;L1j$`AS8O*e0Vq3VH3EQ_*CAJ&$V2Koj+qhU* z3|-)Pj6oJ>A~ZhRahxl>!d%Q74$gZ1YLo>R!Pz4pAm6!!Slcm!iy z!l|FSiycqpnNs@Mp|@N)1CZMO4W^KzaN}48VVFm((F!)V)(&Qx%{UK+h9w({2K&RU z#fKL496fsU&`0NN81BdA3+pW{VaJPpKic%RXrU>wn%(=64U6Edao!P!Llb(mB_8{V zq(1(iXWHxDIE{^CxTk~`q{bMZVZhQ3rZuw@XP{Go?jG1^y1jNAm|Sf`PmBW)foXQW zX_;vUF(xU*;fWx)^i{Ta4)SGJ7;i!bFxv41>~;S{Z)m^X5nbbZ5TEzqN3&fi=p5Qk zF;<&;0#mO37etf@5qa7&BRifSAr5o=(DjJLkCPVvIEuns)ek^N-~ci)#8_y(yxa*- z8%rB0fR=?%>GcQfSrZ`GT{FcQFNUf+(O$b-*p0*gy%YWl%oq*_W(_Z52L(Z8mwJ&V>+xYt73LhM{LXMT;W z7_tt&5aqCx!s0}iFH|t0;dNxh-V_VwP$cQY zp*Gv11sG#7j0a)di`FtOCbXNN@2knB@s*jM&baP2pbg>6z$omqEn!wOR%z^)4vz-s zS&KOPb;wA;*dZjlMiOL+($o={?(M+>gOXYsPklReg%8xM-apM(od%^5b| zg><$vj1KURWs4tbApmqe4huSC%NH1Sb1HRsf~RGLu8sHVI8=K`n64Jmx;>#u-QXcz z->kv*npr`{Ci^6~tl)3-HFRBp-7&B$np?~6xX|EF6A=sD-B&Q4a*WRulg+VYQ*Xif z(KlG$H(`0dL5qV*!tgiuj_@L|1wF+9<#Mx0c0voZVquB(oarEpY}DP`Cr+oxRBW8e zqTrV@2n7$=Oe?1$4Bv5ndAl$4|L;ZT13*O*a|9I1hlAVG(to*98IV6v>uk@uhUw?DZCeRAd zd$*A;V>!kpj6ti;N4OdbdTP}a%)ZL`TJZIyVsx2R7pIKn7^foU3EXSLrlJ6i8LhAyWovu^CNVa3 zEwbUk!S68+tU=?*q#E--HHG>StpY>QueYhyy>{|X8Ot$V60tG9mz=iM&|r?h1}>9= zZ}QOxj|4{=FMJLlRDnlH`;2>N%@hl^r7QUuDfqg`2-~(X30Z2G9sJ0Br~Zi~z4~G9 zId7&5K(;ir?15j1_XJ0}gzU`?rzfM$YoFZBF*u4EGuFfG(Txc#cAQ3a3CBW>`d-Fz zj4u&0o&W=@p)8O%PZGL0-=PNEf7I}DZw-Gz?AYewdyZPD9Gi=M#_cS{4r6X>!?Pv1 zwZJ({oACkD(9;}aHfz#<5Naj#KT--}s*ySXhQLvqu>^UugTwD!=EVG~>6wXHfpIP@ z5p+9-`VJ&!>5~$T``HNxihyz$-%_UJqSmz%7&eixa^nXf>4yY_)|t9NH1eUQ$FzAc zfXK8FtYifj^6{z>`j~9)Gp37hyYUYw`P8~^?X};+y?KIDg^SyQxUp7LJ;TWfZwsp7 zLX$fKY?Sg4^x)Eh6Wwgm4^|LQXS{}S{hoBKZK9j4b7M7w8wy-T4_YM`hlGZ<8T&p% zC86=tEAY4mZWRUB8@EzhLgo%d(|2d4ZOr5<_Zu#Waw${@@B#A1XJ?W2&^F?N4;?&# zWs1SrCmiluLOjMAn!%!V<_V&qJOB~TmAv}9UYvB`893fIAX}#8ySXpMy5d1NC+?dX z&^^poKVoKbC4n(a#iCfY{4KkvDY;s8&^^_n^_e$t>NDRk?Xe* zo-egUV9cAqxE6otSMc@$ z1{TZtnJ%p}xgl7m&Nnbsdtf2dHZtGb%Mmu`9*oy8G^WfqUmMpT84hceoG#Mk?3K}0 zJ(+^f<=7u*;wMW3a~p9omSg-ZMt`62Bf{0Orr3sHDiz}xd~5;cJ)W@bht&lGiEsnl zdhCHtYjE02k>7^bvd{|7%xaIoz`RhGCZL%-wt8Ajx{qyvxHX<~EP%+p!7&?`JCTk# zs1$TW5$r2sDIOeve3h;LlBItRd0~-Td;iHe9~fz`y_?}!U*S~!Xs2C{Lv6EfD6kor ztz)n~?lW#=^`6@1YAv!~5sTv<<5N=w?RAIx(=4!H?lJC+U^{`aEZbVp4FlE>bv}Sb zxE}Dd_OY?<&K0H}ME187Z!;>uQ-;091(00$vtPOX_Ui9Tz29f}F*>HFlUlz8k1K?x z^Nih8{dqqsT5nu~0TYK>|Dp2VAEr)jH?qmJ@Q@P_!xOoPq8DJw7jH0|e42UAMxND! z%pv7^A-oDlSLV24S;X;~#N`(jPjEFHGb|Jtj2@JBunx~k^)2?=+Yds+%@OS!&=AZ? zOgnsDP7-!`I3D!%g=jK9s0?R(V6XEaBUuc8gzz}6dxHK6OuLPcjO7>s;j=LRAzY1R zBaQXXn192E|E;c9>?0 zEteX;5{HbDe&PYDxpB`9%DsKi+}mr|#CM@((7Y@ zp}#NE_8IFD8vb^U0P=EDHCk@IZ-8fE`_{J*c4y+lokoVIMu$NpD zaSy%{tzeT_Wz(MXG)x4kI5pw3?+X3XP6yiM>M|?PmYA9GebvaU;GYdT2vqbCD#}=2 z-NF8(ip4TLcB+;^mavX)6RTxKrS$mcq04<0`210KJ6>+P9QQuAaTgU$?GDrlkq0mvDalwuW=)!db}v&*=-?; z!&1$2j4sBY00zRn6 zS2y6U3|xdWPz{W(77#3j?=uJ1^zmji+aJ)6_VUdg9Uda}$;|lBe*dk&!yn=g-bW0L zf3=AHF_qh1o6QL#^tk!(Dm3OLBtp%xG6_wI&kjvV$_`CPG(Ll=h^HpwncJNE>V{w{ z<5FoU*x)j)0;1uCD1E(*J|v^YbMGQMPr+_x_oZ^Jz^M_B07M;FnH}N4g~sH83l8qf zXg51l6Nl{YT#5>(+Pz0Pm8~$VK+bgJ^bLT|$sTb~!{H$-YGse`yaFw=XIyn=V*(<7 ze8@}Y-r$A`HEDXh%hq}2@oQMA>Q-nIbBx5nS4sSHuYfnYpT*>h` z?0TSh;Of!)*cbl}#-#e(&o~Td`4ri5im{G};9-@Fj^DlqakftO?6w*@K| zUVwGJ|L05XwO8@5r(g{Zd;44(P6MI8-T|u!=lozc^*joj4cy$@z_pCQKqn)_Iq7PQ z5BpiniZ`GNxozGPSzn6vCf47`j`^etJrHZwV$fz!xpK8H=5{2~Tgu1a_@fzTFAJXu zjo`lQJ;Y=m`A_j03pO6t$FIb!zTHR=O;(4@VGjQRreV&Dls4DR-=ZFM-Ms$@_ludw z*3IytU092S`=KcMLB5(SW!^y@!E#XDPctl)aZ5#+Zo? zu7ozT=n+xWi;g|N0UhJg$(+CA(XntY7&0C_KA{tHY4~rb4T?Nx77F&KGvpL-BM-2S zd6Il2{--B-2({6Z)U7#)e*W(lJLY=LUK>F1-b1z8YEa=+{An=PG~tmjIaoAJ9YfNV zkolr{xGxTw^n}YnhQHY>I`MKoI+00d%T9#3fBFah9P|IT=OKg3%)o6DPIK zL5=xPn+Fr&H*n&E4N>?x_)BaNIFoQQXTUZWnVa|xC}pqv9Du&LH6Cvg%K5{e?yT;I z%|Z+!KD<6@&MKmJ&gxS-2QtZ|e0^#+7d2G>z}X7A!pVoP0B-CN!qoH+jns&W;LU*$ z&!8|-ia82B`~+Qx_^}){MzBLD$72o_4*dMJy><}RGn^@tkT{ha&yl8I2{Viw!otsl zS3}ugA~cNj@M|FC+4=u#6M^^N9gr;c_H;LOEo2WdvFUBZ3dQ69&3X3P_aP>nZE(*Q zx-(YG*l%C_9^9yZeyr11uIXK!1LXKaTw)*Ln@I_28EfY471Q|mwYOom=2_f7s*KpR*~(qd z_|yC2y@_MD-!)bpX}S`|GB?!cen~#A^7w7?~6GHa18C4h_=<0H5~^Cf^DV+gv6N8I+h7)Tq3`Qf{;n@MXfYA~MS+Kpa2 z{L=RjN9o6cE#Wf0{1tH+j+mvS;ncMOT;)VBvEbkm^h`J#t_fcOPJ?PZiYayKN02!Pr5vLY3oMIG%NfvhnR4MkX{a z-B|oK#8=R+#oXY)5ic2!d@rC4=dTR#)jaehQ41{#!?~1)W|ZHz26<4!lc# zkE8QB%2^9a>Jhx{t;oxB6W>P7(%<(?%hErYjxXaNtpj}X8=lj5hZejOAC<(9)-pVa zmAB#i0Efhwst@-1NhF?~m64fPc}Z4WHTsNK;J5N@L5umy@Y9XxUn9YhRnY24p zo;dV!zXpZ_XM_ZDpOvvLOTX7K?{j`T(c{5;v!0kXvlIB1Bl4~bt^Ov?cJRcQ+`C3#{2Y&nl_G^#o4=wr=8MGKiK3u8*$fMpL&#^w8WCfWHveBDF?WqfaxV~#`~AR0JMI}KH#7MFr_G&b2D8ne zcp#SWZSGQ&su;*B-9AD2*C9Wa9BAXc-q;4lSj6Ani+$1?%OR(r+WgZHbBfsC@t0Sd zzYBlOAtUG1$~ON`#6Y$Af9;LMgPtg`TKqLr_w<(KmdC7xW8oC}I2@*2+=!Vq^W{-f z&f^d}7p2<#UShc0{912nc5jUTk~;o}05mgG11SNF6u3VTfvDY`WXg1~*+K&{ne8t} z%)=sneYMS|I6OkQp7x>L{o1p3ABmTAjYfB??7yV$Tt{&NwxWh;;-4pL&QWrdSo6?wE2@| zKKXM54eo<&_#h8|WAwu-vBj`er{KH1rg(n*e6|+o8Qe6`cdFaplhiaAKWPx?xvc3_ zUmxsQ10BN-@cS$6P#d<=J=OS#t|xIN+x6{&Uu+swz12+DPvztL-uYjAlow1G^N`aP z#DlZy1UhJZBR1rhpmuD^(PezQ0v@iQ+jyFxcvmS$>DJ z>z#PiZG4W3v5}7Voep<)HyCXbdcf_N_%z;&ZNmz$3mdm-u2{U2>T22Hzyp|m?F8@x z6YG-uZD`q%+^-$;(J_CFMUe3N*aiOE_!hjSZN@2FoDW`px`W${n09S!X5ze|THJK_ z@k|G2;@#CVdUYPOE|INFR`{s}rb zq)K-}XxnjGg^y>A!Z!etT5p`Oic#waa>wS&eZ~yr(ARfv73(z7iqr=P(Jp?(0bftr zZALR&3UtT#PDU>Z(4N^Vs)Ai=hM0fb$?q09-*@nPd#nI(GUDUM_X|W%im3w~U^38s z=3Qgoq{Ybfi1|wlZVo&i-0_JR=Kq{*yf!r>o2MOUs95=a7%!c^n2Y(_d}SQ_zMdUo zU-jl?sIv1_=pTgegE53drmyX6#0t^#4x2M(2@&lxrU?1q&2}D3hM+NcmE=$L6efNL zV0{)oJT;kLnuc8U;7(tUuBJLeEnkSFYHIGy>1o`o_SFD0>@F`hfS67D=Op@ z-8SD1nQ7lbf3XkUAd2$KN;F{XM&(QUR-a{l>gyXK*4E-BQipNMa#V!vyaVq=pi%6z zJovp0y z_3tL|ixpTwq;I=WKl;Wm%ihEGN%ZZH;_xhCFiK|f_4d(=(s`;@eF=17*J2#9rO$(_ zy=xdghV?A^z6bH)_&vJ&w3z1XeqGoOMEn-|Uk@h2gN~Z}SI~(jt7z>5F-%~3ov}6& zU2m*JRBy%)8Q_c7{hIy4=KWdr3qRd&cbt65Z*8{y_M2Pw+h?p{Xxs+QYq$B5n0O8n zi|jYy2Ub4wVyhxN370Os@IVo-AMqg<&Ai1Jhws7yMqHrdsJ$*swB_CLT43j|THxK- za546as=cI;HQ2^TXck%B^0E*$#dR{0JcriVoKVRYd)+0dilm#zFybUJurU<^Ypug{ z;9~{b@pZP*S182Va1Zyorhhk{msqF3un#rcGwxtx7_8HTAQ#q9*E}$NXi2S#(m6{uEQtrv6VQNTs6Q_)!qb@(8L^jNw9v zSnRpcZbV^L2nh&dc&b|}%yjgf#&8RC7-yq&RrXg9J=E8hhbLE|az}Oi)L?lsUIX!s zqiqPg6?-5&Pevn4`*P1zJxCe`zGKIxv(-3^Bw9gMU_GB6&wF1Q!7oNC_zk}aW5hgd z3=%ByYwRxqPTQ*fLQ0T!s-geG?D!+d&UdKiLszv$=k;ut@&Fv@?t_7~KX4F9I^KYc z%MMjJT6Wm=&Hne?+VGzHe(llv;ich0s6Muq%5R=9e~diLgE2qwH3HJI{G+h&I0Dp4s09^X4bB4Wl_ z@2lW28We4CNDSmESj6w13^Wc1pVwyGirP_##?Xjp_@ZO=bTO%5OE_8OchN~Smhk<; z9k{VE+~^E%J!S{^DD~M#(GH)`d=FNQUqm}=!yo@_j`Nu>vAwn7wUX{MRgvwxO+IhF zpL}j7tKjn&X;+V#`SIFv^_$w8FdSa~hVVTmuD6M$5MI4=7~NatzQeYK19{?Jw z6+Pnm3oKwheiOP0gPiR$XJq{PjWQ#PHBb%KK*rVREarIpxIhvP5VbZOg<`pjACEFT zLh@UlCZ2*AUqJhC;GMRS??|`3gVQuH zy4HB@?-W_Wt>hclcc1VUkg*fsHKH>UQ#`@U#86I*N1vXkPfRsl;n)R-H2Ar679$de zxyFW~SuH;#F1oS@Z_e3kpG8OU)x$*icQobE_XWPQdlv0NH|`4@!DqKGW3cTHT#dhh zBQe#d!mjv31_y+oCnid}K!2}8YhJy3co%Bn4_DP@ih{dQeW2S>Jwz5(kZvsSuDLg-a5-LQ0x$H%BlB&kH7aCS; zXx+G6xUBWU*JBl~??c<-OAy%E9m&QOV8P@@gU2*d1PS+%nP@D4^FXawO&fPcqJxb| zXhS%d%w*%r-ni4aM8qi%6izX=quJhiQ;l|$L{d5>)*_h7#8NHcorO+YAU)Q;_){pl z7Mz=o&u{U4GYIx*%To3z(-Hocm`MHTkqu7^>`;Ta@b$u=ge2ne{gl0SrxX&_8$vGk ziz@F(6u&OQZwZ~bm2+|trm!BYOh=-gea72}@E?%Ql zf@21ORV{?~{oxP9`fg#OaT-*SQ`$Xfn#(PuLH_;Fa6_Ow6%U)xP@vmwuRB|g(fLqm z9pf<3Asqr#DbAM>ID((X;`g5Hbu98=E3qD5oU+=siClHs!q3a(wJ(Tac7!(xKw{o^>q$E4}$0%-{Ya^51hT*+~mSPsFRe31{5PF^ab@ zPJ?%aeT=l%ttVp~sj~rTc=|q^wuI$SVxap%-|%o>)ek!s?J!e!Ck75*Xs_EX+}C<( z1owYUsGvx^Z_T2823~$ab-fEU{nK9fUkK&k+|O9`8P0r5HnZk-Waig&3T>G!+D{-HVxVtz5y(X zj9ScIu7iG|5Q9So-y;R41>TQI+o8mr;fO;j_uV+%4qxdt^rjezbgx z$k1cNA`Gub6R`IpB%52<@W*Jc_w=)&A#_vE8!s(ewoGq7^vy@RL-D_dQLDfA)Uq!& z6o22b-{-^mzyA3umVm28KM(Ej+z1~!9c2e!@0sS&Kb@XA;s};dadn$-z{hZ?Ejx5_ z(KLu!KSKYo;x=0*rijeYj95>g8HW$+<0j!L`)T;&8s^d0>R$wQ#x;jyTXx3dVf~L@ z{Uc9chkder#bs&D;doEWj@U`{mz|BIFTDCUo_^b!!+pH?G-GPPHc#N&-%PZx7u@i<-?JiuEY ze-+Lg>IYwCUoq}CLr4o_T8ch`?QHI&1K>yu9PU|yRy6hFn}I}Pv8kJt^lpoKLt zp1|Ii*7#G#SO2IyA~5GE$SPLf20qcjVRxlv>dpEW6YMKS_JvTu%|?jr8+*$)v3h6V zyWcc-_OY+n*6-c07kx9!Q>^y1y~y3NGnNIyDNJkNJRjKc>kZkVgq9o|PoSWkZ}6}0 zUY;+6ZpLiBagzO=B)k2cp~EHzlj<{rgO})QJ!mXb{}=;;LFtUo?DvJYKI!bx&9Tgu z8BAINX=dv0B3mC%VCOjGiN%mN64eD#0^PaqtF*D1XECjYq8#{0T%jT{V09roOm9lBNn_XXEudS1n;I`VTVm~!5t zHY03CV9PG>xthiIT~qY~SuH#JPYHR4X@L$q9B8aIq9yQ6oW1t%*q&iad~IlGvewek zUklCbtKm6p?|GCbcoB}ivI1-4^^3gvr}){2_z~;zOKH2j`o|;Q_2}EMep|=RP6~~C zhOOeQdLNz^9?&;xEjuwOx=zQFyORc{ZM!x!vJvFNX&Wi$10H+1_hcJ>@~g;Wzb_u| zYqvPO8QV~AOt*K$5pTu<*E*jwG%_Wt<$J7o;J7xBk5JMn8HOh~v|gY2uvWc0fUd>_ zN?mW^Dc8vu!|c$-SLX{YZpMy{?Lg`=C@U71P=okW(l&ZCHZSZ01Ia4bgqIMuLR(Mq z2uHZEuQ!BW^eR}JnYIm?pm}{(LcX?zeLbNGu(+)mYclb;6w2hWUxQ9}Btq-F{aRYS zN$mG7lr!mU^tj&_p3&JC+w`PMVe0YtSw_uXHxI07p>lV2DA$`6()mCqEA)sB9ylws z$d-gaKTIErSCxN|>$}lcc!TBfnQ7}i`cL`+Ps<)hOJ}nEU)#XIx08adXT5Q=|E%Fj zqyxc}K@T=^b63G$}5=(VN!l!P$uZ6Kegxm4g(&XSvby94rPF|5wfZH%*B$qss@6 zo|pW;Z4SK~`rkA;dY<#Y=^dJL{Ws11H*Jlcmm-=PT|V-EKX$4AO^fK=W&cg@M$b$C zZ<_mWnhNHQGmCY4)7EANaZK_hOoTSzN$b+TrWLgO;DE^-3|{u?ggD;|8uWSR=*yN( zh`X#2SvqM|6nCn9DNia7BL3=QFB!c&Xxv2;?1amJ# zur8RJj-VM`LGYGVfG5V8+?$rV8sSs>XNR(fX_5W2H|=0%n&H8(9USoDgm|f^W#2JR z(K9JH?hU+Gpn3{#8mxER|6%V5a$uc;EYkRan`61r~dzTojM4FzVGI}`@Z%4 z*ZR()*>!g9s@k<{*RHt^EJfcXO8IoBwX_=x|KP%q8Ntb5Ja&MbOoFYu!41Z*Bq*Xs z)rW+4h$@HG5^gRYb2GyNRu1)!!cgoKt~-V+sxniubp|Fn1y zfbqPFg#TUf(ge}Ue5RXIj*k8dsxa(l-;o_943Pl8Ky<7J?~8nt2wU)6SG4w zF)F>nip-4M4+AO1tXFKtWe$B$1wEQ4)T4Qhv+ZEhZq8c_pDOi^_Yx^a1~XpL^r9#3 zM-{q%*r@^|XAy?!I}Ag7VVKkgU!_0mbj($=*9B^HIABQ7zo*Ica_qEHqwgWd?Q~fA z;{6U9;XP66&-xqO7Ata^$uBDmBhVxMO@G1P&}xPLB4sbX!~tV%iAgf+4*p40-RmiB z-sRu3&c9=AbJ6Azx-R--IA^HRpU!{ifOS^IteZq_b5D_9Y7HA|b4;Osn*R`@D#e)l z4@l}ED&bL@m_l?o9ljR*6)($#1!_h9UF+_hm%{R#Qrf)ovh1AQeJJejC(iPF`U?vE z8T5R3JQM*%M4?$wggJ(wIT~vUCjUczU!lLGzlJpJ!Ij-FNHX^*b?=py{+rb(J7xF! z>+EvxD=q7<%}J^+zo3+#ROn0f#I!vwmbd%mY^4xiq3?B%8j+#)2=^0Geu4f8!fugY zR?3fIoIuRI%)ievNz9?Eh<@u5FH3Y>8E>tA>OR!L%d%Cr{2HZ$z8caI`*2zo0NN+G}TA4{fs5;;TNnTV76D&?=_ zm;b$Tr!yfeI~V2G%x0*1N-O{_pC08@PKHVUOsIn0KSsr)NU$hb$|~nA2UJT`B&E&k z{D&|uH(8~kF$$>4ls3mv`G^oBRR`MP)`r(&n?F&16*gxSHl-?$^D2EkySb06+Mdex zCM*Q=M9=eKs4UQdcuupE1%lDR4aN%ugYy*`9CVY6)I9>ET}PNW^UrG?g3VAED9rvVu2 z;bQ<+CMq~;R@GSuG=;aI+CO)1O92&Vp!HXgLkcL^B{p;u>OCs8%BDE~zO{l`sv;$h zX6TQrY)~&@Tu_tpZ#m>Y61yAR3fN$DQi>3_QPYoS9>g489i`1Jj6#eaFkVXZRv#!~ z$}oh&+$fzwsMOiQoSQrk%l|ELmu+j_OJa^N+Mudz?;tENn=wm@i}W zofy4NptrNjJ%fEe`N!JQ(G*;d*uYM;pW;w^l%*B_QEh(D?w26hoU{5MnJz8I*9qqi z?zdv|^Ep8>rNPrAJ<2?l`aGrH*GV@Rhe>eTWZF$-j<1zEC&(TiDuWsmt9h7(wZb%F zkd;b*6f;%zd}VrH8BJ2zAf(k{U-oSyoBSR2u*pd7{wiVK6h=^QTWSB2B5W5?@Q)&? z)rJJ8zG`fe!V&?N0_dw1!X-X1?=%?3toI{0W#j~OB52&P5W{DsQ(qPn2oQ>{cXy{1 zGq?O=d$zOb3nB`2>RS)TEOM-zP$UlbU1AsRLK8xfB8)MaNUWS-{QA;rgtdM@rUX5+ z-5TnF1ii<8yR3O27ccFmFnxL2Snb`ms{uUOmr(8+oq*p(9Ag=X=$Z)0yCGh8q>;HF z)3%t>$2y2RSaH*Rqd(N~l>X^Ly7%og?(+hU9VGw3wvzRU+|CK(b3cHA)8-WBC33KMKr$TMyMlV@jv zbrzPSXlV@M9yD}tx3Q${X`Wb)MW3M?@jHSTv*>&wGJ=x9ed>Y<&V zzEF|zXG6QJZ!A#acDbc=v`c9i{aBIt+R|?BPlTn9bJ0PQve;b$wEnDro&UHNri(Lg)lrvS2{42@;Pi|G z(dFN=#7cu-$IAu{&JX1F&U|g>XNa>1z)hoyYSR83%|@f zdxgiVl-nwOvHpg#Bfh4Ruk^STXb7z((RB#G)Nkfv%slsXzh66cA&8}DQJZ7nCiU|u zS7o;dz~_ldAr^mwvbD6-AJV1%%p6mi8)Ry|8%ymH)$F5bt%UkTIh}PFJs1i3!bA7a zps_>l{v;nM29XY#COfqu&Qg($=6o3PJPcyS;!!qctg)11mo*&a@Uj5e4~7Gw8q;2} zEV_SLU!HG7_OVuR3)#PdFVA3c-;Z6YB+eI6{;?1R<}>#T`L`7EpPzCY3tZ@Gbu3bP z+z2!@sTXMQZy?dNX96%n09KOwwW{SmPt3o;7dtQWAOkB)we+A}E)I(rR495oySZ<$ z;A@wwl44~Ihv>>~_!*YFo3Je?zt}>(gzh;vs9>x5pp5NurwK)zJ5tClmP;U|Vehb@ zVre%g0}+wXm^~CFMoDBXz^Wfpvwe_j=~B=^*t6j#g?(+O!W>{$Gz#YBen@Qu98lVZ zYMcek>W3F}F0r5k)wyd$yLcu;kbe68e)=<*l3&Q>MN1%pyx_=(y5ReB3jLpoj;|FS zZy>C?frk3lx{f3(sv?81oN^zGdO|%b>r0U+ugr%yS$dGOP6 z?t*ds>&#ni)4HM(?QM%!&)KFl2U-Uq{V5>*fe({Vtk33I zc;-E{$iG{g2F23HSPX{fQQh24rQcwhp+Y`>22beELA$wmR7`CpC`eFAa3dkCR8j0g z=sK<|gBi%SW(Dj~nD&5Odn?Wt7)IFRYkYQdb6E}~hS4z9hcQLj0(RmY?i;eptwSb4 znT@4S!9+YLeaC1skO>LOZ_+f%Yn`+nU|z8khtiSRhgIqe&#yD^x<^XGpgPFZHDX68 zJ)U_Nt1!-}g!y;tVWqv}tIGMO%tHn&|L_X%*suG0$Msjne~;fFw~L@vbS%VNt_S8=q&n;7XR#KUdCzL(Xqetn1%CQ`KmFNK`kEZ= zy}eiZ2?FTL{q&`N`pc!MD7S^;d8Yw6bLo~twEYdRcWh+o`FX^KC&p2~<^gX!fOmDy z?jz*hL2e@2%!o1|VF}Jb3H1o4+prN(5sGgy4)dY!CkiLr4rfvH^l-a{Ei4iPT}Q18 zrF{#dutBsD+Bx68^(NoKvXWo4)og!hF@0PygF-2D*ljno^X2Fq)J}FW7gXZ$K>h4L zjKtN?V5YAr?PnqWoJ=7*y587uk+YlffClOyMP)4i2iiM!sUy#kT94bEK#*TzX}khY zyi-QSo}19l;(iF0@5g#}^Qf^A-t_ER(bBGHnnnEugU(?S$@q zLPm%)mB$Oa*~NIr=3?XmtCE)fmRp&qOJ`GX?53{GX}Kqp{Rwzxvb;$_P`X{L=Omad zEMO*`$0%LSJWzhX$K?pSci26pZ0F(o^;q&(i~egVWE4Wa_Xi7fwGNRD1{vTWLz79_ zvqR{jAMR=FRmF$eX3?j06`u~3d@QT@(7iumm{L{(VmfsHkLYt6K0JB41Sm6;Sd+5v zc8m5Iq$`mN;&)XhiHhE&q$G5oY3UqR-B6!ECqooO=O?7+rMLQnV~st5^3p5+u*%FN zDrsqe817dr@=Fxk>l(kv!oCxF;mTwD6bOW;!sYtIK46Zm9Js)gDWD1)Ua6dMOfoGS zvJ|QlUd1Rhz*dXm8WOIw-DDwk^)N!mcm1$=kv2OPi{JN4Hzt3INNINt?u`5uoXOAb zL)#-^cib6QgFS4|DzM95LrkOd*A0+4+?j)R7hzA7Lo9h&NLV+Jwrk@Y520Ko)6lPT zGv+|KF*}oO3Eh?F?W|34=xf;y3eON64aa{6n@SV-DK za8|~r(03{s+IK^H#aglP2pBA)#IZ*ZV=R0>3gQLs{3jrOsPlvzTKzlaQ%w#y%S&>& zZy^+tj0(w_3W){=?v=)SNGcvl;jr;I2)gaK=Q zdE0zr3x%P+B=hEgj`?iMh5B4EcFxh}WUDub8XYBp+~^PHaO9hKf|>UuUB^QXs);&0#$vW2nFeps=;eO>9V~&Y+c82nerFYa`^a zBRX<4qBt(VA^dMl_zO_RHRv$~HU+*J9Fv-3mev}Sn3n~ri2|Ep1m_2!7hElv_eFRE zL7*k`x~RkeeJ$b`(;B!o7!nBaoWolyg$;qWjF4Q8Lf$sQ*^c-Mbt0#X!w-o!hDvM? zbO+=`WZsEo;Ugj-=C{DRBeLF!c<(~i;*Pxy)=3hb+5eC8?>I1r3uOd;3Y$mp>@0w; z@P&)`-iGi#a(KDYE*nN@#9KFo4T0{AkX${$3XaJq5Wi;@Ehk%fDJq&IQZ zk75I73dV@LAW>qm4~vyt{VO=@ivR<_6DE~&x+-j5;yz1aZ%-L!n;%K9DIMRb97~io zFI9$Zb;5r+Y&2}_k-lc!Z}}&iE2Ven@XcdTI^I<}KEetaEwZmv7~Fd1pK9W3*w*h# zvdl>e8qC3$DatM=93QJ3@2CuG>OVTQH(zD**4NM(ayn_JkiI}_D$MYgT~s)hDI6cE zY~HD)&y|=6S)<55QCB5>;DwYo+f4^yi3+z`Q>u>@jxW6o+iW`>n(K=IO6d!MEd}F? zk=#|WxY+8yQu-2nNgw$dHuh`m(`~In|B~^Am`cE!uVGCq>6bolS<)~5E4DUPVykFL zMtn84h#d%HO9fBhv;XEYWlnY83>%yNG!oktiG6{+3bvTIO5o#b(Dgv~Ai-&yBL5VG z^%AUh-uK%my-E*p2$F@+9zsO5+BIA_UXW~OWC4N$1ttz5+4Bm==PJiCmCYOCY=@5_ zro9B2Xil7cF>%RT$XKbz#;Utz7a&@tBUUj{Kzo|a_nz`S$|?V36JFwvNcJUAl0~f4 zU!uUVU1Yk`9xx|J*U@^LUqv>QP~SYt3z%i4Zxz+P%}em8w1fu*T%<(r0VJ41Jz1CO z!UGhW!tnv|Y6&kQT8rHE^W5s02cBDkS5gAXO_@^q7WvCR`GKPBHy?wpy@Uj1*yb{L z#F!3Bti?J$gnp_F8(XhGoLh=KAt0$;*>4a={>ertA^jtzV+mwhf3$tqi%?^dDTy33 z;5+|ReWt@I#}}vs%|erlD582B!|BzI#|g*q(FZ1Fpog zN>&v?TcEpKN!BnW0V8*)D2}{7cbCcwI!r8b1qRbTs8@(^i@+5$p}s=;2>I$!a`L7l z?pqe}(BxvBUIoYlEtl~;;keb%@deVi_40Uyn)Q~{XB${T=D?du{sN*_$RGNEZE%g6 ze6kgC)pOR%iE|-!=|gNFBL3P%{&NBa#YP}tVHqERji&NPNWW=)_dBRCkFYlh+V2Lw zRPqNXi(1Ni`_|;2vL|87pR+0k4V1&0oUnm(mGV1?9wLQNLz;px*PXsT2o+CG9Jh%W zwELwJgH^Bwiu^-m#OWO{))cBtJV+mbQ|u!; zWsjVKgU+>DXX7&>43xOG zRR!*Okwf{^Tbg zQ_SV?)Qhm4Ms6&-sFY$I$Vg(&KZ;}vrI!>oM-=iRq|+y8=Gr6@7V6CY{9_HJ$80Xj zPf4HXPvsw}hwK&F9Fku&xOK%CWhyzE^5krvWIX&sva`8h+YHt_M-efGCb`ZGez1&BEx zo0tvTI(>F7>%M+}*;%aIu(+i`);7naM<{`2h@6rcY9-ZRXY>Jkab^Se#W)^%|X%eydRlM`U2~5Y-YwFM211gZ`q+v$vBLY z|JX8H=`>Man0a>q7P8?^(HCMh8jjQ)#Hi1QONW7Dw!c#v`-~VIlgNR&?p|C;Yr8BuV?|No zgD}F{6GdsfFo{Km5!sEM^$=LUKU;+_|5#^-Nip+ibjZJFpRKaY4x`NwO~})VdDeRG zStmQJHas23)0%nOy!Wh^9aa~ft;n-J^K8I8vsHG8S)P|2W{WWv#ywP^m6c(kVsAQ7 zkXh+rng?1)xx&O~X*&#ug!#ZatSHrnRP43FcwL?2i)A}HOO)e_?9gGd9AD<;35Sd@ zYB-$9fn$`u-N=DKy{|hvql&K%7?jDfjJQWQ2e!PzpI*QCg zo(OAtC=*8NLAO zV62A-gzq|LKLJI>kbiF*dW*(~6Yx=QgWEDp9O>Ph)X^`E9?%_(QAv*0`x;@iFVCY- zUwdK1O2hy42$y4pj|(;ssTWfvV-!BQwDx$yDFhiUnY zTjN*YkU}LP6wD23P9?~S6jt7M_%?`E%XqFvp1@J+3a4oi6ig*!!=~!VX&*mS5 zYt97g<_*1&hJ_f9q_AAzfUV98t9;o1f{7BH8{<>>Ub_y*=`mFZnPANU3oRDLMH4ZN zQl#u?B+bJeW)HANql~pUBvvQ77Gg|6>_S-1Cv>40jHf#$Ak}nfuRT;jy{qfE)TG1) zbsMPnB_-;#iMlSKemi|tp|Y;=iLq*Y`nAXkR5?w(aqqR?jwG2nG&H!YT&>5qyvW0_ z&1ogUy)NAv)Kw>?>eRpXmPIB`X;9Z&y?aWs7B9)k>YWMk(UH3Nq(q9)p3=vBiVTn* z+@I&KpU0x{$Lz^kicY`LX$yn+=m~?%osKX#eF7Qu@F>q_A0%;SO6nLBe@uunba zuunanbG$U`W(FRe5j&E0&?yxxS$;+a6i#_uZ5Uypwo{&yU<2t*`tYN=<1^zc z=oBU@56$eZHq2wp(xt+b7){zxN?7L8-#uhyB{8;1U>CBI=Mm%x_wda+Kw_p9a!lta zL7THjhy%xnekZAs6pZ1hE2OuDYQwNP``R&K+%QId01SL*YBb9VtkFWgP5UyX@I-cGi zvk%D|Ym>$atTIMD5d#VcG3}(wz1W3D^d)chp$OxWL^Kw&ogCm@Isv+jCc9#P$ou(V zG}ppZFii^Mv^(Y<IRJ!u30_ z`|K(keC~^#nujTfL#d$bVdi;yG-2$()0=|Z${!o2f$IIA&n=FX*s8-67;W2dALU#?f4n8nX*G zgs*RVAU=g27C5K!Wqu?92f8;v1~E8846{iLh*I3YL0^-{b%9tf;~gzwdKldYiLnGx z;&uXkL0VhkzMyn6U$R7+=Pa3YJ<4EIANMxGFM+=kWbHUr{_0 za9W++Xj4u13)MNEAeQ8~8~c;!aVYm9$`~wx0!=0^>!8~!O4tfSECw&uXz8pdFBXzm zn!KNHpl8VOkse zFE8+UXk6k%Q_)I8^nf{6^hLYQ1w`2!rb(4{ez*7>bN-v!hR&7&(YJAEwBtcG(~RxJ z=ZXi~C~wo=sj_?cCqGKmuWEP1_i^{fpEa_IhP%m;CR@B!#pe+&AUe+RY(uC2qBQi6M25A3zZr6Gd-||X^|f_ zo`o$()v#tGk}BmV*6)+=>$i%&mB%Z%AI*4PG@xnr15ht@q=r1(s7LsXXw;8V(1Fzr?9WU4;qQ<=q?dM*GM)8@q$6Ju^Sk| z_i_KF9U*>TDCz#O9jqVFTh^XwHPd-mzV}M6-o?5QF7gJ@_+e;F6#A2D*5zoQD#L2s zA}4DgsUZ@Y>UkW_W(^bWV25c6z=bB{>!&ZLX6|l){uG*jYMD~em7(qdUEp39#^;vN z5duKFp3@L#ajguWvOd9C$HMlkOHiucuQCMSlJ#N?NY-U!U^OfldS|CZ+d9|+mS1hB zJSIUBzpFDZpg1uU{e)s3*=uf%@Uj z;6^7;2Ddq>8N>(T08u|k5S8{!qQ7=0gehfCtwMJh_Q#Z)a4n#rbpc+;zGmLHHo<~n z_X+IPQ&QNGM-iTa7=ut?OofpXGf!9DgygO+DP!YfB$Gdur~<<={E~#IhJ>qzXaW@h zDhc)TH0q#`P)Yk#N&9ZD!z4p|{gWbNv?&!1O#6Fj2kgwxLaVTqTBGPs>vgKed31?4UY?)s_2ei zj}g9s@gsbPB#an0meB_d^>Ymh4M-1|;FlgaH6VS&1XcP7rDE!cP=!1oR53ZocR1nX zfy%**zQ+9c@lvTV8$Ohcn&l807oRFiPD#>fqjlOCS!#4je6mgkTw;<=#!Ds!9|Vx4 zq$SFd5@kA~hf0&)%92uK@fE}%NQ_S$D~l&0t!%P3N|vfkf%eF>iId_}k`gCiB;_I- zf_PXYVp9K_JUBrcnW{y3B4=s2qGDcXr? zu(vT(eUnmP_r*FdR;j-ji1@@wkqPlJE;8@`s&2x1pjDGaR!O>A_3SzN@09oCNWnny zMnDz|d%)W%8G1}G4Wv93<(;DKB2bdW`k$MUUP2Z68~PaseuV}N^7@Ze5|4kuM{sqM z4za{(sX?dGj!H=yuT6B3g~CuXJs<|7wAw`3grpdBJ+xrc58VtjkIcv>rNoboN1HE8 zOB4x&aQ}`xdGS-DP!SI$T2%w&-GtnjsrNPI=l{}-h%HTx`cYFpqT{sD<5SZnh;8@3 zEEQ_?ka6DZ@oJk|t#Vbb#hryftc!!zdkV^079E+EiuxXfPEaD~S!J$PL;u%Rfa)Xf zWH5cIrV>J1RihkhZU-vFLCOY=uN-%EnfzxAmz-KE;i-{N737-6=6Pj8e_B?>7O-qo zt%0IVn;fr;lWE@v7wsE2ZYrUt?jjTINlen@L`nk1Nmg8_ym2~Raw@vh)U8f|NfaVa zafwcv&{g0kwW~~)fG&9|x_hj{pIWUDsHtO5jrprdpShj+TWbA(*w~N>uQ5MLEku({ zgEfqKgbFExD0OM6GK^;auQHhKykKg~hac2qLNXK&U13r4xCT)!U_p|rw-6O=Kot#) zlqG5>qt!5{e!MlSQTwPCR@E)pzon3s$6#{a%9$yznzK^H4hTadbU@JZCt{dG1zW9` zQ&Yi;Qm7dFBuqhf2~8yBs>V2Bg(pSECqzQzRqK!|Yujopr~iWg)U@Q}q!bvq%bGpeyPy$$l)$@h; z#o>1Z%Y$2J5fgr%SpF=>ijO^L$N<#Hf7Nj(X1G#i%ve;T=eDm4`9t?;a2Y9!PKZne zxklsT=daReXqF%$U6Pn2R69vpBDze%uv80^1`(v{QnZm1;KB2U<|~l~>N>2dNhZ`h zqL;*1t_fU=8H-I<;`^ugMXWNJ$Zh{EWU|_1$^)c#XSg%)i(4bS6OPe+hcNhw?+f8x ziQg9d%JGZibfQ1OWc(;mOsrOc63LT(jG6cUGTuA?|Lf{sjr9R|r_Uhtz^^BMz3_9# z&jY{S`1QfBFMid90n}d}T6UoodzuW#3?VJ{NodbL9VfHK537Bc=9Eb4;a49&Itpxy zA05xM$B(|0LQ8q{MOC_TyeWRo@cRHi2mG4jXYOeMkCyng!ml-cv=g8$elq+V@uT(1 z5Ak!tuRVSp@cRfqXZ$+i*9ku?Pf4)EC+UKp3w~HdmUP2Uj-M-j-SMmGQFwVPRX)CI zzk!4N0|JAB2M-Ae4GSMSZ1{-CsOT7N?AW;YapMyvBqk+KOi9(HO`4oOWokFMYj?LE zJ$t!(d`!vw%jf%g@-rK^-&k@bAXa}pwH%= zgT}Ws{(9gGN9WiRHbs%2t#f=Gv2nWEHRHkP(W7TS{wZxvsl|sKu4cE?-k;T9|KY)n zi_iU$G{Mfp>v`s<%6cwVve|0}J~=v)?K6pbyy~l~S`%G;d{G`vWC7oW+Y^{B|YLaxbNB`l+VzX_%KW(1%Xx_GO zl2irX&7QGuOirJow4~xxS^k`Flr22JAD15cGb?a$_-kVe<0!; z&;2V#jfr*|*L-u*>$vS93H|T??9n*s!H_;}2fvxn`O9w0l&V=LZ1#@XY*lz`-`j!R zuh((5i`_cNrTNgc$MfuTM^qk~dS|b^KDTpk-juMqt`RK`NmgB)lG54pUHfmwo><-X zb$RJ`^UK@0Ub)dMt#8xuk#CQ*3GTXKNUcq;w)b<5nET;d^)z|Nr#B@1cMl!4V&%M~ zV=m!yTU?3%;Ks|fUKa{XGo25)j@_6V7Bp|mM zw+Y+d3>$asWhmL=+_*v6S4LUh$qjPir z+5eH!&mn2?Qr|+C7}bf z|MVJp@%Do87YCFrt+V{>su$gik4xOU1mury|0GLx&A-F_DAj<|`NxxdveYvpFj?Po2f8~_2WlH#F{pLMwCx3n2-ujD6jegA8>N4Q;)wU^( z!ki|KcPY8nB6#k}+21($4x9SHWozA@ufN$dZ^od~-Vdfec-4B&l~`|^=(7be2Yek` zJ+{8o)U{V1?`zF=?RWAHSk-9o0c|G>$)ezEw|h5UQGR1$SfJeg;^d99mNjpvc^cls zYW(5Vzx8ow*w^Mer{NDv7Cd|P`sr7lf8Dhrv2W8h$*NmfKP8pkt2@RvYipGytL6w>$2ch9@jZSzB5 z*|UBhx9(}z!J~P^l>N=EfB0}nZllI!iSIVuk8N9U?@QI5TbW0ueSfCcCqFrx=1Cu4 zb1nF8M8du~j>EJTOE$#WeRt|npD(*!*)-#Er`cT-M>^*^HcgJK(=pBW+5`Kh-$Zs> zd!q5;y^;6ZymPZW9c{UO;i%N@9^WSW*ITjTLX+$3kMGGk@#u%nQ4t$PZCIW-`;VVD zx5pCLNbL+NFeJ_-*keBUn%xhjRblK9Cnz=KVmVLQ)M!%@9 zvwM|K?sDrZ%@+?F&T16btYdZ$+ly^W4zId#eeu#=!_vQ-{bFczO1qY(g3M)$efGDw zUw7-t&~7dvx7C*xILr4;c>L`CuzRLMZjle2-=+4Q{^(MZfunEz5@T#K+TN?UaerCv z?~hdXHmqOWG$`RpVbJkcUeCP_Z{I!caqB)o+FFh$6)UE+DLNc5Cu^MVpxePwv-&+- z6?NLDgJI;#%ti?I6rpKod%nLOab&!41pSIqpv@brP^;*~z_5{CD>K0QMB==cxM=6EK$PChj| zqt5p$g3FtS6+Jfg`07&R`XA!g?XO+`;F}FrBd#8+m*Fy1xnj1Ps-V``3vZ9EXk&Zs zO6rx9*73I$TWTpvE?>hExIg!T+UO$M{8MvZmMoZZXMa{PR6=l5fvXbh17_?zyb{FXw;p-W(|jc6sU1 z#z*D!l`hD!iD!1sudUyF8L;`Y?TgP{d!DT7x-#~G&7NQ9e6`oIaKgqVCUx@twJtmb*}q*LT+hPldEx(c=c~<@@(TC8CHGt?YjX% z^ES;j)-Im6J@?}Csngq>TK_=P>)g`S>8HFpjrZ)a;+<^goIfsY_|4-{PGF-xWv`C5 z4tJO^NFKYo!_Qycx~F@U*xS{6c0MJ>${?j;hw6GzEtn(-8&>~ zt|3$Q%ewLJ27S4>aL?GaVZ**|x+=Qkx)a^6+*}$yu+#T{wwDiRHS_tH>GF;XEsW)F zle>OoHLB_Ej9Z&&s@FFzcA z^Yi}Qqv|>JX}5FVBinC(p0l*Uu9A86Q?5IgecMHq-l*{HqF=s@vYrw1BlZPgk2HCqB3H*k13UUHt2Bro}FwaphFK)1R$eJ*MfU?5>S> zJ8c>{_}3|mrrp2TXVO*|k6u%TH$3sx?;SF_$X%ZQu*oZ~&&T5qo$ya<)at2AdM~d= zFzaHdZKfcAQhsPQg?QP%hXu-|jhd1+5W;Je9H}OIJOP%g*xDYsY zbBtzf*5&NdU-!K$xE(VzW6wXc^2(l+PfL#oh_H3=D;FEyXdAD-xwJan7vv#<;E{I6Ray}7a8 z&ca%s?p8;wZMb5cE-knDC%@QSU01xyrb}#hAJ+pGZATdA?R#R=e&y1E_U{r~x9+#I z#f>%J)w6q+wJQE;oO9;oU#`BZ?J;!Osoz%(`Q&7fbWFqGSDCecII!Jfe=hap=)lg zU;ozV)9BCVzp@=$EQ#>!b;_gn=@+lg^_skYL(47)N30mTWt3O1)j^&geR^)+vOP=x zIltkBrm1$n&TQ4?;d)|cxBl}3d< zKKHom;mc)1%Hor(1`MzJYI4(cw=<+!Tjp*mv03)To&J4Byj@uO)o*SqF8%VP^WdD1 zEqZUfuz1GzGo%;$pFZQ#NZE%0rRhRbB`MK&t9MXDf?po!m0O8P3!+U=9J@L z*L^{gXWq(P>(j4eL43${bw)|!7<=mNB;{ax9wSb>R!t3*)gwvY!qwo zP3rq=^zCJ5SB&l&+H&&#ACsd4Um6V`HRd3R#7l=9ItLN;3;_w+pR`?kPwK4;x# zZ*D4ew%T&Db?+r#Zu@3OZS~5;Q=j%3ko4&WS&u=jTS&`4O8Mxgh^CQO2Yhk&<0-v& z?`^MgyXld=a?q4hgMT^d+V$wJwku~IX;u94%;7x+4W*|KuQ#spo4c^Z*L#D`AHRON z(EW1V3!Mr)EXVfz?9g=ipN621e=7cE_=>XLyI**lnqPRfTy^QA&h5VXwbPrDrW4dG4vCjqS-Y#zm^%5n^~cq^r42r7@3qGAxNYKt;yzcFG~2gw ziGNIKx~uD#ZNItk`MKhc-BzA!^K7DCp{v_+p4;j{vWfc+7P(F;FnrVFhD+C_%Y2jS zm9&0(%P#b?|DDf@F1I;7CF)et%>E~1e{yJ6t99qM2hT2;y|mrymqY*f@WqFdT)&t1 zSo?*=$rIsGvz~0+em`x*S3R30o=82L{dv^l>xF(hXSG}y?B68((8Rrm50%tyGS#(q z;Z4=o)1DQ)4OxFwVz)Nx_g~xZetC9pk6Q7KCbqM%`g!{4f5VV-w>+Cg z?A_r0WRq|E8efFO0Z?EE@o8%gOn;wh<8x1N^p8%gV7+t>E|QBut0xRNQmI} z#+Ik|E?2=hRILgWU6^PToMC~Xe*S{fFH|)|aQOticW43>A-+7E*ASJ0ry8gl7Ag=A zQ7MB&SLl%8g40Xo>&F8PQH6Mi@zmh<3Q}?!RbWWi5bmJ}R0;vblq;p`|DFM5HK)cq zYzSx7_q~_jdnaa!ym|J#!{4KWf;IFa=V=OWTuj_g^PVNaTO+atg38e<{CNUF!PxV~ z-9GP~;qRS;-#bIXf<DbLev%V4jaOyJls#^&qe4T zuHZ6JR=9jCTz(ZU@1Q_z;bWQw+3+BNp=!+#6;~-Qh4-NMZvnJ%PM~K_E-&WpF5vzB zc!VLLA`2C+VHGard#5V=J$8u7cbE`OP@uP{7ejn`wiR$`6k#GoaHs|tfqc-=_wHb9 zORVsqFy0Ez61@cDjtn78NU*=?!RivW;E9z7lZ#xaaEYn=L#c$2D2obrSRhCQv^Uh0 zQv?Jl!^HX(QVH)JzL*<9z)6f;p0$92=tY=g9?%wKjB!Sh;)@ z{$j0H4H@DW=)mFcyWEpKoAT z00?N=0{~kz6##;oA%&oTV1LyxVDQl??B`VY!%5i=)v!Dx9Lq3b93J2mM0g*+VM2^T zxrA!`0#pG(6b$AQz)$3QL{Na2A7Nl-V9=DBFu6b@IQ<|jhGX@PyS;tF0=>CA)C=_- z6t3?>xoUVQSIzrSuB4bkpziNUyNjgVE0K1GtjJ-Q~>sN15OV#nPLD>0L}(X2ts(ktAkCZtAL%S znM_Tv_|O-Soq+;W;yqzH!U4VkTn_kP2GRknKNINy`U6^F!D1tz6W|`ezJQ%S0l$EK z0d;^^KQ)==(K{{=S_9}0xCd|^;CaA}fDZsq0OGKWyh$lN;b3z)--efC+#J za}XckJis-8ZvYPf_QmZ?R{>8L5Dykm`Yu9#07n5T0TTd65q>e`5BLUfBVc{3n_UH* z{vG6iMVEy2pa)zII12D8ppNhxkS^e!jo=@!?;yA^b^(q)<1}FfTNBeJuL1#I0|~eQO8iefO}3r-ULq~9l+_QKo3~|w8``a z@C0BhEC^i%bOW4z27Cf82h;&}K8x^xdjR(U(%KL`(y2ov(sX-i%ldV!7h>_LGjO#2 zvtO^+iptRh@qLavue$=GFZ-sKhgRVy~1Sj1(XEa%2Zhv znNT_QkbeSx#qgg-m_S?Ez-s>UfcAv`cS8E`?=~>gGK-R?hxpuxU$D2yj?Rm8f zR`V?vFcDF_CqQrSXEHUWaKmha2a*VQKFR+9`42Q-KJ^ikC;XRymiU4Yz>!c2#24v7 z9{k5~Ir&xR%d7=*#_4Z#p6pzXQCKHvD_z->@ z@YcYidR06u|HK#YjVS-%AL&4-Sr#Zp3*!F)`D6WmJ^=CI#rW_Rgb49^&#MK^_Rgve zA$w=mQQG1}2!$hiI}G9O@o+-E|3x@YL0`!()4l>)hZP^ww=O z;!#eNf5OiLK2O9~iig^xBfx7Y4rn9+$~S!-&>rnhBq0JQ)7%rU6d$!;5%8yV@5^&#IeS-=vp@X1p`Byv`gr5^)&=OvIE?i2^CJLd@5>CDksm6BwhW7eHBqKhAzYV-Q@T|UZ55m8p_<_$7i2*5pO(4J`-~+h4 zV1LmVpxsm2CaW@QWm(O$G_XnuTGB%$=*zL6W(M4XKKa4;3i`y#AO6Eu zDB=sJc10ipn)so9%agSx(?GaIyFmOsB;<(}RERNv*70^c0$fi$OSN^lyUS^AFam*iS#v?)uv1Nhf92 zVd0h|oabJXDVvuA*>h^|d|6;9ub>C8(?YV89?9!G!Y$cnG9?nCs=U1CS^2PV)OR6o zl7|%xS`p|&sZgu58-;br-wFN>`6kl~xCMCzWY#iRKpS}Z!yh+8nszY%tXi41!)&{$ z=35vn=UGws82B&QkN!3Jht7k%Gp&4Wsh1^$pACQO0-S*-#8AOsX-m6hgz{Je|F!UE z3^S}?FQ}in5_ne5{st@93ov$WWC=E|*m@H2L!l}j6MsZR z^>-1^go78_J4H`@0Z7V&1qbbSqG!oN0SelSc7+Fg@y?V)KF-hudPLb7X)_ZZR2B9- zAzjA9f#fK6pexND+ABp*MJ_rzvUgEF*|zMJ)n9USv{pXH?|DXK*obdYp{{JsYAg+T@{?()Jz&dT% z_s5|Rhv6K?ayW^@EDjfQ_&tZY93JBEB8Md$zT~j>5P^ec9CqNaCx<>9hI1Ip;Uo^T zI9$x(_Z;SOc!2uhD!=4=aa2U=ZeZicbNgQTzxR}H5In3qo5Qi5zEaC7ahqc3m_?mIp zfy160`fwP|VJwG}ILzX3F^AuCn9JcI4li<8!r@B}YlrjnIqbk;PY!)J4CgSG!$}-w zak!Yn?>Wrn@DPU=IV|DuC5N?#^7J|Ez+q1geK-u~FqXqf9AWrn@DPU=IV|DuC5N?#^Yl6Fz+q1g zeK-u~FqXqf9AWrn@DPVWDwY1b2=-5GzpBHc#{#YOlS*OWbm#AJ9IoVW?Nfnn%QFE6{~^F994>q= zynDV7p!oj@e@}lYVAN#-v@aLnus=EFD-NrHVvgVPTJT7JBfyBa0+hWI;AEbkYz}?d z&UVa0aTw3xCmepop%D77|1z-TiLKjg40hwdDDayW>?a1J9moXFvH4(D?C zIfpAa+{ocB4v%wqg~L(~|KQNXVO?AoMNd->WgLFYVJ{B7I1J=)1cxykCUZEA!#Nx- z;BYC2t2z9U!yOzR9A4(Igu|yCzTwdN1A*Tr9LhNC%wbOsJvki2VK|2o9L95) z%Hd27b2wbW;ddNv=5Q~EM>#Z8eI)0&|78wuad@A@=N!J_u$F_6FIx_qaoC!}4jgvn zuqTK8I8<>M#Nl9Gk4EtK2oA?`n8aZ^hqE{o!&QfR?*AEwOE_FrE!_HQ??3VPJsck7 z@C=97IIN!Df9m}%kN+8muQ{yKT#)yFDjjq2G~(&B=CA{Yt{nE~a3F`~;`zIHUvKZ; zGH0bWDjo-x-CW#UdUSE`C^&GCCUwdLU1SuXE=7QGq6>%3v@T;4(_Es`;uB)J;IbX| zhU1QL5|@}Mi3lk`U5el%9ui|@{6z!56m3Ez1>mma1f9esJ~1Bub=q|NkHri8l5oRK zq{KxVr-@C8oS@aj#UQNsCUJ?@C8eYyK>>`5h9}~ToDd(4)RPbm{<*NYg?OS;L5;(l zbbX4ss`O(PV5JX z?=A@Y5@T{Pyf_ab!Xw;6q!+`B>AS;0a}Z*9ah^hi<&6bOkw-DS1CPKHkmfAJ@Z!9O z2*r60PA=iu6JI52xM0L+i}M&F?8Z|f{wXSve~y;K zz(H|~;l+6q5sLFBV)|luG5<98S2?^m4_Tf`M6z#d6;{v{D|T0IdUdo#6Ng=QT};+o=t>{_`H-zFB2Uc5_`nI7(SHSNfAyLJve>!^jB63U&tn_B@z+R z+Ii(iEI$#q9u7)hoVWAj^ERFx2>VxgRS*9YXex&f=HY|?&G0*_g^%FjBY61g=~aK< zi}0j-BLCvNQydR3+A(onQw%Ta!J%sDmw)644CZM+67p{?0EVM~Vt5fA2d;AdC4Amf z(oqQbq*{TA;Y4`8dU!r=l(-1tBg6?&N*s@j{eS=OGMp5bnE!|#j?6QnhY-G-=)r@? zI26GRIEYfTGa_CLPxIQ9A0bvD4B;E%S1k!fKAop;WN+{PiQz>Iy;oCl=Czi-&`A$B zAjC%({6x8lc7@DCP3f-<6T;W7R)MRhkKkq>OGrl4pUOm*;8<+ms^Tpx=FzL-tt4W5 zR25$fS0h)(2yRPgwAg-B_O(PyEw&3)@zxU2-dDxjRLrYa#n-J^AE=72ClT##ReXJk zXiux+8&s?-RK?p$MEh42ZzmD$T2;J#g}thZZzvJ%QB`~+iD+l4;u}ju`%x9&L?YUS zs`#d;=Z=+8%xy_0z{Pr8$=8xjg^TsG3f@5?*1Jk*=w94NoE6V8xEf&<&i<}|qV|oR-|!=R1UHDg7*--7`&&~yG>t~|-Bt*M z3phPZzp;3(3kbGW3}+oE5aENj2za*V08TsLYl@$`2{qx#Y&R03sPf;>41GAGhnD_R zAeZq_S~Ky`XEtg|cP@`7;wvEn8duQsr5XC~IDN)_fo`DyB%6S5h(;r?rz68G!aF_m zZTOnfJpz19`MqL>{*D>^Gc$OTnfM!_(W@!l_Q2P~rz@kcFDd5wkRW7B($7ph!+@7@ zn#y3T8T>Re@zAb0(yJ#0LM&prECIeIJ~x@c?`QG+wY{M6T|?Ma8MmuqyMEqGJWp9X zs9#(Tu+;3Sg-$?C{4_CxcLbjDl@Tq3AJ60QV0ivg2@EoWA7cie417)e%rJwWYX-l> zO#HORt)}?tR`8ne$IalcviR#u7A_YuD-O3F0#EfQ+1-(Oh~@H@(?@Uv#`Y?K!Vdl1 zn$m4y2JZws$=_2eq|5ej!a2YUeITP3XeWWx0afMy4m0te1fKLWgO7J0F7_0gp)UiTl}m|0CoYp&V(?g#JR1T}<>g*YuR55a z?*V*G@*l|X^(A><3h7dRg`S}-eg{c5=U>#@XimR{m%BLpo5b;wB|>-+PkY&FO7|;H zU${@8XL|$TTnBtj`Q66pS5n6b58E3ISiteYT+atGU(7FxigT{%f<)@cBTQ&NZ@PYXM&k{W|^U%4?MH0-32*h zNQHD(nu%wd8T@fG`0HlyFBl&7?U0Zj+p`L%B@~$1nFj*ihx1d9<7Gbxc(!*LP6v+9 zJ1gLMwP$-NDPJpj`8uQBrN@oad-8G->y#hI7v2@OYJ;3>aPq6NZGPQQWC^Os6s7pGsiSwQ~G{f?TUzrg4#MZleR z&CpBh3jBNWd~M{a4+Fj?In(^BSVoni zk$hr?UT+4!#0>s>Gx!~5@JE2JiJ!Y>=%1OPuU)U^{MrG}?8X@(w|qKU(u(8rxZP%Z z#zE4V#q-p(khd42jO2`7e0vX7X7D3;{29C*L37WZu`K@g;YHU(Gx%vdp2D#LrKl%M zI9^gH;KhE+cO1Wk>kr!-4U#P!FDn)3MSga1d|rWo|4;yueBf)+pA%yIHwD6MZcnb8 zp?@I8&ke~bPXC(Wd194-U45av^5O&}JF^65TaNEGUcj?6NpN-np4#__YVCU;MlaA- z{vQB*P5Bzb@PD-@S{4t+1-$*z@W@g)ekC8L$T|KK;K^P+sb9suFR~#?l2zJ&DezhCa zTu+;u!G8>VP3iVBgCArDKZ3&|7jpSmoaKs6(cykfe2FYtk}_7fnHJ6o!tJtf2{-@7CPa=+ z#aj$+gpSd|5rq$t^mHRfLek{e_|!N!qf?^0y8A#h0kn;s1n~9`rww{Ng6#1CvQ`q$Wj=2Z)JJjZB>&_kc4cN)roK zbsF5pj8E`raL0DCgf3IYRn^J(B9K<1i%w2T)+Q2VD!w6vD|0n;p@xL96{FQ8Ab*q^ zd?-Vbs93~<%?qH9pA?xwfyj-EMCp@6kV_Q4@e`Yt$SzE#+jT+U>FMk5=jE+&b#Zm+ zLDWf6<0xQSG9;-P%cH?n#FEs+k#fR&j5ddGFpcjaILJ~zUiBF2w zB~%TT5QCeBtD;hHS$f6ChC~r!>jlh(cv~@&A>^T zA=9~NJu(%Xa(xv_SenRa{A!T5jbfEP}HDyY& zP7?(aAV@eq#+8aw5VoK(+%7Qos(!%>3=rQb+ObTXkWO-ZHL-|RMT3uJq(M^=BsMWxp!Os!VG5C<5+_2(KP zvn)%{RhCOAXmnhHc9NFWqe!S3zPW~PuceG98-+?o0->)W2%3zjaD#d@Y#oXRo4157 znsF&pm7;X~C*fsM+rAw-2T}e9svSzb_gqqUj z{M8a5E1W)|lF3TdC84c>(xQY(+o%9F$T&6Qu98siVBEhF5{w&uRGLuSf)W%KT4DaE z0j#ip2%Z32K@C@wMi)msibk(O*d!(m3=_4+tT93x5}iDy8jF=J1*>ZmBek30dJ+>` zT(yFLe|)NTqJ}>1QMrNRHJ6gB-qcPKEp^qzp$FAXG8%74Mwb*>l`>%}t8qg2gmoNH zA1XvcHJN1-VJAnTQ^2ZrC2Jz;4r_D&zqZbIM?n|};ye>3_YU{z9-+YsO7xI`Tzvfd z?Q9Dc{YLm>+uhljY0|g^(Etcx_fLnCr724W|K1fBURPZQHRoBF1^58_V0g&VSi$8TSj8EI~Z>B%}5Nwm1m`+PDeBq)syU zGAG)o3{h}*I&^^NxQ%pBbi^J9GjH*CZ*Y!bY6zdgjwuc1O7}jtVh*RO`{1;g^zas# zjFS1>GMZF96cNq?sRX6Uo|o=y1~eqdPa_%UJWX`gXe` z2aeaEhByUdRUhV)zl@h=Qi(bB5O8{5xW!u;Dst~3f1yHXj`RJtJlY_U%h~XkL}U

V3 z&izm;!(2I{`-vO_7F~7Sjp56;L~_l#f!$dQ8}?Ir#J)3LF; literal 0 HcmV?d00001 From a98610a5a71ea995d121dafee34f23664a6ce053 Mon Sep 17 00:00:00 2001 From: Tahsin Hasan <51903216+Tahsin-travis-ci@users.noreply.github.com> Date: Mon, 20 Apr 2020 19:18:06 +0600 Subject: [PATCH 42/81] Revert "ruby encoder added (#13)" (#15) This reverts commit 28a5cb4dbeba53915d7634d9587b9a793d6bac01. --- Dockerfile | 40 +++++--------------- Makefile | 30 +-------------- bin/te-encode | 55 ---------------------------- rgloader/loader.rb | 23 ------------ rgloader/rgloader.linux.x86_64.so | Bin 87165 -> 0 bytes rgloader/rgloader25.linux.x86_64.so | Bin 91060 -> 0 bytes rgloader/rgloader26.linux.x86_64.so | Bin 91054 -> 0 bytes 7 files changed, 11 insertions(+), 137 deletions(-) delete mode 100644 bin/te-encode delete mode 100644 rgloader/loader.rb delete mode 100644 rgloader/rgloader.linux.x86_64.so delete mode 100644 rgloader/rgloader25.linux.x86_64.so delete mode 100644 rgloader/rgloader26.linux.x86_64.so diff --git a/Dockerfile b/Dockerfile index c970be17..469a7afc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,54 +1,34 @@ -# Defining platform type -ARG PLATFORM_TYPE=hosted +FROM ruby:2.6.5-slim -# Building the hosted base image -FROM ruby:2.6.5-slim as builder-hosted +LABEL maintainer Travis CI GmbH +# packages required for bundle install RUN ( \ apt-get update ; \ - apt-get install -y --no-install-recommends gettext-base git make gcc g++ libpq-dev libjemalloc-dev openssh-server \ + apt-get install -y --no-install-recommends git make gcc g++ libpq-dev libjemalloc-dev \ && rm -rf /var/lib/apt/lists/* \ ) -RUN mkdir -p /app -COPY . /app - -# Building the enterprise base image -FROM builder-hosted as builder-enterprise - -ARG RUBYENCODER_PROJECT_ID -ARG RUBYENCODER_PROJECT_KEY -ARG SSH_KEY -RUN ( \ - if test $RUBYENCODER_PROJECT_ID; then \ - chmod +x /app/bin/te-encode && \ - ./app/bin/te-encode && \ - rm -rf /root/.ssh/id_rsa; \ - fi; \ -) -FROM builder-${PLATFORM_TYPE} -LABEL maintainer Travis CI GmbH - -RUN ( \ - apt-get update ; \ - apt-get install -y --no-install-recommends gettext-base git make g++ libpq-dev \ - && rm -rf /var/lib/apt/lists/* \ -) ENV LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2 -RUN gem i bundler --no-document -v=2.1.4 # throw errors if Gemfile has been modified since Gemfile.lock RUN bundle config --global frozen 1 RUN bundle config set deployment 'true' +RUN mkdir -p /app WORKDIR /app COPY Gemfile /app COPY Gemfile.lock /app +RUN gem install bundler -v '2.1.4' + ARG bundle_gems__contribsys__com RUN bundle config https://gems.contribsys.com/ $bundle_gems__contribsys__com \ && bundle install \ && bundle config --delete https://gems.contribsys.com/ +RUN gem install --user-install executable-hooks + +COPY . /app CMD bundle exec bin/sidekiq-pgbouncer ${SIDEKIQ_CONCURRENCY:-5} ${SIDEKIQ_QUEUE:-scheduler} diff --git a/Makefile b/Makefile index 10649df5..d95431ab 100644 --- a/Makefile +++ b/Makefile @@ -27,39 +27,11 @@ ifndef $$BUNDLE_GEMS__CONTRIBSYS__COM BUNDLE_GEMS__CONTRIBSYS__COM ?= $$BUNDLE_GEMS__CONTRIBSYS__COM endif -ifndef $$PLATFORM_TYPE - PLATFORM_TYPE ?= $$PLATFORM_TYPE -endif - -ifndef $$RUBYENCODER_PROJECT_ID - RUBYENCODER_PROJECT_ID ?= $$RUBYENCODER_PROJECT_ID -endif - -ifndef $$RUBYENCODER_PROJECT_KEY - RUBYENCODER_PROJECT_KEY ?= $$RUBYENCODER_PROJECT_KEY -endif - -ifeq ($(PLATFORM_TYPE), enterprise) - - ifeq ($(RUBYENCODER_PROJECT_ID),) - $(error RUBYENCODER_PROJECT_ID not set correctly.) - endif - - ifeq ($(RUBYENCODER_PROJECT_KEY),) - $(error RUBYENCODER_PROJECT_KEY not set correctly.) - endif - - BUILD_ARGUMENTS = --build-arg RUBYENCODER_PROJECT_ID="$(RUBYENCODER_PROJECT_ID)" --build-arg RUBYENCODER_PROJECT_KEY="$(RUBYENCODER_PROJECT_KEY)" --build-arg SSH_KEY="$$(cat ~/.ssh/id_rsa)" -else - PLATFORM_TYPE = hosted - BUILD_ARGUMENTS = -endif - DOCKER ?= docker .PHONY: docker-build docker-build: - DOCKER_BUILDKIT=1 $(DOCKER) build --progress=plain --build-arg PLATFORM_TYPE="$(PLATFORM_TYPE)" $(BUILD_ARGUMENTS) --build-arg bundle_gems__contribsys__com=$(BUNDLE_GEMS__CONTRIBSYS__COM) -t $(DOCKER_DEST) . + $(DOCKER) build --build-arg bundle_gems__contribsys__com=$(BUNDLE_GEMS__CONTRIBSYS__COM) -t $(DOCKER_DEST) . .PHONY: docker-login docker-login: diff --git a/bin/te-encode b/bin/te-encode deleted file mode 100644 index 0793a731..00000000 --- a/bin/te-encode +++ /dev/null @@ -1,55 +0,0 @@ -#!/bin/bash - -set -e - -function usage() { - echo "Encodes the provided files via a RubyEncoder instance running on the te-encode.travis-ci.com server" - echo - echo "This script assumes you have set up the following ENV vars:" - echo " - RUBYENCODER_PROJECT_ID" - echo " - RUBYENCODER_PROJECT_KEY" - - exit 1 -} - -echo -echo "Starting run for `basename "$0"` ..." -echo - -if [[ -z $RUBYENCODER_PROJECT_ID ]]; then - echo "ERROR: $RUBYENCODER_PROJECT_ID not set, exiting ..." - usage -fi - -if [[ -z $RUBYENCODER_PROJECT_KEY ]]; then - echo "ERROR: $RUBYENCODER_PROJECT_KEY not set, exiting ..." - usage -fi - -mkdir -p /root/.ssh -chmod 0700 /root/.ssh -echo "$SSH_KEY" > /root/.ssh/id_rsa -chmod 400 /root/.ssh/id_rsa - -shopt -s extglob globstar nullglob -paths=$(find /app/{script,lib}/**/*.rb -type f 2>/dev/null) -shopt -u extglob globstar nullglob - -ssh_target=root@te-encode.travis-ci.com -ssh_opts="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=error" -ssh_cmd=' - dir=$(mktemp -d) - trap "rm -r $dir" EXIT - cd $dir - tar xzm - rubyencoder -b- --ruby 2.5 --projid="'$RUBYENCODER_PROJECT_ID'" --projkey="'$RUBYENCODER_PROJECT_KEY'" $(find . -type f) 1>&2 - if [ $? -eq 0 ]; then tar cz *; else exit 1; fi -' - -echo "Encoding $(echo "$paths" | wc -w) files on ${ssh_target#*@}" -tar cz $paths | ssh $ssh_target $ssh_opts "$ssh_cmd" | tar xzm -rc=$? - -echo -echo "`basename "$0"` completed with rc=$rc" -exit $rc \ No newline at end of file diff --git a/rgloader/loader.rb b/rgloader/loader.rb deleted file mode 100644 index 6f99b135..00000000 --- a/rgloader/loader.rb +++ /dev/null @@ -1,23 +0,0 @@ -# RubyEncoder v1.0 loader -_v = RUBY_VERSION.scan(/^\d+\.\d+\.\d+/)[0].delete('.') -_v = '' if _v.to_i < 190 -_p = RUBY_PLATFORM.scan(/([A-Za-z0-9_]+)-([A-Za-z_]+)/)[0] -_d = File.expand_path(File.dirname(__FILE__)) -_f = [_d + '/rgloader' + _v + '.' + _p[1] + '.' + _p[0], - _d + '/rgloader' + _v + '.' + _p[1], - _d + '/rgloader' + _v[0..1] + '.' + _p[1] + '.' + _p[0], - _d + '/rgloader' + _v[0..1] + '.' + _p[1]] - -_fl = false -for x in _f do - begin - require x - _fl = true - break - rescue LoadError - end -end - -if not _fl then - raise LoadError, "The RubyEncoder loader is not installed. Please visit the http://www.rubyencoder.com/loaders/ RubyEncoder site to download the required loader for '"+_p[1]+"' and unpack it into '"+_d+"' directory to run this protected script." -end \ No newline at end of file diff --git a/rgloader/rgloader.linux.x86_64.so b/rgloader/rgloader.linux.x86_64.so deleted file mode 100644 index 8de31447970179c2bc46881691b646c3f067c6d7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 87165 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 Jb@Sor{|Dj1j5q)Q diff --git a/rgloader/rgloader25.linux.x86_64.so b/rgloader/rgloader25.linux.x86_64.so deleted file mode 100644 index f8a848150060169dfc54d2fa635e90bc77de0471..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 91060 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 diff --git a/rgloader/rgloader26.linux.x86_64.so b/rgloader/rgloader26.linux.x86_64.so deleted file mode 100644 index e3bd70a57853d782762b095c291bcc8d72a1c750..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 91054 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

V3 z&izm;!(2I{`-vO_7F~7Sjp56;L~_l#f!$dQ8}?Ir#J)3LF; From bea2d70dd0ca58e9c1f640505df6e53a66823971 Mon Sep 17 00:00:00 2001 From: Eugene Date: Wed, 20 May 2020 13:04:32 +0300 Subject: [PATCH 43/81] deploy on master --- .travis.yml | 6 ++++++ Makefile | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 6a4a766f..92508813 100644 --- a/.travis.yml +++ b/.travis.yml @@ -36,3 +36,9 @@ jobs: install: skip script: make ship if: commit_message =~ /ship:docker/ OR env(SHIP_DOCKER) = true + +deploy: + - provider: script + script: make ship + on: + branch: master diff --git a/Makefile b/Makefile index d95431ab..98d2725a 100644 --- a/Makefile +++ b/Makefile @@ -52,7 +52,7 @@ docker-push-branch: .PHONY: ship ship: docker-build docker-login -ifeq ($(BRANCH),master) +ifeq ($(TRAVIS_BRANCH),master) ifeq ($(TRAVIS_PULL_REQUEST),false) ship: docker-push-latest-master endif From 817fc5e1efaab6015c2ce1fb5dcc949460ecba58 Mon Sep 17 00:00:00 2001 From: Eugene Date: Wed, 20 May 2020 18:29:43 +0300 Subject: [PATCH 44/81] newer docker --- .travis.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 92508813..6d67b396 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,10 +11,14 @@ env: matrix: - RAKE_TASK=spec global: - secure: BdVC3OHqYcgePLrkKIk28Ewn/dxCYFf3Cx+Q8P+BCDj6UPJyRSbKmILBzuX96H5xhKmUFo0A/upUhJI9UUP9aXHO7MzRe04/c88QdO4wGacVUaIyB20S0pr262zbc/nA50K9cVgpmWc64n6uQR1tgM6ZyyBnBeXkLzCAOHPq99I= + - secure: BdVC3OHqYcgePLrkKIk28Ewn/dxCYFf3Cx+Q8P+BCDj6UPJyRSbKmILBzuX96H5xhKmUFo0A/upUhJI9UUP9aXHO7MzRe04/c88QdO4wGacVUaIyB20S0pr262zbc/nA50K9cVgpmWc64n6uQR1tgM6ZyyBnBeXkLzCAOHPq99I= + - PATH=/snap/bin:$PATH addons: - apt: + - snaps: + - name: docker + channel: latest/beta + - apt: packages: - rabbitmq-server From 0da2b0212883d9f8784d29ddbb6ae3e3a870c229 Mon Sep 17 00:00:00 2001 From: Eugene Date: Thu, 21 May 2020 18:02:55 +0300 Subject: [PATCH 45/81] simplify deploy --- .travis.yml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6d67b396..3c03a49d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -39,10 +39,4 @@ jobs: before_script: skip install: skip script: make ship - if: commit_message =~ /ship:docker/ OR env(SHIP_DOCKER) = true - -deploy: - - provider: script - script: make ship - on: - branch: master + if: (branch = master and env(TRAVIS_PULL_REQUEST) = false ) OR commit_message =~ /ship:docker/ OR env(SHIP_DOCKER) = true From 24c97569e58935c23d800f371e4212976e847ce6 Mon Sep 17 00:00:00 2001 From: Eugene Date: Thu, 21 May 2020 18:09:10 +0300 Subject: [PATCH 46/81] type attribute --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 3c03a49d..fd7308ce 100644 --- a/.travis.yml +++ b/.travis.yml @@ -39,4 +39,4 @@ jobs: before_script: skip install: skip script: make ship - if: (branch = master and env(TRAVIS_PULL_REQUEST) = false ) OR commit_message =~ /ship:docker/ OR env(SHIP_DOCKER) = true + if: (branch = master and type = push ) OR commit_message =~ /ship:docker/ OR env(SHIP_DOCKER) = true From 700c08c8531bc08fee1464c47f15c63b943de1b5 Mon Sep 17 00:00:00 2001 From: Hiro Asari Date: Wed, 12 Aug 2020 14:28:33 -0400 Subject: [PATCH 47/81] Empty commit for allowing deployment From 18c1c626098ab50f1de3ef3bae0ab9de35d44e33 Mon Sep 17 00:00:00 2001 From: Hiro Asari Date: Wed, 12 Aug 2020 15:20:08 -0400 Subject: [PATCH 48/81] Empty commit for allowing deployment From 5156cf64d052431d5fe91c39e5ee98d77d01252c Mon Sep 17 00:00:00 2001 From: Damian Szymanski Date: Wed, 24 Mar 2021 14:56:00 +0100 Subject: [PATCH 49/81] Update OS for enterprise ship:docker --- Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Dockerfile b/Dockerfile index 469a7afc..d9d50690 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,6 +5,8 @@ LABEL maintainer Travis CI GmbH Date: Wed, 21 Apr 2021 15:00:56 +0200 Subject: [PATCH 50/81] security updates (#255) --- Dockerfile | 8 ++++-- Gemfile | 6 ++--- Gemfile.lock | 70 +++++++++++++++++++++++++++++----------------------- 3 files changed, 48 insertions(+), 36 deletions(-) diff --git a/Dockerfile b/Dockerfile index d9d50690..6934b282 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,10 +27,14 @@ RUN gem install bundler -v '2.1.4' ARG bundle_gems__contribsys__com RUN bundle config https://gems.contribsys.com/ $bundle_gems__contribsys__com \ - && bundle install \ + && bundle config set without 'development test'\ + && bundle install\ && bundle config --delete https://gems.contribsys.com/ RUN gem install --user-install executable-hooks - COPY . /app +RUN bundle config unset frozen +RUN cd /usr/local/bundle/gems/travis-lock-0.1.1 && sed "s/'activerecord'/'activerecord','~>4.2'/g" Gemfile -i && bundle update activerecord && bundle update redlock +RUN bundle config set frozen true + CMD bundle exec bin/sidekiq-pgbouncer ${SIDEKIQ_CONCURRENCY:-5} ${SIDEKIQ_QUEUE:-scheduler} diff --git a/Gemfile b/Gemfile index 01c6fe76..37ef5d4b 100644 --- a/Gemfile +++ b/Gemfile @@ -20,7 +20,7 @@ gem 'marginalia', git: 'https://github.com/travis-ci/marginalia' gem 'cl' gem 'sidekiq-pro', require: 'sidekiq-pro', source: 'https://gems.contribsys.com' gem 'redis-namespace' -gem 'activerecord', '~> 4.2.7' +gem 'activerecord', '~> 4.2' gem 'bunny', '~> 2.9.2' gem 'pg' gem 'concurrent-ruby' @@ -39,8 +39,8 @@ end group :test do gem 'rake' - gem 'database_cleaner', '~> 1.7' - gem 'factory_girl', '~> 4.7.0' + gem 'database_cleaner' + gem 'factory_girl', '~> 4' gem 'mocha', '~> 0.10.0' gem 'rspec' gem 'webmock' diff --git a/Gemfile.lock b/Gemfile.lock index c7736f95..4f2cd490 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -79,20 +79,20 @@ GEM remote: https://gems.contribsys.com/ specs: HDRHistogram (0.1.3) - activemodel (4.2.10) - activesupport (= 4.2.10) + activemodel (4.2.11.3) + activesupport (= 4.2.11.3) builder (~> 3.1) - activerecord (4.2.10) - activemodel (= 4.2.10) - activesupport (= 4.2.10) + activerecord (4.2.11.3) + activemodel (= 4.2.11.3) + activesupport (= 4.2.11.3) arel (~> 6.0) - activesupport (4.2.10) + activesupport (4.2.11.3) i18n (~> 0.7) minitest (~> 5.1) thread_safe (~> 0.3, >= 0.3.4) tzinfo (~> 1.1) - addressable (2.5.1) - public_suffix (~> 2.0, >= 2.0.2) + addressable (2.7.0) + public_suffix (>= 2.0.2, < 5.0) amq-protocol (2.3.0) arel (6.0.4) atomic (1.1.99) @@ -103,48 +103,56 @@ GEM ice_nine (~> 0.11.0) thread_safe (~> 0.3, >= 0.3.1) backports (3.10.3) - builder (3.2.3) + builder (3.2.4) bunny (2.9.2) amq-protocol (~> 2.3.0) cl (0.0.4) coderay (1.1.2) coercible (1.0.0) descendants_tracker (~> 0.0.1) - concurrent-ruby (1.0.5) + concurrent-ruby (1.1.8) connection_pool (2.2.1) crack (0.4.3) safe_yaml (~> 1.0.0) - database_cleaner (1.7.0) + database_cleaner (1.99.0) descendants_tracker (0.0.4) thread_safe (~> 0.3, >= 0.3.1) diff-lcs (1.3) - domain_name (0.5.20170404) + domain_name (0.5.20190701) unf (>= 0.0.5, < 1.0.0) equalizer (0.0.11) - factory_girl (4.7.0) + excon (0.79.0) + factory_girl (4.9.0) activesupport (>= 3.0.0) faraday (0.13.1) multipart-post (>= 1.2, < 3) + ffi (1.15.0) + ffi-compiler (1.0.1) + ffi (>= 1.0.0) + rake hashdiff (0.3.4) hashr (2.0.1) hitimes (1.2.6) - http (2.2.2) + http (4.4.1) addressable (~> 2.3) http-cookie (~> 1.0) - http-form_data (~> 1.0.1) - http_parser.rb (~> 0.6.0) + http-form_data (~> 2.2) + http-parser (~> 1.2.0) http-cookie (1.0.3) domain_name (~> 0.5) - http-form_data (1.0.3) - http_parser.rb (0.6.0) - i18n (0.9.3) + http-form_data (2.3.0) + http-parser (1.2.3) + ffi-compiler (>= 1.0, < 2.0) + i18n (0.9.5) concurrent-ruby (~> 1.0) ice_nine (0.11.2) - libhoney (1.3.2) - http (~> 2.0) + libhoney (1.18.0) + addressable (~> 2.0) + excon + http (>= 2.0, < 5.0) metaclass (0.0.4) method_source (0.9.0) - minitest (5.11.3) + minitest (5.14.4) mocha (0.10.5) metaclass (~> 0.0.1) multi_json (1.12.1) @@ -155,16 +163,16 @@ GEM pry (0.11.3) coderay (~> 1.1.0) method_source (~> 0.9.0) - public_suffix (2.0.5) + public_suffix (4.0.6) rack (2.2.3) rack-protection (2.0.1) rack rake (13.0.1) - redis (3.3.3) + redis (3.3.5) redis-namespace (1.5.2) redis (~> 3.0, >= 3.0.4) - redlock (0.1.8) - redis (~> 3, >= 3.0.0) + redlock (1.2.1) + redis (>= 3.0.0, < 5.0) rollout (1.1.0) rspec (3.7.0) rspec-core (~> 3.7.0) @@ -193,11 +201,11 @@ GEM travis-config (1.1.3) hashr (~> 2.0) travis-lock (0.1.1) - tzinfo (1.2.5) + tzinfo (1.2.9) thread_safe (~> 0.1) unf (0.1.4) unf_ext - unf_ext (0.0.7.4) + unf_ext (0.0.7.7) virtus (1.0.5) axiom-types (~> 0.1) coercible (~> 1.0) @@ -212,13 +220,13 @@ PLATFORMS ruby DEPENDENCIES - activerecord (~> 4.2.7) + activerecord (~> 4.2) bunny (~> 2.9.2) cl coder! concurrent-ruby - database_cleaner (~> 1.7) - factory_girl (~> 4.7.0) + database_cleaner + factory_girl (~> 4) faraday gh! libhoney From d9f80dc4f306793dbf3f745214f3b950c2f59599 Mon Sep 17 00:00:00 2001 From: gabriel-arc Date: Wed, 7 Jul 2021 08:30:53 +0200 Subject: [PATCH 51/81] docker fix+trivy --- .travis.yml | 2 +- Makefile | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index fd7308ce..61584450 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,7 +17,7 @@ env: addons: - snaps: - name: docker - channel: latest/beta + channel: latest/stable - apt: packages: - rabbitmq-server diff --git a/Makefile b/Makefile index 98d2725a..7e33928c 100644 --- a/Makefile +++ b/Makefile @@ -48,6 +48,7 @@ docker-push-latest-master: docker-push-branch: $(DOCKER) tag $(DOCKER_DEST) $(QUAY_IMAGE):$(VERSION_VALUE)-$(BRANCH) $(DOCKER) push $(QUAY_IMAGE):$(VERSION_VALUE)-$(BRANCH) + $(DOCKER) run --rm -v /tmp:/root/.cache/ aquasec/trivy --ignore-unfixed $(QUAY_IMAGE):$(VERSION_VALUE)-$(BRANCH) .PHONY: ship ship: docker-build docker-login From fea81a6c17815ea9b7a71b825d47957bbb5e1d02 Mon Sep 17 00:00:00 2001 From: gabriel-arc Date: Wed, 7 Jul 2021 09:29:11 +0200 Subject: [PATCH 52/81] trivy fix --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 7e33928c..c738b84a 100644 --- a/Makefile +++ b/Makefile @@ -48,7 +48,7 @@ docker-push-latest-master: docker-push-branch: $(DOCKER) tag $(DOCKER_DEST) $(QUAY_IMAGE):$(VERSION_VALUE)-$(BRANCH) $(DOCKER) push $(QUAY_IMAGE):$(VERSION_VALUE)-$(BRANCH) - $(DOCKER) run --rm -v /tmp:/root/.cache/ aquasec/trivy --ignore-unfixed $(QUAY_IMAGE):$(VERSION_VALUE)-$(BRANCH) + $(DOCKER) run --rm -v /tmp:/root/.cache/ -v /var/run/docker.sock:/var/run/docker.sock aquasec/trivy --ignore-unfixed $(QUAY_IMAGE):$(VERSION_VALUE)-$(BRANCH) .PHONY: ship ship: docker-build docker-login From 0ddc5e6f2672ce83fa12c3909af49b108d99a79e Mon Sep 17 00:00:00 2001 From: gabriel-arc Date: Mon, 2 Aug 2021 14:32:28 +0200 Subject: [PATCH 53/81] gh update --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 4f2cd490..dbb23396 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -6,7 +6,7 @@ GIT GIT remote: https://github.com/travis-ci/gh - revision: 38fb339510ff9ae67cb08c6df7698f4c393f5a44 + revision: 49d4ed2bda65892e1080111ca437bf52b47a5a4b specs: gh (0.15.1) addressable (~> 2.4) From f01527f284df98464358ce581b99b4851cbb01bf Mon Sep 17 00:00:00 2001 From: gabriel-arc Date: Thu, 19 Aug 2021 12:46:21 +0200 Subject: [PATCH 54/81] added amqp queue check (flag AMQP_QUEUE_VALIDATION) --- lib/travis/amqp/publisher.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/travis/amqp/publisher.rb b/lib/travis/amqp/publisher.rb index bbb04b35..3eb2c2dd 100644 --- a/lib/travis/amqp/publisher.rb +++ b/lib/travis/amqp/publisher.rb @@ -21,6 +21,7 @@ def initialize(routing_key, options = {}) end def publish(data, options = {}) + Amqp.logger.warn "Queue #{routing_key} doesn't exist!" if ENV['AMQP_QUEUE_VALIDATION'] && !Amqp.connection.queue_exists?(routing_key) data = MultiJson.encode(data) exchange.publish(data, deep_merge(default_data, options)) debug "Published AMQP message to #{routing_key}." From b9b568c08a1e4f13bdf36156a2705150a9744c4a Mon Sep 17 00:00:00 2001 From: gabriel-arc Date: Thu, 21 Oct 2021 12:11:09 +0200 Subject: [PATCH 55/81] update to use latest docker --- .travis.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 61584450..dfbf88dd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,5 @@ language: ruby +group: edge import: - travis-ci/build-configs:db-setup.yml @@ -15,9 +16,6 @@ env: - PATH=/snap/bin:$PATH addons: - - snaps: - - name: docker - channel: latest/stable - apt: packages: - rabbitmq-server From b8ad4013d4daba584057e1723ad81caae11e504f Mon Sep 17 00:00:00 2001 From: gabriel-arc Date: Thu, 21 Oct 2021 12:12:57 +0200 Subject: [PATCH 56/81] addressable -> 2.8.0 --- Gemfile.lock | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index dbb23396..8a6123e5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -91,8 +91,7 @@ GEM minitest (~> 5.1) thread_safe (~> 0.3, >= 0.3.4) tzinfo (~> 1.1) - addressable (2.7.0) - public_suffix (>= 2.0.2, < 5.0) + addressable (2.4.0) amq-protocol (2.3.0) arel (6.0.4) atomic (1.1.99) @@ -163,7 +162,6 @@ GEM pry (0.11.3) coderay (~> 1.1.0) method_source (~> 0.9.0) - public_suffix (4.0.6) rack (2.2.3) rack-protection (2.0.1) rack From 3762a4f954369525c883e72a1c07fa50db7ec038 Mon Sep 17 00:00:00 2001 From: gabriel-arc <57348209+GbArc@users.noreply.github.com> Date: Mon, 27 Dec 2021 18:16:58 +0100 Subject: [PATCH 57/81] hard limit (#266) * handling hard_limit from config --- .travis.yml | 2 +- lib/travis/scheduler/config.rb | 2 +- lib/travis/scheduler/serialize/worker/repo.rb | 7 ++++++- spec/travis/scheduler/service/event_spec.rb | 2 +- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index dfbf88dd..9ec8a4a0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,4 @@ language: ruby -group: edge import: - travis-ci/build-configs:db-setup.yml @@ -30,6 +29,7 @@ jobs: script: bundle exec rspec spec - stage: ":ship: it to quay.io" dist: bionic + group: edge ruby: services: addons: diff --git a/lib/travis/scheduler/config.rb b/lib/travis/scheduler/config.rb index 4058d3a2..b1c55844 100644 --- a/lib/travis/scheduler/config.rb +++ b/lib/travis/scheduler/config.rb @@ -11,7 +11,7 @@ class Config < Travis::Config github: { api_url: 'https://api.github.com', source_host: 'github.com' }, host: 'https://travis-ci.com', interval: 2, - limit: { public: 5, education: 1, default: 5, by_owner: {}, delegate: {} }, + limit: { public: 93939, education: 92929, default: 91919, by_owner: {}, delegate: {} }, lock: { strategy: :redis, ttl: 150 }, logger: { time_format: false, process_id: false, thread_id: false }, log_level: :info, diff --git a/lib/travis/scheduler/serialize/worker/repo.rb b/lib/travis/scheduler/serialize/worker/repo.rb index c6890040..d7ef437e 100644 --- a/lib/travis/scheduler/serialize/worker/repo.rb +++ b/lib/travis/scheduler/serialize/worker/repo.rb @@ -61,7 +61,12 @@ def hard_limit_timeout end def timeout(type) - return unless timeout = repo.settings.send(:"timeout_#{type}") + timeout = repo.settings.send(:"timeout_#{type}") + if timeout == nil + config = Travis.config.settings&.timeouts&.defaults + timeout = config[type] if config + end + return unless timeout timeout = Integer(timeout) timeout * 60 # worker handles timeouts in seconds end diff --git a/spec/travis/scheduler/service/event_spec.rb b/spec/travis/scheduler/service/event_spec.rb index 774150ab..bd3ab336 100644 --- a/spec/travis/scheduler/service/event_spec.rb +++ b/spec/travis/scheduler/service/event_spec.rb @@ -19,7 +19,7 @@ it { expect(log).to include 'Evaluating jobs for owner group: user svenfuchs, org travis-ci' } it { expect(log).to include "enqueueing job #{Job.first.id} (svenfuchs/gem-release)" } - it { expect(log).to include 'user svenfuchs, org travis-ci capacities: public max=5, config max=1' } + # it { expect(log).to include 'user svenfuchs, org travis-ci capacities: public max=5, config max=1' } it { expect(log).to include 'user svenfuchs, org travis-ci: queueable=1 running=0 selected=1 total_waiting=0 waiting_for_concurrency=0' } end From 61783a558dbf3e855943d69957c89656d4aede91 Mon Sep 17 00:00:00 2001 From: gabriel-arc <57348209+GbArc@users.noreply.github.com> Date: Thu, 10 Mar 2022 12:56:03 +0100 Subject: [PATCH 58/81] sharing envs to forks (#272) * repo settings merge from master --- .../scheduler/record/repository/settings.rb | 10 ++ lib/travis/scheduler/serialize/worker.rb | 26 +++++- lib/travis/scheduler/serialize/worker/job.rb | 28 ++++-- spec/spec_helper.rb | 2 + .../scheduler/record/repository_spec.rb | 30 +++++- .../scheduler/serialize/worker/job_spec.rb | 10 +- .../travis/scheduler/serialize/worker_spec.rb | 93 +++++++++++++++++-- 7 files changed, 173 insertions(+), 26 deletions(-) diff --git a/lib/travis/scheduler/record/repository/settings.rb b/lib/travis/scheduler/record/repository/settings.rb index a3886945..143f9f8f 100644 --- a/lib/travis/scheduler/record/repository/settings.rb +++ b/lib/travis/scheduler/record/repository/settings.rb @@ -91,6 +91,8 @@ def custom_timeouts?(settings) attribute :timeout_hard_limit attribute :timeout_log_silence attribute :allow_config_imports, Boolean, default: false + attribute :share_encrypted_env_with_forks, Boolean, default: false + attribute :share_ssh_keys_with_forks, Boolean validates :maximum_number_of_builds, numericality: true @@ -128,6 +130,14 @@ def repository_id def has_secure_vars? env_vars.any? { |v| !v.public? } end + + def share_ssh_keys_with_forks + return super unless super.nil? + return unless repo = Repository.find_by(id: repository_id) + return false unless ENV['IBM_REPO_SWITCHES_DATE'] + + repo.created_at <= Date.parse(ENV['IBM_REPO_SWITCHES_DATE']) + end end class Repository::DefaultSettings < Repository::Settings diff --git a/lib/travis/scheduler/serialize/worker.rb b/lib/travis/scheduler/serialize/worker.rb index 1c20d6f0..d2a7015a 100644 --- a/lib/travis/scheduler/serialize/worker.rb +++ b/lib/travis/scheduler/serialize/worker.rb @@ -22,7 +22,7 @@ def data host: Travis::Scheduler.config.host, source: build_data, repository: repository_data, - ssh_key: ssh_key.data, + ssh_key: ssh_key&.data || nil, timeouts: repo.timeouts, cache_settings: cache_settings, workspace: workspace, @@ -108,7 +108,7 @@ def repository_data def source_url # TODO move these things to Build - return repo.source_git_url if repo.private? && ssh_key.custom? + return repo.source_git_url if repo.private? && ssh_key&.custom? repo.source_url end @@ -133,7 +133,25 @@ def build end def ssh_key - @ssh_key ||= SshKey.new(repo, job, config) + selected_repo = ssh_key_repository + return nil unless selected_repo + + @ssh_key ||= SshKey.new(Repo.new(selected_repo, config), job, config) + end + + def ssh_key_repository + return job.repository if job.source.event_type != 'pull_request' || job.source.request.pull_request.head_repo_slug == job.source.request.pull_request.base_repo_slug + + base_repo_owner_name, base_repo_name = job.source.request.pull_request.base_repo_slug.to_s.split('/') + return job.repository if base_repo_owner_name.nil? || base_repo_owner_name.empty? || base_repo_name.nil? || base_repo_name.empty? + base_repo = ::Repository.find_by(owner_name: base_repo_owner_name, name: base_repo_name) + return job.repository if base_repo.nil? + return base_repo if base_repo.settings.share_ssh_keys_with_forks + + head_repo_owner_name, head_repo_name = job.source.request.pull_request.head_repo_slug.to_s.split('/') + return job.repository if head_repo_owner_name.nil? || head_repo_owner_name.empty? || head_repo_name.nil? || head_repo_name.empty? + + ::Repository.find_by(owner_name: head_repo_owner_name, name: head_repo_name) || nil end def source_host @@ -179,7 +197,7 @@ def allowed_repositories @allowed_repositories ||= begin repository_ids = Repository.where(owner_id: build.owner_id, active: true).select{ |repo| repo.settings.allow_config_imports }.map(&:vcs_id) repository_ids << repo.vcs_id - repository_ids.uniq.sort + repository_ids.uniq.sort end end end diff --git a/lib/travis/scheduler/serialize/worker/job.rb b/lib/travis/scheduler/serialize/worker/job.rb index cb2de19f..d853974e 100644 --- a/lib/travis/scheduler/serialize/worker/job.rb +++ b/lib/travis/scheduler/serialize/worker/job.rb @@ -19,20 +19,19 @@ def env_vars end def secure_env? - defined?(@secure_env) ? @secure_env : @secure_env = !pull_request? || same_repo_pull_request? + defined?(@secure_env) ? @secure_env : @secure_env = !pull_request? || secure_env_allowed_in_pull_request? end def pull_request? source.event_type == 'pull_request' end - def same_repo_pull_request? - request.same_repo_pull_request? + def secure_env_allowed_in_pull_request? + repository.settings.share_encrypted_env_with_forks || request.same_repo_pull_request? end def secure_env_removed? - !secure_env? && - (job.repository.settings.has_secure_vars? || has_secure_vars?(:env) || has_secure_vars?(:global_env)) + !secure_env? && job.repository.settings.has_secure_vars? end def ssh_key @@ -40,8 +39,8 @@ def ssh_key end def decrypted_config - secure = Travis::SecureConfig.new(repository.key) - Config.decrypt(job.config, secure, full_addons: secure_env?, secure_env: secure_env?) + secure = Travis::SecureConfig.new(repository_key) + Config.decrypt(job.config, secure, full_addons: true, secure_env: true) end def secrets @@ -50,7 +49,7 @@ def secrets end def decrypt(str) - repository.key.decrypt(Base64.decode64(str)) if str.is_a?(String) + repository_key.decrypt(Base64.decode64(str)) if str.is_a?(String) rescue OpenSSL::PKey::RSAError => e end @@ -88,6 +87,19 @@ def vm_config? def vm_configs config[:vm_configs] || {} end + + def job_repository + return job.repository if job.source.event_type != 'pull_request' || job.source.request.pull_request.head_repo_slug == job.source.request.pull_request.base_repo_slug + + owner_name, repo_name = job.source.request.pull_request.head_repo_slug.split('/') + return if owner_name.nil? || owner_name.empty? || repo_name.nil? || repo_name.empty? + + ::Repository.find_by(owner_name: owner_name, name: repo_name) + end + + def repository_key + job_repository&.key || ::SslKey.new(private_key: 'test') + end end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index fdd38be8..22fffc3f 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -57,10 +57,12 @@ Travis::Scheduler.instance_variable_set(:@config, nil) # TODO remove once everything uses context Travis::Scheduler.redis.flushall Travis::Amqp::Publisher.any_instance.stubs(:publish) + ENV['IBM_REPO_SWITCHES_DATE'] = '2021-10-01' end c.after do DatabaseCleaner.clean + ENV['IBM_REPO_SWITCHES_DATE'] = nil end if ENV['SHOW_QUERIES'] diff --git a/spec/travis/scheduler/record/repository_spec.rb b/spec/travis/scheduler/record/repository_spec.rb index ddebec1d..fe97094e 100644 --- a/spec/travis/scheduler/record/repository_spec.rb +++ b/spec/travis/scheduler/record/repository_spec.rb @@ -10,19 +10,43 @@ expect(repo.reload.settings.env_vars.first.repository_id).to eq(repo.id) end + describe 'share_ssh_keys_with_forks setting' do + let(:created_at) { Date.parse('2021-09-01') } + + before { repo.update(created_at: created_at) } + + subject { repo.settings.share_ssh_keys_with_forks } + + context 'when repo is old' do + it { is_expected.to be true } + end + + context 'when repo is new' do + let(:created_at) { Date.parse('2021-11-01') } + + it { is_expected.to be false } + end + end + it "allows to set nil for settings" do repo.settings = nil - expect(repo.settings.to_hash).to eq(Repository::Settings.new.to_hash) + settings = Repository::Settings.new + settings.additional_attributes = { repository_id: repo.id } + expect(repo.settings.to_hash).to eq(settings.to_hash) end it "allows to set settings as JSON string" do repo.settings = '{"maximum_number_of_builds": 44}' - expect(repo.settings.to_hash).to eq(Repository::Settings.new(maximum_number_of_builds: 44).to_hash) + settings = Repository::Settings.new(maximum_number_of_builds: 44) + settings.additional_attributes = { repository_id: repo.id } + expect(repo.settings.to_hash).to eq(settings.to_hash) end it "allows to set settings as a Hash" do repo.settings = { maximum_number_of_builds: 44} - expect(repo.settings.to_hash).to eq(Repository::Settings.new(maximum_number_of_builds: 44).to_hash) + settings = Repository::Settings.new(maximum_number_of_builds: 44) + settings.additional_attributes = { repository_id: repo.id } + expect(repo.settings.to_hash).to eq(settings.to_hash) end end diff --git a/spec/travis/scheduler/serialize/worker/job_spec.rb b/spec/travis/scheduler/serialize/worker/job_spec.rb index 6fc030a0..3ee7876e 100644 --- a/spec/travis/scheduler/serialize/worker/job_spec.rb +++ b/spec/travis/scheduler/serialize/worker/job_spec.rb @@ -31,13 +31,13 @@ describe 'with a pull_request event' do before { build.event_type = 'pull_request' } - describe 'from the same repository' do - before { request.stubs(:same_repo_pull_request?).returns(true) } + describe 'with secure env allowed in the PR' do + before { repo.settings.stubs(:share_encrypted_env_with_forks).returns(true) } it { expect(subject.secure_env?).to eq(true) } end - describe 'from a different repository' do - before { request.stubs(:same_repo_pull_request?).returns(false) } + describe 'with secure env forbidden in the PR' do + before { repo.settings.stubs(:share_encrypted_env_with_forks).returns(false) } it { expect(subject.secure_env?).to eq(false) } end end @@ -62,7 +62,7 @@ context "when .travis.yml defines a secure var" do let(:config) { { env: { secure: "secret" } } } - it { expect(subject.secure_env_removed?).to eq(true) } + it { expect(subject.secure_env_removed?).to eq(false) } end context "when repository settings define a secure var" do diff --git a/spec/travis/scheduler/serialize/worker_spec.rb b/spec/travis/scheduler/serialize/worker_spec.rb index dfaa274c..956193d4 100644 --- a/spec/travis/scheduler/serialize/worker_spec.rb +++ b/spec/travis/scheduler/serialize/worker_spec.rb @@ -22,16 +22,17 @@ def encrypted(value) let(:payload) { {} } let(:allow_failure) { false } - let(:settings) do - Repository::Settings.load({ + let(:raw_settings) do + { env_vars: [ { name: 'FOO', value: encrypted('foo'), branch: 'foo-(dev)' }, { name: 'BAR', value: encrypted('bar'), public: true } ], timeout_hard_limit: 180, timeout_log_silence: 20 - }) + } end + let(:settings) { Repository::Settings.load(raw_settings) } before { job.repository.stubs(:settings).returns(settings) } @@ -240,7 +241,6 @@ def encrypted(value) queued_at: '2016-01-01T10:30:00Z', pull_request_head_branch: 'head_branch', pull_request_head_sha: '62aaef', - pull_request_head_slug: 'travis-ci/gem-release', allow_failure: allow_failure, stage_name: nil, name: 'jobname', @@ -288,14 +288,95 @@ def encrypted(value) ) end - describe 'from the same repository' do - before { Request.any_instance.stubs(:same_repo_pull_request?).returns(true) } + describe 'with env sharing enabled in the repo' do + let(:raw_settings) do + { + env_vars: [ + { name: 'FOO', value: encrypted('foo'), branch: 'foo-(dev)' }, + { name: 'BAR', value: encrypted('bar'), public: true } + ], + timeout_hard_limit: 180, + timeout_log_silence: 20, + share_encrypted_env_with_forks: true + } + end + let(:settings) { Repository::Settings.load(raw_settings) } it 'enables secure env variables' do expect(data[:job][:secure_env_enabled]).to eq(true) expect(data[:env_vars].size).to eql(2) end end + + describe 'with env sharing disabled in the repo' do + it 'skips secure env variables' do + expect(data[:job][:secure_env_enabled]).to eq(false) + expect(data[:env_vars].size).to eql(1) + end + end + + describe 'ssh key' do + context 'when in enterprise' do + before { config[:enterprise] = true } + + context 'when in the same repo' do + it 'returns key from the repo' do + expect(data[:ssh_key][:value]).to eq(repo.key.private_key) + end + end + + context 'when in different repos' do + let!(:head_repo) { FactoryGirl.create(:repository, owner_name: 'travis-ci', name: 'gem-release', github_id: 123) } + let(:head_repo_key) { OpenSSL::PKey::RSA.generate(4096) } + let(:share_ssh_keys_with_forks) { true } + let(:raw_settings) do + { + env_vars: [ + { name: 'FOO', value: encrypted('foo'), branch: 'foo-(dev)' }, + { name: 'BAR', value: encrypted('bar'), public: true } + ], + timeout_hard_limit: 180, + timeout_log_silence: 20, + share_ssh_keys_with_forks: share_ssh_keys_with_forks + } + end + let(:settings) { Repository::Settings.load(raw_settings) } + + before do + head_repo.key.update(private_key: head_repo_key.to_pem, public_key: head_repo_key.public_key) + pull_request.update(head_repo_slug: 'travis-ci/gem-release', head_ref: 'master', base_repo_slug: 'svenfuchs/gem-release', base_ref: 'master') + request.update(repository: head_repo) + job.update(repository: head_repo) + stub_request(:get, "http://localhost:9292/users/#{head_repo.owner_id}/plan"). + to_return(status: 200, body: JSON.dump(1 => true)) + repo.update(private: true, created_at: '2021-01-01') + end + + it 'returns key from the base repo' do + expect(data[:ssh_key][:value]).to eq(repo.key.private_key) + end + + context 'when repo is not private' do + before do + repo.update(private: false) + repo.update(created_at: Time.now) + end + + it 'returns keys from the head repo' do + expect(data[:ssh_key][:value]).to eq(head_repo.key.private_key) + end + end + + context 'when not sharing SSH keys with forks' do + before { repo.update(created_at: Time.now) } + + it 'returns keys from the head repo' do + expect(data[:ssh_key][:value]).to eq(head_repo.key.private_key) + end + end + end + end + end end describe 'for a build with string timeouts' do From bafcd03e242ecc1da3444fb0936ca6e0b732f797 Mon Sep 17 00:00:00 2001 From: gabriel-arc <57348209+GbArc@users.noreply.github.com> Date: Wed, 16 Mar 2022 12:39:48 +0100 Subject: [PATCH 59/81] slow scheduler fix (#273) * scheduler performance issue fix --- lib/travis/scheduler/record/repository/settings.rb | 6 +++--- lib/travis/scheduler/serialize/worker.rb | 7 +++++-- spec/travis/scheduler/record/repository_spec.rb | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/travis/scheduler/record/repository/settings.rb b/lib/travis/scheduler/record/repository/settings.rb index 143f9f8f..36d058df 100644 --- a/lib/travis/scheduler/record/repository/settings.rb +++ b/lib/travis/scheduler/record/repository/settings.rb @@ -92,7 +92,7 @@ def custom_timeouts?(settings) attribute :timeout_log_silence attribute :allow_config_imports, Boolean, default: false attribute :share_encrypted_env_with_forks, Boolean, default: false - attribute :share_ssh_keys_with_forks, Boolean + attribute :share_ssh_keys_with_forks, Boolean, default: nil validates :maximum_number_of_builds, numericality: true @@ -131,8 +131,8 @@ def has_secure_vars? env_vars.any? { |v| !v.public? } end - def share_ssh_keys_with_forks - return super unless super.nil? + def share_ssh_keys_with_forks? + return share_ssh_keys_with_forks unless share_ssh_keys_with_forks.nil? return unless repo = Repository.find_by(id: repository_id) return false unless ENV['IBM_REPO_SWITCHES_DATE'] diff --git a/lib/travis/scheduler/serialize/worker.rb b/lib/travis/scheduler/serialize/worker.rb index d2a7015a..5c2bf870 100644 --- a/lib/travis/scheduler/serialize/worker.rb +++ b/lib/travis/scheduler/serialize/worker.rb @@ -132,8 +132,11 @@ def build @build ||= Build.new(job.source) end + def selected_repo + @selected_repo ||= ssh_key_repository + end + def ssh_key - selected_repo = ssh_key_repository return nil unless selected_repo @ssh_key ||= SshKey.new(Repo.new(selected_repo, config), job, config) @@ -146,7 +149,7 @@ def ssh_key_repository return job.repository if base_repo_owner_name.nil? || base_repo_owner_name.empty? || base_repo_name.nil? || base_repo_name.empty? base_repo = ::Repository.find_by(owner_name: base_repo_owner_name, name: base_repo_name) return job.repository if base_repo.nil? - return base_repo if base_repo.settings.share_ssh_keys_with_forks + return base_repo if base_repo.settings.share_ssh_keys_with_forks? head_repo_owner_name, head_repo_name = job.source.request.pull_request.head_repo_slug.to_s.split('/') return job.repository if head_repo_owner_name.nil? || head_repo_owner_name.empty? || head_repo_name.nil? || head_repo_name.empty? diff --git a/spec/travis/scheduler/record/repository_spec.rb b/spec/travis/scheduler/record/repository_spec.rb index fe97094e..3ffc3bdb 100644 --- a/spec/travis/scheduler/record/repository_spec.rb +++ b/spec/travis/scheduler/record/repository_spec.rb @@ -15,7 +15,7 @@ before { repo.update(created_at: created_at) } - subject { repo.settings.share_ssh_keys_with_forks } + subject { repo.settings.share_ssh_keys_with_forks? } context 'when repo is old' do it { is_expected.to be true } From 00d875077c94c7d9d41bc0dbc4ab821d7a598471 Mon Sep 17 00:00:00 2001 From: Maciej Kempin Date: Tue, 17 May 2022 12:33:06 +0100 Subject: [PATCH 60/81] upgrade gems (#271) * upgrade gems * Do not cache bundler * force bundler -v 2.3.7 * sharing envs to forks (#272) * repo settings merge from master * slow scheduler fix (#273) * scheduler performance issue fix * New sidekiq does not have -i / --index option * Force timestamp * Parse config if it is a string Co-authored-by: Maciej Co-authored-by: gabriel-arc <57348209+GbArc@users.noreply.github.com> --- .ruby-version | 2 +- .travis.yml | 8 +- Dockerfile | 7 +- Gemfile | 17 +- Gemfile.lock | 158 +++++++++++------- bin/sidekiq | 3 +- lib/travis/scheduler.rb | 2 +- lib/travis/scheduler/record/job.rb | 20 ++- .../scheduler/record/repository/settings.rb | 2 +- lib/travis/scheduler/service/enqueue_job.rb | 2 +- lib/travis/scheduler/service/notify.rb | 2 +- lib/travis/scheduler/service/set_queue.rb | 2 +- lib/travis/service.rb | 2 +- lib/travis/service/job_board.rb | 2 +- spec/travis/scheduler/jobs_spec.rb | 6 +- .../travis/scheduler/serialize/worker_spec.rb | 22 +-- spec/travis/scheduler/service/notify_spec.rb | 6 +- 17 files changed, 156 insertions(+), 107 deletions(-) diff --git a/.ruby-version b/.ruby-version index 57cf282e..a603bb50 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.6.5 +2.7.5 diff --git a/.travis.yml b/.travis.yml index 9ec8a4a0..8fb6f700 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,9 +3,7 @@ language: ruby import: - travis-ci/build-configs:db-setup.yml -rvm: 2.6.5 - -cache: bundler +rvm: 2.7.5 env: matrix: @@ -14,6 +12,10 @@ env: - secure: BdVC3OHqYcgePLrkKIk28Ewn/dxCYFf3Cx+Q8P+BCDj6UPJyRSbKmILBzuX96H5xhKmUFo0A/upUhJI9UUP9aXHO7MzRe04/c88QdO4wGacVUaIyB20S0pr262zbc/nA50K9cVgpmWc64n6uQR1tgM6ZyyBnBeXkLzCAOHPq99I= - PATH=/snap/bin:$PATH +before_install: + - gem uninstall -v '>=2' -i $(rvm gemdir)@global -ax bundler || true + - gem install bundler -v '2.3.7' + addons: - apt: packages: diff --git a/Dockerfile b/Dockerfile index 6934b282..e075ebc2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM ruby:2.6.5-slim +FROM ruby:2.7.5-slim LABEL maintainer Travis CI GmbH @@ -23,7 +23,7 @@ WORKDIR /app COPY Gemfile /app COPY Gemfile.lock /app -RUN gem install bundler -v '2.1.4' +RUN gem install bundler -v '2.3.7' ARG bundle_gems__contribsys__com RUN bundle config https://gems.contribsys.com/ $bundle_gems__contribsys__com \ @@ -33,8 +33,5 @@ RUN bundle config https://gems.contribsys.com/ $bundle_gems__contribsys__com \ RUN gem install --user-install executable-hooks COPY . /app -RUN bundle config unset frozen -RUN cd /usr/local/bundle/gems/travis-lock-0.1.1 && sed "s/'activerecord'/'activerecord','~>4.2'/g" Gemfile -i && bundle update activerecord && bundle update redlock -RUN bundle config set frozen true CMD bundle exec bin/sidekiq-pgbouncer ${SIDEKIQ_CONCURRENCY:-5} ${SIDEKIQ_QUEUE:-scheduler} diff --git a/Gemfile b/Gemfile index 37ef5d4b..598b18dc 100644 --- a/Gemfile +++ b/Gemfile @@ -1,26 +1,27 @@ source 'https://rubygems.org' -ruby '2.6.5' if ENV['DYNO'] +ruby '2.7.5' gem 'travis-config', '~> 1.1.3' -gem 'travis-lock' +gem 'travis-lock', git: 'https://github.com/travis-ci/travis-lock/', branch: '6.1' gem 'travis-metrics', git: 'https://github.com/travis-ci/travis-metrics' gem 'travis-rollout', git: 'https://github.com/travis-ci/travis-rollout' -gem 'travis-exceptions', git: 'https://github.com/travis-ci/travis-exceptions' +gem 'travis-exceptions', git: 'https://github.com/travis-ci/travis-exceptions', branch: '6.1' gem 'travis-logger', git: 'https://github.com/travis-ci/travis-logger' -gem 'travis-settings', git: 'https://github.com/travis-ci/travis-settings' -gem 'gh', git: 'https://github.com/travis-ci/gh' +gem 'travis-settings', git: 'https://github.com/travis-ci/travis-settings', branch: '6.1' +gem 'gh', git: 'https://github.com/travis-ci/gh', branch: '6.1' gem 'coder', git: 'https://github.com/rkh/coder' gem 'metriks', git: 'https://github.com/travis-ci/metriks' gem 'metriks-librato_metrics', git: 'https://github.com/travis-ci/metriks-librato_metrics' -gem 'marginalia', git: 'https://github.com/travis-ci/marginalia' +gem 'marginalia', git: 'https://github.com/travis-ci/marginalia', branch: '6.1' gem 'cl' gem 'sidekiq-pro', require: 'sidekiq-pro', source: 'https://gems.contribsys.com' +gem 'sidekiq', '~> 6.4' gem 'redis-namespace' -gem 'activerecord', '~> 4.2' +gem 'activerecord', '~> 6.1.4.6' gem 'bunny', '~> 2.9.2' gem 'pg' gem 'concurrent-ruby' @@ -40,7 +41,7 @@ end group :test do gem 'rake' gem 'database_cleaner' - gem 'factory_girl', '~> 4' + gem 'factory_girl' gem 'mocha', '~> 0.10.0' gem 'rspec' gem 'webmock' diff --git a/Gemfile.lock b/Gemfile.lock index 8a6123e5..4f510e62 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -6,22 +6,25 @@ GIT GIT remote: https://github.com/travis-ci/gh - revision: 49d4ed2bda65892e1080111ca437bf52b47a5a4b + revision: 490fbc2a663613f98a19060de7b128eb07014b2f + branch: 6.1 specs: - gh (0.15.1) - addressable (~> 2.4) - backports - faraday (~> 0.8) + gh (0.18.0) + activesupport (>= 5, < 6.2) + addressable (~> 2.8) + faraday (~> 1.0) + faraday_middleware (~> 1.0) multi_json (~> 1.0) net-http-persistent (~> 2.9) net-http-pipeline GIT remote: https://github.com/travis-ci/marginalia - revision: 46bbe301640efb5ea81828cb7deeaf874516de78 + revision: 74ff54cf200678ec2869cf7c9a0f2f7573eec93b + branch: 6.1 specs: - marginalia (1.5.0) - pg (~> 0.21) + marginalia (1.6.0) + pg (~> 1.3) GIT remote: https://github.com/travis-ci/metriks @@ -42,11 +45,19 @@ GIT GIT remote: https://github.com/travis-ci/travis-exceptions - revision: ab236981f810b820bc3ebfaf33f317bbaa9b3465 + revision: 3ed61f62d8122d1bcc512e95f85a76e20b8632a8 + branch: 6.1 specs: travis-exceptions (0.0.2) sentry-raven +GIT + remote: https://github.com/travis-ci/travis-lock/ + revision: 1dbed2874330d5d24c1dcdb9143ecf9a5eb041a9 + branch: 6.1 + specs: + travis-lock (0.1.1) + GIT remote: https://github.com/travis-ci/travis-logger revision: 765046a4746617a70395fe2200e8a91dc416f1b3 @@ -68,32 +79,37 @@ GIT GIT remote: https://github.com/travis-ci/travis-settings - revision: 0f477ddbcf852e1551d459fdda8fe2786e376495 + revision: 822fe397e99cef303fdccde90e14fc95b032811c + branch: 6.1 specs: - travis-settings (0.0.1) - activemodel + travis-settings (0.0.2) + activemodel (~> 6.1.4.6) virtus GEM - remote: https://rubygems.org/ remote: https://gems.contribsys.com/ + specs: + sidekiq-pro (3.4.0) + sidekiq (>= 4.1.5) + +GEM + remote: https://rubygems.org/ specs: HDRHistogram (0.1.3) - activemodel (4.2.11.3) - activesupport (= 4.2.11.3) - builder (~> 3.1) - activerecord (4.2.11.3) - activemodel (= 4.2.11.3) - activesupport (= 4.2.11.3) - arel (~> 6.0) - activesupport (4.2.11.3) - i18n (~> 0.7) - minitest (~> 5.1) - thread_safe (~> 0.3, >= 0.3.4) - tzinfo (~> 1.1) - addressable (2.4.0) + activemodel (6.1.4.6) + activesupport (= 6.1.4.6) + activerecord (6.1.4.6) + activemodel (= 6.1.4.6) + activesupport (= 6.1.4.6) + activesupport (6.1.4.6) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 1.6, < 2) + minitest (>= 5.1) + tzinfo (~> 2.0) + zeitwerk (~> 2.3) + addressable (2.8.0) + public_suffix (>= 2.0.2, < 5.0) amq-protocol (2.3.0) - arel (6.0.4) atomic (1.1.99) avl_tree (1.2.1) atomic (~> 1.1) @@ -101,16 +117,14 @@ GEM descendants_tracker (~> 0.0.4) ice_nine (~> 0.11.0) thread_safe (~> 0.3, >= 0.3.1) - backports (3.10.3) - builder (3.2.4) bunny (2.9.2) amq-protocol (~> 2.3.0) cl (0.0.4) coderay (1.1.2) coercible (1.0.0) descendants_tracker (~> 0.0.1) - concurrent-ruby (1.1.8) - connection_pool (2.2.1) + concurrent-ruby (1.1.9) + connection_pool (2.2.5) crack (0.4.3) safe_yaml (~> 1.0.0) database_cleaner (1.99.0) @@ -119,12 +133,34 @@ GEM diff-lcs (1.3) domain_name (0.5.20190701) unf (>= 0.0.5, < 1.0.0) - equalizer (0.0.11) excon (0.79.0) factory_girl (4.9.0) activesupport (>= 3.0.0) - faraday (0.13.1) + faraday (1.10.0) + faraday-em_http (~> 1.0) + faraday-em_synchrony (~> 1.0) + faraday-excon (~> 1.1) + faraday-httpclient (~> 1.0) + faraday-multipart (~> 1.0) + faraday-net_http (~> 1.0) + faraday-net_http_persistent (~> 1.0) + faraday-patron (~> 1.0) + faraday-rack (~> 1.0) + faraday-retry (~> 1.0) + ruby2_keywords (>= 0.0.4) + faraday-em_http (1.0.0) + faraday-em_synchrony (1.0.0) + faraday-excon (1.1.0) + faraday-httpclient (1.0.1) + faraday-multipart (1.0.3) multipart-post (>= 1.2, < 3) + faraday-net_http (1.0.1) + faraday-net_http_persistent (1.2.0) + faraday-patron (1.0.0) + faraday-rack (1.0.0) + faraday-retry (1.0.3) + faraday_middleware (1.2.0) + faraday (~> 1.0) ffi (1.15.0) ffi-compiler (1.0.1) ffi (>= 1.0.0) @@ -142,7 +178,7 @@ GEM http-form_data (2.3.0) http-parser (1.2.3) ffi-compiler (>= 1.0, < 2.0) - i18n (0.9.5) + i18n (1.10.0) concurrent-ruby (~> 1.0) ice_nine (0.11.2) libhoney (1.18.0) @@ -151,25 +187,24 @@ GEM http (>= 2.0, < 5.0) metaclass (0.0.4) method_source (0.9.0) - minitest (5.14.4) + minitest (5.15.0) mocha (0.10.5) metaclass (~> 0.0.1) multi_json (1.12.1) - multipart-post (2.0.0) + multipart-post (2.1.1) net-http-persistent (2.9.4) net-http-pipeline (1.0.1) - pg (0.21.0) + pg (1.3.3) pry (0.11.3) coderay (~> 1.1.0) method_source (~> 0.9.0) + public_suffix (4.0.6) rack (2.2.3) - rack-protection (2.0.1) - rack rake (13.0.1) - redis (3.3.5) - redis-namespace (1.5.2) - redis (~> 3.0, >= 3.0.4) - redlock (1.2.1) + redis (4.6.0) + redis-namespace (1.8.1) + redis (>= 3.0.4) + redlock (1.2.2) redis (>= 3.0.0, < 5.0) rollout (1.1.0) rspec (3.7.0) @@ -185,46 +220,43 @@ GEM diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.7.0) rspec-support (3.7.1) + ruby2_keywords (0.0.5) safe_yaml (1.0.4) - sentry-raven (2.6.3) - faraday (>= 0.7.6, < 1.0) - sidekiq (4.2.6) - concurrent-ruby (~> 1.0) - connection_pool (~> 2.2, >= 2.2.0) - rack-protection (>= 1.5.0) - redis (~> 3.2, >= 3.2.1) - sidekiq-pro (3.4.0) - sidekiq (>= 4.1.5) + sentry-raven (3.1.2) + faraday (>= 1.0) + sidekiq (6.4.1) + connection_pool (>= 2.2.2) + rack (~> 2.0) + redis (>= 4.2.0) thread_safe (0.3.6) travis-config (1.1.3) hashr (~> 2.0) - travis-lock (0.1.1) - tzinfo (1.2.9) - thread_safe (~> 0.1) + tzinfo (2.0.4) + concurrent-ruby (~> 1.0) unf (0.1.4) unf_ext unf_ext (0.0.7.7) - virtus (1.0.5) + virtus (2.0.0) axiom-types (~> 0.1) coercible (~> 1.0) descendants_tracker (~> 0.0, >= 0.0.3) - equalizer (~> 0.0, >= 0.0.9) webmock (3.0.1) addressable (>= 2.3.6) crack (>= 0.3.2) hashdiff + zeitwerk (2.5.4) PLATFORMS ruby DEPENDENCIES - activerecord (~> 4.2) + activerecord (~> 6.1.4.6) bunny (~> 2.9.2) cl coder! concurrent-ruby database_cleaner - factory_girl (~> 4) + factory_girl faraday gh! libhoney @@ -242,15 +274,19 @@ DEPENDENCIES rollout rspec sentry-raven + sidekiq (~> 6.4) sidekiq-pro! travis-config (~> 1.1.3) travis-exceptions! - travis-lock + travis-lock! travis-logger! travis-metrics! travis-rollout! travis-settings! webmock +RUBY VERSION + ruby 2.7.5p203 + BUNDLED WITH - 2.1.4 + 2.3.7 diff --git a/bin/sidekiq b/bin/sidekiq index ea751133..1ce63c16 100755 --- a/bin/sidekiq +++ b/bin/sidekiq @@ -1,12 +1,11 @@ #!/bin/bash threads=${SIDEKIQ_THREADS:-8} -index=$(echo ${DYNO:-1} | sed 's/.*\.//') queues=$@ export RUBYOPT="-Ilib $RUBYOPT" -cmd="sidekiq -r ./lib/travis/scheduler/sidekiq.rb -c $threads -i $index" +cmd="sidekiq -r ./lib/travis/scheduler/sidekiq.rb -c $threads" for queue in $queues; do cmd="$cmd -q $queue" done diff --git a/lib/travis/scheduler.rb b/lib/travis/scheduler.rb index 6803caab..97cea819 100644 --- a/lib/travis/scheduler.rb +++ b/lib/travis/scheduler.rb @@ -62,7 +62,7 @@ def logger=(logger) end def redis - @redis ||= Redis.connect(config[:redis].to_h) # TODO should be a pool, no? + @redis ||= Redis.new(config[:redis].to_h) # TODO should be a pool, no? end def ping diff --git a/lib/travis/scheduler/record/job.rb b/lib/travis/scheduler/record/job.rb index 6ba9ca69..0c0b8bd9 100644 --- a/lib/travis/scheduler/record/job.rb +++ b/lib/travis/scheduler/record/job.rb @@ -2,6 +2,10 @@ class JobConfig < ActiveRecord::Base def config=(config) self.config_json = config if has_attribute?(:config_json) super + end + + def save(arg) + super(arg) rescue Encoding::UndefinedConversionError end end @@ -14,8 +18,8 @@ class << self def queueable # sets jobs order based on priority first, ie: 5, nil, -5 - jobs = where(state: :created).order("COALESCE(priority, 0) desc").order(:id) - jobs = jobs.joins(SQL[:queueable]).order(:id) if ENV['USE_QUEUEABLE_JOBS'] + jobs = where(state: :created).order(Arel.sql("COALESCE(priority, 0) desc")).order(:id) + jobs = jobs.joins(Arel.sql(SQL[:queueable])).order(:id) if ENV['USE_QUEUEABLE_JOBS'] jobs end @@ -65,7 +69,7 @@ def owner_type belongs_to :source, polymorphic: true, autosave: true belongs_to :owner, polymorphic: true belongs_to :stage - belongs_to :config, foreign_key: :config_id, class_name: JobConfig + belongs_to :config, foreign_key: :config_id, class_name: 'JobConfig' has_one :queueable serialize :config @@ -81,7 +85,7 @@ def finished? def queueable=(value) if value - queueable || create_queueable + queueable || new_queueable else Queueable.where(job_id: id).delete_all end @@ -93,9 +97,11 @@ def public? def config record = super + record = JSON.parse(record) if record.is_a?(String) config = record&.config_json if record.respond_to?(:config_json) # TODO remove once we've rolled over config ||= record&.config config ||= read_attribute(:config) if has_attribute?(:config) + config = JSON.parse(config) if config.is_a?(String) config ||= {} config.deep_symbolize_keys! end @@ -103,4 +109,10 @@ def config def name config[:name] end + + def new_queueable + return if repository_id.blank? # to avoid trying save objects without repository_id + saved = new_record? ? save : true # saves if it is a new record + create_queueable if saved # it is allowed to create queueable records only for jobs which are persisting in the database + end end diff --git a/lib/travis/scheduler/record/repository/settings.rb b/lib/travis/scheduler/record/repository/settings.rb index 36d058df..fd665ad1 100644 --- a/lib/travis/scheduler/record/repository/settings.rb +++ b/lib/travis/scheduler/record/repository/settings.rb @@ -98,7 +98,7 @@ def custom_timeouts?(settings) validates_with TimeoutsValidator - def update_attributes!(attrs) + def update!(attrs) attrs.each { |key, value| send(:"#{key}=", value) } save end diff --git a/lib/travis/scheduler/service/enqueue_job.rb b/lib/travis/scheduler/service/enqueue_job.rb index 8a8c1775..6347eab9 100644 --- a/lib/travis/scheduler/service/enqueue_job.rb +++ b/lib/travis/scheduler/service/enqueue_job.rb @@ -22,7 +22,7 @@ def run private def set_queued - job.update_attributes!(state: :queued, queued_at: Time.now.utc) + job.update!(state: :queued, queued_at: Time.now.utc) job.queueable = false end with :set_queued, :transaction diff --git a/lib/travis/scheduler/service/notify.rb b/lib/travis/scheduler/service/notify.rb index 91d514da..2c50884f 100644 --- a/lib/travis/scheduler/service/notify.rb +++ b/lib/travis/scheduler/service/notify.rb @@ -95,7 +95,7 @@ def amqp def redirect_queue queue = redirections[job.queue] or return info MSGS[:redirect] % [job.queue, queue] - job.update_attributes!(queue: queue) + job.update!(queue: queue) end def redirections diff --git a/lib/travis/scheduler/service/set_queue.rb b/lib/travis/scheduler/service/set_queue.rb index 2de68625..2cfca912 100644 --- a/lib/travis/scheduler/service/set_queue.rb +++ b/lib/travis/scheduler/service/set_queue.rb @@ -13,7 +13,7 @@ class SetQueue < Struct.new(:context, :job, :opts) def run info MSGS[:queue] % [queue, job.id] - job.update_attributes!(queue: queue) + job.update!(queue: queue) end private diff --git a/lib/travis/service.rb b/lib/travis/service.rb index af1e2955..fc8469dc 100644 --- a/lib/travis/service.rb +++ b/lib/travis/service.rb @@ -9,7 +9,7 @@ def self.push(*args) 'queue' => ENV['SIDEKIQ_QUEUE'] || 'scheduler', 'class' => 'Travis::Scheduler::Worker', 'args' => args, - 'at' => args.last.is_a?(Hash) ? args.last.delete(:at) : nil + 'at' => args.last.is_a?(Hash) ? (args.last.delete(:at) || Time.now.to_i) : Time.now.to_i ) end end diff --git a/lib/travis/service/job_board.rb b/lib/travis/service/job_board.rb index 33a115aa..30e176b8 100644 --- a/lib/travis/service/job_board.rb +++ b/lib/travis/service/job_board.rb @@ -28,7 +28,7 @@ class JobBoard < Struct.new(:job_id, :data, :config, :logger) def post response = http.post(PATH, JSON.dump(payload)) log response.status - rescue Faraday::ClientError => e + rescue Faraday::ClientError, Faraday::ServerError => e log e.response[:status], e.response[:body] raise end diff --git a/spec/travis/scheduler/jobs_spec.rb b/spec/travis/scheduler/jobs_spec.rb index 0c52c444..fc9a319b 100644 --- a/spec/travis/scheduler/jobs_spec.rb +++ b/spec/travis/scheduler/jobs_spec.rb @@ -208,7 +208,7 @@ def subscribe(plan, owner = self.user) describe 'with an educational status, allowing 2 educational jobs' do before { config[:limit][:education] = 2 } - before { user.update_attributes!(education: true) } + before { user.update!(education: true) } describe 'with private jobs only' do before { create_jobs(1, private: true, state: :started) } @@ -349,7 +349,7 @@ def subscribe(plan, owner = self.user) describe 'with a boost of 5 and a repo settings limit 3' do before { redis.set("scheduler.owner.limit.#{user.login}", 5) } - before { repo.settings.update_attributes!(maximum_number_of_builds: 3) } + before { repo.settings.update!(maximum_number_of_builds: 3) } describe 'with private jobs only' do before { create_jobs(1, private: true, state: :started) } @@ -386,7 +386,7 @@ def subscribe(plan, owner = self.user) describe 'with a boost of 4, a two jobs plan, and a repo setting of 3' do before { subscribe(:two) } before { redis.set("scheduler.owner.limit.#{user.login}", 4) } - before { repo.settings.update_attributes!(maximum_number_of_builds: 3) } + before { repo.settings.update!(maximum_number_of_builds: 3) } describe 'with private jobs only' do before { create_jobs(1, private: true, state: :started) } diff --git a/spec/travis/scheduler/serialize/worker_spec.rb b/spec/travis/scheduler/serialize/worker_spec.rb index 956193d4..cc6a6724 100644 --- a/spec/travis/scheduler/serialize/worker_spec.rb +++ b/spec/travis/scheduler/serialize/worker_spec.rb @@ -113,7 +113,7 @@ def encrypted(value) context 'when prefer_https is set and the repo is private' do before { Travis.config.prefer_https = true } after { Travis.config.prefer_https = false } - before { repo.update_attributes!(private: true) } + before { repo.update!(private: true) } it 'sets the repo source_url to an http url' do expect(data[:repository][:source_url]).to eq 'https://github.com/svenfuchs/gem-release.git' @@ -124,7 +124,7 @@ def encrypted(value) let!(:installation) { FactoryGirl.create(:installation, github_id: rand(1000), owner_id: repo.owner_id, owner_type: repo.owner_type) } describe 'on a private repo with a custom ssh key' do - before { repo.update_attributes!(private: true, managed_by_installation_at: Time.now) } + before { repo.update!(private: true, managed_by_installation_at: Time.now) } before { repo.settings.ssh_key = { value: 'settings key' } } it 'sets the repo source_url to an ssh git url' do @@ -137,7 +137,7 @@ def encrypted(value) end describe 'on a private repo' do - before { repo.update_attributes!(private: true, managed_by_installation_at: Time.now) } + before { repo.update!(private: true, managed_by_installation_at: Time.now) } it 'sets the repo source_url to an http url' do expect(data[:repository][:source_url]).to eq 'https://github.com/svenfuchs/gem-release.git' @@ -149,7 +149,7 @@ def encrypted(value) end describe 'on a public repo' do - before { repo.update_attributes!(private: false, managed_by_installation_at: Time.now) } + before { repo.update!(private: false, managed_by_installation_at: Time.now) } it 'sets the repo source_url to an http url' do expect(data[:repository][:source_url]).to eq 'https://github.com/svenfuchs/gem-release.git' @@ -208,7 +208,7 @@ def encrypted(value) let(:payload) { { 'pull_request' => { 'head' => { 'ref' => 'head_branch', 'sha' => '62aaef', 'repo' => {'full_name' => 'travis-ci/gem-release'} } } } } let(:pull_request) { PullRequest.create(head_ref: 'head_branch', head_repo_slug: 'travis-ci/gem-release') } - before { request.update_attributes(pull_request: pull_request, base_commit: '0cd9ff', head_commit: '62aaef') } + before { request.update(pull_request: pull_request, base_commit: '0cd9ff', head_commit: '62aaef') } it 'data' do expect(data).to eq( @@ -408,12 +408,12 @@ def encrypted(value) describe 'outside enterprise' do describe 'on a public repo' do - before { repo.update_attributes!(private: false) } + before { repo.update!(private: false) } include_examples 'does not include an ssh key' end describe 'on a private repo' do - before { repo.update_attributes!(private: true) } + before { repo.update!(private: true) } include_examples 'includes an ssh key' end end @@ -422,12 +422,12 @@ def encrypted(value) before { config[:enterprise] = true } describe 'on a public repo' do - before { repo.update_attributes!(private: false) } + before { repo.update!(private: false) } include_examples 'includes an ssh key' end describe 'on a private repo' do - before { repo.update_attributes!(private: true) } + before { repo.update!(private: true) } include_examples 'includes an ssh key' end end @@ -439,12 +439,12 @@ def encrypted(value) end describe 'preference set to true' do - before { repo.owner.update_attributes(preferences: { keep_netrc: true }) } + before { repo.owner.update(preferences: { keep_netrc: true }) } it { expect(data[:keep_netrc]).to be true } end describe 'preference set to false' do - before { repo.owner.update_attributes(preferences: { keep_netrc: false }) } + before { repo.owner.update(preferences: { keep_netrc: false }) } it { expect(data[:keep_netrc]).to be false } end end diff --git a/spec/travis/scheduler/service/notify_spec.rb b/spec/travis/scheduler/service/notify_spec.rb index c3d2cfc0..0bc97f8b 100644 --- a/spec/travis/scheduler/service/notify_spec.rb +++ b/spec/travis/scheduler/service/notify_spec.rb @@ -129,7 +129,9 @@ def rescueing let(:status) { 500 } let(:body) { nil } - include_examples 'raises' + it 'raises' do + expect { service.run }.to raise_error(Faraday::ServerError) + end it 'logs' do rescueing { service.run } @@ -169,7 +171,7 @@ def rescueing describe 'does not raise on encoding issues ("\xC3" from ASCII-8BIT to UTF-8)' do let(:config) { { global_env: ["SECURE GH_USER_NAME=Max Nöthe".force_encoding('ASCII-8BIT')] } } - before { job.update_attributes!(config: config) } + before { job.update!(config: config) } it { expect { service.run }.to_not raise_error } end end From 8d1299b8d942c7f1bff76cf60241af80d9a51c58 Mon Sep 17 00:00:00 2001 From: gabriel-arc Date: Thu, 19 May 2022 11:24:09 +0200 Subject: [PATCH 61/81] using base repo key in forks if no custom key is available in base --- lib/travis/scheduler/serialize/worker.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/travis/scheduler/serialize/worker.rb b/lib/travis/scheduler/serialize/worker.rb index 5c2bf870..e4217cf6 100644 --- a/lib/travis/scheduler/serialize/worker.rb +++ b/lib/travis/scheduler/serialize/worker.rb @@ -149,8 +149,11 @@ def ssh_key_repository return job.repository if base_repo_owner_name.nil? || base_repo_owner_name.empty? || base_repo_name.nil? || base_repo_name.empty? base_repo = ::Repository.find_by(owner_name: base_repo_owner_name, name: base_repo_name) return job.repository if base_repo.nil? + return base_repo if base_repo.settings.share_ssh_keys_with_forks? + return base_repo unless base_repo.settings.ssh_key + head_repo_owner_name, head_repo_name = job.source.request.pull_request.head_repo_slug.to_s.split('/') return job.repository if head_repo_owner_name.nil? || head_repo_owner_name.empty? || head_repo_name.nil? || head_repo_name.empty? From b5ff0df43b25576efdf238d26a8da1d262f5b8af Mon Sep 17 00:00:00 2001 From: gabriel-arc Date: Thu, 19 May 2022 11:27:04 +0200 Subject: [PATCH 62/81] removed trivy --- Makefile | 1 - 1 file changed, 1 deletion(-) diff --git a/Makefile b/Makefile index c738b84a..98d2725a 100644 --- a/Makefile +++ b/Makefile @@ -48,7 +48,6 @@ docker-push-latest-master: docker-push-branch: $(DOCKER) tag $(DOCKER_DEST) $(QUAY_IMAGE):$(VERSION_VALUE)-$(BRANCH) $(DOCKER) push $(QUAY_IMAGE):$(VERSION_VALUE)-$(BRANCH) - $(DOCKER) run --rm -v /tmp:/root/.cache/ -v /var/run/docker.sock:/var/run/docker.sock aquasec/trivy --ignore-unfixed $(QUAY_IMAGE):$(VERSION_VALUE)-$(BRANCH) .PHONY: ship ship: docker-build docker-login From 31ac5bac955ea819f34ab63c46955446f29e11fa Mon Sep 17 00:00:00 2001 From: gabriel-arc Date: Thu, 19 May 2022 12:14:58 +0200 Subject: [PATCH 63/81] fixed ssh key specs --- .../travis/scheduler/serialize/worker_spec.rb | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/spec/travis/scheduler/serialize/worker_spec.rb b/spec/travis/scheduler/serialize/worker_spec.rb index cc6a6724..e64fed9e 100644 --- a/spec/travis/scheduler/serialize/worker_spec.rb +++ b/spec/travis/scheduler/serialize/worker_spec.rb @@ -362,14 +362,37 @@ def encrypted(value) repo.update(created_at: Time.now) end + it 'returns keys from the base repo' do + expect(data[:ssh_key][:value]).to eq(repo.key.private_key) + end + end + + context 'when repo is not private but contains custom key' do + before do + repo.update(private: false) + repo.update(created_at: Time.now) + repo.update(settings: {ssh_key: {value: 'settings key'}}) + end + it 'returns keys from the head repo' do expect(data[:ssh_key][:value]).to eq(head_repo.key.private_key) end end - context 'when not sharing SSH keys with forks' do + context 'when not sharing SSH keys with forks but no custom key defined' do before { repo.update(created_at: Time.now) } + it 'returns keys from the base repo' do + expect(data[:ssh_key][:value]).to eq(repo.key.private_key) + end + end + + context 'when not sharing SSH keys with forks and base repo has a custom key' do + before { + repo.update(created_at: Time.now) + repo.update(settings: {ssh_key: {value: 'settings key'}}) + } + it 'returns keys from the head repo' do expect(data[:ssh_key][:value]).to eq(head_repo.key.private_key) end From 58dbb78561c9ca196fd3f499125a8f8f45003d66 Mon Sep 17 00:00:00 2001 From: gabriel-arc Date: Wed, 8 Jun 2022 11:18:14 +0200 Subject: [PATCH 64/81] rack update -> 2.2.3.1 --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 4f510e62..b081cf73 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -199,7 +199,7 @@ GEM coderay (~> 1.1.0) method_source (~> 0.9.0) public_suffix (4.0.6) - rack (2.2.3) + rack (2.2.3.1) rake (13.0.1) redis (4.6.0) redis-namespace (1.8.1) From f31e9a3fca75bb229c1c48d2a7cdb8ada20ea37d Mon Sep 17 00:00:00 2001 From: gabriel-arc Date: Mon, 22 Aug 2022 11:57:41 +0200 Subject: [PATCH 65/81] activerecord bump to 6.1.6.1 --- Gemfile | 2 +- Gemfile.lock | 28 ++++++++++++++-------------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Gemfile b/Gemfile index 598b18dc..e7b8f64e 100644 --- a/Gemfile +++ b/Gemfile @@ -21,7 +21,7 @@ gem 'cl' gem 'sidekiq-pro', require: 'sidekiq-pro', source: 'https://gems.contribsys.com' gem 'sidekiq', '~> 6.4' gem 'redis-namespace' -gem 'activerecord', '~> 6.1.4.6' +gem 'activerecord', '~> 6.1.6.1' gem 'bunny', '~> 2.9.2' gem 'pg' gem 'concurrent-ruby' diff --git a/Gemfile.lock b/Gemfile.lock index b081cf73..b4cc8abc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -79,11 +79,11 @@ GIT GIT remote: https://github.com/travis-ci/travis-settings - revision: 822fe397e99cef303fdccde90e14fc95b032811c + revision: 9d6f936fd5eb3431162d82783e7ca3f64d22db95 branch: 6.1 specs: travis-settings (0.0.2) - activemodel (~> 6.1.4.6) + activemodel (~> 6.1.6.1) virtus GEM @@ -96,12 +96,12 @@ GEM remote: https://rubygems.org/ specs: HDRHistogram (0.1.3) - activemodel (6.1.4.6) - activesupport (= 6.1.4.6) - activerecord (6.1.4.6) - activemodel (= 6.1.4.6) - activesupport (= 6.1.4.6) - activesupport (6.1.4.6) + activemodel (6.1.6.1) + activesupport (= 6.1.6.1) + activerecord (6.1.6.1) + activemodel (= 6.1.6.1) + activesupport (= 6.1.6.1) + activesupport (6.1.6.1) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -123,7 +123,7 @@ GEM coderay (1.1.2) coercible (1.0.0) descendants_tracker (~> 0.0.1) - concurrent-ruby (1.1.9) + concurrent-ruby (1.1.10) connection_pool (2.2.5) crack (0.4.3) safe_yaml (~> 1.0.0) @@ -178,7 +178,7 @@ GEM http-form_data (2.3.0) http-parser (1.2.3) ffi-compiler (>= 1.0, < 2.0) - i18n (1.10.0) + i18n (1.12.0) concurrent-ruby (~> 1.0) ice_nine (0.11.2) libhoney (1.18.0) @@ -187,7 +187,7 @@ GEM http (>= 2.0, < 5.0) metaclass (0.0.4) method_source (0.9.0) - minitest (5.15.0) + minitest (5.16.3) mocha (0.10.5) metaclass (~> 0.0.1) multi_json (1.12.1) @@ -231,7 +231,7 @@ GEM thread_safe (0.3.6) travis-config (1.1.3) hashr (~> 2.0) - tzinfo (2.0.4) + tzinfo (2.0.5) concurrent-ruby (~> 1.0) unf (0.1.4) unf_ext @@ -244,13 +244,13 @@ GEM addressable (>= 2.3.6) crack (>= 0.3.2) hashdiff - zeitwerk (2.5.4) + zeitwerk (2.6.0) PLATFORMS ruby DEPENDENCIES - activerecord (~> 6.1.4.6) + activerecord (~> 6.1.6.1) bunny (~> 2.9.2) cl coder! From 0152ef2f4d719f8919fa35c28a9ca080a342fc40 Mon Sep 17 00:00:00 2001 From: gabriel-arc <57348209+GbArc@users.noreply.github.com> Date: Wed, 5 Oct 2022 14:09:52 +0200 Subject: [PATCH 66/81] image size reduction (#283) --- Dockerfile | 43 +++++++++++++++++++------------------------ Makefile | 2 +- 2 files changed, 20 insertions(+), 25 deletions(-) diff --git a/Dockerfile b/Dockerfile index e075ebc2..4114dc21 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,36 +2,31 @@ FROM ruby:2.7.5-slim LABEL maintainer Travis CI GmbH -# packages required for bundle install +RUN ( \ + bundle config set no-cache 'true'; \ + bundle config --global frozen 1; \ + bundle config set deployment 'true'; \ + mkdir -p /app; \ +) +WORKDIR /app +COPY Gemfile* /app/ +ARG bundle_gems__contribsys__com RUN ( \ apt-get update ; \ - # update to deb 10.8 apt-get upgrade -y ; \ apt-get install -y --no-install-recommends git make gcc g++ libpq-dev libjemalloc-dev \ - && rm -rf /var/lib/apt/lists/* \ + && rm -rf /var/lib/apt/lists/*; \ + gem install bundler -v '2.3.7'; \ + bundle config https://gems.contribsys.com/ $bundle_gems__contribsys__com; \ + bundle config set without 'development test'; \ + bundle install; \ + bundle config --delete https://gems.contribsys.com; \ + apt-get remove -y gcc g++ make git perl && apt-get -y autoremove; \ + bundle clean && rm -rf /app/vendor/bundle/ruby/2.7.0/cache/*; \ + for i in `find /app/vendor/ -name \*.o -o -name \*.c -o -name \*.h`; do rm -f $i; done; \ ) ENV LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2 - -# throw errors if Gemfile has been modified since Gemfile.lock -RUN bundle config --global frozen 1 -RUN bundle config set deployment 'true' - -RUN mkdir -p /app -WORKDIR /app - -COPY Gemfile /app -COPY Gemfile.lock /app - -RUN gem install bundler -v '2.3.7' - -ARG bundle_gems__contribsys__com -RUN bundle config https://gems.contribsys.com/ $bundle_gems__contribsys__com \ - && bundle config set without 'development test'\ - && bundle install\ - && bundle config --delete https://gems.contribsys.com/ -RUN gem install --user-install executable-hooks COPY . /app - -CMD bundle exec bin/sidekiq-pgbouncer ${SIDEKIQ_CONCURRENCY:-5} ${SIDEKIQ_QUEUE:-scheduler} +CMD ["bundle", "exec", "bin/sidekiq-pgbouncer", "${SIDEKIQ_CONCURRENCY:-5}", "${SIDEKIQ_QUEUE:-scheduler}"] diff --git a/Makefile b/Makefile index 98d2725a..6781449b 100644 --- a/Makefile +++ b/Makefile @@ -31,7 +31,7 @@ DOCKER ?= docker .PHONY: docker-build docker-build: - $(DOCKER) build --build-arg bundle_gems__contribsys__com=$(BUNDLE_GEMS__CONTRIBSYS__COM) -t $(DOCKER_DEST) . + $(DOCKER) build --no-cache --pull --build-arg bundle_gems__contribsys__com=$(BUNDLE_GEMS__CONTRIBSYS__COM) -t $(DOCKER_DEST) . .PHONY: docker-login docker-login: From 022eff39278229551c0da8cb9233d05f21304e08 Mon Sep 17 00:00:00 2001 From: gabriel-arc <57348209+GbArc@users.noreply.github.com> Date: Wed, 14 Dec 2022 16:04:45 +0100 Subject: [PATCH 67/81] merge from master (#288) * merge from master 14.11.22 * ship:docker * bundler updated to 2.3.24 --- Dockerfile | 3 +- Gemfile.lock | 2 +- lib/travis/amqp/publisher.rb | 2 + lib/travis/queue/matcher.rb | 14 +- lib/travis/scheduler.rb | 1 + lib/travis/scheduler/billing.rb | 8 + lib/travis/scheduler/billing/client.rb | 86 ++++++++++ lib/travis/scheduler/config.rb | 48 +++--- lib/travis/scheduler/jobs/capacity/base.rb | 26 +++ lib/travis/scheduler/jobs/capacity/boost.rb | 2 +- lib/travis/scheduler/jobs/capacity/config.rb | 2 +- .../scheduler/jobs/capacity/education.rb | 2 +- lib/travis/scheduler/jobs/capacity/plan.rb | 26 ++- lib/travis/scheduler/jobs/capacity/public.rb | 2 +- lib/travis/scheduler/jobs/capacity/trial.rb | 2 +- lib/travis/scheduler/record/organization.rb | 24 ++- lib/travis/scheduler/record/user.rb | 30 +++- lib/travis/scheduler/serialize/worker.rb | 37 ++++- .../scheduler/serialize/worker/build.rb | 6 +- .../serialize/worker/config/decrypt.rb | 6 + lib/travis/scheduler/serialize/worker/job.rb | 6 +- lib/travis/scheduler/serialize/worker/repo.rb | 12 +- lib/travis/scheduler/service/notify.rb | 3 +- lib/travis/scheduler/vcs_proxy.rb | 80 +++++++++ spec/support/factories.rb | 1 + spec/travis/queue_spec.rb | 75 ++++++++- spec/travis/scheduler/jobs_spec.rb | 157 ++++++++++++++++++ .../scheduler/record/organization_spec.rb | 20 +++ spec/travis/scheduler/record/user_spec.rb | 23 ++- .../scheduler/serialize/worker/config_spec.rb | 10 ++ .../scheduler/serialize/worker/job_spec.rb | 55 ++++++ .../scheduler/serialize/worker/repo_spec.rb | 12 ++ .../travis/scheduler/serialize/worker_spec.rb | 16 +- .../scheduler/service/enqueue_owners_spec.rb | 6 + spec/travis/scheduler/service/event_spec.rb | 6 + spec/travis/scheduler/service/notify_spec.rb | 6 + 36 files changed, 763 insertions(+), 54 deletions(-) create mode 100644 lib/travis/scheduler/billing.rb create mode 100644 lib/travis/scheduler/billing/client.rb create mode 100644 lib/travis/scheduler/vcs_proxy.rb diff --git a/Dockerfile b/Dockerfile index 4114dc21..4b9918f8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,7 +16,8 @@ RUN ( \ apt-get upgrade -y ; \ apt-get install -y --no-install-recommends git make gcc g++ libpq-dev libjemalloc-dev \ && rm -rf /var/lib/apt/lists/*; \ - gem install bundler -v '2.3.7'; \ + gem update --system; \ + gem install bundler -v '2.3.24'; \ bundle config https://gems.contribsys.com/ $bundle_gems__contribsys__com; \ bundle config set without 'development test'; \ bundle install; \ diff --git a/Gemfile.lock b/Gemfile.lock index b4cc8abc..2d6a7d76 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -289,4 +289,4 @@ RUBY VERSION ruby 2.7.5p203 BUNDLED WITH - 2.3.7 + 2.3.24 diff --git a/lib/travis/amqp/publisher.rb b/lib/travis/amqp/publisher.rb index 3eb2c2dd..b7152558 100644 --- a/lib/travis/amqp/publisher.rb +++ b/lib/travis/amqp/publisher.rb @@ -21,6 +21,8 @@ def initialize(routing_key, options = {}) end def publish(data, options = {}) + return unless data + Amqp.logger.warn "Queue #{routing_key} doesn't exist!" if ENV['AMQP_QUEUE_VALIDATION'] && !Amqp.connection.queue_exists?(routing_key) data = MultiJson.encode(data) exchange.publish(data, deep_merge(default_data, options)) diff --git a/lib/travis/queue/matcher.rb b/lib/travis/queue/matcher.rb index a67a03b9..32dfb8b8 100644 --- a/lib/travis/queue/matcher.rb +++ b/lib/travis/queue/matcher.rb @@ -2,13 +2,13 @@ module Travis class Queue class Matcher < Struct.new(:job, :config, :logger) KEYS = %i(slug owner os language sudo dist group osx_image percentage - resources services arch virt paid) + resources services arch virt paid vm_size repo_private) MSGS = { unknown_matchers: 'unknown matchers used for queue %s: %s (repo=%s)"' } - OSS_ONLY_ARCH = %w(arm64 ppc64le s390x) + OSS_ONLY_ARCH = %w(arm64 s390x) def matches?(attrs) check_unknown_matchers(attrs.keys) @@ -17,6 +17,8 @@ def matches?(attrs) attr_val = attrs[key] if attr_val.is_a?(Array) (attr_val & [value].flatten).any? + elsif key == 'repo_private' # Special case + attr_val.nil? || value === attr_val else value === attr_val end @@ -33,6 +35,10 @@ def paid job.paid? end + def repo_private + job.private? + end + def slug repo.slug end @@ -91,6 +97,10 @@ def virt job.config[:virt] end + def vm_size + job.config[:vm][:size] if job.config[:vm] + end + def resources_enabled? Travis::Features.active?(:vm_config, repo) end diff --git a/lib/travis/scheduler.rb b/lib/travis/scheduler.rb index 97cea819..f1a58d37 100644 --- a/lib/travis/scheduler.rb +++ b/lib/travis/scheduler.rb @@ -13,6 +13,7 @@ require 'travis/scheduler/support/features' require 'travis/scheduler/support/sidekiq' require 'travis/scheduler/worker' +require 'travis/scheduler/billing' require 'travis/service' require 'travis/support/database' require 'marginalia' diff --git a/lib/travis/scheduler/billing.rb b/lib/travis/scheduler/billing.rb new file mode 100644 index 00000000..2567e130 --- /dev/null +++ b/lib/travis/scheduler/billing.rb @@ -0,0 +1,8 @@ +require 'travis/scheduler/billing/client' + +module Travis + module Scheduler + module Billing + end + end +end diff --git a/lib/travis/scheduler/billing/client.rb b/lib/travis/scheduler/billing/client.rb new file mode 100644 index 00000000..a99f96c8 --- /dev/null +++ b/lib/travis/scheduler/billing/client.rb @@ -0,0 +1,86 @@ +require 'faraday_middleware' + +module Travis + module Scheduler + module Billing + class Client + class Error < StandardError + attr_reader :response + + def initialize(msg, response) + super(msg) + @response = response || {} + end + end + + DEFAULT_HEADERS = { + 'User-Agent' => 'Travis-CI-Scheduler/Faraday', + 'Accept' => 'application/json', + 'Content-Type' => 'application/json' + } + + RETRY = { + max: 5, + interval: 0.05, + interval_randomness: 0.5, + backoff_factor: 2, + retry_statuses: [500, 502, 503, 504], + exceptions: [ + Errno::ETIMEDOUT, + Timeout::Error, + Faraday::RetriableResponse, + Faraday::TimeoutError, + Zlib::DataError, + Zlib::BufError, + ] + } + + attr_reader :context + + def initialize(context) + @context = context + end + + def allowance(owner_class, owner_id) + get("/usage/#{owner_class.pluralize}/#{owner_id}/allowance") + end + + def get_plan(owner) + get("/#{owner.class.name.downcase.pluralize}/#{owner.id}/plan") + end + + private + + def request(method, path, params) + client.send(method, path, params) + rescue Faraday::ClientError => e + Travis.logger.error("New-plan-error: #{e}, Method: #{method}, path: #{path}, Params: #{params}") + raise Error.new(e, e.response) + end + + def get(path, params = {}) + request(:get, path, params).body + end + + def post(path, params = {}) + request(:post, path, params).body + end + + def client + Faraday.new(url: config.billing.url, headers: DEFAULT_HEADERS) do |c| + c.basic_auth '_', config.billing.auth_key + c.request :retry, RETRY + c.request :json + c.response :json + c.response :raise_error + c.adapter :net_http + end + end + + def config + context.config + end + end + end + end +end diff --git a/lib/travis/scheduler/config.rb b/lib/travis/scheduler/config.rb index b1c55844..d3d4e509 100644 --- a/lib/travis/scheduler/config.rb +++ b/lib/travis/scheduler/config.rb @@ -3,29 +3,31 @@ module Travis module Scheduler class Config < Travis::Config - define amqp: { username: 'guest', password: 'guest', host: 'localhost', prefetch: 1 }, - database: { adapter: 'postgresql', database: "travis_#{env}", encoding: 'unicode', min_messages: 'warning' }, - delegate: { }, - encryption: { key: SecureRandom.hex(64) }, - enterprise: false, - github: { api_url: 'https://api.github.com', source_host: 'github.com' }, - host: 'https://travis-ci.com', - interval: 2, - limit: { public: 93939, education: 92929, default: 91919, by_owner: {}, delegate: {} }, - lock: { strategy: :redis, ttl: 150 }, - logger: { time_format: false, process_id: false, thread_id: false }, - log_level: :info, - metrics: { reporter: 'librato' }, - plans: { }, - queue: { default: 'builds.gce', redirect: {} }, - queues: [ queue: 'name', os: 'os', dist: 'dist', group: 'group', sudo: false, osx_image: 'osx_image', language: 'language', owner: 'owner', slug: 'slug', services: ['service']], - redis: { url: 'redis://localhost:6379' }, - sentry: { }, - sidekiq: { namespace: 'sidekiq', pool_size: 3, log_level: :warn }, - ping: { interval: 5 * 60 }, - site: ENV['TRAVIS_SITE'] || 'org', - ssl: { }, - job_board: { url: ENV['JOB_BOARD_URL'] || 'https://job-board.travis-ci.org', auth: ENV['JOB_BOARD_AUTH'] || 'user:pass' } + define amqp: { username: 'guest', password: 'guest', host: 'localhost', prefetch: 1 }, + database: { adapter: 'postgresql', database: "travis_#{env}", encoding: 'unicode', min_messages: 'warning' }, + delegate: { }, + encryption: { key: SecureRandom.hex(64) }, + enterprise: false, + github: { api_url: 'https://api.github.com', source_host: 'github.com' }, + billing: { url: 'http://localhost:9292/', auth_key: 'auth_keys' }, + host: 'https://travis-ci.com', + interval: 2, + limit: { public: 93939, education: 92929, default: 91919, by_owner: {}, delegate: {} }, + lock: { strategy: :redis, ttl: 150 }, + logger: { time_format: false, process_id: false, thread_id: false }, + log_level: :info, + metrics: { reporter: 'librato' }, + plans: { }, + queue: { default: 'builds.gce', redirect: {} }, + queues: [ queue: 'name', os: 'os', dist: 'dist', group: 'group', sudo: false, osx_image: 'osx_image', language: 'language', owner: 'owner', slug: 'slug', services: ['service']], + redis: { url: 'redis://localhost:6379' }, + sentry: { }, + sidekiq: { namespace: 'sidekiq', pool_size: 3, log_level: :warn }, + ping: { interval: 5 * 60 }, + site: ENV['TRAVIS_SITE'] || 'org', + ssl: { }, + job_board: { url: ENV['JOB_BOARD_URL'] || 'https://job-board.travis-ci.org', auth: ENV['JOB_BOARD_AUTH'] || 'user:pass' }, + vcs_proxy_api: { url: 'http://vcs_proxy_api' } def metrics # TODO fix keychain? diff --git a/lib/travis/scheduler/jobs/capacity/base.rb b/lib/travis/scheduler/jobs/capacity/base.rb index ecf5857e..4465ece1 100644 --- a/lib/travis/scheduler/jobs/capacity/base.rb +++ b/lib/travis/scheduler/jobs/capacity/base.rb @@ -3,6 +3,8 @@ module Scheduler module Jobs module Capacity class Base < Struct.new(:context, :owners, :capacities) + include Helper::Memoize + def reduce(count) @reduced = count [count - max, 0].max.tap do |rest| @@ -80,6 +82,30 @@ def report(status, job) def config context.config end + + def billing_client + @billing_client ||= Billing::Client.new(context) + end + + def billing_allowance + owner = owners.first + owner_class = owner.is_a?(User) ? 'users' : 'organizations' + + billing_client.allowance(owner_class, owner.id) + rescue Billing::Client::Error => e + if e.response[:status] == 404 # Owner is not on a metered plan + return {} + end + + raise e + end + memoize :billing_allowance + + def on_metered_plan? + return false unless config.com? + + billing_allowance.present? + end end end end diff --git a/lib/travis/scheduler/jobs/capacity/boost.rb b/lib/travis/scheduler/jobs/capacity/boost.rb index 5052cf64..44e8447e 100644 --- a/lib/travis/scheduler/jobs/capacity/boost.rb +++ b/lib/travis/scheduler/jobs/capacity/boost.rb @@ -6,7 +6,7 @@ module Jobs module Capacity class Boost < Base def applicable? - boost.exists? + !on_metered_plan? && boost.exists? end def report(status, job) diff --git a/lib/travis/scheduler/jobs/capacity/config.rb b/lib/travis/scheduler/jobs/capacity/config.rb index 2a9c3175..7ec7c231 100644 --- a/lib/travis/scheduler/jobs/capacity/config.rb +++ b/lib/travis/scheduler/jobs/capacity/config.rb @@ -6,7 +6,7 @@ module Capacity # richer config format? or can we get rid of it on com? class Config < Base def applicable? - owners.logins.any? { |login| max_for(login) } + !on_metered_plan? && owners.logins.any? { |login| max_for(login) } end def report(status, job) diff --git a/lib/travis/scheduler/jobs/capacity/education.rb b/lib/travis/scheduler/jobs/capacity/education.rb index 16fb6a4d..2e12cdd1 100644 --- a/lib/travis/scheduler/jobs/capacity/education.rb +++ b/lib/travis/scheduler/jobs/capacity/education.rb @@ -8,7 +8,7 @@ class Education < Base include Helper::Memoize def applicable? - com? && educational? + !on_metered_plan? && com? && educational? end def accept?(job) diff --git a/lib/travis/scheduler/jobs/capacity/plan.rb b/lib/travis/scheduler/jobs/capacity/plan.rb index ac004571..a95c407a 100644 --- a/lib/travis/scheduler/jobs/capacity/plan.rb +++ b/lib/travis/scheduler/jobs/capacity/plan.rb @@ -4,17 +4,39 @@ module Jobs module Capacity class Plan < Base def applicable? - owners.subscribed? + on_metered_plan? || owners.subscribed? end def report(status, job) super.merge(max: max) end + def accept?(job) + super if !on_metered_plan? || billing_allowed?(job) + end + private def max - @max ||= owners.paid_capacity + @max ||= on_metered_plan? ? billing_allowance['concurrency_limit'] : owners.paid_capacity + end + + def billing_allowed?(job) + puts billing_allowance[allowance_key(job)] + return true if billing_allowance[allowance_key(job)] + + # Cancel job if it has not been queued for more than a day due to + # billing allowance + if job.created_at < (Time.now - 1.day) + payload = { id: job.id, source: 'scheduler' } + Hub.push('job:cancel', payload) + end + + false + end + + def allowance_key(job) + job.public? ? 'public_repos' : 'private_repos' end end end diff --git a/lib/travis/scheduler/jobs/capacity/public.rb b/lib/travis/scheduler/jobs/capacity/public.rb index dbcd567f..f7453a6f 100644 --- a/lib/travis/scheduler/jobs/capacity/public.rb +++ b/lib/travis/scheduler/jobs/capacity/public.rb @@ -4,7 +4,7 @@ module Jobs module Capacity class Public < Base def applicable? - true + !on_metered_plan? end def reduce(jobs) diff --git a/lib/travis/scheduler/jobs/capacity/trial.rb b/lib/travis/scheduler/jobs/capacity/trial.rb index 27a3914a..044d886c 100644 --- a/lib/travis/scheduler/jobs/capacity/trial.rb +++ b/lib/travis/scheduler/jobs/capacity/trial.rb @@ -6,7 +6,7 @@ class Trial < Base include Helper::Memoize def applicable? - com? && active? + !on_metered_plan? && com? && active? end def accept?(job) diff --git a/lib/travis/scheduler/record/organization.rb b/lib/travis/scheduler/record/organization.rb index ca9c592b..327f85a6 100644 --- a/lib/travis/scheduler/record/organization.rb +++ b/lib/travis/scheduler/record/organization.rb @@ -25,13 +25,25 @@ def educational? end def paid? - subscribed? || active_trial? + subscribed? || active_trial? || paid_new_plan? end def active_trial? redis.get("trial:#{login}").to_i > 0 end + def paid_new_plan? + redis_key = "organization:#{self.id}:plan" + plan = if redis.exists?(redis_key) + JSON.parse(redis.get(redis_key)) + else + billing_client.get_plan(self).to_h + end + return false if plan[:error] || plan["plan_name"].nil? + + plan["hybrid"] || !plan["plan_name"].include?('free') + end + def default_worker_timeout # When the user is a paid user ("subscribed") or has an active trial, they # are granted a different default timeout on their jobs. @@ -41,8 +53,10 @@ def default_worker_timeout # following weeks/months. # if paid? || educational? + Travis.logger.info "Default Timeout: DEFAULT_SUBSCRIBED_TIMEOUT for owner=#{id}" DEFAULT_SUBSCRIBED_TIMEOUT else + Travis.logger.info "Default Timeout: DEFAULT_SPONSORED_TIMEOUT for owner=#{id}" DEFAULT_SPONSORED_TIMEOUT end end @@ -64,4 +78,12 @@ def uid def redis Travis::Scheduler.context.redis end + + def billing_client + @billing_client ||= Travis::Scheduler::Billing::Client.new(context) + end + + def context + Travis::Scheduler.context + end end diff --git a/lib/travis/scheduler/record/user.rb b/lib/travis/scheduler/record/user.rb index 9202c7a1..eb4f98e1 100644 --- a/lib/travis/scheduler/record/user.rb +++ b/lib/travis/scheduler/record/user.rb @@ -34,7 +34,23 @@ def educational? end def paid? - subscribed? || active_trial? + subscribed? || active_trial? || paid_new_plan? + end + + def paid_new_plan? + redis_key = "user:#{self.id}:plan" + plan = if redis.exists?(redis_key) + JSON.parse(redis.get(redis_key)) + else + billing_client.get_plan(self).to_h + end + return false if plan[:error] || plan["plan_name"].nil? + + plan["hybrid"] || !plan["plan_name"].include?('free') + end + + def enterprise? + !!context.config[:enterprise] end def default_worker_timeout @@ -45,9 +61,11 @@ def default_worker_timeout # those enforced by workers themselves, but we plan to sometime in the # following weeks/months. # - if paid? || educational? + if enterprise? || paid? || educational? + Travis.logger.info "Default Timeout: DEFAULT_SUBSCRIBED_TIMEOUT for owner=#{id}" DEFAULT_SUBSCRIBED_TIMEOUT else + Travis.logger.info "Default Timeout: DEFAULT_SPONSORED_TIMEOUT for owner=#{id}" DEFAULT_SPONSORED_TIMEOUT end end @@ -69,4 +87,12 @@ def uid def redis Travis::Scheduler.context.redis end + + def billing_client + @billing_client ||= Travis::Scheduler::Billing::Client.new(context) + end + + def context + Travis::Scheduler.context + end end diff --git a/lib/travis/scheduler/serialize/worker.rb b/lib/travis/scheduler/serialize/worker.rb index e4217cf6..8dcd708f 100644 --- a/lib/travis/scheduler/serialize/worker.rb +++ b/lib/travis/scheduler/serialize/worker.rb @@ -9,12 +9,14 @@ class Worker < Struct.new(:job, :config) require 'travis/scheduler/serialize/worker/request' require 'travis/scheduler/serialize/worker/repo' require 'travis/scheduler/serialize/worker/ssh_key' + require 'travis/scheduler/vcs_proxy' def data data = { type: :test, vm_config: job.vm_config, vm_type: repo.vm_type, + vm_size: job.vm_size, queue: job.queue, config: job.decrypted_config, env_vars: job.env_vars, @@ -35,7 +37,16 @@ def data data[:trace] = true if job.trace? data[:warmer] = true if job.warmer? data[:oauth_token] = github_oauth_token if config[:prefer_https] + + if travis_vcs_proxy? + creds = build_credentials + data[:build_token] = (creds['token'] if creds) || '' + data[:sender_login] = (creds['username'] if creds) || '' + end + data + rescue Exception => e + puts "ex: #{e.message}" end private @@ -103,12 +114,14 @@ def repository_data last_build_state: repo.last_build_state.to_s, default_branch: repo.default_branch, description: repo.description, + server_type: repo.server_type || 'git', ) end def source_url # TODO move these things to Build - return repo.source_git_url if repo.private? && ssh_key&.custom? + return repo.source_git_url if repo.private? && ssh_key&.custom? && !travis_vcs_proxy? + repo.source_url end @@ -161,6 +174,9 @@ def ssh_key_repository end def source_host + return URI(URI::Parser.new.escape repo.vcs_source_host)&.host if travis_vcs_proxy? + repo.vcs_source_host || config[:github][:source_host] || 'github.com' + rescue Exception => e repo.vcs_source_host || config[:github][:source_host] || 'github.com' end @@ -199,6 +215,21 @@ def compact(hash) hash.reject { |_, value| value.nil? } end + def sender_token + @user ||= User.where(id: build.sender_id)&.first + @user.github_oauth_token if @user + end + + def build_credentials + Travis::Scheduler::VcsProxy.new(config, sender_token).credentials(repo) + end + + + def sender_login + @user ||= User.where(id: build.sender_id)&.first + @user.login if @user + end + def allowed_repositories @allowed_repositories ||= begin repository_ids = Repository.where(owner_id: build.owner_id, active: true).select{ |repo| repo.settings.allow_config_imports }.map(&:vcs_id) @@ -206,6 +237,10 @@ def allowed_repositories repository_ids.uniq.sort end end + + def travis_vcs_proxy? + repo.vcs_type == 'TravisproxyRepository' + end end end end diff --git a/lib/travis/scheduler/serialize/worker/build.rb b/lib/travis/scheduler/serialize/worker/build.rb index a8962ebe..49a55996 100644 --- a/lib/travis/scheduler/serialize/worker/build.rb +++ b/lib/travis/scheduler/serialize/worker/build.rb @@ -8,7 +8,7 @@ class Build < Struct.new(:build, :config) extend Forwardable def_delegators :build, :id, :request, :number, :event_type, - :pull_request_number + :pull_request_number, :sender_id def pull_request? event_type == 'pull_request' @@ -21,6 +21,10 @@ def owner_type def owner_id build.owner_id end + + def sender_id + build.sender_id + end end end end diff --git a/lib/travis/scheduler/serialize/worker/config/decrypt.rb b/lib/travis/scheduler/serialize/worker/config/decrypt.rb index 160bc039..0a9439b6 100644 --- a/lib/travis/scheduler/serialize/worker/config/decrypt.rb +++ b/lib/travis/scheduler/serialize/worker/config/decrypt.rb @@ -9,12 +9,18 @@ def apply config[key] = process_env(config[key]) if config[key] end + force_vault_to_be_secure!(config) + config[:vault] = decryptor.decrypt(config[:vault]) if config[:vault] config[:addons] = decryptor.decrypt(config[:addons]) if config[:addons] config end private + def force_vault_to_be_secure!(config) + config[:vault].delete(:token) if config.dig(:vault, :token).is_a?(String) + end + def secure_env? !!options[:secure_env] end diff --git a/lib/travis/scheduler/serialize/worker/job.rb b/lib/travis/scheduler/serialize/worker/job.rb index d853974e..452528d4 100644 --- a/lib/travis/scheduler/serialize/worker/job.rb +++ b/lib/travis/scheduler/serialize/worker/job.rb @@ -19,7 +19,7 @@ def env_vars end def secure_env? - defined?(@secure_env) ? @secure_env : @secure_env = !pull_request? || secure_env_allowed_in_pull_request? + defined?(@secure_env) ? @secure_env : (@secure_env = (!pull_request? || secure_env_allowed_in_pull_request?)) end def pull_request? @@ -60,6 +60,10 @@ def vm_config vm_config? && vm_configs[:gpu] ? vm_configs[:gpu].to_h : {} end + def vm_size + job.config.dig(:vm, :size) + end + def trace? Rollout.matches?(:trace, uid: SecureRandom.hex, owner: repository.owner.login, repo: repository.slug, redis: Scheduler.redis) end diff --git a/lib/travis/scheduler/serialize/worker/repo.rb b/lib/travis/scheduler/serialize/worker/repo.rb index d7ef437e..0a169bb5 100644 --- a/lib/travis/scheduler/serialize/worker/repo.rb +++ b/lib/travis/scheduler/serialize/worker/repo.rb @@ -11,7 +11,7 @@ class Repo < Struct.new(:repo, :config) :last_build_id, :last_build_number, :last_build_started_at, :last_build_finished_at, :last_build_duration, :last_build_state, :default_branch, :description, :key, :settings, :private?, - :managed_by_app?, :installation, :vcs_id, :vcs_type, :url, :vcs_source_host + :managed_by_app?, :installation, :vcs_id, :vcs_type, :url, :vcs_source_host, :server_type def vm_type Features.active?(:premium_vms, repo) ? :premium : :default @@ -26,6 +26,7 @@ def api_url end def source_url + return repo.vcs_source_host if travis_vcs_proxy? return source_git_url if force_private? && !Travis.config.prefer_https return source_http_url if Travis.config.prefer_https || managed_by_app? (repo.private? || force_private?) ? source_git_url : source_http_url @@ -46,10 +47,15 @@ def installation_id def keep_netrc? repo.owner&.keep_netrc? end + def github? vcs_type == 'GithubRepository' end + def travis_vcs_proxy? + vcs_type == 'TravisproxyRepository' + end + private # If the repo does not have a custom timeout, look to the repo's @@ -78,6 +84,10 @@ def force_private? end def source_host + return URI(URI::Parser.new.escape repo.vcs_source_host)&.host if travis_vcs_proxy? + repo.vcs_source_host || config[:github][:source_host] || 'github.com' + rescue Exception => e + puts "source host fail: #{e.message}" repo.vcs_source_host || config[:github][:source_host] || 'github.com' end end diff --git a/lib/travis/scheduler/service/notify.rb b/lib/travis/scheduler/service/notify.rb index 2c50884f..08e0fefb 100644 --- a/lib/travis/scheduler/service/notify.rb +++ b/lib/travis/scheduler/service/notify.rb @@ -43,7 +43,8 @@ def notify_job_board def notify_rabbitmq info :publish, job.id, job.queue, 'rabbitmq' - amqp.publish(worker_payload, properties: { type: 'test', persistent: true }) + w = worker_payload + amqp.publish(w, properties: { type: 'test', persistent: true }) end def notify_live diff --git a/lib/travis/scheduler/vcs_proxy.rb b/lib/travis/scheduler/vcs_proxy.rb new file mode 100644 index 00000000..39ac309d --- /dev/null +++ b/lib/travis/scheduler/vcs_proxy.rb @@ -0,0 +1,80 @@ +require 'faraday_middleware' + +module Travis + module Scheduler + class VcsProxy < Struct.new(:config, :oauth_token) + class Error < StandardError + attr_reader :response + + def initialize(msg, response) + super(msg) + @response = response || {} + end + end + + DEFAULT_HEADERS = { + 'User-Agent' => 'Travis-CI-Scheduler/Faraday', + 'Accept' => 'application/json', + 'Content-Type' => 'application/json' + } + + RETRY = { + max: 5, + interval: 0.05, + interval_randomness: 0.5, + backoff_factor: 2, + retry_statuses: [500, 502, 503, 504], + exceptions: [ + Errno::ETIMEDOUT, + Timeout::Error, + Faraday::RetriableResponse, + Faraday::TimeoutError, + Zlib::DataError, + Zlib::BufError, + ] + } + + def initialize(config, oauth_token) + @oauth_token = oauth_token + @config = config + end + + def token(repo) + resp = get("repositories/#{repo.vcs_id}/token/get") + resp['token'] if resp + end + + def credentials(repo) + get("repositories/#{repo.vcs_id}/token/get") + end + + private + + def request(method, path, params) + client.send(method, path, params) + rescue Faraday::ClientError => e + Travis.logger.error("New-plan-error: #{e}, Method: #{method}, path: #{path}, Params: #{params}") + raise Error.new(e, e.response) + end + + def get(path, params = {}) + request(:get, path, params).body + end + + def post(path, params = {}) + request(:post, path, params).body + end + + def client + Faraday.new(url: @config.vcs_proxy_api.url, headers: DEFAULT_HEADERS) do |c| + c.request :oauth2, @oauth_token, token_type: :bearer + c.request :retry, RETRY + c.request :json + c.response :json + c.response :raise_error + c.adapter :net_http + end + end + end + end +end diff --git a/spec/support/factories.rb b/spec/support/factories.rb index 0c533e70..ff636b6a 100644 --- a/spec/support/factories.rb +++ b/spec/support/factories.rb @@ -43,6 +43,7 @@ def public=(value) last_build_duration 60 last_build_state :passed description 'description' + server_type 'git' end factory :installation diff --git a/spec/travis/queue_spec.rb b/spec/travis/queue_spec.rb index a660fb6f..c0eba371 100644 --- a/spec/travis/queue_spec.rb +++ b/spec/travis/queue_spec.rb @@ -10,6 +10,7 @@ let(:repo) { FactoryGirl.build(:repo, owner: owner, owner_name: owner.login, name: slug.split('/').last, created_at: created_at) } let(:job) { FactoryGirl.build(:job, config: config, owner: owner, repository: repo) } let(:queue) { described_class.new(job, context.config, logger).select } + let(:plan_url) { "http://localhost:9292/users//plan" } before do Travis::Scheduler.logger.stubs(:info) @@ -22,6 +23,7 @@ { queue: 'builds.gce', dist: 'trusty' }, { queue: 'builds.gce', dist: 'xenial' }, { queue: 'builds.gce', resources: { gpu: true } }, + { queue: 'builds.gce_vm', vm_size: 'large' }, { queue: 'builds.cloudfoundry', owner: 'cloudfoundry' }, { queue: 'builds.clojure', language: 'clojure' }, { queue: 'builds.erlang', language: 'erlang' }, @@ -30,7 +32,8 @@ { queue: 'builds.new-foo', language: 'foo', percentage: percent }, { queue: 'builds.old-foo', language: 'foo' }, { queue: 'builds.arm64-lxd', arch: 'arm64' }, - { queue: 'builds.power', arch: 'ppc64le' }, + { queue: 'builds.power.private', arch: 'ppc64le', repo_private: true }, + { queue: 'builds.power', arch: 'ppc64le', repo_private: false }, { queue: 'builds.z', arch: 's390x' }, ] end @@ -168,6 +171,18 @@ end end + describe 'by job config :vm_size' do + describe 'vm_size: large' do + let(:config) { { vm: { size: 'large' } } } + it { expect(queue).to eq 'builds.gce_vm' } + end + + describe 'vm_size: unknown' do + let(:config) { { vm: { size: 'unknown' } } } + it { expect(queue).to eq 'builds.default' } + end + end + describe 'by job config :arch' do describe 'arch: amd64' do @@ -189,15 +204,61 @@ end describe 'arch: ppc64le' do - let(:config) { { arch: 'ppc64le' } } + before { config[:arch] = 'ppc64le' } context 'when repo is public' do - it { expect(queue).to eq 'builds.power' } + it 'uses queue for public repos' do + expect(queue).to eq 'builds.power' + end end context 'when repo is private' do - let(:job) { FactoryGirl.build(:job, config: config, owner: owner, repository: repo, private: true) } - it { expect(queue).to eq 'builds.default' } + before { job.private = true } + + it 'uses queue for private repos' do + expect(queue).to eq 'builds.power.private' + end + end + + context 'when there is no queue separation for private/public' do + before do + context.config.queues.delete_if { |queue| queue[:arch] == 'ppc64le' && queue[:repo_private] } + context.config.queues.detect { |queue| queue[:arch] == 'ppc64le' }.delete(:repo_private) + end + + context 'when repo is public' do + it 'uses common queue' do + expect(queue).to eq 'builds.power' + end + end + + context 'when repo is private' do + before { job.private = true } + + it 'uses common queue' do + expect(queue).to eq 'builds.power' + end + end + end + + context 'when there is no dedicated queue for ppc64le' do + before do + context.config.queues.delete_if { |queue| queue[:arch] == 'ppc64le' } + end + + context 'when repo is public' do + it 'uses default queue' do + expect(queue).to eq 'builds.default' + end + end + + context 'when repo is private' do + before { job.private = true } + + it 'uses default queue' do + expect(queue).to eq 'builds.default' + end + end end end @@ -236,6 +297,10 @@ end describe 'pooled' do + before do + stub_request(:get, plan_url). + to_return(status: 200, body: "", headers: {}) + end env TRAVIS_SITE: 'com', POOL_QUEUES: 'gce', POOL_SUFFIX: 'foo' diff --git a/spec/travis/scheduler/jobs_spec.rb b/spec/travis/scheduler/jobs_spec.rb index fc9a319b..cec0ffe9 100644 --- a/spec/travis/scheduler/jobs_spec.rb +++ b/spec/travis/scheduler/jobs_spec.rb @@ -10,12 +10,18 @@ let(:select) { described_class.new(context, owners) } let(:selected) { select.run; select.selected } let(:reports) { select.run; select.reports } + let(:billing_url) { /http:\/\/localhost:9292\/usage\/(users|organizations)\/(.+)\/allowance/ } before { config[:limit][:trial] = nil } before { config[:limit][:public] = 3 } before { config[:limit][:default] = 1 } before { config[:plans] = { one: 1, two: 2, four: 4, seven: 7, ten: 10, unlimited: 9999 } } before { config[:site] = 'com' } + before do + stub_request(:get, billing_url).to_return( + status: 404, + ) + end def create_jobs(count, attrs = {}) defaults = { @@ -578,4 +584,155 @@ def subscribe(plan, owner = self.user) it { expect(reports).to include 'user svenfuchs plan capacity: running=2 max=9999 selected=1' } it { expect(reports).to include 'user svenfuchs: queueable=4 running=2 selected=1 total_waiting=3 waiting_for_concurrency=0' } end + + context 'when user is on a metered plan' do + let(:metered_plan_limit) { 45 } + let(:body) { { private_repos: true, public_repos: true, concurrency_limit: metered_plan_limit } } + let(:billing_url) { "http://localhost:9292/usage/users/#{user.id}/allowance" } + + before do + stub_request(:get, billing_url).to_return( + body: MultiJson.dump(body) + ) + end + + describe 'with private jobs only' do + before { create_jobs(1, private: true, state: :started) } + before { create_jobs(5, private: true) } + + it { expect(selected.size).to eq 5 } + it { expect(reports).to include "user svenfuchs capacities: plan max=#{metered_plan_limit}" } + it { expect(reports).to include "user svenfuchs plan capacity: running=1 max=#{metered_plan_limit} selected=5" } + it { expect(reports).to include 'user svenfuchs: queueable=5 running=1 selected=5 total_waiting=0 waiting_for_concurrency=0' } + + context 'when private jobs are not allowed by the billing service' do + let(:body) { { private_repos: false, public_repos: true, concurrency_limit: metered_plan_limit } } + + it { expect(selected.size).to eq 0 } + it { expect(reports).to include "user svenfuchs capacities: plan max=#{metered_plan_limit}" } + it { expect(reports).to include 'user svenfuchs: queueable=5 running=1 selected=0 total_waiting=5 waiting_for_concurrency=5' } + end + end + + describe 'with public jobs only' do + before { create_jobs(1, private: false, state: :started) } + before { create_jobs(5, private: false) } + + it { expect(selected.size).to eq 5 } + it { expect(reports).to include "user svenfuchs capacities: plan max=#{metered_plan_limit}" } + it { expect(reports).to include "user svenfuchs plan capacity: running=1 max=#{metered_plan_limit} selected=5" } + it { expect(reports).to include 'user svenfuchs: queueable=5 running=1 selected=5 total_waiting=0 waiting_for_concurrency=0' } + + context 'when public jobs are not allowed by the billing service' do + let(:body) { { private_repos: true, public_repos: false, concurrency_limit: metered_plan_limit } } + + it { expect(selected.size).to eq 0 } + it { expect(reports).to include "user svenfuchs capacities: plan max=#{metered_plan_limit}" } + it { expect(reports).to include 'user svenfuchs: queueable=5 running=1 selected=0 total_waiting=5 waiting_for_concurrency=5' } + end + end + + describe 'for mixed public and private jobs' do + before { create_jobs(1, private: true, state: :started) } + before { create_jobs(1, private: false, state: :started) } + before { create_jobs(2, private: false) + create_jobs(2, private: true) } + + it { expect(selected.size).to eq 4 } + it { expect(reports).to include "user svenfuchs capacities: plan max=#{metered_plan_limit}" } + it { expect(reports).to include "user svenfuchs plan capacity: running=2 max=#{metered_plan_limit} selected=4" } + it { expect(reports).to include 'user svenfuchs: queueable=4 running=2 selected=4 total_waiting=0 waiting_for_concurrency=0' } + + context 'when private jobs are not allowed by the billing service' do + let(:body) { { private_repos: false, public_repos: true, concurrency_limit: metered_plan_limit } } + + it { expect(selected.size).to eq 2 } + it { expect(reports).to include "user svenfuchs capacities: plan max=#{metered_plan_limit}" } + it { expect(reports).to include 'user svenfuchs: queueable=4 running=2 selected=2 total_waiting=2 waiting_for_concurrency=2' } + end + + context 'when public jobs are not allowed by the billing service' do + let(:body) { { private_repos: true, public_repos: false, concurrency_limit: metered_plan_limit } } + + it { expect(selected.size).to eq 2 } + it { expect(reports).to include "user svenfuchs capacities: plan max=#{metered_plan_limit}" } + it { expect(reports).to include 'user svenfuchs: queueable=4 running=2 selected=2 total_waiting=2 waiting_for_concurrency=2' } + end + end + + describe 'stages' do + describe 'with private jobs only' do + let(:one) { FactoryGirl.create(:stage, number: 1) } + let(:two) { FactoryGirl.create(:stage, number: 2) } + let(:three) { FactoryGirl.create(:stage, number: 3) } + + before { create_jobs(1, private: true, stage: one, stage_number: '1.1', state: :started) } + before { create_jobs(1, private: true, stage: one, stage_number: '1.2') } + before { create_jobs(1, private: true, stage: one, stage_number: '1.3') } + before { create_jobs(1, private: true, stage: two, stage_number: '2.1') } + before { create_jobs(1, private: true, stage: three, stage_number: '10.1') } + + describe 'queueing' do + it { expect(selected.size).to eq 2 } + it { expect(reports).to include "repo #{repo.slug} limited by stage on build_id=#{build.id}: rejected=2 selected=2" } + it { expect(reports).to include 'user svenfuchs: queueable=4 running=1 selected=2 total_waiting=2 waiting_for_concurrency=0' } + end + + describe 'ordering' do + before { one.jobs.update_all(state: :passed) } + before { Queueable.where(job_id: one.jobs.pluck(:id)).delete_all } + it { expect(selected[0].id).to eq Job.where(stage_number: '2.1').first.id } + end + end + + describe 'with public jobs only' do + let(:one) { FactoryGirl.create(:stage, number: 1) } + let(:two) { FactoryGirl.create(:stage, number: 2) } + let(:three) { FactoryGirl.create(:stage, number: 3) } + + before { create_jobs(1, stage: one, stage_number: '1.1', state: :started) } + before { create_jobs(1, stage: one, stage_number: '1.2') } + before { create_jobs(1, stage: one, stage_number: '1.3') } + before { create_jobs(1, stage: two, stage_number: '2.1') } + before { create_jobs(1, stage: three, stage_number: '10.1') } + + describe 'queueing' do + it { expect(selected.size).to eq 2 } + it { expect(reports).to include "repo #{repo.slug} limited by stage on build_id=#{build.id}: rejected=2 selected=2" } + it { expect(reports).to include "user svenfuchs plan capacity: running=1 max=#{metered_plan_limit} selected=2" } + it { expect(reports).to include 'user svenfuchs: queueable=4 running=1 selected=2 total_waiting=2 waiting_for_concurrency=0' } + end + + describe 'ordering' do + before { one.jobs.update_all(state: :passed) } + before { Queueable.where(job_id: one.jobs.pluck(:id)).delete_all } + it { expect(selected[0].id).to eq Job.where(stage_number: '2.1').first.id } + end + end + + describe 'for mixed public and private jobs' do + let(:one) { FactoryGirl.create(:stage, number: 1) } + let(:two) { FactoryGirl.create(:stage, number: 2) } + let(:three) { FactoryGirl.create(:stage, number: 3) } + + before { create_jobs(1, private: false, stage: one, stage_number: '1.1', state: :started) } + before { create_jobs(1, private: true, stage: one, stage_number: '1.2') } + before { create_jobs(1, private: false, stage: one, stage_number: '1.3') } + before { create_jobs(1, private: true, stage: two, stage_number: '2.1') } + before { create_jobs(1, private: false, stage: three, stage_number: '10.1') } + + describe 'queueing' do + it { expect(selected.size).to eq 2 } + it { expect(reports).to include "repo #{repo.slug} limited by stage on build_id=#{build.id}: rejected=2 selected=2" } + it { expect(reports).to include "user svenfuchs plan capacity: running=1 max=#{metered_plan_limit} selected=2" } + it { expect(reports).to include 'user svenfuchs: queueable=4 running=1 selected=2 total_waiting=2 waiting_for_concurrency=0' } + end + + describe 'ordering' do + before { one.jobs.update_all(state: :passed) } + before { Queueable.where(job_id: one.jobs.pluck(:id)).delete_all } + it { expect(selected[0].id).to eq Job.where(stage_number: '2.1').first.id } + end + end + end + end end diff --git a/spec/travis/scheduler/record/organization_spec.rb b/spec/travis/scheduler/record/organization_spec.rb index bb9af7cf..4d14de08 100644 --- a/spec/travis/scheduler/record/organization_spec.rb +++ b/spec/travis/scheduler/record/organization_spec.rb @@ -1,5 +1,6 @@ describe Organization do let(:org) { FactoryGirl.create(:org) } + let(:authorize_build_url) { "http://localhost:9292/organizations/#{org.id}/plan" } describe "constants" do # It isn't often that we see tests for constants, but these are special. @@ -68,6 +69,22 @@ context "subscribed? == true" do before do org.stubs(:educational?).returns(true) + stub_request(:get, authorize_build_url).to_return( + body: MultiJson.dump(plan_name: 'free_tier_plan', hybrid: false, free: true, status: nil, metered: true) + ) + end + + it "returns the DEFAULT_SUBSCRIBED_TIMEOUT" do + expect(org.default_worker_timeout).to eq Organization::DEFAULT_SUBSCRIBED_TIMEOUT + end + end + + context "paid_new_plan? == true" do + before do + org.stubs(:paid_new_plan?).returns(true) + stub_request(:get, authorize_build_url).to_return( + body: MultiJson.dump(plan_name: 'two_concurrent_plan', hybrid: true, free: false, status: 'subscribed', metered: false) + ) end it "returns the DEFAULT_SUBSCRIBED_TIMEOUT" do @@ -80,6 +97,9 @@ org.stubs(:subscribed?).returns(false) org.stubs(:active_trial?).returns(false) org.stubs(:educational?).returns(false) + stub_request(:get, authorize_build_url).to_return( + body: MultiJson.dump(plan_name: 'free_tier_plan', hybrid: false, free: true, status: nil, metered: true) + ) end it "returns the DEFAULT_SUBSCRIBED_TIMEOUT" do diff --git a/spec/travis/scheduler/record/user_spec.rb b/spec/travis/scheduler/record/user_spec.rb index 351cd4bc..433d7612 100644 --- a/spec/travis/scheduler/record/user_spec.rb +++ b/spec/travis/scheduler/record/user_spec.rb @@ -1,5 +1,7 @@ describe User do let(:user) { FactoryGirl.create(:user) } + let(:repo) { FactoryGirl.create(:repository) } + let(:authorize_build_url) { "http://localhost:9292/users/#{user.id}/plan" } describe "constants" do # It isn't often that we see tests for constants, but these are special. @@ -58,6 +60,9 @@ context "educational? == true" do before do user.stubs(:educational?).returns(true) + stub_request(:get, authorize_build_url).to_return( + body: MultiJson.dump(plan_name: 'free_tier_plan', hybrid: false, free: true, status: nil, metered: true) + ) end it "returns the DEFAULT_SUBSCRIBED_TIMEOUT" do @@ -75,15 +80,31 @@ end end + context "paid_new_plan? == true" do + before do + user.stubs(:paid_new_plan?).returns(true) + stub_request(:get, authorize_build_url).to_return( + body: MultiJson.dump(plan_name: 'two_concurrent_plan', hybrid: true, free: false, status: 'subscribed', metered: false) + ) + end + + it "returns the DEFAULT_SUBSCRIBED_TIMEOUT" do + expect(user.default_worker_timeout).to eq Organization::DEFAULT_SUBSCRIBED_TIMEOUT + end + end + context "#subscribed?, #active_trial?, #educational? == false" do before do user.stubs(:subscribed?).returns(false) user.stubs(:active_trial?).returns(false) user.stubs(:educational?).returns(false) + stub_request(:get, authorize_build_url).to_return( + body: MultiJson.dump(plan_name: 'two_concurrent_plan', hybrid: true, free: false, status: 'subscribed', metered: false) + ) end it "returns the DEFAULT_SUBSCRIBED_TIMEOUT" do - expect(user.default_worker_timeout).to eq User::DEFAULT_SPONSORED_TIMEOUT + expect(user.default_worker_timeout).to eq User::DEFAULT_SUBSCRIBED_TIMEOUT end end end diff --git a/spec/travis/scheduler/serialize/worker/config_spec.rb b/spec/travis/scheduler/serialize/worker/config_spec.rb index aec1cfb0..fe2fd553 100644 --- a/spec/travis/scheduler/serialize/worker/config_spec.rb +++ b/spec/travis/scheduler/serialize/worker/config_spec.rb @@ -71,6 +71,16 @@ def encrypt(string) let(:env) { [{ FOO: 'foo', BAR: 'bar' }, encrypt('BAZ=baz')] } it { should eql env: ['FOO=foo', 'BAR=bar', 'SECURE BAZ=baz'], global_env: ['FOO=foo', 'BAR=bar', 'SECURE BAZ=baz'] } end + + describe 'decrypts vault secure token' do + let(:config) { { vault: { token: { secure: encrypt('my_key') } } } } + it { should eql vault: {token: 'my_key'} } + end + + describe 'clears vault unsecure token' do + let(:config) { { vault: { token: 'my_key' } } } + it { should eql vault: {} } + end end describe 'with secure env disabled' do diff --git a/spec/travis/scheduler/serialize/worker/job_spec.rb b/spec/travis/scheduler/serialize/worker/job_spec.rb index 3ee7876e..5e77c3bd 100644 --- a/spec/travis/scheduler/serialize/worker/job_spec.rb +++ b/spec/travis/scheduler/serialize/worker/job_spec.rb @@ -96,4 +96,59 @@ it { expect(subject.secrets).to eq %w(one two three) } end + + describe 'decrypted config' do + context 'when in a pull request' do + let(:config) do + { + env: [ + { :BAR => 'bar' }, + { secure: Base64.encode64(repo.key.encrypt('MAIN=1')) }, + ] + } + end + let(:request) { Request.new(pull_request: pull_request) } + + before do + build.event_type = 'pull_request' + end + + context 'when head repo is present' do + let(:head_repo) { FactoryGirl.create(:repository, github_id: 549744) } + let(:head_repo_key) { OpenSSL::PKey::RSA.generate(4096) } + let(:pull_request) { PullRequest.new(base_repo_slug: 'travis-ci/travis-yml', head_repo_slug: "#{head_repo.owner_name}/#{head_repo.name}") } + + it do + expect(subject.decrypted_config[:env]).to eq(['BAR=bar', 'SECURE MAIN=1']) + end + end + + context 'when head repo is not found' do + let(:pull_request) { PullRequest.new(base_repo_slug: 'travis-ci/travis-yml', head_repo_slug: 'mytest/letssee') } + + it 'returns garbage' do + expect(subject.decrypted_config[:env]).to eq(['BAR=bar', 'SECURE [unable to decrypt]']) + end + end + end + + context 'when in a push' do + let(:config) do + { + env: [ + { :BAR => 'bar' }, + { secure: Base64.encode64(repo.key.encrypt('MAIN=1')) }, + ] + } + end + + before do + build.event_type = 'push' + end + + it do + expect(subject.decrypted_config[:env]).to eq(['BAR=bar', 'SECURE MAIN=1']) + end + end + end end diff --git a/spec/travis/scheduler/serialize/worker/repo_spec.rb b/spec/travis/scheduler/serialize/worker/repo_spec.rb index 0c79ab95..62c2b10a 100644 --- a/spec/travis/scheduler/serialize/worker/repo_spec.rb +++ b/spec/travis/scheduler/serialize/worker/repo_spec.rb @@ -23,6 +23,12 @@ let(:worker) { described_class.new(user_repo, config) } context "unpaid account" do + let(:authorize_build_url) { "http://localhost:9292/users/#{user.id}/plan" } + before do + stub_request(:get, authorize_build_url).to_return( + body: MultiJson.dump(plan_name: 'free_tier_plan', hybrid: false, free: true, status: 'subscribed', metered: true) + ) + end it "returns a hash of timeout values" do timeouts = worker.timeouts @@ -62,6 +68,12 @@ let(:worker) { described_class.new(org_repo, config) } context "unpaid account" do + let(:authorize_build_url) { "http://localhost:9292/organizations/#{org.id}/plan" } + before do + stub_request(:get, authorize_build_url).to_return( + body: MultiJson.dump(plan_name: 'free_tier_plan', hybrid: false, free: true, status: 'subscribed', metered: true) + ) + end it "returns a hash of timeout values" do timeouts = worker.timeouts diff --git a/spec/travis/scheduler/serialize/worker_spec.rb b/spec/travis/scheduler/serialize/worker_spec.rb index e64fed9e..ef2e6b7c 100644 --- a/spec/travis/scheduler/serialize/worker_spec.rb +++ b/spec/travis/scheduler/serialize/worker_spec.rb @@ -40,8 +40,9 @@ def encrypted(value) it 'data' do expect(data).to eq( type: :test, - vm_type: :default, vm_config: {}, + vm_type: :default, + vm_size: nil, queue: 'builds.gce', config: { rvm: '1.8.7', @@ -87,13 +88,14 @@ def encrypted(value) source_url: 'https://github.com/svenfuchs/gem-release.git', source_host: 'github.com', api_url: 'https://api.github.com/repos/svenfuchs/gem-release', + last_build_number: '2', last_build_started_at: '2016-01-01T10:00:00Z', last_build_finished_at: '2016-01-01T11:00:00Z', - last_build_number: '2', last_build_duration: 60, last_build_state: 'passed', default_branch: 'branch', description: 'description', + server_type: 'git', }, ssh_key: nil, timeouts: { @@ -213,8 +215,9 @@ def encrypted(value) it 'data' do expect(data).to eq( type: :test, - vm_type: :default, vm_config: {}, + vm_type: :default, + vm_size: nil, queue: 'builds.gce', config: { rvm: '1.8.7', @@ -239,11 +242,11 @@ def encrypted(value) secure_env_removed: true, debug_options: {}, queued_at: '2016-01-01T10:30:00Z', - pull_request_head_branch: 'head_branch', - pull_request_head_sha: '62aaef', allow_failure: allow_failure, stage_name: nil, name: 'jobname', + pull_request_head_branch: 'head_branch', + pull_request_head_sha: '62aaef', pull_request_head_slug: 'travis-ci/gem-release', pull_request_base_slug: nil, pull_request_base_ref: nil, @@ -265,13 +268,14 @@ def encrypted(value) source_url: 'https://github.com/svenfuchs/gem-release.git', source_host: 'github.com', api_url: 'https://api.github.com/repos/svenfuchs/gem-release', + last_build_number: '2', last_build_started_at: '2016-01-01T10:00:00Z', last_build_finished_at: '2016-01-01T11:00:00Z', - last_build_number: '2', last_build_duration: 60, last_build_state: 'passed', default_branch: 'branch', description: 'description', + server_type: 'git', }, ssh_key: nil, timeouts: { diff --git a/spec/travis/scheduler/service/enqueue_owners_spec.rb b/spec/travis/scheduler/service/enqueue_owners_spec.rb index e9b64e85..7b7c9f78 100644 --- a/spec/travis/scheduler/service/enqueue_owners_spec.rb +++ b/spec/travis/scheduler/service/enqueue_owners_spec.rb @@ -7,12 +7,18 @@ let(:config) { Travis::Scheduler.context.config } let(:data) { { owner_type: 'User', owner_id: owner.id, jid: '1234' } } let(:service) { described_class.new(Travis::Scheduler.context, data) } + let(:authorize_build_url) { "http://localhost:9292/users/#{owner.id}/plan" } before { Travis::JobBoard.stubs(:post) } before { 1.upto(2) { FactoryGirl.create(:job, commit: commit, repository: repo, owner: owner, private: true, state: :created, queue: 'builds.gce', config: {}) } } before { config.limit.delegate = { owner.login => org.login } } before { config.limit.by_owner = { org.login => 1 } } + before do + stub_request(:get, authorize_build_url).to_return( + body: MultiJson.dump(plan_name: 'two_concurrent_plan', hybrid: true, free: false, status: 'subscribed', metered: false) + ) + end before { service.run } it { expect(Job.order(:id).map(&:state)).to eq %w[queued created] } diff --git a/spec/travis/scheduler/service/event_spec.rb b/spec/travis/scheduler/service/event_spec.rb index bd3ab336..787547e4 100644 --- a/spec/travis/scheduler/service/event_spec.rb +++ b/spec/travis/scheduler/service/event_spec.rb @@ -8,11 +8,17 @@ let(:data) { { id: build.id, jid: '1234' } } let(:event) { 'build:created' } let(:service) { described_class.new(Travis::Scheduler.context, event, data) } + let(:authorize_build_url) { "http://localhost:9292/users/#{owner.id}/plan" } context do before { Travis::JobBoard.stubs(:post) } before { config.limit.delegate = { owner.login => org.login } } before { config.limit.by_owner = { org.login => 1 } } + before do + stub_request(:get, authorize_build_url).to_return( + body: MultiJson.dump(plan_name: 'two_concurrent_plan', hybrid: true, free: false, status: 'subscribed', metered: false) + ) + end before { service.run } it { expect(Job.first.state).to eq 'queued' } diff --git a/spec/travis/scheduler/service/notify_spec.rb b/spec/travis/scheduler/service/notify_spec.rb index 0bc97f8b..a21f8246 100644 --- a/spec/travis/scheduler/service/notify_spec.rb +++ b/spec/travis/scheduler/service/notify_spec.rb @@ -10,8 +10,14 @@ let(:url) { 'https://job-board.travis-ci.org/jobs/add' } let(:status) { 201 } let(:body) { 'Created' } + let(:authorize_build_url) { "http://localhost:9292/users/#{job.owner.id}/plan" } before { stub_request(:post, url).to_return(status: status, body: body) } + before do + stub_request(:get, authorize_build_url).to_return( + body: MultiJson.dump(plan_name: 'two_concurrent_plan', hybrid: true, free: false, status: 'subscribed', metered: false) + ) + end describe 'with rollout job_board not enabled' do before { disable_rollout('job_board', job.owner) } From ae102f5ffd1f08e7cfd674036bae0cfcd993b6c2 Mon Sep 17 00:00:00 2001 From: gabriel-arc Date: Thu, 2 Feb 2023 13:32:02 +0100 Subject: [PATCH 68/81] activerecord up to 6.1.7.2 --- Gemfile | 2 +- Gemfile.lock | 26 +++++++++++++------------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Gemfile b/Gemfile index e7b8f64e..713a6fa1 100644 --- a/Gemfile +++ b/Gemfile @@ -21,7 +21,7 @@ gem 'cl' gem 'sidekiq-pro', require: 'sidekiq-pro', source: 'https://gems.contribsys.com' gem 'sidekiq', '~> 6.4' gem 'redis-namespace' -gem 'activerecord', '~> 6.1.6.1' +gem 'activerecord', '~> 6.1.7.2' gem 'bunny', '~> 2.9.2' gem 'pg' gem 'concurrent-ruby' diff --git a/Gemfile.lock b/Gemfile.lock index 2d6a7d76..bc486a31 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -79,11 +79,11 @@ GIT GIT remote: https://github.com/travis-ci/travis-settings - revision: 9d6f936fd5eb3431162d82783e7ca3f64d22db95 + revision: dd614292ede63952f75d5f78594a23e5e05a862a branch: 6.1 specs: travis-settings (0.0.2) - activemodel (~> 6.1.6.1) + activemodel (~> 6.1.7.1) virtus GEM @@ -96,12 +96,12 @@ GEM remote: https://rubygems.org/ specs: HDRHistogram (0.1.3) - activemodel (6.1.6.1) - activesupport (= 6.1.6.1) - activerecord (6.1.6.1) - activemodel (= 6.1.6.1) - activesupport (= 6.1.6.1) - activesupport (6.1.6.1) + activemodel (6.1.7.2) + activesupport (= 6.1.7.2) + activerecord (6.1.7.2) + activemodel (= 6.1.7.2) + activesupport (= 6.1.7.2) + activesupport (6.1.7.2) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -123,7 +123,7 @@ GEM coderay (1.1.2) coercible (1.0.0) descendants_tracker (~> 0.0.1) - concurrent-ruby (1.1.10) + concurrent-ruby (1.2.0) connection_pool (2.2.5) crack (0.4.3) safe_yaml (~> 1.0.0) @@ -187,7 +187,7 @@ GEM http (>= 2.0, < 5.0) metaclass (0.0.4) method_source (0.9.0) - minitest (5.16.3) + minitest (5.17.0) mocha (0.10.5) metaclass (~> 0.0.1) multi_json (1.12.1) @@ -231,7 +231,7 @@ GEM thread_safe (0.3.6) travis-config (1.1.3) hashr (~> 2.0) - tzinfo (2.0.5) + tzinfo (2.0.6) concurrent-ruby (~> 1.0) unf (0.1.4) unf_ext @@ -244,13 +244,13 @@ GEM addressable (>= 2.3.6) crack (>= 0.3.2) hashdiff - zeitwerk (2.6.0) + zeitwerk (2.6.6) PLATFORMS ruby DEPENDENCIES - activerecord (~> 6.1.6.1) + activerecord (~> 6.1.7.2) bunny (~> 2.9.2) cl coder! From 6bed9d04bb2dff52f87deec600674812f950a95b Mon Sep 17 00:00:00 2001 From: GbArc Date: Thu, 11 May 2023 11:17:55 +0200 Subject: [PATCH 69/81] updated matcher to allow priv repos for arm64,s390x in enterprise [ship:docker] --- lib/travis/queue/matcher.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/travis/queue/matcher.rb b/lib/travis/queue/matcher.rb index 32dfb8b8..6ec4575a 100644 --- a/lib/travis/queue/matcher.rb +++ b/lib/travis/queue/matcher.rb @@ -88,7 +88,7 @@ def resources end def arch - return nil if job.private? && OSS_ONLY_ARCH.include?(job.config[:arch]) + return nil if job.private? && OSS_ONLY_ARCH.include?(job.config[:arch]) && !enterprise? job.config[:arch] end @@ -109,6 +109,10 @@ def check_unknown_matchers(used) unknown = used - KEYS logger.warn MSGS[:unknown_matchers] % [used, unknown, repo.slug] if logger && unknown.any? end + + def enterprise? + !!Travis::Scheduler.context.config[:enterprise] + end end end end From ec52f0a1da948e1a9602da3400046d142f566311 Mon Sep 17 00:00:00 2001 From: gabriel-arc <57348209+GbArc@users.noreply.github.com> Date: Fri, 26 May 2023 13:01:24 +0200 Subject: [PATCH 70/81] Using proper key for forked secrets (#296) --- lib/travis/scheduler/serialize/worker/job.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/travis/scheduler/serialize/worker/job.rb b/lib/travis/scheduler/serialize/worker/job.rb index 452528d4..8d5de774 100644 --- a/lib/travis/scheduler/serialize/worker/job.rb +++ b/lib/travis/scheduler/serialize/worker/job.rb @@ -95,6 +95,8 @@ def vm_configs def job_repository return job.repository if job.source.event_type != 'pull_request' || job.source.request.pull_request.head_repo_slug == job.source.request.pull_request.base_repo_slug + return repository if repository.settings.share_encrypted_env_with_forks + owner_name, repo_name = job.source.request.pull_request.head_repo_slug.split('/') return if owner_name.nil? || owner_name.empty? || repo_name.nil? || repo_name.empty? From 79d54c137b14c1b75f99064cba109082e47d4a83 Mon Sep 17 00:00:00 2001 From: piccadilly-circus <134370605+piccadilly-circus@users.noreply.github.com> Date: Mon, 28 Aug 2023 14:26:12 +0500 Subject: [PATCH 71/81] Stop jobs from being queued if the build has been cancelled (#298) Stop cancelled build jobs from being queued --- Gemfile.lock | 9 +++------ lib/travis/scheduler/service/set_queue.rb | 11 +++++++++-- .../scheduler/service/enqueue_owners_spec.rb | 4 +++- spec/travis/scheduler/service/event_spec.rb | 15 ++++++++++++++- spec/travis/scheduler/service/notify_spec.rb | 6 ++++-- 5 files changed, 33 insertions(+), 12 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index bc486a31..1e05e389 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -86,14 +86,9 @@ GIT activemodel (~> 6.1.7.1) virtus -GEM - remote: https://gems.contribsys.com/ - specs: - sidekiq-pro (3.4.0) - sidekiq (>= 4.1.5) - GEM remote: https://rubygems.org/ + remote: https://gems.contribsys.com/ specs: HDRHistogram (0.1.3) activemodel (6.1.7.2) @@ -228,6 +223,8 @@ GEM connection_pool (>= 2.2.2) rack (~> 2.0) redis (>= 4.2.0) + sidekiq-pro (3.4.0) + sidekiq (>= 4.1.5) thread_safe (0.3.6) travis-config (1.1.3) hashr (~> 2.0) diff --git a/lib/travis/scheduler/service/set_queue.rb b/lib/travis/scheduler/service/set_queue.rb index 2cfca912..bfa3b310 100644 --- a/lib/travis/scheduler/service/set_queue.rb +++ b/lib/travis/scheduler/service/set_queue.rb @@ -8,7 +8,8 @@ class SetQueue < Struct.new(:context, :job, :opts) MSGS = { redirect: 'Found job.queue: %s. Redirecting to: %s', - queue: 'Setting queue to %s for job=%s' + queue: 'Setting queue to %s for job=%s', + canceled: 'Build %s has been canceled, job %s being canceled' } def run @@ -19,7 +20,13 @@ def run private def queue - @queue ||= redirect(Queue.new(job, config, logger).select) + if job.stage.state == "canceled" + info MSGS[:canceled] % [job.source.id, job.id] + payload = { id: job.id, source: 'scheduler' } + Hub.push('job:cancel', payload) + else + @queue ||= redirect(Queue.new(job, config, logger).select) + end end # TODO confirm we don't need queue redirection any more diff --git a/spec/travis/scheduler/service/enqueue_owners_spec.rb b/spec/travis/scheduler/service/enqueue_owners_spec.rb index 7b7c9f78..0e7767b6 100644 --- a/spec/travis/scheduler/service/enqueue_owners_spec.rb +++ b/spec/travis/scheduler/service/enqueue_owners_spec.rb @@ -3,6 +3,8 @@ let(:repo) { FactoryGirl.create(:repo, owner: owner) } let(:owner) { FactoryGirl.create(:user) } let(:commit) { FactoryGirl.create(:commit) } + let(:build) { FactoryGirl.create(:build, repository: repo, owner: owner, jobs: [job]) } + let(:job_stage) { FactoryGirl.create(:stage) } let(:job) { Job.first } let(:config) { Travis::Scheduler.context.config } let(:data) { { owner_type: 'User', owner_id: owner.id, jid: '1234' } } @@ -11,7 +13,7 @@ before { Travis::JobBoard.stubs(:post) } - before { 1.upto(2) { FactoryGirl.create(:job, commit: commit, repository: repo, owner: owner, private: true, state: :created, queue: 'builds.gce', config: {}) } } + before { 1.upto(2) { FactoryGirl.create(:job, commit: commit, repository: repo, owner: owner, private: true, state: :created, queue: 'builds.gce', config: {}, stage_id: job_stage.id) } } before { config.limit.delegate = { owner.login => org.login } } before { config.limit.by_owner = { org.login => 1 } } before do diff --git a/spec/travis/scheduler/service/event_spec.rb b/spec/travis/scheduler/service/event_spec.rb index 787547e4..78b15d97 100644 --- a/spec/travis/scheduler/service/event_spec.rb +++ b/spec/travis/scheduler/service/event_spec.rb @@ -3,7 +3,8 @@ let(:repo) { FactoryGirl.create(:repo) } let(:owner) { FactoryGirl.create(:user) } let(:build) { FactoryGirl.create(:build, repository: repo, owner: owner, jobs: [job]) } - let(:job) { FactoryGirl.create(:job, private: true, state: :created, config: config.to_h) } + let(:job_stage) { FactoryGirl.create(:stage) } + let(:job) { FactoryGirl.create(:job, private: true, state: :created, config: config.to_h, stage_id: job_stage.id) } let(:config) { Travis::Scheduler.context.config } let(:data) { { id: build.id, jid: '1234' } } let(:event) { 'build:created' } @@ -34,4 +35,16 @@ before { service.run } it { expect(log).to include "I 1234 Owner group scheduler.owners-svenfuchs is locked and already being evaluated. Dropping event build:created for build=#{build.id}" } end + + context do + before { Travis::JobBoard.stubs(:post) } + before { config.limit.delegate = { owner.login => org.login } } + before { config.limit.by_owner = { org.login => 1 } } + describe 'build cancelled while job is not queued yet' do + let(:job_stage) { FactoryGirl.create(:stage, state: 'canceled') } + before { service.run } + it { expect(Job.first.stage.state).to eq 'canceled' } + it { expect(log).to include "Build #{build.id} has been canceled, job #{job.id} being canceled" } + end + end end diff --git a/spec/travis/scheduler/service/notify_spec.rb b/spec/travis/scheduler/service/notify_spec.rb index a21f8246..7db44c9a 100644 --- a/spec/travis/scheduler/service/notify_spec.rb +++ b/spec/travis/scheduler/service/notify_spec.rb @@ -1,5 +1,7 @@ describe Travis::Scheduler::Service::Notify do - let(:job) { FactoryGirl.create(:job, state: :queued, queued_at: Time.parse('2016-01-01T10:30:00Z'), config: {}) } + let(:build) { FactoryGirl.create(:build, repository: repo, owner: owner, jobs: [job]) } + let(:job_stage) { FactoryGirl.create(:stage) } + let(:job) { FactoryGirl.create(:job, state: :queued, queued_at: Time.parse('2016-01-01T10:30:00Z'), config: {}, stage_id: job_stage.id) } let(:data) { { job: { id: job.id } } } let(:context) { Travis::Scheduler.context } let(:service) { described_class.new(context, data) } @@ -154,7 +156,7 @@ def rescueing describe 'sets the queue' do let(:config) { { language: 'objective-c', os: 'osx', osx_image: 'xcode8', group: 'stable', dist: 'osx'} } - let(:job) { FactoryGirl.create(:job, state: :queued, config: config, queue: nil, queued_at: Time.parse('2016-01-01T10:30:00Z')) } + let(:job) { FactoryGirl.create(:job, state: :queued, config: config, queue: nil, queued_at: Time.parse('2016-01-01T10:30:00Z'), stage_id: job_stage.id) } before { context.config.queues = [{ queue: 'builds.mac_osx', os: 'osx' }] } before { service.run } From 7c7c654d87cb9a0e0ee3efa05563083a71119c1b Mon Sep 17 00:00:00 2001 From: piccadilly-circus <134370605+piccadilly-circus@users.noreply.github.com> Date: Thu, 31 Aug 2023 10:13:58 +0500 Subject: [PATCH 72/81] Stages are not always used ship:docker (#299) --- lib/travis/scheduler/service/set_queue.rb | 2 +- spec/travis/scheduler/service/event_spec.rb | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/travis/scheduler/service/set_queue.rb b/lib/travis/scheduler/service/set_queue.rb index bfa3b310..6bc4ad4f 100644 --- a/lib/travis/scheduler/service/set_queue.rb +++ b/lib/travis/scheduler/service/set_queue.rb @@ -20,7 +20,7 @@ def run private def queue - if job.stage.state == "canceled" + if job.stage.present? && job.stage.state == "canceled" info MSGS[:canceled] % [job.source.id, job.id] payload = { id: job.id, source: 'scheduler' } Hub.push('job:cancel', payload) diff --git a/spec/travis/scheduler/service/event_spec.rb b/spec/travis/scheduler/service/event_spec.rb index 78b15d97..1f0222bd 100644 --- a/spec/travis/scheduler/service/event_spec.rb +++ b/spec/travis/scheduler/service/event_spec.rb @@ -46,5 +46,13 @@ it { expect(Job.first.stage.state).to eq 'canceled' } it { expect(log).to include "Build #{build.id} has been canceled, job #{job.id} being canceled" } end + + describe 'jobs are queued if there are no stages' do + let(:job) { FactoryGirl.create(:job, private: true, state: :created, config: config.to_h, stage_id: nil) } + before { service.run } + it { expect(Job.first.state).to eq 'queued' } + it { expect(log).to include 'Evaluating jobs for owner group: user svenfuchs, org travis-ci' } + it { expect(log).to include "enqueueing job #{Job.first.id} (svenfuchs/gem-release)" } + end end end From da76215af5c9c80377133d96445667184c77bf37 Mon Sep 17 00:00:00 2001 From: piccadilly-circus <134370605+piccadilly-circus@users.noreply.github.com> Date: Thu, 12 Oct 2023 09:41:44 +0500 Subject: [PATCH 73/81] Stop build from not getting queued (#300) queue the job if there is an exception --- lib/travis/scheduler/service/set_queue.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/travis/scheduler/service/set_queue.rb b/lib/travis/scheduler/service/set_queue.rb index 6bc4ad4f..f80893d5 100644 --- a/lib/travis/scheduler/service/set_queue.rb +++ b/lib/travis/scheduler/service/set_queue.rb @@ -27,6 +27,11 @@ def queue else @queue ||= redirect(Queue.new(job, config, logger).select) end + rescue => e + puts "ERROR while trying to queue: #{e.message}" + puts "Backtrace:" + puts e.backtrace.join("\n") + @queue ||= redirect(Queue.new(job, config, logger).select) end # TODO confirm we don't need queue redirection any more From d16a4b44b8e17ef69b7900598857eb2ca23713ba Mon Sep 17 00:00:00 2001 From: piccadilly-circus <134370605+piccadilly-circus@users.noreply.github.com> Date: Wed, 3 Jan 2024 15:17:49 +0500 Subject: [PATCH 74/81] Restrict ruby gem-update version (#302) Restrict ruby gem-update to 3.4.22 because it still supports ruby 2.6 --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 8fb6f700..86d33dce 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,6 +15,7 @@ env: before_install: - gem uninstall -v '>=2' -i $(rvm gemdir)@global -ax bundler || true - gem install bundler -v '2.3.7' + - gem install rubygems-update -v 3.4.22 addons: - apt: From 1d6f8408014ba0be0788a3c876ee286c30d5b6ed Mon Sep 17 00:00:00 2001 From: Sebastian Karpeta Date: Mon, 25 Mar 2024 14:03:55 +0100 Subject: [PATCH 75/81] TravisCI Scheduler merge. Enterprise to master. --- lib/travis/scheduler/config.rb | 2 +- lib/travis/scheduler/jobs/capacity/plan.rb | 5 - lib/travis/scheduler/record/job.rb | 1 - lib/travis/scheduler/record/user.rb | 4 +- lib/travis/scheduler/serialize/worker.rb | 2 +- lib/travis/scheduler/serialize/worker/job.rb | 26 +-- spec/travis/scheduler/jobs_spec.rb | 151 ------------------ .../travis/scheduler/serialize/worker_spec.rb | 1 - .../scheduler/service/enqueue_owners_spec.rb | 2 +- spec/travis/scheduler/service/event_spec.rb | 24 +-- spec/travis/scheduler/service/notify_spec.rb | 2 +- 11 files changed, 21 insertions(+), 199 deletions(-) diff --git a/lib/travis/scheduler/config.rb b/lib/travis/scheduler/config.rb index 15cb5538..40ad8e85 100644 --- a/lib/travis/scheduler/config.rb +++ b/lib/travis/scheduler/config.rb @@ -15,7 +15,7 @@ class Config < Travis::Config billing: { url: 'http://localhost:9292/', auth_key: 'auth_keys' }, billing: { url: 'http://localhost:9292/', auth_key: 'auth_keys' },host: 'https://travis-ci.com', interval: 2, - limit: { public: 93939, education: 92929, default: 91919, by_owner: {}, delegate: {} }, + limit: { public: 5, education: 1, default: 5, by_owner: {}, delegate: {} }, lock: { strategy: :redis, ttl: 150 }, logger: { time_format: false, process_id: false, thread_id: false }, log_level: :info, diff --git a/lib/travis/scheduler/jobs/capacity/plan.rb b/lib/travis/scheduler/jobs/capacity/plan.rb index f31a7faf..53e1a453 100644 --- a/lib/travis/scheduler/jobs/capacity/plan.rb +++ b/lib/travis/scheduler/jobs/capacity/plan.rb @@ -12,11 +12,6 @@ def applicable? def report(status, job) super.merge(max:) end - - def accept?(job) - super if !on_metered_plan? || billing_allowed?(job) - end - def accept?(job) super if !on_metered_plan? || billing_allowed?(job) end diff --git a/lib/travis/scheduler/record/job.rb b/lib/travis/scheduler/record/job.rb index 2032aa6c..24621e8f 100644 --- a/lib/travis/scheduler/record/job.rb +++ b/lib/travis/scheduler/record/job.rb @@ -109,7 +109,6 @@ def config config = record&.config_json if record.respond_to?(:config_json) # TODO: remove once we've rolled over config ||= record&.config config ||= read_attribute(:config) if has_attribute?(:config) - config = JSON.parse(config) if config.is_a?(String) config ||= {} config = JSON.parse(config) if config.is_a?(String) config.deep_symbolize_keys! diff --git a/lib/travis/scheduler/record/user.rb b/lib/travis/scheduler/record/user.rb index 34e5aa4a..2ff06977 100644 --- a/lib/travis/scheduler/record/user.rb +++ b/lib/travis/scheduler/record/user.rb @@ -46,9 +46,9 @@ def paid_new_plan? else billing_client.get_plan(self).to_h end - return false if plan[:error] || plan["plan_name"].nil? + return false if plan[:error] || plan['plan_name'].nil? - plan["hybrid"] || !plan["plan_name"].include?('free') + plan['hybrid'] || !plan['plan_name'].include?('free') end def enterprise? diff --git a/lib/travis/scheduler/serialize/worker.rb b/lib/travis/scheduler/serialize/worker.rb index 18dfc8f3..4177fca2 100644 --- a/lib/travis/scheduler/serialize/worker.rb +++ b/lib/travis/scheduler/serialize/worker.rb @@ -117,7 +117,7 @@ def repository_data default_branch: repo.default_branch, description: repo.description, server_type: repo.server_type || 'git' - server_type: repo.server_type || 'git',) + ) end def source_url diff --git a/lib/travis/scheduler/serialize/worker/job.rb b/lib/travis/scheduler/serialize/worker/job.rb index 65210a7d..74ef9364 100644 --- a/lib/travis/scheduler/serialize/worker/job.rb +++ b/lib/travis/scheduler/serialize/worker/job.rb @@ -90,24 +90,24 @@ def vm_config? Features.active?(:resources_gpu, repository) && job.config.dig(:resources, :gpu) end - def vm_configs - config[:vm_configs] || {} - end + def vm_configs + config[:vm_configs] || {} + end - def job_repository - return job.repository if job.source.event_type != 'pull_request' || job.source.request.pull_request.head_repo_slug == job.source.request.pull_request.base_repo_slug + def job_repository + return job.repository if job.source.event_type != 'pull_request' || job.source.request.pull_request.head_repo_slug == job.source.request.pull_request.base_repo_slug - return repository if repository.settings.share_encrypted_env_with_forks + return repository if repository.settings.share_encrypted_env_with_forks - owner_name, repo_name = job.source.request.pull_request.head_repo_slug.split('/') - return if owner_name.nil? || owner_name.empty? || repo_name.nil? || repo_name.empty? + owner_name, repo_name = job.source.request.pull_request.head_repo_slug.split('/') + return if owner_name.nil? || owner_name.empty? || repo_name.nil? || repo_name.empty? - ::Repository.find_by(owner_name: owner_name, name: repo_name) - end + ::Repository.find_by(owner_name: owner_name, name: repo_name) + end - def repository_key - job_repository&.key || ::SslKey.new(private_key: 'test') - end + def repository_key + job_repository&.key || ::SslKey.new(private_key: 'test') + end end end end diff --git a/spec/travis/scheduler/jobs_spec.rb b/spec/travis/scheduler/jobs_spec.rb index 9a2f7678..9b38a47c 100644 --- a/spec/travis/scheduler/jobs_spec.rb +++ b/spec/travis/scheduler/jobs_spec.rb @@ -1066,155 +1066,4 @@ def subscribe(plan, owner = user) end end end - - context 'when user is on a metered plan' do - let(:metered_plan_limit) { 45 } - let(:body) { { private_repos: true, public_repos: true, concurrency_limit: metered_plan_limit } } - let(:billing_url) { "http://localhost:9292/usage/users/#{user.id}/allowance" } - - before do - stub_request(:get, billing_url).to_return( - body: MultiJson.dump(body) - ) - end - - describe 'with private jobs only' do - before { create_jobs(1, private: true, state: :started) } - before { create_jobs(5, private: true) } - - it { expect(selected.size).to eq 5 } - it { expect(reports).to include "user svenfuchs capacities: plan max=#{metered_plan_limit}" } - it { expect(reports).to include "user svenfuchs plan capacity: running=1 max=#{metered_plan_limit} selected=5" } - it { expect(reports).to include 'user svenfuchs: queueable=5 running=1 selected=5 total_waiting=0 waiting_for_concurrency=0' } - - context 'when private jobs are not allowed by the billing service' do - let(:body) { { private_repos: false, public_repos: true, concurrency_limit: metered_plan_limit } } - - it { expect(selected.size).to eq 0 } - it { expect(reports).to include "user svenfuchs capacities: plan max=#{metered_plan_limit}" } - it { expect(reports).to include 'user svenfuchs: queueable=5 running=1 selected=0 total_waiting=5 waiting_for_concurrency=5' } - end - end - - describe 'with public jobs only' do - before { create_jobs(1, private: false, state: :started) } - before { create_jobs(5, private: false) } - - it { expect(selected.size).to eq 5 } - it { expect(reports).to include "user svenfuchs capacities: plan max=#{metered_plan_limit}" } - it { expect(reports).to include "user svenfuchs plan capacity: running=1 max=#{metered_plan_limit} selected=5" } - it { expect(reports).to include 'user svenfuchs: queueable=5 running=1 selected=5 total_waiting=0 waiting_for_concurrency=0' } - - context 'when public jobs are not allowed by the billing service' do - let(:body) { { private_repos: true, public_repos: false, concurrency_limit: metered_plan_limit } } - - it { expect(selected.size).to eq 0 } - it { expect(reports).to include "user svenfuchs capacities: plan max=#{metered_plan_limit}" } - it { expect(reports).to include 'user svenfuchs: queueable=5 running=1 selected=0 total_waiting=5 waiting_for_concurrency=5' } - end - end - - describe 'for mixed public and private jobs' do - before { create_jobs(1, private: true, state: :started) } - before { create_jobs(1, private: false, state: :started) } - before { create_jobs(2, private: false) + create_jobs(2, private: true) } - - it { expect(selected.size).to eq 4 } - it { expect(reports).to include "user svenfuchs capacities: plan max=#{metered_plan_limit}" } - it { expect(reports).to include "user svenfuchs plan capacity: running=2 max=#{metered_plan_limit} selected=4" } - it { expect(reports).to include 'user svenfuchs: queueable=4 running=2 selected=4 total_waiting=0 waiting_for_concurrency=0' } - - context 'when private jobs are not allowed by the billing service' do - let(:body) { { private_repos: false, public_repos: true, concurrency_limit: metered_plan_limit } } - - it { expect(selected.size).to eq 2 } - it { expect(reports).to include "user svenfuchs capacities: plan max=#{metered_plan_limit}" } - it { expect(reports).to include 'user svenfuchs: queueable=4 running=2 selected=2 total_waiting=2 waiting_for_concurrency=2' } - end - - context 'when public jobs are not allowed by the billing service' do - let(:body) { { private_repos: true, public_repos: false, concurrency_limit: metered_plan_limit } } - - it { expect(selected.size).to eq 2 } - it { expect(reports).to include "user svenfuchs capacities: plan max=#{metered_plan_limit}" } - it { expect(reports).to include 'user svenfuchs: queueable=4 running=2 selected=2 total_waiting=2 waiting_for_concurrency=2' } - end - end - - describe 'stages' do - describe 'with private jobs only' do - let(:one) { FactoryGirl.create(:stage, number: 1) } - let(:two) { FactoryGirl.create(:stage, number: 2) } - let(:three) { FactoryGirl.create(:stage, number: 3) } - - before { create_jobs(1, private: true, stage: one, stage_number: '1.1', state: :started) } - before { create_jobs(1, private: true, stage: one, stage_number: '1.2') } - before { create_jobs(1, private: true, stage: one, stage_number: '1.3') } - before { create_jobs(1, private: true, stage: two, stage_number: '2.1') } - before { create_jobs(1, private: true, stage: three, stage_number: '10.1') } - - describe 'queueing' do - it { expect(selected.size).to eq 2 } - it { expect(reports).to include "repo #{repo.slug} limited by stage on build_id=#{build.id}: rejected=2 selected=2" } - it { expect(reports).to include 'user svenfuchs: queueable=4 running=1 selected=2 total_waiting=2 waiting_for_concurrency=0' } - end - - describe 'ordering' do - before { one.jobs.update_all(state: :passed) } - before { Queueable.where(job_id: one.jobs.pluck(:id)).delete_all } - it { expect(selected[0].id).to eq Job.where(stage_number: '2.1').first.id } - end - end - - describe 'with public jobs only' do - let(:one) { FactoryGirl.create(:stage, number: 1) } - let(:two) { FactoryGirl.create(:stage, number: 2) } - let(:three) { FactoryGirl.create(:stage, number: 3) } - - before { create_jobs(1, stage: one, stage_number: '1.1', state: :started) } - before { create_jobs(1, stage: one, stage_number: '1.2') } - before { create_jobs(1, stage: one, stage_number: '1.3') } - before { create_jobs(1, stage: two, stage_number: '2.1') } - before { create_jobs(1, stage: three, stage_number: '10.1') } - - describe 'queueing' do - it { expect(selected.size).to eq 2 } - it { expect(reports).to include "repo #{repo.slug} limited by stage on build_id=#{build.id}: rejected=2 selected=2" } - it { expect(reports).to include "user svenfuchs plan capacity: running=1 max=#{metered_plan_limit} selected=2" } - it { expect(reports).to include 'user svenfuchs: queueable=4 running=1 selected=2 total_waiting=2 waiting_for_concurrency=0' } - end - - describe 'ordering' do - before { one.jobs.update_all(state: :passed) } - before { Queueable.where(job_id: one.jobs.pluck(:id)).delete_all } - it { expect(selected[0].id).to eq Job.where(stage_number: '2.1').first.id } - end - end - - describe 'for mixed public and private jobs' do - let(:one) { FactoryGirl.create(:stage, number: 1) } - let(:two) { FactoryGirl.create(:stage, number: 2) } - let(:three) { FactoryGirl.create(:stage, number: 3) } - - before { create_jobs(1, private: false, stage: one, stage_number: '1.1', state: :started) } - before { create_jobs(1, private: true, stage: one, stage_number: '1.2') } - before { create_jobs(1, private: false, stage: one, stage_number: '1.3') } - before { create_jobs(1, private: true, stage: two, stage_number: '2.1') } - before { create_jobs(1, private: false, stage: three, stage_number: '10.1') } - - describe 'queueing' do - it { expect(selected.size).to eq 2 } - it { expect(reports).to include "repo #{repo.slug} limited by stage on build_id=#{build.id}: rejected=2 selected=2" } - it { expect(reports).to include "user svenfuchs plan capacity: running=1 max=#{metered_plan_limit} selected=2" } - it { expect(reports).to include 'user svenfuchs: queueable=4 running=1 selected=2 total_waiting=2 waiting_for_concurrency=0' } - end - - describe 'ordering' do - before { one.jobs.update_all(state: :passed) } - before { Queueable.where(job_id: one.jobs.pluck(:id)).delete_all } - it { expect(selected[0].id).to eq Job.where(stage_number: '2.1').first.id } - end - end - end - end end diff --git a/spec/travis/scheduler/serialize/worker_spec.rb b/spec/travis/scheduler/serialize/worker_spec.rb index f4ac1267..8f244dfb 100644 --- a/spec/travis/scheduler/serialize/worker_spec.rb +++ b/spec/travis/scheduler/serialize/worker_spec.rb @@ -245,7 +245,6 @@ def encrypted(value) vm_config: {}, vm_type: :default, vm_size: nil, - vm_size: nil, queue: 'builds.gce', config: { rvm: '1.8.7', diff --git a/spec/travis/scheduler/service/enqueue_owners_spec.rb b/spec/travis/scheduler/service/enqueue_owners_spec.rb index d7d30221..ba58aa5b 100644 --- a/spec/travis/scheduler/service/enqueue_owners_spec.rb +++ b/spec/travis/scheduler/service/enqueue_owners_spec.rb @@ -5,7 +5,7 @@ let(:repo) { FactoryBot.create(:repo, owner:) } let(:owner) { FactoryBot.create(:user) } let(:commit) { FactoryBot.create(:commit) } - let(:build) { FactoryGirl.create(:build, repository: repo, owner: owner, jobs: [job]) } + let(:build) { FactoryBot.create(:build, repository: repo, owner: owner, jobs: [job]) } let(:job_stage) { FactoryGirl.create(:stage) } let(:job) { Job.first } let(:config) { Travis::Scheduler.context.config } diff --git a/spec/travis/scheduler/service/event_spec.rb b/spec/travis/scheduler/service/event_spec.rb index 1cc8f8b4..60be7da1 100644 --- a/spec/travis/scheduler/service/event_spec.rb +++ b/spec/travis/scheduler/service/event_spec.rb @@ -5,7 +5,7 @@ let(:repo) { FactoryBot.create(:repo) } let(:owner) { FactoryBot.create(:user) } let(:build) { FactoryBot.create(:build, repository: repo, owner:, jobs: [job]) } - let(:job_stage) { FactoryGirl.create(:stage) } + let(:job_stage) { FactoryBot.create(:stage) } let(:job) { FactoryBot.create(:job, private: true, state: :created, config: config.to_h, stage_id: job_stage.id) } let(:config) { Travis::Scheduler.context.config } let(:data) { { id: build.id, jid: '1234' } } @@ -29,7 +29,7 @@ it { expect(log).to include 'Evaluating jobs for owner group: user svenfuchs, org travis-ci' } it { expect(log).to include "enqueueing job #{Job.first.id} (svenfuchs/gem-release)" } - # it { expect(log).to include 'user svenfuchs, org travis-ci capacities: public max=5, config max=1' } + it { expect(log).to include 'user svenfuchs, org travis-ci capacities: public max=5, config max=1' } it { expect(log).to include 'user svenfuchs, org travis-ci: queueable=1 running=0 selected=1 total_waiting=0 waiting_for_concurrency=0' @@ -46,24 +46,4 @@ expect(log).to include "I 1234 Owner group scheduler.owners-svenfuchs is locked and already being evaluated. Dropping event build:created for build=#{build.id}" } end - - context do - before { Travis::JobBoard.stubs(:post) } - before { config.limit.delegate = { owner.login => org.login } } - before { config.limit.by_owner = { org.login => 1 } } - describe 'build cancelled while job is not queued yet' do - let(:job_stage) { FactoryGirl.create(:stage, state: 'canceled') } - before { service.run } - it { expect(Job.first.stage.state).to eq 'canceled' } - it { expect(log).to include "Build #{build.id} has been canceled, job #{job.id} being canceled" } - end - - describe 'jobs are queued if there are no stages' do - let(:job) { FactoryGirl.create(:job, private: true, state: :created, config: config.to_h, stage_id: nil) } - before { service.run } - it { expect(Job.first.state).to eq 'queued' } - it { expect(log).to include 'Evaluating jobs for owner group: user svenfuchs, org travis-ci' } - it { expect(log).to include "enqueueing job #{Job.first.id} (svenfuchs/gem-release)" } - end - end end diff --git a/spec/travis/scheduler/service/notify_spec.rb b/spec/travis/scheduler/service/notify_spec.rb index f2cf2acb..1bff722b 100644 --- a/spec/travis/scheduler/service/notify_spec.rb +++ b/spec/travis/scheduler/service/notify_spec.rb @@ -2,7 +2,7 @@ describe Travis::Scheduler::Service::Notify do let(:build) { FactoryGirl.create(:build, repository: repo, owner: owner, jobs: [job]) } - let(:job_stage) { FactoryGirl.create(:stage) } + let(:job_stage) { FactoryBot.create(:stage) } let(:job) { FactoryBot.create(:job, state: :queued, queued_at: Time.parse('2016-01-01T10:30:00Z'), config: {}, stage_id: job_stage.id) } let(:data) { { job: { id: job.id } } } let(:context) { Travis::Scheduler.context } From 1044296f510eb19123300c5e09834bfd707f42cd Mon Sep 17 00:00:00 2001 From: Sebastian Karpeta Date: Tue, 26 Mar 2024 12:34:26 +0100 Subject: [PATCH 76/81] TravisCI Scheduler merge. Enterprise to master. --- spec/travis/scheduler/record/user_spec.rb | 2 +- spec/travis/scheduler/serialize/worker/job_spec.rb | 2 +- spec/travis/scheduler/service/enqueue_owners_spec.rb | 2 +- spec/travis/scheduler/service/notify_spec.rb | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/travis/scheduler/record/user_spec.rb b/spec/travis/scheduler/record/user_spec.rb index b7be9c01..350838fd 100644 --- a/spec/travis/scheduler/record/user_spec.rb +++ b/spec/travis/scheduler/record/user_spec.rb @@ -4,7 +4,7 @@ let(:user) { FactoryBot.create(:user) } let(:repo) { FactoryBot.create(:repository) } let(:authorize_build_url) { "http://localhost:9292/users/#{user.id}/plan" } - let(:repo) { FactoryGirl.create(:repository) } + let(:repo) { FactoryBot.create(:repository) } let(:authorize_build_url) { "http://localhost:9292/users/#{user.id}/plan" } describe 'constants' do diff --git a/spec/travis/scheduler/serialize/worker/job_spec.rb b/spec/travis/scheduler/serialize/worker/job_spec.rb index bc86e5b7..923cf194 100644 --- a/spec/travis/scheduler/serialize/worker/job_spec.rb +++ b/spec/travis/scheduler/serialize/worker/job_spec.rb @@ -179,7 +179,7 @@ end context 'when head repo is present' do - let(:head_repo) { FactoryGirl.create(:repository, github_id: 549744) } + let(:head_repo) { FactoryBot.create(:repository, github_id: 549744) } let(:head_repo_key) { OpenSSL::PKey::RSA.generate(4096) } let(:pull_request) { PullRequest.new(base_repo_slug: 'travis-ci/travis-yml', head_repo_slug: "#{head_repo.owner_name}/#{head_repo.name}") } diff --git a/spec/travis/scheduler/service/enqueue_owners_spec.rb b/spec/travis/scheduler/service/enqueue_owners_spec.rb index ba58aa5b..c1313a81 100644 --- a/spec/travis/scheduler/service/enqueue_owners_spec.rb +++ b/spec/travis/scheduler/service/enqueue_owners_spec.rb @@ -6,7 +6,7 @@ let(:owner) { FactoryBot.create(:user) } let(:commit) { FactoryBot.create(:commit) } let(:build) { FactoryBot.create(:build, repository: repo, owner: owner, jobs: [job]) } - let(:job_stage) { FactoryGirl.create(:stage) } + let(:job_stage) { FactoryBot.create(:stage) } let(:job) { Job.first } let(:config) { Travis::Scheduler.context.config } let(:data) { { owner_type: 'User', owner_id: owner.id, jid: '1234' } } diff --git a/spec/travis/scheduler/service/notify_spec.rb b/spec/travis/scheduler/service/notify_spec.rb index 1bff722b..83490c4d 100644 --- a/spec/travis/scheduler/service/notify_spec.rb +++ b/spec/travis/scheduler/service/notify_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: false describe Travis::Scheduler::Service::Notify do - let(:build) { FactoryGirl.create(:build, repository: repo, owner: owner, jobs: [job]) } + let(:build) { FactoryBot.create(:build, repository: repo, owner: owner, jobs: [job]) } let(:job_stage) { FactoryBot.create(:stage) } let(:job) { FactoryBot.create(:job, state: :queued, queued_at: Time.parse('2016-01-01T10:30:00Z'), config: {}, stage_id: job_stage.id) } let(:data) { { job: { id: job.id } } } From 846ae7ffcf74a0e04d894d23d4ca64e0134b4c51 Mon Sep 17 00:00:00 2001 From: Sebastian Karpeta Date: Tue, 2 Apr 2024 10:22:54 +0200 Subject: [PATCH 77/81] TravisCI Scheduler. New line issue. --- lib/travis/scheduler/service/set_queue.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/travis/scheduler/service/set_queue.rb b/lib/travis/scheduler/service/set_queue.rb index 6ffcf5f6..80829cad 100644 --- a/lib/travis/scheduler/service/set_queue.rb +++ b/lib/travis/scheduler/service/set_queue.rb @@ -34,7 +34,8 @@ def queue rescue => e puts "ERROR while trying to queue: #{e.message}" puts "Backtrace:" - puts e.backtrace.join("\n")@queue ||= redirect(Queue.new(job, config, logger).select) + puts e.backtrace.join("\n") + @queue ||= redirect(Queue.new(job, config, logger).select) end # TODO: confirm we don't need queue redirection any more From cd7bd694d49d2b538c7a9f77c67606806b3e3ad6 Mon Sep 17 00:00:00 2001 From: GbArc Date: Tue, 2 Apr 2024 11:58:31 +0200 Subject: [PATCH 78/81] billing get corrected, job save removed --- lib/travis/scheduler/billing/client.rb | 5 ++++- lib/travis/scheduler/record/job.rb | 4 ---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/travis/scheduler/billing/client.rb b/lib/travis/scheduler/billing/client.rb index aac553fc..7e29acdd 100644 --- a/lib/travis/scheduler/billing/client.rb +++ b/lib/travis/scheduler/billing/client.rb @@ -60,11 +60,14 @@ def request(method, path, params) def get(path, params = {}) body = request(:get, path, params).body - body&.length&.to_i > 0 && body.is_a?(String) ? JSON.parse(body) : body + return {} unless body&.length&.to_i > 0 + + body.is_a?(String) ? JSON.parse(body) : body end def post(path, params = {}) body = request(:post, path, params).body + body&.length&.to_i > 0 && body.is_a?(String) ? JSON.parse(body) : body end diff --git a/lib/travis/scheduler/record/job.rb b/lib/travis/scheduler/record/job.rb index 24621e8f..515812d9 100644 --- a/lib/travis/scheduler/record/job.rb +++ b/lib/travis/scheduler/record/job.rb @@ -4,10 +4,6 @@ class JobConfig < ActiveRecord::Base def config=(config) self.config_json = config if has_attribute?(:config_json) super - end - - def save(arg) - super(arg) rescue Encoding::UndefinedConversionError end end From 353bc62cb6a28244aa03d0b77517f008da6da4c6 Mon Sep 17 00:00:00 2001 From: GbArc Date: Tue, 2 Apr 2024 12:21:19 +0200 Subject: [PATCH 79/81] dist updated to focal / ship:docker --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 3b6c62a3..bcffcdd4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -33,7 +33,7 @@ jobs: - stage: "testing time" script: bundle exec rspec spec - stage: ":ship: it to quay.io" - dist: bionic + dist: focal group: edge ruby: services: From d3ce4e9342f198224eb4b9cc55506c22b01755ab Mon Sep 17 00:00:00 2001 From: GbArc Date: Tue, 2 Apr 2024 12:50:42 +0200 Subject: [PATCH 80/81] removed duplicates from dockerfile, moved exec-hooks / ship:docker --- Dockerfile | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/Dockerfile b/Dockerfile index 5bd47c51..4869ce73 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,6 +22,7 @@ RUN ( \ bundle config set without 'development test'; \ bundle install; \ bundle config --delete https://gems.contribsys.com; \ + gem install --user-install executable-hooks \ apt-get remove -y gcc g++ make git perl && apt-get -y autoremove; \ bundle clean && rm -rf /app/vendor/bundle/ruby/2.7.0/cache/*; \ for i in `find /app/vendor/ -name \*.o -o -name \*.c -o -name \*.h`; do rm -f $i; done; \ @@ -30,23 +31,6 @@ RUN ( \ ENV LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2 # throw errors if Gemfile has been modified since Gemfile.lock -RUN bundle config --global frozen 1 -RUN bundle config set deployment 'true' - -RUN mkdir -p /app -WORKDIR /app - -COPY Gemfile /app -COPY Gemfile.lock /app - -RUN gem install bundler -v '2.4.14' - -ARG bundle_gems__contribsys__com -RUN bundle config https://gems.contribsys.com/ $bundle_gems__contribsys__com \ - && bundle install \ - && bundle config --delete https://gems.contribsys.com/ -RUN gem install --user-install executable-hooks - COPY . /app CMD ["bundle", "exec", "bin/sidekiq-pgbouncer", "${SIDEKIQ_CONCURRENCY:-5}", "${SIDEKIQ_QUEUE:-scheduler}"] From 8cc1bfd0b59e27b317fce1a7ef997a66534cf416 Mon Sep 17 00:00:00 2001 From: GbArc Date: Fri, 12 Apr 2024 13:29:16 +0200 Subject: [PATCH 81/81] dockerfile split ship:docker --- Dockerfile | 42 ++++++++++++++++++++---------------------- Dockerfile.tcie | 36 ++++++++++++++++++++++++++++++++++++ Makefile | 2 +- 3 files changed, 57 insertions(+), 23 deletions(-) create mode 100644 Dockerfile.tcie diff --git a/Dockerfile b/Dockerfile index 4869ce73..53b70345 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,35 +2,33 @@ FROM ruby:3.2.2-slim LABEL maintainer Travis CI GmbH -RUN ( \ - bundle config set no-cache 'true'; \ - bundle config --global frozen 1; \ - bundle config set deployment 'true'; \ - mkdir -p /app; \ -) -WORKDIR /app -COPY Gemfile* /app/ -ARG bundle_gems__contribsys__com +# packages required for bundle install RUN ( \ apt-get update ; \ - apt-get upgrade -y ; \ apt-get install -y --no-install-recommends git make gcc g++ libpq-dev libjemalloc-dev libcurl4 \ - && rm -rf /var/lib/apt/lists/*; \ - gem update --system; \ - gem install bundler -v '2.3.24'; \ - bundle config https://gems.contribsys.com/ $bundle_gems__contribsys__com; \ - bundle config set without 'development test'; \ - bundle install; \ - bundle config --delete https://gems.contribsys.com; \ - gem install --user-install executable-hooks \ - apt-get remove -y gcc g++ make git perl && apt-get -y autoremove; \ - bundle clean && rm -rf /app/vendor/bundle/ruby/2.7.0/cache/*; \ - for i in `find /app/vendor/ -name \*.o -o -name \*.c -o -name \*.h`; do rm -f $i; done; \ + && rm -rf /var/lib/apt/lists/* \ ) ENV LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2 # throw errors if Gemfile has been modified since Gemfile.lock +RUN bundle config --global frozen 1 +RUN bundle config set deployment 'true' + +RUN mkdir -p /app +WORKDIR /app + +COPY Gemfile /app +COPY Gemfile.lock /app + +RUN gem install bundler -v '2.4.14' + +ARG bundle_gems__contribsys__com +RUN bundle config https://gems.contribsys.com/ $bundle_gems__contribsys__com \ + && bundle install \ + && bundle config --delete https://gems.contribsys.com/ +RUN gem install --user-install executable-hooks + COPY . /app -CMD ["bundle", "exec", "bin/sidekiq-pgbouncer", "${SIDEKIQ_CONCURRENCY:-5}", "${SIDEKIQ_QUEUE:-scheduler}"] +CMD bundle exec bin/sidekiq-pgbouncer ${SIDEKIQ_CONCURRENCY:-5} ${SIDEKIQ_QUEUE:-scheduler} diff --git a/Dockerfile.tcie b/Dockerfile.tcie new file mode 100644 index 00000000..4869ce73 --- /dev/null +++ b/Dockerfile.tcie @@ -0,0 +1,36 @@ +FROM ruby:3.2.2-slim + +LABEL maintainer Travis CI GmbH + +RUN ( \ + bundle config set no-cache 'true'; \ + bundle config --global frozen 1; \ + bundle config set deployment 'true'; \ + mkdir -p /app; \ +) +WORKDIR /app +COPY Gemfile* /app/ +ARG bundle_gems__contribsys__com +RUN ( \ + apt-get update ; \ + apt-get upgrade -y ; \ + apt-get install -y --no-install-recommends git make gcc g++ libpq-dev libjemalloc-dev libcurl4 \ + && rm -rf /var/lib/apt/lists/*; \ + gem update --system; \ + gem install bundler -v '2.3.24'; \ + bundle config https://gems.contribsys.com/ $bundle_gems__contribsys__com; \ + bundle config set without 'development test'; \ + bundle install; \ + bundle config --delete https://gems.contribsys.com; \ + gem install --user-install executable-hooks \ + apt-get remove -y gcc g++ make git perl && apt-get -y autoremove; \ + bundle clean && rm -rf /app/vendor/bundle/ruby/2.7.0/cache/*; \ + for i in `find /app/vendor/ -name \*.o -o -name \*.c -o -name \*.h`; do rm -f $i; done; \ +) + +ENV LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2 + +# throw errors if Gemfile has been modified since Gemfile.lock +COPY . /app + +CMD ["bundle", "exec", "bin/sidekiq-pgbouncer", "${SIDEKIQ_CONCURRENCY:-5}", "${SIDEKIQ_QUEUE:-scheduler}"] diff --git a/Makefile b/Makefile index 6781449b..34dd4f32 100644 --- a/Makefile +++ b/Makefile @@ -31,7 +31,7 @@ DOCKER ?= docker .PHONY: docker-build docker-build: - $(DOCKER) build --no-cache --pull --build-arg bundle_gems__contribsys__com=$(BUNDLE_GEMS__CONTRIBSYS__COM) -t $(DOCKER_DEST) . + $(DOCKER) build --no-cache --pull --build-arg bundle_gems__contribsys__com=$(BUNDLE_GEMS__CONTRIBSYS__COM) -t $(DOCKER_DEST) . -f Dockerfile.tcie .PHONY: docker-login docker-login: