Skip to content

Commit abeed26

Browse files
authored
Read storage-cli config JSON from capi (AzureRM) (#4581)
* Read storage-cli config JSON from capi (AzureRM) Stop building storage-cli JSON inside CC; instead consume files rendered by capi job templates. StorageCliClient now selects the JSON file by `resource_type`.
1 parent b6d5357 commit abeed26

File tree

13 files changed

+406
-108
lines changed

13 files changed

+406
-108
lines changed

config/cloud_controller.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,12 @@ directories:
320320
diagnostics: /tmp
321321

322322
stacks_file: config/stacks.yml
323+
324+
storage_cli_config_file_droplets: config/storage_cli_config_droplets.json
325+
storage_cli_config_file_packages: config/storage_cli_config_packages.json
326+
storage_cli_config_file_buildpacks: config/storage_cli_config_buildpacks.json
327+
storage_cli_config_file_resource_pool: config/storage_cli_config_resource_pool.json
328+
323329
newrelic_enabled: false
324330

325331
max_service_credential_bindings_per_app_service_instance: 1

lib/cloud_controller/blobstore/client_provider.rb

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ def self.provide(options:, directory_key:, root_dir: nil, resource_type: nil)
1616
provide_fog(options, directory_key, root_dir)
1717
elsif options[:blobstore_type] == 'storage-cli'
1818
# storage-cli is an experimental feature and not yet fully implemented. !!! DO NOT USE IN PRODUCTION !!!
19-
provide_storage_cli(options, directory_key, root_dir)
19+
provide_storage_cli(options, directory_key, root_dir, resource_type)
2020
else
2121
provide_webdav(options, directory_key, root_dir)
2222
end
@@ -71,14 +71,14 @@ def provide_webdav(options, directory_key, root_dir)
7171
Client.new(SafeDeleteClient.new(retryable_client, root_dir))
7272
end
7373

74-
def provide_storage_cli(options, directory_key, root_dir)
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-
root_dir: root_dir,
80-
min_size: options[:minimum_size],
81-
max_size: options[:maximum_size])
74+
def provide_storage_cli(options, directory_key, root_dir, resource_type)
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+
)
8282

8383
logger = Steno.logger('cc.blobstore.storage_cli_client')
8484
errors = [StandardError]

lib/cloud_controller/blobstore/storage_cli/azure_storage_cli_client.rb

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,6 @@ def cli_path
55
ENV['AZURE_STORAGE_CLI_PATH'] || '/var/vcap/packages/azure-storage-cli/bin/azure-storage-cli'
66
end
77

8-
def build_config(connection_config)
9-
{
10-
account_name: connection_config[:azure_storage_account_name],
11-
account_key: connection_config[:azure_storage_access_key],
12-
container_name: @directory_key,
13-
environment: connection_config[:environment]
14-
}.compact
15-
end
16-
178
CloudController::Blobstore::StorageCliClient.register('AzureRM', AzureStorageCliClient)
189
end
1910
end

lib/cloud_controller/blobstore/storage_cli/storage_cli_client.rb

Lines changed: 75 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -16,29 +16,88 @@ 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:, 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)
23+
raise 'Missing resource_type' if resource_type.nil?
2524

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

29-
impl_class.new(connection_config:, directory_key:, root_dir:, min_size:, max_size:)
32+
impl_class.new(provider: provider, directory_key: directory_key, root_dir: root_dir, resource_type: resource_type, min_size: min_size, max_size: max_size,
33+
config_path: config_path_for(resource_type))
34+
end
35+
36+
RESOURCE_TYPE_KEYS = {
37+
'droplets' => :storage_cli_config_file_droplets,
38+
'buildpack_cache' => :storage_cli_config_file_droplets,
39+
'buildpacks' => :storage_cli_config_file_buildpacks,
40+
'packages' => :storage_cli_config_file_packages,
41+
'resource_pool' => :storage_cli_config_file_resource_pool
42+
}.freeze
43+
44+
def fetch_config(resource_type)
45+
path = config_path_for(resource_type)
46+
validate_config_path!(path)
47+
48+
json = fetch_json(path)
49+
validate_json_object!(json, path)
50+
validate_required_keys!(json, path)
51+
52+
json
53+
end
54+
55+
def config_path_for(resource_type)
56+
normalized = resource_type.to_s
57+
key = RESOURCE_TYPE_KEYS.fetch(normalized) do
58+
raise BlobstoreError.new("Unknown resource_type: #{resource_type}")
59+
end
60+
VCAP::CloudController::Config.config.get(key)
61+
end
62+
63+
def fetch_json(path)
64+
Oj.load(File.read(path))
65+
rescue Oj::ParseError, EncodingError => e
66+
raise BlobstoreError.new("Failed to parse storage-cli JSON at #{path}: #{e.message}")
67+
end
68+
69+
def validate_config_path!(path)
70+
return if path && File.file?(path) && File.readable?(path)
71+
72+
raise BlobstoreError.new("Storage-cli config file not found or not readable at: #{path.inspect}")
73+
end
74+
75+
def validate_json_object!(json, path)
76+
raise BlobstoreError.new("Config at #{path} must be a JSON object") unless json.is_a?(Hash)
77+
end
78+
79+
def validate_required_keys!(json, path)
80+
provider = json['provider'].to_s.strip
81+
raise BlobstoreError.new("No provider specified in config file: #{path.inspect}") if provider.empty?
82+
83+
required = %w[account_key account_name container_name environment]
84+
missing = required.reject { |k| json.key?(k) && !json[k].to_s.strip.empty? }
85+
return if missing.empty?
86+
87+
raise BlobstoreError.new("Missing required keys in #{path}: #{missing.join(', ')}")
3088
end
3189
end
3290

