summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKamil Trzcinski <ayufan@ayufan.eu>2016-06-14 14:49:59 +0200
committerKamil Trzcinski <ayufan@ayufan.eu>2016-06-14 14:49:59 +0200
commit3f5819fb7aa367ac302fe7d62857cfd604b5792f (patch)
tree68c1d6da2f7a24ba5e62d7077f4d30e8176882c8
parent1b62b86fdd1ad98e680c534f05fb32ff6e23fca4 (diff)
parent4b964011cfd6861403726248574f5926edc5d495 (diff)
downloadgitlab-ce-3f5819fb7aa367ac302fe7d62857cfd604b5792f.tar.gz
Merge remote-tracking branch 'origin/master' into artifacts-expire-date
-rw-r--r--CHANGELOG1
-rw-r--r--doc/ci/docker/using_docker_build.md224
-rw-r--r--doc/ci/docker/using_docker_images.md2
-rw-r--r--doc/ci/examples/php.md4
-rw-r--r--doc/ci/runners/README.md6
-rw-r--r--doc/container_registry/README.md23
-rw-r--r--doc/development/instrumentation.md11
-rw-r--r--lib/banzai/pipeline/description_pipeline.rb17
-rw-r--r--lib/ci/gitlab_ci_yaml_processor.rb41
-rw-r--r--lib/gitlab/ci/config.rb16
-rw-r--r--lib/gitlab/ci/config/node/configurable.rb61
-rw-r--r--lib/gitlab/ci/config/node/entry.rb77
-rw-r--r--lib/gitlab/ci/config/node/factory.rb39
-rw-r--r--lib/gitlab/ci/config/node/global.rb18
-rw-r--r--lib/gitlab/ci/config/node/null.rb27
-rw-r--r--lib/gitlab/ci/config/node/script.rb29
-rw-r--r--lib/gitlab/ci/config/node/validation_helpers.rb34
-rw-r--r--lib/gitlab/metrics/instrumentation.rb11
-rw-r--r--spec/lib/gitlab/ci/config/node/configurable_spec.rb35
-rw-r--r--spec/lib/gitlab/ci/config/node/factory_spec.rb49
-rw-r--r--spec/lib/gitlab/ci/config/node/global_spec.rb104
-rw-r--r--spec/lib/gitlab/ci/config/node/null_spec.rb23
-rw-r--r--spec/lib/gitlab/ci/config/node/script_spec.rb48
-rw-r--r--spec/lib/gitlab/ci/config_spec.rb42
-rw-r--r--spec/lib/gitlab/metrics/instrumentation_spec.rb4
25 files changed, 824 insertions, 122 deletions
diff --git a/CHANGELOG b/CHANGELOG
index 2aed8eb322b..e71a154d1d5 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -77,6 +77,7 @@ v 8.9.0 (unreleased)
- All classes in the Banzai::ReferenceParser namespace are now instrumented
- Remove deprecated issues_tracker and issues_tracker_id from project model
- Allow users to create confidential issues in private projects
+ - Measure CPU time for instrumented methods
v 8.8.5 (unreleased)
- Ensure branch cleanup regardless of whether the GitHub import process succeeds
diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md
index ca52a483a59..7f83f846454 100644
--- a/doc/ci/docker/using_docker_build.md
+++ b/doc/ci/docker/using_docker_build.md
@@ -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 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. run test against created image,
-1. push image to remote registry,
-1. deploy server from pushed image
+1. create an application image,
+1. run tests against the created image,
+1. push image to a remote registry, and
+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
$ docker build -t my-image dockerfiles/
$ docker run my-docker-image /script/to/run/tests
@@ -19,24 +19,25 @@ $ docker tag my-image 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 running GitLab Runner in privileged mode which can be harmful when untrusted code is run.**
+This requires special configuration of GitLab Runner to enable `docker` support during builds.
-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.
-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. During GitLab Runner installation select `shell` as method of executing build scripts or use command:
```bash
- $ sudo gitlab-runner register -n \
+ $ sudo gitlab-ci-multi-runner register -n \
--url https://gitlab.com/ci \
- --token RUNNER_TOKEN \
+ --registration-token REGISTRATION_TOKEN \
--executor shell
--description "My Runner"
```
@@ -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.
-6. However, by adding `gitlab-runner` to `docker` group you are effectively granting `gitlab-runner` full root permissions.
-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).
+> **Note:**
+* 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
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).
@@ -87,9 +90,9 @@ In order to do that follow the steps:
mode:
```bash
- sudo gitlab-runner register -n \
+ sudo gitlab-ci-multi-runner register -n \
--url https://gitlab.com/ci \
- --token RUNNER_TOKEN \
+ --registration-token REGISTRATION_TOKEN \
--executor docker \
--description "My Docker Runner" \
--docker-image "docker:latest" \
@@ -119,11 +122,7 @@ In order to do that follow the steps:
Insecure = false
```
- If you want to use the Shared Runners available on your GitLab CE/EE
- 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:
+1. You can now use `docker` in the build script (note the inclusion of the `docker:dind` service):
```yaml
image: docker:latest
@@ -141,14 +140,177 @@ In order to do that follow the steps:
- docker run my-docker-image /script/to/run/tests
```
-1. However, by enabling `--docker-privileged` you are effectively disabling all
- 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
- [Runtime privilege and Linux capabilities][docker-cap].
+Docker-in-Docker works well, and is the recommended configuration, but it is not without its own challenges:
+* By enabling `--docker-privileged`, you are effectively disabling all of
+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
+[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.
+### 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-cap]: https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities
diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md
index 56ac2195c49..a849905ac6b 100644
--- a/doc/ci/docker/using_docker_images.md
+++ b/doc/ci/docker/using_docker_images.md
@@ -23,7 +23,7 @@ To use GitLab Runner with docker you need to register a new runner to use the
`docker` executor:
```bash
-gitlab-runner register \
+gitlab-ci-multi-runner register \
--url "https://gitlab.com/" \
--registration-token "PROJECT_REGISTRATION_TOKEN" \
--description "docker-ruby-2.1" \
diff --git a/doc/ci/examples/php.md b/doc/ci/examples/php.md
index 26953014502..17e1c64bb8a 100644
--- a/doc/ci/examples/php.md
+++ b/doc/ci/examples/php.md
@@ -263,10 +263,10 @@ terminal execute:
```bash
# Check using docker executor
-gitlab-runner exec docker test:app
+gitlab-ci-multi-runner exec docker test:app
# Check using shell executor
-gitlab-runner exec shell test:app
+gitlab-ci-multi-runner exec shell test:app
```
## Example project
diff --git a/doc/ci/runners/README.md b/doc/ci/runners/README.md
index b42d7a62ebc..400784da617 100644
--- a/doc/ci/runners/README.md
+++ b/doc/ci/runners/README.md
@@ -63,10 +63,10 @@ instance.
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
`DISABLE SHARED RUNNERS` button. Previous versions of GitLab defaulted shared runners to
disabled.
@@ -93,7 +93,7 @@ setup a specific runner for this project.
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
diff --git a/doc/container_registry/README.md b/doc/container_registry/README.md
index 4df24ef13cc..1b465434498 100644
--- a/doc/container_registry/README.md
+++ b/doc/container_registry/README.md
@@ -79,27 +79,8 @@ delete them.
This feature requires GitLab 8.8 and GitLab Runner 1.2.
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 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.
+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).
## Limitations
diff --git a/doc/development/instrumentation.md b/doc/development/instrumentation.md
index 9168c70945a..50d2866ca46 100644
--- a/doc/development/instrumentation.md
+++ b/doc/development/instrumentation.md
@@ -97,15 +97,16 @@ def #{name}(#{args_signature})
trans = Gitlab::Metrics::Instrumentation.transaction
if trans
- start = Time.now
- retval = super
- duration = (Time.now - start) * 1000.0
+ start = Time.now
+ cpu_start = Gitlab::Metrics::System.cpu_time
+ retval = super
+ duration = (Time.now - start) * 1000.0
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,
- { duration: duration },
+ { duration: duration, cpu_duration: cpu_duration },
method: #{label.inspect})
end
diff --git a/lib/banzai/pipeline/description_pipeline.rb b/lib/banzai/pipeline/description_pipeline.rb
index f2395867658..042fb2e6e14 100644
--- a/lib/banzai/pipeline/description_pipeline.rb
+++ b/lib/banzai/pipeline/description_pipeline.rb
@@ -1,23 +1,16 @@
module Banzai
module Pipeline
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)
super(context).merge(
# SanitizationFilter
- whitelist: whitelist
+ whitelist: WHITELIST
)
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
diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb
index 76d84433cbe..e0b89cead06 100644
--- a/lib/ci/gitlab_ci_yaml_processor.rb
+++ b/lib/ci/gitlab_ci_yaml_processor.rb
@@ -2,6 +2,8 @@ module Ci
class GitlabCiYamlProcessor
class ValidationError < StandardError; end
+ include Gitlab::Ci::Config::Node::ValidationHelpers
+
DEFAULT_STAGES = %w(build test deploy)
DEFAULT_STAGE = 'test'
ALLOWED_YAML_KEYS = [:before_script, :after_script, :image, :services, :types, :stages, :variables, :cache]
@@ -11,10 +13,12 @@ module Ci
ALLOWED_CACHE_KEYS = [:key, :untracked, :paths]
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)
- @config = Gitlab::Ci::Config.new(config).to_hash
+ @ci_config = Gitlab::Ci::Config.new(config)
+ @config = @ci_config.to_hash
+
@path = path
initial_parsing
@@ -52,7 +56,6 @@ module Ci
private
def initial_parsing
- @before_script = @config[:before_script] || []
@after_script = @config[:after_script]
@image = @config[:image]
@services = @config[:services]
@@ -80,7 +83,7 @@ module Ci
{
stage_idx: stages.index(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] || [],
name: name,
only: job[:only],
@@ -99,6 +102,10 @@ module Ci
end
def validate!
+ unless @ci_config.valid?
+ raise ValidationError, @ci_config.errors.first
+ end
+
validate_global!
@jobs.each do |name, job|
@@ -109,10 +116,6 @@ module Ci
end
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)
raise ValidationError, "after_script should be an array of strings"
end
@@ -304,28 +307,6 @@ module Ci
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)
if only_params.present?
return false unless matching?(only_params, ref, tag, trigger_request)
diff --git a/lib/gitlab/ci/config.rb b/lib/gitlab/ci/config.rb
index ffe633d4b63..b48d3592f16 100644
--- a/lib/gitlab/ci/config.rb
+++ b/lib/gitlab/ci/config.rb
@@ -1,11 +1,21 @@
module Gitlab
module Ci
+ ##
+ # Base GitLab CI Configuration facade
+ #
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)
- loader = Loader.new(config)
- @config = loader.load!
+ @config = Loader.new(config).load!
+
+ @global = Node::Global.new(@config)
+ @global.process!
end
def to_hash
diff --git a/lib/gitlab/ci/config/node/configurable.rb b/lib/gitlab/ci/config/node/configurable.rb
new file mode 100644
index 00000000000..d60f87f3f94
--- /dev/null
+++ b/lib/gitlab/ci/config/node/configurable.rb
@@ -0,0 +1,61 @@
+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
diff --git a/lib/gitlab/ci/config/node/entry.rb b/lib/gitlab/ci/config/node/entry.rb
new file mode 100644
index 00000000000..52758a962f3
--- /dev/null
+++ b/lib/gitlab/ci/config/node/entry.rb
@@ -0,0 +1,77 @@
+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
diff --git a/lib/gitlab/ci/config/node/factory.rb b/lib/gitlab/ci/config/node/factory.rb
new file mode 100644
index 00000000000..787ca006f5a
--- /dev/null
+++ b/lib/gitlab/ci/config/node/factory.rb
@@ -0,0 +1,39 @@
+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
diff --git a/lib/gitlab/ci/config/node/global.rb b/lib/gitlab/ci/config/node/global.rb
new file mode 100644
index 00000000000..044603423d5
--- /dev/null
+++ b/lib/gitlab/ci/config/node/global.rb
@@ -0,0 +1,18 @@
+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
diff --git a/lib/gitlab/ci/config/node/null.rb b/lib/gitlab/ci/config/node/null.rb
new file mode 100644
index 00000000000..4f590f6bec8
--- /dev/null
+++ b/lib/gitlab/ci/config/node/null.rb
@@ -0,0 +1,27 @@
+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
diff --git a/lib/gitlab/ci/config/node/script.rb b/lib/gitlab/ci/config/node/script.rb
new file mode 100644
index 00000000000..5072bf0db7d
--- /dev/null
+++ b/lib/gitlab/ci/config/node/script.rb
@@ -0,0 +1,29 @@
+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
diff --git a/lib/gitlab/ci/config/node/validation_helpers.rb b/lib/gitlab/ci/config/node/validation_helpers.rb
new file mode 100644
index 00000000000..42ef60244ba
--- /dev/null
+++ b/lib/gitlab/ci/config/node/validation_helpers.rb
@@ -0,0 +1,34 @@
+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
diff --git a/lib/gitlab/metrics/instrumentation.rb b/lib/gitlab/metrics/instrumentation.rb
index 0f115893a15..ad9ce3fa442 100644
--- a/lib/gitlab/metrics/instrumentation.rb
+++ b/lib/gitlab/metrics/instrumentation.rb
@@ -149,13 +149,16 @@ module Gitlab
trans = Gitlab::Metrics::Instrumentation.transaction
if trans
- start = Time.now
- retval = super
- duration = (Time.now - start) * 1000.0
+ start = Time.now
+ cpu_start = Gitlab::Metrics::System.cpu_time
+ retval = super
+ duration = (Time.now - start) * 1000.0
if duration >= Gitlab::Metrics.method_call_threshold
+ cpu_duration = Gitlab::Metrics::System.cpu_time - cpu_start
+
trans.add_metric(Gitlab::Metrics::Instrumentation::SERIES,
- { duration: duration },
+ { duration: duration, cpu_duration: cpu_duration },
method: #{label.inspect})
end
diff --git a/spec/lib/gitlab/ci/config/node/configurable_spec.rb b/spec/lib/gitlab/ci/config/node/configurable_spec.rb
new file mode 100644
index 00000000000..47c68f96dc8
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/node/configurable_spec.rb
@@ -0,0 +1,35 @@
+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
diff --git a/spec/lib/gitlab/ci/config/node/factory_spec.rb b/spec/lib/gitlab/ci/config/node/factory_spec.rb
new file mode 100644
index 00000000000..d681aa32456
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/node/factory_spec.rb
@@ -0,0 +1,49 @@
+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
diff --git a/spec/lib/gitlab/ci/config/node/global_spec.rb b/spec/lib/gitlab/ci/config/node/global_spec.rb
new file mode 100644
index 00000000000..b1972172435
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/node/global_spec.rb
@@ -0,0 +1,104 @@
+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
diff --git a/spec/lib/gitlab/ci/config/node/null_spec.rb b/spec/lib/gitlab/ci/config/node/null_spec.rb
new file mode 100644
index 00000000000..36101c62462
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/node/null_spec.rb
@@ -0,0 +1,23 @@
+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
diff --git a/spec/lib/gitlab/ci/config/node/script_spec.rb b/spec/lib/gitlab/ci/config/node/script_spec.rb
new file mode 100644
index 00000000000..e4d6481f8a5
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/node/script_spec.rb
@@ -0,0 +1,48 @@
+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
diff --git a/spec/lib/gitlab/ci/config_spec.rb b/spec/lib/gitlab/ci/config_spec.rb
index 4d46abe520f..3871d939feb 100644
--- a/spec/lib/gitlab/ci/config_spec.rb
+++ b/spec/lib/gitlab/ci/config_spec.rb
@@ -29,17 +29,43 @@ describe Gitlab::Ci::Config do
expect(config.to_hash).to eq hash
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
context 'when config is invalid' do
- let(:yml) { '// invalid' }
-
- describe '.new' do
- it 'raises error' do
- expect { config }.to raise_error(
- Gitlab::Ci::Config::Loader::FormatError,
- /Invalid configuration format/
- )
+ context 'when yml is incorrect' do
+ let(:yml) { '// invalid' }
+
+ describe '.new' do
+ it 'raises error' do
+ expect { config }.to raise_error(
+ Gitlab::Ci::Config::Loader::FormatError,
+ /Invalid configuration format/
+ )
+ 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
diff --git a/spec/lib/gitlab/metrics/instrumentation_spec.rb b/spec/lib/gitlab/metrics/instrumentation_spec.rb
index 220e86924a2..c6e979b69a4 100644
--- a/spec/lib/gitlab/metrics/instrumentation_spec.rb
+++ b/spec/lib/gitlab/metrics/instrumentation_spec.rb
@@ -57,7 +57,7 @@ describe Gitlab::Metrics::Instrumentation do
and_return(transaction)
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')
@dummy.foo
@@ -137,7 +137,7 @@ describe Gitlab::Metrics::Instrumentation do
and_return(transaction)
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')
@dummy.new.bar