Skip to content
Snippets Groups Projects
Commit e604344c authored by Kamil Trzcińśki's avatar Kamil Trzcińśki
Browse files

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

CE Backport: Update Web IDE config to accept ports

Closes gitlab-ee#10179

See merge request gitlab-org/gitlab-ce!26679
parents a7d3a5e4 6ee1d8cf
No related branches found
No related tags found
No related merge requests found
Showing
with 682 additions and 18 deletions
Loading
Loading
@@ -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
Loading
Loading
Loading
Loading
@@ -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)
Loading
Loading
@@ -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
Loading
Loading
# 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
Loading
Loading
@@ -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]
Loading
Loading
@@ -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
Loading
Loading
# 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
Loading
Loading
@@ -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
Loading
Loading
Loading
Loading
@@ -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)
Loading
Loading
@@ -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
 
Loading
Loading
Loading
Loading
@@ -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
 
Loading
Loading
@@ -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)
Loading
Loading
@@ -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] }]
Loading
Loading
Loading
Loading
@@ -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]
Loading
Loading
Loading
Loading
@@ -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
Loading
Loading
@@ -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
Loading
Loading
@@ -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
 
Loading
Loading
Loading
Loading
@@ -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)
Loading
Loading
Loading
Loading
@@ -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)
Loading
Loading
@@ -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
Loading
Loading
# 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
Loading
Loading
@@ -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)
Loading
Loading
@@ -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
Loading
Loading
@@ -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
Loading
Loading
@@ -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
Loading
Loading
@@ -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
 
Loading
Loading
# 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
Loading
Loading
@@ -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
Loading
Loading
@@ -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
Loading
Loading
@@ -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
 
Loading
Loading
@@ -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
 
Loading
Loading
# 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
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment