Commit 58fe0bc1 authored by Grzegorz Bizon's avatar Grzegorz Bizon

Merge branch 'master' into 'ce-to-ee-2018-06-05'

 Conflicts:
   app/models/ci/pipeline.rb
   app/serializers/pipeline_details_entity.rb
   spec/serializers/pipeline_serializer_spec.rb
parents 22f6450d e7822f0e
...@@ -96,6 +96,10 @@ gem 'grape', '~> 1.0' ...@@ -96,6 +96,10 @@ gem 'grape', '~> 1.0'
gem 'grape-entity', '~> 0.7.1' gem 'grape-entity', '~> 0.7.1'
gem 'rack-cors', '~> 1.0.0', require: 'rack/cors' gem 'rack-cors', '~> 1.0.0', require: 'rack/cors'
# GraphQL API
gem 'graphql', '~> 1.8.0'
gem 'graphiql-rails', '~> 1.4.10'
# Disable strong_params so that Mash does not respond to :permitted? # Disable strong_params so that Mash does not respond to :permitted?
gem 'hashie-forbidden_attributes' gem 'hashie-forbidden_attributes'
......
...@@ -390,6 +390,10 @@ GEM ...@@ -390,6 +390,10 @@ GEM
rake (~> 12) rake (~> 12)
grape_logging (1.7.0) grape_logging (1.7.0)
grape grape
graphiql-rails (1.4.10)
railties
sprockets-rails
graphql (1.8.1)
grpc (1.11.0) grpc (1.11.0)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
googleapis-common-protos-types (~> 1.0.0) googleapis-common-protos-types (~> 1.0.0)
...@@ -1088,6 +1092,8 @@ DEPENDENCIES ...@@ -1088,6 +1092,8 @@ DEPENDENCIES
grape-entity (~> 0.7.1) grape-entity (~> 0.7.1)
grape-path-helpers (~> 1.0) grape-path-helpers (~> 1.0)
grape_logging (~> 1.7) grape_logging (~> 1.7)
graphiql-rails (~> 1.4.10)
graphql (~> 1.8.0)
grpc (~> 1.11.0) grpc (~> 1.11.0)
gssapi gssapi
haml_lint (~> 0.26.0) haml_lint (~> 0.26.0)
......
...@@ -224,6 +224,9 @@ table { ...@@ -224,6 +224,9 @@ table {
} }
.nav-tabs { .nav-tabs {
// Override bootstrap's default border
border-bottom: 0;
.nav-link { .nav-link {
border-top: 0; border-top: 0;
border-left: 0; border-left: 0;
......
class GraphqlController < ApplicationController
# Unauthenticated users have access to the API for public data
skip_before_action :authenticate_user!
before_action :check_graphql_feature_flag!
def execute
variables = Gitlab::Graphql::Variables.new(params[:variables]).to_h
query = params[:query]
operation_name = params[:operationName]
context = {
current_user: current_user
}
result = GitlabSchema.execute(query, variables: variables, context: context, operation_name: operation_name)
render json: result
end
rescue_from StandardError do |exception|
log_exception(exception)
render_error("Internal server error")
end
rescue_from Gitlab::Graphql::Variables::Invalid do |exception|
render_error(exception.message, status: :unprocessable_entity)
end
private
# Overridden from the ApplicationController to make the response look like
# a GraphQL response. That is nicely picked up in Graphiql.
def render_404
render_error("Not found!", status: :not_found)
end
def render_error(message, status: 500)
error = { errors: [message: message] }
render json: error, status: status
end
def check_graphql_feature_flag!
render_404 unless Feature.enabled?(:graphql)
end
end
...@@ -5,13 +5,12 @@ class Groups::GroupMembersController < Groups::ApplicationController ...@@ -5,13 +5,12 @@ class Groups::GroupMembersController < Groups::ApplicationController
include MembersPresentation include MembersPresentation
include SortingHelper include SortingHelper
# Authorize def self.admin_not_required_endpoints
before_action :authorize_admin_group_member!, except: [:index, :leave, :request_access, :update, :override] %i[index leave request_access]
before_action :authorize_update_group_member!, only: [:update, :override] end
skip_cross_project_access_check :index, :create, :update, :destroy, :request_access, # Authorize
:approve_access_request, :leave, :resend_invite, before_action :authorize_admin_group_member!, except: admin_not_required_endpoints
:override
skip_cross_project_access_check :index, :create, :update, :destroy, :request_access, skip_cross_project_access_check :index, :create, :update, :destroy, :request_access,
:approve_access_request, :leave, :resend_invite, :approve_access_request, :leave, :resend_invite,
......
class Groups::MilestonesController < Groups::ApplicationController class Groups::MilestonesController < Groups::ApplicationController
prepend EE::Groups::MilestonesController
include MilestoneActions include MilestoneActions
before_action :group_projects before_action :group_projects
...@@ -77,17 +79,14 @@ class Groups::MilestonesController < Groups::ApplicationController ...@@ -77,17 +79,14 @@ class Groups::MilestonesController < Groups::ApplicationController
def milestones def milestones
milestones = MilestonesFinder.new(search_params).execute milestones = MilestonesFinder.new(search_params).execute
legacy_milestones =
if params[:only_group_milestones]
[]
else
GroupMilestone.build_collection(group, group_projects, params)
end
@sort = params[:sort] || 'due_date_asc' @sort = params[:sort] || 'due_date_asc'
MilestoneArray.sort(milestones + legacy_milestones, @sort) MilestoneArray.sort(milestones + legacy_milestones, @sort)
end end
def legacy_milestones
GroupMilestone.build_collection(group, group_projects, params)
end
def milestone def milestone
@milestone = @milestone =
if params[:title] if params[:title]
......
module Functions
class BaseFunction < GraphQL::Function
end
end
module Functions
class Echo < BaseFunction
argument :text, GraphQL::STRING_TYPE
description "Testing endpoint to validate the API with"
def call(obj, args, ctx)
username = ctx[:current_user]&.username
"#{username.inspect} says: #{args[:text]}"
end
end
end
class GitlabSchema < GraphQL::Schema
use BatchLoader::GraphQL
use Gitlab::Graphql::Authorize
use Gitlab::Graphql::Present
query(Types::QueryType)
# mutation(Types::MutationType)
end
module Resolvers
class BaseResolver < GraphQL::Schema::Resolver
end
end
module Resolvers
module FullPathResolver
extend ActiveSupport::Concern
prepended do
argument :full_path, GraphQL::ID_TYPE,
required: true,
description: 'The full path of the project or namespace, e.g., "gitlab-org/gitlab-ce"'
end
def model_by_full_path(model, full_path)
BatchLoader.for(full_path).batch(key: "#{model.model_name.param_key}:full_path") do |full_paths, loader|
# `with_route` avoids an N+1 calculating full_path
results = model.where_full_path_in(full_paths).with_route
results.each { |project| loader.call(project.full_path, project) }
end
end
end
end
module Resolvers
class MergeRequestResolver < BaseResolver
prepend FullPathResolver
type Types::ProjectType, null: true
argument :iid, GraphQL::ID_TYPE,
required: true,
description: 'The IID of the merge request, e.g., "1"'
def resolve(full_path:, iid:)
project = model_by_full_path(Project, full_path)
return unless project.present?
BatchLoader.for(iid.to_s).batch(key: project.id) do |iids, loader|
results = project.merge_requests.where(iid: iids)
results.each { |mr| loader.call(mr.iid.to_s, mr) }
end
end
end
end
module Resolvers
class ProjectResolver < BaseResolver
prepend FullPathResolver
type Types::ProjectType, null: true
def resolve(full_path:)
model_by_full_path(Project, full_path)
end
end
end
module Types
class BaseEnum < GraphQL::Schema::Enum
end
end
module Types
class BaseField < GraphQL::Schema::Field
prepend Gitlab::Graphql::Authorize
end
end
module Types
class BaseInputObject < GraphQL::Schema::InputObject
end
end
module Types
module BaseInterface
include GraphQL::Schema::Interface
end
end
module Types
class BaseObject < GraphQL::Schema::Object
prepend Gitlab::Graphql::Present
field_class Types::BaseField
end
end
module Types
class BaseScalar < GraphQL::Schema::Scalar
end
end
module Types
class BaseUnion < GraphQL::Schema::Union
end
end
module Types
class MergeRequestType < BaseObject
present_using MergeRequestPresenter
graphql_name 'MergeRequest'
field :id, GraphQL::ID_TYPE, null: false
field :iid, GraphQL::ID_TYPE, null: false
field :title, GraphQL::STRING_TYPE, null: false
field :description, GraphQL::STRING_TYPE, null: true
field :state, GraphQL::STRING_TYPE, null: true
field :created_at, Types::TimeType, null: false
field :updated_at, Types::TimeType, null: false
field :source_project, Types::ProjectType, null: true
field :target_project, Types::ProjectType, null: false
# Alias for target_project
field :project, Types::ProjectType, null: false
field :project_id, GraphQL::INT_TYPE, null: false, method: :target_project_id
field :source_project_id, GraphQL::INT_TYPE, null: true
field :target_project_id, GraphQL::INT_TYPE, null: false
field :source_branch, GraphQL::STRING_TYPE, null: false
field :target_branch, GraphQL::STRING_TYPE, null: false
field :work_in_progress, GraphQL::BOOLEAN_TYPE, method: :work_in_progress?, null: false
field :merge_when_pipeline_succeeds, GraphQL::BOOLEAN_TYPE, null: true
field :diff_head_sha, GraphQL::STRING_TYPE, null: true
field :merge_commit_sha, GraphQL::STRING_TYPE, null: true
field :user_notes_count, GraphQL::INT_TYPE, null: true
field :should_remove_source_branch, GraphQL::BOOLEAN_TYPE, method: :should_remove_source_branch?, null: true
field :force_remove_source_branch, GraphQL::BOOLEAN_TYPE, method: :force_remove_source_branch?, null: true
field :merge_status, GraphQL::STRING_TYPE, null: true
field :in_progress_merge_commit_sha, GraphQL::STRING_TYPE, null: true
field :merge_error, GraphQL::STRING_TYPE, null: true
field :allow_collaboration, GraphQL::BOOLEAN_TYPE, null: true
field :should_be_rebased, GraphQL::BOOLEAN_TYPE, method: :should_be_rebased?, null: false
field :rebase_commit_sha, GraphQL::STRING_TYPE, null: true
field :rebase_in_progress, GraphQL::BOOLEAN_TYPE, method: :rebase_in_progress?, null: false
field :diff_head_sha, GraphQL::STRING_TYPE, null: true
field :merge_commit_message, GraphQL::STRING_TYPE, null: true
field :merge_ongoing, GraphQL::BOOLEAN_TYPE, method: :merge_ongoing?, null: false
field :source_branch_exists, GraphQL::BOOLEAN_TYPE, method: :source_branch_exists?, null: false
field :mergeable_discussions_state, GraphQL::BOOLEAN_TYPE, null: true
field :web_url, GraphQL::STRING_TYPE, null: true
field :upvotes, GraphQL::INT_TYPE, null: false
field :downvotes, GraphQL::INT_TYPE, null: false
field :subscribed, GraphQL::BOOLEAN_TYPE, method: :subscribed?, null: false
end
end
module Types
class MutationType < BaseObject
graphql_name "Mutation"
# TODO: Add Mutations as fields
end
end
module Types
class ProjectType < BaseObject
graphql_name 'Project'
field :id, GraphQL::ID_TYPE, null: false
field :full_path, GraphQL::ID_TYPE, null: false
field :path, GraphQL::STRING_TYPE, null: false
field :name_with_namespace, GraphQL::STRING_TYPE, null: false
field :name, GraphQL::STRING_TYPE, null: false
field :description, GraphQL::STRING_TYPE, null: true
field :default_branch, GraphQL::STRING_TYPE, null: true
field :tag_list, GraphQL::STRING_TYPE, null: true
field :ssh_url_to_repo, GraphQL::STRING_TYPE, null: true
field :http_url_to_repo, GraphQL::STRING_TYPE, null: true
field :web_url, GraphQL::STRING_TYPE, null: true
field :star_count, GraphQL::INT_TYPE, null: false
field :forks_count, GraphQL::INT_TYPE, null: false
field :created_at, Types::TimeType, null: true
field :last_activity_at, Types::TimeType, null: true
field :archived, GraphQL::BOOLEAN_TYPE, null: true
field :visibility, GraphQL::STRING_TYPE, null: true
field :container_registry_enabled, GraphQL::BOOLEAN_TYPE, null: true
field :shared_runners_enabled, GraphQL::BOOLEAN_TYPE, null: true
field :lfs_enabled, GraphQL::BOOLEAN_TYPE, null: true
field :merge_requests_ff_only_enabled, GraphQL::BOOLEAN_TYPE, null: true
field :avatar_url, GraphQL::STRING_TYPE, null: true, resolve: -> (project, args, ctx) do
project.avatar_url(only_path: false)
end
%i[issues merge_requests wiki snippets].each do |feature|
field "#{feature}_enabled", GraphQL::BOOLEAN_TYPE, null: true, resolve: -> (project, args, ctx) do
project.feature_available?(feature, ctx[:current_user])
end
end
field :jobs_enabled, GraphQL::BOOLEAN_TYPE, null: true, resolve: -> (project, args, ctx) do
project.feature_available?(:builds, ctx[:current_user])
end
field :public_jobs, GraphQL::BOOLEAN_TYPE, method: :public_builds, null: true
field :open_issues_count, GraphQL::INT_TYPE, null: true, resolve: -> (project, args, ctx) do
project.open_issues_count if project.feature_available?(:issues, ctx[:current_user])
end
field :import_status, GraphQL::STRING_TYPE, null: true
field :ci_config_path, GraphQL::STRING_TYPE, null: true
field :only_allow_merge_if_pipeline_succeeds, GraphQL::BOOLEAN_TYPE, null: true
field :request_access_enabled, GraphQL::BOOLEAN_TYPE, null: true
field :only_allow_merge_if_all_discussions_are_resolved, GraphQL::BOOLEAN_TYPE, null: true
field :printing_merge_request_link_enabled, GraphQL::BOOLEAN_TYPE, null: true
end
end
module Types
class QueryType < BaseObject
graphql_name 'Query'
field :project, Types::ProjectType,
null: true,
resolver: Resolvers::ProjectResolver,
description: "Find a project" do
authorize :read_project
end
field :merge_request, Types::MergeRequestType,
null: true,
resolver: Resolvers::MergeRequestResolver,
description: "Find a merge request" do
authorize :read_merge_request
end
field :echo, GraphQL::STRING_TYPE, null: false, function: Functions::Echo.new
end
end
module Types
class TimeType < BaseScalar
graphql_name 'Time'
description 'Time represented in ISO 8601'
def self.coerce_input(value, ctx)
Time.parse(value)
end
def self.coerce_result(value, ctx)
value.iso8601
end
end
end
...@@ -181,6 +181,25 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated ...@@ -181,6 +181,25 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
.can_push_to_branch?(source_branch) .can_push_to_branch?(source_branch)
end end
def mergeable_discussions_state
# This avoids calling MergeRequest#mergeable_discussions_state without
# considering the state of the MR first. If a MR isn't mergeable, we can
# safely short-circuit it.
if merge_request.mergeable_state?(skip_ci_check: true, skip_discussions_check: true)
merge_request.mergeable_discussions_state?
else
false
end
end
def web_url
Gitlab::UrlBuilder.build(merge_request)
end
def subscribed?
merge_request.subscribed?(current_user, merge_request.target_project)
end
private private
def cached_can_be_reverted? def cached_can_be_reverted?
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
.settings-header .settings-header
%h4 %h4
= _('Variables') = _('Variables')
= link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'secret-variables'), target: '_blank', rel: 'noopener noreferrer' = link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'variables'), target: '_blank', rel: 'noopener noreferrer'
%button.btn.btn-default.js-settings-toggle{ type: "button" } %button.btn.btn-default.js-settings-toggle{ type: "button" }
= expanded ? _('Collapse') : _('Expand') = expanded ? _('Collapse') : _('Expand')
%p.append-bottom-0 %p.append-bottom-0
......
---
title: Setup graphql with initial project & merge request query
merge_request: 19008
author:
type: added
constraints(::Constraints::FeatureConstrainer.new(:graphql)) do
post '/api/graphql', to: 'graphql#execute'
mount GraphiQL::Rails::Engine, at: '/-/graphql-explorer', graphql_path: '/api/graphql'
end
::API::API.logger Rails.logger ::API::API.logger Rails.logger
mount ::API::API => '/' mount ::API::API => '/'
...@@ -184,10 +184,10 @@ configuration. Then customize everything from buildpacks to CI/CD. ...@@ -184,10 +184,10 @@ configuration. Then customize everything from buildpacks to CI/CD.
- [Auto DevOps](topics/autodevops/index.md) - [Auto DevOps](topics/autodevops/index.md)
- [Deployment of Helm, Ingress, and Prometheus on Kubernetes](user/project/clusters/index.md#installing-applications) - [Deployment of Helm, Ingress, and Prometheus on Kubernetes](user/project/clusters/index.md#installing-applications)
- [Protected secret variables](ci/variables/README.md#protected-secret-variables) - [Protected variables](ci/variables/README.md#protected-variables)
- [Easy creation of Kubernetes clusters on GKE](user/project/clusters/index.md#adding-and-creating-a-new-gke-cluster-via-gitlab) - [Easy creation of Kubernetes clusters on GKE](user/project/clusters/index.md#adding-and-creating-a-new-gke-cluster-via-gitlab)
- [Multiple Kubernetes Clusters](user/project/clusters/index.md#multiple-kubernetes-clusters) **[PREMIUM]** - [Multiple Kubernetes Clusters](user/project/clusters/index.md#multiple-kubernetes-clusters) **[PREMIUM]**
- [Environment-specific secret variables](ci/variables/README.md#limiting-environment-scopes-of-secret-variables) **[PREMIUM]** - [Environment-specific variables](ci/variables/README.md#limiting-environment-scopes-of-variables) **[PREMIUM]**
### Monitor ### Monitor
......
# GraphQL API (Beta)
> [Introduced][ce-19008] in GitLab 11.0.
[GraphQL](https://graphql.org/) is a query language for APIs that
allows clients to request exactly the data they need, making it
possible to get all required data in a limited number of requests.
The GraphQL data (fields) can be described in the form of types,
allowing clients to use [clientside GraphQL
libraries](https://graphql.org/code/#graphql-clients) to consume the
API and avoid manual parsing.
Since there's no fixed endpoints and datamodel, new abilities can be
added to the API without creating breaking changes. This allows us to
have a versionless API as described in [the GraphQL
documentation](https://graphql.org/learn/best-practices/#versioning).
## Enabling the GraphQL feature
The GraphQL API itself is currently in Alpha, and therefore hidden behind a
feature flag. You can enable the feature using the [features api][features-api] on a self-hosted instance.
For example:
```shell
curl --data "value=100" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/features/graphql
```
## Available queries
A first iteration of a GraphQL API includes only 2 queries: `project` and
`merge_request` and only returns scalar fields, or fields of the type `Project`
or `MergeRequest`.
## GraphiQL
The API can be explored by using the GraphiQL IDE, it is available on your
instance on `gitlab.example.com/-/graphql-explorer`.
[ce-19008]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/19008
[features-api]: ../features.md
...@@ -496,7 +496,7 @@ To configure access for `registry.example.com`, follow these steps: ...@@ -496,7 +496,7 @@ To configure access for `registry.example.com`, follow these steps:
bXlfdXNlcm5hbWU6bXlfcGFzc3dvcmQ= bXlfdXNlcm5hbWU6bXlfcGFzc3dvcmQ=
``` ```
1. Create a [secret variable] `DOCKER_AUTH_CONFIG` with the content of the 1. Create a [variable] `DOCKER_AUTH_CONFIG` with the content of the
Docker configuration file as the value: Docker configuration file as the value:
```json ```json
...@@ -632,7 +632,7 @@ creation. ...@@ -632,7 +632,7 @@ creation.
[postgres-hub]: https://hub.docker.com/r/_/postgres/ [postgres-hub]: https://hub.docker.com/r/_/postgres/
[mysql-hub]: https://hub.docker.com/r/_/mysql/ [mysql-hub]: https://hub.docker.com/r/_/mysql/
[runner-priv-reg]: http://docs.gitlab.com/runner/configuration/advanced-configuration.html#using-a-private-container-registry [runner-priv-reg]: http://docs.gitlab.com/runner/configuration/advanced-configuration.html#using-a-private-container-registry
[secret variable]: ../variables/README.md#secret-variables [variable]: ../variables/README.md#variables
[entrypoint]: https://docs.docker.com/engine/reference/builder/#entrypoint [entrypoint]: https://docs.docker.com/engine/reference/builder/#entrypoint
[cmd]: https://docs.docker.com/engine/reference/builder/#cmd [cmd]: https://docs.docker.com/engine/reference/builder/#cmd
[register]: https://docs.gitlab.com/runner/register/ [register]: https://docs.gitlab.com/runner/register/
...@@ -249,7 +249,7 @@ the basis of [Review apps](review_apps/index.md). ...@@ -249,7 +249,7 @@ the basis of [Review apps](review_apps/index.md).
NOTE: **Note:** NOTE: **Note:**
The `name` and `url` parameters can use most of the CI/CD variables, The `name` and `url` parameters can use most of the CI/CD variables,
including [predefined](variables/README.md#predefined-variables-environment-variables), including [predefined](variables/README.md#predefined-variables-environment-variables),
[secret](variables/README.md#secret-variables) and [project/group ones](variables/README.md#variables) and
[`.gitlab-ci.yml` variables](yaml/README.md#variables). You however cannot use variables [`.gitlab-ci.yml` variables](yaml/README.md#variables). You however cannot use variables
defined under `script` or on the Runner's side. There are also other variables that defined under `script` or on the Runner's side. There are also other variables that
are unsupported in the context of `environment:name`. You can read more about are unsupported in the context of `environment:name`. You can read more about
......
...@@ -58,7 +58,7 @@ The application is ready to use, but you need some additional steps to deploy it ...@@ -58,7 +58,7 @@ The application is ready to use, but you need some additional steps to deploy it
1. Log in to Artifactory with your user's credentials. 1. Log in to Artifactory with your user's credentials.
1. From the main screen, click on the `libs-release-local` item in the **Set Me Up** panel. 1. From the main screen, click on the `libs-release-local` item in the **Set Me Up** panel.
1. Copy to clipboard the configuration snippet under the **Deploy** paragraph. 1. Copy to clipboard the configuration snippet under the **Deploy** paragraph.
1. Change the `url` value in order to have it configurable via secret variables. 1. Change the `url` value in order to have it configurable via variables.
1. Copy the snippet in the `pom.xml` file for your project, just after the 1. Copy the snippet in the `pom.xml` file for your project, just after the
`dependencies` section. The snippet should look like this: `dependencies` section. The snippet should look like this:
...@@ -98,7 +98,7 @@ parameter in `.gitlab-ci.yml` to use the custom location instead of the default ...@@ -98,7 +98,7 @@ parameter in `.gitlab-ci.yml` to use the custom location instead of the default
</settings> </settings>
``` ```
Username and password will be replaced by the correct values using secret variables. Username and password will be replaced by the correct values using variables.
### Configure GitLab CI/CD for `simple-maven-dep` ### Configure GitLab CI/CD for `simple-maven-dep`
...@@ -107,8 +107,8 @@ Now it's time we set up [GitLab CI/CD](https://about.gitlab.com/features/gitlab- ...@@ -107,8 +107,8 @@ Now it's time we set up [GitLab CI/CD](https://about.gitlab.com/features/gitlab-
GitLab CI/CD uses a file in the root of the repo, named `.gitlab-ci.yml`, to read the definitions for jobs GitLab CI/CD uses a file in the root of the repo, named `.gitlab-ci.yml`, to read the definitions for jobs
that will be executed by the configured GitLab Runners. You can read more about this file in the [GitLab Documentation](https://docs.gitlab.com/ee/ci/yaml/). that will be executed by the configured GitLab Runners. You can read more about this file in the [GitLab Documentation](https://docs.gitlab.com/ee/ci/yaml/).
First of all, remember to set up secret variables for your deployment. Navigate to your project's **Settings > CI/CD** page First of all, remember to set up variables for your deployment. Navigate to your project's **Settings > CI/CD > Variables** page
and add the following secret variables (replace them with your current values, of course): and add the following ones (replace them with your current values, of course):
- **MAVEN_REPO_URL**: `http://artifactory.example.com:8081/artifactory` (your Artifactory URL) - **MAVEN_REPO_URL**: `http://artifactory.example.com:8081/artifactory` (your Artifactory URL)
- **MAVEN_REPO_USER**: `gitlab` (your Artifactory username) - **MAVEN_REPO_USER**: `gitlab` (your Artifactory username)
...@@ -156,7 +156,7 @@ by running all Maven phases in a sequential order, therefore, executing `mvn tes ...@@ -156,7 +156,7 @@ by running all Maven phases in a sequential order, therefore, executing `mvn tes
Both `build` and `test` jobs leverage the `mvn` command to compile the application and to test it as defined in the test suite that is part of the application. Both `build` and `test` jobs leverage the `mvn` command to compile the application and to test it as defined in the test suite that is part of the application.
Deploy to Artifactory is done as defined by the secret variables we have just set up. Deploy to Artifactory is done as defined by the variables we have just set up.
The deployment occurs only if we're pushing or merging to `master` branch, so that the development versions are tested but not published. The deployment occurs only if we're pushing or merging to `master` branch, so that the development versions are tested but not published.
Done! Now you have all the changes in the GitLab repo, and a pipeline has already been started for this commit. In the **Pipelines** tab you can see what's happening. Done! Now you have all the changes in the GitLab repo, and a pipeline has already been started for this commit. In the **Pipelines** tab you can see what's happening.
......
...@@ -111,7 +111,7 @@ We also use two secure variables: ...@@ -111,7 +111,7 @@ We also use two secure variables:
## Storing API keys ## Storing API keys
Secure Variables can added by going to your project's Secure Variables can added by going to your project's
**Settings ➔ CI / CD ➔ Secret variables**. The variables that are defined **Settings ➔ CI / CD ➔ Variables**. The variables that are defined
in the project settings are sent along with the build script to the Runner. in the project settings are sent along with the build script to the Runner.
The secure variables are stored out of the repository. Never store secrets in The secure variables are stored out of the repository. Never store secrets in
your project's `.gitlab-ci.yml`. It is also important that the secret's value your project's `.gitlab-ci.yml`. It is also important that the secret's value
......
...@@ -406,7 +406,7 @@ and further delves into the principles of GitLab CI/CD than discussed in this ar ...@@ -406,7 +406,7 @@ and further delves into the principles of GitLab CI/CD than discussed in this ar
We need to be able to deploy to AWS with our AWS account credentials, but we certainly We need to be able to deploy to AWS with our AWS account credentials, but we certainly
don't want to put secrets into source code. Luckily GitLab provides a solution for this don't want to put secrets into source code. Luckily GitLab provides a solution for this
with [Secret Variables](../../../ci/variables/README.md). This can get complicated with [Variables](../../../ci/variables/README.md). This can get complicated
due to [IAM](https://aws.amazon.com/iam/) management. As a best practice, you shouldn't due to [IAM](https://aws.amazon.com/iam/) management. As a best practice, you shouldn't
use root security credentials. Proper IAM credential management is beyond the scope of this use root security credentials. Proper IAM credential management is beyond the scope of this
article, but AWS will remind you that using root credentials is unadvised and against their article, but AWS will remind you that using root credentials is unadvised and against their
...@@ -428,7 +428,7 @@ fully understand [IAM Best Practices in AWS](http://docs.aws.amazon.com/IAM/late ...@@ -428,7 +428,7 @@ fully understand [IAM Best Practices in AWS](http://docs.aws.amazon.com/IAM/late
To deploy our build artifacts, we need to install the [AWS CLI](https://aws.amazon.com/cli/) on To deploy our build artifacts, we need to install the [AWS CLI](https://aws.amazon.com/cli/) on
the Shared Runner. The Shared Runner also needs to be able to authenticate with your AWS the Shared Runner. The Shared Runner also needs to be able to authenticate with your AWS
account to deploy the artifacts. By convention, AWS CLI will look for `AWS_ACCESS_KEY_ID` account to deploy the artifacts. By convention, AWS CLI will look for `AWS_ACCESS_KEY_ID`
and `AWS_SECRET_ACCESS_KEY`. GitLab's CI gives us a way to pass the secret variables we and `AWS_SECRET_ACCESS_KEY`. GitLab's CI gives us a way to pass the variables we
set up in the prior section using the `variables` portion of the `deploy` job. At the end, set up in the prior section using the `variables` portion of the `deploy` job. At the end,
we add directives to ensure deployment `only` happens on pushes to `master`. This way, every we add directives to ensure deployment `only` happens on pushes to `master`. This way, every
single branch still runs through CI, and only merging (or committing directly) to master will single branch still runs through CI, and only merging (or committing directly) to master will
......
...@@ -116,11 +116,11 @@ cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys ...@@ -116,11 +116,11 @@ cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys
cat ~/.ssh/id_rsa cat ~/.ssh/id_rsa
``` ```
Now, let's add it to your GitLab project as a [secret variable](../../variables/README.md#secret-variables). Now, let's add it to your GitLab project as a [variable](../../variables/README.md#variables).
Secret variables are user-defined variables and are stored out of `.gitlab-ci.yml`, for security purposes. Variables are user-defined variables and are stored out of `.gitlab-ci.yml`, for security purposes.
They can be added per project by navigating to the project's **Settings** > **CI/CD**. They can be added per project by navigating to the project's **Settings** > **CI/CD**.
![secret variables page](img/secret_variables_page.png) ![variables page](img/secret_variables_page.png)
To the field **KEY**, add the name `SSH_PRIVATE_KEY`, and to the **VALUE** field, paste the private key you've copied earlier. To the field **KEY**, add the name `SSH_PRIVATE_KEY`, and to the **VALUE** field, paste the private key you've copied earlier.
We'll use this variable in the `.gitlab-ci.yml` later, to easily connect to our remote server as the deployer user without entering its password. We'll use this variable in the `.gitlab-ci.yml` later, to easily connect to our remote server as the deployer user without entering its password.
......
...@@ -263,7 +263,7 @@ on that specific branch: ...@@ -263,7 +263,7 @@ on that specific branch:
- trigger **manual actions** on existing pipelines - trigger **manual actions** on existing pipelines
- **retry/cancel** existing jobs (using Web UI or Pipelines API) - **retry/cancel** existing jobs (using Web UI or Pipelines API)
**Secret variables** marked as **protected** are accessible only to jobs that **Variables** marked as **protected** are accessible only to jobs that
run on protected branches, avoiding untrusted users to get unintended access to run on protected branches, avoiding untrusted users to get unintended access to
sensitive information like deployment credentials and tokens. sensitive information like deployment credentials and tokens.
......
...@@ -25,7 +25,7 @@ with any type of [executor](https://docs.gitlab.com/runner/executors/) ...@@ -25,7 +25,7 @@ with any type of [executor](https://docs.gitlab.com/runner/executors/)
## How it works ## How it works
1. Create a new SSH key pair locally with [ssh-keygen](http://linux.die.net/man/1/ssh-keygen) 1. Create a new SSH key pair locally with [ssh-keygen](http://linux.die.net/man/1/ssh-keygen)
1. Add the private key as a [secret variable](../variables/README.md) to 1. Add the private key as a [variable](../variables/README.md) to
your project your project
1. Run the [ssh-agent](http://linux.die.net/man/1/ssh-agent) during job to load 1. Run the [ssh-agent](http://linux.die.net/man/1/ssh-agent) during job to load
the private key. the private key.
...@@ -49,7 +49,7 @@ to access it. This is where an SSH key pair comes in handy. ...@@ -49,7 +49,7 @@ to access it. This is where an SSH key pair comes in handy.
**Do not** add a passphrase to the SSH key, or the `before_script` will\ **Do not** add a passphrase to the SSH key, or the `before_script` will\
prompt for it. prompt for it.
1. Create a new [secret variable](../variables/README.md#secret-variables). 1. Create a new [variable](../variables/README.md#variables).
As **Key** enter the name `SSH_PRIVATE_KEY` and in the **Value** field paste As **Key** enter the name `SSH_PRIVATE_KEY` and in the **Value** field paste
the content of your _private_ key that you created earlier. the content of your _private_ key that you created earlier.
...@@ -157,7 +157,7 @@ ssh-keyscan example.com ...@@ -157,7 +157,7 @@ ssh-keyscan example.com
ssh-keyscan 1.2.3.4 ssh-keyscan 1.2.3.4
``` ```
Create a new [secret variable](../variables/README.md#secret-variables) with Create a new [variable](../variables/README.md#variables) with
`SSH_KNOWN_HOSTS` as "Key", and as a "Value" add the output of `ssh-keyscan`. `SSH_KNOWN_HOSTS` as "Key", and as a "Value" add the output of `ssh-keyscan`.
NOTE: **Note:** NOTE: **Note:**
...@@ -165,7 +165,7 @@ If you need to connect to multiple servers, all the server host keys ...@@ -165,7 +165,7 @@ If you need to connect to multiple servers, all the server host keys
need to be collected in the **Value** of the variable, one key per line. need to be collected in the **Value** of the variable, one key per line.
TIP: **Tip:** TIP: **Tip:**
By using a secret variable instead of `ssh-keyscan` directly inside By using a variable instead of `ssh-keyscan` directly inside
`.gitlab-ci.yml`, it has the benefit that you don't have to change `.gitlab-ci.yml` `.gitlab-ci.yml`, it has the benefit that you don't have to change `.gitlab-ci.yml`
if the host domain name changes for some reason. Also, the values are predefined if the host domain name changes for some reason. Also, the values are predefined
by you, meaning that if the host keys suddenly change, the CI/CD job will fail, by you, meaning that if the host keys suddenly change, the CI/CD job will fail,
......
...@@ -109,7 +109,7 @@ The action is irreversible. ...@@ -109,7 +109,7 @@ The action is irreversible.
it will not trigger a job. it will not trigger a job.
- If your project is public, passing the token in plain text is probably not the - If your project is public, passing the token in plain text is probably not the
wisest idea, so you might want to use a wisest idea, so you might want to use a
[secret variable](../variables/README.md#secret-variables) for that purpose. [variable](../variables/README.md#variables) for that purpose.
To trigger a job you need to send a `POST` request to GitLab's API endpoint: To trigger a job you need to send a `POST` request to GitLab's API endpoint:
......
...@@ -10,17 +10,17 @@ The variables can be overwritten and they take precedence over each other in ...@@ -10,17 +10,17 @@ The variables can be overwritten and they take precedence over each other in
this order: this order:
1. [Trigger variables][triggers] or [scheduled pipeline variables](../../user/project/pipelines/schedules.md#making-use-of-scheduled-pipeline-variables) (take precedence over all) 1. [Trigger variables][triggers] or [scheduled pipeline variables](../../user/project/pipelines/schedules.md#making-use-of-scheduled-pipeline-variables) (take precedence over all)
1. Project-level [secret variables](#secret-variables) or [protected secret variables](#protected-secret-variables) 1. Project-level [variables](#variables) or [protected variables](#protected-variables)
1. Group-level [secret variables](#secret-variables) or [protected secret variables](#protected-secret-variables) 1. Group-level [variables](#variables) or [protected variables](#protected-variables)
1. YAML-defined [job-level variables](../yaml/README.md#variables) 1. YAML-defined [job-level variables](../yaml/README.md#variables)
1. YAML-defined [global variables](../yaml/README.md#variables) 1. YAML-defined [global variables](../yaml/README.md#variables)
1. [Deployment variables](#deployment-variables) 1. [Deployment variables](#deployment-variables)
1. [Predefined variables](#predefined-variables-environment-variables) (are the 1. [Predefined variables](#predefined-variables-environment-variables) (are the
lowest in the chain) lowest in the chain)
For example, if you define `API_TOKEN=secure` as a secret variable and For example, if you define `API_TOKEN=secure` as a project variable and
`API_TOKEN=yaml` in your `.gitlab-ci.yml`, the `API_TOKEN` will take the value `API_TOKEN=yaml` in your `.gitlab-ci.yml`, the `API_TOKEN` will take the value
`secure` as the secret variables are higher in the chain. `secure` as the project variables are higher in the chain.
## Unsupported variables ## Unsupported variables
...@@ -168,58 +168,58 @@ script: ...@@ -168,58 +168,58 @@ script:
- 'eval $LS_CMD' # will execute 'ls -al $TMP_DIR' - 'eval $LS_CMD' # will execute 'ls -al $TMP_DIR'
``` ```
## Secret variables ## Variables
NOTE: **Note:** NOTE: **Note:**
Group-level secret variables were added in GitLab 9.4. Group-level variables were added in GitLab 9.4.
CAUTION: **Important:** CAUTION: **Important:**
Be aware that secret variables are not masked, and their values can be shown Be aware that variables are not masked, and their values can be shown
in the job logs if explicitly asked to do so. If your project is public or in the job logs if explicitly asked to do so. If your project is public or
internal, you can set the pipelines private from your [project's Pipelines internal, you can set the pipelines private from your [project's Pipelines
settings](../../user/project/pipelines/settings.md#visibility-of-pipelines). settings](../../user/project/pipelines/settings.md#visibility-of-pipelines).
Follow the discussion in issue [#13784][ce-13784] for masking the secret variables. Follow the discussion in issue [#13784][ce-13784] for masking the variables.
GitLab CI allows you to define per-project or per-group secret variables GitLab CI allows you to define per-project or per-group variables
that are set in the pipeline environment. The secret variables are stored out of that are set in the pipeline environment. The variables are stored out of
the repository (not in `.gitlab-ci.yml`) and are securely passed to GitLab Runner the repository (not in `.gitlab-ci.yml`) and are securely passed to GitLab Runner
making them available during a pipeline run. It's the recommended method to making them available during a pipeline run. It's the recommended method to
use for storing things like passwords, SSH keys and credentials. use for storing things like passwords, SSH keys and credentials.
Project-level secret variables can be added by going to your project's Project-level variables can be added by going to your project's
**Settings > CI/CD**, then finding the section called **Secret variables**. **Settings > CI/CD**, then finding the section called **Variables**.
Likewise, group-level secret variables can be added by going to your group's Likewise, group-level variables can be added by going to your group's
**Settings > CI/CD**, then finding the section called **Secret variables**. **Settings > CI/CD**, then finding the section called **Variables**.
Any variables of [subgroups] will be inherited recursively. Any variables of [subgroups] will be inherited recursively.
![Secret variables](img/secret_variables.png) ![Variables](img/secret_variables.png)
Once you set them, they will be available for all subsequent pipelines. You can also Once you set them, they will be available for all subsequent pipelines. You can also
[protect your variables](#protected-secret-variables). [protect your variables](#protected-variables).
### Protected secret variables ### Protected variables
>**Notes:** >**Notes:**
This feature requires GitLab 9.3 or higher. This feature requires GitLab 9.3 or higher.
Secret variables could be protected. Whenever a secret variable is Variables could be protected. Whenever a variable is
protected, it would only be securely passed to pipelines running on the protected, it would only be securely passed to pipelines running on the
[protected branches] or [protected tags]. The other pipelines would not get any [protected branches] or [protected tags]. The other pipelines would not get any
protected variables. protected variables.
Protected variables can be added by going to your project's Protected variables can be added by going to your project's
**Settings > CI/CD**, then finding the section called **Settings > CI/CD**, then finding the section called
**Secret variables**, and check "Protected". **Variables**, and check "Protected".
Once you set them, they will be available for all subsequent pipelines. Once you set them, they will be available for all subsequent pipelines.
### Limiting environment scopes of secret variables **[PREMIUM]** ### Limiting environment scopes of variables **[PREMIUM]**
>**Notes:** >**Notes:**
[Introduced][ee-2112] in [GitLab Premium][premium] 9.4. [Introduced][ee-2112] in [GitLab Premium][premium] 9.4.
You can limit the environment scope of a secret variable by You can limit the environment scope of a variable by
[defining which environments][envs] it can be available for. [defining which environments][envs] it can be available for.
Wildcards can be used, and the default environment scope is `*` which means Wildcards can be used, and the default environment scope is `*` which means
...@@ -252,7 +252,7 @@ An example project service that defines deployment variables is the ...@@ -252,7 +252,7 @@ An example project service that defines deployment variables is the
CAUTION: **Warning:** CAUTION: **Warning:**
Enabling debug tracing can have severe security implications. The Enabling debug tracing can have severe security implications. The
output **will** contain the content of all your secret variables and any other output **will** contain the content of all your variables and any other
secrets! The output **will** be uploaded to the GitLab server and made visible secrets! The output **will** be uploaded to the GitLab server and made visible
in job traces! in job traces!
...@@ -440,7 +440,7 @@ job_name: ...@@ -440,7 +440,7 @@ job_name:
``` ```
You can also list all environment variables with the `export` command, You can also list all environment variables with the `export` command,
but be aware that this will also expose the values of all the secret variables but be aware that this will also expose the values of all the variables
you set, in the job log: you set, in the job log:
```yaml ```yaml
...@@ -493,7 +493,7 @@ It is possible to use variables expressions with only / except policies in ...@@ -493,7 +493,7 @@ It is possible to use variables expressions with only / except policies in
`.gitlab-ci.yml`. By using this approach you can limit what jobs are going to `.gitlab-ci.yml`. By using this approach you can limit what jobs are going to
be created within a pipeline after pushing a code to GitLab. be created within a pipeline after pushing a code to GitLab.
This is particularly useful in combination with secret variables and triggered This is particularly useful in combination with variables and triggered
pipeline variables. pipeline variables.
```yaml ```yaml
...@@ -571,27 +571,8 @@ Below you can find supported syntax reference: ...@@ -571,27 +571,8 @@ Below you can find supported syntax reference:
Pattern matching is case-sensitive by default. Use `i` flag modifier, like Pattern matching is case-sensitive by default. Use `i` flag modifier, like
`/pattern/i` to make a pattern case-insensitive. `/pattern/i` to make a pattern case-insensitive.
### Secret variables with an environment scope
We do support secret variables defined with an environment scope. Given that
there is a secret variable `$STAGING_SECRET` defined in a scope of
`review/staging/*`, following job that is using dynamic environments feature,
is going to be created, based on the matching variable expression:
```yaml
my-job:
stage: staging
environment:
name: review/$CI_JOB_STAGE/deploy
script:
- 'deploy staging'
only:
variables:
- $STAGING_SECRET == 'something'
```
[ee-2112]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/2112 [ee-2112]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/2112
[ce-13784]: https://gitlab.com/gitlab-org/gitlab-ce/issues/13784 "Simple protection of CI secret variables" [ce-13784]: https://gitlab.com/gitlab-org/gitlab-ce/issues/13784 "Simple protection of CI variables"
[premium]: https://about.gitlab.com/products/ "Available only in GitLab Premium" [premium]: https://about.gitlab.com/products/ "Available only in GitLab Premium"
[envs]: ../environments.md [envs]: ../environments.md
[protected branches]: ../../user/project/protected_branches.md [protected branches]: ../../user/project/protected_branches.md
......
...@@ -17,7 +17,7 @@ There are basically two places where you can use any defined variables: ...@@ -17,7 +17,7 @@ There are basically two places where you can use any defined variables:
| Definition | Can be expanded? | Expansion place | Description | | Definition | Can be expanded? | Expansion place | Description |
|--------------------------------------|-------------------|-----------------|--------------| |--------------------------------------|-------------------|-----------------|--------------|
| `environment:url` | yes | GitLab | The variable expansion is made by GitLab's [internal variable expansion mechanism](#gitlab-internal-variable-expansion-mechanism).<ul><li>**Supported:** all variables defined for a job (secret variables, variables from `.gitlab-ci.yml`, variables from triggers, variables from pipeline schedules)</li><li>**Not suported:** variables defined in Runner's `config.toml` and variables created in job's `script`</li></ul> | | `environment:url` | yes | GitLab | The variable expansion is made by GitLab's [internal variable expansion mechanism](#gitlab-internal-variable-expansion-mechanism).<ul><li>**Supported:** all variables defined for a job (project/group variables, variables from `.gitlab-ci.yml`, variables from triggers, variables from pipeline schedules)</li><li>**Not suported:** variables defined in Runner's `config.toml` and variables created in job's `script`</li></ul> |
| `environment:name` | yes | GitLab | Similar to `environment:url`, but the variables expansion **doesn't support**: <ul><li>variables that are based on the environment's name (`CI_ENVIRONMENT_NAME`, `CI_ENVIRONMENT_SLUG`)</li><li>any other variables related to environment (currently only `CI_ENVIRONMENT_URL`)</li><li>[persisted variables](#persisted-variables)</li></ul> | | `environment:name` | yes | GitLab | Similar to `environment:url`, but the variables expansion **doesn't support**: <ul><li>variables that are based on the environment's name (`CI_ENVIRONMENT_NAME`, `CI_ENVIRONMENT_SLUG`)</li><li>any other variables related to environment (currently only `CI_ENVIRONMENT_URL`)</li><li>[persisted variables](#persisted-variables)</li></ul> |
| `variables` | yes | Runner | The variable expansion is made by GitLab Runner's [internal variable expansion mechanism](#gitlab-runner-internal-variable-expansion-mechanism) | | `variables` | yes | Runner | The variable expansion is made by GitLab Runner's [internal variable expansion mechanism](#gitlab-runner-internal-variable-expansion-mechanism) |
| `image` | yes | Runner | The variable expansion is made by GitLab Runner's [internal variable expansion mechanism](#gitlab-runner-internal-variable-expansion-mechanism) | | `image` | yes | Runner | The variable expansion is made by GitLab Runner's [internal variable expansion mechanism](#gitlab-runner-internal-variable-expansion-mechanism) |
...@@ -55,7 +55,7 @@ since the expansion is done in GitLab before any Runner will get the job. ...@@ -55,7 +55,7 @@ since the expansion is done in GitLab before any Runner will get the job.
### GitLab Runner internal variable expansion mechanism ### GitLab Runner internal variable expansion mechanism
- **Supported:** secret variables, `.gitlab-ci.yml` variables, `config.toml` variables, and - **Supported:** project/group variables, `.gitlab-ci.yml` variables, `config.toml` variables, and
variables from triggers and pipeline schedules variables from triggers and pipeline schedules
- **Not supported:** variables defined inside of scripts (e.g., `export MY_VARIABLE="test"`) - **Not supported:** variables defined inside of scripts (e.g., `export MY_VARIABLE="test"`)
...@@ -76,7 +76,7 @@ are using a different variables syntax. ...@@ -76,7 +76,7 @@ are using a different variables syntax.
**Supported:** **Supported:**
- The `script` may use all available variables that are default for the shell (e.g., `$PATH` which - The `script` may use all available variables that are default for the shell (e.g., `$PATH` which
should be present in all bash/sh shells) and all variables defined by GitLab CI/CD (secret variables, should be present in all bash/sh shells) and all variables defined by GitLab CI/CD (project/group variables,
`.gitlab-ci.yml` variables, `config.toml` variables, and variables from triggers and pipeline schedules). `.gitlab-ci.yml` variables, `config.toml` variables, and variables from triggers and pipeline schedules).
- The `script` may also use all variables defined in the lines before. So, for example, if you define - The `script` may also use all variables defined in the lines before. So, for example, if you define
a variable `export MY_VARIABLE="test"`: a variable `export MY_VARIABLE="test"`:
...@@ -112,10 +112,10 @@ They are: ...@@ -112,10 +112,10 @@ They are:
- by the definitions [described in the table](#gitlab-ci-yml-file) where the "Expansion place" is "GitLab" - by the definitions [described in the table](#gitlab-ci-yml-file) where the "Expansion place" is "GitLab"
- in the `only` and `except` [variables expressions](README.md#variables-expressions) - in the `only` and `except` [variables expressions](README.md#variables-expressions)
## Secret variables with an environment scope ## Variables with an environment scope
Secret variables defined with an environment scope are supported. Given that Variables defined with an environment scope are supported. Given that
there is a secret variable `$STAGING_SECRET` defined in a scope of there is a variable `$STAGING_SECRET` defined in a scope of
`review/staging/*`, the following job that is using dynamic environments `review/staging/*`, the following job that is using dynamic environments
is going to be created, based on the matching variable expression: is going to be created, based on the matching variable expression:
......
...@@ -327,7 +327,7 @@ Refs strategy equals to simplified only/except configuration, whereas ...@@ -327,7 +327,7 @@ Refs strategy equals to simplified only/except configuration, whereas
kubernetes strategy accepts only `active` keyword. kubernetes strategy accepts only `active` keyword.
`variables` keyword is used to define variables expressions. In other words `variables` keyword is used to define variables expressions. In other words
you can use predefined variables / secret variables / project / group or you can use predefined variables / project / group or
environment-scoped variables to define an expression GitLab is going to environment-scoped variables to define an expression GitLab is going to
evaluate in order to decide whether a job should be created or not. evaluate in order to decide whether a job should be created or not.
...@@ -1428,7 +1428,7 @@ Runner itself](../variables/README.md#predefined-variables-environment-variables ...@@ -1428,7 +1428,7 @@ Runner itself](../variables/README.md#predefined-variables-environment-variables
One example would be `CI_COMMIT_REF_NAME` which has the value of One example would be `CI_COMMIT_REF_NAME` which has the value of
the branch or tag name for which project is built. Apart from the variables the branch or tag name for which project is built. Apart from the variables
you can set in `.gitlab-ci.yml`, there are also the so called you can set in `.gitlab-ci.yml`, there are also the so called
[secret variables](../variables/README.md#secret-variables) [Variables](../variables/README.md#variables)
which can be set in GitLab's UI. which can be set in GitLab's UI.
[Learn more about variables and their priority.][variables] [Learn more about variables and their priority.][variables]
......
...@@ -32,6 +32,8 @@ description: 'Learn how to contribute to GitLab.' ...@@ -32,6 +32,8 @@ description: 'Learn how to contribute to GitLab.'
- [GitLab utilities](utilities.md) - [GitLab utilities](utilities.md)
- [API styleguide](api_styleguide.md) Use this styleguide if you are - [API styleguide](api_styleguide.md) Use this styleguide if you are
contributing to the API. contributing to the API.
- [GrapQL API styleguide](api_graphql_styleguide.md) Use this
styleguide if you are contribution to the [GraphQL API](../api/graphql/index.md)
- [Sidekiq guidelines](sidekiq_style_guide.md) for working with Sidekiq workers - [Sidekiq guidelines](sidekiq_style_guide.md) for working with Sidekiq workers
- [Working with Gitaly](gitaly.md) - [Working with Gitaly](gitaly.md)
- [Manage feature flags](feature_flags.md) - [Manage feature flags](feature_flags.md)
......
# GraphQL API
## Authentication
Authentication happens through the `GraphqlController`, right now this
uses the same authentication as the Rails application. So the session
can be shared.
It is also possible to add a `private_token` to the querystring, or
add a `HTTP_PRIVATE_TOKEN` header.
### Authorization
Fields can be authorized using the same abilities used in the Rails
app. This can be done using the `authorize` helper:
```ruby
module Types
class QueryType < BaseObject
graphql_name 'Query'
field :project, Types::ProjectType, null: true, resolver: Resolvers::ProjectResolver do
authorize :read_project
end
end
```
The object found by the resolve call is used for authorization.
This works for authorizing a single record, for authorizing
collections, we should only load what the currently authenticated user
is allowed to view. Preferably we use our existing finders for that.
## Types
When exposing a model through the GraphQL API, we do so by creating a
new type in `app/graphql/types`.
When exposing properties in a type, make sure to keep the logic inside
the definition as minimal as possible. Instead, consider moving any
logic into a presenter:
```ruby
class Types::MergeRequestType < BaseObject
present_using MergeRequestPresenter
name 'MergeRequest'
end
```
An existing presenter could be used, but it is also possible to create
a new presenter specifically for GraphQL.
The presenter is initialized using the object resolved by a field, and
the context.
## Resolvers
To find objects to display in a field, we can add resolvers to
`app/graphql/resolvers`.
Arguments can be defined within the resolver, those arguments will be
made available to the fields using the resolver.
We already have a `FullPathLoader` that can be included in other
resolvers to quickly find Projects and Namespaces which will have a
lot of dependant objects.
To limit the amount of queries performed, we can use `BatchLoader`.
## Testing
_full stack_ tests for a graphql query or mutation live in
`spec/requests/api/graphql`.
When adding a query, the `a working graphql query` shared example can
be used to test if the query renders valid results.
Using the `GraphqlHelpers#all_graphql_fields_for`-helper, a query
including all available fields can be constructed. This makes it easy
to add a test rendering all possible fields for a query.
...@@ -349,7 +349,7 @@ In this case: ...@@ -349,7 +349,7 @@ In this case:
### Fake tokens ### Fake tokens
There may be times where a token is needed to demonstrate an API call using There may be times where a token is needed to demonstrate an API call using
cURL or a secret variable used in CI. It is strongly advised not to use real cURL or a variable used in CI. It is strongly advised not to use real
tokens in documentation even if the probability of a token being exploited is tokens in documentation even if the probability of a token being exploited is
low. low.
......
...@@ -57,7 +57,7 @@ $ sudo gitlab-rails runner "Service.where(type: ['JenkinsService', 'JenkinsDepre ...@@ -57,7 +57,7 @@ $ sudo gitlab-rails runner "Service.where(type: ['JenkinsService', 'JenkinsDepre
$ bundle exec rails runner "Service.where(type: ['JenkinsService', 'JenkinsDeprecatedService', 'GithubService']).delete_all" production $ bundle exec rails runner "Service.where(type: ['JenkinsService', 'JenkinsDeprecatedService', 'GithubService']).delete_all" production
``` ```
### Secret variables environment scopes ### Variables environment scopes
If you're using this feature and there are variables sharing the same If you're using this feature and there are variables sharing the same
key, but they have different scopes in a project, then you might want to key, but they have different scopes in a project, then you might want to
......
...@@ -492,8 +492,8 @@ directory (repositories, uploads). ...@@ -492,8 +492,8 @@ directory (repositories, uploads).
To restore a backup, you will also need to restore `/etc/gitlab/gitlab-secrets.json` To restore a backup, you will also need to restore `/etc/gitlab/gitlab-secrets.json`
(for Omnibus packages) or `/home/git/gitlab/.secret` (for installations (for Omnibus packages) or `/home/git/gitlab/.secret` (for installations
from source). This file contains the database encryption key, from source). This file contains the database encryption key,
[CI secret variables](../ci/variables/README.md#secret-variables), and [CI/CD variables](../ci/variables/README.md#variables), and
secret variables used for [two-factor authentication](../user/profile/account/two_factor_authentication.md). variables used for [two-factor authentication](../user/profile/account/two_factor_authentication.md).
If you fail to restore this encryption key file along with the application data If you fail to restore this encryption key file along with the application data
backup, users with two-factor authentication enabled and GitLab Runners will backup, users with two-factor authentication enabled and GitLab Runners will
lose access to your GitLab server. lose access to your GitLab server.
......
...@@ -452,7 +452,7 @@ repo or by specifying a project variable: ...@@ -452,7 +452,7 @@ repo or by specifying a project variable:
file in it, Auto DevOps will detect the chart and use it instead of the [default file in it, Auto DevOps will detect the chart and use it instead of the [default
one](https://gitlab.com/charts/charts.gitlab.io/tree/master/charts/auto-deploy-app). one](https://gitlab.com/charts/charts.gitlab.io/tree/master/charts/auto-deploy-app).
This can be a great way to control exactly how your application is deployed. This can be a great way to control exactly how your application is deployed.
- **Project variable** - Create a [project variable](../../ci/variables/README.md#secret-variables) - **Project variable** - Create a [variable](../../ci/variables/README.md#variables)
`AUTO_DEVOPS_CHART` with the URL of a custom chart to use. `AUTO_DEVOPS_CHART` with the URL of a custom chart to use.
### Customizing `.gitlab-ci.yml` ### Customizing `.gitlab-ci.yml`
...@@ -524,7 +524,7 @@ also be customized, and you can easily use a [custom buildpack](#custom-buildpac ...@@ -524,7 +524,7 @@ also be customized, and you can easily use a [custom buildpack](#custom-buildpac
TIP: **Tip:** TIP: **Tip:**
Set up the replica variables using a Set up the replica variables using a
[project variable](../../ci/variables/README.md#secret-variables) [project variable](../../ci/variables/README.md#variables)
and scale your application by just redeploying it! and scale your application by just redeploying it!
CAUTION: **Caution:** CAUTION: **Caution:**
...@@ -599,7 +599,7 @@ staging environment and deploy to production manually. For this scenario, the ...@@ -599,7 +599,7 @@ staging environment and deploy to production manually. For this scenario, the
`STAGING_ENABLED` environment variable was introduced. `STAGING_ENABLED` environment variable was introduced.
If `STAGING_ENABLED` is defined in your project (e.g., set `STAGING_ENABLED` to If `STAGING_ENABLED` is defined in your project (e.g., set `STAGING_ENABLED` to
`1` as a secret variable), then the application will be automatically deployed `1` as a variable), then the application will be automatically deployed
to a `staging` environment, and a `production_manual` job will be created for to a `staging` environment, and a `production_manual` job will be created for
you when you're ready to manually deploy to production. you when you're ready to manually deploy to production.
...@@ -612,7 +612,7 @@ A [canary environment](https://docs.gitlab.com/ee/user/project/canary_deployment ...@@ -612,7 +612,7 @@ A [canary environment](https://docs.gitlab.com/ee/user/project/canary_deployment
before any changes are deployed to production. before any changes are deployed to production.
If `CANARY_ENABLED` is defined in your project (e.g., set `CANARY_ENABLED` to If `CANARY_ENABLED` is defined in your project (e.g., set `CANARY_ENABLED` to
`1` as a secret variable) then two manual jobs will be created: `1` as a variable) then two manual jobs will be created:
- `canary` which will deploy the application to the canary environment - `canary` which will deploy the application to the canary environment
- `production_manual` which is to be used by you when you're ready to manually - `production_manual` which is to be used by you when you're ready to manually
...@@ -628,7 +628,7 @@ This will allow you to first check how the app is behaving, and later manually ...@@ -628,7 +628,7 @@ This will allow you to first check how the app is behaving, and later manually
increasing the rollout up to 100%. increasing the rollout up to 100%.
If `INCREMENTAL_ROLLOUT_ENABLED` is defined in your project (e.g., set If `INCREMENTAL_ROLLOUT_ENABLED` is defined in your project (e.g., set
`INCREMENTAL_ROLLOUT_ENABLED` to `1` as a secret variable), then instead of the `INCREMENTAL_ROLLOUT_ENABLED` to `1` as a variable), then instead of the
standard `production` job, 4 different standard `production` job, 4 different
[manual jobs](../../ci/pipelines.md#manual-actions-from-the-pipeline-graph) [manual jobs](../../ci/pipelines.md#manual-actions-from-the-pipeline-graph)
will be created: will be created:
......
...@@ -233,7 +233,7 @@ When adding more than one Kubernetes clusters to your project, you need to ...@@ -233,7 +233,7 @@ When adding more than one Kubernetes clusters to your project, you need to
differentiate them with an environment scope. The environment scope associates differentiate them with an environment scope. The environment scope associates
clusters and [environments](../../../ci/environments.md) in an 1:1 relationship clusters and [environments](../../../ci/environments.md) in an 1:1 relationship
similar to how the similar to how the
[environment-specific variables](../../../ci/variables/README.md#limiting-environment-scopes-of-secret-variables) [environment-specific variables](../../../ci/variables/README.md#limiting-environment-scopes-of-variables)
work. work.
The default environment scope is `*`, which means all jobs, regardless of their The default environment scope is `*`, which means all jobs, regardless of their
......
...@@ -34,6 +34,7 @@ The following languages and frameworks are supported. ...@@ -34,6 +34,7 @@ The following languages and frameworks are supported.
| Scala (sbt) | [find-sec-bugs](https://find-sec-bugs.github.io/) | | Scala (sbt) | [find-sec-bugs](https://find-sec-bugs.github.io/) |
| Go (experimental) | [Go AST Scanner](https://github.com/GoASTScanner/gas) | | Go (experimental) | [Go AST Scanner](https://github.com/GoASTScanner/gas) |
| PHP | [phpcs-security-audit](https://github.com/FloeDesignTechnologies/phpcs-security-audit) | | PHP | [phpcs-security-audit](https://github.com/FloeDesignTechnologies/phpcs-security-audit) |
| .NET | [Security Code Scan](https://security-code-scan.github.io) |
## How it works ## How it works
......
<script> <script>
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import popover from '~/vue_shared/directives/popover'; import popover from '~/vue_shared/directives/popover';
import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import StackedProgressBar from '~/vue_shared/components/stacked_progress_bar.vue'; import StackedProgressBar from '~/vue_shared/components/stacked_progress_bar.vue';
...@@ -18,6 +19,7 @@ ...@@ -18,6 +19,7 @@
}, },
directives: { directives: {
popover, popover,
tooltip,
}, },
props: { props: {
itemTitle: { itemTitle: {
...@@ -33,6 +35,16 @@ ...@@ -33,6 +35,16 @@
type: [Object, String, Number], type: [Object, String, Number],
required: true, required: true,
}, },
itemValueStale: {
type: Boolean,
required: false,
default: false,
},
itemValueStaleTooltip: {
type: String,
required: false,
default: '',
},
successLabel: { successLabel: {
type: String, type: String,
required: false, required: false,
...@@ -132,8 +144,10 @@ ...@@ -132,8 +144,10 @@
<div <div
v-if="isValueTypeGraph" v-if="isValueTypeGraph"
class="node-detail-value" class="node-detail-value"
:class="{ 'd-flex': itemValueStale }"
> >
<stacked-progress-bar <stacked-progress-bar
:css-class="itemValueStale ? 'flex-fill' : ''"
:success-label="successLabel" :success-label="successLabel"
:failure-label="failureLabel" :failure-label="failureLabel"
:neutral-label="neutralLabel" :neutral-label="neutralLabel"
...@@ -141,6 +155,14 @@ ...@@ -141,6 +155,14 @@
:failure-count="itemValue.failureCount" :failure-count="itemValue.failureCount"
:total-count="itemValue.totalCount" :total-count="itemValue.totalCount"
/> />
<icon
v-tooltip
v-show="itemValueStale"
name="time-out"
css-classes="prepend-left-10 detail-value-stale-icon"
data-container="body"
:title="itemValueStaleTooltip"
/>
</div> </div>
<template v-if="isValueTypeCustom"> <template v-if="isValueTypeCustom">
<geo-node-sync-settings <geo-node-sync-settings
......
...@@ -4,6 +4,8 @@ ...@@ -4,6 +4,8 @@
import { VALUE_TYPE } from '../../constants'; import { VALUE_TYPE } from '../../constants';
import DetailsSectionMixin from '../../mixins/details_section_mixin';
import GeoNodeDetailItem from '../geo_node_detail_item.vue'; import GeoNodeDetailItem from '../geo_node_detail_item.vue';
import SectionRevealButton from './section_reveal_button.vue'; import SectionRevealButton from './section_reveal_button.vue';
...@@ -13,6 +15,9 @@ ...@@ -13,6 +15,9 @@
SectionRevealButton, SectionRevealButton,
GeoNodeDetailItem, GeoNodeDetailItem,
}, },
mixins: [
DetailsSectionMixin,
],
props: { props: {
nodeDetails: { nodeDetails: {
type: Object, type: Object,
...@@ -98,14 +103,12 @@ ...@@ -98,14 +103,12 @@
class="col-md-6 prepend-left-15 prepend-top-10 section-items-container" class="col-md-6 prepend-left-15 prepend-top-10 section-items-container"
> >
<geo-node-detail-item <geo-node-detail-item
v-for="(nodeDetailItem, index) in nodeDetailItems" :item-title="s__('GeoNodes|Storage config')"
:key="index" :item-value="storageShardsStatus"
:css-class="nodeDetailItem.cssClass" :item-value-type="$options.valueType.PLAIN"
:item-title="nodeDetailItem.itemTitle" :item-value-stale="statusInfoStale"
:item-value="nodeDetailItem.itemValue" :item-value-stale-tooltip="statusInfoStaleMessage"
:item-value-type="nodeDetailItem.itemValueType" :css-class="storageShardsCssClass"
:success-label="nodeDetailItem.successLabel"
:neutral-label="nodeDetailItem.neutraLabel"
/> />
</div> </div>
</div> </div>
......
...@@ -4,6 +4,8 @@ ...@@ -4,6 +4,8 @@
import { VALUE_TYPE, CUSTOM_TYPE } from '../../constants'; import { VALUE_TYPE, CUSTOM_TYPE } from '../../constants';
import DetailsSectionMixin from '../../mixins/details_section_mixin';
import GeoNodeDetailItem from '../geo_node_detail_item.vue'; import GeoNodeDetailItem from '../geo_node_detail_item.vue';
import SectionRevealButton from './section_reveal_button.vue'; import SectionRevealButton from './section_reveal_button.vue';
...@@ -12,6 +14,9 @@ ...@@ -12,6 +14,9 @@
SectionRevealButton, SectionRevealButton,
GeoNodeDetailItem, GeoNodeDetailItem,
}, },
mixins: [
DetailsSectionMixin,
],
props: { props: {
nodeDetails: { nodeDetails: {
type: Object, type: Object,
...@@ -135,6 +140,8 @@ ...@@ -135,6 +140,8 @@
:item-title="nodeDetailItem.itemTitle" :item-title="nodeDetailItem.itemTitle"
:item-value="nodeDetailItem.itemValue" :item-value="nodeDetailItem.itemValue"
:item-value-type="nodeDetailItem.itemValueType" :item-value-type="nodeDetailItem.itemValueType"
:item-value-stale="statusInfoStale"
:item-value-stale-tooltip="statusInfoStaleMessage"
:custom-type="nodeDetailItem.customType" :custom-type="nodeDetailItem.customType"
:event-type-log-status="nodeDetailItem.eventTypeLogStatus" :event-type-log-status="nodeDetailItem.eventTypeLogStatus"
/> />
......
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
import { VALUE_TYPE, HELP_INFO_URL } from '../../constants'; import { VALUE_TYPE, HELP_INFO_URL } from '../../constants';
import DetailsSectionMixin from '../../mixins/details_section_mixin';
import GeoNodeDetailItem from '../geo_node_detail_item.vue'; import GeoNodeDetailItem from '../geo_node_detail_item.vue';
import SectionRevealButton from './section_reveal_button.vue'; import SectionRevealButton from './section_reveal_button.vue';
...@@ -11,6 +13,9 @@ ...@@ -11,6 +13,9 @@
GeoNodeDetailItem, GeoNodeDetailItem,
SectionRevealButton, SectionRevealButton,
}, },
mixins: [
DetailsSectionMixin,
],
props: { props: {
nodeDetails: { nodeDetails: {
type: Object, type: Object,
...@@ -122,6 +127,8 @@ ...@@ -122,6 +127,8 @@
:item-title="nodeDetailItem.itemTitle" :item-title="nodeDetailItem.itemTitle"
:item-value="nodeDetailItem.itemValue" :item-value="nodeDetailItem.itemValue"
:item-value-type="nodeDetailItem.itemValueType" :item-value-type="nodeDetailItem.itemValueType"
:item-value-stale="statusInfoStale"
:item-value-stale-tooltip="statusInfoStaleMessage"
:success-label="nodeDetailItem.successLabel" :success-label="nodeDetailItem.successLabel"
:neutral-label="nodeDetailItem.neutraLabel" :neutral-label="nodeDetailItem.neutraLabel"
:failure-label="nodeDetailItem.failureLabel" :failure-label="nodeDetailItem.failureLabel"
......
...@@ -28,4 +28,6 @@ export const TIME_DIFF = { ...@@ -28,4 +28,6 @@ export const TIME_DIFF = {
HOUR: 3600, HOUR: 3600,
}; };
export const STATUS_DELAY_THRESHOLD_MS = 60000;
export const HELP_INFO_URL = 'https://docs.gitlab.com/ee/administration/geo/disaster_recovery/background_verification.html#repository-verification'; export const HELP_INFO_URL = 'https://docs.gitlab.com/ee/administration/geo/disaster_recovery/background_verification.html#repository-verification';
import { s__, sprintf } from '~/locale';
import timeAgoMixin from '~/vue_shared/mixins/timeago';
import { STATUS_DELAY_THRESHOLD_MS } from '../constants';
export default {
mixins: [timeAgoMixin],
computed: {
statusInfoStale() {
const elapsedMilliseconds = Math.abs(this.nodeDetails.statusCheckTimestamp - Date.now());
return elapsedMilliseconds > STATUS_DELAY_THRESHOLD_MS;
},
statusInfoStaleMessage() {
return sprintf(s__('GeoNodes|Data is out of date from %{timeago}'), {
timeago: this.timeFormated(
this.nodeDetails.statusCheckTimestamp,
),
});
},
},
};
...@@ -68,6 +68,7 @@ export default class GeoNodesStore { ...@@ -68,6 +68,7 @@ export default class GeoNodesStore {
revision: rawNodeDetails.revision, revision: rawNodeDetails.revision,
primaryVersion: rawNodeDetails.primaryVersion, primaryVersion: rawNodeDetails.primaryVersion,
primaryRevision: rawNodeDetails.primaryRevision, primaryRevision: rawNodeDetails.primaryRevision,
statusCheckTimestamp: rawNodeDetails.last_successful_status_check_timestamp * 1000,
replicationSlotWAL: rawNodeDetails.replication_slots_max_retained_wal_bytes, replicationSlotWAL: rawNodeDetails.replication_slots_max_retained_wal_bytes,
missingOAuthApplication: rawNodeDetails.missing_oauth_application || false, missingOAuthApplication: rawNodeDetails.missing_oauth_application || false,
syncStatusUnavailable: rawNodeDetails.sync_status_unavailable || false, syncStatusUnavailable: rawNodeDetails.sync_status_unavailable || false,
......
...@@ -51,6 +51,10 @@ ...@@ -51,6 +51,10 @@
top: $system-header-height + $header-height; top: $system-header-height + $header-height;
} }
.content-wrapper {
margin-top: $system-header-height + $header-height;
}
.ide.nav-only { .ide.nav-only {
// body element on WebIDE page // body element on WebIDE page
padding-top: $header-height + $system-header-height; padding-top: $header-height + $system-header-height;
...@@ -67,6 +71,8 @@ ...@@ -67,6 +71,8 @@
// right sidebar eg: mr page // right sidebar eg: mr page
.nav-sidebar, .nav-sidebar,
.right-sidebar, .right-sidebar,
// web IDE status bar
.ide-status-bar,
// navless pages' footer eg: login page // navless pages' footer eg: login page
// navless pages' footer border eg: login page // navless pages' footer border eg: login page
&.devise-layout-html body .footer-container, &.devise-layout-html body .footer-container,
...@@ -176,7 +182,9 @@ ...@@ -176,7 +182,9 @@
} }
.with-performance-bar.with-system-header.with-system-footer & { .with-performance-bar.with-system-header.with-system-footer & {
@include ide-height-with((system-header: true, system-footer: true, performance: true, flash: true)); @include ide-height-with(
(system-header: true, system-footer: true, performance: true, flash: true)
);
} }
} }
} }
...@@ -115,6 +115,10 @@ ...@@ -115,6 +115,10 @@
margin-top: 4px; margin-top: 4px;
} }
.detail-value-stale-icon {
color: $gl-warning;
}
.node-detail-value-bold { .node-detail-value-bold {
font-weight: $gl-font-weight-bold; font-weight: $gl-font-weight-bold;
} }
......
...@@ -3,6 +3,19 @@ module EE ...@@ -3,6 +3,19 @@ module EE
module GroupMembersController module GroupMembersController
extend ActiveSupport::Concern extend ActiveSupport::Concern
class_methods do
extend ::Gitlab::Utils::Override
override :admin_not_required_endpoints
def admin_not_required_endpoints
super.concat(%i[update override])
end
end
included do
before_action :authorize_update_group_member!, only: [:update, :override]
end
# rubocop:disable Gitlab/ModuleWithInstanceVariables # rubocop:disable Gitlab/ModuleWithInstanceVariables
def override def override
member = @group.members.find_by!(id: params[:id]) member = @group.members.find_by!(id: params[:id])
......
module EE
module Groups
module MilestonesController
extend ::Gitlab::Utils::Override
override :legacy_milestones
def legacy_milestones
params[:only_group_milestones] ? [] : super
end
end
end
end
---
title: Show status information stale icon in Geo admin dashboard
merge_request: 5653
author:
type: added
---
title: Fix breadcrumbs being covered by System Header message
merge_request:
author:
type: fixed
---
title: Fix Web IDE status bar if System Footer message is present
merge_request:
author:
type: fixed
module Constraints
class FeatureConstrainer
attr_reader :feature
def initialize(feature)
@feature = feature
end
def matches?(_request)
Feature.enabled?(feature)
end
end
end
module Gitlab
module Graphql
StandardGraphqlError = Class.new(StandardError)
end
end
module Gitlab
module Graphql
# Allow fields to declare permissions their objects must have. The field
# will be set to nil unless all required permissions are present.
module Authorize
extend ActiveSupport::Concern
def self.use(schema_definition)
schema_definition.instrument(:field, Instrumentation.new)
end
def required_permissions
@required_permissions ||= []
end
def authorize(*permissions)
required_permissions.concat(permissions)
end
end
end
end
module Gitlab
module Graphql
module Authorize
class Instrumentation
# Replace the resolver for the field with one that will only return the
# resolved object if the permissions check is successful.
#
# Collections are not supported. Apply permissions checks for those at the
# database level instead, to avoid loading superfluous data from the DB
def instrument(_type, field)
field_definition = field.metadata[:type_class]
return field unless field_definition.respond_to?(:required_permissions)
return field if field_definition.required_permissions.empty?
old_resolver = field.resolve_proc
new_resolver = -> (obj, args, ctx) do
resolved_obj = old_resolver.call(obj, args, ctx)
checker = build_checker(ctx[:current_user], field_definition.required_permissions)
if resolved_obj.respond_to?(:then)
resolved_obj.then(&checker)
else
checker.call(resolved_obj)
end
end
field.redefine do
resolve(new_resolver)
end
end
private
def build_checker(current_user, abilities)
proc do |obj|
# Load the elements if they weren't loaded by BatchLoader yet
obj = obj.sync if obj.respond_to?(:sync)
obj if abilities.all? { |ability| Ability.allowed?(current_user, ability, obj) }
end
end
end
end
end
end
module Gitlab
module Graphql
module Present
extend ActiveSupport::Concern
prepended do
def self.present_using(kls)
@presenter_class = kls
end
def self.presenter_class
@presenter_class
end
end
def self.use(schema_definition)
schema_definition.instrument(:field, Instrumentation.new)
end
end
end
end
module Gitlab
module Graphql
module Present
class Instrumentation
def instrument(type, field)
presented_in = field.metadata[:type_class].owner
return field unless presented_in.respond_to?(:presenter_class)
return field unless presented_in.presenter_class
old_resolver = field.resolve_proc
resolve_with_presenter = -> (presented_type, args, context) do
object = presented_type.object
presenter = presented_in.presenter_class.new(object, **context.to_h)
old_resolver.call(presenter, args, context)
end
field.redefine do
resolve(resolve_with_presenter)
end
end
end
end
end
end
module Gitlab
module Graphql
class Variables
Invalid = Class.new(Gitlab::Graphql::StandardGraphqlError)
def initialize(param)
@param = param
end
def to_h
ensure_hash(@param)
end
private
# Handle form data, JSON body, or a blank value
def ensure_hash(ambiguous_param)
case ambiguous_param
when String
if ambiguous_param.present?
ensure_hash(JSON.parse(ambiguous_param))
else
{}
end
when Hash, ActionController::Parameters
ambiguous_param
when nil
{}
else
raise Invalid, "Unexpected parameter: #{ambiguous_param}"
end
rescue JSON::ParserError => e
raise Invalid.new(e)
end
end
end
end
require 'spec_helper'
describe GraphqlController do
describe 'execute' do
let(:user) { nil }
before do
sign_in(user) if user
run_test_query!
end
subject { query_response }
context 'graphql is disabled by feature flag' do
let(:user) { nil }
before do
stub_feature_flags(graphql: false)
end
it 'returns 404' do
run_test_query!
expect(response).to have_gitlab_http_status(404)
end
end
context 'signed out' do
let(:user) { nil }
it 'runs the query with current_user: nil' do
is_expected.to eq('echo' => 'nil says: test success')
end
end
context 'signed in' do
let(:user) { create(:user, username: 'Simon') }
it 'runs the query with current_user set' do
is_expected.to eq('echo' => '"Simon" says: test success')
end
end
context 'invalid variables' do
it 'returns an error' do
run_test_query!(variables: "This is not JSON")
expect(response).to have_gitlab_http_status(422)
expect(json_response['errors'].first['message']).not_to be_nil
end
end
end
# Chosen to exercise all the moving parts in GraphqlController#execute
def run_test_query!(variables: { 'text' => 'test success' })
query = <<~QUERY
query Echo($text: String) {
echo(text: $text)
}
QUERY
post :execute, query: query, operationName: 'Echo', variables: variables
end
def query_response
json_response['data']
end
end
require 'spec_helper'
describe GitlabSchema do
it 'uses batch loading' do
expect(field_instrumenters).to include(BatchLoader::GraphQL)
end
it 'enables the preload instrumenter' do
expect(field_instrumenters).to include(BatchLoader::GraphQL)
end
it 'enables the authorization instrumenter' do
expect(field_instrumenters).to include(instance_of(::Gitlab::Graphql::Authorize::Instrumentation))
end
it 'enables using presenters' do
expect(field_instrumenters).to include(instance_of(::Gitlab::Graphql::Present::Instrumentation))
end
it 'has the base mutation' do
pending('Adding an empty mutation breaks the documentation explorer')
expect(described_class.mutation).to eq(::Types::MutationType.to_graphql)
end
it 'has the base query' do
expect(described_class.query).to eq(::Types::QueryType.to_graphql)
end
def field_instrumenters
described_class.instrumenters[:field]
end
end
require 'spec_helper'
describe Resolvers::MergeRequestResolver do
include GraphqlHelpers
set(:project) { create(:project, :repository) }
set(:merge_request_1) { create(:merge_request, :simple, source_project: project, target_project: project) }
set(:merge_request_2) { create(:merge_request, :rebased, source_project: project, target_project: project) }
set(:other_project) { create(:project, :repository) }
set(:other_merge_request) { create(:merge_request, source_project: other_project, target_project: other_project) }
let(:full_path) { project.full_path }
let(:iid_1) { merge_request_1.iid }
let(:iid_2) { merge_request_2.iid }
let(:other_full_path) { other_project.full_path }
let(:other_iid) { other_merge_request.iid }
describe '#resolve' do
it 'batch-resolves merge requests by target project full path and IID' do
path = full_path # avoid database query
result = batch(max_queries: 2) do
[resolve_mr(path, iid_1), resolve_mr(path, iid_2)]
end
expect(result).to contain_exactly(merge_request_1, merge_request_2)
end
it 'can batch-resolve merge requests from different projects' do
path = project.full_path # avoid database queries
other_path = other_full_path
result = batch(max_queries: 3) do
[resolve_mr(path, iid_1), resolve_mr(path, iid_2), resolve_mr(other_path, other_iid)]
end
expect(result).to contain_exactly(merge_request_1, merge_request_2, other_merge_request)
end
it 'resolves an unknown iid to nil' do
result = batch { resolve_mr(full_path, -1) }
expect(result).to be_nil
end
it 'resolves a known iid for an unknown full_path to nil' do
result = batch { resolve_mr('unknown/project', iid_1) }
expect(result).to be_nil
end
end
def resolve_mr(full_path, iid)
resolve(described_class, args: { full_path: full_path, iid: iid })
end
end
require 'spec_helper'
describe Resolvers::ProjectResolver do
include GraphqlHelpers
set(:project1) { create(:project) }
set(:project2) { create(:project) }
set(:other_project) { create(:project) }
describe '#resolve' do
it 'batch-resolves projects by full path' do
paths = [project1.full_path, project2.full_path]
result = batch(max_queries: 1) do
paths.map { |path| resolve_project(path) }
end
expect(result).to contain_exactly(project1, project2)
end
it 'resolves an unknown full_path to nil' do
result = batch { resolve_project('unknown/project') }
expect(result).to be_nil
end
end
def resolve_project(full_path)
resolve(described_class, args: { full_path: full_path })
end
end
require 'spec_helper'
describe GitlabSchema.types['Project'] do
it { expect(described_class.graphql_name).to eq('Project') }
end
require 'spec_helper'
describe GitlabSchema.types['Query'] do
it 'is called Query' do
expect(described_class.graphql_name).to eq('Query')
end
it { is_expected.to have_graphql_fields(:project, :merge_request, :echo) }
describe 'project field' do
subject { described_class.fields['project'] }
it 'finds projects by full path' do
is_expected.to have_graphql_arguments(:full_path)
is_expected.to have_graphql_type(Types::ProjectType)
is_expected.to have_graphql_resolver(Resolvers::ProjectResolver)
end
it 'authorizes with read_project' do
is_expected.to require_graphql_authorizations(:read_project)
end
end
describe 'merge_request field' do
subject { described_class.fields['mergeRequest'] }
it 'finds MRs by project and IID' do
is_expected.to have_graphql_arguments(:full_path, :iid)
is_expected.to have_graphql_type(Types::MergeRequestType)
is_expected.to have_graphql_resolver(Resolvers::MergeRequestResolver)
end
it 'authorizes with read_merge_request' do
is_expected.to require_graphql_authorizations(:read_merge_request)
end
end
end
require 'spec_helper'
describe GitlabSchema.types['Time'] do
let(:iso) { "2018-06-04T15:23:50+02:00" }
let(:time) { Time.parse(iso) }
it { expect(described_class.graphql_name).to eq('Time') }
it 'coerces Time object into ISO 8601' do
expect(described_class.coerce_isolated_result(time)).to eq(iso)
end
it 'coerces an ISO-time into Time object' do
expect(described_class.coerce_isolated_input(iso)).to eq(time)
end
end
...@@ -56,6 +56,22 @@ describe('GeoNodeDetailItemComponent', () => { ...@@ -56,6 +56,22 @@ describe('GeoNodeDetailItemComponent', () => {
vm.$destroy(); vm.$destroy();
}); });
it('renders stale information status icon when `itemValueStale` prop is true', () => {
const itemValueStaleTooltip = 'Data is out of date from 8 hours ago';
const vm = createComponent({
itemValueType: VALUE_TYPE.GRAPH,
itemValue: { successCount: 5, failureCount: 3, totalCount: 10 },
itemValueStale: true,
itemValueStaleTooltip,
});
const iconEl = vm.$el.querySelector('.detail-value-stale-icon');
expect(iconEl).not.toBeNull();
expect(iconEl.dataset.originalTitle).toBe(itemValueStaleTooltip);
expect(iconEl.querySelector('use').getAttribute('xlink:href')).toContain('time-out');
vm.$destroy();
});
it('renders sync settings item value', () => { it('renders sync settings item value', () => {
const vm = createComponent({ const vm = createComponent({
itemValueType: VALUE_TYPE.CUSTOM, itemValueType: VALUE_TYPE.CUSTOM,
......
import Vue from 'vue';
import DetailsSectionMixin from 'ee/geo_nodes/mixins/details_section_mixin';
import { STATUS_DELAY_THRESHOLD_MS } from 'ee/geo_nodes/constants';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { mockNodeDetails } from '../mock_data';
const createComponent = (nodeDetails = mockNodeDetails) => {
const Component = Vue.extend({
template: '<div></div>',
mixins: [DetailsSectionMixin],
data() {
return { nodeDetails };
},
});
return mountComponent(Component);
};
describe('DetailsSectionMixin', () => {
let vm;
afterEach(() => {
vm.$destroy();
});
describe('computed', () => {
describe('statusInfoStale', () => {
it('returns true when `nodeDetails.statusCheckTimestamp` is past the value of STATUS_DELAY_THRESHOLD_MS', () => {
// Move statusCheckTimestamp to 2 minutes in the past
const statusCheckTimestamp = new Date(Date.now() - STATUS_DELAY_THRESHOLD_MS * 2).getTime();
vm = createComponent(Object.assign({}, mockNodeDetails, { statusCheckTimestamp }));
expect(vm.statusInfoStale).toBe(true);
});
it('returns false when `nodeDetails.statusCheckTimestamp` is under the value of STATUS_DELAY_THRESHOLD_MS', () => {
// Move statusCheckTimestamp to 30 seconds in the past
const statusCheckTimestamp = new Date(Date.now() - STATUS_DELAY_THRESHOLD_MS / 2).getTime();
vm = createComponent(Object.assign({}, mockNodeDetails, { statusCheckTimestamp }));
expect(vm.statusInfoStale).toBe(false);
});
});
describe('statusInfoStaleMessage', () => {
it('returns stale information message containing the duration elapsed', () => {
// Move statusCheckTimestamp to 1 minute in the past
const statusCheckTimestamp = new Date(Date.now() - STATUS_DELAY_THRESHOLD_MS).getTime();
vm = createComponent(Object.assign({}, mockNodeDetails, { statusCheckTimestamp }));
expect(vm.statusInfoStaleMessage).toBe('Data is out of date from about a minute ago');
});
});
});
});
...@@ -148,6 +148,7 @@ export const mockNodeDetails = { ...@@ -148,6 +148,7 @@ export const mockNodeDetails = {
revision: 'b93c51849b', revision: 'b93c51849b',
primaryVersion: '10.4.0-pre', primaryVersion: '10.4.0-pre',
primaryRevision: 'b93c51849b', primaryRevision: 'b93c51849b',
statusCheckTimestamp: 1515142330,
replicationSlotWAL: 502658737, replicationSlotWAL: 502658737,
missingOAuthApplication: false, missingOAuthApplication: false,
storageShardsMatch: false, storageShardsMatch: false,
......
...@@ -90,11 +90,13 @@ describe Gitlab::PathRegex do ...@@ -90,11 +90,13 @@ describe Gitlab::PathRegex do
let(:routes_not_starting_in_wildcard) { routes_without_format.select { |p| p !~ %r{^/[:*]} } } let(:routes_not_starting_in_wildcard) { routes_without_format.select { |p| p !~ %r{^/[:*]} } }
let(:top_level_words) do let(:top_level_words) do
words = routes_not_starting_in_wildcard.map do |route| routes_not_starting_in_wildcard
route.split('/')[1] .map { |route| route.split('/')[1] }
end.compact .concat(ee_top_level_words)
.concat(files_in_public)
(words + ee_top_level_words + files_in_public + Array(API::API.prefix.to_s)).uniq .concat(Array(API::API.prefix.to_s))
.compact
.uniq
end end
let(:ee_top_level_words) do let(:ee_top_level_words) do
......
require 'spec_helper'
describe 'getting merge request information' do
include GraphqlHelpers
let(:project) { create(:project, :repository) }
let(:merge_request) { create(:merge_request, source_project: project) }
let(:current_user) { create(:user) }
let(:query) do
attributes = {
'fullPath' => merge_request.project.full_path,
'iid' => merge_request.iid
}
graphql_query_for('mergeRequest', attributes)
end
context 'when the user has access to the merge request' do
before do
project.add_developer(current_user)
post_graphql(query, current_user: current_user)
end
it 'returns the merge request' do
expect(graphql_data['mergeRequest']).not_to be_nil
end
# This is a field coming from the `MergeRequestPresenter`
it 'includes a web_url' do
expect(graphql_data['mergeRequest']['webUrl']).to be_present
end
it_behaves_like 'a working graphql query'
end
context 'when the user does not have access to the merge request' do
before do
post_graphql(query, current_user: current_user)
end
it 'returns an empty field' do
post_graphql(query, current_user: current_user)
expect(graphql_data['mergeRequest']).to be_nil
end
it_behaves_like 'a working graphql query'
end
end
require 'spec_helper'
describe 'getting project information' do
include GraphqlHelpers
let(:project) { create(:project, :repository) }
let(:current_user) { create(:user) }
let(:query) do
graphql_query_for('project', 'fullPath' => project.full_path)
end
context 'when the user has access to the project' do
before do
project.add_developer(current_user)
post_graphql(query, current_user: current_user)
end
it 'includes the project' do
expect(graphql_data['project']).not_to be_nil
end
it_behaves_like 'a working graphql query'
end
context 'when the user does not have access to the project' do
before do
post_graphql(query, current_user: current_user)
end
it 'returns an empty field' do
post_graphql(query, current_user: current_user)
expect(graphql_data['project']).to be_nil
end
it_behaves_like 'a working graphql query'
end
end
require 'spec_helper'
describe 'api', 'routing' do
context 'when graphql is disabled' do
before do
stub_feature_flags(graphql: false)
end
it 'does not route to the GraphqlController' do
expect(get('/api/graphql')).not_to route_to('graphql#execute')
end
it 'does not expose graphiql' do
expect(get('/-/graphql-explorer')).not_to route_to('graphiql/rails/editors#show')
end
end
context 'when graphql is disabled' do
before do
stub_feature_flags(graphql: true)
end
it 'routes to the GraphqlController' do
expect(get('/api/graphql')).not_to route_to('graphql#execute')
end
it 'exposes graphiql' do
expect(get('/-/graphql-explorer')).not_to route_to('graphiql/rails/editors#show')
end
end
end
module GraphqlHelpers
# makes an underscored string look like a fieldname
# "merge_request" => "mergeRequest"
def self.fieldnamerize(underscored_field_name)
graphql_field_name = underscored_field_name.to_s.camelize
graphql_field_name[0] = graphql_field_name[0].downcase
graphql_field_name
end
# Run a loader's named resolver
def resolve(resolver_class, obj: nil, args: {}, ctx: {})
resolver_class.new(object: obj, context: ctx).resolve(args)
end
# Runs a block inside a BatchLoader::Executor wrapper
def batch(max_queries: nil, &blk)
wrapper = proc do
begin
BatchLoader::Executor.ensure_current
yield
ensure
BatchLoader::Executor.clear_current
end
end
if max_queries
result = nil
expect { result = wrapper.call }.not_to exceed_query_limit(max_queries)
result
else
wrapper.call
end
end
def graphql_query_for(name, attributes = {}, fields = nil)
fields ||= all_graphql_fields_for(name.classify)
attributes = attributes_to_graphql(attributes)
<<~QUERY
{
#{name}(#{attributes}) {
#{fields}
}
}
QUERY
end
def all_graphql_fields_for(class_name)
type = GitlabSchema.types[class_name.to_s]
return "" unless type
type.fields.map do |name, field|
if scalar?(field)
name
else
"#{name} { #{all_graphql_fields_for(field_type(field))} }"
end
end.join("\n")
end
def attributes_to_graphql(attributes)
attributes.map do |name, value|
"#{GraphqlHelpers.fieldnamerize(name.to_s)}: \"#{value}\""
end.join(", ")
end
def post_graphql(query, current_user: nil)
post api('/', current_user, version: 'graphql'), query: query
end
def graphql_data
json_response['data']
end
def graphql_errors
json_response['data']
end
def scalar?(field)
field_type(field).kind.scalar?
end
def field_type(field)
if field.type.respond_to?(:of_type)
field.type.of_type
else
field.type
end
end
end
RSpec::Matchers.define :require_graphql_authorizations do |*expected|
match do |field|
field_definition = field.metadata[:type_class]
expect(field_definition).to respond_to(:required_permissions)
expect(field_definition.required_permissions).to contain_exactly(*expected)
end
end
RSpec::Matchers.define :have_graphql_fields do |*expected|
match do |kls|
field_names = expected.map { |name| GraphqlHelpers.fieldnamerize(name) }
expect(kls.fields.keys).to contain_exactly(*field_names)
end
end
RSpec::Matchers.define :have_graphql_arguments do |*expected|
include GraphqlHelpers
match do |field|
argument_names = expected.map { |name| GraphqlHelpers.fieldnamerize(name) }
expect(field.arguments.keys).to contain_exactly(*argument_names)
end
end
RSpec::Matchers.define :have_graphql_type do |expected|
match do |field|
expect(field.type).to eq(expected.to_graphql)
end
end
RSpec::Matchers.define :have_graphql_resolver do |expected|
match do |field|
case expected
when Method
expect(field.metadata[:type_class].resolve_proc).to eq(expected)
else
expect(field.metadata[:type_class].resolver).to eq(expected)
end
end
end
require 'spec_helper'
shared_examples 'a working graphql query' do
include GraphqlHelpers
it 'is returns a successfull response', :aggregate_failures do
expect(response).to be_success
expect(graphql_errors['errors']).to be_nil
expect(json_response.keys).to include('data')
end
end
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