diff --git a/app/models/project_setting.rb b/app/models/project_setting.rb index f15923ce672469a5d7f26ee034daf94286fb2f65..ac25c3f3c2f204fb88c3198f18e6eedb5d33ef6c 100644 --- a/app/models/project_setting.rb +++ b/app/models/project_setting.rb @@ -54,6 +54,11 @@ class ProjectSetting < ApplicationRecord Feature.enabled?(:legacy_open_source_license_available, type: :ops) end + # Checks if a given domain is already assigned to any existing project + def self.unique_domain_exists?(domain) + where(pages_unique_domain: domain).exists? + end + def squash_enabled_by_default? %w[always default_on].include?(squash_option) end diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb index a0748015e4834cbbf7598ba6a59b7fb9dbebcd0f..93821f016edd646cf8a89a21bfaa0280d2914610 100644 --- a/app/services/projects/update_service.rb +++ b/app/services/projects/update_service.rb @@ -39,10 +39,10 @@ def execute else update_failed! end - rescue ValidationError => e - error(e.message) rescue ApiError => e error(e.message, status: :api_error) + rescue ValidationError, Gitlab::Pages::UniqueDomainGenerationFailure => e + error(e.message) end def run_auto_devops_pipeline? diff --git a/lib/gitlab/pages.rb b/lib/gitlab/pages.rb index defe054d631261b9e4687d9ea9e0475223ad7d36..928248c123524953caeb22389935c800d3a1c834 100644 --- a/lib/gitlab/pages.rb +++ b/lib/gitlab/pages.rb @@ -9,6 +9,12 @@ module Pages include JwtAuthenticatable + class UniqueDomainGenerationFailure < StandardError + def initialize(msg = "Can't generate unique domain for GitLab Pages") + super(msg) + end + end + class << self def verify_api_request(request_headers) decode_jwt(request_headers[INTERNAL_API_REQUEST_HEADER], issuer: 'gitlab-pages') @@ -35,9 +41,7 @@ def add_unique_domain_to(project) return if project.project_setting.pages_unique_domain_in_database.present? project.project_setting.pages_unique_domain_enabled = true - project.project_setting.pages_unique_domain = Gitlab::Pages::RandomDomain.generate( - project_path: project.path, - namespace_path: project.parent.full_path) + project.project_setting.pages_unique_domain = generate_unique_domain(project) end def multiple_versions_enabled_for?(project) @@ -47,6 +51,17 @@ def multiple_versions_enabled_for?(project) project.licensed_feature_available?(:pages_multiple_versions) && project.project_setting.pages_multiple_versions_enabled end + + private + + def generate_unique_domain(project) + 10.times do + pages_unique_domain = Gitlab::Pages::RandomDomain.generate(project_path: project.path) + return pages_unique_domain unless ProjectSetting.unique_domain_exists?(pages_unique_domain) + end + + raise UniqueDomainGenerationFailure + end end end end diff --git a/lib/gitlab/pages/random_domain.rb b/lib/gitlab/pages/random_domain.rb index 8aa7611c91042a8d999887b64c2db471638e4c18..6d71639d3af39695fa7257236e5fe10e4529f987 100644 --- a/lib/gitlab/pages/random_domain.rb +++ b/lib/gitlab/pages/random_domain.rb @@ -3,41 +3,27 @@ module Gitlab module Pages class RandomDomain - PROJECT_PATH_LIMIT = 48 - SUBDOMAIN_LABEL_LIMIT = 63 + PROJECT_PATH_LIMIT = 56 - def self.generate(project_path:, namespace_path:) - new(project_path: project_path, namespace_path: namespace_path).generate + def self.generate(project_path:) + new(project_path: project_path).generate end - def initialize(project_path:, namespace_path:) + def initialize(project_path:) @project_path = project_path - @namespace_path = namespace_path end # Subdomains have a limit of 63 bytes (https://www.freesoft.org/CIE/RFC/1035/9.htm) # For this reason we're limiting each part of the unique subdomain # - # The domain is made up of 3 parts, like: projectpath-namespacepath-randomstring - # - project path: between 1 and 48 chars - # - namespace path: when the project path has less than 48 chars, - # the namespace full path will be used to fill the value up to 48 chars - # - random hexadecimal: to ensure a random value, the domain is then filled - # with a random hexadecimal value to complete 63 chars + # The domain is made up of 2 parts, like: projectpath-randomstring + # - project path: between 1 and 56 chars + # - random hexadecimal: to ensure a random value of length 6 def generate domain = project_path.byteslice(0, PROJECT_PATH_LIMIT) - # if the project_path has less than PROJECT_PATH_LIMIT chars, - # fill the domain with the parent full_path up to 48 chars like: - # projectpath-namespacepath - if domain.length < PROJECT_PATH_LIMIT - namespace_size = PROJECT_PATH_LIMIT - domain.length - 1 - domain.concat('-', namespace_path.byteslice(0, namespace_size)) - end - - # Complete the domain with random hexadecimal values util it is 63 chars long # PS.: SecureRandom.hex return an string twice the size passed as argument. - domain.concat('-', SecureRandom.hex(SUBDOMAIN_LABEL_LIMIT - domain.length - 1)) + domain.concat('-', SecureRandom.hex(3)) # Slugify ensures the format and size (63 chars) of the given string Gitlab::Utils.slugify(domain) @@ -45,7 +31,7 @@ def generate private - attr_reader :project_path, :namespace_path + attr_reader :project_path end end end diff --git a/spec/lib/gitlab/pages/random_domain_spec.rb b/spec/lib/gitlab/pages/random_domain_spec.rb index 978412bb72cca8da9348d038c693539aa3df9ca1..697f3360a42a330da704b00e635d148e351cb0ac 100644 --- a/spec/lib/gitlab/pages/random_domain_spec.rb +++ b/spec/lib/gitlab/pages/random_domain_spec.rb @@ -3,10 +3,8 @@ require 'spec_helper' RSpec.describe Gitlab::Pages::RandomDomain, feature_category: :pages do - let(:namespace_path) { 'namespace' } - subject(:generator) do - described_class.new(project_path: project_path, namespace_path: namespace_path) + described_class.new(project_path: project_path) end RSpec.shared_examples 'random domain' do |domain| @@ -14,31 +12,30 @@ expect(SecureRandom) .to receive(:hex) .and_wrap_original do |_, size, _| - ('h' * size) + ('h' * size * 2) end generated = generator.generate expect(generated).to eq(domain) - expect(generated.length).to eq(63) end end context 'when project path is less than 48 chars' do let(:project_path) { 'p' } - it_behaves_like 'random domain', 'p-namespace-hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh' + it_behaves_like 'random domain', 'p-hhhhhh' end context 'when project path is close to 48 chars' do - let(:project_path) { 'p' * 45 } + let(:project_path) { 'p' * 56 } - it_behaves_like 'random domain', 'ppppppppppppppppppppppppppppppppppppppppppppp-na-hhhhhhhhhhhhhh' + it_behaves_like 'random domain', 'pppppppppppppppppppppppppppppppppppppppppppppppppppppppp-hhhhhh' end context 'when project path is larger than 48 chars' do - let(:project_path) { 'p' * 49 } + let(:project_path) { 'p' * 57 } - it_behaves_like 'random domain', 'pppppppppppppppppppppppppppppppppppppppppppppppp-hhhhhhhhhhhhhh' + it_behaves_like 'random domain', 'pppppppppppppppppppppppppppppppppppppppppppppppppppppppp-hhhhhh' end end diff --git a/spec/lib/gitlab/pages_spec.rb b/spec/lib/gitlab/pages_spec.rb index c20956788ac5dfa16a2111ab661afbc408fe3b9f..928e5a6c461169d0b418ffe357c8038e3b9aed8d 100644 --- a/spec/lib/gitlab/pages_spec.rb +++ b/spec/lib/gitlab/pages_spec.rb @@ -131,6 +131,36 @@ expect(project.project_setting.pages_unique_domain).to eq('unique-domain') end end + + context 'when a unique domain is already in use and needs to generate a new one' do + it 'generates a different unique domain if the original is already taken' do + allow(Gitlab::Pages::RandomDomain).to receive(:generate).and_return('existing-domain', 'new-unique-domain') + + # Simulate the existing domain being in use + create(:project_setting, pages_unique_domain: 'existing-domain') + + described_class.add_unique_domain_to(project) + + expect(project.project_setting.pages_unique_domain_enabled).to eq(true) + expect(project.project_setting.pages_unique_domain).to eq('new-unique-domain') + end + end + + context 'when generated 10 unique domains are already in use' do + it 'raises an error' do + allow(Gitlab::Pages::RandomDomain).to receive(:generate).and_return('existing-domain') + + # Simulate the existing domain being in use + create(:project_setting, pages_unique_domain: 'existing-domain') + + expect { described_class.add_unique_domain_to(project) }.to raise_error( + described_class::UniqueDomainGenerationFailure, + "Can't generate unique domain for GitLab Pages" + ) + + expect(project.project_setting.pages_unique_domain).to be_nil + end + end end end end