Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app/controllers/v3/space_manifests_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,9 @@ def incompatible_with_buildpacks(lifecycle_type, manifest)
end

def incompatible_with_docker(lifecycle_type, manifest)
# Allow docker + buildpack when lifecycle is explicitly set to buildpack (custom stack usage)
return false if lifecycle_type == 'buildpack' && manifest.requested?(:lifecycle) && manifest.lifecycle == 'buildpack'

lifecycle_type == 'buildpack' && manifest.docker
end
end
25 changes: 24 additions & 1 deletion app/fetchers/buildpack_lifecycle_fetcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,16 @@ module VCAP::CloudController
class BuildpackLifecycleFetcher
class << self
def fetch(buildpack_names, stack_name, lifecycle=Config.config.get(:default_app_lifecycle))
# Try to find the stack in the database first
stack = Stack.find(name: stack_name) if stack_name.is_a?(String)

# If not found and it looks like a custom stack URL, use it as-is (normalized)
if stack.nil? && stack_name.is_a?(String) && is_custom_stack?(stack_name)
stack = normalize_stack_url(stack_name)
end

{
stack: Stack.find(name: stack_name),
stack: stack,
buildpack_infos: ordered_buildpacks(buildpack_names, stack_name, lifecycle)
}
end
Expand All @@ -20,6 +28,21 @@ def ordered_buildpacks(buildpack_names, stack_name, lifecycle)
BuildpackInfo.new(buildpack_name, buildpack_record)
end
end