33-
def initialize(connection_config:, directory_key:, root_dir:, min_size: nil, max_size: nil)
91+
def initialize(provider:, directory_key:, resource_type:, root_dir:, config_path:, min_size: nil, max_size: nil)
3492
@cli_path = cli_path
3593
@directory_key = directory_key
94+
@resource_type = resource_type.to_s
3695
@root_dir = root_dir
3796
@min_size = min_size || 0
3897
@max_size = max_size
39-
config = build_config(connection_config)
40-
@config_file = write_config_file(config)
41-
@fork = connection_config.fetch(:fork, false)
98+
@provider = provider
99+
@config_file = config_path
100+
logger.info('storage_cli_config_selected', resource_type: @resource_type, path: @config_file)
42101
end
43102

44103
def local?
@@ -88,28 +147,16 @@ def cp_to_blobstore(source_path, destination_key)
88147
end
89148

90149
def cp_file_between_keys(source_key, destination_key)
91-
if @fork
92-
run_cli('copy', partitioned_key(source_key), partitioned_key(destination_key))
93-
else
94-
# Azure CLI doesn't support server-side copy yet, so fallback to local copy
95-
Tempfile.create('blob-copy') do |tmp|
96-
download_from_blobstore(source_key, tmp.path)
97-
cp_to_blobstore(tmp.path, destination_key)
98-
end
99-
end
150+
run_cli('copy', partitioned_key(source_key), partitioned_key(destination_key))
100151
end
101152

102153
def delete_all(_=nil)
103154
# page_size is currently not considered. Azure SDK / API has a limit of 5000
104-
pass unless @fork
105-
106155
# Currently, storage-cli does not support bulk deletion.
107156
run_cli('delete-recursive', @root_dir)
108157
end
109158

110159
def delete_all_in_path(path)
111-
pass unless @fork
112-
113160
# Currently, storage-cli does not support bulk deletion.
114161
run_cli('delete-recursive', partitioned_key(path))
115162
end
@@ -123,29 +170,19 @@ def delete_blob(blob)
123170
end
124171

125172
def blob(key)
126-
if @fork
127-
properties = properties(key)
128-
return nil if properties.nil? || properties.empty?
129-
130-
signed_url = sign_url(partitioned_key(key), verb: 'get', expires_in_seconds: 3600)
131-
StorageCliBlob.new(key, properties:, signed_url:)
132-
elsif exists?(key)
133-
# Azure CLI does not support getting blob properties directly, so fallback to local check
134-
signed_url = sign_url(partitioned_key(key), verb: 'get', expires_in_seconds: 3600)
135-
StorageCliBlob.new(key, signed_url:)
136-
end
173+
properties = properties(key)
174+
return nil if properties.nil? || properties.empty?
175+
176+
signed_url = sign_url(partitioned_key(key), verb: 'get', expires_in_seconds: 3600)
177+
StorageCliBlob.new(key, properties:, signed_url:)
137178
end
138179

139180
def files_for(prefix, _ignored_directory_prefixes=[])
140-
return nil unless @fork
141-
142181
files, _status = run_cli('list', prefix)
143182
files.split("\n").map(&:strip).reject(&:empty?).map { |file| StorageCliBlob.new(file) }
144183
end
145184

146185
def ensure_bucket_exists
147-
return unless @fork
148-
149186
run_cli('ensure-bucket-exists')
150187
end
151188

@@ -194,18 +231,6 @@ def build_config(connection_config)
194231
raise NotImplementedError
195232
end
196233

