summaryrefslogtreecommitdiff
path: root/spec/uploaders/object_storage_spec.rb
diff options
context:
space:
mode:
Diffstat (limited to 'spec/uploaders/object_storage_spec.rb')
-rw-r--r--spec/uploaders/object_storage_spec.rb329
1 files changed, 292 insertions, 37 deletions
diff --git a/spec/uploaders/object_storage_spec.rb b/spec/uploaders/object_storage_spec.rb
index 5344dbeb512..1566021934a 100644
--- a/spec/uploaders/object_storage_spec.rb
+++ b/spec/uploaders/object_storage_spec.rb
@@ -8,7 +8,7 @@ class Implementation < GitlabUploader
include ::RecordsUploads::Concern
prepend ::ObjectStorage::Extension::RecordsUploads
- storage_options Gitlab.config.uploads
+ storage_location :uploads
private
@@ -18,10 +18,12 @@ class Implementation < GitlabUploader
end
end
-RSpec.describe ObjectStorage do
+# TODO: Update feature_category once object storage group ownership has been determined.
+RSpec.describe ObjectStorage, :clean_gitlab_redis_shared_state, feature_category: :shared do
let(:uploader_class) { Implementation }
let(:object) { build_stubbed(:user) }
- let(:uploader) { uploader_class.new(object, :file) }
+ let(:file_column) { :file }
+ let(:uploader) { uploader_class.new(object, file_column) }
describe '#object_store=' do
before do
@@ -103,6 +105,34 @@ RSpec.describe ObjectStorage do
expect(subject).to eq("my/prefix/user/#{object.id}/filename")
end
end
+
+ context 'when model has final path defined for the file column' do
+ # Changing this to `foo` to make a point that not all uploaders are mounted
+ # as `file`. They can be mounted as different names, for example, `avatar`.
+ let(:file_column) { :foo }
+
+ before do
+ allow(object).to receive(:foo_final_path).and_return('123-final-path')
+ end
+
+ it 'uses the final path instead' do
+ expect(subject).to eq('123-final-path')
+ end
+
+ context 'and a bucket prefix is configured' do
+ before do
+ allow(uploader_class).to receive(:object_store_options) do
+ double(
+ bucket_prefix: 'my/prefix'
+ )
+ end
+ end
+
+ it 'uses the prefix with the final path' do
+ expect(subject).to eq("my/prefix/123-final-path")
+ end
+ end
+ end
end
end
end
@@ -446,7 +476,7 @@ RSpec.describe ObjectStorage do
end
describe '#fog_credentials' do
- let(:connection) { Settingslogic.new("provider" => "AWS") }
+ let(:connection) { GitlabSettings::Options.build("provider" => "AWS") }
before do
allow(uploader_class).to receive(:options) do
@@ -479,7 +509,7 @@ RSpec.describe ObjectStorage do
}
end
- let(:options) { Settingslogic.new(raw_options) }
+ let(:options) { GitlabSettings::Options.build(raw_options) }
before do
allow(uploader_class).to receive(:options) do
@@ -494,8 +524,15 @@ RSpec.describe ObjectStorage do
describe '.workhorse_authorize' do
let(:has_length) { true }
let(:maximum_size) { nil }
+ let(:use_final_store_path) { false }
- subject { uploader_class.workhorse_authorize(has_length: has_length, maximum_size: maximum_size) }
+ subject do
+ uploader_class.workhorse_authorize(
+ has_length: has_length,
+ maximum_size: maximum_size,
+ use_final_store_path: use_final_store_path
+ )
+ end
context 'when FIPS is enabled', :fips_mode do
it 'response enables FIPS' do
@@ -528,18 +565,23 @@ RSpec.describe ObjectStorage do
shared_examples 'uses remote storage' do
it_behaves_like 'returns the maximum size given' do
- it "returns remote store" do
+ it "returns remote object properties for a temporary upload" do
is_expected.to have_key(:RemoteObject)
expect(subject[:RemoteObject]).to have_key(:ID)
expect(subject[:RemoteObject]).to include(Timeout: a_kind_of(Integer))
expect(subject[:RemoteObject][:Timeout]).to be(ObjectStorage::DirectUpload::TIMEOUT)
- expect(subject[:RemoteObject]).to have_key(:GetURL)
- expect(subject[:RemoteObject]).to have_key(:DeleteURL)
- expect(subject[:RemoteObject]).to have_key(:StoreURL)
- expect(subject[:RemoteObject][:GetURL]).to include(described_class::TMP_UPLOAD_PATH)
- expect(subject[:RemoteObject][:DeleteURL]).to include(described_class::TMP_UPLOAD_PATH)
- expect(subject[:RemoteObject][:StoreURL]).to include(described_class::TMP_UPLOAD_PATH)
+
+ upload_path = File.join(described_class::TMP_UPLOAD_PATH, subject[:RemoteObject][:ID])
+
+ expect(subject[:RemoteObject][:GetURL]).to include(upload_path)
+ expect(subject[:RemoteObject][:DeleteURL]).to include(upload_path)
+ expect(subject[:RemoteObject][:StoreURL]).to include(upload_path)
+ expect(subject[:RemoteObject][:SkipDelete]).to eq(false)
+
+ ::Gitlab::Redis::SharedState.with do |redis|
+ expect(redis.hlen(ObjectStorage::PendingDirectUpload::KEY)).to be_zero
+ end
end
end
end
@@ -551,12 +593,12 @@ RSpec.describe ObjectStorage do
expect(subject[:RemoteObject]).to have_key(:MultipartUpload)
expect(subject[:RemoteObject][:MultipartUpload]).to have_key(:PartSize)
- expect(subject[:RemoteObject][:MultipartUpload]).to have_key(:PartURLs)
- expect(subject[:RemoteObject][:MultipartUpload]).to have_key(:CompleteURL)
- expect(subject[:RemoteObject][:MultipartUpload]).to have_key(:AbortURL)
- expect(subject[:RemoteObject][:MultipartUpload][:PartURLs]).to all(include(described_class::TMP_UPLOAD_PATH))
- expect(subject[:RemoteObject][:MultipartUpload][:CompleteURL]).to include(described_class::TMP_UPLOAD_PATH)
- expect(subject[:RemoteObject][:MultipartUpload][:AbortURL]).to include(described_class::TMP_UPLOAD_PATH)
+
+ upload_path = File.join(described_class::TMP_UPLOAD_PATH, subject[:RemoteObject][:ID])
+
+ expect(subject[:RemoteObject][:MultipartUpload][:PartURLs]).to all(include(upload_path))
+ expect(subject[:RemoteObject][:MultipartUpload][:CompleteURL]).to include(upload_path)
+ expect(subject[:RemoteObject][:MultipartUpload][:AbortURL]).to include(upload_path)
end
end
end
@@ -570,6 +612,80 @@ RSpec.describe ObjectStorage do
end
end
+ shared_examples 'handling object storage final upload path' do |multipart|
+ context 'when use_final_store_path is true' do
+ let(:use_final_store_path) { true }
+ let(:final_store_path) { File.join('@final', 'abc', '123', 'somefilename') }
+ let(:escaped_path) { escape_path(final_store_path) }
+
+ before do
+ stub_object_storage_multipart_init_with_final_store_path("#{storage_url}#{final_store_path}") if multipart
+
+ allow(uploader_class).to receive(:generate_final_store_path).and_return(final_store_path)
+ end
+
+ it 'uses the full path instead of the temporary one' do
+ expect(subject[:RemoteObject][:ID]).to eq(final_store_path)
+
+ expect(subject[:RemoteObject][:GetURL]).to include(escaped_path)
+ expect(subject[:RemoteObject][:StoreURL]).to include(escaped_path)
+
+ if multipart
+ expect(subject[:RemoteObject][:MultipartUpload][:PartURLs]).to all(include(escaped_path))
+ expect(subject[:RemoteObject][:MultipartUpload][:CompleteURL]).to include(escaped_path)
+ expect(subject[:RemoteObject][:MultipartUpload][:AbortURL]).to include(escaped_path)
+ end
+
+ expect(subject[:RemoteObject][:SkipDelete]).to eq(true)
+
+ expect(
+ ObjectStorage::PendingDirectUpload.exists?(uploader_class.storage_location_identifier, final_store_path)
+ ).to eq(true)
+ end
+
+ context 'and bucket prefix is configured' do
+ let(:prefixed_final_store_path) { "my/prefix/#{final_store_path}" }
+ let(:escaped_path) { escape_path(prefixed_final_store_path) }
+
+ before do
+ allow(uploader_class.object_store_options).to receive(:bucket_prefix).and_return('my/prefix')
+
+ if multipart
+ stub_object_storage_multipart_init_with_final_store_path("#{storage_url}#{prefixed_final_store_path}")
+ end
+ end
+
+ it 'sets the remote object ID to the final path without prefix' do
+ expect(subject[:RemoteObject][:ID]).to eq(final_store_path)
+ end
+
+ it 'returns the final path with prefix' do
+ expect(subject[:RemoteObject][:GetURL]).to include(escaped_path)
+ expect(subject[:RemoteObject][:StoreURL]).to include(escaped_path)
+
+ if multipart
+ expect(subject[:RemoteObject][:MultipartUpload][:PartURLs]).to all(include(escaped_path))
+ expect(subject[:RemoteObject][:MultipartUpload][:CompleteURL]).to include(escaped_path)
+ expect(subject[:RemoteObject][:MultipartUpload][:AbortURL]).to include(escaped_path)
+ end
+ end
+
+ it 'creates the pending upload entry without the prefix' do
+ is_expected.to have_key(:RemoteObject)
+
+ expect(
+ ObjectStorage::PendingDirectUpload.exists?(uploader_class.storage_location_identifier, final_store_path)
+ ).to eq(true)
+ end
+ end
+ end
+
+ def escape_path(path)
+ # This is what the private method Fog::AWS::Storage#object_to_path will do to the object name
+ Fog::AWS.escape(path).gsub('%2F', '/')
+ end
+ end
+
context 'when object storage is disabled' do
before do
allow(Gitlab.config.uploads.object_store).to receive(:enabled) { false }
@@ -613,6 +729,8 @@ RSpec.describe ObjectStorage do
expect(subject[:RemoteObject][:StoreURL]).to start_with(storage_url)
end
end
+
+ it_behaves_like 'handling object storage final upload path'
end
context 'for unknown length' do
@@ -633,6 +751,8 @@ RSpec.describe ObjectStorage do
expect(subject[:RemoteObject][:MultipartUpload][:AbortURL]).to start_with(storage_url)
end
end
+
+ it_behaves_like 'handling object storage final upload path', :multipart
end
end
@@ -660,6 +780,8 @@ RSpec.describe ObjectStorage do
expect(subject[:RemoteObject][:StoreURL]).to start_with(storage_url)
end
end
+
+ it_behaves_like 'handling object storage final upload path'
end
context 'for unknown length' do
@@ -673,6 +795,8 @@ RSpec.describe ObjectStorage do
expect(subject[:RemoteObject][:StoreURL]).to start_with(storage_url)
end
end
+
+ it_behaves_like 'handling object storage final upload path'
end
end
@@ -701,6 +825,8 @@ RSpec.describe ObjectStorage do
expect(subject[:RemoteObject][:StoreURL]).to start_with(storage_url)
end
end
+
+ it_behaves_like 'handling object storage final upload path'
end
context 'for unknown length' do
@@ -721,6 +847,8 @@ RSpec.describe ObjectStorage do
expect(subject[:RemoteObject][:MultipartUpload][:AbortURL]).to start_with(storage_url)
end
end
+
+ it_behaves_like 'handling object storage final upload path', :multipart
end
end
end
@@ -880,9 +1008,10 @@ RSpec.describe ObjectStorage do
expect(uploader).to be_exists
expect(uploader).to be_cached
+ expect(uploader.cache_only).to be_falsey
expect(uploader).not_to be_file_storage
expect(uploader.path).not_to be_nil
- expect(uploader.path).not_to include('tmp/cache')
+ expect(uploader.path).to include('tmp/uploads')
expect(uploader.path).not_to include('tmp/cache')
expect(uploader.object_store).to eq(described_class::Store::REMOTE)
end
@@ -905,41 +1034,167 @@ RSpec.describe ObjectStorage do
expect(uploader.object_store).to eq(described_class::Store::REMOTE)
end
end
+
+ context 'when uploaded file remote_id matches a pending direct upload entry' do
+ let(:object) { build_stubbed(:ci_job_artifact) }
+ let(:final_path) { '@final/test/123123' }
+ let(:fog_config) { Gitlab.config.uploads.object_store }
+ let(:bucket) { 'uploads' }
+ let(:uploaded_file) { UploadedFile.new(temp_file.path, filename: "my_file.txt", remote_id: final_path) }
+ let(:fog_file_path) { final_path }
+
+ let(:fog_connection_2) do
+ stub_object_storage_uploader(
+ config: fog_config,
+ uploader: uploader_class,
+ direct_upload: true
+ )
+ end
+
+ let!(:fog_file_2) do
+ fog_connection_2.directories.new(key: bucket).files.create( # rubocop:disable Rails/SaveBang
+ key: fog_file_path,
+ body: 'content'
+ )
+ end
+
+ before do
+ ObjectStorage::PendingDirectUpload.prepare(
+ uploader_class.storage_location_identifier,
+ final_path
+ )
+ end
+
+ it 'file to be cached and remote stored with final path set' do
+ expect { subject }.not_to raise_error
+
+ expect(uploader).to be_exists
+ expect(uploader).to be_cached
+ expect(uploader.cache_only).to be_falsey
+ expect(uploader).not_to be_file_storage
+ expect(uploader.path).to eq(uploaded_file.remote_id)
+ expect(uploader.object_store).to eq(described_class::Store::REMOTE)
+
+ expect(object.file_final_path).to eq(uploaded_file.remote_id)
+ end
+
+ context 'when bucket prefix is configured' do
+ let(:fog_config) do
+ Gitlab.config.uploads.object_store.tap do |config|
+ config[:remote_directory] = 'main-bucket'
+ config[:bucket_prefix] = 'uploads'
+ end
+ end
+
+ let(:bucket) { 'main-bucket' }
+ let(:fog_file_path) { "uploads/#{final_path}" }
+
+ it 'stores the file final path in the db without the prefix' do
+ expect { subject }.not_to raise_error
+
+ expect(uploader.store_path).to eq("uploads/#{final_path}")
+ expect(object.file_final_path).to eq(final_path)
+ end
+ end
+
+ context 'when file is stored' do
+ subject do
+ uploader.store!(uploaded_file)
+ end
+
+ it 'file to be remotely stored in permament location' do
+ subject
+
+ expect(uploader).to be_exists
+ expect(uploader).not_to be_cached
+ expect(uploader.path).to eq(uploaded_file.remote_id)
+ end
+
+ it 'does not trigger Carrierwave copy and delete because it is already in the final location' do
+ expect_next_instance_of(CarrierWave::Storage::Fog::File) do |instance|
+ expect(instance).not_to receive(:copy_to)
+ expect(instance).not_to receive(:delete)
+ end
+
+ subject
+ end
+ end
+ end
end
end
end
end
describe '#retrieve_from_store!' do
- [:group, :project, :user].each do |model|
- context "for #{model}s" do
- let(:models) { create_list(model, 3, :with_avatar).map(&:reload) }
- let(:avatars) { models.map(&:avatar) }
+ context 'uploaders that includes the RecordsUploads extension' do
+ [:group, :project, :user].each do |model|
+ context "for #{model}s" do
+ let(:models) { create_list(model, 3, :with_avatar).map(&:reload) }
+ let(:avatars) { models.map(&:avatar) }
+
+ it 'batches fetching uploads from the database' do
+ # Ensure that these are all created and fully loaded before we start
+ # running queries for avatars
+ models
+
+ expect { avatars }.not_to exceed_query_limit(1)
+ end
+
+ it 'does not attempt to replace methods' do
+ models.each do |model|
+ expect(model.avatar.upload).to receive(:method_missing).and_call_original
- it 'batches fetching uploads from the database' do
- # Ensure that these are all created and fully loaded before we start
- # running queries for avatars
- models
+ model.avatar.upload.path
+ end
+ end
- expect { avatars }.not_to exceed_query_limit(1)
+ it 'fetches a unique upload for each model' do
+ expect(avatars.map(&:url).uniq).to eq(avatars.map(&:url))
+ expect(avatars.map(&:upload).uniq).to eq(avatars.map(&:upload))
+ end
end
+ end
+ end
- it 'does not attempt to replace methods' do
- models.each do |model|
- expect(model.avatar.upload).to receive(:method_missing).and_call_original
+ describe 'filename' do
+ let(:model) { create(:ci_job_artifact, :remote_store, :archive) }
- model.avatar.upload.path
- end
+ before do
+ stub_artifacts_object_storage
+ end
+
+ shared_examples 'ensuring correct filename' do
+ it 'uses the original filename' do
+ expect(model.reload.file.filename).to eq('ci_build_artifacts.zip')
end
+ end
- it 'fetches a unique upload for each model' do
- expect(avatars.map(&:url).uniq).to eq(avatars.map(&:url))
- expect(avatars.map(&:upload).uniq).to eq(avatars.map(&:upload))
+ context 'when model has final path defined for the file column' do
+ before do
+ model.update_column(:file_final_path, 'some/final/path/abc-123')
end
+
+ it_behaves_like 'ensuring correct filename'
+ end
+
+ context 'when model has no final path defined for the file column' do
+ it_behaves_like 'ensuring correct filename'
end
end
end
+ describe '.generate_final_store_path' do
+ subject(:final_path) { uploader_class.generate_final_store_path }
+
+ before do
+ allow(Digest::SHA2).to receive(:hexdigest).and_return('somehash1234')
+ end
+
+ it 'returns the generated hashed path' do
+ expect(final_path).to eq('@final/so/me/hash1234')
+ end
+ end
+
describe 'OpenFile' do
subject { ObjectStorage::Concern::OpenFile.new(file) }