Commit 5665fd9f authored by Kamil Trzciński's avatar Kamil Trzciński

Merge branch 'fj-10179-add-ports-to-web-ide-config' into 'master'

Update Web IDE config to accept ports

Closes #10179 and #5276

See merge request gitlab-org/gitlab-ee!9818
parents c2b9adad ef34b9d4
---
title: Update Web IDE config to accept ports
merge_request: 9818
author:
type: added
......@@ -11,7 +11,8 @@ module Gitlab
def initialize(config, opts = {})
@config = build_config(config, opts)
@global = Entry::Global.new(@config)
@global = Entry::Global.new(@config,
with_image_ports: true)
@global.compose!
rescue Gitlab::Config::Loader::FormatError => e
raise Config::ConfigError, e.message
......
......@@ -20,6 +20,7 @@ module Gitlab
validations do
validates :config, allowed_keys: ALLOWED_KEYS
validates :config, job_port_unique: { data: ->(record) { record.ports } }
with_options allow_nil: true do
validates :tags, array_of_strings: true
......
......@@ -50,6 +50,26 @@ describe Ci::CreateWebIdeTerminalService do
it_behaves_like 'be successful'
end
context 'for configuration with ports' do
let(:config_content) do
<<-EOS
terminal:
image:
name: ruby:2.2
ports:
- 80
script: rspec
services:
- name: test
alias: test
ports:
- 8080
EOS
end
it_behaves_like 'be successful'
end
end
end
......
......@@ -3,7 +3,7 @@
require 'spec_helper'
describe Gitlab::WebIde::Config::Entry::Terminal do
let(:entry) { described_class.new(config) }
let(:entry) { described_class.new(config, with_image_ports: true) }
describe '.nodes' do
context 'when filtering all the entry/node names' do
......@@ -30,6 +30,21 @@ describe Gitlab::WebIde::Config::Entry::Terminal do
expect(entry).to be_valid
end
end
context 'when the same port is not duplicated' do
let(:config) do
{
image: { name: "ruby", ports: [80] },
services: [{ name: "mysql", alias: "service1", ports: [81] }, { name: "mysql", alias: "service2", ports: [82] }]
}
end
describe '#valid?' do
it 'is valid' do
expect(entry).to be_valid
end
end
end
end
context 'when entry value is not correct' do
......@@ -63,6 +78,23 @@ describe Gitlab::WebIde::Config::Entry::Terminal do
end
end
end
context 'when the same port is duplicated' do
let(:config) do
{
image: { name: "ruby", ports: [80] },
services: [{ name: "mysql", ports: [80] }, { name: "mysql", ports: [81] }]
}
end
describe '#valid?' do
it 'is invalid' do
expect(entry).not_to be_valid
expect(entry.errors.count).to eq 1
expect(entry.errors.first).to match "each port number can only be referenced once"
end
end
end
end
end
......@@ -80,10 +112,6 @@ describe Gitlab::WebIde::Config::Entry::Terminal do
end
describe '#value' do
before do
entry.compose!
end
context 'when entry is correct' do
let(:config) do
{ before_script: %w[ls pwd],
......
......@@ -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
......
......@@ -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
......
# 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
#
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
......
# 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
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
......
......@@ -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
......
......@@ -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] }]
......
......@@ -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]
......
......@@ -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
......
......@@ -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)
......
......@@ -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
......
# 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
it 'populates fabricated object with the proper name attribute' do
expect(subject.name).to eq(image_name)
end
it 'does not populate the ports' do
expect(subject.ports).to be_empty
end
end
context 'when image is defined as hash' do
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
is_expected.to be_kind_of(described_class)
......@@ -32,6 +37,13 @@ describe Gitlab::Ci::Build::Image do
expect(subject.name).to eq(image_name)
expect(subject.entrypoint).to eq(entrypoint)
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
context 'when image name is empty' do
......@@ -67,6 +79,10 @@ describe Gitlab::Ci::Build::Image do
expect(subject.first).to be_kind_of(described_class)
expect(subject.first.name).to eq(service_image_name)
end
it 'does not populate the ports' do
expect(subject.first.ports).to be_empty
end
end
context 'when service is defined as hash' do
......@@ -75,7 +91,7 @@ describe Gitlab::Ci::Build::Image do
let(:service_command) { 'sleep 30' }
let(:job) do
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
it 'fabricates an non-empty array of objects' do
......@@ -89,6 +105,11 @@ describe Gitlab::Ci::Build::Image do
expect(subject.first.entrypoint).to eq(service_entrypoint)
expect(subject.first.alias).to eq(service_alias)
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
......
# 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
expect(entry.entrypoint).to be_nil
end
end
describe '#ports' do
it "returns image's ports" do
expect(entry.ports).to be_nil
end
end
end
context 'when configuration is a hash' do
......@@ -69,6 +75,38 @@ describe Gitlab::Ci::Config::Entry::Image do
expect(entry.entrypoint).to eq %w(/bin/sh run)
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
context 'when entry value is not correct' do
......@@ -76,8 +114,8 @@ describe Gitlab::Ci::Config::Entry::Image do
describe '#errors' do
it 'saves errors' do
expect(entry.errors)
.to include 'image config should be a hash or a string'
expect(entry.errors.first)
.to match /config should be a hash or a string/
end
end
......@@ -93,8 +131,8 @@ describe Gitlab::Ci::Config::Entry::Image do
describe '#errors' do
it 'saves errors' do
expect(entry.errors)
.to include 'image config contains unknown keys: non_existing'
expect(entry.errors.first)
.to match /config contains unknown keys: non_existing/
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
expect(entry.command).to be_nil
end
end
describe '#ports' do
it "returns service's ports" do
expect(entry.ports).to be_nil
end
end
end
context 'when configuration is a hash' do
......@@ -81,6 +87,40 @@ describe Gitlab::Ci::Config::Entry::Service do
expect(entry.entrypoint).to eq %w(/bin/sh run)
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
context 'when entry value is not correct' do
......@@ -88,8 +128,8 @@ describe Gitlab::Ci::Config::Entry::Service do
describe '#errors' do
it 'saves errors' do
expect(entry.errors)
.to include 'service config should be a hash or a string'
expect(entry.errors.first)
.to match /config should be a hash or a string/
end
end
......@@ -105,8 +145,8 @@ describe Gitlab::Ci::Config::Entry::Service do
describe '#errors' do
it 'saves errors' do
expect(entry.errors)
.to include 'service config contains unknown keys: non_existing'
expect(entry.errors.first)
.to match /config contains unknown keys: non_existing/
end
end
......@@ -116,4 +156,26 @@ describe Gitlab::Ci::Config::Entry::Service do
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
......@@ -32,4 +32,91 @@ describe Gitlab::Ci::Config::Entry::Services do
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
......@@ -123,6 +123,63 @@ describe Gitlab::Ci::Config do
)
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
context "when using 'include' directive" do
......
......@@ -1233,7 +1233,7 @@ module Gitlab
config = YAML.dump({ services: [10, "test"], rspec: { script: "test" } })
expect do
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
it "returns errors if job services parameter is not an array" do
......@@ -1247,7 +1247,7 @@ module Gitlab
config = YAML.dump({ rspec: { script: "test", services: [10, "test"] } })
expect do
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
it "returns error if job configuration is invalid" 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['job_info']).to eq(expected_job_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,
'alias' => nil, 'command' => nil },
'alias' => nil, 'command' => nil, 'ports' => [] },
{ '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['artifacts']).to eq(expected_artifacts)
expect(json_response['cache']).to eq(expected_cache)
......@@ -853,6 +853,56 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
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)
new_params = params.merge(token: token, last_update: last_update)
post api('/jobs/request'), params: new_params, headers: { 'User-Agent' => user_agent }
......
......@@ -306,6 +306,56 @@ describe Ci::CreatePipelineService do
it_behaves_like 'a failed pipeline'
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
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