Skip to content

Commit a3f317d

Browse files
committed
adapt ccng to read config from json, add config checks, adapt test
1 parent 8c8c630 commit a3f317d

File tree

4 files changed

+174
-66
lines changed

4 files changed

+174
-66
lines changed

lib/cloud_controller/blobstore/client_provider.rb

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -72,14 +72,13 @@ def provide_webdav(options, directory_key, root_dir)
7272
end
7373

7474
def provide_storage_cli(options, directory_key, root_dir, resource_type)
75-
raise BlobstoreError.new('connection_config for storage-cli is not provided') unless options[:connection_config]
76-
77-
client = StorageCliClient.build(connection_config: options.fetch(:connection_config),
78-
directory_key: directory_key,
79-
resource_type: resource_type,
80-
root_dir: root_dir,
81-
min_size: options[:minimum_size],
82-
max_size: options[:maximum_size])
75+
client = StorageCliClient.build(
76+
directory_key: directory_key,
77+
resource_type: resource_type,
78+
root_dir: root_dir,
79+
min_size: options[:minimum_size],
80+
max_size: options[:maximum_size]
81+
)
8382

8483
logger = Steno.logger('cc.blobstore.storage_cli_client')
8584
errors = [StandardError]

lib/cloud_controller/blobstore/storage_cli/storage_cli_client.rb

Lines changed: 67 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -16,29 +16,82 @@ class << self
1616
attr_reader :registry
1717

1818
def register(provider, klass)
19-
registry[provider] = klass
19+
registry[provider.to_s] = klass
2020
end
2121

22-
def build(connection_config:, directory_key:, root_dir:, resource_type: nil, min_size: nil, max_size: nil)
23-
provider = connection_config[:provider]
24-
raise 'Missing connection_config[:provider]' if provider.nil?
22+
def build(directory_key:, root_dir:, resource_type: nil, min_size: nil, max_size: nil)
2523
raise 'Missing resource_type' if resource_type.nil?
2624

27-
impl_class = registry[provider]
25+
cfg = fetch_and_validate_config!(resource_type)
26+
provider = cfg['provider']
27+
28+
key = provider.to_s
29+
impl_class = registry[key] || registry[key.downcase] || registry[key.upcase]
2830
raise "No storage CLI client registered for provider #{provider}" unless impl_class
2931

30-
impl_class.new(connection_config:, directory_key:, root_dir:, resource_type:, min_size:, max_size:)
32+
impl_class.new(provider:, directory_key:, root_dir:, resource_type:, min_size:, max_size:)
33+
end
34+
35+
def fetch_and_validate_config!(resource_type)
36+
path = config_path_for!(resource_type)
37+
38+
begin
39+
json = Oj.load(File.read(path))
40+
rescue StandardError => e
41+
raise BlobstoreError.new("Failed to parse storage-cli config JSON at #{path}: #{e.message}")
42+
end
43+
44+
validate_required_keys!(json, path)
45+
json
46+
end
47+
48+
def config_path_for!(resource_type)
49+
key =
50+
case resource_type.to_s
51+
when 'droplets', 'buildpack_cache' then :storage_cli_config_file_droplets
52+
when 'buildpacks' then :storage_cli_config_file_buildpacks
53+
when 'packages' then :storage_cli_config_file_packages
54+
when 'resource_pool' then :storage_cli_config_file_resource_pool
55+
else
56+
raise BlobstoreError.new("Unknown resource_type: #{resource_type}")
57+
end
58+
59+
path = VCAP::CloudController::Config.config.get(key)
60+
raise BlobstoreError.new("storage-cli config file not found or not readable at: #{path.inspect}") unless path && File.file?(path) && File.readable?(path)
61+
62+
path
63+
end
64+
65+
def validate_required_keys!(json, path)
66+
validate_provider!(json, path)
67+
required = %w[
68+
azure_storage_access_key
69+
azure_storage_account_name
70+
container_name
71+
environment
72+
]
73+
missing = required.reject { |k| json.key?(k) && !json[k].to_s.strip.empty? }
74+
return if missing.empty?
75+
76+
raise BlobstoreError.new("Missing required keys in config file #{path}: #{missing.join(', ')} (json: #{json})")
77+
end
78+
79+
def validate_provider!(json, path)
80+
provider = json['provider']
81+
return unless provider.nil? || provider.to_s.strip.empty?
82+
83+
raise BlobstoreError.new("No provider specified in config file: #{path.inspect} json: #{json}")
3184
end
3285
end
3386

