Skip to content

Commit 12c1b6d

Browse files
committed
POC: bosh-azure-storage-cli based blobstore client
Wrapper client for https://github.com/cloudfoundry/bosh-azure-storage-cli
1 parent f9e7faf commit 12c1b6d

File tree

3 files changed

+201
-0
lines changed

3 files changed

+201
-0
lines changed
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
module CloudController
2+
module Blobstore
3+
class AzureBlob < Blob
4+
attr_reader :key, :signed_url
5+
6+
def initialize(key, exists:, signed_url:)
7+
@key = key
8+
@exists = exists
9+
@signed_url = signed_url
10+
end
11+
12+
def file
13+
self
14+
end
15+
16+
def exists?
17+
@exists
18+
end
19+
20+
def local_path
21+
nil
22+
end
23+
24+
def internal_download_url
25+
signed_url
26+
end
27+
28+
def public_download_url
29+
signed_url
30+
end
31+
32+
def attributes(*)
33+
{ key: @key }
34+
end
35+
end
36+
end
37+
end
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
require 'open3'
2+
require 'tempfile'
3+
require 'fileutils'
4+
require 'cloud_controller/blobstore/base_client'
5+
require 'cloud_controller/blobstore/cli/azure_blob'
6+
7+
module CloudController
8+
module Blobstore
9+
# POC: This client uses the `azure-storage-cli` tool from bosh to interact with Azure Blob Storage.
10+
# It is a proof of concept and not intended for production use.
11+
# Goal of this POC is to find out if the bosh blobstore CLIs can be used as a replacement for the fog.
12+
13+
class AzureCliClient < BaseClient
14+
attr_reader :root_dir, :min_size, :max_size
15+
16+
def initialize(fog_connection:, directory_key:, root_dir:, min_size: nil, max_size: nil)
17+
@cli_path = ENV['AZURE_STORAGE_CLI_PATH'] || '/var/vcap/packages/azure-storage-cli/bin/azure-storage-cli'
18+
@directory_key = directory_key
19+
@root_dir = root_dir
20+
@min_size = min_size
21+
@max_size = max_size
22+
23+
config = {
24+
'account_name' => fog_connection[:azure_storage_account_name],
25+
'account_key' => fog_connection[:azure_storage_access_key],
26+
'container_name' => @directory_key,
27+
'environment' => fog_connection[:environment]
28+
29+
}.compact
30+
31+
@config_file = write_config_file(config, fog_connection[:container_name])
32+
end
33+
34+
def cp_to_blobstore(source_path, destination_key)
35+
logger.info("[azure-blobstore] cp_to_blobstore: uploading #{source_path}#{destination_key}")
36+
run_cli('put', source_path, partitioned_key(destination_key))
37+
end
38+
39+
# rubocop:disable Lint/UnusedMethodArgument
40+
def download_from_blobstore(source_key, destination_path, mode: nil)
41+
# rubocop:enable Lint/UnusedMethodArgument
42+
logger.info("[azure-blobstore] download_from_blobstore: downloading #{source_key}#{destination_path}")
43+
FileUtils.mkdir_p(File.dirname(destination_path))
44+
run_cli('get', partitioned_key(source_key), destination_path)
45+
46+
# POC: Writing chunks to file is not implemented yet
47+
# POC: mode is not used for now
48+
end
49+
50+
def exists?(blobstore_key)
51+
key = partitioned_key(blobstore_key)
52+
logger.info("[azure-blobstore] [exists?] Checking existence for: #{key}")
53+
status = run_cli('exists', key, allow_nonzero: true)
54+
55+
if status.exitstatus == 0
56+
return true
57+
elsif status.exitstatus == 3
58+
return false
59+
end
60+
61+
false
62+
rescue StandardError => e
63+
logger.error("[azure-blobstore] [exists?] azure-storage-cli exists raised error: #{e.message} for #{key}")
64+
false
65+
end
66+
67+
def delete_blob(blob)
68+
delete(blob.file.key)
69+
end
70+
71+
def delete(key)
72+
logger.info("[azure-blobstore] delete: removing blob with key #{key}")
73+
run_cli('delete', partitioned_key(key))
74+
end
75+
76+
# Methods like `delete_all` and `delete_all_in_path` are not implemented in this POC.
77+
78+
def blob(key)
79+
logger.info("[azure-blobstore] blob: retrieving blob with key #{key}")
80+
81+
return nil unless exists?(key)
82+
83+
signed_url = sign_url(partitioned_key(key), verb: 'get', expires_in_seconds: 3600)
84+
AzureBlob.new(key, exists: true, signed_url: signed_url)
85+
end
86+
87+
def sign_url(key, verb:, expires_in_seconds:)
88+
logger.info("[azure-blobstore] sign_url: signing URL for key #{key} with verb #{verb} and expires_in_seconds #{expires_in_seconds}")
89+
stdout, stderr, status = Open3.capture3(@cli_path, '-c', @config_file, 'sign', key, verb.to_s.downcase, "#{expires_in_seconds}s")
90+
raise "azure-storage-cli sign failed: #{stderr}" unless status.success?
91+
92+
stdout.strip
93+
end
94+
95+
def ensure_bucket_exists
96+
# POC - not sure if this is needed
97+
end
98+
99+
def cp_file_between_keys(source_key, destination_key)
100+
logger.info("[azure-blobstore] cp_file_between_keys: copying from #{source_key} to #{destination_key}")
101+
# Azure CLI doesn't support server-side copy yet, so fallback to local copy
102+
# POC! We should copy directly in the cli if possible
103+
Tempfile.create('blob-copy') do |tmp|
104+
download_from_blobstore(source_key, tmp.path)
105+
cp_to_blobstore(tmp.path, destination_key)
106+
end
107+
end
108+
109+
def local?
110+
false
111+
end
112+
113+
private
114+
115+
def run_cli(command, *args, allow_nonzero: false)
116+
logger.info("[azure-blobstore] Running azure-storage-cli: #{@cli_path} -c #{@config_file} #{command} #{args.join(' ')}")
117+
_, stderr, status = Open3.capture3(@cli_path, '-c', @config_file, command, *args)
118+
return status if allow_nonzero
119+
120+
raise "azure-storage-cli #{command} failed: #{stderr}" unless status.success?
121+
122+
status
123+
end
124+
125+
def write_config_file(config, container_name)
126+
config_dir = File.join(tmpdir, 'blobstore-configs')
127+
FileUtils.mkdir_p(config_dir)
128+
129+
config_file_path = File.join(config_dir, "blobstore-config-#{container_name}")
130+
File.open(config_file_path, 'w', 0o600) do |f|
131+
f.write(Oj.dump(config))
132+
end
133+
config_file_path
134+
end
135+
136+
def tmpdir
137+
VCAP::CloudController::Config.config.get(:directories, :tmpdir)
138+
end
139+
140+
def logger
141+
@logger ||= Steno.logger('cc.azure_cli_client')
142+
end
143+
end
144+
end
145+
end

