diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 3c9ca55c547..f21c4d45245 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -44,7 +44,6 @@ Lint/NonAtomicFileOperation: Exclude: - 'spec/support/integration/setup.rb' - 'spec/unit/lib/cloud_controller/blobstore/client_shared.rb' - - 'spec/unit/lib/cloud_controller/blobstore/webdav/dav_client_spec.rb' # Offense count: 15 # This cop supports unsafe autocorrection (--autocorrect-all). @@ -581,7 +580,6 @@ Style/HashSlice: - 'app/controllers/services/service_plans_controller.rb' - 'app/presenters/system_environment/service_binding_presenter.rb' - 'lib/cloud_controller/blobstore/storage_cli/storage_cli_blob.rb' - - 'lib/cloud_controller/blobstore/webdav/dav_blob.rb' - 'lib/cloud_controller/telemetry_logger.rb' - 'spec/unit/lib/cloud_controller/errands/rotate_database_key_spec.rb' diff --git a/README.md b/README.md index 57e30f280c9..27686c35a59 100644 --- a/README.md +++ b/README.md @@ -38,8 +38,7 @@ When deployed via capi-release only: | Provider | `blobstore_type` | Backends | Notes | |----------|------------------|-----------------------------------------------|-------| -| Storage CLI | `storage-cli` | S3, S3-compatible, GCS, Azure, Alibaba Cloud | | -| WebDAV | `webdav` | WebDAV servers | | +| Storage CLI | `storage-cli` | S3, S3-compatible, GCS, Azure, Alibaba Cloud, DAV | | | Local | `local`, `local-temp-storage` | Filesystem, NFS | Development and testing only | ### Runtime diff --git a/config/bosh-lite.yml b/config/bosh-lite.yml index 17746e4817a..dd6bad784dd 100644 --- a/config/bosh-lite.yml +++ b/config/bosh-lite.yml @@ -122,48 +122,28 @@ quota_definitions: default_quota_definition: default resource_pool: - blobstore_type: webdav - webdav_config: - blobstore_timeout: 5 - public_endpoint: http://blobstore.bosh-lite.com - private_endpoint: https://blobstore.service.cf.internal:4443 - username: blobstore-username - password: blobstore-password + blobstore_type: local + local_blobstore_path: /var/vcap/store/cloud_controller_ng/resource_pool minimum_size: 65536 maximum_size: 536870912 resource_directory_key: bosh-lite.com-cc-resources packages: - blobstore_type: webdav - webdav_config: - blobstore_timeout: 5 - public_endpoint: http://blobstore.bosh-lite.com - private_endpoint: https://blobstore.service.cf.internal:4443 - username: blobstore-username - password: blobstore-password + blobstore_type: local + local_blobstore_path: /var/vcap/store/cloud_controller_ng/packages app_package_directory_key: bosh-lite.com-cc-packages max_valid_packages_stored: 5 max_package_size: 1073741824 droplets: - blobstore_type: webdav - webdav_config: - blobstore_timeout: 5 - public_endpoint: http://blobstore.bosh-lite.com - private_endpoint: https://blobstore.service.cf.internal:4443 - username: blobstore-username - password: blobstore-password + blobstore_type: local + local_blobstore_path: /var/vcap/store/cloud_controller_ng/droplets droplet_directory_key: bosh-lite.com-cc-droplets max_staged_droplets_stored: 5 buildpacks: - blobstore_type: webdav - webdav_config: - blobstore_timeout: 5 - public_endpoint: http://blobstore.bosh-lite.com - private_endpoint: https://blobstore.service.cf.internal:4443 - username: blobstore-username - password: blobstore-password + blobstore_type: local + local_blobstore_path: /var/vcap/store/cloud_controller_ng/buildpacks buildpack_directory_key: bosh-lite.com-cc-buildpacks db_encryption_key: db-encryption-key diff --git a/decisions/0014-storage-clis-for-blobstore-operations.md b/decisions/0014-storage-clis-for-blobstore-operations.md index a2513fe8c51..e896b08210c 100644 --- a/decisions/0014-storage-clis-for-blobstore-operations.md +++ b/decisions/0014-storage-clis-for-blobstore-operations.md @@ -27,7 +27,8 @@ Cloud Controller previously used the fog gem family to interface with blobstores The migration described in this ADR is complete. As a result: -- The `blobstore_provider` field accepts both native storage-cli type names AND the legacy WebDAV alias (`webdav` → `dav`) +- The `blobstore_provider` field accepts native storage-cli type names (azurebs, s3, gcs, alioss, dav) +- The legacy native WebDAV provider (`blobstore_type: webdav`) has been removed — use `blobstore_type: storage-cli` with `provider: dav` instead - **Legacy fog provider names** (AzureRM, AWS, Google, aliyun) are **no longer supported** — use native storage-cli type names (azurebs, s3, gcs, alioss) - ✅ Enabled the removal of all fog related gems — fog-aws, fog-core, fog-azure-rm, fog-google, and fog-aliyun have all been removed diff --git a/lib/cloud_controller/blobstore/client_provider.rb b/lib/cloud_controller/blobstore/client_provider.rb index 8f7ec528604..8e30fa1dbb7 100644 --- a/lib/cloud_controller/blobstore/client_provider.rb +++ b/lib/cloud_controller/blobstore/client_provider.rb @@ -1,7 +1,6 @@ require 'cloud_controller/blobstore/client' require 'cloud_controller/blobstore/retryable_client' require 'cloud_controller/blobstore/error_handling_client' -require 'cloud_controller/blobstore/webdav/dav_client' require 'cloud_controller/blobstore/local/local_client' require 'cloud_controller/blobstore/safe_delete_client' require 'cloud_controller/blobstore/storage_cli/storage_cli_client' @@ -17,8 +16,10 @@ def self.provide(options:, directory_key:, root_dir: nil, resource_type: nil) provide_local(options, directory_key, root_dir, use_temp_storage: true) when 'storage-cli' provide_storage_cli(options, directory_key, root_dir, resource_type) + when 'webdav' + raise BlobstoreError.new("blobstore_type 'webdav' is no longer supported; use 'storage-cli' with provider 'dav'") else - provide_webdav(options, directory_key, root_dir) + raise BlobstoreError.new("Unknown blobstore type: #{options[:blobstore_type].inspect}") end end @@ -42,22 +43,6 @@ def provide_local(options, directory_key, root_dir, use_temp_storage:) Client.new(SafeDeleteClient.new(retryable_client, root_dir)) end - def provide_webdav(options, directory_key, root_dir) - client = DavClient.build( - options.fetch(:webdav_config), - directory_key, - root_dir, - options[:minimum_size], - options[:maximum_size] - ) - - logger = Steno.logger('cc.blobstore.dav_client') - errors = [StandardError] - retryable_client = RetryableClient.new(client:, errors:, logger:) - - Client.new(SafeDeleteClient.new(retryable_client, root_dir)) - end - def provide_storage_cli(options, directory_key, root_dir, resource_type) client = StorageCliClient.new( directory_key: directory_key, diff --git a/lib/cloud_controller/blobstore/storage_cli/storage_cli_client.rb b/lib/cloud_controller/blobstore/storage_cli/storage_cli_client.rb index cd30c8339f7..ff05e1701c9 100644 --- a/lib/cloud_controller/blobstore/storage_cli/storage_cli_client.rb +++ b/lib/cloud_controller/blobstore/storage_cli/storage_cli_client.rb @@ -218,10 +218,6 @@ def cli_path ENV['STORAGE_CLI_PATH'] || '/var/vcap/packages/storage-cli/bin/storage-cli' end - def build_config(connection_config) - raise NotImplementedError - end - def tmpdir VCAP::CloudController::Config.config.get(:directories, :tmpdir) rescue StandardError diff --git a/lib/cloud_controller/blobstore/webdav/dav_blob.rb b/lib/cloud_controller/blobstore/webdav/dav_blob.rb deleted file mode 100644 index ff8e5edeeb7..00000000000 --- a/lib/cloud_controller/blobstore/webdav/dav_blob.rb +++ /dev/null @@ -1,38 +0,0 @@ -require 'cloud_controller/blobstore/webdav/nginx_secure_link_signer' - -module CloudController - module Blobstore - class DavBlob < Blob - attr_reader :key - - def initialize(httpmessage:, key:, signer:) - @httpmessage = httpmessage - @key = key - @signer = signer - end - - def internal_download_url - expires = Time.now.utc.to_i + 3600 - @signer.sign_internal_url(path: @key, expires: expires) - end - - def public_download_url - expires = Time.now.utc.to_i + 3600 - @signer.sign_public_url(path: @key, expires: expires) - end - - def attributes(*keys) - @attributes ||= { - etag: @httpmessage.headers['ETag'], - last_modified: @httpmessage.headers['Last-Modified'], - content_length: @httpmessage.headers['Content-Length'], - created_at: nil - } - - return @attributes if keys.empty? - - @attributes.select { |key, _| keys.include? key } - end - end - end -end diff --git a/lib/cloud_controller/blobstore/webdav/dav_client.rb b/lib/cloud_controller/blobstore/webdav/dav_client.rb deleted file mode 100644 index 421a68d45f1..00000000000 --- a/lib/cloud_controller/blobstore/webdav/dav_client.rb +++ /dev/null @@ -1,243 +0,0 @@ -require 'cloud_controller/blobstore/base_client' -require 'cloud_controller/blobstore/errors' -require 'cloud_controller/blobstore/webdav/dav_blob' -require 'cloud_controller/blobstore/webdav/nginx_secure_link_signer' -require 'cloud_controller/blobstore/webdav/http_client_provider' -require 'nokogiri' - -module CloudController - module Blobstore - class DavClient < BaseClient - attr_reader :root_dir - - def initialize( - directory_key:, - httpclient:, - signer:, - endpoint:, - user: nil, - password: nil, - root_dir: nil, - min_size: nil, - max_size: nil - ) - @directory_key = directory_key - @min_size = min_size || 0 - @max_size = max_size - @root_dir = root_dir - @client = httpclient - @endpoint = endpoint - @headers = {} - - if user && password - @headers['Authorization'] = 'Basic ' + - Base64.strict_encode64("#{user}:#{password}").strip - end - - @signer = signer - end - - def self.build(options, directory_key, root_dir=nil, min_size=nil, max_size=nil) - new( - directory_key: directory_key, - httpclient: HTTPClientProvider.provide(ca_cert_path: options[:ca_cert_path], connect_timeout: options[:blobstore_timeout], receive_timeout: 120), - signer: NginxSecureLinkSigner.build(options:, directory_key:), - endpoint: options[:private_endpoint], - user: options[:username], - password: options[:password], - root_dir: root_dir, - min_size: min_size, - max_size: max_size - ) - end - - def local? - false - end - - def exists?(key) - response = with_error_handling do - @client.head(url(key), header: @headers) - end - - if response.status == 200 - true - elsif response.status == 404 - false - else - raise_blobstore_error("Could not get object existence, #{response.status}/#{response.content}") - end - end - - def download_from_blobstore(source_key, destination_path, mode: nil) - FileUtils.mkdir_p(File.dirname(destination_path)) - File.open(destination_path, 'wb') do |file| - response = with_error_handling do - @client.get(url(source_key), {}, @headers) do |block| - file.write(block) - end - end - - raise_blobstore_error("Could not fetch object, #{response.status}/#{response.content}") if response.status != 200 - - file.chmod(mode) if mode - end - end - - def cp_to_blobstore(source_path, destination_key) - start = Time.now.utc - log_entry = 'cp-skip' - size = -1 - - logger.info('cp-start', destination_key: destination_key, source_path: source_path, bucket: @directory_key) - - File.open(source_path) do |file| - size = file.size - next unless within_limits?(size) - - response = with_error_handling { @client.put(url(destination_key), file, @headers) } - - raise_blobstore_error("Could not create object, #{response.status}/#{response.content}") if response.status != 201 && response.status != 204 - - log_entry = 'cp-finish' - end - - duration = Time.now.utc - start - logger.info(log_entry, - destination_key: destination_key, - duration_seconds: duration, - size: size) - end - - def cp_file_between_keys(source_key, destination_key) - destination_url = url(destination_key) - destination_header = { 'Destination' => destination_url } - - response = with_error_handling { @client.put(destination_url, '', @headers) } - raise_blobstore_error("Could not copy object while creating destination, #{response.status}/#{response.content}") if response.status != 201 && response.status != 204 - - response = with_error_handling { @client.request(:copy, url(source_key), header: @headers.merge(destination_header)) } - raise FileNotFound.new("Could not find object '#{source_key}', #{response.status}/#{response.content}") if response.status == 404 - - raise_blobstore_error("Could not copy object, #{response.status}/#{response.content}") if response.status != 201 && response.status != 204 - end - - def delete(key) - response = with_error_handling { @client.delete(url(key), header: @headers) } - return if response.status == 204 - - raise FileNotFound.new("Could not find object '#{key}', #{response.status}/#{response.content}") if response.status == 404 - raise ConflictError.new("Conflict deleting object '#{key}', #{response.status}/#{response.content}") if response.status == 409 - - raise_blobstore_error("Could not delete object, #{response.status}/#{response.content}") - end - - def blob(key, **) - response = with_error_handling { @client.head(url(key), header: @headers) } - return DavBlob.new(httpmessage: response, key: partitioned_key(key), signer: @signer) if response.status == 200 - - raise_blobstore_error("Could not get object, #{response.status}/#{response.content}") if response.status != 404 - end - - def delete_blob(blob) - response = with_error_handling { @client.delete(url_from_blob_key(blob.key), header: @headers) } - return if response.status == 404 - - raise_blobstore_error("Could not delete object, #{response.status}/#{response.content}") if response.status != 204 - end - - def delete_all(_=nil) - url = url_without_key - response = with_error_handling { @client.delete(url, header: @headers) } - return if response.status == 204 - - raise FileNotFound.new("Could not find object '#{URI(url).path}', #{response.status}/#{response.content}") if response.status == 404 - - raise_blobstore_error("Could not delete all, #{response.status}/#{response.content}") - end - - def delete_all_in_path(path) - url = url(path) + '/' - response = with_error_handling { @client.delete(url, header: @headers) } - return if response.status == 204 - - # requesting to delete something which is already gone is ok - return if response.status == 404 - - raise_blobstore_error("Could not delete all in path, #{response.status}/#{response.content}") - end - - def files_for(prefix, ignored_directory_prefixes=[]) - queue = [''] - Enumerator.new do |yielder| - until queue.empty? - path = queue.shift - - request_url = url_without_key + path - response = with_error_handling { @client.request(:propfind, request_url, nil, nil, @headers) } - xml = Nokogiri::XML.parse(response.body) - - props = xml.xpath('//D:prop')[1..] - props.each do |prop| - full_path = get_full_path(path, prop.xpath('D:displayname').first.text) - next if prefix && !full_path.start_with?(prefix) - next if ignored_directory_prefixes.any? && full_path.start_with?(*ignored_directory_prefixes) - - if prop.xpath('D:resourcetype').children.empty? - blob = DavBlob.new(key: full_path, httpmessage: nil, signer: nil) - yielder << blob - else - queue.insert(0, full_path) - end - end - end - end - end - - def ensure_bucket_exists - # This is not relevant for webdav but it must be defined to conform to the - # CloudController::Blobstore::Client interface - end - - private - - def raise_blobstore_error(error_message) - logger.error("Error with blobstore: #{error_message}") - raise BlobstoreError.new(error_message) - end - - def url(key) - [@endpoint, 'admin', @directory_key, partitioned_key(key)].compact.join('/') - end - - def url_from_blob_key(key) - [@endpoint, 'admin', @directory_key, key].compact.join('/') - end - - def url_without_key - [@endpoint, 'admin', @directory_key, @root_dir].compact.join('/') + '/' - end - - def get_full_path(path, display_name) - if path.present? - [path, display_name].join('/') - else - display_name - end - end - - def logger - @logger ||= Steno.logger('cc.blobstore.dav_client') - end - - def with_error_handling - yield - rescue OpenSSL::SSL::SSLError => e - logger.error("SSL verification failed: #{e.message}") - raise BlobstoreError.new('SSL verification failed') - rescue StandardError => e - raise_blobstore_error(e.message) - end - end - end -end diff --git a/lib/cloud_controller/blobstore/webdav/http_client_provider.rb b/lib/cloud_controller/blobstore/webdav/http_client_provider.rb deleted file mode 100644 index 93df576a6b2..00000000000 --- a/lib/cloud_controller/blobstore/webdav/http_client_provider.rb +++ /dev/null @@ -1,17 +0,0 @@ -module CloudController - module Blobstore - class HTTPClientProvider - def self.provide(ca_cert_path: nil, connect_timeout: nil, receive_timeout: nil) - client = HTTPClient.new - client.connect_timeout = connect_timeout if connect_timeout - client.receive_timeout = receive_timeout if receive_timeout - client.ssl_config.verify_mode = VCAP::CloudController::Config.config.get(:skip_cert_verify) ? OpenSSL::SSL::VERIFY_NONE : OpenSSL::SSL::VERIFY_PEER - client.ssl_config.set_default_paths - - client.ssl_config.add_trust_ca(ca_cert_path) if ca_cert_path && File.exist?(ca_cert_path) - - client - end - end - end -end diff --git a/lib/cloud_controller/blobstore/webdav/nginx_secure_link_signer.rb b/lib/cloud_controller/blobstore/webdav/nginx_secure_link_signer.rb deleted file mode 100644 index c94998f8f0e..00000000000 --- a/lib/cloud_controller/blobstore/webdav/nginx_secure_link_signer.rb +++ /dev/null @@ -1,76 +0,0 @@ -require 'cloud_controller/blobstore/webdav/http_client_provider' - -module CloudController - module Blobstore - class NginxSecureLinkSigner - def self.build(options:, directory_key:) - NginxSecureLinkSigner.new( - internal_endpoint: options[:private_endpoint], - internal_path_prefix: directory_key, - public_endpoint: options[:public_endpoint], - public_path_prefix: directory_key, - basic_auth_user: options[:username], - basic_auth_password: options[:password], - httpclient: HTTPClientProvider.provide(ca_cert_path: options[:ca_cert_path]) - ) - end - - def initialize(internal_endpoint:, public_endpoint:, basic_auth_user:, basic_auth_password:, httpclient:, internal_path_prefix: nil, public_path_prefix: nil) - @internal_uri = URI(internal_endpoint) - @internal_path_prefix = internal_path_prefix - @public_uri = URI(public_endpoint) - @public_path_prefix = public_path_prefix - @client = httpclient - @headers = {} - @headers['Authorization'] = 'Basic ' + Base64.strict_encode64("#{basic_auth_user}:#{basic_auth_password}").strip - end - - def sign_internal_url(expires:, path:) - request_uri = uri(expires: expires, path: File.join([@internal_path_prefix, path].compact)) - response_uri = make_request(uri: request_uri) - - signed_uri = @internal_uri.clone - signed_uri.scheme = 'https' - signed_uri.path = response_uri.path - signed_uri.query = response_uri.query - signed_uri.to_s - end - - def sign_public_url(expires:, path:) - request_uri = uri(expires: expires, path: File.join([@public_path_prefix, path].compact)) - response_uri = make_request(uri: request_uri) - - signed_uri = @public_uri.clone - signed_uri.scheme = 'https' - signed_uri.path = response_uri.path - signed_uri.query = response_uri.query - signed_uri.to_s - end - - private - - def make_request(uri:) - response = @client.get(uri, header: @headers) - - raise SigningRequestError.new("Could not get a signed url, #{response.status}/#{response.content}") unless response.status == 200 - - URI(response.content) - rescue OpenSSL::SSL::SSLError => e - err = SigningRequestError.new("Could not get a signed url: #{e.message}") - err.set_backtrace(e.backtrace) - raise err - end - - def uri(expires:, path:) - uri = @internal_uri.clone - uri.path = '/sign' - uri.query = { - expires: expires, - path: File.join(['/', path]) - }.to_query - - uri.to_s - end - end - end -end diff --git a/lib/cloud_controller/config_schemas/api_schema.rb b/lib/cloud_controller/config_schemas/api_schema.rb index 511febfebf7..d0389dc1389 100644 --- a/lib/cloud_controller/config_schemas/api_schema.rb +++ b/lib/cloud_controller/config_schemas/api_schema.rb @@ -221,16 +221,14 @@ class ApiSchema < VCAP::Config resource_directory_key: String, optional(:blobstore_type) => String, optional(:local_blobstore_path) => String, - optional(:connection_config) => Hash, - optional(:webdav_config) => Hash + optional(:connection_config) => Hash }, buildpacks: { buildpack_directory_key: String, optional(:blobstore_type) => String, optional(:local_blobstore_path) => String, - optional(:connection_config) => Hash, - optional(:webdav_config) => Hash + optional(:connection_config) => Hash }, packages: { @@ -239,8 +237,7 @@ class ApiSchema < VCAP::Config app_package_directory_key: String, optional(:blobstore_type) => String, optional(:local_blobstore_path) => String, - optional(:connection_config) => Hash, - optional(:webdav_config) => Hash + optional(:connection_config) => Hash }, droplets: { @@ -248,8 +245,7 @@ class ApiSchema < VCAP::Config max_staged_droplets_stored: Integer, optional(:blobstore_type) => String, optional(:local_blobstore_path) => String, - optional(:connection_config) => Hash, - optional(:webdav_config) => Hash + optional(:connection_config) => Hash }, db_encryption_key: enum(String, NilClass), diff --git a/lib/cloud_controller/config_schemas/blobstore_benchmarks_schema.rb b/lib/cloud_controller/config_schemas/blobstore_benchmarks_schema.rb index 51d031b86aa..2f68d70198e 100644 --- a/lib/cloud_controller/config_schemas/blobstore_benchmarks_schema.rb +++ b/lib/cloud_controller/config_schemas/blobstore_benchmarks_schema.rb @@ -11,7 +11,6 @@ class BlobstoreBenchmarksSchema < VCAP::Config optional(:local_blobstore_path) => String, optional(:connection_config) => Hash, - optional(:webdav_config) => Hash, optional(:resource_directory_key) => String, optional(:buildpack_directory_key) => String, diff --git a/lib/cloud_controller/config_schemas/clock_schema.rb b/lib/cloud_controller/config_schemas/clock_schema.rb index 1b385af7526..d82664fd46e 100644 --- a/lib/cloud_controller/config_schemas/clock_schema.rb +++ b/lib/cloud_controller/config_schemas/clock_schema.rb @@ -137,16 +137,14 @@ class ClockSchema < VCAP::Config resource_directory_key: String, optional(:blobstore_type) => String, optional(:local_blobstore_path) => String, - optional(:connection_config) => Hash, - optional(:webdav_config) => Hash + optional(:connection_config) => Hash }, buildpacks: { buildpack_directory_key: String, optional(:blobstore_type) => String, optional(:local_blobstore_path) => String, - optional(:connection_config) => Hash, - optional(:webdav_config) => Hash + optional(:connection_config) => Hash }, packages: { @@ -154,16 +152,14 @@ class ClockSchema < VCAP::Config app_package_directory_key: String, optional(:blobstore_type) => String, optional(:local_blobstore_path) => String, - optional(:connection_config) => Hash, - optional(:webdav_config) => Hash + optional(:connection_config) => Hash }, droplets: { droplet_directory_key: String, optional(:blobstore_type) => String, optional(:local_blobstore_path) => String, - optional(:connection_config) => Hash, - optional(:webdav_config) => Hash + optional(:connection_config) => Hash }, db_encryption_key: enum(String, NilClass), diff --git a/lib/cloud_controller/config_schemas/deployment_updater_schema.rb b/lib/cloud_controller/config_schemas/deployment_updater_schema.rb index 207a0b12f64..e582c6519a2 100644 --- a/lib/cloud_controller/config_schemas/deployment_updater_schema.rb +++ b/lib/cloud_controller/config_schemas/deployment_updater_schema.rb @@ -123,16 +123,14 @@ class DeploymentUpdaterSchema < VCAP::Config resource_directory_key: String, optional(:blobstore_type) => String, optional(:local_blobstore_path) => String, - optional(:connection_config) => Hash, - optional(:webdav_config) => Hash + optional(:connection_config) => Hash }, buildpacks: { buildpack_directory_key: String, optional(:blobstore_type) => String, optional(:local_blobstore_path) => String, - optional(:connection_config) => Hash, - optional(:webdav_config) => Hash + optional(:connection_config) => Hash }, packages: { @@ -140,16 +138,14 @@ class DeploymentUpdaterSchema < VCAP::Config app_package_directory_key: String, optional(:blobstore_type) => String, optional(:local_blobstore_path) => String, - optional(:connection_config) => Hash, - optional(:webdav_config) => Hash + optional(:connection_config) => Hash }, droplets: { droplet_directory_key: String, optional(:blobstore_type) => String, optional(:local_blobstore_path) => String, - optional(:connection_config) => Hash, - optional(:webdav_config) => Hash + optional(:connection_config) => Hash }, stacks_file: String, diff --git a/lib/cloud_controller/config_schemas/worker_schema.rb b/lib/cloud_controller/config_schemas/worker_schema.rb index c6b99c27a67..f678bf6039b 100644 --- a/lib/cloud_controller/config_schemas/worker_schema.rb +++ b/lib/cloud_controller/config_schemas/worker_schema.rb @@ -122,16 +122,14 @@ class WorkerSchema < VCAP::Config resource_directory_key: String, optional(:blobstore_type) => String, optional(:local_blobstore_path) => String, - optional(:connection_config) => Hash, - optional(:webdav_config) => Hash + optional(:connection_config) => Hash }, buildpacks: { buildpack_directory_key: String, optional(:blobstore_type) => String, optional(:local_blobstore_path) => String, - optional(:connection_config) => Hash, - optional(:webdav_config) => Hash + optional(:connection_config) => Hash }, packages: { @@ -140,16 +138,14 @@ class WorkerSchema < VCAP::Config app_package_directory_key: String, optional(:blobstore_type) => String, optional(:local_blobstore_path) => String, - optional(:connection_config) => Hash, - optional(:webdav_config) => Hash + optional(:connection_config) => Hash }, droplets: { droplet_directory_key: String, optional(:blobstore_type) => String, optional(:local_blobstore_path) => String, - optional(:connection_config) => Hash, - optional(:webdav_config) => Hash + optional(:connection_config) => Hash }, db_encryption_key: enum(String, NilClass), diff --git a/spec/fixtures/certs/webdav_ca.crt b/spec/fixtures/certs/webdav_ca.crt deleted file mode 100644 index 3a95877e9ab..00000000000 --- a/spec/fixtures/certs/webdav_ca.crt +++ /dev/null @@ -1,29 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIE/TCCAuegAwIBAgIBATALBgkqhkiG9w0BAQswEDEOMAwGA1UEAxMFZGVhQ0Ew -HhcNMTYwMzE0MjA0MTQ2WhcNMjYwMzE0MjA0MTQ5WjAQMQ4wDAYDVQQDEwVkZWFD -QTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALyPpyfTaM9bFbhs+y5/ -bsQE6RCbN2ZJSMJpZoqxVwNpGM3LA/GQLyTHtVOnmwUFfB/ecomlHrZUjhZKpEFB -xEu0CgiOX9UGJxWewHV6qt/O4oRw2IvHUUBJjoGOdyJfNa8TI5NaTq3d/P4vna/v -9sKzOhSxIN3pX8n3Oo8u08Zs8vFWG5PGETkN6lfIj47ZiImRJXXXHKC5yUn40rJe -FXYa9l8OIOHbYPxinUDLy+hCHZ9OjR15mzP/jIEkig8aLtwlWm3I/7RhQIknvGq9 -MD05Z6m0HZ2MRUzhuRSSbgrXB52R1OpgtImN3ObOReL+7O3EPkmnGvY3sM6pjNy9 -KTaEZmS55Rh+Fr05lrTbfL2Sx4DJouUNqnDNivTJ62nhPtKBRH/F64MopvM4R5BW -ctQSCfAA59KHMSJvs3/JL8oxF9bb8eRYKeOZqpWFP8Lh0LZZ5ck8fYEUZQKrEOXF -YnAyNIYNGPyyeTMpsYXA9Q1j6C+XjLpMMpjPWLywfpqWL1cinOCjh/hF9H9vz26I -/aVd4xVaY7/YUg1FuERbHGIHhkSpRNjf2NlPBlq4tkFbVrjb3UxZQsmv1Bp7Vwc8 -Hm6oqQb7Y2VfWHPHXEjo1VXY/ixCUCCdU2U2RiCjl30iZtVvAfy8ZVjKdTQQ7Gna -QRIn8pGERytFkhnQf87Wh3jFAgMBAAGjZjBkMA4GA1UdDwEB/wQEAwIABjASBgNV -HRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBSEW4BQ7/ABJ5mjE8Rvn4OB0S+fTzAf -BgNVHSMEGDAWgBSEW4BQ7/ABJ5mjE8Rvn4OB0S+fTzALBgkqhkiG9w0BAQsDggIB -AFmPDMWC+Mpi1UmuSJhhV5+EGbJfOaTg36Fj5B75K9ym68GDvRHCIv4EPgXUv67S -AOFRTESK+G3o+6YHxaAwK5aIc/fAccd3p7mCV3DZGpnFagvYAkmoClstkzAQkED7 -TEXBxL0/gzXucpw9p/JIiujI0B7K4TvsPNAm/PG7HwTHyJIgG4S+U0laLKtyAGQV -NAJZbKNMq7kROPYDVIUhuZlohnhbSXbSAJEew9IRDz5ZMl1NcOyTlmlFG4RiRewF -PcGWtwCHuaWjNoSpaG40A1ShYsz4WQpEoU+ltF9CD1ipPSnBBl2KfqvPcN52f4VH -fAEeIFhQHcLGbmaXMQ6u/iwPBcjh/3wxDZv5Gm8Ikx8V02CeghnNNvyIgMUduxnK -M1snpvAqwX01mj3VHPQzb4qLusrkkuK+ibSMMe5p1oJNjfR9Yhswlv09qUdu3ncS -BIBu/onganyafwV73M24vCiDlQHsKC/bRuiwkJSxdHyra8jU+YZ9BPfGKV7VebHE -nFJsP/01YeZRkQAryIvRRD+duGf/KWPV1pFrKcprJvkrjrdOL7JeCwVSLXd8TqmA -a6QVM1DaNi7qoUep/tRXGOWeFenJdWeO/fB4pmvUU1l98h2cgS9CkOdiEEYdIcNr -CzvDzs4BDcLQEREYOhdsWzuXf6iIxmh7G4PVD/ZU5Fxb ------END CERTIFICATE----- diff --git a/spec/unit/jobs/runtime/orphaned_blobs_cleanup_spec.rb b/spec/unit/jobs/runtime/orphaned_blobs_cleanup_spec.rb index e1f94e181ae..afa02be5856 100644 --- a/spec/unit/jobs/runtime/orphaned_blobs_cleanup_spec.rb +++ b/spec/unit/jobs/runtime/orphaned_blobs_cleanup_spec.rb @@ -44,10 +44,10 @@ module Jobs::Runtime end describe '#cleanup' do - let(:droplet_blobstore) { instance_double(CloudController::Blobstore::DavClient, files_for: droplet_files, root_dir: droplet_root_dir) } + let(:droplet_blobstore) { instance_double(CloudController::Blobstore::Client, files_for: droplet_files, root_dir: droplet_root_dir) } let(:droplet_files) { [] } let(:droplet_root_dir) { nil } - let(:package_blobstore) { instance_double(CloudController::Blobstore::DavClient, files_for: package_files, root_dir: package_root_dir) } + let(:package_blobstore) { instance_double(CloudController::Blobstore::Client, files_for: package_files, root_dir: package_root_dir) } let(:package_files) { [] } let(:package_root_dir) { nil } let(:buildpack_blobstore) { instance_double(CloudController::Blobstore::Client, files_for: buildpack_files, root_dir: buildpack_root_dir) } diff --git a/spec/unit/lib/cloud_controller/blobstore/client_provider_spec.rb b/spec/unit/lib/cloud_controller/blobstore/client_provider_spec.rb index 5dfdf0b7c2e..6c90ed9c668 100644 --- a/spec/unit/lib/cloud_controller/blobstore/client_provider_spec.rb +++ b/spec/unit/lib/cloud_controller/blobstore/client_provider_spec.rb @@ -5,20 +5,6 @@ module Blobstore RSpec.describe ClientProvider do let(:options) { { blobstore_type: } } - context 'when webdav is requested' do - let(:blobstore_type) { 'webdav' } - - before do - options.merge!(webdav_config: { private_endpoint: 'http://private.example.com', public_endpoint: 'http://public.example.com' }) - end - - it 'provides a webdav client' do - allow(DavClient).to receive(:new).and_call_original - ClientProvider.provide(options: options, directory_key: 'key') - expect(DavClient).to have_received(:new) - end - end - context 'when storage-cli is requested' do let(:blobstore_type) { 'storage-cli' } let(:directory_key) { 'some-bucket' } @@ -102,6 +88,24 @@ module Blobstore ) end end + + context 'when webdav blobstore type is requested' do + let(:blobstore_type) { 'webdav' } + + it 'raises a BlobstoreError with migration guidance' do + expect { ClientProvider.provide(options: options, directory_key: 'key') }. + to raise_error(BlobstoreError, /no longer supported/) + end + end + + context 'when an unknown blobstore type is requested' do + let(:blobstore_type) { 'unknown-type' } + + it 'raises a BlobstoreError' do + expect { ClientProvider.provide(options: options, directory_key: 'key') }. + to raise_error(BlobstoreError, /Unknown blobstore type/) + end + end end end end diff --git a/spec/unit/lib/cloud_controller/blobstore/retryable_client_spec.rb b/spec/unit/lib/cloud_controller/blobstore/retryable_client_spec.rb index ebfbf40ded5..5b2d0f95a8a 100644 --- a/spec/unit/lib/cloud_controller/blobstore/retryable_client_spec.rb +++ b/spec/unit/lib/cloud_controller/blobstore/retryable_client_spec.rb @@ -25,7 +25,7 @@ class RetryableError < StandardError end describe 'conforms to blobstore client interface' do - let(:deletable_blob) { instance_double(DavBlob, key: nil) } + let(:deletable_blob) { Blob.new } it_behaves_like 'a blobstore client' end diff --git a/spec/unit/lib/cloud_controller/blobstore/webdav/dav_blob_spec.rb b/spec/unit/lib/cloud_controller/blobstore/webdav/dav_blob_spec.rb deleted file mode 100644 index 3047bfaf4af..00000000000 --- a/spec/unit/lib/cloud_controller/blobstore/webdav/dav_blob_spec.rb +++ /dev/null @@ -1,71 +0,0 @@ -require 'spec_helper' -require_relative '../blob_shared' - -module CloudController - module Blobstore - RSpec.describe DavBlob do - subject(:blob) { DavBlob.new(httpmessage: httpmessage, key: 'fo/ob/foobar', signer: signer) } - let(:httpmessage) { instance_double(HTTP::Message, headers: {}) } - let(:key) { 'fo/ob/foobar' } - let(:signer) { instance_double(NginxSecureLinkSigner) } - - describe 'being a blob' do - before do - allow(signer).to receive(:sign_public_url) - allow(signer).to receive(:sign_internal_url) - end - - it_behaves_like 'a blob' - end - - describe 'attributes' do - let(:headers) { { 'ETag' => 'the-etag', 'Last-Modified' => 'modified-date', 'Content-Length' => 123_455 } } - - before do - allow(httpmessage).to receive(:headers).and_return(headers) - end - - it "returns the blob's attributes" do - expect(blob.attributes).to eq( - { - etag: 'the-etag', - last_modified: 'modified-date', - content_length: 123_455, - created_at: nil - } - ) - end - - it 'returns attributes for a set of keys' do - expect(blob.attributes(:etag)).to eq({ etag: 'the-etag' }) - end - end - - describe 'internal_download_url' do - before do - allow(signer).to receive(:sign_internal_url) - end - - it 'requests a signed expiring url with expiration of 1 hour' do - Timecop.freeze(Time.utc(2008, 1, 1, 12, 0, 0)) do - blob.internal_download_url - expect(signer).to have_received(:sign_internal_url).with(path: 'fo/ob/foobar', expires: 1_199_192_400) - end - end - end - - describe 'public_download_url' do - before do - allow(signer).to receive(:sign_public_url) - end - - it 'generates a signed expiring url with expiration of 1 hour' do - Timecop.freeze(Time.utc(2008, 1, 1, 12, 0, 0)) do - blob.public_download_url - expect(signer).to have_received(:sign_public_url).with(path: 'fo/ob/foobar', expires: 1_199_192_400) - end - end - end - end - end -end diff --git a/spec/unit/lib/cloud_controller/blobstore/webdav/dav_client_spec.rb b/spec/unit/lib/cloud_controller/blobstore/webdav/dav_client_spec.rb deleted file mode 100644 index f4caad00c4b..00000000000 --- a/spec/unit/lib/cloud_controller/blobstore/webdav/dav_client_spec.rb +++ /dev/null @@ -1,971 +0,0 @@ -require 'spec_helper' -require_relative '../client_shared' - -module CloudController - module Blobstore - RSpec.describe DavClient do - subject(:client) do - DavClient.new( - directory_key: directory_key, - httpclient: httpclient, - signer: signer, - endpoint: 'http://localhost', - user: user, - password: password, - root_dir: root_dir, - min_size: min_size, - max_size: max_size - ) - end - let(:httpclient) { instance_double(HTTPClient) } - let(:response) { instance_double(HTTP::Message) } - let(:signer) { instance_double(NginxSecureLinkSigner) } - let(:directory_key) { 'droplets' } - let(:root_dir) { nil } - let(:min_size) { nil } - let(:max_size) { nil } - let(:user) { nil } - let(:password) { nil } - let(:logger) { instance_double(Steno::Logger, error: nil, info: nil) } - - before do - allow(Steno).to receive(:logger).and_return(logger) - end - - describe 'conforms to blobstore client interface' do - let(:deletable_blob) { instance_double(DavBlob, key: nil) } - - before do - allow(httpclient).to receive_messages(head: instance_double(HTTP::Message, status: 200)) - allow(httpclient).to receive_messages(put: instance_double(HTTP::Message, status: 201)) - allow(httpclient).to receive_messages(get: instance_double(HTTP::Message, status: 200)) - allow(httpclient).to receive_messages(delete: instance_double(HTTP::Message, status: 204)) - allow(httpclient).to receive_messages(request: instance_double(HTTP::Message, status: 201)) - end - - it_behaves_like 'a blobstore client' - end - - describe 'basic auth' do - let(:user) { 'username' } - let(:password) { 'top-sekret' } - - it 'adds Authorization header when there is a user and password' do - allow(response).to receive_messages(status: 200) - allow(httpclient).to receive_messages(head: response) - - client.exists?('foobar') - - expect(httpclient).to have_received(:head).with(anything, header: { 'Authorization' => 'Basic dXNlcm5hbWU6dG9wLXNla3JldA==' }) - end - end - - describe '#exists?' do - it 'returns true for an object that already exists' do - allow(response).to receive_messages(status: 200) - allow(httpclient).to receive_messages(head: response) - - expect(client.exists?('foobar')).to be(true) - expect(httpclient).to have_received(:head).with('http://localhost/admin/droplets/fo/ob/foobar', header: {}) - end - - it 'returns false for an object that does not exist' do - allow(response).to receive_messages(status: 404) - allow(httpclient).to receive_messages(head: response) - - expect(client.exists?('foobar')).to be(false) - expect(httpclient).to have_received(:head).with('http://localhost/admin/droplets/fo/ob/foobar', header: {}) - end - - it 'raises a BlobstoreError if response status is neither 200 nor 404' do - allow(response).to receive_messages(status: 500, content: '') - allow(httpclient).to receive_messages(head: response) - - expect { client.exists?('foobar') }.to raise_error BlobstoreError, /Could not get object existence/ - expect(logger).to have_received(:error).with(/^Error with blobstore: Could not get object existence/) - expect(httpclient).to have_received(:head).with('http://localhost/admin/droplets/fo/ob/foobar', header: {}) - end - - context 'when an OpenSSL::SSL::SSLError is raised' do - it 'reraises a BlobstoreError' do - allow(httpclient).to receive(:head).and_raise(OpenSSL::SSL::SSLError.new) - expect { client.exists?('foobar') }.to raise_error BlobstoreError, /SSL verification failed/ - expect(logger).to have_received(:error).with(/^SSL verification failed: /) - end - end - - context 'when an unknown error occurs with the blobstore' do - it 'raises a BlobstoreError' do - allow(httpclient).to receive(:head).and_raise(Errno::ECONNREFUSED.new) - expect { client.exists?('foobar') }.to raise_error BlobstoreError - expect(logger).to have_received(:error).with('Error with blobstore: Connection refused') - end - end - end - - describe '#download_from_blobstore' do - let(:ssl_config) { instance_double(HTTPClient::SSLConfig, :verify_mode= => nil, set_default_paths: nil, add_trust_ca: nil) } - let(:httpclient) { instance_double(HTTPClient, ssl_config:) } - let(:destination_path) { File.join(Dir.mktmpdir, SecureRandom.uuid) } - - before do - allow(HTTPClient).to receive_messages(new: httpclient) - end - - after do - File.delete(destination_path) if File.exist?(destination_path) - end - - it 'fetches an object' do - allow(response).to receive_messages(status: 200) - allow(httpclient).to receive(:get).and_yield('content').and_return(response) - - client.download_from_blobstore('foobar', destination_path) - - expect(File.read(destination_path)).to eq('content') - expect(httpclient).to have_received(:get).with('http://localhost/admin/droplets/fo/ob/foobar', {}, {}) - end - - it 'raises an exception when there is an error fetching an object' do - allow(response).to receive_messages(status: 500, content: 'error message') - allow(httpclient).to receive_messages(get: response) - - expect { client.download_from_blobstore('foobar', destination_path) }.to raise_error BlobstoreError, /Could not fetch object/ - expect(logger).to have_received(:error).with(/^Error with blobstore: Could not fetch object/) - expect(httpclient).to have_received(:get).with('http://localhost/admin/droplets/fo/ob/foobar', {}, {}) - end - - describe 'file permissions' do - before do - @original_umask = File.umask - File.umask(0o022) - end - - after do - File.umask(@original_umask) - end - - context 'when not specifying a mode' do - it 'does not change permissions on the file' do - allow(response).to receive_messages(status: 200) - allow(httpclient).to receive(:get).and_yield('content').and_return(response) - - client.download_from_blobstore('foobar', destination_path) - - expect(sprintf('%o', mode: File.stat(destination_path).mode)).to eq('100644') - end - end - - context 'when specifying a mode' do - it 'does change permissions on the file' do - allow(response).to receive_messages(status: 200) - allow(httpclient).to receive(:get).and_yield('content').and_return(response) - - client.download_from_blobstore('foobar', destination_path, mode: 0o753) - - expect(sprintf('%o', mode: File.stat(destination_path).mode)).to eq('100753') - end - end - end - - context 'when an OpenSSL::SSL::SSLError is raised' do - it 'reraises a BlobstoreError' do - allow(httpclient).to receive(:get).and_raise(OpenSSL::SSL::SSLError.new) - expect { client.download_from_blobstore('foobar', destination_path) }.to raise_error BlobstoreError, /SSL verification failed/ - expect(logger).to have_received(:error).with(/^SSL verification failed: OpenSSL::SSL::SSLError/) - end - end - - context 'when an unknown error occurs with the blobstore' do - it 'raises a BlobstoreError' do - allow(httpclient).to receive(:get).and_raise(Errno::ECONNRESET.new) - expect { client.download_from_blobstore('foobar', destination_path) }.to raise_error BlobstoreError - expect(logger).to have_received(:error).with(/^Error with blobstore: Connection reset by peer/) - end - end - end - - describe '#cp_to_blobstore' do - let!(:tmpfile) do - Tempfile.open('') do |tmpfile| - tmpfile.write(content) - tmpfile - end - end - let(:content) { 'file content' } - - after do - tmpfile.unlink - end - - it 'creates an object' do - allow(response).to receive_messages(status: 201, content: '') - - expect(httpclient).to receive(:put) do |*args| - uri, body, = args - expect(uri).to eq('http://localhost/admin/droplets/fo/ob/foobar') - expect(body).to be_a(File) - expect(body.read).to eq('file content') - response - end - - client.cp_to_blobstore(tmpfile.path, 'foobar') - end - - it 'overwrites an existing file' do - allow(response).to receive_messages(status: 204, content: '') - allow(httpclient).to receive(:put).and_return(response) - - expect(httpclient).to receive(:put) do |*args| - uri, body, = args - expect(uri).to eq('http://localhost/admin/droplets/fo/ob/foobar') - expect(body).to be_a(File) - expect(body.read).to eq('file content') - response - end - - client.cp_to_blobstore(tmpfile.path, 'foobar') - end - - it 'raises an exception when there is an error creating an object' do - allow(response).to receive_messages(status: 500, content: nil) - allow(httpclient).to receive_messages(put: response) - - expect { client.cp_to_blobstore(tmpfile.path, 'foobar') }.to raise_error BlobstoreError, /Could not create object/ - expect(logger).to have_received(:error).with(/^Error with blobstore: Could not create object/) - end - - describe 'file size limits' do - let(:min_size) { 20 } - let(:max_size) { 50 } - - context 'too small file' do - let(:content) { 'a' * (min_size - 1) } - - it 'does not copy files below the minimum size limit' do - allow(httpclient).to receive_messages(put: nil) - - client.cp_to_blobstore(tmpfile.path, 'foobar') - - expect(httpclient).not_to have_received(:put) - end - end - - context 'too large file' do - let(:content) { 'a' * (max_size + 1) } - - it 'does not copy files above the maximum size limit' do - allow(httpclient).to receive_messages(put: nil) - - client.cp_to_blobstore(tmpfile.path, 'foobar') - - expect(httpclient).not_to have_received(:put) - end - end - end - - context 'when an OpenSSL::SSL::SSLError is raised' do - it 'reraises a BlobstoreError' do - allow(httpclient).to receive(:put).and_raise(OpenSSL::SSL::SSLError.new) - expect { client.cp_to_blobstore(tmpfile.path, 'foobar') }.to raise_error BlobstoreError, /SSL verification failed/ - expect(logger).to have_received(:error).with(/^SSL verification failed: OpenSSL::SSL::SSLError/) - end - end - - context 'when an unknown error occurs with the blobstore' do - it 'raises a BlobstoreError' do - allow(httpclient).to receive(:put).and_raise(Errno::ENETUNREACH.new) - expect { client.cp_to_blobstore(tmpfile.path, 'foobar') }.to raise_error BlobstoreError - expect(logger).to have_received(:error).with(/^Error with blobstore: Network is unreachable/) - end - end - end - - describe '#cp_r_to_blobstore' do - let(:source_dir) { Dir.mktmpdir } - let!(:tmpfile1) do - Tempfile.open('', source_dir) do |tmpfile| - tmpfile.write('file 1') - tmpfile - end - end - let!(:tmpfile2) do - Tempfile.open('', source_dir) do |tmpfile| - tmpfile.write('file 2') - tmpfile - end - end - let(:tmpfile1_sha) { Digester.new.digest_path(tmpfile1.path) } - let(:tmpfile2_sha) { Digester.new.digest_path(tmpfile2.path) } - let(:nested_dir) { Dir.mkdir(File.join(source_dir, 'nested')) } - let!(:nested_tmpfile1) do - Tempfile.open('', source_dir) do |tmpfile| - tmpfile.write('nested file 1') - tmpfile - end - end - let(:nested_tmpfile1_sha) { Digester.new.digest_path(nested_tmpfile1.path) } - - before do - allow(httpclient).to receive_messages(head: instance_double(HTTP::Message, status: 404)) - end - - after do - FileUtils.rm_rf(source_dir) - end - - it 'uploads all the files in the directory and nested directories' do - allow(response).to receive_messages(status: 201, content: '') - allow(httpclient).to receive(:put).and_return(response) - - client.cp_r_to_blobstore(source_dir) - - expect(httpclient).to have_received(:put).thrice - expect(httpclient).to have_received(:put).with( - "http://localhost/admin/droplets/#{tmpfile1_sha[0..1]}/#{tmpfile1_sha[2..3]}/#{tmpfile1_sha}", a_kind_of(File), {} - ) - expect(httpclient).to have_received(:put).with("http://localhost/admin/droplets/#{tmpfile2_sha[0..1]}/#{tmpfile2_sha[2..3]}/#{tmpfile2_sha}", a_kind_of(File), {}) - expect(httpclient).to have_received(:put).with( - "http://localhost/admin/droplets/#{nested_tmpfile1_sha[0..1]}/#{nested_tmpfile1_sha[2..3]}/#{nested_tmpfile1_sha}", a_kind_of(File), {} - ) - end - - context 'when a file already exists in the blobstore' do - before do - success_response = instance_double(HTTP::Message, status: 200) - allow(httpclient).to receive(:head).with(/#{nested_tmpfile1_sha}/, anything).and_return(success_response) - end - - it 'does not re-upload it' do - allow(response).to receive_messages(status: 201, content: '') - allow(httpclient).to receive(:put).and_return(response) - - client.cp_r_to_blobstore(source_dir) - - expect(httpclient).to have_received(:put).twice - expect(httpclient).not_to have_received(:put).with( - "http://localhost/admin/droplets/#{nested_tmpfile1_sha[0..1]}/#{nested_tmpfile1_sha[2..3]}/#{nested_tmpfile1_sha}", a_kind_of(File), {} - ) - end - end - - describe 'file size limits' do - let(:min_size) { 20 } - let(:max_size) { 50 } - - context 'too small file' do - let!(:small_file) do - Tempfile.open('', source_dir) do |tmpfile| - tmpfile.write('a' * (min_size - 1)) - tmpfile - end - end - let(:small_file_sha) { Digester.new.digest_path(small_file.path) } - - it 'does not copy files below the minimum size limit' do - allow(response).to receive_messages(status: 201, content: '') - allow(httpclient).to receive(:put).and_return(response) - - client.cp_r_to_blobstore(source_dir) - - expect(httpclient).not_to have_received(:put).with( - "http://localhost/admin/droplets/#{small_file_sha[0..1]}/#{small_file_sha[2..3]}/#{small_file_sha}", a_kind_of(File), {} - ) - end - end - - context 'too large file' do - let!(:large_file) do - Tempfile.open('', source_dir) do |tmpfile| - tmpfile.write('a' * (max_size + 1)) - tmpfile - end - end - let(:large_file_sha) { Digester.new.digest_path(large_file.path) } - - it 'does not copy files above the maximum size limit' do - allow(response).to receive_messages(status: 201, content: '') - allow(httpclient).to receive(:put).and_return(response) - - client.cp_r_to_blobstore(source_dir) - - expect(httpclient).not_to have_received(:put).with( - "http://localhost/admin/droplets/#{large_file_sha[0..1]}/#{large_file_sha[2..3]}/#{large_file_sha}", a_kind_of(File), {} - ) - end - end - end - - describe 'file permissions' do - context 'sufficient file permissions' do - let!(:sufficiently_permissioned_file) do - Tempfile.open('', source_dir) do |tmpfile| - File.chmod(0o600, tmpfile) - tmpfile - end - end - let(:sufficient_perm_file_sha) { Digester.new.digest_path(sufficiently_permissioned_file.path) } - - it 'copies files above the maximum size limit' do - allow(response).to receive_messages(status: 201, content: '') - allow(httpclient).to receive(:put).and_return(response) - - client.cp_r_to_blobstore(source_dir) - - expect(httpclient).to have_received(:put).with( - "http://localhost/admin/droplets/#{sufficient_perm_file_sha[0..1]}/#{sufficient_perm_file_sha[2..3]}/#{sufficient_perm_file_sha}", a_kind_of(File), {} - ) - end - end - - context 'insufficient file permissions' do - let!(:insufficiently_permissioned_file) do - Tempfile.open('', source_dir) do |tmpfile| - File.chmod(0o444, tmpfile) - tmpfile - end - end - let(:insufficient_perm_file_sha) { Digester.new.digest_path(insufficiently_permissioned_file.path) } - - it 'does not copy files above the maximum size limit' do - allow(response).to receive_messages(status: 201, content: '') - allow(httpclient).to receive(:put).and_return(response) - - client.cp_r_to_blobstore(source_dir) - - expect(httpclient).not_to have_received(:put).with( - "http://localhost/admin/droplets/#{insufficient_perm_file_sha[0..1]}/#{insufficient_perm_file_sha[2..3]}/#{insufficient_perm_file_sha}", a_kind_of(File), {} - ) - end - end - end - - context 'when an OpenSSL::SSL::SSLError is raised' do - it 'reraises a BlobstoreError' do - allow(httpclient).to receive(:put).and_raise(OpenSSL::SSL::SSLError.new) - expect { client.cp_r_to_blobstore(source_dir) }.to raise_error BlobstoreError, /SSL verification failed/ - expect(logger).to have_received(:error).with(/^SSL verification failed: OpenSSL::SSL::SSLError/) - end - end - - context 'when an unknown error occurs with the blobstore' do - it 'raises a BlobstoreError' do - allow(httpclient).to receive(:put).and_raise(Errno::EHOSTUNREACH.new) - expect { client.cp_r_to_blobstore(source_dir) }.to raise_error BlobstoreError - expect(logger).to have_received(:error).with(/^Error with blobstore: No route to host/) - end - end - end - - describe '#cp_file_between_keys' do - it 'creates an empty file at the destination location to ensure all folder paths are create before the copy' do - allow(response).to receive_messages(status: 204, content: '') - allow(httpclient).to receive_messages(put: response, request: response) - - client.cp_file_between_keys('foobar', 'bazbar') - - expect(httpclient).to have_received(:put).with('http://localhost/admin/droplets/ba/zb/bazbar', '', {}) - end - - it 'copies the file from the source key to the destination key' do - allow(response).to receive_messages(status: 204, content: '') - allow(httpclient).to receive_messages(put: response, request: response) - - client.cp_file_between_keys('foobar', 'bazbar') - - expect(httpclient).to have_received(:request). - with( - :copy, - 'http://localhost/admin/droplets/fo/ob/foobar', - header: { 'Destination' => 'http://localhost/admin/droplets/ba/zb/bazbar' } - ) - end - - it 'raises an exception when there is an error copying an object' do - allow(response).to receive_messages(status: 500, content: 'Internal Server Error') - allow(httpclient).to receive_messages(put: instance_double(HTTP::Message, status: 204, content: ''), request: response) - - expect { client.cp_file_between_keys('foobar', 'bazbar') }.to raise_error BlobstoreError, /Could not copy object/ - expect(logger).to have_received(:error).with(/^Error with blobstore: Could not copy object/) - end - - it 'raises an exception when there is an error creating the destination object' do - allow(response).to receive_messages(status: 500, content: 'Internal Server Error') - allow(httpclient).to receive(:put).and_return(response) - - expect { client.cp_file_between_keys('foobar', 'bazbar') }.to raise_error BlobstoreError, /Could not copy object/ - expect(logger).to have_received(:error).with(/^Error with blobstore: Could not copy object/) - end - - context 'when the source key has no file associated with it' do - it 'raises a FileNotFound Error' do - allow(response).to receive_messages(status: 404, content: 'Not Found') - allow(httpclient).to receive_messages(put: instance_double(HTTP::Message, status: 204, content: ''), request: response) - - expect do - client.cp_file_between_keys('foobar', 'bazbar') - end.to raise_error(CloudController::Blobstore::FileNotFound, /Could not find object 'foobar'/) - end - end - - context 'when an OpenSSL::SSL::SSLError is raised' do - context 'when creating a destination' do - it 'reraises a BlobstoreError' do - allow(httpclient).to receive(:put).and_raise(OpenSSL::SSL::SSLError.new) - expect { client.cp_file_between_keys('foobar', 'bazbar') }.to raise_error BlobstoreError, /SSL verification failed/ - expect(logger).to have_received(:error).with(/^SSL verification failed: OpenSSL::SSL::SSLError/) - end - end - - context 'when an unknown error occurs with the blobstore' do - it 'raises a BlobstoreError' do - allow(httpclient).to receive(:put).and_raise(Errno::EBADF.new) - expect { client.cp_file_between_keys('foobar', 'bazbar') }.to raise_error BlobstoreError - expect(logger).to have_received(:error).with(/^Error with blobstore: Bad file descriptor/) - end - end - - context 'when copying files' do - it 'reraises a BlobstoreError' do - allow(response).to receive_messages(status: 204, content: '') - allow(httpclient).to receive(:put).and_return(response) - allow(httpclient).to receive(:request).and_raise(OpenSSL::SSL::SSLError.new) - expect { client.cp_file_between_keys('foobar', 'bazbar') }.to raise_error BlobstoreError, /SSL verification failed/ - expect(logger).to have_received(:error).with(/^SSL verification failed: OpenSSL::SSL::SSLError/) - end - end - end - end - - describe '#delete' do - it 'deletes an object' do - allow(response).to receive_messages(status: 204, content: '') - allow(httpclient).to receive(:delete).and_return(response) - - client.delete('foobar') - - expect(httpclient).to have_received(:delete).with('http://localhost/admin/droplets/fo/ob/foobar', header: {}) - end - - it 'raises FileNotFound error when the file is not found in blobstore during deleting' do - allow(response).to receive_messages(status: 404, content: 'Not Found') - allow(httpclient).to receive(:delete).and_return(response) - - expect do - client.delete('foobar') - end.to raise_error CloudController::Blobstore::FileNotFound, /Could not find object 'foobar'/ - end - - it 'raises an exception when there is an error deleting an object' do - allow(response).to receive_messages(status: 500, content: '') - expect(httpclient).to receive(:delete).and_return(response) - - expect { client.delete('foobar') }.to raise_error BlobstoreError, /Could not delete object/ - expect(logger).to have_received(:error).with(/^Error with blobstore: Could not delete object/) - end - - it 'raises a ConflictError when there is a conflict deleting an object' do - allow(response).to receive_messages(status: 409, content: '') - expect(httpclient).to receive(:delete).and_return(response) - - expect { client.delete('foobar') }.to raise_error ConflictError, /Conflict deleting object/ - end - - context 'when an OpenSSL::SSL::SSLError is raised' do - it 'reraises a BlobstoreError' do - allow(httpclient).to receive(:delete).and_raise(OpenSSL::SSL::SSLError.new) - expect { client.delete('foobar') }.to raise_error BlobstoreError, /SSL verification failed/ - expect(logger).to have_received(:error).with(/^SSL verification failed: OpenSSL::SSL::SSLError/) - end - end - - context 'when an unknown error occurs with the blobstore' do - it 'raises a BlobstoreError' do - allow(httpclient).to receive(:delete).and_raise(Errno::EIO.new) - expect { client.delete('and bingo was his name-o') }.to raise_error BlobstoreError - expect(logger).to have_received(:error).with(%r{^Error with blobstore: Input/output error}) - end - end - end - - describe '#delete_all' do - let(:root_dir) { 'buildpack_cache' } - - it 'deletes the collection' do - allow(httpclient).to receive(:delete).and_return(instance_double(HTTP::Message, status: 204, content: '')) - client.delete_all - expect(httpclient).to have_received(:delete).with('http://localhost/admin/droplets/buildpack_cache/', header: {}) - end - - it 'raises FileNotfound when the server returns 404' do - allow(httpclient).to receive(:delete).and_return(instance_double(HTTP::Message, status: 404, content: '')) - expect do - client.delete_all - end.to raise_error(FileNotFound, /Could not find object/) - expect(httpclient).to have_received(:delete).with('http://localhost/admin/droplets/buildpack_cache/', header: {}) - end - - it 'raises an error when the server returns any other code' do - allow(httpclient).to receive(:delete).and_return(instance_double(HTTP::Message, status: 500, content: '')) - expect do - client.delete_all - end.to raise_error(BlobstoreError, /Could not delete all/) - expect(httpclient).to have_received(:delete).with('http://localhost/admin/droplets/buildpack_cache/', header: {}) - end - - context 'when an OpenSSL::SSL::SSLError is raised' do - it 'reraises a BlobstoreError' do - allow(httpclient).to receive(:delete).and_raise(OpenSSL::SSL::SSLError.new) - expect { client.delete_all }.to raise_error BlobstoreError, /SSL verification failed/ - expect(logger).to have_received(:error).with(/^SSL verification failed: OpenSSL::SSL::SSLError/) - end - end - - context 'when an unknown error occurs with the blobstore' do - it 'raises a BlobstoreError' do - allow(httpclient).to receive(:delete).and_raise(Errno::EHOSTUNREACH.new) - expect { client.delete_all }.to raise_error BlobstoreError - expect(logger).to have_received(:error).with(/^Error with blobstore: No route to host/) - end - end - end - - describe '#delete_all_in_path' do - let(:root_dir) { 'buildpack_cache' } - - it 'deletes the collection' do - allow(httpclient).to receive(:delete).and_return(instance_double(HTTP::Message, status: 204, content: '')) - client.delete_all_in_path('foobar') - expect(httpclient).to have_received(:delete).with('http://localhost/admin/droplets/buildpack_cache/fo/ob/foobar/', header: {}) - end - - it 'does not fail when the collection does not exist' do - allow(httpclient).to receive(:delete).and_return(instance_double(HTTP::Message, status: 404, content: '')) - client.delete_all_in_path('foobar') - expect(httpclient).to have_received(:delete).with('http://localhost/admin/droplets/buildpack_cache/fo/ob/foobar/', header: {}) - end - - it 'raises an error when the server returns any other code' do - allow(httpclient).to receive(:delete).and_return(instance_double(HTTP::Message, status: 500, content: '')) - expect do - client.delete_all_in_path('foobar') - end.to raise_error(BlobstoreError, /Could not delete all in path/) - expect(logger).to have_received(:error).with(/^Error with blobstore: Could not delete all in path/) - expect(httpclient).to have_received(:delete).with('http://localhost/admin/droplets/buildpack_cache/fo/ob/foobar/', header: {}) - end - - context 'when an OpenSSL::SSL::SSLError is raised' do - it 'reraises a BlobstoreError' do - allow(httpclient).to receive(:delete).and_raise(OpenSSL::SSL::SSLError.new) - expect { client.delete_all_in_path('foobar') }.to raise_error BlobstoreError, /SSL verification failed/ - expect(logger).to have_received(:error).with(/^SSL verification failed: OpenSSL::SSL::SSLError/) - end - end - - context 'when an unknown error occurs with the blobstore' do - it 'raises a BlobstoreError' do - allow(httpclient).to receive(:delete).and_raise(Errno::ECONNREFUSED.new) - expect { client.delete_all_in_path('foobar') }.to raise_error BlobstoreError - expect(logger).to have_received(:error).with(/^Error with blobstore: Connection refused/) - end - end - end - - describe '#blob' do - it 'returns a blob' do - allow(response).to receive_messages(status: 200) - allow(httpclient).to receive_messages(head: response) - - blob = client.blob('foobar') - - expect(blob).to be_a(DavBlob) - end - - it 'returns nil if there is no object at the key' do - allow(response).to receive_messages(status: 404) - allow(httpclient).to receive_messages(head: response) - - blob = client.blob('foobar') - - expect(blob).to be_nil - end - - it 'raises a BlobstoreError if response status is neither 200 nor 404' do - allow(response).to receive_messages(status: 500, content: '') - allow(httpclient).to receive_messages(head: response) - - expect { client.exists?('foobar') }.to raise_error BlobstoreError, /Could not get object/ - expect(logger).to have_received(:error).with(/^Error with blobstore: Could not get object/) - end - - context 'when an OpenSSL::SSL::SSLError is raised' do - it 'reraises a BlobstoreError' do - allow(httpclient).to receive(:head).and_raise(OpenSSL::SSL::SSLError.new) - expect { client.blob('foobar') }.to raise_error BlobstoreError, /SSL verification failed/ - expect(logger).to have_received(:error).with(/^SSL verification failed: OpenSSL::SSL::SSLError/) - end - end - - context 'when an unknown error occurs with the blobstore' do - it 'raises a BlobstoreError' do - allow(httpclient).to receive(:head).and_raise(Errno::ECONNREFUSED.new) - expect { client.blob('foobar') }.to raise_error BlobstoreError - expect(logger).to have_received(:error).with(/^Error with blobstore: Connection refused/) - end - end - end - - describe '#delete_blob' do - it 'deletes the blobs key' do - allow(response).to receive_messages(status: 204, content: '') - allow(httpclient).to receive_messages(delete: response) - blob = DavBlob.new(httpmessage: instance_double(HTTPClient), key: 'fo/ob/foobar', signer: nil) - - client.delete_blob(blob) - - expect(httpclient).to have_received(:delete).with('http://localhost/admin/droplets/fo/ob/foobar', header: {}) - end - - it 'does not error if the object is already deleted' do - allow(response).to receive_messages(status: 404, content: 'Not Found') - allow(httpclient).to receive_messages(delete: response) - blob = DavBlob.new(httpmessage: instance_double(HTTPClient), key: 'fo/ob/foobar', signer: nil) - - expect { client.delete_blob(blob) }.not_to raise_error - expect(httpclient).to have_received(:delete).with('http://localhost/admin/droplets/fo/ob/foobar', header: {}) - end - - context 'when an OpenSSL::SSL::SSLError is raised' do - it 'reraises a BlobstoreError' do - blob = DavBlob.new(httpmessage: instance_double(HTTPClient), key: 'fo/ob/foobar', signer: nil) - allow(httpclient).to receive(:delete).and_raise(OpenSSL::SSL::SSLError.new) - - expect { client.delete_blob(blob) }.to raise_error BlobstoreError, /SSL verification failed/ - expect(logger).to have_received(:error).with(/^SSL verification failed: OpenSSL::SSL::SSLError/) - end - end - - context 'when an unknown error occurs with the blobstore' do - it 'raises a BlobstoreError' do - blob = DavBlob.new(httpmessage: instance_double(HTTPClient), key: 'fo/ob/foobar', signer: nil) - allow(httpclient).to receive(:delete).and_raise(Errno::ECONNREFUSED.new) - - expect { client.delete_blob(blob) }.to raise_error BlobstoreError - expect(logger).to have_received(:error).with(/^Error with blobstore: Connection refused/) - end - end - end - - describe '#files_for' do - let(:root_xml_body) do - <<-XML - - - - - - always-ignore-first-obj - - - - - - - - - - nested-dir-1 - - - - - - - - - - nested-dir-2 - - - - - - - - XML - end - let(:nested_xml_body) do - <<-XML - - - - - - always-ignore-first-obj - - - - - - - - - - some-blob - - - - - - XML - end - let(:nested_xml_body_2) do - <<-XML - - - - - - always-ignore-first-obj - - - - - - - - - - some-other-blob - - - - - - XML - end - - let(:fake_root_response) { double(:response, body: root_xml_body) } - let(:fake_nested_response) { double(:response, body: nested_xml_body) } - let(:fake_nested_response_2) { double(:response, body: nested_xml_body_2) } - - before do - allow(httpclient).to receive(:request). - with(:propfind, 'http://localhost/admin/droplets/', nil, nil, {}). - and_return(fake_root_response) - allow(httpclient).to receive(:request). - with(:propfind, 'http://localhost/admin/droplets/nested-dir-2', nil, nil, {}). - and_return(fake_nested_response_2) - allow(httpclient).to receive(:request). - with(:propfind, 'http://localhost/admin/droplets/nested-dir-1', nil, nil, {}). - and_return(fake_nested_response) - end - - it 'enumerates a web dav blobstore' do - blob_keys = client.files_for('nested').first(2).map(&:key) - - expect(blob_keys).to match_array(%w[nested-dir-1/some-blob nested-dir-2/some-other-blob]) - end - - context 'when provided a path to ignore' do - let(:root_xml_body) do - <<-XML - - - - - - always-ignore-first-obj - - - - - - - - - - ignored-directory - - - - - - - - XML - end - - it 'does not enumerate ignored directories' do - blob_keys = client.files_for('', %w[some-directory ignored-directory]) - expect(blob_keys.first).to be_nil - end - end - - context 'when provided a prefix to enumerate' do - let(:root_xml_body) do - <<-XML - - - - - - always-ignore-first-obj - - - - - - - - - - ab-directory - - - - - - - - - - cd-directory - - - - - - - - XML - end - - before do - allow(httpclient).to receive(:request). - with(:propfind, 'http://localhost/admin/droplets/ab-directory', nil, nil, {}). - and_return(fake_nested_response) - end - - it 'only enumerates directories that start with the prefix' do - blob_keys = client.files_for('ab').map(&:key) - - expect(blob_keys).to match_array(%w[ab-directory/some-blob]) - end - end - end - - context 'when root_dir is configured' do - let(:root_dir) { 'root_dir' } - - it 'includes it in the key' do - allow(response).to receive_messages(status: 200) - allow(httpclient).to receive_messages(head: response) - - expect(client.exists?('foobar')).to be(true) - expect(httpclient).to have_received(:head).with('http://localhost/admin/droplets/root_dir/fo/ob/foobar', header: {}) - end - end - end - end -end diff --git a/spec/unit/lib/cloud_controller/blobstore/webdav/nginx_secure_link_signer_spec.rb b/spec/unit/lib/cloud_controller/blobstore/webdav/nginx_secure_link_signer_spec.rb deleted file mode 100644 index f2cf060e3d7..00000000000 --- a/spec/unit/lib/cloud_controller/blobstore/webdav/nginx_secure_link_signer_spec.rb +++ /dev/null @@ -1,141 +0,0 @@ -require 'spec_helper' -require 'cloud_controller/blobstore/webdav/nginx_secure_link_signer' - -module CloudController - module Blobstore - RSpec.describe NginxSecureLinkSigner do - subject(:signer) do - NginxSecureLinkSigner.new( - internal_endpoint: internal_endpoint, - internal_path_prefix: internal_path_prefix, - public_endpoint: public_endpoint, - public_path_prefix: public_path_prefix, - basic_auth_user: user, - basic_auth_password: password, - httpclient: httpclient - ) - end - - let(:httpclient) { instance_double(HTTPClient) } - - let(:expires) { 16_726_859_876 } # some time in the year 2500 - - let(:internal_endpoint) { 'http://internal.example.com' } - let(:internal_path_prefix) { nil } - - let(:public_endpoint) { 'https://public.example.com' } - let(:public_path_prefix) { nil } - - let(:user) { 'some-user' } - let(:password) { 'some-password' } - let(:basic_auth_header) { { 'Authorization' => 'Basic ' + Base64.strict_encode64("#{user}:#{password}").strip } } - - describe '#sign_internal_url' do - let(:response) { instance_double(HTTP::Message, content: 'https://signed.example.com?valid-signing=some-md5-stuff', status: 200) } - - before do - allow(httpclient).to receive_messages(get: response) - end - - it 'requests a signed url from the blobstore with expires and path params' do - signer.sign_internal_url(expires: expires, path: 'some/path') - - expected_request_uri = 'http://internal.example.com/sign?expires=16726859876&path=%2Fsome%2Fpath' - expect(httpclient).to have_received(:get).with(expected_request_uri, header: basic_auth_header) - end - - it 'returns the signed url from the response with the internal endpoint host as the signed uri host' do - signed_url = signer.sign_internal_url(expires: expires, path: 'some/path') - expect(signed_url).to eq('https://internal.example.com?valid-signing=some-md5-stuff') - end - - context 'when internal_path_prefix is configured' do - let(:internal_path_prefix) { '/read/directory' } - - it 'prepends it to the path in the signing request' do - signer.sign_internal_url(expires: expires, path: 'some/path') - - expected_request_uri = 'http://internal.example.com/sign?expires=16726859876&path=%2Fread%2Fdirectory%2Fsome%2Fpath' - expect(httpclient).to have_received(:get).with(expected_request_uri, header: basic_auth_header) - end - end - - context 'when the request returns an error' do - let(:response) { instance_double(HTTP::Message, status: 401, content: '') } - - it 'raises an error' do - expect do - signer.sign_internal_url(expires: expires, path: 'some/path') - end.to raise_error(SigningRequestError, /Could not get a signed url/) - end - end - - it 'raises SigningRequestError when HTTPClient raises SSLError' do - allow(httpclient).to receive(:get).and_raise(OpenSSL::SSL::SSLError.new('My SSL Error')) - - expect do - signer.sign_internal_url(expires: expires, path: 'some/path') - end.to raise_error(SigningRequestError, /My SSL Error/) - end - end - - describe '#sign_public_url' do - let(:response) { instance_double(HTTP::Message, content: 'https://signed.example.com?valid-signing=some-md5-stuff', status: 200) } - - before do - allow(httpclient).to receive_messages(get: response) - end - - it 'requests a signed url from the blobstore with expires and path params' do - signer.sign_public_url(expires: expires, path: 'some/path') - - expected_request_uri = 'http://internal.example.com/sign?expires=16726859876&path=%2Fsome%2Fpath' - expect(httpclient).to have_received(:get).with(expected_request_uri, header: basic_auth_header) - end - - it 'returns the signed url from the response with the public endpoint host as the signed uri host' do - signed_url = signer.sign_public_url(expires: expires, path: 'some/path') - expect(signed_url).to eq('https://public.example.com?valid-signing=some-md5-stuff') - end - - context 'when public_path_prefix is configured' do - let(:public_path_prefix) { '/read/directory' } - - it 'prepends it to the path in the signing request' do - signer.sign_public_url(expires: expires, path: 'some/path') - - expected_request_uri = 'http://internal.example.com/sign?expires=16726859876&path=%2Fread%2Fdirectory%2Fsome%2Fpath' - expect(httpclient).to have_received(:get).with(expected_request_uri, header: basic_auth_header) - end - end - - context 'when the public endpoint does not have https scheme' do - let(:public_endpoint) { 'http://blobstore.example.com' } - - it 'signs the url with an https scheme' do - signed_url = signer.sign_public_url(expires: expires, path: 'some/path') - expect(signed_url).to eq('https://blobstore.example.com?valid-signing=some-md5-stuff') - end - end - - context 'when the request returns an error' do - let(:response) { instance_double(HTTP::Message, status: 401, content: '') } - - it 'raises an error' do - expect do - signer.sign_public_url(expires: expires, path: 'some/path') - end.to raise_error(SigningRequestError, /Could not get a signed url/) - end - end - - it 'raises SigningRequestError when HTTPClient raises SSLError' do - allow(httpclient).to receive(:get).and_raise(OpenSSL::SSL::SSLError.new('My SSL Error')) - - expect do - signer.sign_internal_url(expires: expires, path: 'some/path') - end.to raise_error(SigningRequestError, /My SSL Error/) - end - end - end - end -end