diff --git a/app/controllers/projects/pages_controller.rb b/app/controllers/projects/pages_controller.rb index 7500ae6cc8a460c8100dfd5afe463d840d4f481a..a20bd6ac1ec48dbb53f42c28940933646b620451 100644 --- a/app/controllers/projects/pages_controller.rb +++ b/app/controllers/projects/pages_controller.rb @@ -53,6 +53,23 @@ def update end end + def regenerate_unique_domain + return render_403 unless can?(current_user, :update_pages, @project) + return render_403 unless @project.project_setting.pages_unique_domain_enabled? + + result = Gitlab::Pages.generate_unique_domain(@project) + + respond_to do |format| + format.html do + if result && @project.project_setting.update(pages_unique_domain: result) + redirect_to project_pages_path(@project), notice: _('Successfully regenerated unique domain') + else + redirect_to project_pages_path(@project), alert: _('Failed to regenerate unique domain') + end + end + end + end + private def project_params diff --git a/app/views/projects/pages/_access.html.haml b/app/views/projects/pages/_access.html.haml index e357f6a05b8531f2065c0fbd72449cb275907141..9620b7c7339263d1d672db48d78d5a3684c40279 100644 --- a/app/views/projects/pages/_access.html.haml +++ b/app/views/projects/pages/_access.html.haml @@ -6,8 +6,16 @@ icon: 'doc-text', count: @project.pages_domains.size + (pages_url ? 1 : 0), options: { class: 'gl-mt-5', data: { testid: 'access-page-container' } }, - footer_options: { class: 'gl-bg-red-50' }) do |c| - - c.with_body do + footer_options: { class: 'gl-bg-red-50' }) do |content| + - if @project.project_setting.pages_unique_domain_enabled? + - content.with_actions do + = render Pajamas::ButtonComponent.new(href: regenerate_unique_domain_project_pages_path(@project), + method: :post, + size: :small, + button_options: { data: { confirm: s_('GitLabPages|Are you sure you want to regenerate the unique domain? The previous URL will stop working.'), 'confirm-btn-variant': 'danger' }, + "aria-label": s_('GitLabPages|Regenerate unique domain') }) do + = s_('GitLabPages|Regenerate unique domain') + - content.with_body do %ul.content-list %li = external_link(pages_url_text, pages_url) @@ -17,7 +25,7 @@ = external_link(domain.url, domain.url) - unless @project.public_pages? - - c.with_footer do + - content.with_footer do - help_page = help_page_path('user/project/pages/pages_access_control.md') - link_start = '<a href="%{url}" target="_blank" class="gl-alert-link" rel="noopener noreferrer">'.html_safe % { url: help_page } - link_end = '</a>'.html_safe diff --git a/config/routes/project.rb b/config/routes/project.rb index fb7b9a3e5cff5fe1af2e6ae9ac298b78af24aa95..9f5fd1c5949d19eb2a43814357425f89a3e16432 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -530,7 +530,8 @@ defaults: { format: 'json' }, constraints: { template_type: %r{issue|merge_request}, format: 'json' } - resource :pages, only: [:new, :show, :update, :destroy] do # rubocop: disable Cop/PutProjectRoutesUnderScope + resource :pages, only: [:new, :show, :update, :destroy, :regenerate_unique_domain] do # rubocop: disable Cop/PutProjectRoutesUnderScope + post :regenerate_unique_domain # rubocop:todo Cop/PutProjectRoutesUnderScope resources :domains, except: :index, controller: 'pages_domains', constraints: { id: %r{[^/]+} } do # rubocop: disable Cop/PutProjectRoutesUnderScope member do post :verify # rubocop:todo Cop/PutProjectRoutesUnderScope diff --git a/doc/user/project/pages/introduction.md b/doc/user/project/pages/introduction.md index d06d4008e8c637bd9c2327219e3ca1fa2eb877b0..b8a5d1799a41401d7876966efffcd9e4a5491ea0 100644 --- a/doc/user/project/pages/introduction.md +++ b/doc/user/project/pages/introduction.md @@ -298,6 +298,26 @@ as artifacts, and GitLab doesn't know which one you want to deploy. The previous YAML example uses [user-defined job names](index.md#user-defined-job-names). +## Regenerate unique domain for GitLab Pages + +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/481746) in GitLab 17.7. + +You can regenerate the unique domain for your GitLab Pages site. + +After the domain is regenerated, the previous URL is no longer active. +If anyone tries to access the old URL, they'll receive a `404` error. + +Prerequisites + +- You must have at least the Maintainer role for the project. +- The **Use unique domain** setting [must be enabled](index.md#unique-domains) in your project's Pages settings. + +To regenerate a unique domain for your GitLab Pages site: + +1. On the left sidebar, select **Deploy > Pages**. +1. Next to **Access pages**, press **Regenerate unique domain**. +1. GitLab generates a new unique domain for your Pages site. + ## Known issues For a list of known issues, see the GitLab [public issue tracker](https://gitlab.com/gitlab-org/gitlab/-/issues?label_name[]=Category%3APages). diff --git a/lib/gitlab/pages.rb b/lib/gitlab/pages.rb index f2a5d6f4f8507641efe1673c761f025e996438a9..2596713120961aa8a7279a387287ba929372c821 100644 --- a/lib/gitlab/pages.rb +++ b/lib/gitlab/pages.rb @@ -57,11 +57,12 @@ def multiple_versions_enabled_for?(project) project.licensed_feature_available?(:pages_multiple_versions) end - private - def generate_unique_domain(project) 10.times do pages_unique_domain = Gitlab::Pages::RandomDomain.generate(project_path: project.path) + + return false if pages_unique_domain.blank? + return pages_unique_domain unless ProjectSetting.unique_domain_exists?(pages_unique_domain) || Namespace.top_level.by_path(pages_unique_domain).present? diff --git a/locale/gitlab.pot b/locale/gitlab.pot index f38f1f073dd2dde0a629d3ba7cd95a68bcb7296d..53f0c0ecaa120a00b718790201783138b44e39e8 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -23398,6 +23398,9 @@ msgstr "" msgid "Failed to publish issue on status page." msgstr "" +msgid "Failed to regenerate unique domain" +msgstr "" + msgid "Failed to remove a Zoom meeting" msgstr "" @@ -25271,6 +25274,9 @@ msgstr "" msgid "GitLabPages|Access pages" msgstr "" +msgid "GitLabPages|Are you sure you want to regenerate the unique domain? The previous URL will stop working." +msgstr "" + msgid "GitLabPages|Are you sure?" msgstr "" @@ -25310,6 +25316,9 @@ msgstr "" msgid "GitLabPages|Pages" msgstr "" +msgid "GitLabPages|Regenerate unique domain" +msgstr "" + msgid "GitLabPages|Remove certificate" msgstr "" @@ -54604,6 +54613,9 @@ msgstr "" msgid "Successfully linked ID(s): %{item_ids}." msgstr "" +msgid "Successfully regenerated unique domain" +msgstr "" + msgid "Successfully removed email." msgstr "" diff --git a/spec/controllers/projects/pages_controller_spec.rb b/spec/controllers/projects/pages_controller_spec.rb index a9fc8082ae9b497068dc7353ffb2d386dd0d1ae4..8afc4ed7b805e926057828b29a7b3c16c62535e0 100644 --- a/spec/controllers/projects/pages_controller_spec.rb +++ b/spec/controllers/projects/pages_controller_spec.rb @@ -219,4 +219,87 @@ end end end + + describe 'POST regenerate_unique_domain' do + before do + project.project_setting.update!( + pages_unique_domain_enabled: true, + pages_unique_domain: 'pages-abcde' + ) + end + + context 'when update is successful' do + it 'redirects with success message' do + original_domain = project.project_setting.pages_unique_domain + + post :regenerate_unique_domain, params: { namespace_id: project.namespace, project_id: project } + + expect(response).to redirect_to(project_pages_path(project)) + expect(flash[:notice]).to eq('Successfully regenerated unique domain') + project.reload + expect(project.project_setting.pages_unique_domain).not_to eq(original_domain) + expect(project.project_setting.pages_unique_domain).to be_present + end + end + + context 'when update fails' do + before do + allow(Gitlab::Pages::RandomDomain).to receive(:generate).and_return(false) + end + + it 'redirects with error message when domain regeneration fails' do + post :regenerate_unique_domain, params: { namespace_id: project.namespace, project_id: project } + + expect(response).to redirect_to(project_pages_path(project)) + expect(flash[:alert]).to eq('Failed to regenerate unique domain') + end + + it 'redirects with error message when project setting update fails' do + allow_next_instance_of(ProjectSetting) do |instance| + allow(instance).to receive(:update).and_return(false) + end + + post :regenerate_unique_domain, params: { namespace_id: project.namespace, project_id: project } + + expect(response).to redirect_to(project_pages_path(project)) + expect(flash[:alert]).to eq('Failed to regenerate unique domain') + end + end + + context 'when user does not have permission' do + before do + project.add_developer(user) + end + + it 'returns 404' do + post :regenerate_unique_domain, params: { namespace_id: project.namespace, project_id: project } + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when unique domains is not enabled' do + before do + project.project_setting.update!(pages_unique_domain_enabled: false) + end + + it 'returns 403' do + post :regenerate_unique_domain, params: { namespace_id: project.namespace, project_id: project } + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'when pages is disabled' do + before do + allow(Gitlab.config.pages).to receive(:enabled).and_return(false) + end + + it 'returns 404 status' do + post :regenerate_unique_domain, params: { namespace_id: project.namespace, project_id: project } + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end end diff --git a/spec/lib/gitlab/pages_spec.rb b/spec/lib/gitlab/pages_spec.rb index dcf102d8e1c6330192174bcbaacfc75c446cd827..907ea6511ad8d4ab0cce678eb7a418e2584e1500 100644 --- a/spec/lib/gitlab/pages_spec.rb +++ b/spec/lib/gitlab/pages_spec.rb @@ -177,6 +177,78 @@ end end + describe '.generate_unique_domain' do + let(:project) { create(:project, path: 'test-project') } + + context 'when a unique domain can be generated' do + before do + allow(Gitlab::Pages::RandomDomain).to receive(:generate) + .with(project_path: project.path) + .and_return('unique-domain-123') + + allow(ProjectSetting).to receive(:unique_domain_exists?) + .with('unique-domain-123') + .and_return(false) + end + + it 'returns the generated unique domain' do + expect(described_class.generate_unique_domain(project)).to eq('unique-domain-123') + end + + it 'attempts generation only once when first attempt succeeds' do + expect(Gitlab::Pages::RandomDomain).to receive(:generate).once + + described_class.generate_unique_domain(project) + end + end + + context 'when first attempts fail but later succeeds' do + before do + # First two attempts generate existing domains + allow(Gitlab::Pages::RandomDomain).to receive(:generate) + .with(project_path: project.path) + .and_return('existing-domain-1', 'existing-domain-2', 'unique-domain-123') + + allow(ProjectSetting).to receive(:unique_domain_exists?) + .with('existing-domain-1').and_return(true) + allow(ProjectSetting).to receive(:unique_domain_exists?) + .with('existing-domain-2').and_return(true) + allow(ProjectSetting).to receive(:unique_domain_exists?) + .with('unique-domain-123').and_return(false) + end + + it 'returns the first unique domain generated' do + expect(described_class.generate_unique_domain(project)).to eq('unique-domain-123') + end + end + + context 'when unique domain generation fails after all attempts' do + before do + allow(Gitlab::Pages::RandomDomain).to receive(:generate) + .with(project_path: project.path) + .and_return('existing-domain') + + allow(ProjectSetting).to receive(:unique_domain_exists?) + .with('existing-domain') + .and_return(true) + end + + it 'raises UniqueDomainGenerationFailure after 10 attempts' do + expect(Gitlab::Pages::RandomDomain).to receive(:generate).exactly(10).times + + expect { described_class.generate_unique_domain(project) } + .to raise_error(Gitlab::Pages::UniqueDomainGenerationFailure) + end + end + + context 'when project is nil' do + it 'raises NoMethodError' do + expect { described_class.generate_unique_domain(nil) } + .to raise_error(NoMethodError) + end + end + end + describe '#update_default_domain_redirect' do let(:project) { build(:project) }