lib/cloud_controller/blobstore/client_provider.rb

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
require 'cloud_controller/blobstore/fog/error_handling_client'
55
require 'cloud_controller/blobstore/webdav/dav_client'
66
require 'cloud_controller/blobstore/safe_delete_client'
7+
require 'cloud_controller/blobstore/cli/azure_cli_client'
78
require 'google/apis/errors'
89

910
module CloudController
@@ -12,6 +13,8 @@ class ClientProvider
1213
def self.provide(options:, directory_key:, root_dir: nil, resource_type: nil)
1314
if options[:blobstore_type].blank? || (options[:blobstore_type] == 'fog')
1415
provide_fog(options, directory_key, root_dir)
16+
elsif options[:blobstore_type] == 'cli'
17+
provide_azure_cli(options, directory_key, root_dir)
1518
else
1619
provide_webdav(options, directory_key, root_dir)
1720
end
@@ -65,6 +68,22 @@ def provide_webdav(options, directory_key, root_dir)
6568

6669
Client.new(SafeDeleteClient.new(retryable_client, root_dir))
6770
end
71+
72+
def provide_azure_cli(options, directory_key, root_dir)
73+
74+
client = AzureCliClient.new(fog_connection: options.fetch(:fog_connection),
75+
directory_key: directory_key,
76+
root_dir: root_dir,
77+
min_size: options[:minimum_size],
78+
max_size: options[:maximum_size],
79+
)
80+
81+
logger = Steno.logger('cc.blobstore.azure_cli')
82+
errors = [StandardError]
83+
retryable_client = RetryableClient.new(client: client, errors: errors, logger: logger)
84+
85+
Client.new(SafeDeleteClient.new(retryable_client, root_dir))
86+
end
6887
end
6988
end
7089
end

0 commit comments

Comments
 (0)