Skip to content

Commit eac345b

Browse files
[RFC0030 - 4] File-based service bindings (#4026)
* Adapt system env presenter When the app feature 'file-based service bindings' is enabled, SERVICE_BINDING_ROOT is returned instead of VCAP_SERVICES. For an app using file-based service bindings the '/env' endpoint (i.e. GET /v3/apps/:guid/env) returns the following: { ... "system_env_json": { "SERVICE_BINDING_ROOT": "/etc/cf-service-bindings" }, ... } The file runtime_environment/system_env_presenter_spec.rb has been deleted because all the tests it contained are also present in system_environment/system_env_presenter_spec.rb. * Add builder for service binding files - validate binding names and (credential) keys - check for duplicate binding names - check the total bytesize, maximum allowed size is 1MB - files are added in the following order: 1. credential keys 2. VCAP_SERVICES attributes 3. 'type' and 'provider' - in case a credential key equals a VCAP_SERVICES attributes or 'type' or 'provider', it will be overwritten - for VCAP_SERVICES attribute names, underscores are replaced by hyphens - file content is serialized as JSON (non-string objects) * Use ServiceBindingFilesBuilder in recipe builders - AppRecipeBuilder -> Diego::Bbs::Models::DesiredLRP - TaskRecipeBuilder -> Diego::Bbs::Models::TaskDefinition * Rename 'file-based-service-bindings' to 'service-binding-k8s' * Clarify binding naming convention in error message * Add 'file-based-vcap-services' app feature functionality --------- Co-authored-by: Sven Krieger <[email protected]>
1 parent c95c705 commit eac345b

File tree

17 files changed

+650
-116
lines changed

17 files changed

+650
-116
lines changed

app/controllers/runtime/apps_controller.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ def read_env(guid)
7272
staging_env_json: EnvironmentVariableGroup.staging.environment_json,
7373
running_env_json: EnvironmentVariableGroup.running.environment_json,
7474
environment_json: process.app.environment_variables,
75-
system_env_json: SystemEnvPresenter.new(process.service_bindings).system_env,
75+
system_env_json: SystemEnvPresenter.new(process).system_env,
7676
application_env_json: { 'VCAP_APPLICATION' => vcap_application }
7777
}, mode: :compat)
7878
]

app/models/runtime/process_model.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,9 @@ def revisions_enabled?
177177
app.revisions_enabled
178178
end
179179

180+
delegate :service_binding_k8s_enabled, to: :app
181+
delegate :file_based_vcap_services_enabled, to: :app
182+
180183
def package_hash
181184
# this caches latest_package for performance reasons
182185
package = latest_package

app/presenters/system_environment/system_env_presenter.rb

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,20 @@
22
require 'presenters/system_environment/service_binding_presenter'
33

44
class SystemEnvPresenter
5-
def initialize(service_bindings)
6-
@service_bindings = service_bindings
5+
def initialize(app_or_process)
6+
@service_binding_k8s_enabled = app_or_process.service_binding_k8s_enabled
7+
@file_based_vcap_services_enabled = app_or_process.file_based_vcap_services_enabled
8+
@service_bindings = app_or_process.service_bindings
79
end
810

911
def system_env
12+
return { SERVICE_BINDING_ROOT: '/etc/cf-service-bindings' } if @service_binding_k8s_enabled
13+
return { VCAP_SERVICES_FILE_PATH: '/etc/cf-service-bindings/vcap_services' } if @file_based_vcap_services_enabled
14+
15+
vcap_services
16+
end
17+
18+
def vcap_services
1019
{ VCAP_SERVICES: service_binding_env_variables }
1120
end
1221

app/presenters/v3/app_env_presenter.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def to_hash
2525
environment_variables: app.environment_variables,
2626
staging_env_json: EnvironmentVariableGroup.staging.environment_json,
2727
running_env_json: EnvironmentVariableGroup.running.environment_json,
28-
system_env_json: redact_hash(SystemEnvPresenter.new(app.service_bindings).system_env),
28+
system_env_json: redact_hash(SystemEnvPresenter.new(app).system_env),
2929
application_env_json: vcap_application
3030
}
3131
end

lib/cloud_controller/backends/staging_environment_builder.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def build(app, space, lifecycle, memory_limit, staging_disk_in_mb, vars_from_mes
2727
'MEMORY_LIMIT' => "#{memory_limit}m"
2828
}
2929
).
30-
merge(SystemEnvPresenter.new(app.service_bindings).system_env.stringify_keys)
30+
merge(SystemEnvPresenter.new(app).system_env.stringify_keys)
3131
end
3232
end
3333
end

lib/cloud_controller/diego/app_recipe_builder.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
require 'cloud_controller/diego/cnb/desired_lrp_builder'
77
require 'cloud_controller/diego/process_guid'
88
require 'cloud_controller/diego/ssh_key'
9+
require 'cloud_controller/diego/service_binding_files_builder'
910
require 'credhub/config_helpers'
1011
require 'models/helpers/health_check_types'
1112
require 'cloud_controller/diego/main_lrp_action_builder'
@@ -100,7 +101,8 @@ def app_lrp_arguments
100101
organizational_unit: ["organization:#{process.organization.guid}", "space:#{process.space.guid}", "app:#{process.app_guid}"]
101102
),
102103
image_username: process.desired_droplet.docker_receipt_username,
103-
image_password: process.desired_droplet.docker_receipt_password
104+
image_password: process.desired_droplet.docker_receipt_password,
105+
volume_mounted_files: ServiceBindingFilesBuilder.build(process)
104106
}.compact
105107
end
106108

lib/cloud_controller/diego/environment.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def common_json_and_merge(&blk)
4141
@initial_env.
4242
merge(process.environment_json || {}).
4343
merge(blk.call).
44-
merge(SystemEnvPresenter.new(process.service_bindings).system_env)
44+
merge(SystemEnvPresenter.new(process).system_env)
4545

4646
diego_env = diego_env.merge(DATABASE_URL: process.database_uri) if process.database_uri
4747

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
module VCAP::CloudController
2+
module Diego
3+
class ServiceBindingFilesBuilder
4+
class IncompatibleBindings < StandardError; end
5+
6+
MAX_ALLOWED_BYTESIZE = 1_000_000
7+
8+
def self.build(app_or_process)
9+
new(app_or_process).build
10+
end
11+
12+
def initialize(app_or_process)
13+
@app_or_process = app_or_process
14+
@service_binding_k8s_enabled = app_or_process.service_binding_k8s_enabled
15+
@file_based_vcap_services = app_or_process.file_based_vcap_services_enabled
16+
@service_bindings = app_or_process.service_bindings
17+
end
18+
19+
def build
20+
if @service_binding_k8s_enabled
21+
build_service_binding_k8s
22+
elsif @file_based_vcap_services
23+
vcap_services = SystemEnvPresenter.new(@app_or_process).vcap_services[:VCAP_SERVICES]
24+
build_vcap_service_file(vcap_services)
25+
end
26+
end
27+
28+
private
29+
30+
def build_service_binding_k8s
31+
return nil unless @service_binding_k8s_enabled
32+
33+
service_binding_files = {}
34+
names = Set.new # to check for duplicate binding names
35+
total_bytesize = 0 # to check the total bytesize
36+
37+
@service_bindings.select(&:create_succeeded?).each do |service_binding|
38+
sb_hash = ServiceBindingPresenter.new(service_binding, include_instance: true).to_hash
39+
name = sb_hash[:name]
40+
raise IncompatibleBindings.new("Invalid binding name: '#{name}'. Name must match #{binding_naming_convention.inspect}") unless valid_name?(name)
41+
raise IncompatibleBindings.new("Duplicate binding name: #{name}") if names.add?(name).nil?
42+
43+
# add the credentials first
44+
sb_hash.delete(:credentials)&.each { |k, v| total_bytesize += add_file(service_binding_files, name, k.to_s, v) }
45+
46+
# add the rest of the hash; already existing credential keys are overwritten
47+
# VCAP_SERVICES attribute names are transformed (e.g. binding_guid -> binding-guid)
48+
sb_hash.each { |k, v| total_bytesize += add_file(service_binding_files, name, transform_vcap_services_attribute(k.to_s), v) }
49+
50+
# add the type and provider
51+
label = sb_hash[:label]
52+
total_bytesize += add_file(service_binding_files, name, 'type', label)
53+
total_bytesize += add_file(service_binding_files, name, 'provider', label)
54+
end
55+
56+
raise IncompatibleBindings.new("Bindings exceed the maximum allowed bytesize of #{MAX_ALLOWED_BYTESIZE}: #{total_bytesize}") if total_bytesize > MAX_ALLOWED_BYTESIZE
57+
58+
service_binding_files.values
59+
end
60+
61+
def build_vcap_service_file(vcap_services)
62+
path = 'vcap_services'
63+
vcap_services_string = Oj.dump(vcap_services, mode: :compat)
64+
total_bytesize = vcap_services_string.bytesize + path.bytesize
65+
66+
raise IncompatibleBindings.new("Bindings exceed the maximum allowed bytesize of #{MAX_ALLOWED_BYTESIZE}: #{total_bytesize}") if total_bytesize > MAX_ALLOWED_BYTESIZE
67+
68+
[::Diego::Bbs::Models::File.new(path: path, content: vcap_services_string)]
69+
end
70+
71+
def binding_naming_convention
72+
/^[a-z0-9\-.]{1,253}$/
73+
end
74+
75+
# - adds a Diego::Bbs::Models::File object to the service_binding_files hash
76+
# - binding name is used as the directory name, key is used as the file name
77+
# - returns the bytesize of the path and content
78+
# - skips (and returns 0) if the value is nil or an empty array or hash
79+
# - serializes the value to JSON if it is a non-string object
80+
def add_file(service_binding_files, name, key, value)
81+
raise IncompatibleBindings.new("Invalid file name: #{key}") unless valid_name?(key)
82+
83+
path = "#{name}/#{key}"
84+
content = if value.nil?
85+
return 0
86+
elsif value.is_a?(String)
87+
value
88+
else
89+
return 0 if (value.is_a?(Array) || value.is_a?(Hash)) && value.empty?
90+
91+
Oj.dump(value, mode: :compat)
92+
end
93+
94+
service_binding_files[path] = ::Diego::Bbs::Models::File.new(path:, content:)
95+
path.bytesize + content.bytesize
96+
end
97+
98+
def valid_name?(name)
99+
name.match?(binding_naming_convention)
100+
end
101+
102+
def transform_vcap_services_attribute(name)
103+
if %w[binding_guid binding_name instance_guid instance_name syslog_drain_url volume_mounts].include?(name)
104+
name.tr('_', '-')
105+
else
106+
name
107+
end
108+
end
109+
end
110+
end
111+
end

