Commit 3f5819fb authored by Kamil Trzcinski's avatar Kamil Trzcinski

Merge remote-tracking branch 'origin/master' into artifacts-expire-date

parents 1b62b86f 4b964011
...@@ -77,6 +77,7 @@ v 8.9.0 (unreleased) ...@@ -77,6 +77,7 @@ v 8.9.0 (unreleased)
- All classes in the Banzai::ReferenceParser namespace are now instrumented - All classes in the Banzai::ReferenceParser namespace are now instrumented
- Remove deprecated issues_tracker and issues_tracker_id from project model - Remove deprecated issues_tracker and issues_tracker_id from project model
- Allow users to create confidential issues in private projects - Allow users to create confidential issues in private projects
- Measure CPU time for instrumented methods
v 8.8.5 (unreleased) v 8.8.5 (unreleased)
- Ensure branch cleanup regardless of whether the GitHub import process succeeds - Ensure branch cleanup regardless of whether the GitHub import process succeeds
......
...@@ -4,14 +4,14 @@ GitLab CI allows you to use Docker Engine to build and test docker-based project ...@@ -4,14 +4,14 @@ GitLab CI allows you to use Docker Engine to build and test docker-based project
**This also allows to you to use `docker-compose` and other docker-enabled tools.** **This also allows to you to use `docker-compose` and other docker-enabled tools.**
This is one of new trends in Continuous Integration/Deployment to: One of the new trends in Continuous Integration/Deployment is to:
1. create application image, 1. create an application image,
1. run test against created image, 1. run tests against the created image,
1. push image to remote registry, 1. push image to a remote registry, and
1. deploy server from pushed image 1. deploy to a server from the pushed image.
It's also useful in case when your application already has the `Dockerfile` that can be used to create and test image: It's also useful when your application already has the `Dockerfile` that can be used to create and test an image:
```bash ```bash
$ docker build -t my-image dockerfiles/ $ docker build -t my-image dockerfiles/
$ docker run my-docker-image /script/to/run/tests $ docker run my-docker-image /script/to/run/tests
...@@ -19,24 +19,25 @@ $ docker tag my-image my-registry:5000/my-image ...@@ -19,24 +19,25 @@ $ docker tag my-image my-registry:5000/my-image
$ docker push my-registry:5000/my-image $ docker push my-registry:5000/my-image
``` ```
However, this requires special configuration of GitLab Runner to enable `docker` support during build. This requires special configuration of GitLab Runner to enable `docker` support during builds.
**This requires running GitLab Runner in privileged mode which can be harmful when untrusted code is run.**
There are two methods to enable the use of `docker build` and `docker run` during build. ## Runner Configuration
## 1. Use shell executor There are three methods to enable the use of `docker build` and `docker run` during builds; each with their own tradeoffs.
### Use shell executor
The simplest approach is to install GitLab Runner in `shell` execution mode. The simplest approach is to install GitLab Runner in `shell` execution mode.
GitLab Runner then executes build scripts as `gitlab-runner` user. GitLab Runner then executes build scripts as the `gitlab-runner` user.
1. Install [GitLab Runner](https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/#installation). 1. Install [GitLab Runner](https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/#installation).
1. During GitLab Runner installation select `shell` as method of executing build scripts or use command: 1. During GitLab Runner installation select `shell` as method of executing build scripts or use command:
```bash ```bash
$ sudo gitlab-runner register -n \ $ sudo gitlab-ci-multi-runner register -n \
--url https://gitlab.com/ci \ --url https://gitlab.com/ci \
--token RUNNER_TOKEN \ --registration-token REGISTRATION_TOKEN \
--executor shell --executor shell
--description "My Runner" --description "My Runner"
``` ```
...@@ -70,16 +71,18 @@ GitLab Runner then executes build scripts as `gitlab-runner` user. ...@@ -70,16 +71,18 @@ GitLab Runner then executes build scripts as `gitlab-runner` user.
5. You can now use `docker` command and install `docker-compose` if needed. 5. You can now use `docker` command and install `docker-compose` if needed.
6. However, by adding `gitlab-runner` to `docker` group you are effectively granting `gitlab-runner` full root permissions. > **Note:**
For more information please checkout [On Docker security: `docker` group considered harmful](https://www.andreas-jung.com/contents/on-docker-security-docker-group-considered-harmful). * By adding `gitlab-runner` to the `docker` group you are effectively granting `gitlab-runner` full root permissions.
For more information please read [On Docker security: `docker` group considered harmful](https://www.andreas-jung.com/contents/on-docker-security-docker-group-considered-harmful).
## 2. Use docker-in-docker executor ### Use docker-in-docker executor
The second approach is to use the special Docker image with all tools installed The second approach is to use the special docker-in-docker (dind)
[Docker image](https://hub.docker.com/_/docker/) with all tools installed
(`docker` and `docker-compose`) and run the build script in context of that (`docker` and `docker-compose`) and run the build script in context of that
image in privileged mode. image in privileged mode.
In order to do that follow the steps: In order to do that, follow the steps:
1. Install [GitLab Runner](https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/#installation). 1. Install [GitLab Runner](https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/#installation).
...@@ -87,9 +90,9 @@ In order to do that follow the steps: ...@@ -87,9 +90,9 @@ In order to do that follow the steps:
mode: mode:
```bash ```bash
sudo gitlab-runner register -n \ sudo gitlab-ci-multi-runner register -n \
--url https://gitlab.com/ci \ --url https://gitlab.com/ci \
--token RUNNER_TOKEN \ --registration-token REGISTRATION_TOKEN \
--executor docker \ --executor docker \
--description "My Docker Runner" \ --description "My Docker Runner" \
--docker-image "docker:latest" \ --docker-image "docker:latest" \
...@@ -119,11 +122,7 @@ In order to do that follow the steps: ...@@ -119,11 +122,7 @@ In order to do that follow the steps:
Insecure = false Insecure = false
``` ```
If you want to use the Shared Runners available on your GitLab CE/EE 1. You can now use `docker` in the build script (note the inclusion of the `docker:dind` service):
installation in order to build Docker images, then make sure that your
Shared Runners configuration has the `privileged` mode set to `true`.
1. You can now use `docker` from build script:
```yaml ```yaml
image: docker:latest image: docker:latest
...@@ -141,14 +140,177 @@ In order to do that follow the steps: ...@@ -141,14 +140,177 @@ In order to do that follow the steps:
- docker run my-docker-image /script/to/run/tests - docker run my-docker-image /script/to/run/tests
``` ```
1. However, by enabling `--docker-privileged` you are effectively disabling all Docker-in-Docker works well, and is the recommended configuration, but it is not without its own challenges:
the security mechanisms of containers and exposing your host to privilege * By enabling `--docker-privileged`, you are effectively disabling all of
escalation which can lead to container breakout. the security mechanisms of containers and exposing your host to privilege
escalation which can lead to container breakout. For more information, check out the official Docker documentation on
For more information, check out the official Docker documentation on [Runtime privilege and Linux capabilities][docker-cap].
[Runtime privilege and Linux capabilities][docker-cap]. * Using docker-in-docker, each build is in a clean environment without the past
history. Concurrent builds work fine because every build gets it's own instance of docker engine so they won't conflict with each other. But this also means builds can be slower because there's no caching of layers.
* By default, `docker:dind` uses `--storage-driver vfs` which is the slowest form
offered.
An example project using this approach can be found here: https://gitlab.com/gitlab-examples/docker. An example project using this approach can be found here: https://gitlab.com/gitlab-examples/docker.
### Use Docker socket binding
The third approach is to bind-mount `/var/run/docker.sock` into the container so that docker is available in the context of that image.
In order to do that, follow the steps:
1. Install [GitLab Runner](https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/#installation).
1. Register GitLab Runner from the command line to use `docker` and share `/var/run/docker.sock`:
```bash
sudo gitlab-ci-multi-runner register -n \
--url https://gitlab.com/ci \
--registration-token REGISTRATION_TOKEN \
--executor docker \
--description "My Docker Runner" \
--docker-image "docker:latest" \
--docker-volumes /var/run/docker.sock:/var/run/docker.sock
```
The above command will register a new Runner to use the special
`docker:latest` image which is provided by Docker. **Notice that it's using
the Docker daemon of the Runner itself, and any containers spawned by docker commands will be siblings of the Runner rather than children of the runner.** This may have complications and limitations that are unsuitable for your workflow.
The above command will create a `config.toml` entry similar to this:
```
[[runners]]
url = "https://gitlab.com/ci"
token = REGISTRATION_TOKEN
executor = "docker"
[runners.docker]
tls_verify = false
image = "docker:latest"
privileged = false
disable_cache = false
volumes = ["/var/run/docker.sock", "/cache"]
[runners.cache]
Insecure = false
```
1. You can now use `docker` in the build script (note that you don't need to include the `docker:dind` service as when using the Docker in Docker executor):
```yaml
image: docker:latest
before_script:
- docker info
build:
stage: build
script:
- docker build -t my-docker-image .
- docker run my-docker-image /script/to/run/tests
```
While the above method avoids using Docker in privileged mode, you should be aware of the following implications:
* By sharing the docker daemon, you are effectively disabling all
the security mechanisms of containers and exposing your host to privilege
escalation which can lead to container breakout. For example, if a project
ran `docker rm -f $(docker ps -a -q)` it would remove the GitLab Runner
containers.
* Concurrent builds may not work; if your tests
create containers with specific names, they may conflict with each other.
* Sharing files and directories from the source repo into containers may not
work as expected since volume mounting is done in the context of the host
machine, not the build container.
e.g. `docker run --rm -t -i -v $(pwd)/src:/home/app/src test-image:latest run_app_tests`
## Using the GitLab Container Registry
> **Note:**
This feature requires GitLab 8.8 and GitLab Runner 1.2.
Once you've built a Docker image, you can push it up to the built-in [GitLab Container Registry](../../container_registry/README.md). For example, if you're using
docker-in-docker on your runners, this is how your `.gitlab-ci.yml` could look:
```yaml
build:
image: docker:latest
services:
- docker:dind
stage: build
script:
- docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN registry.example.com
- docker build -t registry.example.com/group/project:latest .
- docker push registry.example.com/group/project:latest
```
You have to use the credentials of the special `gitlab-ci-token` user with its
password stored in `$CI_BUILD_TOKEN` in order to push to the Registry connected
to your project. This allows you to automate building and deployment of your
Docker images.
Here's a more elaborate example that splits up the tasks into 4 pipeline stages,
including two tests that run in parallel. The build is stored in the container
registry and used by subsequent stages, downloading the image
when needed. Changes to `master` also get tagged as `latest` and deployed using
an application-specific deploy script:
```yaml
image: docker:latest
services:
- docker:dind
stages:
- build
- test
- release
- deploy
variables:
CONTAINER_TEST_IMAGE: registry.example.com/my-group/my-project:$CI_BUILD_REF_NAME
CONTAINER_RELEASE_IMAGE: registry.example.com/my-group/my-project:latest
before_script:
- docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN registry.example.com
build:
stage: build
script:
- docker build --pull -t $CONTAINER_TEST_IMAGE .
- docker push $CONTAINER_TEST_IMAGE
test1:
stage: test
script:
- docker pull $CONTAINER_TEST_IMAGE
- docker run $CONTAINER_TEST_IMAGE /script/to/run/tests
test2:
stage: test
script:
- docker pull $CONTAINER_TEST_IMAGE
- docker run $CONTAINER_TEST_IMAGE /script/to/run/another/test
release-image:
stage: release
script:
- docker pull $CONTAINER_TEST_IMAGE
- docker tag $CONTAINER_TEST_IMAGE $CONTAINER_RELEASE_IMAGE
- docker push $CONTAINER_RELEASE_IMAGE
only:
- master
deploy:
stage: deploy
script:
- ./deploy.sh
only:
- master
```
Some things you should be aware of when using the Container Registry:
* You must log in to the container registry before running commands. Putting this in `before_script` will run it before each build job.
* Using `docker build --pull` makes sure that Docker fetches any changes to base images before building just in case your cache is stale. It takes slightly longer, but means you don’t get stuck without security patches to base images.
* Doing an explicit `docker pull` before each `docker run` makes sure to fetch the latest image that was just built. This is especially important if you are using multiple runners that cache images locally. Using the git SHA in your image tag makes this less necessary since each build will be unique and you shouldn't ever have a stale image, but it's still possible if you re-build a given commit after a dependency has changed.
* You don't want to build directly to `latest` in case there are multiple builds happening simultaneously.
[docker-in-docker]: https://blog.docker.com/2013/09/docker-can-now-run-within-docker/ [docker-in-docker]: https://blog.docker.com/2013/09/docker-can-now-run-within-docker/
[docker-cap]: https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities [docker-cap]: https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities
...@@ -23,7 +23,7 @@ To use GitLab Runner with docker you need to register a new runner to use the ...@@ -23,7 +23,7 @@ To use GitLab Runner with docker you need to register a new runner to use the
`docker` executor: `docker` executor:
```bash ```bash
gitlab-runner register \ gitlab-ci-multi-runner register \
--url "https://gitlab.com/" \ --url "https://gitlab.com/" \
--registration-token "PROJECT_REGISTRATION_TOKEN" \ --registration-token "PROJECT_REGISTRATION_TOKEN" \
--description "docker-ruby-2.1" \ --description "docker-ruby-2.1" \
......
...@@ -263,10 +263,10 @@ terminal execute: ...@@ -263,10 +263,10 @@ terminal execute:
```bash ```bash
# Check using docker executor # Check using docker executor
gitlab-runner exec docker test:app gitlab-ci-multi-runner exec docker test:app
# Check using shell executor # Check using shell executor
gitlab-runner exec shell test:app gitlab-ci-multi-runner exec shell test:app
``` ```
## Example project ## Example project
......
...@@ -63,7 +63,7 @@ instance. ...@@ -63,7 +63,7 @@ instance.
Now simply register the runner as any runner: Now simply register the runner as any runner:
``` ```
sudo gitlab-runner register sudo gitlab-ci-multi-runner register
``` ```
Shared runners are enabled by default as of GitLab 8.2, but can be disabled with the Shared runners are enabled by default as of GitLab 8.2, but can be disabled with the
...@@ -93,7 +93,7 @@ setup a specific runner for this project. ...@@ -93,7 +93,7 @@ setup a specific runner for this project.
To register the runner, run the command below and follow instructions: To register the runner, run the command below and follow instructions:
``` ```
sudo gitlab-runner register sudo gitlab-ci-multi-runner register
``` ```
### Making an existing Shared Runner Specific ### Making an existing Shared Runner Specific
......
...@@ -79,27 +79,8 @@ delete them. ...@@ -79,27 +79,8 @@ delete them.
This feature requires GitLab 8.8 and GitLab Runner 1.2. This feature requires GitLab 8.8 and GitLab Runner 1.2.
Make sure that your GitLab Runner is configured to allow building docker images. Make sure that your GitLab Runner is configured to allow building docker images.
You have to check the [Using Docker Build documentation](../../ci/docker/using_docker_build.md). You have to check the [Using Docker Build documentation](../ci/docker/using_docker_build.md).
Then see the CI documentation on [Using the GitLab Container Registry](../ci/docker/using_docker_build.md#using-the-gitlab-container-registry).
You can use [docker:dind](https://hub.docker.com/_/docker/) to build your images,
and this is how `.gitlab-ci.yml` should look like:
```
build_image:
image: docker:git
services:
- docker:dind
stage: build
script:
- docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN registry.example.com
- docker build -t registry.example.com/group/project:latest .
- docker push registry.example.com/group/project:latest
```
You have to use the credentials of the special `gitlab-ci-token` user with its
password stored in `$CI_BUILD_TOKEN` in order to push to the Registry connected
to your project. This allows you to automated building and deployment of your
Docker images.
## Limitations ## Limitations
......
...@@ -98,14 +98,15 @@ def #{name}(#{args_signature}) ...@@ -98,14 +98,15 @@ def #{name}(#{args_signature})
if trans if trans
start = Time.now start = Time.now
cpu_start = Gitlab::Metrics::System.cpu_time
retval = super retval = super
duration = (Time.now - start) * 1000.0 duration = (Time.now - start) * 1000.0
if duration >= Gitlab::Metrics.method_call_threshold if duration >= Gitlab::Metrics.method_call_threshold
trans.increment(:method_duration, duration) cpu_duration = Gitlab::Metrics::System.cpu_time - cpu_start
trans.add_metric(Gitlab::Metrics::Instrumentation::SERIES, trans.add_metric(Gitlab::Metrics::Instrumentation::SERIES,
{ duration: duration }, { duration: duration, cpu_duration: cpu_duration },
method: #{label.inspect}) method: #{label.inspect})
end end
......
module Banzai module Banzai
module Pipeline module Pipeline
class DescriptionPipeline < FullPipeline class DescriptionPipeline < FullPipeline
WHITELIST = Banzai::Filter::SanitizationFilter::LIMITED.deep_dup.merge(
elements: Banzai::Filter::SanitizationFilter::LIMITED[:elements] - %w(pre code img ol ul li)
)
def self.transform_context(context) def self.transform_context(context)
super(context).merge( super(context).merge(
# SanitizationFilter # SanitizationFilter
whitelist: whitelist whitelist: WHITELIST
) )
end end
private
def self.whitelist
# Descriptions are more heavily sanitized, allowing only a few elements.
# See http://git.io/vkuAN
whitelist = Banzai::Filter::SanitizationFilter::LIMITED
whitelist[:elements] -= %w(pre code img ol ul li)
whitelist
end
end end
end end
end end
...@@ -2,6 +2,8 @@ module Ci ...@@ -2,6 +2,8 @@ module Ci
class GitlabCiYamlProcessor class GitlabCiYamlProcessor
class ValidationError < StandardError; end class ValidationError < StandardError; end
include Gitlab::Ci::Config::Node::ValidationHelpers
DEFAULT_STAGES = %w(build test deploy) DEFAULT_STAGES = %w(build test deploy)
DEFAULT_STAGE = 'test' DEFAULT_STAGE = 'test'
ALLOWED_YAML_KEYS = [:before_script, :after_script, :image, :services, :types, :stages, :variables, :cache] ALLOWED_YAML_KEYS = [:before_script, :after_script, :image, :services, :types, :stages, :variables, :cache]
...@@ -11,10 +13,12 @@ module Ci ...@@ -11,10 +13,12 @@ module Ci
ALLOWED_CACHE_KEYS = [:key, :untracked, :paths] ALLOWED_CACHE_KEYS = [:key, :untracked, :paths]
ALLOWED_ARTIFACTS_KEYS = [:name, :untracked, :paths, :when, :expire_in] ALLOWED_ARTIFACTS_KEYS = [:name, :untracked, :paths, :when, :expire_in]
attr_reader :before_script, :after_script, :image, :services, :path, :cache attr_reader :after_script, :image, :services, :path, :cache
def initialize(config, path = nil) def initialize(config, path = nil)
@config = Gitlab::Ci::Config.new(config).to_hash @ci_config = Gitlab::Ci::Config.new(config)
@config = @ci_config.to_hash
@path = path @path = path
initial_parsing initial_parsing
...@@ -52,7 +56,6 @@ module Ci ...@@ -52,7 +56,6 @@ module Ci
private private
def initial_parsing def initial_parsing
@before_script = @config[:before_script] || []
@after_script = @config[:after_script] @after_script = @config[:after_script]
@image = @config[:image] @image = @config[:image]
@services = @config[:services] @services = @config[:services]
...@@ -80,7 +83,7 @@ module Ci ...@@ -80,7 +83,7 @@ module Ci
{ {
stage_idx: stages.index(job[:stage]), stage_idx: stages.index(job[:stage]),
stage: job[:stage], stage: job[:stage],
commands: [job[:before_script] || @before_script, job[:script]].flatten.join("\n"), commands: [job[:before_script] || [@ci_config.before_script], job[:script]].flatten.compact.join("\n"),
tag_list: job[:tags] || [], tag_list: job[:tags] || [],
name: name, name: name,
only: job[:only], only: job[:only],
...@@ -99,6 +102,10 @@ module Ci ...@@ -99,6 +102,10 @@ module Ci
end end
def validate! def validate!
unless @ci_config.valid?
raise ValidationError, @ci_config.errors.first
end
validate_global! validate_global!
@jobs.each do |name, job| @jobs.each do |name, job|
...@@ -109,10 +116,6 @@ module Ci ...@@ -109,10 +116,6 @@ module Ci
end end
def validate_global! def validate_global!
unless validate_array_of_strings(@before_script)
raise ValidationError, "before_script should be an array of strings"
end
unless @after_script.nil? || validate_array_of_strings(@after_script) unless @after_script.nil? || validate_array_of_strings(@after_script)
raise ValidationError, "after_script should be an array of strings" raise ValidationError, "after_script should be an array of strings"
end end
...@@ -304,28 +307,6 @@ module Ci ...@@ -304,28 +307,6 @@ module Ci
end end
end end
def validate_duration(value)
value.is_a?(String) && ChronicDuration.parse(value)
rescue ChronicDuration::DurationParseError
false
end
def validate_array_of_strings(values)
values.is_a?(Array) && values.all? { |value| validate_string(value) }
end
def validate_variables(variables)
variables.is_a?(Hash) && variables.all? { |key, value| validate_string(key) && validate_string(value) }
end
def validate_string(value)
value.is_a?(String) || value.is_a?(Symbol)
end
def validate_boolean(value)
value.in?([true, false])
end
def process?(only_params, except_params, ref, tag, trigger_request) def process?(only_params, except_params, ref, tag, trigger_request)
if only_params.present? if only_params.present?
return false unless matching?(only_params, ref, tag, trigger_request) return false unless matching?(only_params, ref, tag, trigger_request)
......
module Gitlab module Gitlab
module Ci module Ci
##
# Base GitLab CI Configuration facade
#
class Config class Config
class LoaderError < StandardError; end delegate :valid?, :errors, to: :@global
##
# Temporary delegations that should be removed after refactoring
#
delegate :before_script, to: :@global
def initialize(config) def initialize(config)
loader = Loader.new(config) @config = Loader.new(config).load!
@config = loader.load!
@global = Node::Global.new(@config)
@global.process!
end end
def to_hash def to_hash
......
module Gitlab
module Ci
class Config
module Node
##
# This mixin is responsible for adding DSL, which purpose is to
# simplifly process of adding child nodes.
#
# This can be used only if parent node is a configuration entry that
# holds a hash as a configuration value, for example:
#
# job:
# script: ...
# artifacts: ...
#
module Configurable
extend ActiveSupport::Concern
def allowed_nodes
self.class.allowed_nodes || {}
end
private
def prevalidate!
unless @value.is_a?(Hash)
@errors << 'should be a configuration entry with hash value'
end
end
def create_node(key, factory)
factory.with(value: @value[key])
factory.nullify! unless @value.has_key?(key)
factory.create!
end
class_methods do
def allowed_nodes
Hash[@allowed_nodes.map { |key, factory| [key, factory.dup] }]
end
private
def allow_node(symbol, entry_class, metadata)
factory = Node::Factory.new(entry_class)
.with(description: metadata[:description])
define_method(symbol) do
raise Entry::InvalidError unless valid?
@nodes[symbol].try(:value)
end
(@allowed_nodes ||= {}).merge!(symbol => factory)
end
end
end
end
end
end
end
module Gitlab
module Ci
class Config
module Node
##
# Base abstract class for each configuration entry node.
#
class Entry
class InvalidError < StandardError; end
attr_accessor :description
def initialize(value)
@value = value
@nodes = {}
@errors = []
prevalidate!
end
def process!
return if leaf?
return unless valid?
compose!
nodes.each(&:process!)
nodes.each(&:validate!)
end
def nodes
@nodes.values
end
def valid?
errors.none?
end
def leaf?
allowed_nodes.none?
end
def errors
@errors + nodes.map(&:errors).flatten
end
def allowed_nodes
{}
end
def validate!
raise NotImplementedError
end
def value
raise NotImplementedError
end
private
def prevalidate!
end
def compose!
allowed_nodes.each do |key, essence|
@nodes[key] = create_node(key, essence)
end
end
def create_node(key, essence)
raise NotImplementedError
end
end
end
end
end
end
module Gitlab
module Ci
class Config
module Node
##
# Factory class responsible for fabricating node entry objects.
#
# It uses Fluent Interface pattern to set all necessary attributes.
#
class Factory
class InvalidFactory < StandardError; end
def initialize(entry_class)
@entry_class = entry_class
@attributes = {}
end
def with(attributes)
@attributes.merge!(attributes)
self
end
def nullify!
@entry_class = Node::Null
self
end
def create!
raise InvalidFactory unless @attributes.has_key?(:value)
@entry_class.new(@attributes[:value]).tap do |entry|
entry.description = @attributes[:description]
end
end
end
end
end
end
end
module Gitlab
module Ci
class Config
module Node
##
# This class represents a global entry - root node for entire
# GitLab CI Configuration file.
#
class Global < Entry
include Configurable
allow_node :before_script, Script,
description: 'Script that will be executed before each job.'
end
end
end
end
end
module Gitlab
module Ci
class Config
module Node
##
# This class represents a configuration entry that is not being used
# in configuration file.
#
# This implements Null Object pattern.
#
class Null < Entry
def value
nil
end
def validate!
nil
end
def method_missing(*)
nil
end
end
end
end
end
end
module Gitlab
module Ci
class Config
module Node
##
# Entry that represents a script.
#
# Each element in the value array is a command that will be executed
# by GitLab Runner. Currently we concatenate these commands with
# new line character as a separator, what is compatible with
# implementation in Runner.
#
class Script < Entry
include ValidationHelpers
def value
@value.join("\n")
end
def validate!
unless validate_array_of_strings(@value)
@errors << 'before_script should be an array of strings'
end
end
end
end
end
end
end
module Gitlab
module Ci
class Config
module Node
module ValidationHelpers
private
def validate_duration(value)
value.is_a?(String) && ChronicDuration.parse(value)
rescue ChronicDuration::DurationParseError
false
end
def validate_array_of_strings(values)
values.is_a?(Array) && values.all? { |value| validate_string(value) }
end
def validate_variables(variables)
variables.is_a?(Hash) &&
variables.all? { |key, value| validate_string(key) && validate_string(value) }
end
def validate_string(value)
value.is_a?(String) || value.is_a?(Symbol)
end
def validate_boolean(value)
value.in?([true, false])
end
end
end
end
end
end
...@@ -150,12 +150,15 @@ module Gitlab ...@@ -150,12 +150,15 @@ module Gitlab
if trans if trans
start = Time.now start = Time.now
cpu_start = Gitlab::Metrics::System.cpu_time
retval = super retval = super
duration = (Time.now - start) * 1000.0 duration = (Time.now - start) * 1000.0
if duration >= Gitlab::Metrics.method_call_threshold if duration >= Gitlab::Metrics.method_call_threshold
cpu_duration = Gitlab::Metrics::System.cpu_time - cpu_start
trans.add_metric(Gitlab::Metrics::Instrumentation::SERIES, trans.add_metric(Gitlab::Metrics::Instrumentation::SERIES,
{ duration: duration }, { duration: duration, cpu_duration: cpu_duration },
method: #{label.inspect}) method: #{label.inspect})
end end
......
require 'spec_helper'
describe Gitlab::Ci::Config::Node::Configurable do
let(:node) { Class.new }
before do
node.include(described_class)
end
describe 'allowed nodes' do
before do
node.class_eval do
allow_node :object, Object, description: 'test object'
end
end
describe '#allowed_nodes' do
it 'has valid allowed nodes' do
expect(node.allowed_nodes).to include :object
end
it 'creates a node factory' do
expect(node.allowed_nodes[:object])
.to be_an_instance_of Gitlab::Ci::Config::Node::Factory
end
it 'returns a duplicated factory object' do
first_factory = node.allowed_nodes[:object]
second_factory = node.allowed_nodes[:object]
expect(first_factory).not_to be_equal(second_factory)
end
end
end
end
require 'spec_helper'
describe Gitlab::Ci::Config::Node::Factory do
describe '#create!' do
let(:factory) { described_class.new(entry_class) }
let(:entry_class) { Gitlab::Ci::Config::Node::Script }
context 'when value setting value' do
it 'creates entry with valid value' do
entry = factory
.with(value: ['ls', 'pwd'])
.create!
expect(entry.value).to eq "ls\npwd"
end
context 'when setting description' do
it 'creates entry with description' do
entry = factory
.with(value: ['ls', 'pwd'])
.with(description: 'test description')
.create!
expect(entry.value).to eq "ls\npwd"
expect(entry.description).to eq 'test description'
end
end
end
context 'when not setting value' do
it 'raises error' do
expect { factory.create! }.to raise_error(
Gitlab::Ci::Config::Node::Factory::InvalidFactory
)
end
end
context 'when creating a null entry' do
it 'creates a null entry' do
entry = factory
.with(value: nil)
.nullify!
.create!
expect(entry).to be_an_instance_of Gitlab::Ci::Config::Node::Null
end
end
end
end
require 'spec_helper'
describe Gitlab::Ci::Config::Node::Global do
let(:global) { described_class.new(hash) }
describe '#allowed_nodes' do
it 'can contain global config keys' do
expect(global.allowed_nodes).to include :before_script
end
it 'returns a hash' do
expect(global.allowed_nodes).to be_a Hash
end
end
context 'when hash is valid' do
let(:hash) do
{ before_script: ['ls', 'pwd'] }
end
describe '#process!' do
before { global.process! }
it 'creates nodes hash' do
expect(global.nodes).to be_an Array
end
it 'creates node object for each entry' do
expect(global.nodes.count).to eq 1
end
it 'creates node object using valid class' do
expect(global.nodes.first)
.to be_an_instance_of Gitlab::Ci::Config::Node::Script
end
it 'sets correct description for nodes' do
expect(global.nodes.first.description)
.to eq 'Script that will be executed before each job.'
end
end
describe '#leaf?' do
it 'is not leaf' do
expect(global).not_to be_leaf
end
end
describe '#before_script' do
context 'when processed' do
before { global.process! }
it 'returns correct script' do
expect(global.before_script).to eq "ls\npwd"
end
end
context 'when not processed' do
it 'returns nil' do
expect(global.before_script).to be nil
end
end
end
end
context 'when hash is not valid' do
before { global.process! }
let(:hash) do
{ before_script: 'ls' }
end
describe '#valid?' do
it 'is not valid' do
expect(global).not_to be_valid
end
end
describe '#errors' do
it 'reports errors from child nodes' do
expect(global.errors)
.to include 'before_script should be an array of strings'
end
end
describe '#before_script' do
it 'raises error' do
expect { global.before_script }.to raise_error(
Gitlab::Ci::Config::Node::Entry::InvalidError
)
end
end
end
context 'when value is not a hash' do
let(:hash) { [] }
describe '#valid?' do
it 'is not valid' do
expect(global).not_to be_valid
end
end
end
end
require 'spec_helper'
describe Gitlab::Ci::Config::Node::Null do
let(:entry) { described_class.new(nil) }
describe '#leaf?' do
it 'is leaf node' do
expect(entry).to be_leaf
end
end
describe '#any_method' do
it 'responds with nil' do
expect(entry.any_method).to be nil
end
end
describe '#value' do
it 'returns nil' do
expect(entry.value).to be nil
end
end
end
require 'spec_helper'
describe Gitlab::Ci::Config::Node::Script do
let(:entry) { described_class.new(value) }
describe '#validate!' do
before { entry.validate! }
context 'when entry value is correct' do
let(:value) { ['ls', 'pwd'] }
describe '#value' do
it 'returns concatenated command' do
expect(entry.value).to eq "ls\npwd"
end
end
describe '#errors' do
it 'does not append errors' do
expect(entry.errors).to be_empty
end
end
describe '#valid?' do
it 'is valid' do
expect(entry).to be_valid
end
end
end
context 'when entry value is not correct' do
let(:value) { 'ls' }
describe '#errors' do
it 'saves errors' do
expect(entry.errors)
.to include /should be an array of strings/
end
end
describe '#valid?' do
it 'is not valid' do
expect(entry).not_to be_valid
end
end
end
end
end
...@@ -29,9 +29,20 @@ describe Gitlab::Ci::Config do ...@@ -29,9 +29,20 @@ describe Gitlab::Ci::Config do
expect(config.to_hash).to eq hash expect(config.to_hash).to eq hash
end end
describe '#valid?' do
it 'is valid' do
expect(config).to be_valid
end
it 'has no errors' do
expect(config.errors).to be_empty
end
end
end end
context 'when config is invalid' do context 'when config is invalid' do
context 'when yml is incorrect' do
let(:yml) { '// invalid' } let(:yml) { '// invalid' }
describe '.new' do describe '.new' do
...@@ -43,5 +54,20 @@ describe Gitlab::Ci::Config do ...@@ -43,5 +54,20 @@ describe Gitlab::Ci::Config do
end end
end end
end end
context 'when config logic is incorrect' do
let(:yml) { 'before_script: "ls"' }
describe '#valid?' do
it 'is not valid' do
expect(config).not_to be_valid
end
it 'has errors' do
expect(config.errors).not_to be_empty
end
end
end
end
end end
end end
...@@ -57,7 +57,7 @@ describe Gitlab::Metrics::Instrumentation do ...@@ -57,7 +57,7 @@ describe Gitlab::Metrics::Instrumentation do
and_return(transaction) and_return(transaction)
expect(transaction).to receive(:add_metric). expect(transaction).to receive(:add_metric).
with(described_class::SERIES, an_instance_of(Hash), with(described_class::SERIES, hash_including(:duration, :cpu_duration),
method: 'Dummy.foo') method: 'Dummy.foo')
@dummy.foo @dummy.foo
...@@ -137,7 +137,7 @@ describe Gitlab::Metrics::Instrumentation do ...@@ -137,7 +137,7 @@ describe Gitlab::Metrics::Instrumentation do
and_return(transaction) and_return(transaction)
expect(transaction).to receive(:add_metric). expect(transaction).to receive(:add_metric).
with(described_class::SERIES, an_instance_of(Hash), with(described_class::SERIES, hash_including(:duration, :cpu_duration),
method: 'Dummy#bar') method: 'Dummy#bar')
@dummy.new.bar @dummy.new.bar
......
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