Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion lib/cloud_controller/blobstore/storage_cli/storage_cli_blob.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,26 @@ module Blobstore
class StorageCliBlob < Blob
attr_reader :key

def initialize(key, properties: nil, signed_url: nil)
def initialize(key, properties: nil, signed_url: nil, storage_cli_client: nil, expires_in_seconds: 3600)
@key = key
@signed_url = signed_url if signed_url
@storage_cli_client = storage_cli_client
@expires_in_seconds = expires_in_seconds
# Set properties to an empty hash if nil to avoid nil errors
@properties = properties || {}
end

def internal_download_url
# For DAV with lazy signing support, generate URL on-demand
return @storage_cli_client.sign_internal_url(@key, verb: 'get', expires_in_seconds: @expires_in_seconds) if @storage_cli_client&.supports_lazy_signing?

signed_url
end

def public_download_url
# For DAV with lazy signing support, generate URL on-demand
return @storage_cli_client.sign_public_url(@key, verb: 'get', expires_in_seconds: @expires_in_seconds) if @storage_cli_client&.supports_lazy_signing?

signed_url
end

Expand Down
37 changes: 27 additions & 10 deletions lib/cloud_controller/blobstore/storage_cli/storage_cli_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,16 @@ class StorageCliClient < BaseClient
'resource_pool' => :storage_cli_config_file_resource_pool
}.freeze

# Native storage-cli type names supported by CC (dav intentionally excluded for now)
STORAGE_CLI_TYPES = %w[azurebs alioss s3 gcs].freeze
# Native storage-cli type names supported by CC
STORAGE_CLI_TYPES = %w[azurebs alioss s3 gcs dav].freeze

# DEPRECATED: Legacy fog provider names (remove after migration window)
LEGACY_PROVIDER_TO_STORAGE_CLI_TYPE = {
'AzureRM' => 'azurebs',
'aliyun' => 'alioss',
'AWS' => 's3',
'Google' => 'gcs'
# 'webdav' => 'dav', # intentionally not enabled yet
'Google' => 'gcs',
'webdav' => 'dav'
}.freeze

