debian-mirror-gitlab/lib/gitlab/ci/interpolation/config.rb

125 lines
3.7 KiB
Ruby
Raw Normal View History

2023-04-23 21:23:45 +05:30
# frozen_string_literal: true
module Gitlab
module Ci
module Interpolation
##
# Interpolation::Config represents a configuration artifact that we want to perform interpolation on.
#
class Config
include Gitlab::Utils::StrongMemoize
##
# Total number of hash nodes traversed. For example, loading a YAML below would result in a hash having 12 nodes
# instead of 9, because hash values are being counted before we recursively traverse them.
#
# test:
# spec:
# env: $[[ inputs.env ]]
#
# $[[ inputs.key ]]:
# name: $[[ inputs.key ]]
# script: my-value
#
# According to our benchmarks performed when developing this code, the worst-case scenario of processing
# a hash with 500_000 nodes takes around 1 second and consumes around 225 megabytes of memory.
#
# The typical scenario, using just a few interpolations takes 250ms and consumes around 20 megabytes of memory.
#
# Given the above the 500_000 nodes should be an upper limit, provided that the are additional safeguard
# present in other parts of the code (example: maximum number of interpolation blocks found). Typical size of a
# YAML configuration with 500k nodes might be around 10 megabytes, which is an order of magnitude higher than
# the 1MB limit for loading YAML on GitLab.com
#
MAX_NODES = 500_000
MAX_NODE_SIZE = 1024 * 1024 # 1MB
TooManyNodesError = Class.new(StandardError)
NodeTooLargeError = Class.new(StandardError)
Visitor = Class.new do
def initialize
@visited = 0
end
def visit!
@visited += 1
raise Config::TooManyNodesError if @visited > Config::MAX_NODES
end
end
attr_reader :errors
def initialize(hash)
@config = hash
@errors = []
end
def to_h
@config
end
##
# The replace! method will yield a block and replace a each of the hash config nodes with a return value of the
# block.
#
# It returns `nil` if there were errors found during the process.
#
def replace!(&block)
recursive_replace(@config, Visitor.new, &block)
rescue TooManyNodesError
@errors.push('config too large')
nil
rescue NodeTooLargeError
@errors.push('config node too large')
nil
end
strong_memoize_attr :replace!
def self.fabricate(config)
case config
when Hash
new(config)
when Interpolation::Config
config
else
raise ArgumentError, 'unknown interpolation config'
end
end
private
def recursive_replace(config, visitor, &block)
visitor.visit!
case config
when Hash
{}.tap do |new_hash|
config.each_pair do |key, value|
new_key = recursive_replace(key, visitor, &block)
new_value = recursive_replace(value, visitor, &block)
if new_key != key
new_hash[new_key] = new_value
else
new_hash[key] = new_value
end
end
end
when Array
config.map { |value| recursive_replace(value, visitor, &block) }
when Symbol
recursive_replace(config.to_s, visitor, &block)
when String
raise NodeTooLargeError if config.bytesize > MAX_NODE_SIZE
yield config
else
config
end
end
end
end
end
end