34-
def initialize(connection_config:, directory_key:, resource_type:, root_dir:, min_size: nil, max_size: nil)
87+
def initialize(provider:, directory_key:, resource_type:, root_dir:, min_size: nil, max_size: nil)
3588
@cli_path = cli_path
3689
@directory_key = directory_key
3790
@resource_type = resource_type.to_s
3891
@root_dir = root_dir
3992
@min_size = min_size || 0
4093
@max_size = max_size
41-
@fork = connection_config.fetch(:fork, false)
94+
@provider = provider
4295

4396
file_path = case @resource_type
4497
when 'droplets', 'buildpack_cache'
@@ -114,28 +167,16 @@ def cp_to_blobstore(source_path, destination_key)
114167
end
115168

116169
def cp_file_between_keys(source_key, destination_key)
117-
if @fork
118-
run_cli('copy', partitioned_key(source_key), partitioned_key(destination_key))
119-
else
120-
# Azure CLI doesn't support server-side copy yet, so fallback to local copy
121-
Tempfile.create('blob-copy') do |tmp|
122-
download_from_blobstore(source_key, tmp.path)
123-
cp_to_blobstore(tmp.path, destination_key)
124-
end
125-
end
170+
run_cli('copy', partitioned_key(source_key), partitioned_key(destination_key))
126171
end
127172

128173
def delete_all(_=nil)
129174
# page_size is currently not considered. Azure SDK / API has a limit of 5000
130-
pass unless @fork
131-
132175
# Currently, storage-cli does not support bulk deletion.
133176
run_cli('delete-recursive', @root_dir)
134177
end
135178

136179
def delete_all_in_path(path)
137-
pass unless @fork
138-
139180
# Currently, storage-cli does not support bulk deletion.
140181
run_cli('delete-recursive', partitioned_key(path))
141182
end
@@ -149,29 +190,19 @@ def delete_blob(blob)
149190
end
150191

151192
def blob(key)
152-
if @fork
153-
properties = properties(key)
154-
return nil if properties.nil? || properties.empty?
155-
156-
signed_url = sign_url(partitioned_key(key), verb: 'get', expires_in_seconds: 3600)
157-
StorageCliBlob.new(key, properties:, signed_url:)
158-
elsif exists?(key)
159-
# Azure CLI does not support getting blob properties directly, so fallback to local check
160-
signed_url = sign_url(partitioned_key(key), verb: 'get', expires_in_seconds: 3600)
161-
StorageCliBlob.new(key, signed_url:)
162-
end
193+
properties = properties(key)
194+
return nil if properties.nil? || properties.empty?
195+
196+
signed_url = sign_url(partitioned_key(key), verb: 'get', expires_in_seconds: 3600)
197+
StorageCliBlob.new(key, properties:, signed_url:)
163198
end
164199

165200
def files_for(prefix, _ignored_directory_prefixes=[])
166-
return nil unless @fork
167-
168201
files, _status = run_cli('list', prefix)
169202
files.split("\n").map(&:strip).reject(&:empty?).map { |file| StorageCliBlob.new(file) }
170203
end
171204

172205
def ensure_bucket_exists
173-
return unless @fork
174-
175206
run_cli('ensure-bucket-exists')
176207
end
177208

spec/unit/lib/cloud_controller/blobstore/client_provider_spec.rb

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -129,24 +129,37 @@ module Blobstore
129129
context 'when storage-cli is requested' do
130130
let(:blobstore_type) { 'storage-cli' }
131131
let(:directory_key) { 'some-bucket' }
132-
let(:resource_type) { 'some-resource-type' }
132+
let(:resource_type) { 'droplets' }
133133
let(:root_dir) { 'some-root-dir' }
134134
let(:storage_cli_client_mock) { class_double(CloudController::Blobstore::StorageCliClient) }
135+
let(:tmpdir) { Dir.mktmpdir('storage_cli_spec') }
136+
let(:config_path) { File.join(tmpdir, 'storage_cli_config_droplets.json') }
135137

136138
before do
137-
options.merge!(connection_config: {}, minimum_size: 100, maximum_size: 1000)
139+
File.write(config_path, '{"provider": "AzureRM",
140+
"account_name": "some-account-name",
141+
"account_key": "some-access-key",
142+
"container_name": "directory_key",
143+
"environment": "AzureCloud" }')
144+
allow(VCAP::CloudController::Config.config).to receive(:get).with(:storage_cli_config_file_droplets).and_return(config_path)
145+
options.merge!(provider: 'AzureRM', minimum_size: 100, maximum_size: 1000)
138146
end
139147