197-
def write_config_file(config)
198-
# TODO: Consider to move the config generation into capi-release
199-
config_dir = File.join(tmpdir, 'blobstore-configs')
200-
FileUtils.mkdir_p(config_dir)
201-
202-
config_file_path = File.join(config_dir, "#{@directory_key}.json")
203-
File.open(config_file_path, 'w', 0o600) do |f|
204-
f.write(Oj.dump(config.transform_keys(&:to_s)))
205-
end
206-
config_file_path
207-
end
208-
209234
def tmpdir
210235
VCAP::CloudController::Config.config.get(:directories, :tmpdir)
211236
rescue StandardError

lib/cloud_controller/config_schemas/api_schema.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,12 @@ class ApiSchema < VCAP::Config
9595
},
9696

9797
stacks_file: String,
98+
99+
optional(:storage_cli_config_file_buildpacks) => String,
100+
optional(:storage_cli_config_file_packages) => String,
101+
optional(:storage_cli_config_file_resource_pool) => String,
102+
optional(:storage_cli_config_file_droplets) => String,
103+
98104
newrelic_enabled: bool,
99105

100106
optional(:max_migration_duration_in_minutes) => Integer,

lib/cloud_controller/config_schemas/clock_schema.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@ class ClockSchema < VCAP::Config
5050

5151
pid_filename: String, # Pid filename to use
5252

53+
optional(:storage_cli_config_file_buildpacks) => String,
54+
optional(:storage_cli_config_file_packages) => String,
55+
optional(:storage_cli_config_file_resource_pool) => String,
56+
optional(:storage_cli_config_file_droplets) => String,
57+
5358
newrelic_enabled: bool,
5459

5560
optional(:max_migration_duration_in_minutes) => Integer,

lib/cloud_controller/config_schemas/worker_schema.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ class WorkerSchema < VCAP::Config
4141
},
4242

4343
stacks_file: String,
44+
45+
optional(:storage_cli_config_file_buildpacks) => String,
46+
optional(:storage_cli_config_file_packages) => String,
47+
optional(:storage_cli_config_file_resource_pool) => String,
48+
optional(:storage_cli_config_file_droplets) => String,
49+
4450
newrelic_enabled: bool,
4551

4652
optional(:max_migration_duration_in_minutes) => Integer,

lib/cloud_controller/dependency_locator.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,8 @@ def legacy_global_app_bits_cache
167167

168168
Blobstore::ClientProvider.provide(
169169
options: options,
170-
directory_key: options.fetch(:resource_directory_key)
170+
directory_key: options.fetch(:resource_directory_key),
171+
resource_type: :resource_pool
171172
)
172173
end
173174

@@ -177,7 +178,8 @@ def global_app_bits_cache
177178
Blobstore::ClientProvider.provide(
178179
options: options,
179180
directory_key: options.fetch(:resource_directory_key),
180-
root_dir: RESOURCE_POOL_DIR
181+
root_dir: RESOURCE_POOL_DIR,
182+
resource_type: :resource_pool
181183
)
182184
end
183185

lib/cloud_controller/resource_pool.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ def initialize(config)
2525
@blobstore = CloudController::Blobstore::ClientProvider.provide(
2626
options: options,
2727
directory_key: options.fetch(:resource_directory_key),
28-
root_dir: CloudController::DependencyLocator::RESOURCE_POOL_DIR
28+
root_dir: CloudController::DependencyLocator::RESOURCE_POOL_DIR,
29+
resource_type: 'resource_pool'
2930
)
3031

3132
@minimum_size = options[:minimum_size] || 0 # TODO: move default into config object?

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

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -129,22 +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) { 'droplets' }
132133
let(:root_dir) { 'some-root-dir' }
133134
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') }
134137

135138
before do
136-
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)
137146
end
138147

139148
it 'provides a storage-cli client' do
140149
allow(StorageCliClient).to receive(:build).and_return(storage_cli_client_mock)
141-
ClientProvider.provide(options:, directory_key:, root_dir:)
142-
expect(StorageCliClient).to have_received(:build).with(connection_config: {}, directory_key: directory_key, root_dir: root_dir, min_size: 100, max_size: 1000)
150+
ClientProvider.provide(options:, directory_key:, root_dir:, resource_type:)
151+
expect(StorageCliClient).to have_received(:build).with(directory_key: directory_key, resource_type: resource_type, root_dir: root_dir,
152+
min_size: 100, max_size: 1000)
143153
end
144154

145-
it 'raises an error if connection_config is not provided' do
146-
options.delete(:connection_config)
147-
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+
}
148163
end
149164
end
150165
end

0 commit comments

Comments
 (0)