lib/cloud_controller/diego/task_environment.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def build
1919
initial_envs.
2020
merge(app_env).
2121
merge('VCAP_APPLICATION' => vcap_application, 'MEMORY_LIMIT' => "#{task.memory_in_mb}m").
22-
merge(SystemEnvPresenter.new(app.service_bindings).system_env.stringify_keys)
22+
merge(SystemEnvPresenter.new(app).system_env.stringify_keys)
2323

2424
task_env = task_env.merge('VCAP_PLATFORM_OPTIONS' => credhub_url) if credhub_url.present? && cred_interpolation_enabled?
2525

lib/cloud_controller/diego/task_recipe_builder.rb

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
require 'cloud_controller/diego/bbs_environment_builder'
66
require 'cloud_controller/diego/task_completion_callback_generator'
77
require 'cloud_controller/diego/task_cpu_weight_calculator'
8+
require 'cloud_controller/diego/service_binding_files_builder'
89

910
module VCAP::CloudController
1011
module Diego
@@ -52,7 +53,8 @@ def build_app_task(config, task)
5253
]
5354
),
5455
image_username: task.droplet.docker_receipt_username,
55-
image_password: task.droplet.docker_receipt_password
56+
image_password: task.droplet.docker_receipt_password,
57+
volume_mounted_files: ServiceBindingFilesBuilder.build(task.app)
5658
}.compact)
5759
end
5860

@@ -90,7 +92,8 @@ def build_staging_task(config, staging_details)
9092
]
9193
),
9294
image_username: staging_details.package.docker_username,
93-
image_password: staging_details.package.docker_password
95+
image_password: staging_details.package.docker_password,
96+
volume_mounted_files: ServiceBindingFilesBuilder.build(staging_details.package.app)
9497
}.compact)
9598
end
9699

0 commit comments

Comments
 (0)