def is_custom_stack?(stack_name)
# Check for various container registry URL formats
return true if stack_name.include?('docker://')
return true if stack_name.match?(%r{^https?://}) # Any https/http URL
return true if stack_name.include?('.') # Any string with a dot (likely a registry)
false
end

def normalize_stack_url(stack_url)
return stack_url if stack_url.start_with?('docker://')
return stack_url.sub(/^https?:\/\//, 'docker://') if stack_url.match?(%r{^https?://})
return "docker://#{stack_url}" if stack_url.match?(%r{^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/.+})
stack_url
end
end
end
end
13 changes: 12 additions & 1 deletion app/messages/app_manifest_message.rb
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,17 @@ def app_lifecycle_hash
Lifecycles::DOCKER
end

# Use docker image as custom stack when lifecycle is explicitly buildpack
stack_value = if requested?(:lifecycle) && @lifecycle == 'buildpack' && requested?(:docker) && docker
docker_image = docker[:image] || docker['image']
docker_image ? "docker://#{docker_image}" : @stack
else
@stack
end

data = {
buildpacks: requested_buildpacks,
stack: @stack,
stack: stack_value,
credentials: @cnb_credentials
}.compact

Expand Down Expand Up @@ -475,6 +483,9 @@ def validate_cnb_enabled!
def validate_docker_buildpacks_combination!
return unless requested?(:docker) && (requested?(:buildpack) || requested?(:buildpacks))

# Allow docker + buildpacks when lifecycle is explicitly set to buildpack (custom stack usage)
return if requested?(:lifecycle) && @lifecycle == 'buildpack'

errors.add(:base, 'Cannot specify both buildpack(s) and docker keys')
end

Expand Down
42 changes: 41 additions & 1 deletion app/messages/buildpack_lifecycle_data_message.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
require 'uri'

module VCAP::CloudController
class BuildpackLifecycleDataMessage < BaseMessage
register_allowed_keys %i[buildpacks stack credentials]
Expand All @@ -19,6 +21,7 @@ class BuildpackLifecycleDataMessage < BaseMessage

validate :buildpacks_content
validate :credentials_content
validate :custom_stack_requires_custom_buildpack

def buildpacks_content
return unless buildpacks.is_a?(Array)
Expand All @@ -40,7 +43,44 @@ def buildpacks_content
def credentials_content
return unless credentials.is_a?(Hash)

errors.add(:credentials, 'credentials value must be a hash') if credentials.any? { |_, v| !v.is_a?(Hash) }
credentials.each do |registry, creds|
unless creds.is_a?(Hash)
errors.add(:credentials, "for registry '#{registry}' must be a hash")
next
end

has_username = creds.key?('username') || creds.key?(:username)
has_password = creds.key?('password') || creds.key?(:password)
errors.add(:base, "credentials for #{registry} must include 'username' and 'password'") unless has_username && has_password
end
end

def custom_stack_requires_custom_buildpack
return unless stack.is_a?(String) && is_custom_stack?(stack)
return unless FeatureFlag.enabled?(:diego_custom_stacks)
return unless buildpacks.is_a?(Array)

buildpacks.each do |buildpack_name|
# If buildpack is a URL, it's custom
next if buildpack_name&.match?(URI::DEFAULT_PARSER.make_regexp)

# Check if it's a system buildpack
system_buildpack = Buildpack.find(name: buildpack_name)
if system_buildpack
errors.add(:base, 'Buildpack must be a custom buildpack when using a custom stack')
break
end
end
end

private

def is_custom_stack?(stack_name)
# Check for various container registry URL formats
return true if stack_name.include?('docker://')
return true if stack_name.match?(%r{^https?://}) # Any https/http URL
return true if stack_name.include?('.') # Any string with a dot (likely a registry)
false
end
end
end
2 changes: 1 addition & 1 deletion app/messages/validators.rb
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ def validate(record)
lifecycle_data_message = lifecycle_data_message_class.new(record.lifecycle_data)
return if lifecycle_data_message.valid?

lifecycle_data_message.errors.full_messages.each do |message|
lifecycle_data_message.errors.each do |attribute, message|
record.errors.add(:lifecycle, message:)
end
end
Expand Down
5 changes: 1 addition & 4 deletions app/models/runtime/cnb_lifecycle_data_model.rb
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,10 @@ def using_custom_buildpack?
end

def to_hash
hash = {
{
buildpacks: buildpacks.map { |buildpack| CloudController::UrlSecretObfuscator.obfuscate(buildpack) },
stack: stack
}
hash[:credentials] = Presenters::Censorship::REDACTED_CREDENTIAL unless credentials.nil?

hash
end

def validate
Expand Down
1 change: 1 addition & 0 deletions app/models/runtime/feature_flag.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class UndefinedFeatureFlagError < StandardError
service_instance_creation: true,
diego_docker: false,
diego_cnb: false,
diego_custom_stacks: false,
set_roles_by_username: true,
unset_roles_by_username: true,
task_creation: true,
Expand Down
43 changes: 43 additions & 0 deletions lib/cloud_controller/diego/buildpack/desired_lrp_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ def cached_dependencies

lifecycle_bundle_key = :"buildpack/#{@stack}"
lifecycle_bundle = @config.get(:diego, :lifecycle_bundles)[lifecycle_bundle_key]

# If custom stack doesn't have a bundle, use the default stack's bundle
if !lifecycle_bundle && @stack.is_a?(String) && is_custom_stack?(@stack)
default_stack = Stack.default.name
lifecycle_bundle_key = :"buildpack/#{default_stack}"
lifecycle_bundle = @config.get(:diego, :lifecycle_bundles)[lifecycle_bundle_key]
end

raise InvalidStack.new("no compiler defined for requested stack '#{@stack}'") unless lifecycle_bundle

[
Expand All @@ -38,6 +46,11 @@ def cached_dependencies
end

def root_fs
# Handle custom stacks (docker:// URLs)
if @stack.is_a?(String) && is_custom_stack?(@stack)
return normalize_stack_url(@stack)
end

@stack_obj ||= Stack.find(name: @stack)
raise CloudController::Errors::ApiError.new_from_details('StackNotFound', @stack) unless @stack_obj

Expand Down Expand Up @@ -65,9 +78,22 @@ def image_layers

lifecycle_bundle_key = :"buildpack/#{@stack}"
lifecycle_bundle = @config.get(:diego, :lifecycle_bundles)[lifecycle_bundle_key]

# If custom stack doesn't have a bundle, use the default stack's bundle
if !lifecycle_bundle && @stack.is_a?(String) && is_custom_stack?(@stack)
default_stack = Stack.default.name
lifecycle_bundle_key = :"buildpack/#{default_stack}"
lifecycle_bundle = @config.get(:diego, :lifecycle_bundles)[lifecycle_bundle_key]
end

raise InvalidStack.new("no compiler defined for requested stack '#{@stack}'") unless lifecycle_bundle

destination = @config.get(:diego, :droplet_destinations)[@stack.to_sym]
# For custom stacks, use a default destination if not configured
if !destination && @stack.is_a?(String) && is_custom_stack?(@stack)
default_stack = Stack.default.name
destination = @config.get(:diego, :droplet_destinations)[default_stack.to_sym]
end
raise InvalidStack.new("no droplet destination defined for requested stack '#{@stack}'") unless destination

layers = [
Expand Down Expand Up @@ -116,6 +142,23 @@ def port_environment_variables
def privileged?
@config.get(:diego, :use_privileged_containers_for_running)
end

private

def is_custom_stack?(stack_name)
# Check for various container registry URL formats
return true if stack_name.include?('docker://')
return true if stack_name.match?(%r{^https?://}) # Any https/http URL
return true if stack_name.include?('.') # Any string with a dot (likely a registry)
false
end

def normalize_stack_url(stack_url)
return stack_url if stack_url.start_with?('docker://')
return stack_url.sub(/^https?:\/\//, 'docker://') if stack_url.match?(%r{^https?://})
return "docker://#{stack_url}" if stack_url.include?('.')
stack_url
end
end
end
end
Expand Down
20 changes: 20 additions & 0 deletions lib/cloud_controller/diego/buildpack/task_action_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,11 @@ def task_environment_variables
end

def stack
# Handle custom stacks (docker:// URLs)
if lifecycle_stack.is_a?(String) && is_custom_stack?(lifecycle_stack)
return normalize_stack_url(lifecycle_stack)
end

@stack ||= Stack.find(name: lifecycle_stack)
raise CloudController::Errors::ApiError.new_from_details('StackNotFound', lifecycle_stack) unless @stack

Expand Down Expand Up @@ -117,6 +122,21 @@ def lifecycle_bundle_key
def lifecycle_stack
lifecycle_data[:stack]
end

def is_custom_stack?(stack_name)
# Check for various container registry URL formats
return true if stack_name.include?('docker://')
return true if stack_name.match?(%r{^https?://}) # Any https/http URL
return true if stack_name.include?('.') # Any string with a dot (likely a registry)
false
end

def normalize_stack_url(stack_url)
return stack_url if stack_url.start_with?('docker://')
return stack_url.sub(%r{^https?://}, 'docker://') if stack_url.match?(%r{^https?://})
return "docker://#{stack_url}" if stack_url.include?('.')
stack_url
end
end
end
end
Expand Down
43 changes: 43 additions & 0 deletions lib/cloud_controller/diego/cnb/desired_lrp_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ def cached_dependencies

lifecycle_bundle_key = :"cnb/#{@stack}"
lifecycle_bundle = @config.get(:diego, :lifecycle_bundles)[lifecycle_bundle_key]

# If custom stack doesn't have a bundle, use the default stack's bundle
if !lifecycle_bundle && @stack.is_a?(String) && is_custom_stack?(@stack)
default_stack = Stack.default.name
lifecycle_bundle_key = :"cnb/#{default_stack}"
lifecycle_bundle = @config.get(:diego, :lifecycle_bundles)[lifecycle_bundle_key]
end

raise InvalidStack.new("no compiler defined for requested stack '#{@stack}'") unless lifecycle_bundle

[
Expand All @@ -38,6 +46,11 @@ def cached_dependencies
end

def root_fs
# Handle custom stacks (docker:// URLs)
if @stack.is_a?(String) && is_custom_stack?(@stack)
return normalize_stack_url(@stack)
end

@stack_obj ||= Stack.find(name: @stack)
raise CloudController::Errors::ApiError.new_from_details('StackNotFound', @stack) unless @stack_obj

Expand Down Expand Up @@ -65,9 +78,22 @@ def image_layers

lifecycle_bundle_key = :"cnb/#{@stack}"
lifecycle_bundle = @config.get(:diego, :lifecycle_bundles)[lifecycle_bundle_key]

# If custom stack doesn't have a bundle, use the default stack's bundle
if !lifecycle_bundle && @stack.is_a?(String) && is_custom_stack?(@stack)
default_stack = Stack.default.name
lifecycle_bundle_key = :"cnb/#{default_stack}"
lifecycle_bundle = @config.get(:diego, :lifecycle_bundles)[lifecycle_bundle_key]
end

raise InvalidStack.new("no compiler defined for requested stack '#{@stack}'") unless lifecycle_bundle

destination = @config.get(:diego, :droplet_destinations)[@stack.to_sym]
# For custom stacks, use a default destination if not configured
if !destination && @stack.is_a?(String) && is_custom_stack?(@stack)
default_stack = Stack.default.name
destination = @config.get(:diego, :droplet_destinations)[default_stack.to_sym]
end
raise InvalidStack.new("no droplet destination defined for requested stack '#{@stack}'") unless destination

layers = [
Expand Down Expand Up @@ -124,6 +150,23 @@ def default_container_env
::Diego::Bbs::Models::EnvironmentVariable.new(name: 'CNB_APP_DIR', value: '/home/vcap/workspace')
]
end

private

def is_custom_stack?(stack_name)
# Check for various container registry URL formats
return true if stack_name.include?('docker://')
return true if stack_name.match?(%r{^https?://}) # Any https/http URL
return true if stack_name.include?('.') # Any string with a dot (likely a registry)
false
end

def normalize_stack_url(stack_url)
return stack_url if stack_url.start_with?('docker://')
return stack_url.sub(/^https?:\/\//, 'docker://') if stack_url.match?(%r{^https?://})
return "docker://#{stack_url}" if stack_url.include?('.')
stack_url
end
end
end
end
Expand Down
31 changes: 29 additions & 2 deletions lib/cloud_controller/diego/lifecycle_protocol.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ def lifecycle_data(staging_details)
lifecycle_data.app_bits_download_uri = @blobstore_url_generator.package_download_url(staging_details.package)
lifecycle_data.app_bits_checksum = staging_details.package.checksum_info
lifecycle_data.buildpack_cache_checksum = staging_details.package.app.buildpack_cache_sha256_checksum
lifecycle_data.build_artifacts_cache_download_uri = @blobstore_url_generator.buildpack_cache_download_url(staging_details.package.app_guid, stack)
lifecycle_data.build_artifacts_cache_upload_uri = @blobstore_url_generator.buildpack_cache_upload_url(staging_details.package.app_guid, stack)
lifecycle_data.build_artifacts_cache_download_uri = @blobstore_url_generator.buildpack_cache_download_url(staging_details.package.app_guid, normalize_stack_for_cache_key(stack))
lifecycle_data.build_artifacts_cache_upload_uri = @blobstore_url_generator.buildpack_cache_upload_url(staging_details.package.app_guid, normalize_stack_for_cache_key(stack))
lifecycle_data.droplet_upload_uri = @blobstore_url_generator.droplet_upload_url(staging_details.staging_guid)
lifecycle_data.buildpacks = @buildpack_entry_generator.buildpack_entries(staging_details.lifecycle.buildpack_infos, stack)
lifecycle_data.stack = stack
Expand Down Expand Up @@ -90,6 +90,33 @@ def container_env_vars_for_process(process)
additional_env + WindowsEnvironmentSage.ponder(process.app)
end

def normalize_stack_for_cache_key(stack_name)
return stack_name unless stack_name.is_a?(String) && is_custom_stack?(stack_name)

# Extract the image name from the Docker URL for cache key compatibility
# Examples:
# https://docker.io/cloudfoundry/cflinuxfs4 -> cflinuxfs4
# docker://cloudfoundry/cflinuxfs3 -> cflinuxfs3
# docker.io/cloudfoundry/cflinuxfs4 -> cflinuxfs4
normalized_url = stack_name.gsub(%r{^(https?://|docker://)}, '')
if normalized_url.include?('/')
# Extract the last part of the path
parts = normalized_url.split('/')
parts.last
else
# If no path, use as-is
normalized_url
end
end

def is_custom_stack?(stack_name)
# Check for various container registry URL formats
return true if stack_name.include?('docker://')
return true if stack_name.match?(%r{^https?://}) # Any https/http URL
return true if stack_name.include?('.') # Any string with a dot (likely a registry)
false
end

def logger
@logger ||= Steno.logger('cc.diego.tr')
end
Expand Down
Loading
Loading