diff options
author | Francisco Javier López <fjlopez@gitlab.com> | 2019-04-03 12:50:54 +0300 |
---|---|---|
committer | Kamil Trzciński <ayufan@ayufan.eu> | 2019-04-03 12:50:54 +0300 |
commit | 6ee1d8cf7778ecef0997c10f22b18ab4b61e9b3b (patch) | |
tree | c3c33ae8baff308b7c3334829a804d532658c1b1 /lib | |
parent | a7d3a5e43957185dc6193d1b97c57fc4eb02e9ea (diff) |
Add port section to CI Image object
In order to implement https://gitlab.com/gitlab-org/gitlab-ee/issues/10179
we need several modifications on the CI config file. We are
adding a new ports section in the default Image object.
Each of these ports will accept: number, protocol and name.
By default this new configuration will be only enabled in
the Web IDE config file.
Diffstat (limited to 'lib')
-rw-r--r-- | lib/api/entities.rb | 5 | ||||
-rw-r--r-- | lib/gitlab/ci/build/image.rb | 10 | ||||
-rw-r--r-- | lib/gitlab/ci/build/port.rb | 32 | ||||
-rw-r--r-- | lib/gitlab/ci/config/entry/image.rb | 22 | ||||
-rw-r--r-- | lib/gitlab/ci/config/entry/port.rb | 46 | ||||
-rw-r--r-- | lib/gitlab/ci/config/entry/ports.rb | 46 | ||||
-rw-r--r-- | lib/gitlab/ci/config/entry/service.rb | 4 | ||||
-rw-r--r-- | lib/gitlab/ci/config/entry/services.rb | 2 | ||||
-rw-r--r-- | lib/gitlab/config/entry/configurable.rb | 10 | ||||
-rw-r--r-- | lib/gitlab/config/entry/factory.rb | 2 | ||||
-rw-r--r-- | lib/gitlab/config/entry/node.rb | 20 | ||||
-rw-r--r-- | lib/gitlab/config/entry/simplifiable.rb | 5 | ||||
-rw-r--r-- | lib/gitlab/config/entry/validators.rb | 102 |
13 files changed, 294 insertions, 12 deletions
diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 4533305bfd3..cc62b5a3661 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -1389,8 +1389,13 @@ module API expose :name, :script, :timeout, :when, :allow_failure end + class Port < Grape::Entity + expose :number, :protocol, :name + end + class Image < Grape::Entity expose :name, :entrypoint + expose :ports, using: JobRequest::Port end class Service < Image diff --git a/lib/gitlab/ci/build/image.rb b/lib/gitlab/ci/build/image.rb index 4dd932f61d4..1d7bfba75cd 100644 --- a/lib/gitlab/ci/build/image.rb +++ b/lib/gitlab/ci/build/image.rb @@ -4,7 +4,7 @@ module Gitlab module Ci module Build class Image - attr_reader :alias, :command, :entrypoint, :name + attr_reader :alias, :command, :entrypoint, :name, :ports class << self def from_image(job) @@ -26,17 +26,25 @@ module Gitlab def initialize(image) if image.is_a?(String) @name = image + @ports = [] elsif image.is_a?(Hash) @alias = image[:alias] @command = image[:command] @entrypoint = image[:entrypoint] @name = image[:name] + @ports = build_ports(image).select(&:valid?) end end def valid? @name.present? end + + private + + def build_ports(image) + image[:ports].to_a.map { |port| ::Gitlab::Ci::Build::Port.new(port) } + end end end end diff --git a/lib/gitlab/ci/build/port.rb b/lib/gitlab/ci/build/port.rb new file mode 100644 index 00000000000..6c4656ffea2 --- /dev/null +++ b/lib/gitlab/ci/build/port.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Build + class Port + DEFAULT_PORT_NAME = 'default_port'.freeze + DEFAULT_PORT_PROTOCOL = 'http'.freeze + + attr_reader :number, :protocol, :name + + def initialize(port) + @name = DEFAULT_PORT_NAME + @protocol = DEFAULT_PORT_PROTOCOL + + case port + when Integer + @number = port + when Hash + @number = port[:number] + @protocol = port.fetch(:protocol, @protocol) + @name = port.fetch(:name, @name) + end + end + + def valid? + @number.present? + end + end + end + end +end diff --git a/lib/gitlab/ci/config/entry/image.rb b/lib/gitlab/ci/config/entry/image.rb index a13a0625e90..0beeb44c272 100644 --- a/lib/gitlab/ci/config/entry/image.rb +++ b/lib/gitlab/ci/config/entry/image.rb @@ -9,24 +9,24 @@ module Gitlab # class Image < ::Gitlab::Config::Entry::Node include ::Gitlab::Config::Entry::Validatable + include ::Gitlab::Config::Entry::Attributable + include ::Gitlab::Config::Entry::Configurable - ALLOWED_KEYS = %i[name entrypoint].freeze + ALLOWED_KEYS = %i[name entrypoint ports].freeze validations do validates :config, hash_or_string: true validates :config, allowed_keys: ALLOWED_KEYS + validates :config, disallowed_keys: %i[ports], unless: :with_image_ports? validates :name, type: String, presence: true validates :entrypoint, array_of_strings: true, allow_nil: true end - def hash? - @config.is_a?(Hash) - end + entry :ports, Entry::Ports, + description: 'Ports used expose the image' - def string? - @config.is_a?(String) - end + attributes :ports def name value[:name] @@ -42,6 +42,14 @@ module Gitlab {} end + + def with_image_ports? + opt(:with_image_ports) + end + + def skip_config_hash_validation? + true + end end end end diff --git a/lib/gitlab/ci/config/entry/port.rb b/lib/gitlab/ci/config/entry/port.rb new file mode 100644 index 00000000000..c239b1225c5 --- /dev/null +++ b/lib/gitlab/ci/config/entry/port.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + class Config + module Entry + ## + # Entry that represents a configuration of an Image Port. + # + class Port < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Validatable + + ALLOWED_KEYS = %i[number protocol name].freeze + + validations do + validates :config, hash_or_integer: true + validates :config, allowed_keys: ALLOWED_KEYS + + validates :number, type: Integer, presence: true + validates :protocol, type: String, inclusion: { in: %w[http https], message: 'should be http or https' }, allow_blank: true + validates :name, type: String, presence: false, allow_nil: true + end + + def number + value[:number] + end + + def protocol + value[:protocol] + end + + def name + value[:name] + end + + def value + return { number: @config } if integer? + return @config if hash? + + {} + end + end + end + end + end +end diff --git a/lib/gitlab/ci/config/entry/ports.rb b/lib/gitlab/ci/config/entry/ports.rb new file mode 100644 index 00000000000..01ffcc7dd87 --- /dev/null +++ b/lib/gitlab/ci/config/entry/ports.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + class Config + module Entry + ## + # Entry that represents a configuration of the ports of a Docker service. + # + class Ports < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Validatable + + validations do + validates :config, type: Array + validates :config, port_name_present_and_unique: true + validates :config, port_unique: true + end + + def compose!(deps = nil) + super do + @entries = [] + @config.each do |config| + @entries << ::Gitlab::Config::Entry::Factory.new(Entry::Port) + .value(config || {}) + .with(key: "port", parent: self, description: "port definition.") # rubocop:disable CodeReuse/ActiveRecord + .create! + end + + @entries.each do |entry| + entry.compose!(deps) + end + end + end + + def value + @entries.map(&:value) + end + + def descendants + @entries + end + end + end + end + end +end diff --git a/lib/gitlab/ci/config/entry/service.rb b/lib/gitlab/ci/config/entry/service.rb index 6df67083310..084fa4047a4 100644 --- a/lib/gitlab/ci/config/entry/service.rb +++ b/lib/gitlab/ci/config/entry/service.rb @@ -10,16 +10,18 @@ module Gitlab class Service < Image include ::Gitlab::Config::Entry::Validatable - ALLOWED_KEYS = %i[name entrypoint command alias].freeze + ALLOWED_KEYS = %i[name entrypoint command alias ports].freeze validations do validates :config, hash_or_string: true validates :config, allowed_keys: ALLOWED_KEYS + validates :config, disallowed_keys: %i[ports], unless: :with_image_ports? validates :name, type: String, presence: true validates :entrypoint, array_of_strings: true, allow_nil: true validates :command, array_of_strings: true, allow_nil: true validates :alias, type: String, allow_nil: true + validates :alias, type: String, presence: true, unless: ->(record) { record.ports.blank? } end def alias diff --git a/lib/gitlab/ci/config/entry/services.rb b/lib/gitlab/ci/config/entry/services.rb index 71475f69218..83baa83711f 100644 --- a/lib/gitlab/ci/config/entry/services.rb +++ b/lib/gitlab/ci/config/entry/services.rb @@ -12,6 +12,7 @@ module Gitlab validations do validates :config, type: Array + validates :config, services_with_ports_alias_unique: true, if: ->(record) { record.opt(:with_image_ports) } end def compose!(deps = nil) @@ -20,6 +21,7 @@ module Gitlab @config.each do |config| @entries << ::Gitlab::Config::Entry::Factory.new(Entry::Service) .value(config || {}) + .with(key: "service", parent: self, description: "service definition.") # rubocop:disable CodeReuse/ActiveRecord .create! end diff --git a/lib/gitlab/config/entry/configurable.rb b/lib/gitlab/config/entry/configurable.rb index 37ba16dba25..6667a5d3d33 100644 --- a/lib/gitlab/config/entry/configurable.rb +++ b/lib/gitlab/config/entry/configurable.rb @@ -21,7 +21,7 @@ module Gitlab include Validatable validations do - validates :config, type: Hash + validates :config, type: Hash, unless: :skip_config_hash_validation? end end @@ -30,6 +30,10 @@ module Gitlab return unless valid? self.class.nodes.each do |key, factory| + # If we override the config type validation + # we can end with different config types like String + next unless config.is_a?(Hash) + factory .value(config[key]) .with(key: key, parent: self) @@ -45,6 +49,10 @@ module Gitlab end # rubocop: enable CodeReuse/ActiveRecord + def skip_config_hash_validation? + false + end + class_methods do def nodes Hash[(@nodes || {}).map { |key, factory| [key, factory.dup] }] diff --git a/lib/gitlab/config/entry/factory.rb b/lib/gitlab/config/entry/factory.rb index 79f9ff32514..3c06b1e0d24 100644 --- a/lib/gitlab/config/entry/factory.rb +++ b/lib/gitlab/config/entry/factory.rb @@ -61,7 +61,7 @@ module Gitlab end def fabricate(entry, value = nil) - entry.new(value, @metadata).tap do |node| + entry.new(value, @metadata) do |node| node.key = @attributes[:key] node.parent = @attributes[:parent] node.default = @attributes[:default] diff --git a/lib/gitlab/config/entry/node.rb b/lib/gitlab/config/entry/node.rb index 9999ab4ff95..e014f15fbd8 100644 --- a/lib/gitlab/config/entry/node.rb +++ b/lib/gitlab/config/entry/node.rb @@ -17,6 +17,8 @@ module Gitlab @metadata = metadata @entries = {} + yield(self) if block_given? + self.class.aspects.to_a.each do |aspect| instance_exec(&aspect) end @@ -44,6 +46,12 @@ module Gitlab @parent ? @parent.ancestors + [@parent] : [] end + def opt(key) + opt = metadata[key] + opt = @parent.opt(key) if opt.nil? && @parent + opt + end + def valid? errors.none? end @@ -85,6 +93,18 @@ module Gitlab "#<#{self.class.name} #{unspecified}{#{key}: #{val.inspect}}>" end + def hash? + @config.is_a?(Hash) + end + + def string? + @config.is_a?(String) + end + + def integer? + @config.is_a?(Integer) + end + def self.default(**) end diff --git a/lib/gitlab/config/entry/simplifiable.rb b/lib/gitlab/config/entry/simplifiable.rb index 5fbf7565e2a..a56a89adb35 100644 --- a/lib/gitlab/config/entry/simplifiable.rb +++ b/lib/gitlab/config/entry/simplifiable.rb @@ -19,7 +19,10 @@ module Gitlab entry = self.class.entry_class(strategy) - super(@subject = entry.new(config, metadata)) + @subject = entry.new(config, metadata) + + yield(@subject) if block_given? + super(@subject) end def self.strategy(name, **opts) diff --git a/lib/gitlab/config/entry/validators.rb b/lib/gitlab/config/entry/validators.rb index d348e11b753..d0ee94370ba 100644 --- a/lib/gitlab/config/entry/validators.rb +++ b/lib/gitlab/config/entry/validators.rb @@ -15,6 +15,17 @@ module Gitlab end end + class DisallowedKeysValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + present_keys = value.try(:keys).to_a & options[:in] + + if present_keys.any? + record.errors.add(attribute, "contains disallowed keys: " + + present_keys.join(', ')) + end + end + end + class AllowedValuesValidator < ActiveModel::EachValidator def validate_each(record, attribute, value) unless options[:in].include?(value.to_s) @@ -186,6 +197,97 @@ module Gitlab end end end + + class PortNamePresentAndUniqueValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + return unless value.is_a?(Array) + + ports_size = value.count + return if ports_size <= 1 + + named_ports = value.select { |e| e.is_a?(Hash) }.map { |e| e[:name] }.compact.map(&:downcase) + + if ports_size != named_ports.size + record.errors.add(attribute, 'when there is more than one port, a unique name should be added') + end + + if ports_size != named_ports.uniq.size + record.errors.add(attribute, 'each port name must be different') + end + end + end + + class PortUniqueValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + value = ports(value) + return unless value.is_a?(Array) + + ports_size = value.count + return if ports_size <= 1 + + if transform_ports(value).size != ports_size + record.errors.add(attribute, 'each port number can only be referenced once') + end + end + + private + + def ports(current_data) + current_data + end + + def transform_ports(raw_ports) + raw_ports.map do |port| + case port + when Integer + port + when Hash + port[:number] + end + end.uniq + end + end + + class JobPortUniqueValidator < PortUniqueValidator + private + + def ports(current_data) + return unless current_data.is_a?(Hash) + + (image_ports(current_data) + services_ports(current_data)).compact + end + + def image_ports(current_data) + return [] unless current_data[:image].is_a?(Hash) + + current_data.dig(:image, :ports).to_a + end + + def services_ports(current_data) + current_data.dig(:services).to_a.flat_map { |service| service.is_a?(Hash) ? service[:ports] : nil } + end + end + + class ServicesWithPortsAliasUniqueValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + current_aliases = aliases(value) + return if current_aliases.empty? + + unless aliases_unique?(current_aliases) + record.errors.add(:config, 'alias must be unique in services with ports') + end + end + + private + + def aliases(value) + value.select { |s| s.is_a?(Hash) && s[:ports] }.pluck(:alias) # rubocop:disable CodeReuse/ActiveRecord + end + + def aliases_unique?(aliases) + aliases.size == aliases.uniq.size + end + end end end end |