Commit 083d64c6 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 0be510a4
......@@ -94,10 +94,7 @@ schedule:review-build-cng:
variables:
HOST_SUFFIX: "${CI_ENVIRONMENT_SLUG}"
DOMAIN: "-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN}"
# v2.3.7 + some stability improvements not yet released:
# - sidekiq readinessProbe should be `pgrep -f sidekiq`: https://gitlab.com/gitlab-org/charts/gitlab/merge_requests/991
# - Allows livenessProbe and readinessProbe to be configured for unicorn: https://gitlab.com/gitlab-org/charts/gitlab/merge_requests/985
GITLAB_HELM_CHART_REF: "df7c52dc69df441909880b8f2fd15e938cdb2047"
GITLAB_HELM_CHART_REF: "v2.4.4"
GITLAB_EDITION: "ce"
environment:
name: review/${CI_COMMIT_REF_NAME}
......
......@@ -3,21 +3,24 @@
# Provides an action which fetches a metrics dashboard according
# to the parameters specified by the controller.
module MetricsDashboard
include RenderServiceResults
extend ActiveSupport::Concern
def metrics_dashboard
result = dashboard_finder.find(
project_for_dashboard,
current_user,
metrics_dashboard_params
metrics_dashboard_params.to_h.symbolize_keys
)
if include_all_dashboards?
if include_all_dashboards? && result
result[:all_dashboards] = dashboard_finder.find_all_paths(project_for_dashboard)
end
respond_to do |format|
if result[:status] == :success
if result.nil?
format.json { continue_polling_response }
elsif result[:status] == :success
format.json { render dashboard_success_response(result) }
else
format.json { render dashboard_error_response(result) }
......@@ -56,7 +59,7 @@ module MetricsDashboard
def dashboard_error_response(result)
{
status: result[:http_status],
status: result[:http_status] || :bad_request,
json: result.slice(:all_dashboards, :message, :status)
}
end
......
......@@ -199,8 +199,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
def metrics_dashboard_params
params
.permit(:embedded, :group, :title, :y_label)
.to_h.symbolize_keys
.permit(:embedded, :group, :title, :y_label, :dashboard_path, :environment)
.merge(dashboard_path: params[:dashboard], environment: environment)
end
......
......@@ -2,6 +2,9 @@
class Projects::GrafanaApiController < Projects::ApplicationController
include RenderServiceResults
include MetricsDashboard
before_action :validate_feature_enabled!, only: [:metrics_dashboard]
def proxy
result = ::Grafana::ProxyService.new(
......@@ -19,6 +22,14 @@ class Projects::GrafanaApiController < Projects::ApplicationController
private
def metrics_dashboard_params
params.permit(:embedded, :grafana_url)
end
def validate_feature_enabled!
render_403 unless Feature.enabled?(:gfm_grafana_integration)
end
def query_params
params.permit(:query, :start, :end, :step)
end
......
# frozen_string_literal: true
module SearchHelper
SEARCH_PERMITTED_PARAMS = [:search, :scope, :project_id, :group_id, :repository_ref, :snippets].freeze
def search_autocomplete_opts(term)
return unless current_user
......@@ -200,7 +202,7 @@ module SearchHelper
search_params = params
.merge(search)
.merge({ scope: scope })
.permit(:search, :scope, :project_id, :group_id, :repository_ref, :snippets)
.permit(SEARCH_PERMITTED_PARAMS)
if @scope == scope
li_class = 'active'
......
......@@ -197,9 +197,7 @@ module TreeHelper
end
def directory_download_links(project, ref, archive_prefix)
formats = ['zip', 'tar.gz', 'tar.bz2', 'tar']
formats.map do |fmt|
Gitlab::Workhorse::ARCHIVE_FORMATS.map do |fmt|
{
text: fmt,
path: project_archive_path(project, id: tree_join(ref, archive_prefix), format: fmt)
......
......@@ -6,11 +6,9 @@ module Releases
attr_accessor :project, :tag_name, :format
FORMATS = %w(zip tar.gz tar.bz2 tar).freeze
class << self
def all(project, tag_name)
Releases::Source::FORMATS.map do |format|
Gitlab::Workhorse::ARCHIVE_FORMATS.map do |format|
Releases::Source.new(project: project,
tag_name: tag_name,
format: format)
......
......@@ -13,7 +13,7 @@ module Groups
TransferError = Class.new(StandardError)
attr_reader :error
attr_reader :error, :new_parent_group
def initialize(group, user, params = {})
super
......@@ -115,3 +115,5 @@ module Groups
end
end
end
Groups::TransferService.prepend_if_ee('EE::Groups::TransferService')
......@@ -13,6 +13,8 @@ module Projects
include Gitlab::ShellAdapter
TransferError = Class.new(StandardError)
attr_reader :new_namespace
def execute(new_namespace)
@new_namespace = new_namespace
......
- formats = [['zip', 'btn-primary'], ['tar.gz'], ['tar.bz2'], ['tar']]
.btn-group.ml-0.w-100
- formats.each do |(fmt, extra_class)|
- Gitlab::Workhorse::ARCHIVE_FORMATS.each_with_index do |fmt, index|
- archive_path = project_archive_path(project, id: tree_join(ref, archive_prefix), path: path, format: fmt)
= link_to fmt, external_storage_url_or_path(archive_path), rel: 'nofollow', download: '', class: "btn btn-xs #{extra_class}"
= link_to fmt, external_storage_url_or_path(archive_path), rel: 'nofollow', download: '', class: "btn btn-xs #{"btn-primary" if index == 0}"
- if @search_objects.to_a.empty?
= render partial: "search/results/empty"
= render_if_exists 'shared/promotions/promote_advanced_search'
= render_if_exists 'search/form_revert_to_basic'
- else
.row-content-block.d-md-flex.text-left.align-items-center
- unless @search_objects.is_a?(Kaminari::PaginatableWithoutCount)
......
---
title: Use Rails 5.2 Redis caching store
merge_request: 19202
author:
type: other
......@@ -247,7 +247,10 @@ module Gitlab
end
# Use caching across all environments
# Full list of options:
# https://api.rubyonrails.org/classes/ActiveSupport/Cache/RedisCacheStore.html#method-c-new
caching_config_hash = Gitlab::Redis::Cache.params
caching_config_hash[:compress] = false
caching_config_hash[:namespace] = Gitlab::Redis::Cache::CACHE_NAMESPACE
caching_config_hash[:expires_in] = 2.weeks # Cache should not grow forever
if Sidekiq.server? # threaded context
......@@ -255,7 +258,7 @@ module Gitlab
caching_config_hash[:pool_timeout] = 1
end
config.cache_store = :redis_store, caching_config_hash
config.cache_store = :redis_cache_store, caching_config_hash
config.active_job.queue_adapter = :sidekiq
......
......@@ -187,9 +187,10 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
resource :import, only: [:new, :create, :show]
resource :avatar, only: [:show, :destroy]
get 'grafana/proxy/:datasource_id/*proxy_path',
to: 'grafana_api#proxy',
as: :grafana_api
scope :grafana, as: :grafana_api do
get 'proxy/:datasource_id/*proxy_path', to: 'grafana_api#proxy'
get :metrics_dashboard, to: 'grafana_api#metrics_dashboard'
end
end
# End of the /-/ scope.
......
......@@ -328,6 +328,8 @@ When you tail the Gitaly logs on your Gitaly server you should see requests
coming in. One sure way to trigger a Gitaly request is to clone a repository
from your GitLab server over HTTP.
DANGER: **Danger:** If you have [custom server-side Git hooks](../custom_hooks.md#custom-server-side-git-hooks) configured, either per repository or globally, you must move these to the Gitaly node. If you have multiple Gitaly nodes, copy your custom hook(s) to all nodes.
### Disabling the Gitaly service in a cluster environment
If you are running Gitaly [as a remote
......
......@@ -583,3 +583,12 @@ Here are some common pitfalls and how to overcome them:
AWS has [fixed limits](https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/aes-limits.html)
for this setting ("Maximum Size of HTTP Request Payloads"), based on the size of
the underlying instance.
### Reverting to basic search
Sometimes there may be issues with your Elasticsearch index data and as such
GitLab will allow you to revert to "basic search" when there are no search
results and assuming that basic search is supported in that scope. This "basic
search" will behave as though you don't have Elasticsearch enabled at all for
your instance and search using other data sources (ie. Postgres data and Git
data).
This diff is collapsed.
......@@ -280,6 +280,6 @@ and customized to fit your workflow. Here are some helpful resources for further
1. [Multiple Kubernetes clusters](index.md#using-multiple-kubernetes-clusters-premium) **(PREMIUM)**
1. [Incremental rollout to production](index.md#incremental-rollout-to-production-premium) **(PREMIUM)**
1. [Disable jobs you don't need with environment variables](index.md#environment-variables)
1. [Use a static IP for your cluster](../../user/project/clusters/index.md#using-a-static-ip)
1. [Use a static IP for your cluster](../../user/clusters/applications.md#using-a-static-ip)
1. [Use your own buildpacks to build your application](index.md#custom-buildpacks)
1. [Prometheus monitoring](../../user/project/integrations/prometheus.md)
......@@ -107,10 +107,20 @@ To resolve this:
project that is used to run your jobs and send the results back to
GitLab. It is used in conjunction with [GitLab
CI/CD](../../ci/README.md), the open-source continuous integration
service included with GitLab that coordinates the jobs. When installing
the GitLab Runner via the applications, it will run in **privileged
mode** by default. Make sure you read the [security
implications](../project/clusters/index.md#security-implications) before doing so.
service included with GitLab that coordinates the jobs.
If the project is on GitLab.com, shared Runners are available
(the first 2000 minutes are free, you can
[buy more later](../../subscriptions/index.md#extra-shared-runners-pipeline-minutes))
and you do not have to deploy one if they are enough for your needs. If a
project-specific Runner is desired, or there are no shared Runners, it is easy
to deploy one.
Note that the deployed Runner will be set as **privileged**, which means it will essentially
have root access to the underlying machine. This is required to build Docker images,
so it is the default. Make sure you read the
[security implications](../project/clusters/index.md#security-implications)
before deploying one.
NOTE: **Note:**
The [`runner/gitlab-runner`](https://gitlab.com/gitlab-org/charts/gitlab-runner)
......@@ -128,12 +138,113 @@ balancing, SSL termination, and name-based virtual hosting. It acts as a
web proxy for your applications and is useful if you want to use [Auto
DevOps](../../topics/autodevops/index.md) or deploy your own web apps.
NOTE: **Note:**
With the following procedure, a load balancer must be installed in your cluster
to obtain the endpoint. You can use either
Ingress, or Knative's own load balancer ([Istio](https://istio.io)) if using Knative.
In order to publish your web application, you first need to find the endpoint which will be either an IP
address or a hostname associated with your load balancer.
To install it, click on the **Install** button for Ingress. GitLab will attempt
to determine the external endpoint and it should be available within a few minutes.
#### Determining the external endpoint automatically
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/17052) in GitLab 10.6.
After you install Ingress, the external endpoint should be available within a few minutes.
TIP: **Tip:**
This endpoint can be used for the
[Auto DevOps base domain](../../topics/autodevops/index.md#auto-devops-base-domain)
using the `KUBE_INGRESS_BASE_DOMAIN` environment variable.
If the endpoint doesn't appear and your cluster runs on Google Kubernetes Engine:
1. Check your [Kubernetes cluster on Google Kubernetes Engine](https://console.cloud.google.com/kubernetes) to ensure there are no errors on its nodes.
1. Ensure you have enough [Quotas](https://console.cloud.google.com/iam-admin/quotas) on Google Kubernetes Engine. For more information, see [Resource Quotas](https://cloud.google.com/compute/quotas).
1. Check [Google Cloud's Status](https://status.cloud.google.com/) to ensure they are not having any disruptions.
Once installed, you may see a `?` for "Ingress IP Address" depending on the
cloud provider. For EKS specifically, this is because the ELB is created
with a DNS name, not an IP address. If GitLab is still unable to
determine the endpoint of your Ingress or Knative application, you can
[determine it manually](#determining-the-external-endpoint-manually).
NOTE: **Note:**
The [`stable/nginx-ingress`](https://github.com/helm/charts/tree/master/stable/nginx-ingress)
chart is used to install this application with a
[`values.yaml`](https://gitlab.com/gitlab-org/gitlab/blob/master/vendor/ingress/values.yaml)
file.
#### Determining the external endpoint manually
If the cluster is on GKE, click the **Google Kubernetes Engine** link in the
**Advanced settings**, or go directly to the
[Google Kubernetes Engine dashboard](https://console.cloud.google.com/kubernetes/)
and select the proper project and cluster. Then click **Connect** and execute
the `gcloud` command in a local terminal or using the **Cloud Shell**.
If the cluster is not on GKE, follow the specific instructions for your
Kubernetes provider to configure `kubectl` with the right credentials.
The output of the following examples will show the external endpoint of your
cluster. This information can then be used to set up DNS entries and forwarding
rules that allow external access to your deployed applications.
If you installed Ingress via the **Applications**, run the following command:
```bash
kubectl get service --namespace=gitlab-managed-apps ingress-nginx-ingress-controller -o jsonpath='{.status.loadBalancer.ingress[0].ip}'
```
Some Kubernetes clusters return a hostname instead, like [Amazon EKS](https://aws.amazon.com/eks/). For these platforms, run:
```bash
kubectl get service --namespace=gitlab-managed-apps ingress-nginx-ingress-controller -o jsonpath='{.status.loadBalancer.ingress[0].hostname}'
```
For Istio/Knative, the command will be different:
```bash
kubectl get svc --namespace=istio-system knative-ingressgateway -o jsonpath='{.status.loadBalancer.ingress[0].ip} '
```
Otherwise, you can list the IP addresses of all load balancers:
```bash
kubectl get svc --all-namespaces -o jsonpath='{range.items[?(@.status.loadBalancer.ingress)]}{.status.loadBalancer.ingress[*].ip} '
```
NOTE: **Note:**
If EKS is used, an [Elastic Load Balancer](https://docs.aws.amazon.com/elasticloadbalancing/)
will also be created, which will incur additional AWS costs.
NOTE: **Note:**
You may see a trailing `%` on some Kubernetes versions, **do not include it**.
The Ingress is now available at this address and will route incoming requests to
the proper service based on the DNS name in the request. To support this, a
wildcard DNS CNAME record should be created for the desired domain name. For example,
`*.myekscluster.com` would point to the Ingress hostname obtained earlier.
#### Using a static IP
By default, an ephemeral external IP address is associated to the cluster's load
balancer. If you associate the ephemeral IP with your DNS and the IP changes,
your apps will not be able to be reached, and you'd have to change the DNS
record again. In order to avoid that, you should change it into a static
reserved IP.
Read how to [promote an ephemeral external IP address in GKE](https://cloud.google.com/compute/docs/ip-addresses/reserve-static-external-ip-address#promote_ephemeral_ip).
#### Pointing your DNS at the external endpoint
Once you've set up the external endpoint, you should associate it with a [wildcard DNS
record](https://en.wikipedia.org/wiki/Wildcard_DNS_record) such as `*.example.com.`
in order to be able to reach your apps. If your external endpoint is an IP address,
use an A record. If your external endpoint is a hostname, use a CNAME record.
#### Web Application Firewall (ModSecurity)
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/65192) in GitLab 12.3 (enabled using `ingress_modsecurity` [feature flag](../../development/feature_flags/development.md#enabling-a-feature-flag-in-development)).
......@@ -259,6 +370,14 @@ chart is used to install this application.
open-source monitoring and alerting system useful to supervise your
deployed applications.
GitLab is able to monitor applications automatically, using the
[Prometheus integration](../project/integrations/prometheus.md). Kubernetes container CPU and
memory metrics are automatically collected, and response metrics are retrieved
from NGINX Ingress as well.
To enable monitoring, simply install Prometheus into the cluster with the
**Install** button.
NOTE: **Note:**
The [`stable/prometheus`](https://github.com/helm/charts/tree/master/stable/prometheus)
chart is used to install this application with a
......
......@@ -42,6 +42,9 @@ it is not possible due to a naming collision. For example:
| `gitlab-org/gitlab` | `@gitlab-org/gitlab` | Yes |
| `gitlab-org/gitlab` | `@foo/bar` | No |
CAUTION: **When updating the path of a user/group or transferring a (sub)group/project:**
If you update the root namespace of a project with NPM packages, your changes will be rejected. To be allowed to do that, make sure to remove any NPM package first. Don't forget to update your `.npmrc` files to follow the above naming convention and run `npm publish` if necessary.
Now, you can configure your project to authenticate with the GitLab NPM
Registry.
......
......@@ -218,149 +218,12 @@ differentiate the new cluster with the rest.
## Installing applications
GitLab can install and manage some applications in your project-level
cluster. For more information on installing, upgrading, uninstalling,
and troubleshooting applications for your project cluster, see
GitLab can install and manage some applications like Helm, GitLab Runner, Ingress,
Prometheus, etc., in your project-level cluster. For more information on
installing, upgrading, uninstalling, and troubleshooting applications for
your project cluster, see
[GitLab Managed Apps](../../clusters/applications.md).
### Getting the external endpoint
NOTE: **Note:**
With the following procedure, a load balancer must be installed in your cluster
to obtain the endpoint. You can use either
[Ingress](#installing-applications), or Knative's own load balancer
([Istio](https://istio.io)) if using [Knative](#installing-applications).
In order to publish your web application, you first need to find the endpoint which will be either an IP
address or a hostname associated with your load balancer.
#### Automatically determining the external endpoint
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/17052) in GitLab 10.6.
After you install [Ingress or Knative](#installing-applications), GitLab attempts to determine the external endpoint
and it should be available within a few minutes. If the endpoint doesn't appear
and your cluster runs on Google Kubernetes Engine:
1. Check your [Kubernetes cluster on Google Kubernetes Engine](https://console.cloud.google.com/kubernetes) to ensure there are no errors on its nodes.
1. Ensure you have enough [Quotas](https://console.cloud.google.com/iam-admin/quotas) on Google Kubernetes Engine. For more information, see [Resource Quotas](https://cloud.google.com/compute/quotas).
1. Check [Google Cloud's Status](https://status.cloud.google.com/) to ensure they are not having any disruptions.
If GitLab is still unable to determine the endpoint of your Ingress or Knative application, you can
manually determine it by following the steps below.
#### Manually determining the external endpoint
If the cluster is on GKE, click the **Google Kubernetes Engine** link in the
**Advanced settings**, or go directly to the
[Google Kubernetes Engine dashboard](https://console.cloud.google.com/kubernetes/)
and select the proper project and cluster. Then click **Connect** and execute
the `gcloud` command in a local terminal or using the **Cloud Shell**.
If the cluster is not on GKE, follow the specific instructions for your
Kubernetes provider to configure `kubectl` with the right credentials.
The output of the following examples will show the external endpoint of your
cluster. This information can then be used to set up DNS entries and forwarding
rules that allow external access to your deployed applications.
If you installed the Ingress [via the **Applications**](#installing-applications),
run the following command:
```bash
kubectl get service --namespace=gitlab-managed-apps ingress-nginx-ingress-controller -o jsonpath='{.status.loadBalancer.ingress[0].ip}'
```
Some Kubernetes clusters return a hostname instead, like [Amazon EKS](https://aws.amazon.com/eks/). For these platforms, run:
```bash
kubectl get service --namespace=gitlab-managed-apps ingress-nginx-ingress-controller -o jsonpath='{.status.loadBalancer.ingress[0].hostname}'
```
For Istio/Knative, the command will be different:
```bash
kubectl get svc --namespace=istio-system knative-ingressgateway -o jsonpath='{.status.loadBalancer.ingress[0].ip} '
```
Otherwise, you can list the IP addresses of all load balancers:
```bash
kubectl get svc --all-namespaces -o jsonpath='{range.items[?(@.status.loadBalancer.ingress)]}{.status.loadBalancer.ingress[*].ip} '
```
#### Using a static IP
By default, an ephemeral external IP address is associated to the cluster's load
balancer. If you associate the ephemeral IP with your DNS and the IP changes,
your apps will not be able to be reached, and you'd have to change the DNS
record again. In order to avoid that, you should change it into a static
reserved IP.
Read how to [promote an ephemeral external IP address in GKE](https://cloud.google.com/compute/docs/ip-addresses/reserve-static-external-ip-address#promote_ephemeral_ip).
#### Pointing your DNS at the external endpoint
Once you've set up the external endpoint, you should associate it with a [wildcard DNS
record](https://en.wikipedia.org/wiki/Wildcard_DNS_record) such as `*.example.com.`
in order to be able to reach your apps. If your external endpoint is an IP address,
use an A record. If your external endpoint is a hostname, use a CNAME record.
#### Deploy services to the cluster
GitLab supports one-click deployment of helpful services to the cluster, many of
which support Auto DevOps. Back on the Kubernetes cluster screen in GitLab, a
list of applications is now available to deploy.
First, install Helm Tiller, a package manager for Kubernetes. This enables
deployment of the other applications.
![Deploy Apps](img/deploy_apps.png)
##### Deploying NGINX Ingress (optional)
Next, if you would like the deployed app to be reachable on the internet, deploy
the Ingress. Note that this will also cause an
[Elastic Load Balancer](https://docs.aws.amazon.com/elasticloadbalancing/)
to be created, which will incur additional AWS costs.
Once installed, you may see a `?` for "Ingress IP Address". This is because the
created ELB is available at a DNS name, not an IP address. To get the DNS name,
run:
```sh
kubectl get service ingress-nginx-ingress-controller -n gitlab-managed-apps -o jsonpath="{.status.loadBalancer.ingress[0].hostname}"
```
Note that you may see a trailing `%` on some Kubernetes versions, **do not include it**.
The Ingress is now available at this address and will route incoming requests to
the proper service based on the DNS name in the request. To support this, a
wildcard DNS CNAME record should be created for the desired domain name. For example,
`*.myekscluster.com` would point to the Ingress hostname obtained earlier.
![Create DNS](img/create_dns.png)
##### Deploying the GitLab Runner (optional)
If the project is on GitLab.com, free shared Runners are available and you do
not have to deploy one. If a project specific Runner is desired, or there are no
shared Runners, it is easy to deploy one.
Simply click on the **Install** button for the GitLab Runner. It is important to
note that the Runner deployed is set as **privileged**, which means it essentially
has root access to the underlying machine. This is required to build docker images,
and so is on by default.
##### Deploying Prometheus (optional)
GitLab is able to monitor applications automatically, utilizing
[Prometheus](../integrations/prometheus.html). Kubernetes container CPU and
memory metrics are automatically collected, and response metrics are retrieved
from NGINX Ingress as well.
To enable monitoring, simply install Prometheus into the cluster with the
**Install** button.
## Deploying to a Kubernetes cluster
A Kubernetes cluster can be the destination for a deployment job. If
......
......@@ -77,7 +77,7 @@ The minimum recommended cluster size to run Knative is 3-nodes, 6 vCPUs, and 22.
![install-knative](img/install-knative.png)
1. After the Knative installation has finished, you can wait for the IP address or hostname to be displayed in the
**Knative Endpoint** field or [retrieve the Istio Ingress Endpoint manually](../#manually-determining-the-external-endpoint).
**Knative Endpoint** field or [retrieve the Istio Ingress Endpoint manually](../../../clusters/applications.md#determining-the-external-endpoint-manually).
NOTE: **Note:**
Running `kubectl` commands on your cluster requires setting up access to the cluster first.
......
......@@ -30,7 +30,7 @@ For other deployments, there is [some configuration](#manually-setting-up-nginx-
### About managed NGINX Ingress deployments
NGINX Ingress is deployed into the `gitlab-managed-apps` namespace, using the [official Helm chart](https://github.com/helm/charts/tree/master/stable/nginx-ingress). NGINX Ingress will be [externally reachable via the Load Balancer's Endpoint](../../clusters/index.md#getting-the-external-endpoint).
NGINX Ingress is deployed into the `gitlab-managed-apps` namespace, using the [official Helm chart](https://github.com/helm/charts/tree/master/stable/nginx-ingress). NGINX Ingress will be [externally reachable via the Load Balancer's Endpoint](../../../clusters/applications.md#ingress).
NGINX is configured for Prometheus monitoring, by setting:
......
......@@ -30,7 +30,7 @@ For other deployments, there is [some configuration](#manually-setting-up-nginx-
### About managed NGINX Ingress deployments
NGINX Ingress is deployed into the `gitlab-managed-apps` namespace, using the [official Helm chart](https://github.com/helm/charts/tree/master/stable/nginx-ingress). NGINX Ingress will be [externally reachable via the Load Balancer's Endpoint](../../clusters/index.md#getting-the-external-endpoint).
NGINX Ingress is deployed into the `gitlab-managed-apps` namespace, using the [official Helm chart](https://github.com/helm/charts/tree/master/stable/nginx-ingress). NGINX Ingress will be [externally reachable via the Load Balancer's Endpoint](../../../clusters/applications.md#ingress).
NGINX is configured for Prometheus monitoring, by setting:
......
---
type: reference, howto
---
# New Pages website from a forked sample
To get started with GitLab Pages from a sample website, the easiest
way to do it is by using one of the [bundled templates](pages_bundled_template.md).
If you don't find one that suits your needs, you can opt by
forking (copying) a [sample project from the most popular Static Site Generators](https://gitlab.com/pages).
<table class="borderless-table center fixed-table middle width-80">
<tr>
<td style="width: 30%"><img src="../img/icons/fork.png" alt="Fork" class="image-noshadow half-width"></td>
<td style="width: 10%">
<strong>
<i class="fa fa-angle-double-right" aria-hidden="true"></i>
</strong>
</td>
<td style="width: 30%"><img src="../img/icons/terminal.png" alt="Deploy" class="image-noshadow half-width"></td>
<td style="width: 10%">
<strong>
<i class="fa fa-angle-double-right" aria-hidden="true"></i>
</strong>
</td>
<td style="width: 30%"><img src="../img/icons/click.png" alt="Visit" class="image-noshadow half-width"></td>
</tr>
<tr>
<td><em>Fork an example project</em></td>
<td></td>
<td><em>Deploy your website</em></td>
<td></td>
<td><em>Visit your website's URL</em></td>
</tr>
</table>
**<i class="fa fa-youtube-play youtube" aria-hidden="true"></i> Watch a [video tutorial](https://www.youtube.com/watch?v=TWqh9MtT4Bg) with all the steps below.**
1. [Fork](../../../../gitlab-basics/fork-project.md) a sample project from the [GitLab Pages examples](https://gitlab.com/pages) group.
1. From the left sidebar, navigate to your project's **CI/CD > Pipelines**
and click **Run pipeline** to trigger GitLab CI/CD to build and deploy your
site to the server.
1. Once the pipeline has finished successfully, find the link to visit your
website from your project's **Settings > Pages**. It can take aproximatelly
30 minutes to be deployed.
You can also take some **optional** further steps:
- _Remove the fork relationship._ The fork relationship is necessary to contribute back to the project you originally forked from. If you don't have any intentions to do so, you can remove it. To do so, navigate to your project's **Settings**, expand **Advanced settings**, and scroll down to **Remove fork relationship**:
![remove fork relationship](../img/remove_fork_relationship.png)
- _Make it a user or group website._ To turn a **project website** forked
from the Pages group into a **user/group** website, you'll need to:
- Rename it to `namespace.gitlab.io`: go to your project's
**Settings > General** and expand **Advanced**. Scroll down to
**Rename repository** and change the path to `namespace.gitlab.io`.
- Adjust your SSG's [base URL](../getting_started_part_one.md#urls-and-baseurls) from `"project-name"` to
`""`. This setting will be at a different place for each SSG, as each of them
have their own structure and file tree. Most likely, it will be in the SSG's
config file.
---
type: reference, howto
---
# Start a new Pages website from scratch or deploy an exising website
If you already have a website and want to deploy it with GitLab Pages,
or, if you want to start a new site from scratch, you'll need to:
- Create a new project in GitLab to hold your site content.
- Set up GitLab CI/CD to deploy your website to Pages.
To do so, follow the steps below.
1. From your **Project**'s **[Dashboard](https://gitlab.com/dashboard/projects)**,
click **New project**, and name it according to the
[Pages domain names](../getting_started_part_one.md#gitlab-pages-domain-names).
1. Clone it to your local computer, add your website
files to your project, add, commit and push to GitLab.
Alternativelly, you can run `git init` in your local directory,
add the remote URL:
`git remote add origin git@gitlab.com:namespace/project-name.git`,
then add, commit, and push to GitLab.
1. From the your **Project**'s page, click **Set up CI/CD**:
![setup GitLab CI/CD](../img/setup_ci.png)
1. Choose one of the templates from the dropbox menu.
Pick up the template corresponding to the SSG you're using (or plain HTML).
![gitlab-ci templates](../img/choose_ci_template.png)
Note that, if you don't find a corresponding template, you can look into
[GitLab Pages group of sample projects](https://gitlab.com/pages),
you may find one among them that suits your needs, from which you
can copy `.gitlab-ci.yml`'s content and adjust for your case.
If you don't find it there either, [learn how to write a `.gitlab-ci.yml`
file for GitLab Pages](../getting_started_part_four.md).
Once you have both site files and `.gitlab-ci.yml` in your project's
root, GitLab CI/CD will build your site and deploy it with Pages.
Once the first build passes, you access your site by
navigating to your **Project**'s **Settings** > **Pages**,
where you'll find its default URL. It can take approximately 30 min to be
deployed.
---
type: reference, howto
---
# New Pages website from a bundled template
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/47857)
in GitLab 11.8.
The simplest way to create a GitLab Pages site is to use one of the most
popular templates, which come already bundled with GitLab and are ready to go.
1. From the top navigation, click the **+** button and select **New project**.
1. Select **Create from Template**.
1. Choose one of the templates starting with **Pages**:
![Project templates for Pages](../img/pages_project_templates_v11_8.png)
1. From the left sidebar, navigate to your project's **CI/CD > Pipelines**
and click **Run pipeline** to trigger GitLab CI/CD to build and deploy your
site to the server.
1. After the pipeline has finished successfully, wait approximately 30 minutes
for your website to be visible. After waiting 30 minutes, find the link to
visit your website from your project's **Settings > Pages**. If the link
leads to a 404 page, wait a few minutes and try again.
Your website is then visible on your domain and you can modify your files
as you wish. For every modification pushed to your repository, GitLab CI/CD
will run a new pipeline to immediately publish your changes to the server.
......@@ -3,24 +3,11 @@ last_updated: 2018-06-04
type: concepts, reference
---
# Static sites and GitLab Pages domains
# GitLab Pages domain names, URLs, and baseurls
On this document, learn how to name your project for GitLab Pages
according to your intended website's URL.
## Static sites
GitLab Pages only supports static websites, meaning,
your output files must be HTML, CSS, and JavaScript only.
To create your static site, you can either hardcode in HTML,
CSS, and JS, or use a [Static Site Generator (SSG)](https://www.staticgen.com/)
to simplify your code and build the static site for you,
which is highly recommendable and much faster than hardcoding.
See the [further reading](#further-reading) section below for
references on static site concepts.
## GitLab Pages domain names
>**Note:**
......@@ -93,11 +80,35 @@ To understand Pages domains clearly, read the examples below.
- On your GitLab instance, replace `gitlab.io` above with your
Pages server domain. Ask your sysadmin for this information.
_Read on about [Projects for GitLab Pages and URL structure](getting_started_part_two.md)._
## URLs and Baseurls
Every Static Site Generator (SSG) default configuration expects
to find your website under a (sub)domain (`example.com`), not
in a subdirectory of that domain (`example.com/subdir`). Therefore,
whenever you publish a project website (`namespace.gitlab.io/project-name`),
you'll have to look for this configuration (base URL) on your SSG's
documentation and set it up to reflect this pattern.
For example, for a Jekyll site, the `baseurl` is defined in the Jekyll
configuration file, `_config.yml`. If your website URL is
`https://john.gitlab.io/blog/`, you need to add this line to `_config.yml`:
```yaml
baseurl: "/blog"
```
On the contrary, if you deploy your website after forking one of
our [default examples](https://gitlab.com/pages), the baseurl will
already be configured this way, as all examples there are project
websites. If you decide to make yours a user or group website, you'll
have to remove this configuration from your project. For the Jekyll
example we've just mentioned, you'd have to change Jekyll's `_config.yml` to:
```yaml
baseurl: ""
```
### Further reading
## Custom Domains
- Read through this technical overview on [Static versus Dynamic Websites](https://about.gitlab.com/blog/2016/06/03/ssg-overview-gitlab-pages-part-1-dynamic-x-static/)
- Understand [how modern Static Site Generators work](https://about.gitlab.com/blog/2016/06/10/ssg-overview-gitlab-pages-part-2/) and what you can add to your static site
- You can use [any SSG with GitLab Pages](https://about.gitlab.com/blog/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/)
- Fork an [example project](https://gitlab.com/pages) to build your website based upon
GitLab Pages supports custom domains and subdomains, served under HTTP or HTTPS.
See [GitLab Pages custom domains and SSL/TLS Certificates](custom_domains_ssl_tls_certification/index.md) for more information.
---
last_updated: 2019-06-04
type: reference, howto
redirect_to: 'index.md'
---
# Projects for GitLab Pages and URL structure
## What you need to get started
To get started with GitLab Pages, you need:
1. A project, thus a repository to hold your website's codebase.
1. A configuration file (`.gitlab-ci.yml`) to deploy your site.
1. A specific `job` called `pages` in the configuration file
that will make GitLab aware that you are deploying a GitLab Pages website.
1. A `public` directory with the static content of the website.
Optional Features:
1. A custom domain or subdomain.
1. A DNS pointing your (sub)domain to your Pages site.
1. **Optional**: an SSL/TLS certificate so your custom
domain is accessible under HTTPS.
The optional settings, custom domain, DNS records, and SSL/TLS certificates, are described in [GitLab Pages custom domains and SSL/TLS Certificates](custom_domains_ssl_tls_certification/index.md)).
## Project
Your GitLab Pages project is a regular project created the
same way you do for the other ones. To get started with GitLab Pages, you have three ways:
- [Use one of the popular project templates bundled with GitLab](#use-one-of-the-popular-pages-templates-bundled-with-gitlab).
- [Fork one of the templates from Page Examples](#fork-a-project-to-get-started-from).
- [Create a new project from scratch](#create-a-project-from-scratch).
### Use one of the popular Pages templates bundled with GitLab
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/47857)
in GitLab 11.8.
The simplest way to create a GitLab Pages site is to
[use one of the most popular templates](index.md#getting-started),
which come already bundled with GitLab and are ready to go.
### Fork a project to get started from
If you don't find an existing project template that suits you,
we've created this [group](https://gitlab.com/pages) of default projects
containing the most popular SSGs templates to get you started.
<table class="borderless-table center fixed-table middle width-80">
<tr>
<td style="width: 30%"><img src="img/icons/fork.png" alt="Fork" class="image-noshadow half-width"></td>
<td style="width: 10%">
<strong>
<i class="fa fa-angle-double-right" aria-hidden="true"></i>
</strong>
</td>
<td style="width: 30%"><img src="img/icons/terminal.png" alt="Deploy" class="image-noshadow half-width"></td>
<td style="width: 10%">
<strong>
<i class="fa fa-angle-double-right" aria-hidden="true"></i>
</strong>
</td>
<td style="width: 30%"><img src="img/icons/click.png" alt="Visit" class="image-noshadow half-width"></td>
</tr>
<tr>
<td><em>Fork an example project</em></td>
<td></td>
<td><em>Deploy your website</em></td>
<td></td>
<td><em>Visit your website's URL</em></td>
</tr>
</table>
**<i class="fa fa-youtube-play youtube" aria-hidden="true"></i> Watch a [video tutorial](https://www.youtube.com/watch?v=TWqh9MtT4Bg) with all the steps below.**
1. [Fork](../../../gitlab-basics/fork-project.md) a sample project from the [GitLab Pages examples](https://gitlab.com/pages) group.
1. From the left sidebar, navigate to your project's **CI/CD > Pipelines**
and click **Run pipeline** to trigger GitLab CI/CD to build and deploy your
site to the server.
1. Once the pipeline has finished successfully, find the link to visit your
website from your project's **Settings > Pages**.
You can also take some **optional** further steps:
- _Remove the fork relationship._ The fork relationship is necessary to contribute back to the project you originally forked from. If you don't have any intentions to do so, you can remove it. To do so, navigate to your project's **Settings**, expand **Advanced settings**, and scroll down to **Remove fork relationship**:
![remove fork relationship](img/remove_fork_relationship.png)
- _Make it a user or group website._ To turn a **project website** forked
from the Pages group into a **user/group** website, you'll need to:
- Rename it to `namespace.gitlab.io`: go to your project's
**Settings > General** and expand **Advanced**. Scroll down to
**Rename repository** and change the path to `namespace.gitlab.io`.
- Adjust your SSG's [base URL](#urls-and-baseurls) from `"project-name"` to
`""`. This setting will be at a different place for each SSG, as each of them
have their own structure and file tree. Most likely, it will be in the SSG's
config file.
### Create a project from scratch
1. From your **Project**'s **[Dashboard](https://gitlab.com/dashboard/projects)**,
click **New project**, and name it according to the
[Pages domain names](getting_started_part_one.md#gitlab-pages-domain-names).
1. Clone it to your local computer, add your website
files to your project, add, commit and push to GitLab.
1. From the your **Project**'s page, click **Set up CI/CD**:
![setup GitLab CI/CD](img/setup_ci.png)
1. Choose one of the templates from the dropbox menu.
Pick up the template corresponding to the SSG you're using (or plain HTML).
![gitlab-ci templates](img/choose_ci_template.png)
Once you have both site files and `.gitlab-ci.yml` in your project's
root, GitLab CI/CD will build your site and deploy it with Pages.
Once the first build passes, you see your site is live by
navigating to your **Project**'s **Settings** > **Pages**,
where you'll find its default URL.
> **Notes:**
>
> - GitLab Pages [supports any SSG](https://about.gitlab.com/blog/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/), but,
> if you don't find yours among the templates, you'll need
> to configure your own `.gitlab-ci.yml`. To do that, please
> read through the article [Creating and Tweaking GitLab CI/CD for GitLab Pages](getting_started_part_four.md). New SSGs are very welcome among
> the [example projects](https://gitlab.com/pages). If you set
> up a new one, please
> [contribute](https://gitlab.com/pages/pages.gitlab.io/blob/master/CONTRIBUTING.md)
> to our examples.
>
> - The second step _"Clone it to your local computer"_, can be done
> differently, achieving the same results: instead of cloning the bare
> repository to you local computer and moving your site files into it,
> you can run `git init` in your local website directory, add the
> remote URL: `git remote add origin git@gitlab.com:namespace/project-name.git`,
> then add, commit, and push to GitLab.
## URLs and Baseurls
Every Static Site Generator (SSG) default configuration expects
to find your website under a (sub)domain (`example.com`), not
in a subdirectory of that domain (`example.com/subdir`). Therefore,
whenever you publish a project website (`namespace.gitlab.io/project-name`),
you'll have to look for this configuration (base URL) on your SSG's
documentation and set it up to reflect this pattern.
For example, for a Jekyll site, the `baseurl` is defined in the Jekyll
configuration file, `_config.yml`. If your website URL is
`https://john.gitlab.io/blog/`, you need to add this line to `_config.yml`:
```yaml
baseurl: "/blog"
```
On the contrary, if you deploy your website after forking one of
our [default examples](https://gitlab.com/pages), the baseurl will
already be configured this way, as all examples there are project
websites. If you decide to make yours a user or group website, you'll
have to remove this configuration from your project. For the Jekyll
example we've just mentioned, you'd have to change Jekyll's `_config.yml` to:
```yaml
baseurl: ""
```
## Custom Domains
GitLab Pages supports custom domains and subdomains, served under HTTP or HTTPS.
See [GitLab Pages custom domains and SSL/TLS Certificates](custom_domains_ssl_tls_certification/index.md) for more information.
This document was moved to [another location](index.md#getting-started).
......@@ -73,6 +73,7 @@ publish any website written directly in plain HTML, CSS, and JavaScript.</p>
To use GitLab Pages, first you need to create a project in GitLab to upload your website's
files to. These projects can be either public, internal, or private, at your own choice.
GitLab will always deploy your website from a very specific folder called `public` in your
repository. Note that when you create a new project in GitLab, a [repository](../repository/index.md)
becomes available automatically.
......@@ -80,67 +81,48 @@ becomes available automatically.
To deploy your site, GitLab will use its built-in tool called [GitLab CI/CD](../../../ci/README.md),
to build your site and publish it to the GitLab Pages server. The sequence of
scripts that GitLab CI/CD runs to accomplish this task is created from a file named
`.gitlab-ci.yml`, which you can [create and modify](getting_started_part_four.md) at will.
`.gitlab-ci.yml`, which you can [create and modify](getting_started_part_four.md) at will. A specific `job` called `pages` in the configuration file will make GitLab aware that you are deploying a GitLab Pages website.
You can either use GitLab's [default domain for GitLab Pages websites](getting_started_part_one.md#gitlab-pages-domain-names),
`*.gitlab.io`, or your own domain (`example.com`). In that case, you'll
need admin access to your domain's registrar (or control panel) to set it up with Pages.
Optionally, when adding your own domain, you can add an SSL/TLS certificate to secure your
site under the HTTPS protocol.
### Getting started
To get started with GitLab Pages, you can either:
- [Create a project from scratch](getting_started_part_two.md#create-a-project-from-scratch).
- [Copy an existing example project](getting_started_part_two.md#fork-a-project-to-get-started-from).
- Use a bundled project template ready to go:
1. From the top navigation, click the **+** button and select **New project**.
1. Select **Create from Template**.
1. Choose one of the templates starting with **Pages**:
![Project templates for Pages](img/pages_project_templates_v11_8.png)
- [Use a bundled website template ready to go](getting_started/pages_bundled_template.md).
- [Copy an existing sample](getting_started/fork_sample_project.md).
- [Create a website from scratch or deploy an existing one](getting_started/new_or_existing_website.md).
1. From the left sidebar, navigate to your project's **CI/CD > Pipelines**
and click **Run pipeline** to trigger GitLab CI/CD to build and deploy your
site to the server.
1. After the pipeline has finished successfully, wait approximately 30 minutes
for your website to be visible. After waiting 30 minutes, find the link to
visit your website from your project's **Settings > Pages**. If the link
leads to a 404 page, wait a few minutes and try again.
Optional features:
Your website is then visible on your domain and you can modify your files
as you wish. For every modification pushed to your repository, GitLab CI/CD
will run a new pipeline to immediately publish your changes to the server.
- Use a [custom domain or subdomain](custom_domains_ssl_tls_certification/index.md#set-up-pages-with-a-custom-domain).
- Add an [SSL/TLS certificate to secure your site under the HTTPS protocol](custom_domains_ssl_tls_certification/index.md#adding-an-ssltls-certificate-to-pages).
_Advanced options:_
- [Use a custom domain](custom_domains_ssl_tls_certification/index.md#set-up-pages-with-a-custom-domain)
- Apply [SSL/TLS certification](custom_domains_ssl_tls_certification/index.md#adding-an-ssltls-certificate-to-pages) to your custom domain
Note that, if you're using GitLab Pages default domain (`.gitlab.io`),
your website will be automatically secure and available under
HTTPS. If you're using your own custom domain, you can
optionally secure it with SSL/TLS certificates.
## Availability
If you're using GitLab.com, your website will be publicly available to the internet.
To restrict access to your website, enable [GitLab Pages Access Control](pages_access_control.md).
If you're using self-managed instances (Core, Starter, Premium, or Ultimate),
your websites will be published on your own server, according to the
[Pages admin settings](../../../administration/pages/index.md) chosen by your sysadmin,
who can opt for making them public or internal to your server.
Note that, if you're using GitLab Pages default domain (`.gitlab.io`),
your website will be automatically secure and available under
HTTPS. If you're using your own custom domain, you can
optionally secure it with SSL/TLS certificates.
## Explore GitLab Pages
To learn more about configuration options for GitLab Pages, read the following:
| Document | Description |
| --- | --- |
| [Static websites and Pages domains](getting_started_part_one.md) | Understand what is a static website, and how GitLab Pages default domains work. |
| [Projects and URL structure](getting_started_part_two.md) | Forking projects and creating new ones from scratch, understanding URLs structure and baseurls. |
| [GitLab Pages domain names, URLs, and baseurls](getting_started_part_one.md) | Understand how GitLab Pages default domains work. |
| [GitLab CI/CD for GitLab Pages](getting_started_part_four.md) | Understand how to create your own `.gitlab-ci.yml` for your site. |
| [Exploring GitLab Pages](introduction.md) | Requirements, technical aspects, specific GitLab CI's configuration options, Access Control, custom 404 pages, limitations, FAQ. |
|---+---|
......
......@@ -69,40 +69,7 @@ don't have to create and edit HTML files manually. For example, Jekyll has the
## GitLab Pages Access Control **(CORE)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/33422) in GitLab 11.5.
You can enable Pages access control on your project, so that only
[members of your project](../../permissions.md#project-members-permissions)
(at least Guest) can access your website:
1. Navigate to your project's **Settings > General > Permissions**.
1. Toggle the **Pages** button to enable the access control.
NOTE: **Note:**
If you don't see the toggle button, that means that it's not enabled.
Ask your administrator to [enable it](../../../administration/pages/index.md#access-control).
1. The Pages access control dropdown allows you to set who can view pages hosted
with GitLab Pages, depending on your project's visibility:
- If your project is private:
- **Only project members**: Only project members will be able to browse the website.
- **Everyone**: Everyone, both logged into and logged out of GitLab, will be able to browse the website, no matter their project membership.
- If your project is internal:
- **Only project members**: Only project members will be able to browse the website.
- **Everyone with access**: Everyone logged into GitLab will be able to browse the website, no matter their project membership.
- **Everyone**: Everyone, both logged into and logged out of GitLab, will be able to browse the website, no matter their project membership.
- If your project is public:
- **Only project members**: Only project members will be able to browse the website.
- **Everyone with access**: Everyone, both logged into and logged out of GitLab, will be able to browse the website, no matter their project membership.
1. Click **Save changes**.
---
The next time someone tries to access your website and the access control is
enabled, they will be presented with a page to sign into GitLab and verify they
can access the website.
To restrict access to your website, enable [GitLab Pages Access Control](pages_access_control.md).
## Unpublishing your Pages
......
......@@ -21,8 +21,8 @@ open source Certificate Authority.
To follow along with this tutorial, we assume you already have:
- Created a [project](getting_started_part_two.md) in GitLab which
contains your website's source code.
- [Created a project](index.md#getting-started) in GitLab
containing your website's source code.
- Acquired a domain (`example.com`) and added a [DNS entry](custom_domains_ssl_tls_certification/index.md#set-up-pages-with-a-custom-domain)
pointing it to your Pages website.
- [Added your domain to your Pages project](custom_domains_ssl_tls_certification/index.md#steps)
......
---
type: reference, howto
---
# GitLab Pages Access Control
> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/33422) in GitLab 11.5.
> - Available on GitLab.com in GitLab 12.4.
You can enable Pages access control on your project, so that only
[members of your project](../../permissions.md#project-members-permissions)
(at least Guest) can access your website:
1. Navigate to your project's **Settings > General > Permissions**.
1. Toggle the **Pages** button to enable the access control.
NOTE: **Note:**
If you don't see the toggle button, that means that it's not enabled.
Ask your administrator to [enable it](../../../administration/pages/index.md#access-control).
1. The Pages access control dropdown allows you to set who can view pages hosted
with GitLab Pages, depending on your project's visibility:
- If your project is private:
- **Only project members**: Only project members will be able to browse the website.
- **Everyone**: Everyone, both logged into and logged out of GitLab, will be able to browse the website, no matter their project membership.
- If your project is internal:
- **Only project members**: Only project members will be able to browse the website.
- **Everyone with access**: Everyone logged into GitLab will be able to browse the website, no matter their project membership.
- **Everyone**: Everyone, both logged into and logged out of GitLab, will be able to browse the website, no matter their project membership.
- If your project is public:
- **Only project members**: Only project members will be able to browse the website.
- **Everyone with access**: Everyone, both logged into and logged out of GitLab, will be able to browse the website, no matter their project membership.
1. Click **Save changes**.
The next time someone tries to access your website and the access control is
enabled, they will be presented with a page to sign into GitLab and verify they
can access the website.
## Terminating a Pages session
If you want to log out from your Pages website,
you can do so by revoking application access token for GitLab Pages:
1. Navigate to your profile's **Settings > Applications**.
1. Find **Authorized applications** at the bottom of the page.
1. Find **GitLab Pages** and press the **Revoke** button.
# frozen_string_literal: true
module Banzai
module Filter
# HTML filter that inserts a placeholder element for each
# reference to a grafana dashboard.
class InlineGrafanaMetricsFilter < Banzai::Filter::InlineEmbedsFilter
# Placeholder element for the frontend to use as an
# injection point for charts.
def create_element(params)
begin_loading_dashboard(params[:url])
doc.document.create_element(
'div',
class: 'js-render-metrics',
'data-dashboard-url': metrics_dashboard_url(params)
)
end
def embed_params(node)
return unless Feature.enabled?(:gfm_grafana_integration)
query_params = Gitlab::Metrics::Dashboard::Url.parse_query(node['href'])
return unless [:panelId, :from, :to].all? do |param|
query_params.include?(param)
end
{ url: node['href'], start: query_params[:from], end: query_params[:to] }
end
# Selects any links with an href contains the configured
# grafana domain for the project
def xpath_search
return unless grafana_url.present?
%(descendant-or-self::a[starts-with(@href, '#{grafana_url}')])
end
private
def project
context[:project]
end
def grafana_url
project&.grafana_integration&.grafana_url
end
def metrics_dashboard_url(params)
Gitlab::Routing.url_helpers.project_grafana_api_metrics_dashboard_url(
project,
embedded: true,
grafana_url: params[:url],
start: format_time(params[:start]),
end: format_time(params[:end])
)
end
# Formats a timestamp from Grafana for compatibility with
# parsing in JS via `new Date(timestamp)`
#
# @param time [String] Represents miliseconds since epoch
def format_time(time)
Time.at(time.to_i / 1000).utc.strftime('%FT%TZ')
end
# Fetches a dashboard and caches the result for the
# FE to fetch quickly while rendering charts
def begin_loading_dashboard(url)
::Gitlab::Metrics::Dashboard::Finder.find(
project,
embedded: true,
grafana_url: url
)
end
end
end
end
......@@ -8,14 +8,17 @@ module Banzai
include Gitlab::Utils::StrongMemoize
METRICS_CSS_CLASS = '.js-render-metrics'
URL = Gitlab::Metrics::Dashboard::Url
Embed = Struct.new(:project_path, :permission)
# Finds all embeds based on the css class the FE
# uses to identify the embedded content, removing
# only unnecessary nodes.
def call
nodes.each do |node|
path = paths_by_node[node]
user_has_access = user_access_by_path[path]
embed = embeds_by_node[node]
user_has_access = user_access_by_embed[embed]
node.remove unless user_has_access
end
......@@ -30,40 +33,69 @@ module Banzai
end
# Returns all nodes which the FE will identify as
# a metrics dashboard placeholder element
# a metrics embed placeholder element
#
# @return [Nokogiri::XML::NodeSet]
def nodes
@nodes ||= doc.css(METRICS_CSS_CLASS)
end
# Maps a node to the full path of a project.
# Maps a node to key properties of an embed.
# Memoized so we only need to run the regex to get
# the project full path from the url once per node.
#
# @return [Hash<Nokogiri::XML::Node, String>]
def paths_by_node
strong_memoize(:paths_by_node) do
nodes.each_with_object({}) do |node, paths|
paths[node] = path_for_node(node)
# @return [Hash<Nokogiri::XML::Node, Embed>]
def embeds_by_node
strong_memoize(:embeds_by_node) do
nodes.each_with_object({}) do |node, embeds|
embed = Embed.new
url = node.attribute('data-dashboard-url').to_s
set_path_and_permission(embed, url, URL.regex, :read_environment)
set_path_and_permission(embed, url, URL.grafana_regex, :read_project) unless embed.permission
embeds[node] = embed if embed.permission
end
end
end
# Gets a project's full_path from the dashboard url
# in the placeholder node. The FE will use the attr
# `data-dashboard-url`, so we want to check against that
# attribute directly in case a user has manually
# created a metrics element (rather than supporting
# an alternate attr in InlineMetricsFilter).
# Attempts to determine the path and permission attributes
# of a url based on expected dashboard url formats and
# sets the attributes on an Embed object
#
# @return [String]
def path_for_node(node)
url = node.attribute('data-dashboard-url').to_s
Gitlab::Metrics::Dashboard::Url.regex.match(url) do |m|
# @param embed [Embed]
# @param url [String]
# @param regex [RegExp]
# @param permission [Symbol]
def set_path_and_permission(embed, url, regex, permission)
return unless path = regex.match(url) do |m|
"#{$~[:namespace]}/#{$~[:project]}"
end
embed.project_path = path
embed.permission = permission
end
# Returns a mapping representing whether the current user
# has permission to view the embed for the project.
# Determined in a batch
#
# @return [Hash<Embed, Boolean>]
def user_access_by_embed
strong_memoize(:user_access_by_embed) do
unique_embeds.each_with_object({}) do |embed, access|
project = projects_by_path[embed.project_path]
access[embed] = Ability.allowed?(user, embed.permission, project)
end
end
end
# Returns a unique list of embeds
#
# @return [Array<Embed>]
def unique_embeds
embeds_by_node.values.uniq
end
# Maps a project's full path to a Project object.
......@@ -74,22 +106,17 @@ module Banzai
def projects_by_path
strong_memoize(:projects_by_path) do
Project.eager_load(:route, namespace: [:route])
.where_full_path_in(paths_by_node.values.uniq)
.where_full_path_in(unique_project_paths)
.index_by(&:full_path)
end
end
# Returns a mapping representing whether the current user
# has permission to view the metrics for the project.
# Determined in a batch
# Returns a list of the full_paths of every project which
# has an embed in the doc
#
# @return [Hash<Project, Boolean>]
def user_access_by_path
strong_memoize(:user_access_by_path) do
projects_by_path.each_with_object({}) do |(path, project), access|
access[path] = Ability.allowed?(user, :read_environment, project)
end
end
# @return [Array<String>]
def unique_project_paths
embeds_by_node.values.map(&:project_path).uniq
end
end
end
......
......@@ -30,6 +30,7 @@ module Banzai
Filter::ImageLazyLoadFilter,
Filter::ImageLinkFilter,
Filter::InlineMetricsFilter,
Filter::InlineGrafanaMetricsFilter,
Filter::TableOfContentsFilter,
Filter::AutolinkFilter,
Filter::ExternalLinkFilter,
......
......@@ -12,6 +12,7 @@ module Gitlab
# @param project [Project]
# @param user [User]
# @param environment [Environment]
# @param options [Hash<Symbol,Any>]
# @param options - embedded [Boolean] Determines whether the
# dashboard is to be rendered as part of an
# issue or location other than the primary
......@@ -31,6 +32,8 @@ module Gitlab
# @param options - cluster [Cluster]
# @param options - cluster_type [Symbol] The level of
# cluster, one of [:admin, :project, :group]
# @param options - grafana_url [String] URL pointing
# to a grafana dashboard panel
# @return [Hash]
def find(project, user, options = {})
service_for(options)
......
......@@ -18,6 +18,7 @@ module Gitlab
# @return [Gitlab::Metrics::Dashboard::Services::BaseService]
def call(params)
return SERVICES::CustomMetricEmbedService if custom_metric_embed?(params)
return SERVICES::GrafanaMetricEmbedService if grafana_metric_embed?(params)
return SERVICES::DynamicEmbedService if dynamic_embed?(params)
return SERVICES::DefaultEmbedService if params[:embedded]
return SERVICES::SystemDashboardService if system_dashboard?(params[:dashboard_path])
......@@ -40,6 +41,10 @@ module Gitlab
SERVICES::CustomMetricEmbedService.valid_params?(params)
end
def grafana_metric_embed?(params)
SERVICES::GrafanaMetricEmbedService.valid_params?(params)
end
def dynamic_embed?(params)
SERVICES::DynamicEmbedService.valid_params?(params)
end
......
......@@ -14,17 +14,31 @@ module Gitlab
def regex
%r{
(?<url>
#{Regexp.escape(Gitlab.config.gitlab.url)}
\/#{Project.reference_pattern}
#{gitlab_pattern}
#{project_pattern}
(?:\/\-)?
\/environments
\/(?<environment>\d+)
\/metrics
(?<query>
\?[a-zA-Z0-9%.()+_=-]+
(&[a-zA-Z0-9%.()+_=-]+)*
)?
(?<anchor>\#[a-z0-9_-]+)?
#{query_pattern}
#{anchor_pattern}
)
}x
end
# Matches dashboard urls for a Grafana embed.
#
# EX - https://<host>/<namespace>/<project>/grafana/metrics_dashboard
def grafana_regex
%r{
(?<url>
#{gitlab_pattern}
#{project_pattern}
(?:\/\-)?
\/grafana
\/metrics_dashboard
#{query_pattern}
#{anchor_pattern}
)
}x
end
......@@ -45,6 +59,24 @@ module Gitlab
def build_dashboard_url(*args)
Gitlab::Routing.url_helpers.metrics_dashboard_namespace_project_environment_url(*args)
end
private
def gitlab_pattern
Regexp.escape(Gitlab.config.gitlab.url)
end
def project_pattern
"\/#{Project.reference_pattern}"
end
def query_pattern
'(?<query>\?[a-zA-Z0-9%.()+_=-]+(&[a-zA-Z0-9%.()+_=-]+)*)?'
end
def anchor_pattern
'(?<anchor>\#[a-z0-9_-]+)?'
end
end
end
end
......
......@@ -14,6 +14,7 @@ module Gitlab
NOTIFICATION_CHANNEL = 'workhorse:notifications'
ALLOWED_GIT_HTTP_ACTIONS = %w[git_receive_pack git_upload_pack info_refs].freeze
DETECT_HEADER = 'Gitlab-Workhorse-Detect-Content-Type'
ARCHIVE_FORMATS = %w(zip tar.gz tar.bz2 tar).freeze
include JwtAuthenticatable
......
......@@ -8498,6 +8498,9 @@ msgstr ""
msgid "GroupSettings|cannot be disabled when the parent group \"Share with group lock\" is enabled, except by the owner of the parent group"
msgstr ""
msgid "GroupSettings|cannot change when group contains projects with NPM packages"
msgstr ""
msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
msgstr ""
......@@ -17645,6 +17648,9 @@ msgstr ""
msgid "TransferGroup|Database is not supported."
msgstr ""
msgid "TransferGroup|Group contains projects with NPM packages."
msgstr ""
msgid "TransferGroup|Group is already a root group."
msgstr ""
......@@ -17672,6 +17678,9 @@ msgstr ""
msgid "TransferProject|Project with same name or path in target namespace already exists"
msgstr ""
msgid "TransferProject|Root namespace can't be updated if project has NPM packages"
msgstr ""
msgid "TransferProject|Transfer failed, please contact an admin."
msgstr ""
......@@ -19209,6 +19218,9 @@ msgstr ""
msgid "You can test your .gitlab-ci.yml in %{linkStart}CI Lint%{linkEnd}."
msgstr ""
msgid "You can try again using %{begin_link}basic search%{end_link}"
msgstr ""
msgid "You cannot access the raw file. Please wait a minute."
msgstr ""
......
......@@ -30,7 +30,7 @@ module QA
yield dir
ensure
FileUtils.remove_entry(dir)
FileUtils.remove_entry(dir, true)
end
private
......
......@@ -27,10 +27,16 @@ module QA
--hostname #{host_name}
--name #{@name}
--volume #{@volume_host_path}:/home/maven
#{@image} sh -c "sleep 60"
#{@image} sh -c "sleep 300"
CMD
shell "docker cp #{@volume_host_path}/. #{@name}:/home/maven"
shell "docker exec -t #{@name} sh -c 'cd /home/maven && mvn deploy -s settings.xml'"
# Stop the container when `mvn deploy` is finished otherwise
# the sleeping container will hold onto the files in @volume_host_path,
# which causes problems when they're created in a tmp dir
# that we want to delete
shell "docker stop #{@name}"
end
end
end
......
......@@ -31,11 +31,13 @@ describe MetricsDashboard do
end
context 'when params are provided' do
let(:params) { { environment: environment } }
before do
allow(controller).to receive(:project).and_return(project)
allow(controller)
.to receive(:metrics_dashboard_params)
.and_return(environment: environment)
.and_return(params)
end
it 'returns the specified dashboard' do
......@@ -43,6 +45,15 @@ describe MetricsDashboard do
expect(json_response).not_to have_key('all_dashboards')
end
context 'when the params are in an alternate format' do
let(:params) { ActionController::Parameters.new({ environment: environment }).permit! }
it 'returns the specified dashboard' do
expect(json_response['dashboard']['dashboard']).to eq('Environment metrics')
expect(json_response).not_to have_key('all_dashboards')
end
end
context 'when parameters are provided and the list of all dashboards is required' do
before do
allow(controller).to receive(:include_all_dashboards?).and_return(true)
......
......@@ -94,4 +94,87 @@ describe Projects::GrafanaApiController do
end
end
end
describe 'GET #metrics_dashboard' do
let(:service_result) { { status: :success, dashboard: '{}' } }
let(:params) do
{
format: :json,
embedded: true,
grafana_url: 'https://grafana.example.com',
namespace_id: project.namespace.full_path,
project_id: project.name
}
end
before do
allow(Gitlab::Metrics::Dashboard::Finder)
.to receive(:find)
.and_return(service_result)
end
context 'when the result is still processing' do
let(:service_result) { nil }
it 'returns 204 no content' do
get :metrics_dashboard, params: params
expect(response).to have_gitlab_http_status(:no_content)
end
end
context 'when the result was successful' do
it 'returns the dashboard response' do
get :metrics_dashboard, params: params
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to eq({
'dashboard' => '{}',
'status' => 'success'
})
end
end
context 'when an error has occurred' do
shared_examples_for 'error response' do |http_status|
it "returns #{http_status}" do
get :metrics_dashboard, params: params
expect(response).to have_gitlab_http_status(http_status)
expect(json_response['status']).to eq('error')
expect(json_response['message']).to eq('error message')
end
end
context 'with an error accessing grafana' do
let(:service_result) do
{
http_status: :service_unavailable,
status: :error,
message: 'error message'
}
end
it_behaves_like 'error response', :service_unavailable
end
context 'with a processing error' do
let(:service_result) { { status: :error, message: 'error message' } }
it_behaves_like 'error response', :bad_request
end
end
context 'when grafana embeds are not enabled' do
before do
stub_feature_flags(gfm_grafana_integration: false)
end
it 'returns 403 immediately' do
get :metrics_dashboard, params: params
expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
end
......@@ -28,6 +28,7 @@ describe 'User uses header search field', :js do
context 'when using the keyboard shortcut' do
before do
find('#search.js-autocomplete-disabled')
find('body').native.send_keys('s')
end
......
# frozen_string_literal: true
require 'spec_helper'
describe Banzai::Filter::InlineGrafanaMetricsFilter do
include FilterSpecHelper
let_it_be(:project) { create(:project) }
let_it_be(:grafana_integration) { create(:grafana_integration, project: project) }
let(:input) { %(<a href="#{url}">example</a>) }
let(:doc) { filter(input) }
let(:url) { grafana_integration.grafana_url + dashboard_path }
let(:dashboard_path) do
'/d/XDaNK6amz/gitlab-omnibus-redis' \
'?from=1570397739557&to=1570484139557' \
'&var-instance=All&panelId=14'
end
context 'when feature flag is disabled' do
before do
stub_feature_flags(gfm_grafana_integration: false)
end
it 'leaves the markdown unchanged' do
expect(unescape(doc.to_s)).to eq(input)
end
end
it 'appends a metrics charts placeholder with dashboard url after metrics links' do
node = doc.at_css('.js-render-metrics')
expect(node).to be_present
dashboard_url = urls.project_grafana_api_metrics_dashboard_url(
project,
embedded: true,
grafana_url: url,
start: "2019-10-06T21:35:39Z",
end: "2019-10-07T21:35:39Z"
)
expect(node.attribute('data-dashboard-url').to_s).to eq(dashboard_url)
end
context 'when the dashboard link is part of a paragraph' do
let(:paragraph) { %(This is an <a href="#{url}">example</a> of metrics.) }
let(:input) { %(<p>#{paragraph}</p>) }
it 'appends the charts placeholder after the enclosing paragraph' do
expect(unescape(doc.at_css('p').to_s)).to include(paragraph)
expect(doc.at_css('.js-render-metrics')).to be_present
end
end
context 'when grafana is not configured' do
before do
allow(project).to receive(:grafana_integration).and_return(nil)
end
it 'leaves the markdown unchanged' do
expect(unescape(doc.to_s)).to eq(input)
end
end
context 'when parameters are missing' do
let(:dashboard_path) { '/d/XDaNK6amz/gitlab-omnibus-redis' }
it 'leaves the markdown unchanged' do
expect(unescape(doc.to_s)).to eq(input)
end
end
private
# Nokogiri escapes the URLs, but we don't care about that
# distinction for the purposes of this filter
def unescape(html)
CGI.unescapeHTML(html)
end
end
......@@ -18,30 +18,48 @@ describe Banzai::Filter::InlineMetricsRedactorFilter do
end
context 'with a metrics charts placeholder' do
let(:input) { %(<div class="js-render-metrics" data-dashboard-url="#{url}"></div>) }
shared_examples_for 'a supported metrics dashboard url' do
context 'no user is logged in' do
it 'redacts the placeholder' do
expect(doc.to_s).to be_empty
end
end
context 'no user is logged in' do
it 'redacts the placeholder' do
expect(doc.to_s).to be_empty
context 'the user does not have permission do see charts' do
let(:doc) { filter(input, current_user: build(:user)) }
it 'redacts the placeholder' do
expect(doc.to_s).to be_empty
end
end
end
context 'the user does not have permission do see charts' do
let(:doc) { filter(input, current_user: build(:user)) }
context 'the user has requisite permissions' do
let(:user) { create(:user) }
let(:doc) { filter(input, current_user: user) }
it 'redacts the placeholder' do
expect(doc.to_s).to be_empty
it 'leaves the placeholder' do
project.add_maintainer(user)
expect(doc.to_s).to eq input
end
end
end
context 'the user has requisite permissions' do
let(:user) { create(:user) }
let(:doc) { filter(input, current_user: user) }
let(:input) { %(<div class="js-render-metrics" data-dashboard-url="#{url}"></div>) }
it 'leaves the placeholder' do
project.add_maintainer(user)
it_behaves_like 'a supported metrics dashboard url'
context 'for a grafana dashboard' do
let(:url) { urls.project_grafana_api_metrics_dashboard_url(project, embedded: true) }
it_behaves_like 'a supported metrics dashboard url'
end
expect(doc.to_s).to eq input
context 'for an internal non-dashboard url' do
let(:url) { urls.project_url(project) }
it 'leaves the placeholder' do
expect(doc.to_s).to be_empty
end
end
end
......
......@@ -75,6 +75,17 @@ describe Gitlab::Metrics::Dashboard::ServiceSelector do
it { is_expected.to be Metrics::Dashboard::CustomMetricEmbedService }
end
context 'with a grafana link' do
let(:arguments) do
{
embedded: true,
grafana_url: 'https://grafana.example.com'
}
end
it { is_expected.to be Metrics::Dashboard::GrafanaMetricEmbedService }
end
end
end
end
......@@ -3,13 +3,41 @@
require 'spec_helper'
describe Gitlab::Metrics::Dashboard::Url do
describe '#regex' do
it 'returns a regular expression' do
expect(described_class.regex).to be_a Regexp
end
shared_examples_for 'a regex which matches the expected url' do
it { is_expected.to be_a Regexp }
it 'matches a metrics dashboard link with named params' do
url = Gitlab::Routing.url_helpers.metrics_namespace_project_environment_url(
expect(subject).to match url
subject.match(url) do |m|
expect(m.named_captures).to eq expected_params
end
end
end
shared_examples_for 'does not match non-matching urls' do
it 'does not match other gitlab urls that contain the term metrics' do
url = Gitlab::Routing.url_helpers.active_common_namespace_project_prometheus_metrics_url('foo', 'bar', :json)
expect(subject).not_to match url
end
it 'does not match other gitlab urls' do
url = Gitlab.config.gitlab.url
expect(subject).not_to match url
end
it 'does not match non-gitlab urls' do
url = 'https://www.super_awesome_site.com/'
expect(subject).not_to match url
end
end
describe '#regex' do
let(:url) do
Gitlab::Routing.url_helpers.metrics_namespace_project_environment_url(
'foo',
'bar',
1,
......@@ -18,8 +46,10 @@ describe Gitlab::Metrics::Dashboard::Url do
group: 'awesome group',
anchor: 'title'
)
end
expected_params = {
let(:expected_params) do
{
'url' => url,
'namespace' => 'foo',
'project' => 'bar',
......@@ -27,31 +57,40 @@ describe Gitlab::Metrics::Dashboard::Url do
'query' => '?dashboard=config%2Fprometheus%2Fcommon_metrics.yml&group=awesome+group&start=2019-08-02T05%3A43%3A09.000Z',
'anchor' => '#title'
}
expect(described_class.regex).to match url
described_class.regex.match(url) do |m|
expect(m.named_captures).to eq expected_params
end
end
it 'does not match other gitlab urls that contain the term metrics' do
url = Gitlab::Routing.url_helpers.active_common_namespace_project_prometheus_metrics_url('foo', 'bar', :json)
subject { described_class.regex }
expect(described_class.regex).not_to match url
end
it_behaves_like 'a regex which matches the expected url'
it_behaves_like 'does not match non-matching urls'
end
it 'does not match other gitlab urls' do
url = Gitlab.config.gitlab.url
describe '#grafana_regex' do
let(:url) do
Gitlab::Routing.url_helpers.namespace_project_grafana_api_metrics_dashboard_url(
'foo',
'bar',
start: '2019-08-02T05:43:09.000Z',
dashboard: 'config/prometheus/common_metrics.yml',
group: 'awesome group',
anchor: 'title'
)
end
expect(described_class.regex).not_to match url
let(:expected_params) do
{
'url' => url,
'namespace' => 'foo',
'project' => 'bar',
'query' => '?dashboard=config%2Fprometheus%2Fcommon_metrics.yml&group=awesome+group&start=2019-08-02T05%3A43%3A09.000Z',
'anchor' => '#title'
}
end
it 'does not match non-gitlab urls' do
url = 'https://www.super_awesome_site.com/'
subject { described_class.grafana_regex }
expect(described_class.regex).not_to match url
end
it_behaves_like 'a regex which matches the expected url'
it_behaves_like 'does not match non-matching urls'
end
describe '#build_dashboard_url' do
......
......@@ -57,14 +57,14 @@ RSpec.describe Release do
subject { release.assets_count }
it 'returns the number of sources' do
is_expected.to eq(Releases::Source::FORMATS.count)
is_expected.to eq(Gitlab::Workhorse::ARCHIVE_FORMATS.count)
end
context 'when a links exists' do
let!(:link) { create(:release_link, release: release) }
it 'counts the link as an asset' do
is_expected.to eq(1 + Releases::Source::FORMATS.count)
is_expected.to eq(1 + Gitlab::Workhorse::ARCHIVE_FORMATS.count)
end
it "excludes sources count when asked" do
......
......@@ -11,7 +11,7 @@ describe Releases::Source do
it 'returns all formats of sources' do
expect(subject.map(&:format))
.to match_array(described_class::FORMATS)
.to match_array(Gitlab::Workhorse::ARCHIVE_FORMATS)
end
end
......
# frozen_string_literal: true
shared_examples 'archive download buttons' do
let(:formats) { %w(zip tar.gz tar.bz2 tar) }
let(:path_to_visit) { project_path(project) }
let(:ref) { project.default_branch }
......@@ -13,7 +12,7 @@ shared_examples 'archive download buttons' do
context 'private project' do
it 'shows archive download buttons with external storage URL prepended and user token appended to their href' do
formats.each do |format|
Gitlab::Workhorse::ARCHIVE_FORMATS.each do |format|
path = archive_path(project, ref, format)
uri = URI('https://cdn.gitlab.com')
uri.path = path
......@@ -28,7 +27,7 @@ shared_examples 'archive download buttons' do
let(:project) { create(:project, :repository, :public) }
it 'shows archive download buttons with external storage URL prepended to their href' do
formats.each do |format|
Gitlab::Workhorse::ARCHIVE_FORMATS.each do |format|
path = archive_path(project, ref, format)
uri = URI('https://cdn.gitlab.com')
uri.path = path
......@@ -45,7 +44,7 @@ shared_examples 'archive download buttons' do
end
it 'shows default archive download buttons' do
formats.each do |format|
Gitlab::Workhorse::ARCHIVE_FORMATS.each do |format|
path = archive_path(project, ref, format)
expect(page).to have_link format, href: path
......
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