def initialize(directory_key:, resource_type:, root_dir:, min_size: nil, max_size: nil)
Expand All @@ -39,10 +39,6 @@ def initialize(directory_key:, resource_type:, root_dir:, min_size: nil, max_siz
provider = cfg['provider']&.to_s
raise BlobstoreError.new("No provider specified in config file: #{File.basename(config_file_path)}") if provider.nil? || provider.empty?

# Explicitly block unfinished webdav storage-cli support to avoid confusion and wasted effort on debugging
# unsupported providers. Remove this check when webdav support is added.
raise "provider '#{provider}' is not supported yet" if %w[webdav dav].include?(provider)

@storage_type =
if STORAGE_CLI_TYPES.include?(provider)
provider
Expand Down Expand Up @@ -172,8 +168,29 @@ def blob(key)
properties = properties(key)
return nil if properties.nil? || properties.empty?

signed_url = sign_url(partitioned_key(key), verb: 'get', expires_in_seconds: 3600)
StorageCliBlob.new(key, properties:, signed_url:)
# For DAV with lazy signing support, pass client reference for on-demand signing
# For other providers, generate signed URL eagerly
if supports_lazy_signing?
StorageCliBlob.new(key, properties: properties, storage_cli_client: self, expires_in_seconds: 3600)
else
signed_url = sign_url(partitioned_key(key), verb: 'get', expires_in_seconds: 3600)
StorageCliBlob.new(key, properties:, signed_url:)
end
end

def supports_lazy_signing?
# Only DAV with external signer needs lazy signing for internal vs public endpoints
@storage_type == 'dav'
end

def sign_internal_url(key, verb:, expires_in_seconds:)
stdout, _status = run_cli('sign-internal', partitioned_key(key), verb.to_s.downcase, "#{expires_in_seconds}s")
stdout.strip
end

def sign_public_url(key, verb:, expires_in_seconds:)
stdout, _status = run_cli('sign-public', partitioned_key(key), verb.to_s.downcase, "#{expires_in_seconds}s")
stdout.strip
end

def files_for(prefix, _ignored_directory_prefixes=[])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,134 @@ module Blobstore
end

describe 'download_urls' do
it 'returns the internal download URL of the blob' do
expect(blob.internal_download_url).to eq(signed_url)
context 'with pre-generated signed URL (eager signing for non-DAV providers)' do
it 'returns the internal download URL of the blob' do
expect(blob.internal_download_url).to eq(signed_url)
end

it 'returns the public download URL of the blob' do
expect(blob.public_download_url).to eq(signed_url)
end
end

context 'with lazy signing (for DAV provider)' do
let(:storage_cli_client) { double('StorageCliClient') }

before do
allow(storage_cli_client).to receive(:supports_lazy_signing?).and_return(true)
end

subject(:lazy_blob) do
StorageCliBlob.new(
'dr/op/droplet-guid',
properties: properties,
storage_cli_client: storage_cli_client,
expires_in_seconds: 3600
)
end

describe '#internal_download_url' do
it 'calls sign_internal_url on the storage_cli_client' do
expect(storage_cli_client).to receive(:sign_internal_url).with(
'dr/op/droplet-guid',
verb: 'get',
expires_in_seconds: 3600
).and_return('https://blobstore.internal:4443/read/cc-droplets/dr/op/droplet-guid?md5=internal123&expires=789')

url = lazy_blob.internal_download_url

expect(url).to eq('https://blobstore.internal:4443/read/cc-droplets/dr/op/droplet-guid?md5=internal123&expires=789')
end

it 'generates URL on-demand each time it is called' do
call_count = 0
allow(storage_cli_client).to receive(:sign_internal_url) do
call_count += 1
"https://blobstore.internal/url-#{call_count}"
end

url1 = lazy_blob.internal_download_url
url2 = lazy_blob.internal_download_url

expect(url1).to eq('https://blobstore.internal/url-1')
expect(url2).to eq('https://blobstore.internal/url-2')
expect(call_count).to eq(2)
end
end

describe '#public_download_url' do
it 'calls sign_public_url on the storage_cli_client' do
expect(storage_cli_client).to receive(:sign_public_url).with(
'dr/op/droplet-guid',
verb: 'get',
expires_in_seconds: 3600
).and_return('https://blobstore.example.com/read/cc-droplets/dr/op/droplet-guid?md5=public456&expires=999')

url = lazy_blob.public_download_url

expect(url).to eq('https://blobstore.example.com/read/cc-droplets/dr/op/droplet-guid?md5=public456&expires=999')
end

it 'generates URL on-demand each time it is called' do
call_count = 0
allow(storage_cli_client).to receive(:sign_public_url) do
call_count += 1
"https://blobstore.public/url-#{call_count}"
end

url1 = lazy_blob.public_download_url
url2 = lazy_blob.public_download_url

expect(url1).to eq('https://blobstore.public/url-1')
expect(url2).to eq('https://blobstore.public/url-2')
expect(call_count).to eq(2)
end
end

it 'uses custom expires_in_seconds when provided' do
custom_blob = StorageCliBlob.new(
'test-key',
properties: properties,
storage_cli_client: storage_cli_client,
expires_in_seconds: 7200
)

expect(storage_cli_client).to receive(:sign_internal_url).with(
'test-key',
verb: 'get',
expires_in_seconds: 7200
).and_return('url')

custom_blob.internal_download_url
end
end

it 'returns the public download URL of the blob' do
expect(blob.public_download_url).to eq(signed_url)
context 'when storage_cli_client does not support lazy signing' do
let(:storage_cli_client) { double('StorageCliClient') }

before do
allow(storage_cli_client).to receive(:supports_lazy_signing?).and_return(false)
end

subject(:non_lazy_blob) do
StorageCliBlob.new(
'test-blob',
properties: properties,
signed_url: signed_url,
storage_cli_client: storage_cli_client
)
end

it 'falls back to using pre-generated signed_url for internal_download_url' do
expect(non_lazy_blob.internal_download_url).to eq(signed_url)
end

it 'falls back to using pre-generated signed_url for public_download_url' do
expect(non_lazy_blob.public_download_url).to eq(signed_url)
end
end

context 'when signed_url is not provided' do
context 'when signed_url is not provided and no storage_cli_client' do
subject(:blob_without_signed_url) { StorageCliBlob.new('test-blob', properties:) }

it 'raises an error when accessing internal_download_url' do
Expand Down
Loading
Loading