140148
it 'provides a storage-cli client' do
141149
allow(StorageCliClient).to receive(:build).and_return(storage_cli_client_mock)
142150
ClientProvider.provide(options:, directory_key:, root_dir:, resource_type:)
143-
expect(StorageCliClient).to have_received(:build).with(connection_config: {}, directory_key: directory_key, resource_type: resource_type, root_dir: root_dir,
151+
expect(StorageCliClient).to have_received(:build).with(directory_key: directory_key, resource_type: resource_type, root_dir: root_dir,
144152
min_size: 100, max_size: 1000)
145153
end
146154

147-
it 'raises an error if connection_config is not provided' do
148-
options.delete(:connection_config)
149-
expect { ClientProvider.provide(options:, directory_key:, root_dir:) }.to raise_error(BlobstoreError, 'connection_config for storage-cli is not provided')
155+
it 'raises an error if provider is not provided' do
156+
config_path = VCAP::CloudController::Config.config.get(:storage_cli_config_file_droplets)
157+
File.write(config_path,
158+
'{"provider": "", "account_name": "some-account-name", "account_key": "some-access-key", "container_name": "directory_key", "environment": "AzureCloud" }')
159+
expect { ClientProvider.provide(options:, directory_key:, root_dir:, resource_type:) }.to raise_error(BlobstoreError) { |e|
160+
expect(e.message).to include('No provider specified in config file:')
161+
expect(e.message).to include(File.basename(config_path))
162+
}
150163
end
151164
end
152165
end

spec/unit/lib/cloud_controller/blobstore/storage_cli/storage_cli_client_spec.rb

Lines changed: 81 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,31 +5,65 @@ module CloudController
55
module Blobstore
66
RSpec.describe StorageCliClient do
77
describe 'registry build and lookup' do
8-
it 'builds the correct client' do
8+
it 'builds the correct client when JSON has provider AzureRM' do
99
droplets_cfg = Tempfile.new(['droplets', '.json'])
10-
droplets_cfg.write({ connection_config: { provider: 'AzureRM' } }.to_json)
10+
droplets_cfg.write({ provider: 'AzureRM',
11+
azure_storage_access_key: 'bommelkey',
12+
azure_storage_account_name: 'bommel',
13+
container_name: 'bommelcontainer',
14+
environment: 'BommelCloud' }.to_json)
1115
droplets_cfg.flush
1216

1317
config_double = instance_double(VCAP::CloudController::Config)
1418
allow(VCAP::CloudController::Config).to receive(:config).and_return(config_double)
1519
allow(config_double).to receive(:get).with(:storage_cli_config_file_droplets).and_return(droplets_cfg.path)
1620

17-
client_from_registry = StorageCliClient.build(connection_config: { provider: 'AzureRM' }, directory_key: 'dummy-key', root_dir: 'dummy-root', resource_type: 'droplets')
21+
client_from_registry = StorageCliClient.build(
22+
directory_key: 'dummy-key',
23+
root_dir: 'dummy-root',
24+
resource_type: 'droplets'
25+
)
1826
expect(client_from_registry).to be_a(AzureStorageCliClient)
1927

2028
droplets_cfg.close!
2129
end
2230

2331
it 'raises an error for an unregistered provider' do
32+
droplets_cfg = Tempfile.new(['droplets', '.json'])
33+
droplets_cfg.write(
34+
{ provider: 'UnknownProvider',
35+
azure_storage_access_key: 'bommelkey',
36+
azure_storage_account_name: 'bommel',
37+
container_name: 'bommelcontainer',
38+
environment: 'BommelCloud' }.to_json
39+
)
40+
droplets_cfg.flush
41+
42+
config_double = instance_double(VCAP::CloudController::Config)
43+
allow(VCAP::CloudController::Config).to receive(:config).and_return(config_double)
44+
allow(config_double).to receive(:get).with(:storage_cli_config_file_droplets).and_return(droplets_cfg.path)
45+
2446
expect do
25-
StorageCliClient.build(connection_config: { provider: 'UnknownProvider' }, directory_key: 'dummy-key', root_dir: 'dummy-root', resource_type: 'droplets')
47+
StorageCliClient.build(directory_key: 'dummy-key', root_dir: 'dummy-root', resource_type: 'droplets')
2648
end.to raise_error(RuntimeError, 'No storage CLI client registered for provider UnknownProvider')
49+
50+
droplets_cfg.close!
2751
end
2852

29-
it 'raises an error if provider is missing' do
53+
it 'raises an error when provider is missing from the JSON' do
54+
droplets_cfg = Tempfile.new(['droplets', '.json'])
55+
droplets_cfg.write({}.to_json)
56+
droplets_cfg.flush
57+
58+
config_double = instance_double(VCAP::CloudController::Config)
59+
allow(VCAP::CloudController::Config).to receive(:config).and_return(config_double)
60+
allow(config_double).to receive(:get).with(:storage_cli_config_file_droplets).and_return(droplets_cfg.path)
61+
3062
expect do
31-
StorageCliClient.build(connection_config: {}, directory_key: 'dummy-key', root_dir: 'dummy-root', resource_type: 'droplets')
32-
end.to raise_error(RuntimeError, 'Missing connection_config[:provider]')
63+
StorageCliClient.build(directory_key: 'dummy-key', root_dir: 'dummy-root', resource_type: 'droplets')
64+
end.to raise_error(CloudController::Blobstore::BlobstoreError, /No provider specified in config file/)
65+
66+
droplets_cfg.close!
3367
end
3468
end
3569

@@ -42,9 +76,12 @@ module Blobstore
4276
let(:resource_pool_cfg) { Tempfile.new(['resource_pool', '.json']) }
4377

4478
before do
45-
# Valid JSON (YAML can parse JSON)
4679
[droplets_cfg, buildpacks_cfg, packages_cfg, resource_pool_cfg].each do |f|
47-
f.write({ connection_config: { provider: 'AzureRM' } }.to_json)
80+
f.write({ provider: 'AzureRM',
81+
azure_storage_access_key: 'bommelkey',
82+
azure_storage_account_name: 'bommel',
83+
container_name: 'bommelcontainer',
84+
environment: 'BommelCloud' }.to_json)
4885
f.flush
4986
end
5087

@@ -59,7 +96,6 @@ module Blobstore
5996
end
6097
end
6198

62-
# Quiet logger noise in specs
6399
allow(Steno).to receive(:logger).and_return(double(info: nil, error: nil))
64100
end
65101

@@ -69,7 +105,6 @@ module Blobstore
69105

70106
def build_client(resource_type)
71107
StorageCliClient.build(
72-
connection_config: { provider: 'AzureRM' },
73108
directory_key: 'dir-key',
74109
root_dir: 'root',
75110
resource_type: resource_type
@@ -119,11 +154,39 @@ def build_client(resource_type)
119154
end.to raise_error(CloudController::Blobstore::BlobstoreError, /not found or not readable/)
120155
end
121156

157+
it 'raises when provider is missing from config file' do
158+
File.write(packages_cfg.path, {
159+
azure_storage_access_key: 'bommelkey',
160+
azure_storage_account_name: 'bommel',
161+
container_name: 'bommelcontainer',
162+
environment: 'BommelCloud'
163+
}.to_json)
164+
165+
expect do
166+
build_client('packages')
167+
end.to raise_error(
168+
CloudController::Blobstore::BlobstoreError,
169+
/No provider specified/
170+
)
171+
end
172+
122173
it 'raises when YAML load fails' do
123-
File.write(packages_cfg.path, '{ this is: [not, valid }')
174+
File.write(packages_cfg.path, { provider: 'AzureRM',
175+
azure_storage_access_key: 'bommelkey',
176+
azure_storage_account_name: 'bommel',
177+
container_name: 'bommelcontainer',
178+
environment: 'BommelCloud' }.to_json)
179+
180+
expect(VCAP::CloudController::YAMLConfig).to receive(:safe_load_file).
181+
with(packages_cfg.path).
182+
and_raise(StandardError.new('parse blew up'))
183+
124184
expect do
125185
build_client('packages')
126-
end.to raise_error(CloudController::Blobstore::BlobstoreError, /Failed to load storage-cli config/)
186+
end.to raise_error(
187+
CloudController::Blobstore::BlobstoreError,
188+
/Failed to load storage-cli config/
189+
)
127190
end
128191
end
129192

@@ -132,21 +195,23 @@ def build_client(resource_type)
132195
let(:droplets_cfg) { Tempfile.new(['droplets', '.json']) }
133196

134197
before do
135-
droplets_cfg.write({ connection_config: { provider: 'AzureRM' } }.to_json)
198+
droplets_cfg.write({ provider: 'AzureRM',
199+
azure_storage_access_key: 'bommelkey',
200+
azure_storage_account_name: 'bommel',
201+
container_name: 'bommelcontainer',
202+
environment: 'BommelCloud' }.to_json)
136203
droplets_cfg.flush
137204

138205
allow(VCAP::CloudController::Config).to receive(:config).and_return(config_double)
139206
allow(config_double).to receive(:get).with(:storage_cli_config_file_droplets).and_return(droplets_cfg.path)
140207

141-
# Avoid logger noise
142208
allow(Steno).to receive(:logger).and_return(double(info: nil, error: nil))
143209
end
144210

145211
after { droplets_cfg.close! }
146212

147213
let(:client) do
148214
StorageCliClient.build(
149-
connection_config: { provider: 'AzureRM' },
150215
directory_key: 'dir',
151216
root_dir: 'root',
152217
resource_type: 'droplets'

0 commit comments

Comments
 (0)