diff --git a/app/models/pages_deployment.rb b/app/models/pages_deployment.rb index 78e0f185a11043ef3a3d625594da3abb4f7f5230..d8f122cfb23d27572c3e98161945be23a8a8be7e 100644 --- a/app/models/pages_deployment.rb +++ b/app/models/pages_deployment.rb @@ -2,10 +2,14 @@ # PagesDeployment stores a zip archive containing GitLab Pages web-site class PagesDeployment < ApplicationRecord + include FileStoreMounter + belongs_to :project, optional: false belongs_to :ci_build, class_name: 'Ci::Build', optional: true validates :file, presence: true validates :file_store, presence: true, inclusion: { in: ObjectStorage::SUPPORTED_STORES } validates :size, presence: true, numericality: { greater_than: 0, only_integer: true } + + mount_file_store_uploader ::Pages::DeploymentUploader end diff --git a/app/uploaders/pages/deployment_uploader.rb b/app/uploaders/pages/deployment_uploader.rb new file mode 100644 index 0000000000000000000000000000000000000000..4fe1a548a05080ada8438f4264eced2d74d15ca1 --- /dev/null +++ b/app/uploaders/pages/deployment_uploader.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Pages + class DeploymentUploader < GitlabUploader + include ObjectStorage::Concern + + storage_options Gitlab.config.pages + + alias_method :upload, :model + + private + + def dynamic_segment + Gitlab::HashedPath.new('pages_deployments', model.id, root_hash: model.project_id) + end + + # @hashed is chosen to avoid conflict with namespace name because we use the same directory for storage + # @ is not valid character for namespace + def base_dir + "@hashed" + end + end +end diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 605729a1435eade3fd0901197cb8a14c7313e198..86df39830df4ac2d0b5df40a633d8fb81a1d35e2 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -394,6 +394,14 @@ production: &base # File that contains the shared secret key for verifying access for gitlab-pages. # Default is '.gitlab_pages_secret' relative to Rails.root (i.e. root of the GitLab app). # secret_file: /home/git/gitlab/.gitlab_pages_secret + object_store: + enabled: false + remote_directory: pages # The bucket name + connection: + provider: AWS + aws_access_key_id: AWS_ACCESS_KEY_ID + aws_secret_access_key: AWS_SECRET_ACCESS_KEY + region: us-east-1 ## Mattermost ## For enabling Add to Mattermost button @@ -1318,6 +1326,14 @@ test: # user: YOUR_USERNAME pages: path: tmp/tests/pages + object_store: + enabled: false + remote_directory: pages # The bucket name + connection: + provider: AWS + aws_access_key_id: AWS_ACCESS_KEY_ID + aws_secret_access_key: AWS_SECRET_ACCESS_KEY + region: us-east-1 repositories: storages: default: diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 2fa2cfb9b6623100ef5bdd5072e4f9ff5a347282..635248d842342500c16d477670f027f5c6b9f8a7 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -297,6 +297,10 @@ Settings.pages['external_https'] ||= false unless Settings.pages['external_https'].present? Settings.pages['artifacts_server'] ||= Settings.pages['enabled'] if Settings.pages['artifacts_server'].nil? Settings.pages['secret_file'] ||= Rails.root.join('.gitlab_pages_secret') +# We want pages zip archives to be stored on the same directory as old pages hierarchical structure +# this will allow us to easier migrate existing instances with NFS +Settings.pages['storage_path'] = Settings.pages['path'] +Settings.pages['object_store'] = ObjectStoreSettings.legacy_parse(Settings.pages['object_store']) # # Geo diff --git a/spec/factories/pages_deployments.rb b/spec/factories/pages_deployments.rb index 1bea003d68375987da9fb8da20d75db869a4b10c..20b8e4782df28ade4a2d267ec2b9022c353fd6c0 100644 --- a/spec/factories/pages_deployments.rb +++ b/spec/factories/pages_deployments.rb @@ -4,9 +4,12 @@ factory :pages_deployment, class: 'PagesDeployment' do project file_store { ObjectStorage::SUPPORTED_STORES.first } - size { 1.megabytes } - # TODO: replace with proper file uploaded in https://gitlab.com/gitlab-org/gitlab/-/issues/245295 - file { "dummy string" } + after(:build) do |deployment, _evaluator| + deployment.file = fixture_file_upload( + Rails.root.join("spec/fixtures/pages.zip") + ) + deployment.size = deployment.file.size + end end end diff --git a/spec/support/helpers/stub_object_storage.rb b/spec/support/helpers/stub_object_storage.rb index 476b7d34ee56f49aeef49f49d5cb7c7704b55e77..60b320e5af09dd20ca600dc3dfb9eeedf07736f5 100644 --- a/spec/support/helpers/stub_object_storage.rb +++ b/spec/support/helpers/stub_object_storage.rb @@ -89,6 +89,13 @@ def stub_terraform_state_object_storage(uploader = described_class, **params) **params) end + def stub_pages_object_storage(uploader = described_class, **params) + stub_object_storage_uploader(config: Gitlab.config.pages.object_store, + uploader: uploader, + remote_directory: 'pages', + **params) + end + def stub_object_storage_multipart_init(endpoint, upload_id = "upload_id") stub_request(:post, %r{\A#{endpoint}tmp/uploads/[a-z0-9-]*\?uploads\z}) .to_return status: 200, body: <<-EOS.strip_heredoc diff --git a/spec/uploaders/pages/deployment_uploader_spec.rb b/spec/uploaders/pages/deployment_uploader_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..1d9a0efe228f4f6e0df5240c0a55873bd3d5033a --- /dev/null +++ b/spec/uploaders/pages/deployment_uploader_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Pages::DeploymentUploader do + let(:pages_deployment) { create(:pages_deployment) } + let(:uploader) { described_class.new(pages_deployment, :file) } + + subject { uploader } + + it_behaves_like "builds correct paths", + store_dir: %r[/\h{2}/\h{2}/\h{64}/pages_deployments/\d+], + cache_dir: %r[pages/@hashed/tmp/cache], + work_dir: %r[pages/@hashed/tmp/work] + + context 'when object store is REMOTE' do + before do + stub_pages_object_storage + end + + include_context 'with storage', described_class::Store::REMOTE + + it_behaves_like 'builds correct paths', store_dir: %r[\A\h{2}/\h{2}/\h{64}/pages_deployments/\d+\z] + end + + context 'when file is stored in valid local_path' do + let(:file) do + fixture_file_upload("spec/fixtures/pages.zip") + end + + before do + uploader.store!(file) + end + + subject { uploader.file.path } + + it { is_expected.to match(%r[#{uploader.root}/@hashed/\h{2}/\h{2}/\h{64}/pages_deployments/#{pages_deployment.id}/pages.zip]) } + end +end