Commit 6ee1d8cf authored by Francisco Javier López's avatar Francisco Javier López Committed by Kamil Trzciński

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.
parent a7d3a5e4
...@@ -1389,8 +1389,13 @@ module API ...@@ -1389,8 +1389,13 @@ module API
expose :name, :script, :timeout, :when, :allow_failure expose :name, :script, :timeout, :when, :allow_failure
end end
class Port < Grape::Entity
expose :number, :protocol, :name
end
class Image < Grape::Entity class Image < Grape::Entity
expose :name, :entrypoint expose :name, :entrypoint
expose :ports, using: JobRequest::Port
end end
class Service < Image class Service < Image
......
...@@ -4,7 +4,7 @@ module Gitlab ...@@ -4,7 +4,7 @@ module Gitlab
module Ci module Ci
module Build module Build
class Image class Image
attr_reader :alias, :command, :entrypoint, :name attr_reader :alias, :command, :entrypoint, :name, :ports
class << self class << self
def from_image(job) def from_image(job)
...@@ -26,17 +26,25 @@ module Gitlab ...@@ -26,17 +26,25 @@ module Gitlab
def initialize(image) def initialize(image)
if image.is_a?(String) if image.is_a?(String)
@name = image @name = image
@ports = []
elsif image.is_a?(Hash) elsif image.is_a?(Hash)
@alias = image[:alias] @alias = image[:alias]
@command = image[:command] @command = image[:command]
@entrypoint = image[:entrypoint] @entrypoint = image[:entrypoint]
@name = image[:name] @name = image[:name]
@ports = build_ports(image).select(&:valid?)
end end
end end
def valid? def valid?
@name.present? @name.present?
end end
private
def build_ports(image)
image[:ports].to_a.map { |port| ::Gitlab::Ci::Build::Port.new(port) }
end
end end
end end
end end
......
# 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
...@@ -9,24 +9,24 @@ module Gitlab ...@@ -9,24 +9,24 @@ module Gitlab
# #
class Image < ::Gitlab::Config::Entry::Node class Image < ::Gitlab::Config::Entry::Node
include ::Gitlab::Config::Entry::Validatable 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 validations do
validates :config, hash_or_string: true validates :config, hash_or_string: true
validates :config, allowed_keys: ALLOWED_KEYS validates :config, allowed_keys: ALLOWED_KEYS
validates :config, disallowed_keys: %i[ports], unless: :with_image_ports?
validates :name, type: String, presence: true validates :name, type: String, presence: true
validates :entrypoint, array_of_strings: true, allow_nil: true validates :entrypoint, array_of_strings: true, allow_nil: true
end end
def hash? entry :ports, Entry::Ports,
@config.is_a?(Hash) description: 'Ports used expose the image'
end
def string? attributes :ports
@config.is_a?(String)
end
def name def name
value[:name] value[:name]
...@@ -42,6 +42,14 @@ module Gitlab ...@@ -42,6 +42,14 @@ module Gitlab
{} {}
end end
def with_image_ports?
opt(:with_image_ports)
end
def skip_config_hash_validation?
true
end
end end
end end
end end
......
# 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
# 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
...@@ -10,16 +10,18 @@ module Gitlab ...@@ -10,16 +10,18 @@ module Gitlab
class Service < Image class Service < Image
include ::Gitlab::Config::Entry::Validatable include ::Gitlab::Config::Entry::Validatable
ALLOWED_KEYS = %i[name entrypoint command alias].freeze ALLOWED_KEYS = %i[name entrypoint command alias ports].freeze
validations do validations do
validates :config, hash_or_string: true validates :config, hash_or_string: true
validates :config, allowed_keys: ALLOWED_KEYS validates :config, allowed_keys: ALLOWED_KEYS
validates :config, disallowed_keys: %i[ports], unless: :with_image_ports?
validates :name, type: String, presence: true validates :name, type: String, presence: true
validates :entrypoint, array_of_strings: true, allow_nil: true validates :entrypoint, array_of_strings: true, allow_nil: true
validates :command, 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, allow_nil: true
validates :alias, type: String, presence: true, unless: ->(record) { record.ports.blank? }
end end
def alias def alias
......
...@@ -12,6 +12,7 @@ module Gitlab ...@@ -12,6 +12,7 @@ module Gitlab
validations do validations do
validates :config, type: Array validates :config, type: Array
validates :config, services_with_ports_alias_unique: true, if: ->(record) { record.opt(:with_image_ports) }
end end
def compose!(deps = nil) def compose!(deps = nil)
...@@ -20,6 +21,7 @@ module Gitlab ...@@ -20,6 +21,7 @@ module Gitlab
@config.each do |config| @config.each do |config|
@entries << ::Gitlab::Config::Entry::Factory.new(Entry::Service) @entries << ::Gitlab::Config::Entry::Factory.new(Entry::Service)
.value(config || {}) .value(config || {})
.with(key: "service", parent: self, description: "service definition.") # rubocop:disable CodeReuse/ActiveRecord
.create! .create!
end end
......
...@@ -21,7 +21,7 @@ module Gitlab ...@@ -21,7 +21,7 @@ module Gitlab
include Validatable include Validatable
validations do validations do
validates :config, type: Hash validates :config, type: Hash, unless: :skip_config_hash_validation?
end end
end end
...@@ -30,6 +30,10 @@ module Gitlab ...@@ -30,6 +30,10 @@ module Gitlab
return unless valid? return unless valid?
self.class.nodes.each do |key, factory| 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 factory
.value(config[key]) .value(config[key])
.with(key: key, parent: self) .with(key: key, parent: self)
...@@ -45,6 +49,10 @@ module Gitlab ...@@ -45,6 +49,10 @@ module Gitlab
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
def skip_config_hash_validation?
false
end
class_methods do class_methods do
def nodes def nodes
Hash[(@nodes || {}).map { |key, factory| [key, factory.dup] }] Hash[(@nodes || {}).map { |key, factory| [key, factory.dup] }]
......
...@@ -61,7 +61,7 @@ module Gitlab ...@@ -61,7 +61,7 @@ module Gitlab
end end
def fabricate(entry, value = nil) def fabricate(entry, value = nil)
entry.new(value, @metadata).tap do |node| entry.new(value, @metadata) do |node|
node.key = @attributes[:key] node.key = @attributes[:key]
node.parent = @attributes[:parent] node.parent = @attributes[:parent]
node.default = @attributes[:default] node.default = @attributes[:default]
......
...@@ -17,6 +17,8 @@ module Gitlab ...@@ -17,6 +17,8 @@ module Gitlab
@metadata = metadata @metadata = metadata
@entries = {} @entries = {}
yield(self) if block_given?
self.class.aspects.to_a.each do |aspect| self.class.aspects.to_a.each do |aspect|
instance_exec(&aspect) instance_exec(&aspect)
end end
...@@ -44,6 +46,12 @@ module Gitlab ...@@ -44,6 +46,12 @@ module Gitlab
@parent ? @parent.ancestors + [@parent] : [] @parent ? @parent.ancestors + [@parent] : []
end end
def opt(key)
opt = metadata[key]
opt = @parent.opt(key) if opt.nil? && @parent
opt
end
def valid? def valid?
errors.none? errors.none?
end end
...@@ -85,6 +93,18 @@ module Gitlab ...@@ -85,6 +93,18 @@ module Gitlab
"#<#{self.class.name} #{unspecified}{#{key}: #{val.inspect}}>" "#<#{self.class.name} #{unspecified}{#{key}: #{val.inspect}}>"
end 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(**) def self.default(**)
end end
......
...@@ -19,7 +19,10 @@ module Gitlab ...@@ -19,7 +19,10 @@ module Gitlab
entry = self.class.entry_class(strategy) 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 end
def self.strategy(name, **opts) def self.strategy(name, **opts)
......
...@@ -15,6 +15,17 @@ module Gitlab ...@@ -15,6 +15,17 @@ module Gitlab
end end
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 class AllowedValuesValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value) def validate_each(record, attribute, value)
unless options[:in].include?(value.to_s) unless options[:in].include?(value.to_s)
...@@ -186,6 +197,97 @@ module Gitlab ...@@ -186,6 +197,97 @@ module Gitlab
end end
end 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 end
end end
......
# frozen_string_literal: true
require 'spec_helper'
describe API::Entities::JobRequest::Image do
let(:ports) { [{ number: 80, protocol: 'http', name: 'name' }]}
let(:image) { double(name: 'image_name', entrypoint: ['foo'], ports: ports)}
let(:entity) { described_class.new(image) }
subject { entity.as_json }
it 'returns the image name' do
expect(subject[:name]).to eq 'image_name'
end
it 'returns the entrypoint' do
expect(subject[:entrypoint]).to eq ['foo']
end
it 'returns the ports' do
expect(subject[:ports]).to eq ports
end
context 'when the ports param is nil' do
let(:ports) { nil }
it 'does not return the ports' do
expect(subject[:ports]).to be_nil
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe ::API::Entities::JobRequest::Port do
let(:port) { double(number: 80, protocol: 'http', name: 'name')}
let(:entity) { described_class.new(port) }
subject { entity.as_json }
it 'returns the port number' do
expect(subject[:number]).to eq 80
end
it 'returns if the port protocol' do
expect(subject[:protocol]).to eq 'http'
end
it 'returns the port name' do
expect(subject[:name]).to eq 'name'
end
end
...@@ -18,11 +18,16 @@ describe Gitlab::Ci::Build::Image do ...@@ -18,11 +18,16 @@ describe Gitlab::Ci::Build::Image do
it 'populates fabricated object with the proper name attribute' do it 'populates fabricated object with the proper name attribute' do
expect(subject.name).to eq(image_name) expect(subject.name).to eq(image_name)
end end
it 'does not populate the ports' do
expect(subject.ports).to be_empty
end
end end
context 'when image is defined as hash' do context 'when image is defined as hash' do
let(:entrypoint) { '/bin/sh' } let(:entrypoint) { '/bin/sh' }
let(:job) { create(:ci_build, options: { image: { name: image_name, entrypoint: entrypoint } } ) }
let(:job) { create(:ci_build, options: { image: { name: image_name, entrypoint: entrypoint, ports: [80] } } ) }
it 'fabricates an object of the proper class' do it 'fabricates an object of the proper class' do
is_expected.to be_kind_of(described_class) is_expected.to be_kind_of(described_class)
...@@ -32,6 +37,13 @@ describe Gitlab::Ci::Build::Image do ...@@ -32,6 +37,13 @@ describe Gitlab::Ci::Build::Image do
expect(subject.name).to eq(image_name) expect(subject.name).to eq(image_name)
expect(subject.entrypoint).to eq(entrypoint) expect(subject.entrypoint).to eq(entrypoint)
end end
it 'populates the ports' do
port = subject.ports.first
expect(port.number).to eq 80
expect(port.protocol).to eq 'http'
expect(port.name).to eq 'default_port'
end
end end
context 'when image name is empty' do context 'when image name is empty' do
...@@ -67,6 +79,10 @@ describe Gitlab::Ci::Build::Image do ...@@ -67,6 +79,10 @@ describe Gitlab::Ci::Build::Image do
expect(subject.first).to be_kind_of(described_class) expect(subject.first).to be_kind_of(described_class)
expect(subject.first.name).to eq(service_image_name) expect(subject.first.name).to eq(service_image_name)
end end
it 'does not populate the ports' do
expect(subject.first.ports).to be_empty
end
end end
context 'when service is defined as hash' do context 'when service is defined as hash' do
...@@ -75,7 +91,7 @@ describe Gitlab::Ci::Build::Image do ...@@ -75,7 +91,7 @@ describe Gitlab::Ci::Build::Image do
let(:service_command) { 'sleep 30' } let(:service_command) { 'sleep 30' }
let(:job) do let(:job) do
create(:ci_build, options: { services: [{ name: service_image_name, entrypoint: service_entrypoint, create(:ci_build, options: { services: [{ name: service_image_name, entrypoint: service_entrypoint,
alias: service_alias, command: service_command }] }) alias: service_alias, command: service_command, ports: [80] }] })
end end
it 'fabricates an non-empty array of objects' do it 'fabricates an non-empty array of objects' do
...@@ -89,6 +105,11 @@ describe Gitlab::Ci::Build::Image do ...@@ -89,6 +105,11 @@ describe Gitlab::Ci::Build::Image do
expect(subject.first.entrypoint).to eq(service_entrypoint) expect(subject.first.entrypoint).to eq(service_entrypoint)
expect(subject.first.alias).to eq(service_alias) expect(subject.first.alias).to eq(service_alias)
expect(subject.first.command).to eq(service_command) expect(subject.first.command).to eq(service_command)
port = subject.first.ports.first
expect(port.number).to eq 80
expect(port.protocol).to eq 'http'
expect(port.name).to eq 'default_port'
end end
end end
......
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Ci::Build::Port do
subject { described_class.new(port) }
context 'when port is defined as an integer' do
let(:port) { 80 }
it 'populates the object' do
expect(subject.number).to eq 80
expect(subject.protocol).to eq described_class::DEFAULT_PORT_PROTOCOL
expect(subject.name).to eq described_class::DEFAULT_PORT_NAME
end
end
context 'when port is defined as hash' do
let(:port) { { number: 80, protocol: 'https', name: 'port_name' } }
it 'populates the object' do
expect(subject.number).to eq 80
expect(subject.protocol).to eq 'https'
expect(subject.name).to eq 'port_name'
end
end
end
...@@ -35,6 +35,12 @@ describe Gitlab::Ci::Config::Entry::Image do ...@@ -35,6 +35,12 @@ describe Gitlab::Ci::Config::Entry::Image do
expect(entry.entrypoint).to be_nil expect(entry.entrypoint).to be_nil
end end
end end
describe '#ports' do
it "returns image's ports" do
expect(entry.ports).to be_nil
end
end
end end
context 'when configuration is a hash' do context 'when configuration is a hash' do
...@@ -69,6 +75,38 @@ describe Gitlab::Ci::Config::Entry::Image do ...@@ -69,6 +75,38 @@ describe Gitlab::Ci::Config::Entry::Image do
expect(entry.entrypoint).to eq %w(/bin/sh run) expect(entry.entrypoint).to eq %w(/bin/sh run)
end end
end end
context 'when configuration has ports' do
let(:ports) { [{ number: 80, protocol: 'http', name: 'foobar' }] }
let(:config) { { name: 'ruby:2.2', entrypoint: %w(/bin/sh run), ports: ports } }
let(:entry) { described_class.new(config, { with_image_ports: image_ports }) }
let(:image_ports) { false }
context 'when with_image_ports metadata is not enabled' do
describe '#valid?' do
it 'is not valid' do
expect(entry).not_to be_valid
expect(entry.errors).to include("image config contains disallowed keys: ports")
end
end
end
context 'when with_image_ports metadata is enabled' do
let(:image_ports) { true }
describe '#valid?' do
it 'is valid' do
expect(entry).to be_valid
end
end
describe '#ports' do
it "returns image's ports" do
expect(entry.ports).to eq ports
end
end
end
end
end end
context 'when entry value is not correct' do context 'when entry value is not correct' do
...@@ -76,8 +114,8 @@ describe Gitlab::Ci::Config::Entry::Image do ...@@ -76,8 +114,8 @@ describe Gitlab::Ci::Config::Entry::Image do
describe '#errors' do describe '#errors' do
it 'saves errors' do it 'saves errors' do
expect(entry.errors) expect(entry.errors.first)
.to include 'image config should be a hash or a string' .to match /config should be a hash or a string/
end end
end end
...@@ -93,8 +131,8 @@ describe Gitlab::Ci::Config::Entry::Image do ...@@ -93,8 +131,8 @@ describe Gitlab::Ci::Config::Entry::Image do
describe '#errors' do describe '#errors' do
it 'saves errors' do it 'saves errors' do
expect(entry.errors) expect(entry.errors.first)
.to include 'image config contains unknown keys: non_existing' .to match /config contains unknown keys: non_existing/
end end
end end
......
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Ci::Config::Entry::Port do
let(:entry) { described_class.new(config) }
before do
entry.compose!
end
context 'when configuration is a string' do
let(:config) { 80 }
describe '#valid?' do
it 'is valid' do
expect(entry).to be_valid
end
end
describe '#value' do
it 'returns valid hash' do
expect(entry.value).to eq(number: 80)
end
end
describe '#number' do
it "returns port number" do
expect(entry.number).to eq 80
end
end
describe '#protocol' do
it "is nil" do
expect(entry.protocol).to be_nil
end
end
describe '#name' do
it "is nil" do
expect(entry.name).to be_nil
end
end
end
context 'when configuration is a hash' do
context 'with the complete hash' do
let(:config) do
{ number: 80,
protocol: 'http',
name: 'foobar' }
end
describe '#valid?' do
it 'is valid' do
expect(entry).to be_valid
end
end
describe '#value' do
it 'returns valid hash' do
expect(entry.value).to eq config
end
end
describe '#number' do
it "returns port number" do
expect(entry.number).to eq 80
end
end
describe '#protocol' do
it "returns port protocol" do
expect(entry.protocol).to eq 'http'
end
end
describe '#name' do
it "returns port name" do
expect(entry.name).to eq 'foobar'
end
end
end
context 'with only the port number' do
let(:config) { { number: 80 } }
describe '#valid?' do
it 'is valid' do
expect(entry).to be_valid
end
end
describe '#value' do
it 'returns valid hash' do
expect(entry.value).to eq(number: 80)
end
end
describe '#number' do
it "returns port number" do
expect(entry.number).to eq 80
end
end
describe '#protocol' do
it "is nil" do
expect(entry.protocol).to be_nil
end
end
describe '#name' do
it "is nil" do
expect(entry.name).to be_nil
end
end
end
context 'without the number' do
let(:config) { { protocol: 'http' } }
describe '#valid?' do
it 'is not valid' do
expect(entry).not_to be_valid
end
end
end
end
context 'when configuration is invalid' do
let(:config) { '80' }
describe '#valid?' do
it 'is valid' do
expect(entry).not_to be_valid
end
end
end
context 'when protocol' do
let(:config) { { number: 80, protocol: protocol, name: 'foobar' } }
context 'is http' do
let(:protocol) { 'http' }
describe '#valid?' do
it 'is valid' do
expect(entry).to be_valid
end
end
end
context 'is https' do
let(:protocol) { 'https' }
describe '#valid?' do
it 'is valid' do
expect(entry).to be_valid
end
end
end
context 'is neither http nor https' do
let(:protocol) { 'foo' }
describe '#valid?' do
it 'is invalid' do
expect(entry.errors).to include("port protocol should be http or https")
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Ci::Config::Entry::Ports do
let(:entry) { described_class.new(config) }
before do
entry.compose!
end
context 'when configuration is valid' do
let(:config) { [{ number: 80, protocol: 'http', name: 'foobar' }] }
describe '#valid?' do
it 'is valid' do
expect(entry).to be_valid
end
end
describe '#value' do
it 'returns valid array' do
expect(entry.value).to eq(config)
end
end
end
context 'when configuration is invalid' do
let(:config) { 'postgresql:9.5' }
describe '#valid?' do
it 'is invalid' do
expect(entry).not_to be_valid
end
end
context 'when any of the ports' do
before do
expect(entry).not_to be_valid
expect(entry.errors.count).to eq 1
end
context 'have the same name' do
let(:config) do
[{ number: 80, protocol: 'http', name: 'foobar' },
{ number: 81, protocol: 'http', name: 'foobar' }]
end
describe '#valid?' do
it 'is invalid' do
expect(entry.errors.first).to match /each port name must be different/
end
end
end
context 'have the same port' do
let(:config) do
[{ number: 80, protocol: 'http', name: 'foobar' },
{ number: 80, protocol: 'http', name: 'foobar1' }]
end
describe '#valid?' do
it 'is invalid' do
expect(entry.errors.first).to match /each port number can only be referenced once/
end
end
end
end
end
end
...@@ -39,6 +39,12 @@ describe Gitlab::Ci::Config::Entry::Service do ...@@ -39,6 +39,12 @@ describe Gitlab::Ci::Config::Entry::Service do
expect(entry.command).to be_nil expect(entry.command).to be_nil
end end
end end
describe '#ports' do
it "returns service's ports" do
expect(entry.ports).to be_nil
end
end
end end
context 'when configuration is a hash' do context 'when configuration is a hash' do
...@@ -81,6 +87,40 @@ describe Gitlab::Ci::Config::Entry::Service do ...@@ -81,6 +87,40 @@ describe Gitlab::Ci::Config::Entry::Service do
expect(entry.entrypoint).to eq %w(/bin/sh run) expect(entry.entrypoint).to eq %w(/bin/sh run)
end end
end end
context 'when configuration has ports' do
let(:ports) { [{ number: 80, protocol: 'http', name: 'foobar' }] }
let(:config) do
{ name: 'postgresql:9.5', alias: 'db', command: %w(cmd run), entrypoint: %w(/bin/sh run), ports: ports }
end
let(:entry) { described_class.new(config, { with_image_ports: image_ports }) }
let(:image_ports) { false }
context 'when with_image_ports metadata is not enabled' do
describe '#valid?' do
it 'is not valid' do
expect(entry).not_to be_valid
expect(entry.errors).to include("service config contains disallowed keys: ports")
end
end
end
context 'when with_image_ports metadata is enabled' do
let(:image_ports) { true }
describe '#valid?' do
it 'is valid' do
expect(entry).to be_valid
end
end
describe '#ports' do
it "returns image's ports" do
expect(entry.ports).to eq ports
end
end
end
end
end end
context 'when entry value is not correct' do context 'when entry value is not correct' do
...@@ -88,8 +128,8 @@ describe Gitlab::Ci::Config::Entry::Service do ...@@ -88,8 +128,8 @@ describe Gitlab::Ci::Config::Entry::Service do
describe '#errors' do describe '#errors' do
it 'saves errors' do it 'saves errors' do
expect(entry.errors) expect(entry.errors.first)
.to include 'service config should be a hash or a string' .to match /config should be a hash or a string/
end end
end end
...@@ -105,8 +145,8 @@ describe Gitlab::Ci::Config::Entry::Service do ...@@ -105,8 +145,8 @@ describe Gitlab::Ci::Config::Entry::Service do
describe '#errors' do describe '#errors' do
it 'saves errors' do it 'saves errors' do
expect(entry.errors) expect(entry.errors.first)
.to include 'service config contains unknown keys: non_existing' .to match /config contains unknown keys: non_existing/
end end
end end
...@@ -116,4 +156,26 @@ describe Gitlab::Ci::Config::Entry::Service do ...@@ -116,4 +156,26 @@ describe Gitlab::Ci::Config::Entry::Service do
end end
end end
end end
context 'when service has ports' do
let(:ports) { [{ number: 80, protocol: 'http', name: 'foobar' }] }
let(:config) do
{ name: 'postgresql:9.5', command: %w(cmd run), entrypoint: %w(/bin/sh run), ports: ports }
end
it 'alias field is mandatory' do
expect(entry).not_to be_valid
expect(entry.errors).to include("service alias can't be blank")
end
end
context 'when service does not have ports' do
let(:config) do
{ name: 'postgresql:9.5', alias: 'db', command: %w(cmd run), entrypoint: %w(/bin/sh run) }
end
it 'alias field is optional' do
expect(entry).to be_valid
end
end
end end
...@@ -32,4 +32,91 @@ describe Gitlab::Ci::Config::Entry::Services do ...@@ -32,4 +32,91 @@ describe Gitlab::Ci::Config::Entry::Services do
end end
end end
end end
context 'when configuration has ports' do
let(:ports) { [{ number: 80, protocol: 'http', name: 'foobar' }] }
let(:config) { ['postgresql:9.5', { name: 'postgresql:9.1', alias: 'postgres_old', ports: ports }] }
let(:entry) { described_class.new(config, { with_image_ports: image_ports }) }
let(:image_ports) { false }
context 'when with_image_ports metadata is not enabled' do
describe '#valid?' do
it 'is not valid' do
expect(entry).not_to be_valid
expect(entry.errors).to include("service config contains disallowed keys: ports")
end
end
end
context 'when with_image_ports metadata is enabled' do
let(:image_ports) { true }
describe '#valid?' do
it 'is valid' do
expect(entry).to be_valid
end
end
describe '#value' do
it 'returns valid array' do
expect(entry.value).to eq([{ name: 'postgresql:9.5' }, { name: 'postgresql:9.1', alias: 'postgres_old', ports: ports }])
end
end
describe 'services alias' do
context 'when they are not unique' do
let(:config) do
['postgresql:9.5',
{ name: 'postgresql:9.1', alias: 'postgres_old', ports: [80] },
{ name: 'ruby', alias: 'postgres_old', ports: [81] }]
end
describe '#valid?' do
it 'is invalid' do
expect(entry).not_to be_valid
expect(entry.errors).to include("services config alias must be unique in services with ports")
end
end
end
context 'when they are unique' do
let(:config) do
['postgresql:9.5',
{ name: 'postgresql:9.1', alias: 'postgres_old', ports: [80] },
{ name: 'ruby', alias: 'ruby', ports: [81] }]
end
describe '#valid?' do
it 'is valid' do
expect(entry).to be_valid
end
end
end
context 'when one of the duplicated alias is in a service without ports' do
let(:config) do
['postgresql:9.5',
{ name: 'postgresql:9.1', alias: 'postgres_old', ports: [80] },
{ name: 'ruby', alias: 'postgres_old' }]
end
it 'is valid' do
expect(entry).to be_valid
end
end
context 'when there are not any ports' do
let(:config) do
['postgresql:9.5',
{ name: 'postgresql:9.1', alias: 'postgres_old' },
{ name: 'ruby', alias: 'postgres_old' }]
end
it 'is valid' do
expect(entry).to be_valid
end
end
end
end
end
end end
...@@ -123,6 +123,63 @@ describe Gitlab::Ci::Config do ...@@ -123,6 +123,63 @@ describe Gitlab::Ci::Config do
) )
end end
end end
context 'when ports have been set' do
context 'in the main image' do
let(:yml) do
<<-EOS
image:
name: ruby:2.2
ports:
- 80
EOS
end
it 'raises an error' do
expect(config.errors).to include("image config contains disallowed keys: ports")
end
end
context 'in the job image' do
let(:yml) do
<<-EOS
image: ruby:2.2
test:
script: rspec
image:
name: ruby:2.2
ports:
- 80
EOS
end
it 'raises an error' do
expect(config.errors).to include("jobs:test:image config contains disallowed keys: ports")
end
end
context 'in the services' do
let(:yml) do
<<-EOS
image: ruby:2.2
test:
script: rspec
image: ruby:2.2
services:
- name: test
alias: test
ports:
- 80
EOS
end
it 'raises an error' do
expect(config.errors).to include("jobs:test:services:service config contains disallowed keys: ports")
end
end
end
end end
context "when using 'include' directive" do context "when using 'include' directive" do
......
...@@ -1233,7 +1233,7 @@ module Gitlab ...@@ -1233,7 +1233,7 @@ module Gitlab
config = YAML.dump({ services: [10, "test"], rspec: { script: "test" } }) config = YAML.dump({ services: [10, "test"], rspec: { script: "test" } })
expect do expect do
Gitlab::Ci::YamlProcessor.new(config) Gitlab::Ci::YamlProcessor.new(config)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "service config should be a hash or a string") end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "services:service config should be a hash or a string")
end end
it "returns errors if job services parameter is not an array" do it "returns errors if job services parameter is not an array" do
...@@ -1247,7 +1247,7 @@ module Gitlab ...@@ -1247,7 +1247,7 @@ module Gitlab
config = YAML.dump({ rspec: { script: "test", services: [10, "test"] } }) config = YAML.dump({ rspec: { script: "test", services: [10, "test"] } })
expect do expect do
Gitlab::Ci::YamlProcessor.new(config) Gitlab::Ci::YamlProcessor.new(config)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "service config should be a hash or a string") end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:services:service config should be a hash or a string")
end end
it "returns error if job configuration is invalid" do it "returns error if job configuration is invalid" do
......
...@@ -470,11 +470,11 @@ describe API::Runner, :clean_gitlab_redis_shared_state do ...@@ -470,11 +470,11 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
expect(json_response['token']).to eq(job.token) expect(json_response['token']).to eq(job.token)
expect(json_response['job_info']).to eq(expected_job_info) expect(json_response['job_info']).to eq(expected_job_info)
expect(json_response['git_info']).to eq(expected_git_info) expect(json_response['git_info']).to eq(expected_git_info)
expect(json_response['image']).to eq({ 'name' => 'ruby:2.1', 'entrypoint' => '/bin/sh' }) expect(json_response['image']).to eq({ 'name' => 'ruby:2.1', 'entrypoint' => '/bin/sh', 'ports' => [] })
expect(json_response['services']).to eq([{ 'name' => 'postgres', 'entrypoint' => nil, expect(json_response['services']).to eq([{ 'name' => 'postgres', 'entrypoint' => nil,
'alias' => nil, 'command' => nil }, 'alias' => nil, 'command' => nil, 'ports' => [] },
{ 'name' => 'docker:stable-dind', 'entrypoint' => '/bin/sh', { 'name' => 'docker:stable-dind', 'entrypoint' => '/bin/sh',
'alias' => 'docker', 'command' => 'sleep 30' }]) 'alias' => 'docker', 'command' => 'sleep 30', 'ports' => [] }])
expect(json_response['steps']).to eq(expected_steps) expect(json_response['steps']).to eq(expected_steps)
expect(json_response['artifacts']).to eq(expected_artifacts) expect(json_response['artifacts']).to eq(expected_artifacts)
expect(json_response['cache']).to eq(expected_cache) expect(json_response['cache']).to eq(expected_cache)
...@@ -853,6 +853,56 @@ describe API::Runner, :clean_gitlab_redis_shared_state do ...@@ -853,6 +853,56 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
end end
end end
describe 'port support' do
let(:job) { create(:ci_build, pipeline: pipeline, options: options) }
context 'when job image has ports' do
let(:options) do
{
image: {
name: 'ruby',
ports: [80]
},
services: ['mysql']
}
end
it 'returns the image ports' do
request_job
expect(response).to have_http_status(:created)
expect(json_response).to include(
'id' => job.id,
'image' => a_hash_including('name' => 'ruby', 'ports' => [{ 'number' => 80, 'protocol' => 'http', 'name' => 'default_port' }]),
'services' => all(a_hash_including('name' => 'mysql')))
end
end
context 'when job services settings has ports' do
let(:options) do
{
image: 'ruby',
services: [
{
name: 'tomcat',
ports: [{ number: 8081, protocol: 'http', name: 'custom_port' }]
}
]
}
end
it 'returns the service ports' do
request_job
expect(response).to have_http_status(:created)
expect(json_response).to include(
'id' => job.id,
'image' => a_hash_including('name' => 'ruby'),
'services' => all(a_hash_including('name' => 'tomcat', 'ports' => [{ 'number' => 8081, 'protocol' => 'http', 'name' => 'custom_port' }])))
end
end
end
def request_job(token = runner.token, **params) def request_job(token = runner.token, **params)
new_params = params.merge(token: token, last_update: last_update) new_params = params.merge(token: token, last_update: last_update)
post api('/jobs/request'), params: new_params, headers: { 'User-Agent' => user_agent } post api('/jobs/request'), params: new_params, headers: { 'User-Agent' => user_agent }
......
...@@ -306,6 +306,56 @@ describe Ci::CreatePipelineService do ...@@ -306,6 +306,56 @@ describe Ci::CreatePipelineService do
it_behaves_like 'a failed pipeline' it_behaves_like 'a failed pipeline'
end end
context 'when config has ports' do
context 'in the main image' do
let(:ci_yaml) do
<<-EOS
image:
name: ruby:2.2
ports:
- 80
EOS
end
it_behaves_like 'a failed pipeline'
end
context 'in the job image' do
let(:ci_yaml) do
<<-EOS
image: ruby:2.2
test:
script: rspec
image:
name: ruby:2.2
ports:
- 80
EOS
end
it_behaves_like 'a failed pipeline'
end
context 'in the service' do
let(:ci_yaml) do
<<-EOS
image: ruby:2.2
test:
script: rspec
image: ruby:2.2
services:
- name: test
ports:
- 80
EOS
end
it_behaves_like 'a failed pipeline'
end
end
end end
context 'when commit contains a [ci skip] directive' do context 'when commit contains a [ci skip] directive' do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment