From 3b2674502b0624bd290031ef98b89f56c10b1790 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Thu, 14 Mar 2019 11:06:48 +0100 Subject: [PATCH 001/154] Redirect Kubernetes installation to the charts docs With the advent of the new chart docs, we should deprecate the old ones and redirect them to the new ones. --- doc/administration/git_protocol.md | 2 +- doc/install/README.md | 4 +- doc/install/kubernetes/gitlab_chart.md | 159 +--------- doc/install/kubernetes/gitlab_omnibus.md | 249 +--------------- doc/install/kubernetes/gitlab_runner_chart.md | 272 +----------------- doc/install/kubernetes/index.md | 17 +- doc/install/kubernetes/preparation/connect.md | 30 +- doc/install/kubernetes/preparation/eks.md | 48 +--- .../kubernetes/preparation/networking.md | 41 +-- doc/install/kubernetes/preparation/rbac.md | 23 +- doc/install/kubernetes/preparation/tiller.md | 110 +------ .../preparation/tools_installation.md | 22 +- 12 files changed, 46 insertions(+), 931 deletions(-) diff --git a/doc/administration/git_protocol.md b/doc/administration/git_protocol.md index 345e8646f9b..1780e1babe9 100644 --- a/doc/administration/git_protocol.md +++ b/doc/administration/git_protocol.md @@ -31,7 +31,7 @@ From the client side, `git` `v2.18.0` or newer must be installed. From the server side, if we want to configure SSH we need to set the `sshd` server to accept the `GIT_PROTOCOL` environment. -In installations using [GitLab Helm Charts](../install/kubernetes/gitlab_chart.md) +In installations using [GitLab Helm Charts](https://docs.gitlab.com/charts/) and [All-in-one docker image](https://docs.gitlab.com/omnibus/docker/), the SSH service is already configured to accept the `GIT_PROTOCOL` environment and users need not do anything more. diff --git a/doc/install/README.md b/doc/install/README.md index 52011526768..53778f7f0d3 100644 --- a/doc/install/README.md +++ b/doc/install/README.md @@ -55,9 +55,9 @@ need to be aware of: - It can be more expensive for smaller installations. The default installation requires more resources than a single node Omnibus deployment, as most services are deployed in a redundant fashion. -- There are some feature [limitations to be aware of](kubernetes/gitlab_chart.md#limitations). +- There are some feature [limitations to be aware of](https://docs.gitlab.com/charts/#limitations). -[**> Install GitLab on Kubernetes using the GitLab Helm charts.**](kubernetes/index.md) +[**> Install GitLab on Kubernetes using the GitLab Helm charts.**](https://docs.gitlab.com/charts/) ## Installing GitLab with Docker diff --git a/doc/install/kubernetes/gitlab_chart.md b/doc/install/kubernetes/gitlab_chart.md index 9db246b3eb3..43655767002 100644 --- a/doc/install/kubernetes/gitlab_chart.md +++ b/doc/install/kubernetes/gitlab_chart.md @@ -1,156 +1,5 @@ -# GitLab Helm Chart +--- +redirect_to: https://docs.gitlab.com/charts/ +--- -This is the official way to install GitLab on a cloud native environment. - -NOTE: **Kubernetes experience required:** -Our Helm charts are recommended for those who are familiar with Kubernetes. -If you're not sure if Kubernetes is for you, our -[Omnibus GitLab packages](../README.md#installing-gitlab-using-the-omnibus-gitlab-package-recommended) -are mature, scalable, support [high availability](../../administration/high_availability/README.md) -and are used today on GitLab.com. -It is not necessary to have GitLab installed on Kubernetes in order to use [GitLab Kubernetes integration](https://docs.gitlab.com/ee/user/project/clusters/index.html). - -## Introduction - -The `gitlab` chart is the best way to operate GitLab on Kubernetes. This chart -contains all the required components to get started, and can scale to large deployments. - -The default deployment includes: - -- Core GitLab components: Unicorn, Shell, Workhorse, Registry, Sidekiq, and Gitaly -- Optional dependencies: Postgres, Redis, Minio -- An auto-scaling, unprivileged [GitLab Runner](https://docs.gitlab.com/runner/) using the Kubernetes executor -- Automatically provisioned SSL via [Let's Encrypt](https://letsencrypt.org/). - -## Limitations - -Some features of GitLab are not currently available: - -- [GitLab Pages](https://gitlab.com/charts/gitlab/issues/37) -- [GitLab Geo](https://gitlab.com/charts/gitlab/issues/8) -- [No in-cluster HA database](https://gitlab.com/charts/gitlab/issues/48) -- MySQL will not be supported, as support is [deprecated within GitLab](https://docs.gitlab.com/omnibus/settings/database.html#using-a-mysql-database-management-server-enterprise-edition-only) - -## Installing GitLab using the Helm Chart - -The `gitlab` chart includes all required dependencies, and takes a few minutes -to deploy. - -TIP: **Tip:** -For production deployments, we strongly recommend using the -[detailed installation instructions](https://gitlab.com/charts/gitlab/blob/master/doc/installation/index.md) -utilizing [external Postgres, Redis, and object storage](https://gitlab.com/charts/gitlab/tree/master/doc/advanced) services. - -### Requirements - -In order to deploy GitLab on Kubernetes, the following are required: - -1. `helm` and `kubectl` [installed on your computer](preparation/tools_installation.md). -1. A Kubernetes cluster, version 1.8 or higher. 6vCPU and 16GB of RAM is recommended. - - [Amazon EKS](https://docs.aws.amazon.com/eks/latest/userguide/getting-started.html) - - [Google GKE](https://cloud.google.com/kubernetes-engine/docs/how-to/creating-a-container-cluster) - - [IBM IKS](https://console.bluemix.net/docs/tutorials/scalable-webapp-kubernetes.html#create_kube_cluster) - - [Microsoft AKS](https://docs.microsoft.com/en-us/azure/aks/kubernetes-walkthrough-portal) -1. A [wildcard DNS entry and external IP address](preparation/networking.md) -1. [Authenticate and connect](preparation/connect.md) to the cluster -1. Configure and initialize [Helm Tiller](preparation/tiller.md). - -### Deployment of GitLab to Kubernetes - -To deploy GitLab, the following three parameters are required: - -- `global.hosts.domain`: the [base domain](preparation/networking.md) of the - wildcard host entry. For example, `example.com` if the wild card entry is - `*.example.com`. -- `global.hosts.externalIP`: the [external IP](preparation/networking.md) which - the wildcard DNS resolves to. -- `certmanager-issuer.email`: the email address to use when requesting new SSL - certificates from Let's Encrypt. - -NOTE: **Note:** -For deployments to Amazon EKS, there are -[additional configuration requirements](preparation/eks.md). A full list of -configuration options is [also available](https://gitlab.com/charts/gitlab/blob/master/doc/installation/command-line-options.md). - -Once you have all of your configuration options collected, you can get any -dependencies and run helm. In this example, the helm release is named "gitlab": - -```sh -helm repo add gitlab https://charts.gitlab.io/ -helm repo update -helm upgrade --install gitlab gitlab/gitlab \ - --timeout 600 \ - --set global.hosts.domain=example.com \ - --set global.hosts.externalIP=10.10.10.10 \ - --set certmanager-issuer.email=email@example.com -``` - -### Monitoring the Deployment - -This will output the list of resources installed once the deployment finishes, -which may take 5-10 minutes. - -The status of the deployment can be checked by running `helm status gitlab` -which can also be done while the deployment is taking place if you run the -command in another terminal. - -### Initial login - -You can access the GitLab instance by visiting the domain name beginning with -`gitlab.` followed by the domain specified during installation. From the example -above, the URL would be `https://gitlab.example.com`. - -If you manually created the secret for initial root password, you -can use that to sign in as `root` user. If not, GitLab automatically -created a random password for `root` user. This can be extracted by the -following command (replace `` by name of the release - which is `gitlab` -if you used the command above): - -```sh -kubectl get secret -gitlab-initial-root-password -ojsonpath={.data.password} | base64 --decode ; echo -``` - -### Outgoing email - -By default outgoing email is disabled. To enable it, provide details for your SMTP server -using the `global.smtp` and `global.email` settings. You can find details for these settings in the -[command line options](https://gitlab.com/charts/gitlab/blob/master/doc/installation/command-line-options.md#email-configuration). - -If your SMTP server requires authentication make sure to read the section on providing -your password in the [secrets documentation](https://gitlab.com/charts/gitlab/blob/master/doc/installation/secrets.md#smtp-password). -You can disable authentication settings with `--set global.smtp.authentication=""`. - -If your Kubernetes cluster is on GKE, be aware that SMTP port [25 is blocked](https://cloud.google.com/compute/docs/tutorials/sending-mail/#using_standard_email_ports). - -### Deploying the Community Edition - -To deploy the Community Edition, include these options in your `helm install` command: - -```sh ---set gitlab.migrations.image.repository=registry.gitlab.com/gitlab-org/build/cng/gitlab-rails-ce ---set gitlab.sidekiq.image.repository=registry.gitlab.com/gitlab-org/build/cng/gitlab-sidekiq-ce ---set gitlab.unicorn.image.repository=registry.gitlab.com/gitlab-org/build/cng/gitlab-unicorn-ce ---set gitlab.unicorn.workhorse.image=registry.gitlab.com/gitlab-org/build/cng/gitlab-workhorse-ce ---set gitlab.task-runner.image.repository=registry.gitlab.com/gitlab-org/build/cng/gitlab-task-runner-ce -``` - -## Updating GitLab using the Helm Chart - -Once your GitLab Chart is installed, configuration changes and chart updates -should be done using `helm upgrade`: - -```sh -helm repo update -helm upgrade --reuse-values gitlab gitlab/gitlab -``` - -## Uninstalling GitLab using the Helm Chart - -To uninstall the GitLab Chart, run the following: - -```sh -helm delete gitlab -``` - -[kube-srv]: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services---service-types -[storageclass]: https://kubernetes.io/docs/concepts/storage/persistent-volumes/#storageclasses +This document was moved to [another location](https://docs.gitlab.com/charts/). diff --git a/doc/install/kubernetes/gitlab_omnibus.md b/doc/install/kubernetes/gitlab_omnibus.md index c0cb7694e91..43655767002 100644 --- a/doc/install/kubernetes/gitlab_omnibus.md +++ b/doc/install/kubernetes/gitlab_omnibus.md @@ -1,246 +1,5 @@ -# GitLab-Omnibus Helm Chart +--- +redirect_to: https://docs.gitlab.com/charts/ +--- -CAUTION: **Caution:** -This chart is **deprecated**. We recommend using the [`gitlab` chart](gitlab_chart.md) -instead. A comparison of the two charts is available in [this video](https://youtu.be/Z6jWR8Z8dv8). - -For more information on available GitLab Helm Charts, see [Installing GitLab on Kubernetes](index.md). - -- This GitLab-Omnibus chart has been tested on Google Kubernetes Engine and Azure Container Service. -- This work is based partially on: . GitLab would like to thank Sergey Nuzhdin for his work. - -## Introduction - -This chart provides an easy way to get started with GitLab, provisioning an -installation with nearly all functionality enabled. SSL is automatically -provisioned via [Let's Encrypt](https://letsencrypt.org/). - -This Helm chart is suited for small to medium deployments and is **deprecated** -and replaced by the [cloud native GitLab chart](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md). -Due to the significant architectural changes, migrating will require backing up -data out of this instance and importing it into the new deployment. - -The deployment includes: - -- A [GitLab Omnibus](https://docs.gitlab.com/omnibus/) Pod, including Mattermost, Container Registry, and Prometheus -- An auto-scaling [GitLab Runner](https://docs.gitlab.com/runner/) using the Kubernetes executor -- [Redis](https://github.com/kubernetes/charts/tree/master/stable/redis) -- [PostgreSQL](https://github.com/kubernetes/charts/tree/master/stable/postgresql) -- [NGINX Ingress](https://github.com/kubernetes/charts/tree/master/stable/nginx-ingress) -- Persistent Volume Claims for Data, Registry, Postgres, and Redis - -## Limitations - -[High Availability](../../administration/high_availability/README.md) and -[Geo](https://docs.gitlab.com/ee/administration/geo/replication/index.html) are not supported. - -## Requirements - -- _At least_ 4 GB of RAM available on your cluster. 41GB of storage and 2 CPU are also required. -- Kubernetes 1.4+ with Beta APIs enabled -- [Persistent Volume](https://kubernetes.io/docs/concepts/storage/persistent-volumes/) provisioner support in the underlying infrastructure -- A [wildcard DNS entry](#networking-requirements), which resolves to the external IP address -- The `kubectl` CLI installed locally and authenticated for the cluster -- The [Helm client](https://github.com/kubernetes/helm/blob/master/docs/quickstart.md) installed locally on your machine - -### Networking requirements - -This chart configures a GitLab server and Kubernetes cluster which can support -dynamic [Review Apps](../../ci/review_apps/index.md), as well as services like -the integrated [Container Registry](../../user/project/container_registry.md) -and [Mattermost](https://docs.gitlab.com/omnibus/gitlab-mattermost/). - -To support the GitLab services and dynamic environments, a wildcard DNS entry -is required which resolves to the [load balancer](#load-balancer-ip) or -[external IP](#external-ip-recommended). Configuration of the DNS entry will depend upon -the DNS service being used. - -#### External IP (recommended) - -To provision an external IP on GCP and Azure, simply request a new address from -the Networking section. Ensure that the region matches the region your container -cluster is created in. It is important that the IP is not assigned at this point -in time. It will be automatically assigned once the Helm chart is installed, -and assigned to the Load Balancer. - -Now that an external IP address has been allocated, ensure that the wildcard -DNS entry you would like to use resolves to this IP. Please consult the -documentation for your DNS service for more information on creating DNS records. - -Finally, set the `baseIP` setting to this IP address when -[deploying GitLab](#configuring-and-installing-gitlab). - -#### Load Balancer IP - -If you do not specify a `baseIP`, an IP will be assigned to the Load Balancer or -Ingress. You can retrieve this IP by running the following command *after* deploying GitLab: - -```sh -kubectl get svc -w --namespace nginx-ingress nginx -``` - -The IP address will be displayed in the `EXTERNAL-IP` field, and should be used -to configure the Wildcard DNS entry. For more information on creating a wildcard -DNS entry, consult the documentation for the DNS server you are using. - -For production deployments of GitLab, we strongly recommend using a -[external IP](#external-ip-recommended). - -## Configuring and Installing GitLab - -For most installations, two parameters are required: - -- `baseDomain`: the [base domain](#networking-requirements) of the wildcard host entry. For example, `mycompany.io` if the wild card entry is `*.mycompany.io`. -- `legoEmail`: Email address to use when requesting new SSL certificates from Let's Encrypt. - -Other common configuration options: - -- `baseIP`: the desired [external IP address](#external-ip-recommended) -- `gitlab`: Choose the [desired edition](https://about.gitlab.com/pricing), either `ee` or `ce`. `ce` is the default. -- `gitlabEELicense`: For Enterprise Edition, the [license](https://docs.gitlab.com/ee/user/admin_area/license.html) can be installed directly via the Chart -- `provider`: Optimizes the deployment for a cloud provider. The default is `gke` for [Google Kubernetes Engine](https://cloud.google.com/kubernetes-engine/), with `acs` also supported for the [Azure Container Service](https://azure.microsoft.com/en-us/services/container-service/). - -For additional configuration options, consult the -[`values.yaml`](https://gitlab.com/charts/gitlab-omnibus/blob/master/values.yaml). - -### Choosing a different GitLab release version - -The version of GitLab installed is based on the `gitlab` setting (see [section](#configuring-and-installing-gitLab) above), and -the value of the corresponding helm setting: `gitlabCEImage` or `gitabEEImage`. - -```yaml -gitlab: CE -gitlabCEImage: gitlab/gitlab-ce:9.5.2-ce.0 -gitlabEEImage: gitlab/gitlab-ee:9.5.2-ee.0 -``` - -The different images can be found in the [gitlab-ce](https://hub.docker.com/r/gitlab/gitlab-ce/tags/) and [gitlab-ee](https://hub.docker.com/r/gitlab/gitlab-ee/tags/) -repositories on Docker Hub. - -### Persistent storage - -NOTE: **Note:** -If you are using a machine type with support for less than 4 attached disks, -like an Azure trial, you should disable dedicated storage for Postgres and Redis. - -By default, persistent storage is enabled for GitLab and the charts it depends -on (Redis and PostgreSQL). Components can have their claim size set from your -`values.yaml`, along with whether to provision separate storage for Postgres and Redis. - -Basic configuration: - -```yaml -redisImage: redis:3.2.10 -redisDedicatedStorage: true -redisStorageSize: 5Gi -postgresImage: postgres:9.6.3 -# If you disable postgresDedicatedStorage, you should consider bumping up gitlabRailsStorageSize -postgresDedicatedStorage: true -postgresStorageSize: 30Gi -gitlabRailsStorageSize: 30Gi -gitlabRegistryStorageSize: 30Gi -gitlabConfigStorageSize: 1Gi -``` - -### Routing and SSL - -Ingress routing and SSL are automatically configured within this Chart. An NGINX -ingress is provisioned and configured, and will route traffic to any service. -SSL certificates are automatically created and configured by -[kube-lego](https://github.com/kubernetes/charts/tree/master/stable/kube-lego). - -NOTE: **Note:** -Let's Encrypt limits a single TLD to five certificate requests within a single -week. This means that common DNS wildcard services like [nip.io](http://nip.io) -and [xip.io](http://xip.io) are unlikely to work. - -## Installing GitLab using the Helm Chart - -NOTE: **Note:** -You may see a temporary error message `SchedulerPredicates failed due to PersistentVolumeClaim is not bound` -while storage provisions. Once the storage provisions, the pods will automatically start. -This may take a couple minutes depending on your cloud provider. If the error persists, -please review the [requirements sections](#requirements) to ensure you have enough RAM, CPU, and storage. - -Add the GitLab Helm repository and initialize Helm: - -```bash -helm init -helm repo add gitlab https://charts.gitlab.io -``` - -Once you have reviewed the [configuration settings](#configuring-and-installing-gitlab), -you can install the chart. We recommending saving your configuration options in a -`values.yaml` file for easier upgrades in the future: - -```bash -helm install --name gitlab -f values.yaml gitlab/gitlab-omnibus -``` - -Or you can pass them on the command line: - -```bash -helm install --name gitlab --set baseDomain=gitlab.io,baseIP=192.0.2.1,gitlab=ee,gitlabEELicense=$LICENSE,legoEmail=email@gitlab.com gitlab/gitlab-omnibus -``` - -## Updating GitLab using the Helm Chart - -If you are upgrading from a previous version to 0.1.35 or above, you will need to -change the access mode values for GitLab's storage. To do this, set the following -in `values.yaml` or on the CLI: - -```sh -gitlabDataAccessMode=ReadWriteMany -gitlabRegistryAccessMode=ReadWriteMany -gitlabConfigAccessMode=ReadWriteMany -``` - -Once your GitLab Chart is installed, configuration changes and chart updates -should be done using `helm upgrade`: - -```sh -helm upgrade -f values.yaml gitlab gitlab/gitlab-omnibus -``` - -## Upgrading from CE to EE using the Helm Chart - -If you have installed the Community Edition using this chart, upgrading to -Enterprise Edition is easy. - -If you are using a `values.yaml` file to specify the configuration options, edit -the file and set `gitlab=ee`. If you would like to run a specific version of -GitLab EE, set `gitlabEEImage` to be the desired GitLab -[docker image](https://hub.docker.com/r/gitlab/gitlab-ee/tags/). Then you can -use `helm upgrade` to update your GitLab instance to EE: - -```bash -helm upgrade -f values.yaml gitlab gitlab/gitlab-omnibus -``` - -You can also upgrade and specify these options via the command line: - -```bash -helm upgrade gitlab --set gitlab=ee,gitlabEEImage=gitlab/gitlab-ee:9.5.5-ee.0 gitlab/gitlab-omnibus -``` - -## Uninstalling GitLab using the Helm Chart - -To uninstall the GitLab Chart, run the following: - -```bash -helm delete --purge gitlab -``` - -## Troubleshooting - -### Storage errors when updating `gitlab-omnibus` versions prior to 0.1.35 - -Users upgrading `gitlab-omnibus` from a version prior to 0.1.35, may see an error -like: `Error: UPGRADE FAILED: PersistentVolumeClaim "gitlab-gitlab-config-storage" is invalid: spec: Forbidden: field is immutable after creation`. - -This is due to a change in the access mode for GitLab storage in version 0.1.35. -To successfully upgrade, the access mode flags must be set to `ReadWriteMany` -as detailed in the [update section](#updating-gitlab-using-the-helm-chart). - -[kube-srv]: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services---service-types -[storageclass]: https://kubernetes.io/docs/concepts/storage/persistent-volumes/#storageclasses +This document was moved to [another location](https://docs.gitlab.com/charts/). diff --git a/doc/install/kubernetes/gitlab_runner_chart.md b/doc/install/kubernetes/gitlab_runner_chart.md index 68b2a146115..08ccf2cf9ad 100644 --- a/doc/install/kubernetes/gitlab_runner_chart.md +++ b/doc/install/kubernetes/gitlab_runner_chart.md @@ -1,269 +1,5 @@ -# GitLab Runner Helm Chart -> **Note:** -These charts have been tested on Google Kubernetes Engine and Azure Container Service. Other Kubernetes installations may work as well, if not please [open an issue](https://gitlab.com/gitlab-org/gitlab-runner/issues). +--- +redirect_to: https://docs.gitlab.com/runner/install/kubernetes.html +--- -The `gitlab-runner` Helm chart deploys a GitLab Runner instance into your -Kubernetes cluster. - -This chart configures the Runner to: - -- Run using the GitLab Runner [Kubernetes executor](https://docs.gitlab.com/runner/install/kubernetes.html) -- For each new job it receives from [GitLab CI](https://about.gitlab.com/features/gitlab-ci-cd/), it will provision a - new pod within the specified namespace to run it. - -For more information on available GitLab Helm Charts, please see our [overview](index.md). - -## Prerequisites - -- Your GitLab Server's API is reachable from the cluster -- Kubernetes 1.4+ with Beta APIs enabled -- The `kubectl` CLI installed locally and authenticated for the cluster -- The [Helm client](https://github.com/kubernetes/helm/blob/master/docs/quickstart.md) installed locally on your machine - -## Configuring GitLab Runner using the Helm Chart - -Create a `values.yaml` file for your GitLab Runner configuration. See [Helm docs](https://github.com/kubernetes/helm/blob/master/docs/chart_template_guide/values_files.md) -for information on how your values file will override the defaults. - -The default configuration can always be found in the [values.yaml](https://gitlab.com/charts/gitlab-runner/blob/master/values.yaml) in the chart repository. - -### Required configuration - -In order for GitLab Runner to function, your config file **must** specify the following: - - - `gitlabUrl` - the GitLab Server URL (with protocol) to register the runner against - - `runnerRegistrationToken` - The Registration Token for adding new Runners to the GitLab Server. This must be - retrieved from your GitLab Instance. See the [GitLab Runner Documentation](../../ci/runners/README.md) for more information. - -Unless you need to specify additional configuration, you are [ready to install](#installing-gitlab-runner-using-the-helm-chart). - -### Other configuration - -The rest of the configuration is [documented in the `values.yaml`](https://gitlab.com/charts/gitlab-runner/blob/master/values.yaml) in the chart repository. - -Here is a snippet of the important settings: - -```yaml -## The GitLab Server URL (with protocol) that want to register the runner against -## ref: https://docs.gitlab.com/runner/commands/README.html#gitlab-runner-register -## -gitlabUrl: http://gitlab.your-domain.com/ - -## The Registration Token for adding new Runners to the GitLab Server. This must -## be retrieved from your GitLab Instance. -## ref: https://docs.gitlab.com/ee/ci/runners/README.html -## -runnerRegistrationToken: "" - -## Set the certsSecretName in order to pass custom certificates for GitLab Runner to use -## Provide resource name for a Kubernetes Secret Object in the same namespace, -## this is used to populate the /etc/gitlab-runner/certs directory -## ref: https://docs.gitlab.com/runner/configuration/tls-self-signed.html#supported-options-for-self-signed-certificates -## -#certsSecretName: - -## Configure the maximum number of concurrent jobs -## ref: https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-global-section -## -concurrent: 10 - -## Defines in seconds how often to check GitLab for a new builds -## ref: https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-global-section -## -checkInterval: 30 - -## For RBAC support: -rbac: - create: false - - ## Run the gitlab-bastion container with the ability to deploy/manage containers of jobs - ## cluster-wide or only within namespace - clusterWideAccess: false - - ## Use the following Kubernetes Service Account name if RBAC is disabled in this Helm chart (see rbac.create) - ## - # serviceAccountName: default - -## Configuration for the Pods that the runner launches for each new job -## -runners: - ## Default container image to use for builds when none is specified - ## - image: ubuntu:16.04 - - ## Run all containers with the privileged flag enabled - ## This will allow the docker:stable-dind image to run if you need to run Docker - ## commands. Please read the docs before turning this on: - ## ref: https://docs.gitlab.com/runner/executors/kubernetes.html#using-docker-dind - ## - privileged: false - - ## Namespace to run Kubernetes jobs in (defaults to 'default') - ## - # namespace: - - ## Build Container specific configuration - ## - builds: - # cpuLimit: 200m - # memoryLimit: 256Mi - cpuRequests: 100m - memoryRequests: 128Mi - - ## Service Container specific configuration - ## - services: - # cpuLimit: 200m - # memoryLimit: 256Mi - cpuRequests: 100m - memoryRequests: 128Mi - - ## Helper Container specific configuration - ## - helpers: - # cpuLimit: 200m - # memoryLimit: 256Mi - cpuRequests: 100m - memoryRequests: 128Mi - -``` - -### Enabling RBAC support - -If your cluster has RBAC enabled, you can choose to either have the chart create its own service account or provide one. - -To have the chart create the service account for you, set `rbac.create` to true. - -### Controlling maximum Runner concurrency - -A single GitLab Runner deployed on Kubernetes is able to execute multiple jobs in parallel by automatically starting additional Runner pods. The [`concurrent` setting](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-global-section) controls the maximum number of pods allowed at a single time, and defaults to `10`. - -```yaml -## Configure the maximum number of concurrent jobs -## ref: https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-global-section -## -concurrent: 10 -``` - -### Running Docker-in-Docker containers with GitLab Runners - -See [Running Privileged Containers for the Runners](#running-privileged-containers-for-the-runners) for how to enable it, -and the [GitLab CI Runner documentation](https://docs.gitlab.com/runner/executors/kubernetes.html#using-docker-in-your-builds) on running dind. - -### Running privileged containers for the Runners - -You can tell the GitLab Runner to run using privileged containers. You may need -this enabled if you need to use the Docker executable within your GitLab CI jobs. - -This comes with several risks that you can read about in the -[GitLab CI Runner documentation](https://docs.gitlab.com/runner/executors/kubernetes.html#using-docker-in-your-builds). - -If you are okay with the risks, and your GitLab CI Runner instance is registered -against a specific project in GitLab that you trust the CI jobs of, you can -enable privileged mode in `values.yaml`: - -```yaml -runners: - ## Run all containers with the privileged flag enabled - ## This will allow the docker:stable-dind image to run if you need to run Docker - ## commands. Please read the docs before turning this on: - ## ref: https://docs.gitlab.com/runner/executors/kubernetes.html#using-docker-dind - ## - privileged: true -``` - -### Providing a custom certificate for accessing GitLab - -You can provide a [Kubernetes Secret](https://kubernetes.io/docs/concepts/configuration/secret/) -to the GitLab Runner Helm Chart, which will be used to populate the container's -`/etc/gitlab-runner/certs` directory. - -Each key name in the Secret will be used as a filename in the directory, with the -file content being the value associated with the key. - -More information on how GitLab Runner uses these certificates can be found in the -[Runner Documentation](https://docs.gitlab.com/runner/configuration/tls-self-signed.html#supported-options-for-self-signed-certificates). - - - The key/file name used should be in the format `.crt`. For example: `gitlab.your-domain.com.crt`. - - Any intermediate certificates need to be concatenated to your server certificate in the same file. - - The hostname used should be the one the certificate is registered for. - -The GitLab Runner Helm Chart does not create a secret for you. In order to create -the secret, you can prepare your certificate on you local machine, and then run -the `kubectl create secret` command from the directory with the certificate - -```bash -kubectl - --namespace - create secret generic - --from-file= -``` - -- `` is the Kubernetes namespace where you want to install the GitLab Runner. -- `` is the Kubernetes Secret resource name. For example: `gitlab-domain-cert` -- `` is the filename for the certificate in your current directory that will be imported into the secret - -You then need to provide the secret's name to the GitLab Runner chart. - -Add the following to your `values.yaml` - -```yaml -## Set the certsSecretName in order to pass custom certificates for GitLab Runner to use -## Provide resource name for a Kubernetes Secret Object in the same namespace, -## this is used to populate the /etc/gitlab-runner/certs directory -## ref: https://docs.gitlab.com/runner/configuration/tls-self-signed.html#supported-options-for-self-signed-certificates -## -certsSecretName: -``` - -- `` is the Kubernetes Secret resource name. For example: `gitlab-domain-cert` - -## Installing GitLab Runner using the Helm Chart - -Add the GitLab Helm repository and initialize Helm: - -```bash -helm repo add gitlab https://charts.gitlab.io -helm init -``` - -Once you [have configured](#configuring-gitlab-runner-using-the-helm-chart) GitLab Runner in your `values.yml` file, -run the following: - -```bash -helm install --namespace --name gitlab-runner -f gitlab/gitlab-runner -``` - -- `` is the Kubernetes namespace where you want to install the GitLab Runner. -- `` is the path to values file containing your custom configuration. See the - [Configuring GitLab Runner using the Helm Chart](#configuring-gitlab-runner-using-the-helm-chart) section to create it. - -## Updating GitLab Runner using the Helm Chart - -Once your GitLab Runner Chart is installed, configuration changes and chart updates should we done using `helm upgrade` - -```bash -helm upgrade --namespace -f gitlab/gitlab-runner -``` - -Where: - -- `` is the Kubernetes namespace where GitLab Runner is installed -- `` is the path to values file containing your custom configuration. See the - [Configuring GitLab Runner using the Helm Chart](#configuring-gitlab-runner-using-the-helm-chart) section to create it. -- `` is the name you gave the chart when installing it. - In the [Installing GitLab Runner using the Helm Chart](#installing-gitlab-runner-using-the-helm-chart) section, we called it `gitlab-runner`. - -## Uninstalling GitLab Runner using the Helm Chart - -To uninstall the GitLab Runner Chart, run the following: - -```bash -helm delete --namespace -``` - -where: - -- `` is the Kubernetes namespace where GitLab Runner is installed -- `` is the name you gave the chart when installing it. - In the [Installing GitLab Runner using the Helm Chart](#installing-gitlab-runner-using-the-helm-chart) section, we called it `gitlab-runner`. +This document was moved to [another location](https://docs.gitlab.com/runner/install/kubernetes.html). diff --git a/doc/install/kubernetes/index.md b/doc/install/kubernetes/index.md index ecc956d04e9..7312bf2d4f7 100644 --- a/doc/install/kubernetes/index.md +++ b/doc/install/kubernetes/index.md @@ -21,16 +21,15 @@ of the application including how it should be deployed, upgraded, and configured ## GitLab Chart This chart contains all the required components to get started, and can scale to -large deployments. It offers a number of benefits: +large deployments. It offers a number of benefits, among others: -- Horizontal scaling of individual components -- No requirement for shared storage to scale -- Containers do not need `root` permissions -- Automatic SSL with Let's Encrypt -- An unprivileged GitLab Runner -- and plenty more. +- Horizontal scaling of individual components. +- No requirement for shared storage to scale. +- Containers do not need `root` permissions. +- Automatic SSL with Let's Encrypt. +- An unprivileged GitLab Runner. -Learn more about the [GitLab chart](gitlab_chart.md). +Learn more about the [GitLab chart](https://docs.gitlab.com/charts/). ## GitLab Runner Chart @@ -39,4 +38,4 @@ and you'd like to leverage the Runner's [Kubernetes capabilities](https://docs.gitlab.com/runner/executors/kubernetes.html), it can be deployed with the GitLab Runner chart. -Learn more about [gitlab-runner chart](gitlab_runner_chart.md). +Learn more about the [GitLab Runner chart](https://docs.gitlab.com/runner/install/kubernetes.html). diff --git a/doc/install/kubernetes/preparation/connect.md b/doc/install/kubernetes/preparation/connect.md index a3a0cba4bf2..db55e03d3d4 100644 --- a/doc/install/kubernetes/preparation/connect.md +++ b/doc/install/kubernetes/preparation/connect.md @@ -1,27 +1,5 @@ -# Connecting your computer to a cluster +--- +redirect_to: https://docs.gitlab.com/charts/installation/cloud/ +--- -In order to deploy software and settings to a cluster, you must connect and authenticate to it. - -## Connect to GKE cluster - -The command for connection to the cluster can be obtained from the -[Google Cloud Platform Console](https://console.cloud.google.com/kubernetes/list) -by the individual cluster. - -Look for the **Connect** button in the clusters list page or use the command below, -filling in your cluster's information: - -``` -gcloud container clusters get-credentials --zone --project -``` - -## Connect to EKS cluster - -For the most up to date instructions, follow the Amazon EKS documentation on -[connecting to a cluster](https://docs.aws.amazon.com/eks/latest/userguide/getting-started.html#eks-configure-kubectl). - -## Connect to local minikube cluster - -If you are doing local development, you can use `minikube` as your -local cluster. If `kubectl cluster-info` is not showing `minikube` as the current -cluster, use `kubectl config set-cluster minikube` to set the active cluster. +This document was moved to [another location](https://docs.gitlab.com/charts/installation/cloud/). diff --git a/doc/install/kubernetes/preparation/eks.md b/doc/install/kubernetes/preparation/eks.md index ea3b075dd82..975d35c11c6 100644 --- a/doc/install/kubernetes/preparation/eks.md +++ b/doc/install/kubernetes/preparation/eks.md @@ -1,45 +1,5 @@ -# Running GitLab on EKS +--- +redirect_to: https://docs.gitlab.com/charts/installation/cloud/eks.html +--- -There are a few nuances to Amazon EKS which are important to be aware of, when deploying GitLab. - -## Persistent volume management - -There are two methods to manage volume claims on Kubernetes: - -1. Manually creating each persistent volume (recommended on EKS) -1. Utilizing dynamic provisioning to automatically create the persistent volumes - -### Manual provisioning of volumes (Recommended) - -Manually creating the volumes allows you to control the zone of each volume, as well as all other details supported by the underlying storage. - -Follow our documentation on [manually creating persistent volumes](https://gitlab.com/charts/gitlab/blob/master/doc/installation/storage.md#manually-creating-static-volumes). - -### Dynamic provisioning of volumes - -Dynamic provisioning utilizes a Kubernetes provisioner, like `aws-ebs`, to automatically create persistent volumes to fulfill each claim. - -With EKS, there are a few important details to keep in mind: - -1. Clusters are required to span multiple AZ's -1. Kubernetes volume provisioners create volumes across zones without regard to which pod they belong to. This leads to scenarios where a pod with multiple volumes being unable to start due to the volumes being in different zones. -1. There is no default Storage Class. - -The easiest way to solve this and still utilize dynamic provisioning is to utilize, or create, a Storage Class that is locked to a specific zone. - -> **Note**: Restricting volumes to specific zone will cause GitLab and any other application using this Storage Class to only reside in that zone. For multiple zone support, utilize [manually provisioned volumes](#manual-provisioning-of-volumes-recommended). - -To create the storage class, download and edit Amazon EKS's [sample Storage Class](https://docs.aws.amazon.com/eks/latest/userguide/storage-classes.html) and add the following parameter: - -```yaml -parameters: - zone: -``` - -Then [specify the Storage Class](https://gitlab.com/charts/gitlab/blob/master/doc/installation/storage.md#using-a-custom-storage-class) name when deploying GitLab. - -## External access to GitLab - -By default, GitLab will an deploy an ingress which will create an associated Elastic Load Balancer. Since the DNS names of ELB's cannot be known ahead of time, it is difficult to utilize Let's Encrypt to automatically provision HTTPS certificates. - -We recommend [using your own certificates](https://gitlab.com/charts/gitlab/blob/master/doc/installation/tls.md#option-2-use-your-own-wildcard-certificate), and then mapping your desired DNS name to the created ELB using a CNAME record. +This document was moved to [another location](https://docs.gitlab.com/charts/installation/cloud/eks.html). diff --git a/doc/install/kubernetes/preparation/networking.md b/doc/install/kubernetes/preparation/networking.md index b9fb9a7399f..2af16a752dc 100644 --- a/doc/install/kubernetes/preparation/networking.md +++ b/doc/install/kubernetes/preparation/networking.md @@ -1,38 +1,5 @@ -# Networking Prerequisites +--- +redirect_to: https://docs.gitlab.com/charts/installation/deployment.html#networking-and-dns +--- -NOTE: **Note:** -Amazon EKS utilizes Elastic Load Balancers, which are addressed by DNS name and -cannot be known ahead of time. If you're using EKS, you can skip this section. - -The `gitlab` chart configures a GitLab server and Kubernetes cluster which can support dynamic [Review Apps](https://docs.gitlab.com/ee/ci/review_apps/index.html), as well as services like the integrated [Container Registry](https://docs.gitlab.com/ee/user/project/container_registry.html). - -To support the GitLab services and dynamic environments, a wildcard DNS entry is required which resolves to the external IP. - -## External IP - -To provision an external IP on GCP and Azure, simply request a new address from the Networking section. Ensure that the region matches the region your container cluster is created in. Note, it is important that the IP is not assigned at this point in time. It will be automatically assigned once the Helm chart is installed, to the Load Balancer. - -Set `global.hosts.externalIP` to this IP address when [deploying GitLab](../gitlab_chart.md#installing-gitlab-using-the-helm-chart). - -Then, create a [wildcard DNS record](#wildcard-dns-entry) which resolves to this IP address. - -### Creating an external IP on GCP - -When creating the external IP, it is critical to create it in the same region as your cluster. Otherwise, the IP address will fail to bind to the Load Balancer. - -1. Open the [web console](https://console.cloud.google.com) -1. In the sidebar, browse to `VPC Network > External IP addresses` -1. Click `Reserve static address` -1. Choose `Regional` and select the region of your cluster -1. Leave `Attached to` blank, as it will be automatically assigned during deployment - -## Wildcard DNS entry - -Now that an external IP address has been allocated, ensure that the wildcard DNS entry you would like to use resolves to this IP. Typically this would be an `A record` for `*`, resolving to the external IP above. - -Please consult the documentation for your DNS service for more information on creating DNS records: - -- [Google Domains](https://support.google.com/domains/answer/3290350?hl=en) -- [GoDaddy](https://www.godaddy.com/help/add-an-a-record-19238) - -Set `global.hosts.domain` to this DNS name when [deploying GitLab](../gitlab_chart.md#installing-gitlab-using-the-helm-chart). +This document was moved to [another location](https://docs.gitlab.com/charts/installation/deployment.html#networking-and-dns). diff --git a/doc/install/kubernetes/preparation/rbac.md b/doc/install/kubernetes/preparation/rbac.md index c5f8d7a7e9e..f94e7c24cdc 100644 --- a/doc/install/kubernetes/preparation/rbac.md +++ b/doc/install/kubernetes/preparation/rbac.md @@ -1,20 +1,5 @@ -# Role Based Access Control +--- +redirect_to: https://docs.gitlab.com/charts/installation/deployment.html#rbac +--- -Until Kubernetes 1.7, there were no permissions within a cluster. With the launch -of 1.7, there is now a [role based access control system (RBAC)](https://kubernetes.io/docs/admin/authorization/rbac/) -which determines what services can perform actions within a cluster. - -RBAC affects a few different aspects of GitLab: - -- [Installation of GitLab using Helm](tiller.md#preparing-for-helm-with-rbac) -- Prometheus monitoring -- GitLab Runner - -## Checking that RBAC is enabled - -Try listing the current cluster roles, if it fails then `RBAC` is disabled. -The following command will output `false` if `RBAC` is disabled and `true` otherwise: - -```sh -kubectl get clusterroles > /dev/null 2>&1 && echo true || echo false -``` +This document was moved to [another location](https://docs.gitlab.com/charts/installation/deployment.html#rbac). diff --git a/doc/install/kubernetes/preparation/tiller.md b/doc/install/kubernetes/preparation/tiller.md index 684df14ac2c..66d6c8faece 100644 --- a/doc/install/kubernetes/preparation/tiller.md +++ b/doc/install/kubernetes/preparation/tiller.md @@ -1,109 +1,5 @@ -# Configuring and initializing Helm Tiller - -To make use of Helm, you must have a [Kubernetes][k8s-io] cluster. Ensure you can -access your cluster using `kubectl`. - -Helm consists of two parts, the `helm` client and a `tiller` server inside Kubernetes. - -NOTE: **Note:** -If you are not able to run Tiller in your cluster, for example on OpenShift, it -is possible to use [Tiller locally](https://docs.gitlab.com/charts/installation/tools.html#local-tiller) -and avoid deploying it into the cluster. This should only be used when Tiller -cannot be normally deployed. - -## Initialize Helm and Tiller - -Tiller is deployed into the cluster and interacts with the Kubernetes API to deploy your applications. If role based access control (RBAC) is enabled, Tiller will need to be [granted permissions](#preparing-for-helm-with-rbac) to allow it to talk to the Kubernetes API. - -If RBAC is not enabled, skip to [initializing Helm](#initialize-helm). - -If you are not sure whether RBAC is enabled in your cluster, or to learn more, read through our [RBAC documentation](rbac.md). - -## Preparing for Helm with RBAC - -Helm's Tiller will need to be granted permissions to perform operations. These instructions grant cluster wide permissions, however for more advanced deployments [permissions can be restricted to a single namespace](https://docs.helm.sh/using_helm/#example-deploy-tiller-in-a-namespace-restricted-to-deploying-resources-only-in-that-namespace). To grant access to the cluster, we will create a new `tiller` service account and bind it to the `cluster-admin` role. - -Create a file `rbac-config.yaml` with the following contents: - -```yaml -apiVersion: v1 -kind: ServiceAccount -metadata: - name: tiller - namespace: kube-system --- -apiVersion: rbac.authorization.k8s.io/v1beta1 -kind: ClusterRoleBinding -metadata: - name: tiller -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: cluster-admin -subjects: - - kind: ServiceAccount - name: tiller - namespace: kube-system -``` - -Next we need to connect to the cluster and upload the RBAC config. - -### Upload the RBAC config - -Some clusters require authentication to use `kubectl` to create the Tiller roles. - -#### Upload the RBAC config as an admin user (GKE) - -For GKE, you need to obtain the admin credentials. This command will output the admin password: - -``` -gcloud container clusters describe --zone --project --format='value(masterAuth.password)' -``` - -Use the admin password to set the admin credentials. Replace the password value below with the output value from the above step: - -``` -kubectl config set-credentials admin --username=admin --password=xxxxxxxxxxxxxx -``` - -Once credentials have been set, create the role: - -``` -kubectl --user=admin create -f rbac-config.yaml -``` - -#### Upload the RBAC config (Non-GKE clusters) - -For other clusters like Amazon EKS, you can directly upload the RBAC configuration. - -``` -kubectl create -f rbac-config.yaml -``` - -## Initialize Helm - -Deploy Helm Tiller with a service account: - -``` -helm init --service-account tiller -``` - -If your cluster previously had Helm/Tiller installed, -run the following to ensure that the deployed version of Tiller matches the local Helm version: - -``` -helm init --upgrade --service-account tiller -``` - -### Patching Helm Tiller for Amazon EKS - -Helm Tiller requires a flag to be enabled to work properly on Amazon EKS: - -``` -kubectl -n kube-system patch deployment tiller-deploy -p '{"spec": {"template": {"spec": {"automountServiceAccountToken": true}}}}' -``` +redirect_to: https://docs.gitlab.com/charts/installation/tools.html +--- -[helm]: https://helm.sh -[helm-using]: https://docs.helm.sh/using_helm -[k8s-io]: https://kubernetes.io/ -[gcp-k8s]: https://console.cloud.google.com/kubernetes/list +This document was moved to [another location](https://docs.gitlab.com/charts/installation/tools.html). diff --git a/doc/install/kubernetes/preparation/tools_installation.md b/doc/install/kubernetes/preparation/tools_installation.md index d2f7a69a0af..66d6c8faece 100644 --- a/doc/install/kubernetes/preparation/tools_installation.md +++ b/doc/install/kubernetes/preparation/tools_installation.md @@ -1,19 +1,5 @@ -# Installing kubectl and Helm on your computer +--- +redirect_to: https://docs.gitlab.com/charts/installation/tools.html +--- -In order to work with the GitLab Helm charts, `kubectl` and `helm` must be installed and configured on your computer. - -## Installing `kubectl` - -`kubectl` is the Kubernetes command line tool, which can be used to deploy settings to the cluster. - -Follow the [official documentation](https://kubernetes.io/docs/tasks/tools/install-kubectl/) for the most up to date instructions. - -## Installing `helm` - -Helm is a package management tool for Kubernetes, and is used to deploy charts. - -You can get Helm from the project's [releases page](https://github.com/kubernetes/helm/releases), or follow other options under the official documentation of [Installing Helm](https://docs.helm.sh/using_helm/#installing-helm). - -# Next steps - -Once installed, proceed to the next [installation step](../gitlab_chart.md#installing-gitlab-using-the-helm-chart). +This document was moved to [another location](https://docs.gitlab.com/charts/installation/tools.html). -- GitLab From 70fff3c06bfbd0caec7e686481ec6f7920219599 Mon Sep 17 00:00:00 2001 From: jk2K <191983-jk2K@users.noreply.gitlab.com> Date: Mon, 25 Mar 2019 10:11:55 +0000 Subject: [PATCH 002/154] docs: fix artifacts folder path, according to GitLab CE 11.8.2 --- doc/administration/job_traces.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/administration/job_traces.md b/doc/administration/job_traces.md index 63945506f3c..aa9d87562a3 100644 --- a/doc/administration/job_traces.md +++ b/doc/administration/job_traces.md @@ -10,13 +10,13 @@ In the following table you can see the phases a trace goes through. | Phase | State | Condition | Data flow | Stored path | | ----- | ----- | --------- | --------- | ----------- | -| 1: patching | Live trace | When a job is running | GitLab Runner => Unicorn => file storage |`#{ROOT_PATH}/builds/#{YYYY_mm}/#{project_id}/#{job_id}.log`| -| 2: overwriting | Live trace | When a job is finished | GitLab Runner => Unicorn => file storage |`#{ROOT_PATH}/builds/#{YYYY_mm}/#{project_id}/#{job_id}.log`| -| 3: archiving | Archived trace | After a job is finished | Sidekiq moves live trace to artifacts folder |`#{ROOT_PATH}/shared/artifacts/#{disk_hash}/#{YYYY_mm_dd}/#{job_id}/#{job_artifact_id}/job.log`| +| 1: patching | Live trace | When a job is running | GitLab Runner => Unicorn => file storage |`#{ROOT_PATH}/gitlab-ci/builds/#{YYYY_mm}/#{project_id}/#{job_id}.log`| +| 2: overwriting | Live trace | When a job is finished | GitLab Runner => Unicorn => file storage |`#{ROOT_PATH}/gitlab-ci/builds/#{YYYY_mm}/#{project_id}/#{job_id}.log`| +| 3: archiving | Archived trace | After a job is finished | Sidekiq moves live trace to artifacts folder |`#{ROOT_PATH}/gitlab-rails/shared/artifacts/#{disk_hash}/#{YYYY_mm_dd}/#{job_id}/#{job_artifact_id}/job.log`| | 4: uploading | Archived trace | After a trace is archived | Sidekiq moves archived trace to [object storage](#uploading-traces-to-object-storage) (if configured) |`#{bucket_name}/#{disk_hash}/#{YYYY_mm_dd}/#{job_id}/#{job_artifact_id}/job.log`| The `ROOT_PATH` varies per your environment. For Omnibus GitLab it -would be `/var/opt/gitlab/gitlab-ci`, whereas for installations from source +would be `/var/opt/gitlab`, whereas for installations from source it would be `/home/git/gitlab`. ## Changing the job traces local location -- GitLab From 8dc7efb2b025c9519b94362f3394ea6a1889ade1 Mon Sep 17 00:00:00 2001 From: Luke Bennett Date: Thu, 14 Mar 2019 20:52:01 +0000 Subject: [PATCH 003/154] Improve project avatar settings Prioritize and simplify project settings content. --- .../{group_avatar.js => avatar_picker.js} | 9 ++++--- .../pages/admin/groups/edit/index.js | 4 +-- .../pages/admin/groups/new/index.js | 4 +-- .../javascripts/pages/groups/edit/index.js | 4 +-- .../javascripts/pages/groups/new/index.js | 4 +-- .../javascripts/pages/projects/edit/index.js | 4 +-- app/views/admin/groups/_form.html.haml | 2 +- app/views/groups/new.html.haml | 2 +- app/views/groups/settings/_general.html.haml | 4 +-- app/views/projects/edit.html.haml | 27 +++++++------------ .../shared/_choose_avatar_button.html.haml | 4 +++ .../_choose_group_avatar_button.html.haml | 4 --- .../ce-proj-settings-ok-avatar-only.yml | 5 ++++ locale/gitlab.pot | 8 +----- 14 files changed, 39 insertions(+), 46 deletions(-) rename app/assets/javascripts/{group_avatar.js => avatar_picker.js} (54%) create mode 100644 app/views/shared/_choose_avatar_button.html.haml delete mode 100644 app/views/shared/_choose_group_avatar_button.html.haml create mode 100644 changelogs/unreleased/ce-proj-settings-ok-avatar-only.yml diff --git a/app/assets/javascripts/group_avatar.js b/app/assets/javascripts/avatar_picker.js similarity index 54% rename from app/assets/javascripts/group_avatar.js rename to app/assets/javascripts/avatar_picker.js index dcda625f587..d38e0b4abaa 100644 --- a/app/assets/javascripts/group_avatar.js +++ b/app/assets/javascripts/avatar_picker.js @@ -1,11 +1,12 @@ import $ from 'jquery'; -export default function groupAvatar() { - $('.js-choose-group-avatar-button').on('click', function onClickGroupAvatar() { +export default function initAvatarPicker() { + $('.js-choose-avatar-button').on('click', function onClickAvatar() { const form = $(this).closest('form'); - return form.find('.js-group-avatar-input').click(); + return form.find('.js-avatar-input').click(); }); - $('.js-group-avatar-input').on('change', function onChangeAvatarInput() { + + $('.js-avatar-input').on('change', function onChangeAvatarInput() { const form = $(this).closest('form'); const filename = $(this) .val() diff --git a/app/assets/javascripts/pages/admin/groups/edit/index.js b/app/assets/javascripts/pages/admin/groups/edit/index.js index d3d125a1859..ad7276132b9 100644 --- a/app/assets/javascripts/pages/admin/groups/edit/index.js +++ b/app/assets/javascripts/pages/admin/groups/edit/index.js @@ -1,3 +1,3 @@ -import groupAvatar from '~/group_avatar'; +import initAvatarPicker from '~/avatar_picker'; -document.addEventListener('DOMContentLoaded', groupAvatar); +document.addEventListener('DOMContentLoaded', initAvatarPicker); diff --git a/app/assets/javascripts/pages/admin/groups/new/index.js b/app/assets/javascripts/pages/admin/groups/new/index.js index 21f1ce222ac..6de740ee9ce 100644 --- a/app/assets/javascripts/pages/admin/groups/new/index.js +++ b/app/assets/javascripts/pages/admin/groups/new/index.js @@ -1,9 +1,9 @@ import BindInOut from '../../../../behaviors/bind_in_out'; import Group from '../../../../group'; -import groupAvatar from '../../../../group_avatar'; +import initAvatarPicker from '~/avatar_picker'; document.addEventListener('DOMContentLoaded', () => { BindInOut.initAll(); new Group(); // eslint-disable-line no-new - groupAvatar(); + initAvatarPicker(); }); diff --git a/app/assets/javascripts/pages/groups/edit/index.js b/app/assets/javascripts/pages/groups/edit/index.js index 01ef445c901..d036ff07d89 100644 --- a/app/assets/javascripts/pages/groups/edit/index.js +++ b/app/assets/javascripts/pages/groups/edit/index.js @@ -1,4 +1,4 @@ -import groupAvatar from '~/group_avatar'; +import initAvatarPicker from '~/avatar_picker'; import TransferDropdown from '~/groups/transfer_dropdown'; import initConfirmDangerModal from '~/confirm_danger_modal'; import initSettingsPanels from '~/settings_panels'; @@ -9,7 +9,7 @@ import groupsSelect from '~/groups_select'; import projectSelect from '~/project_select'; document.addEventListener('DOMContentLoaded', () => { - groupAvatar(); + initAvatarPicker(); new TransferDropdown(); // eslint-disable-line no-new initConfirmDangerModal(); initSettingsPanels(); diff --git a/app/assets/javascripts/pages/groups/new/index.js b/app/assets/javascripts/pages/groups/new/index.js index b2f275dc5ea..57b53eb9e5d 100644 --- a/app/assets/javascripts/pages/groups/new/index.js +++ b/app/assets/javascripts/pages/groups/new/index.js @@ -1,9 +1,9 @@ import BindInOut from '~/behaviors/bind_in_out'; import Group from '~/group'; -import groupAvatar from '~/group_avatar'; +import initAvatarPicker from '~/avatar_picker'; document.addEventListener('DOMContentLoaded', () => { BindInOut.initAll(); new Group(); // eslint-disable-line no-new - groupAvatar(); + initAvatarPicker(); }); diff --git a/app/assets/javascripts/pages/projects/edit/index.js b/app/assets/javascripts/pages/projects/edit/index.js index 899d5925956..278c35d3846 100644 --- a/app/assets/javascripts/pages/projects/edit/index.js +++ b/app/assets/javascripts/pages/projects/edit/index.js @@ -3,7 +3,7 @@ import initSettingsPanels from '~/settings_panels'; import setupProjectEdit from '~/project_edit'; import initConfirmDangerModal from '~/confirm_danger_modal'; import mountBadgeSettings from '~/pages/shared/mount_badge_settings'; -import fileUpload from '~/lib/utils/file_upload'; +import initAvatarPicker from '~/avatar_picker'; import initProjectLoadingSpinner from '../shared/save_project_loader'; import initProjectPermissionsSettings from '../shared/permissions'; @@ -12,7 +12,7 @@ document.addEventListener('DOMContentLoaded', () => { setupProjectEdit(); // Initialize expandable settings panels initSettingsPanels(); - fileUpload('.js-choose-project-avatar-button', '.js-project-avatar-input'); + initAvatarPicker(); initProjectPermissionsSettings(); initConfirmDangerModal(); mountBadgeSettings(PROJECT_BADGE); diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml index 5e05568e384..8fb38f6a690 100644 --- a/app/views/admin/groups/_form.html.haml +++ b/app/views/admin/groups/_form.html.haml @@ -8,7 +8,7 @@ .form-group.row.group-description-holder = f.label :avatar, _("Group avatar"), class: 'col-form-label col-sm-2' .col-sm-10 - = render 'shared/choose_group_avatar_button', f: f + = render 'shared/choose_avatar_button', f: f = render 'shared/old_visibility_level', f: f, visibility_level: visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group, with_label: false diff --git a/app/views/groups/new.html.haml b/app/views/groups/new.html.haml index 51dcc9d0cda..6269678079a 100644 --- a/app/views/groups/new.html.haml +++ b/app/views/groups/new.html.haml @@ -27,7 +27,7 @@ .form-group.group-description-holder.col-sm-12 = f.label :avatar, _("Group avatar"), class: 'label-bold' %div - = render 'shared/choose_group_avatar_button', f: f + = render 'shared/choose_avatar_button', f: f .form-group.col-sm-12 %label.label-bold diff --git a/app/views/groups/settings/_general.html.haml b/app/views/groups/settings/_general.html.haml index 9ed71d19d32..c382a1ed168 100644 --- a/app/views/groups/settings/_general.html.haml +++ b/app/views/groups/settings/_general.html.haml @@ -23,10 +23,10 @@ .avatar-container.rect-avatar.s90 = group_icon(@group, alt: '', class: 'avatar group-avatar s90') = f.label :avatar, _('Group avatar'), class: 'label-bold d-block' - = render 'shared/choose_group_avatar_button', f: f + = render 'shared/choose_avatar_button', f: f - if @group.avatar? %hr - = link_to _('Remove avatar'), group_avatar_path(@group.to_param), data: { confirm: _('Avatar will be removed. Are you sure?')}, method: :delete, class: 'btn btn-danger btn-inverted' + = link_to _('Remove avatar'), group_avatar_path(@group.to_param), data: { confirm: _('Avatar will be removed. Are you sure?')}, method: :delete, class: 'btn btn-link' = render 'shared/visibility_level', f: f, visibility_level: @group.visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index a6a8ca489a9..98017bea0c9 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -40,23 +40,16 @@ = f.label :tag_list, "Topics", class: 'label-bold' = f.text_field :tag_list, value: @project.tag_list.join(', '), maxlength: 2000, class: "form-control" %p.form-text.text-muted Separate topics with commas. - %fieldset.features - %h5.prepend-top-0= _("Project avatar") - .form-group - - if @project.avatar? - .avatar-container.rect-avatar.s160.append-bottom-15 - = project_icon(@project, alt: '', class: 'avatar project-avatar s160', width: 160, height: 160) - - if @project.avatar_in_git - %p.light - = _("Project avatar in repository: %{link}").html_safe % { link: @project.avatar_in_git } - .prepend-top-5.append-bottom-10 - %button.btn.js-choose-project-avatar-button{ type: 'button' }= _("Choose file...") - %span.file_name.prepend-left-default.js-filename= _("No file chosen") - = f.file_field :avatar, class: "js-project-avatar-input hidden" - .form-text.text-muted= _("The maximum file size allowed is 200KB.") - - if @project.avatar? - %hr - = link_to _('Remove avatar'), project_avatar_path(@project), data: { confirm: _("Avatar will be removed. Are you sure?") }, method: :delete, class: "btn btn-danger btn-inverted" + + .form-group.prepend-top-default.append-bottom-20 + .avatar-container.s90 + = project_icon(@project, alt: _('Project avatar'), class: 'avatar project-avatar s90') + = f.label :avatar, _('Project avatar'), class: 'label-bold d-block' + = render 'shared/choose_avatar_button', f: f + - if @project.avatar? + %hr + = link_to _('Remove avatar'), project_avatar_path(@project), data: { confirm: _('Avatar will be removed. Are you sure?')}, method: :delete, class: 'btn btn-link' + = f.submit 'Save changes', class: "btn btn-success js-btn-success-general-project-settings" %section.settings.sharing-permissions.no-animate#js-shared-permissions{ class: ('expanded' if expanded) } diff --git a/app/views/shared/_choose_avatar_button.html.haml b/app/views/shared/_choose_avatar_button.html.haml new file mode 100644 index 00000000000..0d46d047134 --- /dev/null +++ b/app/views/shared/_choose_avatar_button.html.haml @@ -0,0 +1,4 @@ +%button.btn.js-choose-avatar-button{ type: 'button' }= _("Choose file…") +%span.file_name.js-avatar-filename= _("No file chosen") += f.file_field :avatar, class: "js-avatar-input hidden" +.form-text.text-muted= _("The maximum file size allowed is 200KB.") diff --git a/app/views/shared/_choose_group_avatar_button.html.haml b/app/views/shared/_choose_group_avatar_button.html.haml deleted file mode 100644 index 0552fe62090..00000000000 --- a/app/views/shared/_choose_group_avatar_button.html.haml +++ /dev/null @@ -1,4 +0,0 @@ -%button.btn.js-choose-group-avatar-button{ type: 'button' }= _("Choose File ...") -%span.file_name.js-avatar-filename= _("No file chosen") -= f.file_field :avatar, class: "js-group-avatar-input hidden" -.form-text.text-muted= _("The maximum file size allowed is 200KB.") diff --git a/changelogs/unreleased/ce-proj-settings-ok-avatar-only.yml b/changelogs/unreleased/ce-proj-settings-ok-avatar-only.yml new file mode 100644 index 00000000000..10475824a75 --- /dev/null +++ b/changelogs/unreleased/ce-proj-settings-ok-avatar-only.yml @@ -0,0 +1,5 @@ +--- +title: Change project avatar remove button to a link +merge_request: 26589 +author: +type: other diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 177bd189817..a54eb108e83 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -1482,9 +1482,6 @@ msgstr "" msgid "Choose Next at the bottom of the page." msgstr "" -msgid "Choose File ..." -msgstr "" - msgid "Choose a branch/tag (e.g. %{master}) or enter a commit (e.g. %{sha}) to see what's changed or to create a merge request." msgstr "" @@ -1506,7 +1503,7 @@ msgstr "" msgid "Choose between clone or fetch to get the recent application code" msgstr "" -msgid "Choose file..." +msgid "Choose file…" msgstr "" msgid "Choose the top-level group for your repository imports." @@ -6241,9 +6238,6 @@ msgstr "" msgid "Project avatar" msgstr "" -msgid "Project avatar in repository: %{link}" -msgstr "" - msgid "Project details" msgstr "" -- GitLab From 7d746b5a6cf16ff266a2e82a48e29d47b3b08498 Mon Sep 17 00:00:00 2001 From: Luke Bennett Date: Thu, 29 Nov 2018 01:06:10 +0000 Subject: [PATCH 004/154] Port "Simplify admin instance licenses page" Ports CE changes to include a license app scss variable and a jest import alias. --- app/assets/stylesheets/framework/variables.scss | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 5d4c84c494d..8bdc71cb427 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -719,3 +719,8 @@ $compare-branches-sticky-header-height: 68px; - Issue: https://gitlab.com/gitlab-org/design.gitlab.com/issues/242 */ $enable-validation-icons: false; + +/* +Licenses +*/ +$license-header-cell-width: 150px; -- GitLab From de03a8bcb45f11a50bee071e977274c113e7b39a Mon Sep 17 00:00:00 2001 From: Tiger Date: Tue, 26 Mar 2019 17:09:07 +1100 Subject: [PATCH 005/154] Detailed status for builds that fail prerequisites Create FailedUnmetPrerequisites status to allow custom messaging for builds that were unable to be queued due to failing to meet prerequisites (eg. failing to create Kubernetes namespace for deployment). --- lib/gitlab/ci/status/build/factory.rb | 3 +- .../build/failed_unmet_prerequisites.rb | 24 ++++++++++++ spec/factories/ci/builds.rb | 5 +++ .../gitlab/ci/status/build/factory_spec.rb | 29 +++++++++++++++ .../build/failed_unmet_prerequisites_spec.rb | 37 +++++++++++++++++++ 5 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 lib/gitlab/ci/status/build/failed_unmet_prerequisites.rb create mode 100644 spec/lib/gitlab/ci/status/build/failed_unmet_prerequisites_spec.rb diff --git a/lib/gitlab/ci/status/build/factory.rb b/lib/gitlab/ci/status/build/factory.rb index f7d0715e617..96d05842838 100644 --- a/lib/gitlab/ci/status/build/factory.rb +++ b/lib/gitlab/ci/status/build/factory.rb @@ -16,7 +16,8 @@ module Gitlab Status::Build::Skipped], [Status::Build::Cancelable, Status::Build::Retryable], - [Status::Build::Failed], + [Status::Build::FailedUnmetPrerequisites, + Status::Build::Failed], [Status::Build::FailedAllowed, Status::Build::Unschedule, Status::Build::Play, diff --git a/lib/gitlab/ci/status/build/failed_unmet_prerequisites.rb b/lib/gitlab/ci/status/build/failed_unmet_prerequisites.rb new file mode 100644 index 00000000000..a64f901558d --- /dev/null +++ b/lib/gitlab/ci/status/build/failed_unmet_prerequisites.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Status + module Build + class FailedUnmetPrerequisites < Status::Extended + def illustration + { + image: 'illustrations/skipped-job_empty.svg', + size: 'svg-430', + title: _('Failed to create resources'), + content: _('Retry this job in order to create the necessary resources') + } + end + + def self.matches?(build, _) + build.unmet_prerequisites? + end + end + end + end + end +end diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index 067391c1179..f8c494c159e 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -336,6 +336,11 @@ FactoryBot.define do failure_reason 2 end + trait :prerequisite_failure do + failed + failure_reason 10 + end + trait :with_runner_session do after(:build) do |build| build.build_runner_session(url: 'https://localhost') diff --git a/spec/lib/gitlab/ci/status/build/factory_spec.rb b/spec/lib/gitlab/ci/status/build/factory_spec.rb index b379b08ad62..b6231510b91 100644 --- a/spec/lib/gitlab/ci/status/build/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/build/factory_spec.rb @@ -123,6 +123,35 @@ describe Gitlab::Ci::Status::Build::Factory do expect(status.action_path).to include 'retry' end end + + context 'when build has unmet prerequisites' do + let(:build) { create(:ci_build, :prerequisite_failure) } + + it 'matches correct core status' do + expect(factory.core_status).to be_a Gitlab::Ci::Status::Failed + end + + it 'matches correct extended statuses' do + expect(factory.extended_statuses) + .to eq [Gitlab::Ci::Status::Build::Retryable, + Gitlab::Ci::Status::Build::FailedUnmetPrerequisites] + end + + it 'fabricates a failed with unmet prerequisites build status' do + expect(status).to be_a Gitlab::Ci::Status::Build::FailedUnmetPrerequisites + end + + it 'fabricates status with correct details' do + expect(status.text).to eq 'failed' + expect(status.icon).to eq 'status_failed' + expect(status.favicon).to eq 'favicon_status_failed' + expect(status.label).to eq 'failed' + expect(status).to have_details + expect(status).to have_action + expect(status.action_title).to include 'Retry' + expect(status.action_path).to include 'retry' + end + end end context 'when build is a canceled' do diff --git a/spec/lib/gitlab/ci/status/build/failed_unmet_prerequisites_spec.rb b/spec/lib/gitlab/ci/status/build/failed_unmet_prerequisites_spec.rb new file mode 100644 index 00000000000..a4854bdc6b9 --- /dev/null +++ b/spec/lib/gitlab/ci/status/build/failed_unmet_prerequisites_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Status::Build::FailedUnmetPrerequisites do + describe '#illustration' do + subject { described_class.new(double).illustration } + + it { is_expected.to include(:image, :size, :title, :content) } + end + + describe '.matches?' do + let(:build) { create(:ci_build, :created) } + + subject { described_class.matches?(build, double) } + + context 'when build has not failed' do + it { is_expected.to be_falsey } + end + + context 'when build has failed' do + before do + build.drop!(failure_reason) + end + + context 'with unmet prerequisites' do + let(:failure_reason) { :unmet_prerequisites } + + it { is_expected.to be_truthy } + end + + context 'with a different error' do + let(:failure_reason) { :runner_system_failure } + + it { is_expected.to be_falsey } + end + end + end +end -- GitLab From 02b9b5facf0496f7c64a7913cc4f4c8437527f72 Mon Sep 17 00:00:00 2001 From: Tiger Date: Wed, 27 Mar 2019 15:01:35 +1100 Subject: [PATCH 006/154] Expose build failure reason We can use this to show more informative error messages with links to documentation etc. --- app/serializers/build_details_entity.rb | 2 ++ spec/serializers/build_details_entity_spec.rb | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/app/serializers/build_details_entity.rb b/app/serializers/build_details_entity.rb index 9ddce0d2c80..62c26809eeb 100644 --- a/app/serializers/build_details_entity.rb +++ b/app/serializers/build_details_entity.rb @@ -45,6 +45,8 @@ class BuildDetailsEntity < JobEntity erase_project_job_path(project, build) end + expose :failure_reason, if: -> (*) { build.failed? } + expose :terminal_path, if: -> (*) { can_create_build_terminal? } do |build| terminal_project_job_path(project, build) end diff --git a/spec/serializers/build_details_entity_spec.rb b/spec/serializers/build_details_entity_spec.rb index f6bd6e9ede4..1edf69dc290 100644 --- a/spec/serializers/build_details_entity_spec.rb +++ b/spec/serializers/build_details_entity_spec.rb @@ -112,5 +112,15 @@ describe BuildDetailsEntity do expect(subject['merge_request_path']).to be_nil end end + + context 'when the build has failed' do + let(:build) { create(:ci_build, :created) } + + before do + build.drop!(:unmet_prerequisites) + end + + it { is_expected.to include(failure_reason: 'unmet_prerequisites') } + end end end -- GitLab From 11f2ddf021ef86d481f68ee6a1e09b57fd18dcae Mon Sep 17 00:00:00 2001 From: jerasmus Date: Thu, 21 Mar 2019 08:09:15 +0200 Subject: [PATCH 007/154] Display error for unmet prerequisites Added the ability to display an error for unmet prerequisites --- .../jobs/components/empty_state.vue | 2 +- .../javascripts/jobs/components/job_app.vue | 14 +++++++ .../components/unmet_prerequisites_block.vue | 30 +++++++++++++++ app/assets/javascripts/jobs/index.js | 1 + app/assets/javascripts/jobs/store/getters.js | 7 +++- app/assets/stylesheets/framework/callout.scss | 4 ++ app/views/projects/jobs/show.html.haml | 1 + ...-show-error-when-namespace-svc-missing.yml | 5 +++ .../build/failed_unmet_prerequisites.rb | 4 +- locale/gitlab.pot | 9 +++++ .../jobs/components/job_app_spec.js | 36 ++++++++++++++++++ .../unmet_prerequisites_block_spec.js | 37 +++++++++++++++++++ 12 files changed, 146 insertions(+), 4 deletions(-) create mode 100644 app/assets/javascripts/jobs/components/unmet_prerequisites_block.vue create mode 100644 changelogs/unreleased/54506-show-error-when-namespace-svc-missing.yml create mode 100644 spec/javascripts/jobs/components/unmet_prerequisites_block_spec.js diff --git a/app/assets/javascripts/jobs/components/empty_state.vue b/app/assets/javascripts/jobs/components/empty_state.vue index 668fcf3d673..04f910b6b80 100644 --- a/app/assets/javascripts/jobs/components/empty_state.vue +++ b/app/assets/javascripts/jobs/components/empty_state.vue @@ -49,7 +49,7 @@ export default {

{{ title }}

-

{{ content }}

+

{{ content }}

import('ee_component/jobs/components/shared_runner_limit_block.vue'), @@ -48,6 +50,11 @@ export default { required: false, default: null, }, + deploymentHelpUrl: { + type: String, + required: false, + default: null, + }, endpoint: { type: String, required: true, @@ -82,6 +89,7 @@ export default { ]), ...mapGetters([ 'headerTime', + 'hasUnmetPrerequisitesFailure', 'shouldRenderCalloutMessage', 'shouldRenderTriggeredLabel', 'hasEnvironment', @@ -223,6 +231,12 @@ export default { :runners-path="runnerSettingsUrl" /> + + +import { GlLink } from '@gitlab/ui'; +/** + * Renders Unmet Prerequisites block for job's view. + */ +export default { + components: { + GlLink, + }, + props: { + helpPath: { + type: String, + required: true, + }, + }, +}; + + diff --git a/app/assets/javascripts/jobs/index.js b/app/assets/javascripts/jobs/index.js index a32e945627c..25132449458 100644 --- a/app/assets/javascripts/jobs/index.js +++ b/app/assets/javascripts/jobs/index.js @@ -12,6 +12,7 @@ export default () => { render(createElement) { return createElement('job-app', { props: { + deploymentHelpUrl: element.dataset.deploymentHelpUrl, runnerHelpUrl: element.dataset.runnerHelpUrl, runnerSettingsUrl: element.dataset.runnerSettingsUrl, endpoint: element.dataset.endpoint, diff --git a/app/assets/javascripts/jobs/store/getters.js b/app/assets/javascripts/jobs/store/getters.js index 73c1cbc3a99..8c09fb0cfe1 100644 --- a/app/assets/javascripts/jobs/store/getters.js +++ b/app/assets/javascripts/jobs/store/getters.js @@ -3,8 +3,13 @@ import { isScrolledToBottom } from '~/lib/utils/scroll_utils'; export const headerTime = state => (state.job.started ? state.job.started : state.job.created_at); +export const hasUnmetPrerequisitesFailure = state => + state.job && state.job.failure_reason && state.job.failure_reason === 'unmet_prerequisites'; + export const shouldRenderCalloutMessage = state => - !_.isEmpty(state.job.status) && !_.isEmpty(state.job.callout_message); + !_.isEmpty(state.job.status) && + !_.isEmpty(state.job.callout_message) && + !hasUnmetPrerequisitesFailure; /** * When job has not started the key will be null diff --git a/app/assets/stylesheets/framework/callout.scss b/app/assets/stylesheets/framework/callout.scss index 0d8e4afa76f..643b20c56bc 100644 --- a/app/assets/stylesheets/framework/callout.scss +++ b/app/assets/stylesheets/framework/callout.scss @@ -28,6 +28,10 @@ background-color: $red-100; border-color: $red-200; color: $red-700; + + a { + color: $red-700; + } } .bs-callout-warning { diff --git a/app/views/projects/jobs/show.html.haml b/app/views/projects/jobs/show.html.haml index 475bae887ec..81a53f22f67 100644 --- a/app/views/projects/jobs/show.html.haml +++ b/app/views/projects/jobs/show.html.haml @@ -8,6 +8,7 @@ %div{ class: container_class } #js-job-vue-app{ data: { endpoint: project_job_path(@project, @build, format: :json), + deployment_help_url: help_page_path('user/project/clusters/index.html', anchor: 'troubleshooting-failed-deployment-jobs'), runner_help_url: help_page_path('ci/runners/README.html', anchor: 'setting-maximum-job-timeout-for-a-runner'), runner_settings_url: project_runners_path(@build.project, anchor: 'js-runners-settings'), build_options: javascript_build_options } } diff --git a/changelogs/unreleased/54506-show-error-when-namespace-svc-missing.yml b/changelogs/unreleased/54506-show-error-when-namespace-svc-missing.yml new file mode 100644 index 00000000000..3e3784d5413 --- /dev/null +++ b/changelogs/unreleased/54506-show-error-when-namespace-svc-missing.yml @@ -0,0 +1,5 @@ +--- +title: Show error when namespace/svc account missing +merge_request: 26362 +author: +type: added diff --git a/lib/gitlab/ci/status/build/failed_unmet_prerequisites.rb b/lib/gitlab/ci/status/build/failed_unmet_prerequisites.rb index a64f901558d..eaad3969a4c 100644 --- a/lib/gitlab/ci/status/build/failed_unmet_prerequisites.rb +++ b/lib/gitlab/ci/status/build/failed_unmet_prerequisites.rb @@ -7,10 +7,10 @@ module Gitlab class FailedUnmetPrerequisites < Status::Extended def illustration { - image: 'illustrations/skipped-job_empty.svg', + image: 'illustrations/pipelines_failed.svg', size: 'svg-430', title: _('Failed to create resources'), - content: _('Retry this job in order to create the necessary resources') + content: _('Retry this job in order to create the necessary resources.') } end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 1e6f04c7815..963fcc56fc6 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -3568,6 +3568,9 @@ msgstr "" msgid "Failed to check related branches." msgstr "" +msgid "Failed to create resources" +msgstr "" + msgid "Failed to deploy to" msgstr "" @@ -4589,6 +4592,9 @@ msgstr "" msgid "Job|The artifacts will be removed" msgstr "" +msgid "Job|This job failed because the necessary resources were not successfully created." +msgstr "" + msgid "Job|This job is stuck because the project doesn't have any runners online assigned to it." msgstr "" @@ -6825,6 +6831,9 @@ msgstr "" msgid "Retry this job" msgstr "" +msgid "Retry this job in order to create the necessary resources." +msgstr "" + msgid "Retry verification" msgstr "" diff --git a/spec/javascripts/jobs/components/job_app_spec.js b/spec/javascripts/jobs/components/job_app_spec.js index ba5d672f189..6117eb8cff5 100644 --- a/spec/javascripts/jobs/components/job_app_spec.js +++ b/spec/javascripts/jobs/components/job_app_spec.js @@ -17,6 +17,7 @@ describe('Job App ', () => { const props = { endpoint: `${gl.TEST_HOST}jobs/123.json`, runnerHelpUrl: 'help/runner', + deploymentHelpUrl: 'help/deployment', runnerSettingsUrl: 'settings/ci-cd/runners', terminalPath: 'jobs/123/terminal', pagePath: `${gl.TEST_HOST}jobs/123`, @@ -253,6 +254,41 @@ describe('Job App ', () => { }); }); + describe('unmet prerequisites block', () => { + it('renders unmet prerequisites block when there is an unmet prerequisites failure', done => { + mock.onGet(props.endpoint).replyOnce( + 200, + Object.assign({}, job, { + status: { + group: 'failed', + icon: 'status_failed', + label: 'failed', + text: 'failed', + details_path: 'path', + illustration: { + content: 'Retry this job in order to create the necessary resources.', + image: 'path', + size: 'svg-430', + title: 'Failed to create resources', + }, + }, + has_trace: false, + unmet_prerequisites: true, + runners: { + available: true, + }, + tags: [], + }), + ); + vm = mountComponentWithStore(Component, { props, store }); + + setTimeout(() => { + expect(vm.$el.querySelector('.js-job-failed')).not.toBeNull(); + done(); + }, 0); + }); + }); + describe('environments block', () => { it('renders environment block when job has environment', done => { mock.onGet(props.endpoint).replyOnce( diff --git a/spec/javascripts/jobs/components/unmet_prerequisites_block_spec.js b/spec/javascripts/jobs/components/unmet_prerequisites_block_spec.js new file mode 100644 index 00000000000..68fcb321214 --- /dev/null +++ b/spec/javascripts/jobs/components/unmet_prerequisites_block_spec.js @@ -0,0 +1,37 @@ +import Vue from 'vue'; +import component from '~/jobs/components/unmet_prerequisites_block.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; + +describe('Unmet Prerequisites Block Job component', () => { + const Component = Vue.extend(component); + let vm; + const helpPath = '/user/project/clusters/index.html#troubleshooting-failed-deployment-jobs'; + + beforeEach(() => { + vm = mountComponent(Component, { + hasNoRunnersForProject: true, + helpPath, + }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders an alert with the correct message', () => { + const container = vm.$el.querySelector('.js-failed-unmet-prerequisites'); + const alertMessage = + 'This job failed because the necessary resources were not successfully created.'; + + expect(container).not.toBeNull(); + expect(container.innerHTML).toContain(alertMessage); + }); + + it('renders link to help page', () => { + const helpLink = vm.$el.querySelector('.js-help-path'); + + expect(helpLink).not.toBeNull(); + expect(helpLink.innerHTML).toContain('More information'); + expect(helpLink.getAttribute('href')).toEqual(helpPath); + }); +}); -- GitLab From 506a0ee646d6e600a83cc589abc5543166347b06 Mon Sep 17 00:00:00 2001 From: jerasmus Date: Mon, 1 Apr 2019 12:22:10 +0200 Subject: [PATCH 008/154] Fix frontend unit tests Fixed the broken frontend unit tests --- app/assets/javascripts/jobs/components/job_app.vue | 2 +- app/assets/javascripts/jobs/store/getters.js | 4 +--- spec/javascripts/jobs/components/job_app_spec.js | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue index 689d1cb97c7..06750a34c77 100644 --- a/app/assets/javascripts/jobs/components/job_app.vue +++ b/app/assets/javascripts/jobs/components/job_app.vue @@ -218,7 +218,7 @@ export default { />
- + diff --git a/app/assets/javascripts/jobs/store/getters.js b/app/assets/javascripts/jobs/store/getters.js index 8c09fb0cfe1..406b1a2e375 100644 --- a/app/assets/javascripts/jobs/store/getters.js +++ b/app/assets/javascripts/jobs/store/getters.js @@ -7,9 +7,7 @@ export const hasUnmetPrerequisitesFailure = state => state.job && state.job.failure_reason && state.job.failure_reason === 'unmet_prerequisites'; export const shouldRenderCalloutMessage = state => - !_.isEmpty(state.job.status) && - !_.isEmpty(state.job.callout_message) && - !hasUnmetPrerequisitesFailure; + !_.isEmpty(state.job.status) && !_.isEmpty(state.job.callout_message); /** * When job has not started the key will be null diff --git a/spec/javascripts/jobs/components/job_app_spec.js b/spec/javascripts/jobs/components/job_app_spec.js index 6117eb8cff5..cef40117304 100644 --- a/spec/javascripts/jobs/components/job_app_spec.js +++ b/spec/javascripts/jobs/components/job_app_spec.js @@ -272,8 +272,8 @@ describe('Job App ', () => { title: 'Failed to create resources', }, }, + failure_reason: 'unmet_prerequisites', has_trace: false, - unmet_prerequisites: true, runners: { available: true, }, -- GitLab From ba1137c5141ffda6ce8eb9542f31ec1f2b9078c1 Mon Sep 17 00:00:00 2001 From: jerasmus Date: Mon, 1 Apr 2019 12:26:52 +0200 Subject: [PATCH 009/154] Format changed frontend files Ran yarn prettier changed frontend files --- app/assets/javascripts/jobs/components/job_app.vue | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue index 06750a34c77..0670e2b06b9 100644 --- a/app/assets/javascripts/jobs/components/job_app.vue +++ b/app/assets/javascripts/jobs/components/job_app.vue @@ -218,7 +218,10 @@ export default { />
- + -- GitLab From 06b88af04657be961a4da97a586706fb99eb6a27 Mon Sep 17 00:00:00 2001 From: Nathan Friend Date: Tue, 12 Mar 2019 13:35:55 -0300 Subject: [PATCH 010/154] Add reusable project_selector component This commit adds a resuable UI component that allows a user to search for a project name, shows the search results, and allows the user to select one or more projects. This component communicates with its parent using props and events. This component was originally created for use in the EE-specific "Operations Dashboard" page, but it is applicable for CE use cases as well, and so was added as a CE shared component. In addition, some logic was extracted from the frequent_items_list_item component into shared filters to avoid logic duplication. --- .../components/frequent_items_list_item.vue | 57 +++---- app/assets/javascripts/lib/utils/highlight.js | 44 +++++ .../javascripts/lib/utils/text_utility.js | 32 ++++ .../project_selector/project_list_item.vue | 74 +++++++++ .../project_selector/project_selector.vue | 103 ++++++++++++ .../components/project_list_item.scss | 27 ++++ locale/gitlab.pot | 12 ++ spec/frontend/lib/utils/text_utility_spec.js | 27 ++++ .../frequent_items_list_item_spec.js | 49 ++++-- spec/javascripts/lib/utils/higlight_spec.js | 43 +++++ .../project_list_item_spec.js | 104 ++++++++++++ .../project_selector/project_selector_spec.js | 152 ++++++++++++++++++ 12 files changed, 676 insertions(+), 48 deletions(-) create mode 100644 app/assets/javascripts/lib/utils/highlight.js create mode 100644 app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue create mode 100644 app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue create mode 100644 app/assets/stylesheets/components/project_list_item.scss create mode 100644 spec/javascripts/lib/utils/higlight_spec.js create mode 100644 spec/javascripts/vue_shared/components/project_selector/project_list_item_spec.js create mode 100644 spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js diff --git a/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue b/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue index 42d14b65b3a..92c3bcb5012 100644 --- a/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue +++ b/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue @@ -1,6 +1,9 @@ + diff --git a/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue b/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue new file mode 100644 index 00000000000..bb17a9b331e --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue @@ -0,0 +1,103 @@ + + diff --git a/app/assets/stylesheets/components/project_list_item.scss b/app/assets/stylesheets/components/project_list_item.scss new file mode 100644 index 00000000000..6f9933e3d70 --- /dev/null +++ b/app/assets/stylesheets/components/project_list_item.scss @@ -0,0 +1,27 @@ +.project-list-item { + &:not(:disabled):not(.disabled) { + &:focus, + &:active, + &:focus:active { + outline: none; + box-shadow: none; + } + } +} + +// When housed inside a modal, the edge of each item +// should extend to the edge of the modal. +.modal-body { + .project-list-item { + border-radius: 0; + margin-left: -$gl-padding; + margin-right: -$gl-padding; + + // should be replaced by Bootstrap's + // .overflow-hidden utility class once + // we upgrade Bootstrap to at least 4.2.x + .project-namespace-name-container { + overflow: hidden; + } + } +} diff --git a/locale/gitlab.pot b/locale/gitlab.pot index fcbd34a05d5..5626e196d37 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -3166,6 +3166,9 @@ msgstr "" msgid "Ends at (UTC)" msgstr "" +msgid "Enter at least three characters to search" +msgstr "" + msgid "Enter in your Bitbucket Server URL and personal access token below" msgstr "" @@ -7013,6 +7016,9 @@ msgstr "" msgid "Search users" msgstr "" +msgid "Search your projects" +msgstr "" + msgid "SearchAutocomplete|All GitLab" msgstr "" @@ -7405,9 +7411,15 @@ msgstr "" msgid "Something went wrong while resolving this discussion. Please try again." msgstr "" +msgid "Something went wrong, unable to search projects" +msgstr "" + msgid "Something went wrong. Please try again." msgstr "" +msgid "Sorry, no projects matched your search" +msgstr "" + msgid "Sorry, your filter produced no results" msgstr "" diff --git a/spec/frontend/lib/utils/text_utility_spec.js b/spec/frontend/lib/utils/text_utility_spec.js index 0a266b19ea5..3f331055a32 100644 --- a/spec/frontend/lib/utils/text_utility_spec.js +++ b/spec/frontend/lib/utils/text_utility_spec.js @@ -151,4 +151,31 @@ describe('text_utility', () => { ); }); }); + + describe('truncateNamespace', () => { + it(`should return the root namespace if the namespace only includes one level`, () => { + expect(textUtils.truncateNamespace('a / b')).toBe('a'); + }); + + it(`should return the first 2 namespaces if the namespace inlcudes exactly 2 levels`, () => { + expect(textUtils.truncateNamespace('a / b / c')).toBe('a / b'); + }); + + it(`should return the first and last namespaces, separated by "...", if the namespace inlcudes more than 2 levels`, () => { + expect(textUtils.truncateNamespace('a / b / c / d')).toBe('a / ... / c'); + expect(textUtils.truncateNamespace('a / b / c / d / e / f / g / h / i')).toBe('a / ... / h'); + }); + + it(`should return an empty string for invalid inputs`, () => { + [undefined, null, 4, {}, true, new Date()].forEach(input => { + expect(textUtils.truncateNamespace(input)).toBe(''); + }); + }); + + it(`should not alter strings that aren't formatted as namespaces`, () => { + ['', ' ', '\t', 'a', 'a \\ b'].forEach(input => { + expect(textUtils.truncateNamespace(input)).toBe(input); + }); + }); + }); }); diff --git a/spec/javascripts/frequent_items/components/frequent_items_list_item_spec.js b/spec/javascripts/frequent_items/components/frequent_items_list_item_spec.js index 7deed985219..92554bd9a69 100644 --- a/spec/javascripts/frequent_items/components/frequent_items_list_item_spec.js +++ b/spec/javascripts/frequent_items/components/frequent_items_list_item_spec.js @@ -1,6 +1,7 @@ import Vue from 'vue'; import frequentItemsListItemComponent from '~/frequent_items/components/frequent_items_list_item.vue'; import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import { trimText } from 'spec/helpers/vue_component_helper'; import { mockProject } from '../mock_data'; // can also use 'mockGroup', but not useful to test here const createComponent = () => { @@ -40,30 +41,58 @@ describe('FrequentItemsListItemComponent', () => { }); describe('highlightedItemName', () => { - it('should enclose part of project name in & which matches with `matcher` prop', () => { + it('should enclose part of project name in & which matches with `matcher` prop', done => { vm.matcher = 'lab'; - expect(vm.highlightedItemName).toContain('Lab'); + vm.$nextTick() + .then(() => { + expect(vm.$el.querySelector('.js-frequent-items-item-title').innerHTML).toContain( + 'Lab', + ); + }) + .then(done) + .catch(done.fail); }); - it('should return project name as it is if `matcher` is not available', () => { + it('should return project name as it is if `matcher` is not available', done => { vm.matcher = null; - expect(vm.highlightedItemName).toBe(mockProject.name); + vm.$nextTick() + .then(() => { + expect(vm.$el.querySelector('.js-frequent-items-item-title').innerHTML).toBe( + mockProject.name, + ); + }) + .then(done) + .catch(done.fail); }); }); describe('truncatedNamespace', () => { - it('should truncate project name from namespace string', () => { + it('should truncate project name from namespace string', done => { vm.namespace = 'platform / nokia-3310'; - expect(vm.truncatedNamespace).toBe('platform'); + vm.$nextTick() + .then(() => { + expect( + trimText(vm.$el.querySelector('.js-frequent-items-item-namespace').innerHTML), + ).toBe('platform'); + }) + .then(done) + .catch(done.fail); }); - it('should truncate namespace string from the middle if it includes more than two groups in path', () => { + it('should truncate namespace string from the middle if it includes more than two groups in path', done => { vm.namespace = 'platform / hardware / broadcom / Wifi Group / Mobile Chipset / nokia-3310'; - expect(vm.truncatedNamespace).toBe('platform / ... / Mobile Chipset'); + vm.$nextTick() + .then(() => { + expect( + trimText(vm.$el.querySelector('.js-frequent-items-item-namespace').innerHTML), + ).toBe('platform / ... / Mobile Chipset'); + }) + .then(done) + .catch(done.fail); }); }); }); @@ -74,8 +103,8 @@ describe('FrequentItemsListItemComponent', () => { expect(vm.$el.querySelectorAll('a').length).toBe(1); expect(vm.$el.querySelectorAll('.frequent-items-item-avatar-container').length).toBe(1); expect(vm.$el.querySelectorAll('.frequent-items-item-metadata-container').length).toBe(1); - expect(vm.$el.querySelectorAll('.frequent-items-item-title').length).toBe(1); - expect(vm.$el.querySelectorAll('.frequent-items-item-namespace').length).toBe(1); + expect(vm.$el.querySelectorAll('.js-frequent-items-item-title').length).toBe(1); + expect(vm.$el.querySelectorAll('.js-frequent-items-item-namespace').length).toBe(1); }); }); }); diff --git a/spec/javascripts/lib/utils/higlight_spec.js b/spec/javascripts/lib/utils/higlight_spec.js new file mode 100644 index 00000000000..638bbf65ae9 --- /dev/null +++ b/spec/javascripts/lib/utils/higlight_spec.js @@ -0,0 +1,43 @@ +import highlight from '~/lib/utils/highlight'; + +describe('highlight', () => { + it(`should appropriately surround substring matches`, () => { + const expected = 'gitlab'; + + expect(highlight('gitlab', 'it')).toBe(expected); + }); + + it(`should return an empty string in the case of invalid inputs`, () => { + [null, undefined].forEach(input => { + expect(highlight(input, 'match')).toBe(''); + }); + }); + + it(`should return the original value if match is null, undefined, or ''`, () => { + [null, undefined].forEach(match => { + expect(highlight('gitlab', match)).toBe('gitlab'); + }); + }); + + it(`should highlight matches in non-string inputs`, () => { + const expected = '123456'; + + expect(highlight(123456, 45)).toBe(expected); + }); + + it(`should sanitize the input string before highlighting matches`, () => { + const expected = 'hello world'; + + expect(highlight('hello world', 'w')).toBe(expected); + }); + + it(`should not highlight anything if no matches are found`, () => { + expect(highlight('gitlab', 'hello')).toBe('gitlab'); + }); + + it(`should allow wrapping elements to be customized`, () => { + const expected = '123'; + + expect(highlight('123', '2', '', '')).toBe(expected); + }); +}); diff --git a/spec/javascripts/vue_shared/components/project_selector/project_list_item_spec.js b/spec/javascripts/vue_shared/components/project_selector/project_list_item_spec.js new file mode 100644 index 00000000000..8dbdfe97f8f --- /dev/null +++ b/spec/javascripts/vue_shared/components/project_selector/project_list_item_spec.js @@ -0,0 +1,104 @@ +import _ from 'underscore'; +import ProjectListItem from '~/vue_shared/components/project_selector/project_list_item.vue'; +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import { trimText } from 'spec/helpers/vue_component_helper'; + +const localVue = createLocalVue(); + +describe('ProjectListItem component', () => { + let wrapper; + let vm; + loadJSONFixtures('projects.json'); + const project = getJSONFixture('projects.json')[0]; + + beforeEach(() => { + wrapper = shallowMount(localVue.extend(ProjectListItem), { + propsData: { + project, + selected: false, + }, + sync: false, + localVue, + }); + + ({ vm } = wrapper); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('does not render a check mark icon if selected === false', () => { + expect(vm.$el.querySelector('.js-selected-icon.js-unselected')).toBeTruthy(); + }); + + it('renders a check mark icon if selected === true', done => { + wrapper.setProps({ selected: true }); + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.js-selected-icon.js-selected')).toBeTruthy(); + done(); + }); + }); + + it(`emits a "clicked" event when clicked`, () => { + spyOn(vm, '$emit'); + vm.onClick(); + + expect(vm.$emit).toHaveBeenCalledWith('click'); + }); + + it(`renders the project avatar`, () => { + expect(vm.$el.querySelector('.js-project-avatar')).toBeTruthy(); + }); + + it(`renders a simple namespace name with a trailing slash`, done => { + project.name_with_namespace = 'a / b'; + wrapper.setProps({ project: _.clone(project) }); + + vm.$nextTick(() => { + const renderedNamespace = trimText(vm.$el.querySelector('.js-project-namespace').textContent); + + expect(renderedNamespace).toBe('a /'); + done(); + }); + }); + + it(`renders a properly truncated namespace with a trailing slash`, done => { + project.name_with_namespace = 'a / b / c / d / e / f'; + wrapper.setProps({ project: _.clone(project) }); + + vm.$nextTick(() => { + const renderedNamespace = trimText(vm.$el.querySelector('.js-project-namespace').textContent); + + expect(renderedNamespace).toBe('a / ... / e /'); + done(); + }); + }); + + it(`renders the project name`, done => { + project.name = 'my-test-project'; + wrapper.setProps({ project: _.clone(project) }); + + vm.$nextTick(() => { + const renderedName = trimText(vm.$el.querySelector('.js-project-name').innerHTML); + + expect(renderedName).toBe('my-test-project'); + done(); + }); + }); + + it(`renders the project name with highlighting in the case of a search query match`, done => { + project.name = 'my-test-project'; + wrapper.setProps({ project: _.clone(project), matcher: 'pro' }); + + vm.$nextTick(() => { + const renderedName = trimText(vm.$el.querySelector('.js-project-name').innerHTML); + + const expected = 'my-test-project'; + + expect(renderedName).toBe(expected); + done(); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js b/spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js new file mode 100644 index 00000000000..88c1dff76a1 --- /dev/null +++ b/spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js @@ -0,0 +1,152 @@ +import Vue from 'vue'; +import _ from 'underscore'; +import ProjectSelector from '~/vue_shared/components/project_selector/project_selector.vue'; +import ProjectListItem from '~/vue_shared/components/project_selector/project_list_item.vue'; +import { shallowMount } from '@vue/test-utils'; +import { trimText } from 'spec/helpers/vue_component_helper'; + +describe('ProjectSelector component', () => { + let wrapper; + let vm; + loadJSONFixtures('projects.json'); + const allProjects = getJSONFixture('projects.json'); + const searchResults = allProjects.slice(0, 5); + let selected = []; + selected = selected.concat(allProjects.slice(0, 3)).concat(allProjects.slice(5, 8)); + + beforeEach(() => { + jasmine.clock().install(); + + wrapper = shallowMount(Vue.extend(ProjectSelector), { + propsData: { + projectSearchResults: searchResults, + selectedProjects: selected, + showNoResultsMessage: false, + showMinimumSearchQueryMessage: false, + showLoadingIndicator: false, + showSearchErrorMessage: false, + }, + attachToDocument: true, + }); + + ({ vm } = wrapper); + }); + + afterEach(() => { + jasmine.clock().uninstall(); + vm.$destroy(); + }); + + it('renders the search results', () => { + expect(vm.$el.querySelectorAll('.js-project-list-item').length).toBe(5); + }); + + it(`triggers a (debounced) search when the search input value changes`, done => { + spyOn(vm, '$emit'); + const query = 'my test query!'; + const searchInput = vm.$el.querySelector('.js-project-selector-input'); + searchInput.value = query; + searchInput.dispatchEvent(new Event('input')); + + vm.$nextTick(() => { + expect(vm.$emit).not.toHaveBeenCalledWith(); + jasmine.clock().tick(501); + + expect(vm.$emit).toHaveBeenCalledWith('searched', query); + done(); + }); + }); + + it(`debounces the search input`, done => { + spyOn(vm, '$emit'); + const searchInput = vm.$el.querySelector('.js-project-selector-input'); + + const updateSearchQuery = (count = 0) => { + if (count === 10) { + jasmine.clock().tick(101); + + expect(vm.$emit).toHaveBeenCalledTimes(1); + expect(vm.$emit).toHaveBeenCalledWith('searched', `search query #9`); + done(); + } else { + searchInput.value = `search query #${count}`; + searchInput.dispatchEvent(new Event('input')); + + vm.$nextTick(() => { + jasmine.clock().tick(400); + updateSearchQuery(count + 1); + }); + } + }; + + updateSearchQuery(); + }); + + it(`includes a placeholder in the search box`, () => { + expect(vm.$el.querySelector('.js-project-selector-input').placeholder).toBe( + 'Search your projects', + ); + }); + + it(`triggers a "projectClicked" event when a project is clicked`, () => { + spyOn(vm, '$emit'); + wrapper.find(ProjectListItem).vm.$emit('click', _.first(searchResults)); + + expect(vm.$emit).toHaveBeenCalledWith('projectClicked', _.first(searchResults)); + }); + + it(`shows a "no results" message if showNoResultsMessage === true`, done => { + wrapper.setProps({ showNoResultsMessage: true }); + + vm.$nextTick(() => { + const noResultsEl = vm.$el.querySelector('.js-no-results-message'); + + expect(noResultsEl).toBeTruthy(); + + expect(trimText(noResultsEl.textContent)).toEqual('Sorry, no projects matched your search'); + + done(); + }); + }); + + it(`shows a "minimum seach query" message if showMinimumSearchQueryMessage === true`, done => { + wrapper.setProps({ showMinimumSearchQueryMessage: true }); + + vm.$nextTick(() => { + const minimumSearchEl = vm.$el.querySelector('.js-minimum-search-query-message'); + + expect(minimumSearchEl).toBeTruthy(); + + expect(trimText(minimumSearchEl.textContent)).toEqual( + 'Enter at least three characters to search', + ); + + done(); + }); + }); + + it(`shows a error message if showSearchErrorMessage === true`, done => { + wrapper.setProps({ showSearchErrorMessage: true }); + + vm.$nextTick(() => { + const errorMessageEl = vm.$el.querySelector('.js-search-error-message'); + + expect(errorMessageEl).toBeTruthy(); + + expect(trimText(errorMessageEl.textContent)).toEqual( + 'Something went wrong, unable to search projects', + ); + + done(); + }); + }); + + it(`focuses the input element when the focusSearchInput() method is called`, () => { + const input = vm.$el.querySelector('.js-project-selector-input'); + + expect(document.activeElement).not.toBe(input); + vm.focusSearchInput(); + + expect(document.activeElement).toBe(input); + }); +}); -- GitLab From eb95100c066d2d70a2128ea9ac6776f720b0777a Mon Sep 17 00:00:00 2001 From: mfluharty Date: Thu, 28 Mar 2019 14:00:44 -0600 Subject: [PATCH 011/154] Make corrections to address review feedback Refactor tests to follow conventions Add XSS test Eliminate a few unnecessary lines, comments, and parameters Use Vue.set for nested state changes --- .../javascripts/lib/utils/text_utility.js | 4 +- .../project_selector/project_selector.vue | 4 +- .../components/project_list_item.scss | 3 - .../frequent_items_list_item_spec.js | 105 ++++++++---------- .../frequent_items_search_input_spec.js | 25 +++-- .../project_list_item_spec.js | 104 +++++++++-------- .../project_selector/project_selector_spec.js | 84 ++++++-------- 7 files changed, 150 insertions(+), 179 deletions(-) diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js index 386d69ea430..1b7f8732c65 100644 --- a/app/assets/javascripts/lib/utils/text_utility.js +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -172,8 +172,8 @@ export const splitCamelCase = string => * @param {String} string A string namespace, * i.e. "My Group / My Subgroup / My Project" */ -export const truncateNamespace = string => { - if (_.isUndefined(string) || _.isNull(string) || !_.isString(string)) { +export const truncateNamespace = (string = '') => { + if (_.isNull(string) || !_.isString(string)) { return ''; } diff --git a/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue b/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue index bb17a9b331e..596fd48f96a 100644 --- a/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue +++ b/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue @@ -56,8 +56,8 @@ export default { focusSearchInput() { this.$refs.searchInput.focus(); }, - onInput: _.debounce(function debouncedOnInput(e) { - this.$emit('searched', e.target.value); + onInput: _.debounce(function debouncedOnInput() { + this.$emit('searched', this.searchQuery); }, SEARCH_INPUT_TIMEOUT_MS), }, }; diff --git a/app/assets/stylesheets/components/project_list_item.scss b/app/assets/stylesheets/components/project_list_item.scss index 6f9933e3d70..8e7c2c4398c 100644 --- a/app/assets/stylesheets/components/project_list_item.scss +++ b/app/assets/stylesheets/components/project_list_item.scss @@ -17,9 +17,6 @@ margin-left: -$gl-padding; margin-right: -$gl-padding; - // should be replaced by Bootstrap's - // .overflow-hidden utility class once - // we upgrade Bootstrap to at least 4.2.x .project-namespace-name-container { overflow: hidden; } diff --git a/spec/javascripts/frequent_items/components/frequent_items_list_item_spec.js b/spec/javascripts/frequent_items/components/frequent_items_list_item_spec.js index 92554bd9a69..f00bc2eeb6d 100644 --- a/spec/javascripts/frequent_items/components/frequent_items_list_item_spec.js +++ b/spec/javascripts/frequent_items/components/frequent_items_list_item_spec.js @@ -1,26 +1,31 @@ import Vue from 'vue'; import frequentItemsListItemComponent from '~/frequent_items/components/frequent_items_list_item.vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import { shallowMount } from '@vue/test-utils'; import { trimText } from 'spec/helpers/vue_component_helper'; import { mockProject } from '../mock_data'; // can also use 'mockGroup', but not useful to test here const createComponent = () => { const Component = Vue.extend(frequentItemsListItemComponent); - return mountComponent(Component, { - itemId: mockProject.id, - itemName: mockProject.name, - namespace: mockProject.namespace, - webUrl: mockProject.webUrl, - avatarUrl: mockProject.avatarUrl, + return shallowMount(Component, { + propsData: { + itemId: mockProject.id, + itemName: mockProject.name, + namespace: mockProject.namespace, + webUrl: mockProject.webUrl, + avatarUrl: mockProject.avatarUrl, + }, }); }; describe('FrequentItemsListItemComponent', () => { + let wrapper; let vm; beforeEach(() => { - vm = createComponent(); + wrapper = createComponent(); + + ({ vm } = wrapper); }); afterEach(() => { @@ -30,81 +35,61 @@ describe('FrequentItemsListItemComponent', () => { describe('computed', () => { describe('hasAvatar', () => { it('should return `true` or `false` if whether avatar is present or not', () => { - vm.avatarUrl = 'path/to/avatar.png'; + wrapper.setProps({ avatarUrl: 'path/to/avatar.png' }); expect(vm.hasAvatar).toBe(true); - vm.avatarUrl = null; + wrapper.setProps({ avatarUrl: null }); expect(vm.hasAvatar).toBe(false); }); }); describe('highlightedItemName', () => { - it('should enclose part of project name in & which matches with `matcher` prop', done => { - vm.matcher = 'lab'; - - vm.$nextTick() - .then(() => { - expect(vm.$el.querySelector('.js-frequent-items-item-title').innerHTML).toContain( - 'Lab', - ); - }) - .then(done) - .catch(done.fail); + it('should enclose part of project name in & which matches with `matcher` prop', () => { + wrapper.setProps({ matcher: 'lab' }); + + expect(wrapper.find('.js-frequent-items-item-title').html()).toContain( + 'Lab', + ); }); - it('should return project name as it is if `matcher` is not available', done => { - vm.matcher = null; - - vm.$nextTick() - .then(() => { - expect(vm.$el.querySelector('.js-frequent-items-item-title').innerHTML).toBe( - mockProject.name, - ); - }) - .then(done) - .catch(done.fail); + it('should return project name as it is if `matcher` is not available', () => { + wrapper.setProps({ matcher: null }); + + expect(trimText(wrapper.find('.js-frequent-items-item-title').text())).toBe( + mockProject.name, + ); }); }); describe('truncatedNamespace', () => { - it('should truncate project name from namespace string', done => { - vm.namespace = 'platform / nokia-3310'; - - vm.$nextTick() - .then(() => { - expect( - trimText(vm.$el.querySelector('.js-frequent-items-item-namespace').innerHTML), - ).toBe('platform'); - }) - .then(done) - .catch(done.fail); + it('should truncate project name from namespace string', () => { + wrapper.setProps({ namespace: 'platform / nokia-3310' }); + + expect(trimText(wrapper.find('.js-frequent-items-item-namespace').text())).toBe('platform'); }); - it('should truncate namespace string from the middle if it includes more than two groups in path', done => { - vm.namespace = 'platform / hardware / broadcom / Wifi Group / Mobile Chipset / nokia-3310'; - - vm.$nextTick() - .then(() => { - expect( - trimText(vm.$el.querySelector('.js-frequent-items-item-namespace').innerHTML), - ).toBe('platform / ... / Mobile Chipset'); - }) - .then(done) - .catch(done.fail); + it('should truncate namespace string from the middle if it includes more than two groups in path', () => { + wrapper.setProps({ + namespace: 'platform / hardware / broadcom / Wifi Group / Mobile Chipset / nokia-3310', + }); + + expect(trimText(wrapper.find('.js-frequent-items-item-namespace').text())).toBe( + 'platform / ... / Mobile Chipset', + ); }); }); }); describe('template', () => { it('should render component element', () => { - expect(vm.$el.classList.contains('frequent-items-list-item-container')).toBeTruthy(); - expect(vm.$el.querySelectorAll('a').length).toBe(1); - expect(vm.$el.querySelectorAll('.frequent-items-item-avatar-container').length).toBe(1); - expect(vm.$el.querySelectorAll('.frequent-items-item-metadata-container').length).toBe(1); - expect(vm.$el.querySelectorAll('.js-frequent-items-item-title').length).toBe(1); - expect(vm.$el.querySelectorAll('.js-frequent-items-item-namespace').length).toBe(1); + expect(wrapper.classes()).toContain('frequent-items-list-item-container'); + expect(wrapper.findAll('a').length).toBe(1); + expect(wrapper.findAll('.frequent-items-item-avatar-container').length).toBe(1); + expect(wrapper.findAll('.frequent-items-item-metadata-container').length).toBe(1); + expect(wrapper.findAll('.frequent-items-item-title').length).toBe(1); + expect(wrapper.findAll('.frequent-items-item-namespace').length).toBe(1); }); }); }); diff --git a/spec/javascripts/frequent_items/components/frequent_items_search_input_spec.js b/spec/javascripts/frequent_items/components/frequent_items_search_input_spec.js index d564292f1ba..ddbbc5c2d29 100644 --- a/spec/javascripts/frequent_items/components/frequent_items_search_input_spec.js +++ b/spec/javascripts/frequent_items/components/frequent_items_search_input_spec.js @@ -1,19 +1,22 @@ import Vue from 'vue'; import searchComponent from '~/frequent_items/components/frequent_items_search_input.vue'; import eventHub from '~/frequent_items/event_hub'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import { shallowMount } from '@vue/test-utils'; const createComponent = (namespace = 'projects') => { const Component = Vue.extend(searchComponent); - return mountComponent(Component, { namespace }); + return shallowMount(Component, { propsData: { namespace } }); }; describe('FrequentItemsSearchInputComponent', () => { + let wrapper; let vm; beforeEach(() => { - vm = createComponent(); + wrapper = createComponent(); + + ({ vm } = wrapper); }); afterEach(() => { @@ -35,7 +38,7 @@ describe('FrequentItemsSearchInputComponent', () => { describe('mounted', () => { it('should listen `dropdownOpen` event', done => { spyOn(eventHub, '$on'); - const vmX = createComponent(); + const vmX = createComponent().vm; Vue.nextTick(() => { expect(eventHub.$on).toHaveBeenCalledWith( @@ -49,7 +52,7 @@ describe('FrequentItemsSearchInputComponent', () => { describe('beforeDestroy', () => { it('should unbind event listeners on eventHub', done => { - const vmX = createComponent(); + const vmX = createComponent().vm; spyOn(eventHub, '$off'); vmX.$mount(); @@ -67,12 +70,12 @@ describe('FrequentItemsSearchInputComponent', () => { describe('template', () => { it('should render component element', () => { - const inputEl = vm.$el.querySelector('input.form-control'); - - expect(vm.$el.classList.contains('search-input-container')).toBeTruthy(); - expect(inputEl).not.toBe(null); - expect(inputEl.getAttribute('placeholder')).toBe('Search your projects'); - expect(vm.$el.querySelector('.search-icon')).toBeDefined(); + expect(wrapper.classes()).toContain('search-input-container'); + expect(wrapper.contains('input.form-control')).toBe(true); + expect(wrapper.contains('.search-icon')).toBe(true); + expect(wrapper.find('input.form-control').attributes('placeholder')).toBe( + 'Search your projects', + ); }); }); }); diff --git a/spec/javascripts/vue_shared/components/project_selector/project_list_item_spec.js b/spec/javascripts/vue_shared/components/project_selector/project_list_item_spec.js index 8dbdfe97f8f..b95183747bb 100644 --- a/spec/javascripts/vue_shared/components/project_selector/project_list_item_spec.js +++ b/spec/javascripts/vue_shared/components/project_selector/project_list_item_spec.js @@ -1,4 +1,3 @@ -import _ from 'underscore'; import ProjectListItem from '~/vue_shared/components/project_selector/project_list_item.vue'; import { shallowMount, createLocalVue } from '@vue/test-utils'; import { trimText } from 'spec/helpers/vue_component_helper'; @@ -6,99 +5,106 @@ import { trimText } from 'spec/helpers/vue_component_helper'; const localVue = createLocalVue(); describe('ProjectListItem component', () => { + const Component = localVue.extend(ProjectListItem); let wrapper; let vm; + let options; loadJSONFixtures('projects.json'); const project = getJSONFixture('projects.json')[0]; beforeEach(() => { - wrapper = shallowMount(localVue.extend(ProjectListItem), { + options = { propsData: { project, selected: false, }, sync: false, localVue, - }); - - ({ vm } = wrapper); + }; }); afterEach(() => { - vm.$destroy(); + wrapper.vm.$destroy(); }); it('does not render a check mark icon if selected === false', () => { - expect(vm.$el.querySelector('.js-selected-icon.js-unselected')).toBeTruthy(); + wrapper = shallowMount(Component, options); + + expect(wrapper.contains('.js-selected-icon.js-unselected')).toBe(true); }); - it('renders a check mark icon if selected === true', done => { - wrapper.setProps({ selected: true }); + it('renders a check mark icon if selected === true', () => { + options.propsData.selected = true; - vm.$nextTick(() => { - expect(vm.$el.querySelector('.js-selected-icon.js-selected')).toBeTruthy(); - done(); - }); + wrapper = shallowMount(Component, options); + + expect(wrapper.contains('.js-selected-icon.js-selected')).toBe(true); }); it(`emits a "clicked" event when clicked`, () => { + wrapper = shallowMount(Component, options); + ({ vm } = wrapper); + spyOn(vm, '$emit'); - vm.onClick(); + wrapper.vm.onClick(); - expect(vm.$emit).toHaveBeenCalledWith('click'); + expect(wrapper.vm.$emit).toHaveBeenCalledWith('click'); }); it(`renders the project avatar`, () => { - expect(vm.$el.querySelector('.js-project-avatar')).toBeTruthy(); + wrapper = shallowMount(Component, options); + + expect(wrapper.contains('.js-project-avatar')).toBe(true); }); - it(`renders a simple namespace name with a trailing slash`, done => { - project.name_with_namespace = 'a / b'; - wrapper.setProps({ project: _.clone(project) }); + it(`renders a simple namespace name with a trailing slash`, () => { + options.propsData.project.name_with_namespace = 'a / b'; - vm.$nextTick(() => { - const renderedNamespace = trimText(vm.$el.querySelector('.js-project-namespace').textContent); + wrapper = shallowMount(Component, options); + const renderedNamespace = trimText(wrapper.find('.js-project-namespace').text()); - expect(renderedNamespace).toBe('a /'); - done(); - }); + expect(renderedNamespace).toBe('a /'); }); - it(`renders a properly truncated namespace with a trailing slash`, done => { - project.name_with_namespace = 'a / b / c / d / e / f'; - wrapper.setProps({ project: _.clone(project) }); + it(`renders a properly truncated namespace with a trailing slash`, () => { + options.propsData.project.name_with_namespace = 'a / b / c / d / e / f'; - vm.$nextTick(() => { - const renderedNamespace = trimText(vm.$el.querySelector('.js-project-namespace').textContent); + wrapper = shallowMount(Component, options); + const renderedNamespace = trimText(wrapper.find('.js-project-namespace').text()); - expect(renderedNamespace).toBe('a / ... / e /'); - done(); - }); + expect(renderedNamespace).toBe('a / ... / e /'); }); - it(`renders the project name`, done => { - project.name = 'my-test-project'; - wrapper.setProps({ project: _.clone(project) }); + it(`renders the project name`, () => { + options.propsData.project.name = 'my-test-project'; - vm.$nextTick(() => { - const renderedName = trimText(vm.$el.querySelector('.js-project-name').innerHTML); + wrapper = shallowMount(Component, options); + const renderedName = trimText(wrapper.find('.js-project-name').text()); - expect(renderedName).toBe('my-test-project'); - done(); - }); + expect(renderedName).toBe('my-test-project'); }); - it(`renders the project name with highlighting in the case of a search query match`, done => { - project.name = 'my-test-project'; - wrapper.setProps({ project: _.clone(project), matcher: 'pro' }); + it(`renders the project name with highlighting in the case of a search query match`, () => { + options.propsData.project.name = 'my-test-project'; + options.propsData.matcher = 'pro'; + + wrapper = shallowMount(Component, options); + const renderedName = trimText(wrapper.find('.js-project-name').html()); + const expected = 'my-test-project'; + + expect(renderedName).toContain(expected); + }); - vm.$nextTick(() => { - const renderedName = trimText(vm.$el.querySelector('.js-project-name').innerHTML); + it('prevents search query and project name XSS', () => { + const alertSpy = spyOn(window, 'alert'); + options.propsData.project.name = "my-xss-project"; + options.propsData.matcher = "pro"; - const expected = 'my-test-project'; + wrapper = shallowMount(Component, options); + const renderedName = trimText(wrapper.find('.js-project-name').html()); + const expected = 'my-xss-project'; - expect(renderedName).toBe(expected); - done(); - }); + expect(renderedName).toContain(expected); + expect(alertSpy).not.toHaveBeenCalled(); }); }); diff --git a/spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js b/spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js index 88c1dff76a1..ba9ec8f2f19 100644 --- a/spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js +++ b/spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js @@ -38,28 +38,25 @@ describe('ProjectSelector component', () => { }); it('renders the search results', () => { - expect(vm.$el.querySelectorAll('.js-project-list-item').length).toBe(5); + expect(wrapper.findAll('.js-project-list-item').length).toBe(5); }); - it(`triggers a (debounced) search when the search input value changes`, done => { + it(`triggers a (debounced) search when the search input value changes`, () => { spyOn(vm, '$emit'); const query = 'my test query!'; - const searchInput = vm.$el.querySelector('.js-project-selector-input'); - searchInput.value = query; - searchInput.dispatchEvent(new Event('input')); + const searchInput = wrapper.find('.js-project-selector-input'); + searchInput.setValue(query); + searchInput.trigger('input'); - vm.$nextTick(() => { - expect(vm.$emit).not.toHaveBeenCalledWith(); - jasmine.clock().tick(501); + expect(vm.$emit).not.toHaveBeenCalledWith(); + jasmine.clock().tick(501); - expect(vm.$emit).toHaveBeenCalledWith('searched', query); - done(); - }); + expect(vm.$emit).toHaveBeenCalledWith('searched', query); }); - it(`debounces the search input`, done => { + it(`debounces the search input`, () => { spyOn(vm, '$emit'); - const searchInput = vm.$el.querySelector('.js-project-selector-input'); + const searchInput = wrapper.find('.js-project-selector-input'); const updateSearchQuery = (count = 0) => { if (count === 10) { @@ -67,15 +64,12 @@ describe('ProjectSelector component', () => { expect(vm.$emit).toHaveBeenCalledTimes(1); expect(vm.$emit).toHaveBeenCalledWith('searched', `search query #9`); - done(); } else { - searchInput.value = `search query #${count}`; - searchInput.dispatchEvent(new Event('input')); + searchInput.setValue(`search query #${count}`); + searchInput.trigger('input'); - vm.$nextTick(() => { - jasmine.clock().tick(400); - updateSearchQuery(count + 1); - }); + jasmine.clock().tick(400); + updateSearchQuery(count + 1); } }; @@ -83,7 +77,7 @@ describe('ProjectSelector component', () => { }); it(`includes a placeholder in the search box`, () => { - expect(vm.$el.querySelector('.js-project-selector-input').placeholder).toBe( + expect(wrapper.find('.js-project-selector-input').attributes('placeholder')).toBe( 'Search your projects', ); }); @@ -95,58 +89,44 @@ describe('ProjectSelector component', () => { expect(vm.$emit).toHaveBeenCalledWith('projectClicked', _.first(searchResults)); }); - it(`shows a "no results" message if showNoResultsMessage === true`, done => { + it(`shows a "no results" message if showNoResultsMessage === true`, () => { wrapper.setProps({ showNoResultsMessage: true }); - vm.$nextTick(() => { - const noResultsEl = vm.$el.querySelector('.js-no-results-message'); - - expect(noResultsEl).toBeTruthy(); + expect(wrapper.contains('.js-no-results-message')).toBe(true); - expect(trimText(noResultsEl.textContent)).toEqual('Sorry, no projects matched your search'); + const noResultsEl = wrapper.find('.js-no-results-message'); - done(); - }); + expect(trimText(noResultsEl.text())).toEqual('Sorry, no projects matched your search'); }); - it(`shows a "minimum seach query" message if showMinimumSearchQueryMessage === true`, done => { + it(`shows a "minimum seach query" message if showMinimumSearchQueryMessage === true`, () => { wrapper.setProps({ showMinimumSearchQueryMessage: true }); - vm.$nextTick(() => { - const minimumSearchEl = vm.$el.querySelector('.js-minimum-search-query-message'); - - expect(minimumSearchEl).toBeTruthy(); + expect(wrapper.contains('.js-minimum-search-query-message')).toBe(true); - expect(trimText(minimumSearchEl.textContent)).toEqual( - 'Enter at least three characters to search', - ); + const minimumSearchEl = wrapper.find('.js-minimum-search-query-message'); - done(); - }); + expect(trimText(minimumSearchEl.text())).toEqual('Enter at least three characters to search'); }); - it(`shows a error message if showSearchErrorMessage === true`, done => { + it(`shows a error message if showSearchErrorMessage === true`, () => { wrapper.setProps({ showSearchErrorMessage: true }); - vm.$nextTick(() => { - const errorMessageEl = vm.$el.querySelector('.js-search-error-message'); - - expect(errorMessageEl).toBeTruthy(); + expect(wrapper.contains('.js-search-error-message')).toBe(true); - expect(trimText(errorMessageEl.textContent)).toEqual( - 'Something went wrong, unable to search projects', - ); + const errorMessageEl = wrapper.find('.js-search-error-message'); - done(); - }); + expect(trimText(errorMessageEl.text())).toEqual( + 'Something went wrong, unable to search projects', + ); }); it(`focuses the input element when the focusSearchInput() method is called`, () => { - const input = vm.$el.querySelector('.js-project-selector-input'); + const input = wrapper.find('.js-project-selector-input'); - expect(document.activeElement).not.toBe(input); + expect(document.activeElement).not.toBe(input.element); vm.focusSearchInput(); - expect(document.activeElement).toBe(input); + expect(document.activeElement).toBe(input.element); }); }); -- GitLab From bd750af785037a105dc3347d3bd38cb49a003dc2 Mon Sep 17 00:00:00 2001 From: jerasmus Date: Wed, 3 Apr 2019 08:31:04 +0200 Subject: [PATCH 012/154] Update error message Updated the error message in the docs --- doc/user/project/clusters/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md index 3a9a3b4a423..3097fb01722 100644 --- a/doc/user/project/clusters/index.md +++ b/doc/user/project/clusters/index.md @@ -553,7 +553,7 @@ deployment jobs, immediately before the jobs starts. However, sometimes GitLab can not create them. In such instances, your job will fail with the message: ```text -The job failed to complete prerequisite tasks +This job failed because the necessary resources were not successfully created. ``` To find the cause of this error when creating a namespace and service account, check the [logs](../../../administration/logs.md#sidekiqlog). -- GitLab From 70f55be08114e31b1e2eae38bb3ab6d781dce68a Mon Sep 17 00:00:00 2001 From: Alexandru Croitor Date: Tue, 2 Apr 2019 18:47:35 +0300 Subject: [PATCH 013/154] Extract EE specific files/lines for quick actions tests --- spec/services/notes/quick_actions_service_spec.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/services/notes/quick_actions_service_spec.rb b/spec/services/notes/quick_actions_service_spec.rb index 7d2b6d5b8a7..9efdf96bc64 100644 --- a/spec/services/notes/quick_actions_service_spec.rb +++ b/spec/services/notes/quick_actions_service_spec.rb @@ -185,6 +185,7 @@ describe Notes::QuickActionsService do end before do + stub_licensed_features(multiple_issue_assignees: false) project.add_maintainer(maintainer) project.add_maintainer(assignee) end -- GitLab From e90c2adcf5746ef5d378533b912e4eba5f26d5d1 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 3 Apr 2019 13:52:24 +0200 Subject: [PATCH 014/154] Build and deploy serverless functions with gitlabktl --- lib/gitlab/ci/templates/Serverless.gitlab-ci.yml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/gitlab/ci/templates/Serverless.gitlab-ci.yml b/lib/gitlab/ci/templates/Serverless.gitlab-ci.yml index 4f3d08d98fe..4c92dcb7941 100644 --- a/lib/gitlab/ci/templates/Serverless.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Serverless.gitlab-ci.yml @@ -31,11 +31,14 @@ stages: - echo "$CI_REGISTRY_IMAGE" - tm -n "$KUBE_NAMESPACE" --config "$KUBECONFIG" deploy service "$CI_PROJECT_NAME" --from-image "$CI_REGISTRY_IMAGE" --wait +.serverless:build:functions: + stage: build + environment: development + image: registry.gitlab.com/gitlab-org/gitlabktl:latest + script: /usr/bin/gitlabktl serverless build + .serverless:deploy:functions: stage: deploy environment: development - image: gcr.io/triggermesh/tm:v0.0.9 - script: - - tm -n "$KUBE_NAMESPACE" set registry-auth gitlab-registry --registry "$CI_REGISTRY" --username "$CI_REGISTRY_USER" --password "$CI_JOB_TOKEN" --push - - tm -n "$KUBE_NAMESPACE" set registry-auth gitlab-registry --registry "$CI_REGISTRY" --username "$CI_DEPLOY_USER" --password "$CI_DEPLOY_PASSWORD" --pull - - tm -n "$KUBE_NAMESPACE" deploy --wait + image: registry.gitlab.com/gitlab-org/gitlabktl:latest + script: /usr/bin/gitlabktl serverless deploy -- GitLab From da38bdc24aac580bf670f0aba4f5b652daba3701 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 3 Apr 2019 14:16:19 +0200 Subject: [PATCH 015/154] Update serverless docs to use `gitlabktl` --- doc/user/project/clusters/serverless/index.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/doc/user/project/clusters/serverless/index.md b/doc/user/project/clusters/serverless/index.md index 96bf455116c..612b02f8af3 100644 --- a/doc/user/project/clusters/serverless/index.md +++ b/doc/user/project/clusters/serverless/index.md @@ -11,13 +11,12 @@ Run serverless workloads on Kubernetes using [Knative](https://cloud.google.com/ Knative extends Kubernetes to provide a set of middleware components that are useful to build modern, source-centric, container-based applications. Knative brings some significant benefits out of the box through its main components: -- [Build](https://github.com/knative/build): Source-to-container build orchestration. -- [Eventing](https://github.com/knative/eventing): Management and delivery of events. - [Serving](https://github.com/knative/serving): Request-driven compute that can scale to zero. +- [Eventing](https://github.com/knative/eventing): Management and delivery of events. For more information on Knative, visit the [Knative docs repo](https://github.com/knative/docs). -With GitLab serverless, you can deploy both functions-as-a-service (FaaS) and serverless applications. +With GitLab Serverless, you can deploy both functions-as-a-service (FaaS) and serverless applications. ## Prerequisites @@ -41,13 +40,16 @@ To run Knative on Gitlab, you will need: wildcard domain where your applications will be served. Configure your DNS server to use the external IP address or hostname for that domain. 1. **`.gitlab-ci.yml`:** GitLab uses [Kaniko](https://github.com/GoogleContainerTools/kaniko) - to build the application and the [TriggerMesh CLI](https://github.com/triggermesh/tm) to simplify the - deployment of knative services and functions. + to build the application. We also use [gitlabktl](https://gitlab.com/gitlab-org/gitlabktl) + and [TriggerMesh CLI](https://github.com/triggermesh/tm) CLIs to simplify the + deployment of services and functions to Knative. 1. **`serverless.yml`** (for [functions only](#deploying-functions)): When using serverless to deploy functions, the `serverless.yml` file will contain the information for all the functions being hosted in the repository as well as a reference to the runtime being used. -1. **`Dockerfile`** (for [applications only](#deploying-serverless-applications): Knative requires a `Dockerfile` in order to build your application. It should be included - at the root of your project's repo and expose port `8080`. +1. **`Dockerfile`** (for [applications only](#deploying-serverless-applications): Knative requires a + `Dockerfile` in order to build your applications. It should be included at the root of your + project's repo and expose port `8080`. `Dockerfile` is not require if you plan to build serverless functions + using our [runtimes](https://gitlab.com/gitlab-org/serverless/runtimes). 1. **Prometheus** (optional): Installing Prometheus allows you to monitor the scale and traffic of your serverless function/application. See [Installing Applications](../index.md#installing-applications) for more information. -- GitLab From c4c6dcb68c71cd31ecad96bea397a595109aaa24 Mon Sep 17 00:00:00 2001 From: Simon Knox Date: Wed, 3 Apr 2019 13:24:07 +1100 Subject: [PATCH 016/154] Add color util classes for backgrounds and text We have a range of shades for most of the theme colors Grouped them into color maps the same way Bootstrap does for $grays already Also add type scale utils --- app/assets/stylesheets/application.scss | 35 +++----- .../stylesheets/framework/variables.scss | 87 +++++++++++++++++++ app/assets/stylesheets/utilities.scss | 17 ++++ doc/development/fe_guide/style_guide_scss.md | 2 +- 4 files changed, 117 insertions(+), 24 deletions(-) create mode 100644 app/assets/stylesheets/utilities.scss diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 86189143525..8aaa9772715 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -6,40 +6,29 @@ *= require cropper.css */ -/* - * Welcome to GitLab css! - * If you need to add or modify UI component that is common for many pages - * like a table or typography then make changes in the framework/ directory. - * If you need to add unique style that should affect only one page - use pages/ - * directory. - */ - +// Welcome to GitLab css! +// If you need to add or modify UI component that is common for many pages +// like a table or typography then make changes in the framework/ directory. +// If you need to add unique style that should affect only one page - use pages/ +// directory. @import "../../../node_modules/at.js/dist/css/jquery.atwho"; @import "../../../node_modules/pikaday/scss/pikaday"; @import "../../../node_modules/dropzone/dist/basic"; @import "../../../node_modules/select2/select2"; -/* - * GitLab UI framework - */ +// GitLab UI framework @import "framework"; -/* - * Font icons - */ +// Font icons @import "font-awesome"; -/* - * Page specific styles (issues, projects etc): - */ +// Page specific styles (issues, projects etc): @import "pages/**/*"; -/* - * Component specific styles, will be moved to gitlab-ui - */ +// Component specific styles, will be moved to gitlab-ui @import "components/**/*"; -/* - * Styles for JS behaviors. - */ +// Styles for JS behaviors. @import "behaviors"; + +@import "utilities"; diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 7d9781ffb87..e2946e79f9d 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -110,6 +110,84 @@ $gray-800: #4f4f4f; $gray-900: #2e2e2e; $gray-950: #1f1f1f; +$greens: ( + '50': $green-50, + '100': $green-100, + '200': $green-200, + '300': $green-300, + '400': $green-400, + '500': $green-500, + '600': $green-600, + '700': $green-700, + '800': $green-800, + '900': $green-900, + '950': $green-950 +); + +$blues: ( + '50': $blue-50, + '100': $blue-100, + '200': $blue-200, + '300': $blue-300, + '400': $blue-400, + '500': $blue-500, + '600': $blue-600, + '700': $blue-700, + '800': $blue-800, + '900': $blue-900, + '950': $blue-950 +); + +$oranges: ( + '50': $orange-50, + '100': $orange-100, + '200': $orange-200, + '300': $orange-300, + '400': $orange-400, + '500': $orange-500, + '600': $orange-600, + '700': $orange-700, + '800': $orange-800, + '900': $orange-900, + '950': $orange-950 +); + +$reds: ( + '50': $red-50, + '100': $red-100, + '200': $red-200, + '300': $red-300, + '400': $red-400, + '500': $red-500, + '600': $red-600, + '700': $red-700, + '800': $red-800, + '900': $red-900, + '950': $red-950 +); + +$grays: ( + '50': $gray-50, + '100': $gray-100, + '200': $gray-200, + '300': $gray-300, + '400': $gray-400, + '500': $gray-500, + '600': $gray-600, + '700': $gray-700, + '800': $gray-800, + '900': $gray-900, + '950': $gray-950 +); + +$color-ranges: ( + 'primary': $blues, + 'secondary': $grays, + 'success': $greens, + 'warning': $oranges, + 'danger': $reds +); + // GitLab themes $indigo-50: #f7f7ff; @@ -219,6 +297,15 @@ $gl-gray-dark: #313236; $gl-gray-light: #5c5c5c; $gl-header-color: #4c4e54; +$type-scale: ( + 1: 12px, + 2: 14px, + 3: 16px, + 4: 20px, + 5: 28px, + 6: 42px +); + /* * Lists */ diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss new file mode 100644 index 00000000000..3648ec5e239 --- /dev/null +++ b/app/assets/stylesheets/utilities.scss @@ -0,0 +1,17 @@ +@each $variant, $range in $color-ranges { + @each $suffix, $color in $range { + #{'.bg-#{$variant}-#{$suffix}'} { + background-color: $color; + } + + #{'.text-#{$variant}-#{$suffix}'} { + color: $color; + } + } +} + +@each $index, $size in $type-scale { + #{'.text-#{$index}'} { + font-size: $size; + } +} diff --git a/doc/development/fe_guide/style_guide_scss.md b/doc/development/fe_guide/style_guide_scss.md index 6f6b361f423..6e2a66771e4 100644 --- a/doc/development/fe_guide/style_guide_scss.md +++ b/doc/development/fe_guide/style_guide_scss.md @@ -12,7 +12,7 @@ led by the [GitLab UI WG](https://gitlab.com/gitlab-com/www-gitlab-com/merge_req We have a few internal utility classes in [`common.scss`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/stylesheets/framework/common.scss) and we use [Bootstrap's Utility Classes](https://getbootstrap.com/docs/4.3/utilities/) -New utility classes should be added to [`common.scss`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/stylesheets/framework/common.scss). +New utility classes should be added to [`utilities.scss`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/stylesheets/utilities.scss). ### Naming -- GitLab From bfd17bca5dda92936f99722a45b261f2b343ab2c Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 3 Apr 2019 16:01:58 +0200 Subject: [PATCH 017/154] Update GitLab Serverless documentation to use `gitlabktl` --- doc/user/project/clusters/serverless/index.md | 65 ++++++++++++------- 1 file changed, 40 insertions(+), 25 deletions(-) diff --git a/doc/user/project/clusters/serverless/index.md b/doc/user/project/clusters/serverless/index.md index 612b02f8af3..c374acd4915 100644 --- a/doc/user/project/clusters/serverless/index.md +++ b/doc/user/project/clusters/serverless/index.md @@ -91,10 +91,11 @@ Using functions is useful for dealing with independent events without needing to maintain a complex unified infrastructure. This allows you to focus on a single task that can be executed/scaled automatically and independently. -Currently the following [runtimes](https://gitlab.com/triggermesh/runtimes) are offered: +Currently the following [runtimes](https://gitlab.com/gitlab-org/serverless/runtimes) are offered: +- ruby - node.js -- kaniko +- Dockerfile You can find and import all the files referenced in this doc in the **[functions example project](https://gitlab.com/knative-examples/functions)**. @@ -113,13 +114,17 @@ Follow these steps to deploy a function using the Node.js runtime to your Knativ include: template: Serverless.gitlab-ci.yml - functions: + functions:build: + extends: .serverless:build:functions + environment: production + + functions:deploy: extends: .serverless:deploy:functions environment: production ``` - This `.gitlab-ci.yml` creates a `functions` job that invokes some - predefined commands to deploy your functions to your cluster. + This `.gitlab-ci.yml` creates jobs that invokes some predefined commands to + build and deploy your functions to your cluster. `Serverless.gitlab-ci.yml` is a template that allows customization. You can either import it with `include` parameter and use `extends` to @@ -137,29 +142,40 @@ Follow these steps to deploy a function using the Node.js runtime to your Knativ You can find the relevant files for this project in the [functions example project](https://gitlab.com/knative-examples/functions). ```yaml - service: my-functions - description: "Deploying functions from GitLab using Knative" + service: functions + description: "GitLab Serverless functions using Knative" provider: name: triggermesh - registry-secret: gitlab-registry environment: - FOO: BAR - - functions: - echo: - handler: echo - runtime: https://gitlab.com/triggermesh/runtimes/raw/master/nodejs.yaml - description: "echo function using node.js runtime" - buildargs: - - DIRECTORY=echo - environment: - FUNCTION: echo + FOO: value + + functions: + echo-js: + handler: echo-js + source: ./echo-js + runtime: https://gitlab.com/gitlab-org/serverless/runtimes/nodejs + description: "node.js runtime function" + environment: + MY_FUNCTION: echo-js + + echo-rb: + handler: MyEcho.my_function + source: ./echo-rb + runtime: https://gitlab.com/gitlab-org/serverless/runtimes/ruby + description: "Ruby runtime function" + environment: + MY_FUNCTION: echo-rb + + echo-docker: + handler: echo-docker + source: ./echo-docker + description: "Dockerfile runtime function" + environment: + MY_FUNCTION: echo-docker ``` -The `serverless.yml` file references both an `echo` directory (under `buildargs`) and an `echo` file (under `handler`), -which is a reference to `echo.js` in the [repository](https://gitlab.com/knative-examples/functions). Additionally, it -contains three sections with distinct parameters: +Explanation of fields used above: ### `service` @@ -173,7 +189,6 @@ contains three sections with distinct parameters: | Parameter | Description | |-----------|-------------| | `name` | Indicates which provider is used to execute the `serverless.yml` file. In this case, the TriggerMesh `tm` CLI. | -| `registry-secret` | Indicates which registry will be used to store docker images. The sample function is using the GitLab Registry (`gitlab-registry`). A different registry host may be specified using `registry` key in the `provider` object. If changing the default, update the permission and the secret value on the `gitlab-ci.yml` file | | `environment` | Includes the environment variables to be passed as part of function execution for **all** functions in the file, where `FOO` is the variable name and `BAR` are he variable contents. You may replace this with you own variables. | ### `functions` @@ -182,10 +197,10 @@ In the `serverless.yml` example above, the function name is `echo` and the subse | Parameter | Description | |-----------|-------------| -| `handler` | The function's file name. In the example above, both the function name and the handler are the same. | +| `handler` | The function's name. | +| `source` | Directory with sources of a functions. | | `runtime` | The runtime to be used to execute the function. | | `description` | A short description of the function. | -| `buildargs` | Pointer to the function file in the repo. In the sample the function is located in the `echo` directory. | | `environment` | Sets an environment variable for the specific function only. | After the `gitlab-ci.yml` template has been added and the `serverless.yml` file has been -- GitLab From 82c5ca7caf6d42ae12c76332c449610d42dccfa5 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 3 Apr 2019 16:18:34 +0200 Subject: [PATCH 018/154] Fix indentation of exemplary serverless.yml config --- doc/user/project/clusters/serverless/index.md | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/doc/user/project/clusters/serverless/index.md b/doc/user/project/clusters/serverless/index.md index c374acd4915..f2cb619cb15 100644 --- a/doc/user/project/clusters/serverless/index.md +++ b/doc/user/project/clusters/serverless/index.md @@ -150,29 +150,29 @@ Follow these steps to deploy a function using the Node.js runtime to your Knativ environment: FOO: value - functions: - echo-js: - handler: echo-js - source: ./echo-js - runtime: https://gitlab.com/gitlab-org/serverless/runtimes/nodejs - description: "node.js runtime function" - environment: - MY_FUNCTION: echo-js - - echo-rb: - handler: MyEcho.my_function - source: ./echo-rb - runtime: https://gitlab.com/gitlab-org/serverless/runtimes/ruby - description: "Ruby runtime function" - environment: - MY_FUNCTION: echo-rb - - echo-docker: - handler: echo-docker - source: ./echo-docker - description: "Dockerfile runtime function" - environment: - MY_FUNCTION: echo-docker + functions: + echo-js: + handler: echo-js + source: ./echo-js + runtime: https://gitlab.com/gitlab-org/serverless/runtimes/nodejs + description: "node.js runtime function" + environment: + MY_FUNCTION: echo-js + + echo-rb: + handler: MyEcho.my_function + source: ./echo-rb + runtime: https://gitlab.com/gitlab-org/serverless/runtimes/ruby + description: "Ruby runtime function" + environment: + MY_FUNCTION: echo-rb + + echo-docker: + handler: echo-docker + source: ./echo-docker + description: "Dockerfile runtime function" + environment: + MY_FUNCTION: echo-docker ``` Explanation of fields used above: -- GitLab From 6ba1a8e9f3a5998319b41d1bab5f4abec0bd0587 Mon Sep 17 00:00:00 2001 From: Enrique Alcantara Date: Mon, 1 Apr 2019 09:06:58 -0400 Subject: [PATCH 019/154] Support url and number inputs in gl_form_errors --- app/assets/javascripts/gl_field_errors.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/gl_field_errors.js b/app/assets/javascripts/gl_field_errors.js index d5d5954ce6a..c4fd719c8d0 100644 --- a/app/assets/javascripts/gl_field_errors.js +++ b/app/assets/javascripts/gl_field_errors.js @@ -15,7 +15,7 @@ export default class GlFieldErrors { initValidators() { // register selectors here as needed - const validateSelectors = [':text', ':password', '[type=email]'] + const validateSelectors = [':text', ':password', '[type=email]', '[type=url]', '[type=number]'] .map(selector => `input${selector}`) .join(','); -- GitLab From ca32773dbfc345de551f8b107692d1995e717fa3 Mon Sep 17 00:00:00 2001 From: Enrique Alcantara Date: Fri, 22 Mar 2019 15:04:41 -0400 Subject: [PATCH 020/154] Display inline validation error messages In the new cluster forms, display form error messages below form fields instead of a summary banner on top of the form. Include client side validation in order to display user friendly error messages. Also remove unnecessary placeholders. --- app/assets/stylesheets/framework/forms.scss | 4 +- .../clusters/clusters/gcp/_form.html.haml | 47 +++++++------- .../clusters/clusters/user/_form.html.haml | 61 +++++++++---------- locale/gitlab.pot | 14 ++++- 4 files changed, 71 insertions(+), 55 deletions(-) diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index 3b1d1d67509..8dd1489af4f 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -204,8 +204,10 @@ label { margin-top: #{$grid-size / 2}; } -.gl-field-error { +.gl-field-error, +.invalid-feedback { color: $red-500; + font-size: $gl-font-size; } .gl-show-field-errors { diff --git a/app/views/clusters/clusters/gcp/_form.html.haml b/app/views/clusters/clusters/gcp/_form.html.haml index 00582e19662..3e0f8955081 100644 --- a/app/views/clusters/clusters/gcp/_form.html.haml +++ b/app/views/clusters/clusters/gcp/_form.html.haml @@ -7,25 +7,27 @@ - help_link_end = ' %{external_link_icon}'.html_safe % { external_link_icon: external_link_icon } %p - - link_to_help_page = link_to(s_('ClusterIntegration|help page'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer') - = s_('ClusterIntegration|Read our %{link_to_help_page} on Kubernetes cluster integration.').html_safe % { link_to_help_page: link_to_help_page} + - link_to_help_page = link_to(s_('ClusterIntegration|help page'), + help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer') + = s_('ClusterIntegration|Read our %{link_to_help_page} on Kubernetes cluster integration.').html_safe % { link_to_help_page: link_to_help_page } %p= link_to('Select a different Google account', @authorize_url) -= form_for @gcp_cluster, html: { class: 'js-gke-cluster-creation prepend-top-20', data: { token: token_in_session } }, url: clusterable.create_gcp_clusters_path, as: :cluster do |field| - = form_errors(@gcp_cluster) - .form-group - = field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-bold' - = field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name') += bootstrap_form_for @gcp_cluster, html: { class: 'gl-show-field-errors js-gke-cluster-creation prepend-top-20', + data: { token: token_in_session } }, url: clusterable.create_gcp_clusters_path, as: :cluster do |field| + = field.text_field :name, required: true, title: s_('ClusterIntegration|Cluster name is required.'), + label: s_('ClusterIntegration|Kubernetes cluster name'), label_class: 'label-bold' - if has_multiple_clusters? - .form-group - = field.label :environment_scope, s_('ClusterIntegration|Environment scope'), class: 'label-bold' - = field.text_field :environment_scope, class: 'form-control', placeholder: s_('ClusterIntegration|Environment scope') + = field.form_group :environment_scope, label: { text: s_('ClusterIntegration|Environment scope'), + class: 'label-bold' } do + = field.text_field :environment_scope, required: true, class: 'form-control', + title: 'Environment scope is required.', wrapper: false .form-text.text-muted= s_("ClusterIntegration|Choose which of your environments will use this cluster.") = field.fields_for :provider_gcp, @gcp_cluster.provider_gcp do |provider_gcp_field| .form-group - = provider_gcp_field.label :gcp_project_id, s_('ClusterIntegration|Google Cloud Platform project'), class: 'label-bold' + = provider_gcp_field.label :gcp_project_id, s_('ClusterIntegration|Google Cloud Platform project'), + class: 'label-bold' .js-gcp-project-id-dropdown-entry-point{ data: { docsUrl: 'https://console.cloud.google.com/home/dashboard' } } = provider_gcp_field.hidden_field :gcp_project_id .dropdown @@ -47,9 +49,9 @@ %p.form-text.text-muted = s_('ClusterIntegration|Learn more about %{help_link_start}zones%{help_link_end}.').html_safe % { help_link_start: help_link_start % { url: zones_link_url }, help_link_end: help_link_end } - .form-group - = provider_gcp_field.label :num_nodes, s_('ClusterIntegration|Number of nodes'), class: 'label-bold' - = provider_gcp_field.text_field :num_nodes, class: 'form-control', placeholder: '3' + = provider_gcp_field.number_field :num_nodes, required: true, placeholder: '3', + title: s_('ClusterIntegration|Number of nodes must be a numerical value.'), + label: s_('ClusterIntegration|Number of nodes'), label_class: 'label-bold' .form-group = provider_gcp_field.label :machine_type, s_('ClusterIntegration|Machine type'), class: 'label-bold' @@ -64,13 +66,14 @@ = s_('ClusterIntegration|Learn more about %{help_link_start_machine_type}machine types%{help_link_end} and %{help_link_start_pricing}pricing%{help_link_end}.').html_safe % { help_link_start_machine_type: help_link_start % { url: machine_type_link_url }, help_link_start_pricing: help_link_start % { url: pricing_link_url }, help_link_end: help_link_end } .form-group - .form-check - = provider_gcp_field.check_box :legacy_abac, { class: 'form-check-input' }, false, true - = provider_gcp_field.label :legacy_abac, s_('ClusterIntegration|RBAC-enabled cluster'), class: 'form-check-label label-bold' - .form-text.text-muted - = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).') - = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.') - = link_to _('More information'), help_page_path('user/project/clusters/index.md', anchor: 'role-based-access-control-rbac-core-only'), target: '_blank' + = provider_gcp_field.check_box :legacy_abac, { label: s_('ClusterIntegration|RBAC-enabled cluster'), + label_class: 'label-bold' }, false, true + .form-text.text-muted + = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).') + = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.') + = link_to _('More information'), help_page_path('user/project/clusters/index.md', + anchor: 'role-based-access-control-rbac-core-only'), target: '_blank' .form-group - = field.submit s_('ClusterIntegration|Create Kubernetes cluster'), class: 'js-gke-cluster-creation-submit btn btn-success', disabled: true + = field.submit s_('ClusterIntegration|Create Kubernetes cluster'), + class: 'js-gke-cluster-creation-submit btn btn-success', disabled: true diff --git a/app/views/clusters/clusters/user/_form.html.haml b/app/views/clusters/clusters/user/_form.html.haml index 136f98d0126..bcd7f00bd7c 100644 --- a/app/views/clusters/clusters/user/_form.html.haml +++ b/app/views/clusters/clusters/user/_form.html.haml @@ -1,40 +1,39 @@ -= form_for @user_cluster, url: clusterable.create_user_clusters_path, as: :cluster do |field| - = form_errors(@user_cluster) - .form-group - = field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-bold' - = field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name') += bootstrap_form_for @user_cluster, html: { class: 'gl-show-field-errors' }, + url: clusterable.create_user_clusters_path, as: :cluster do |field| + = field.text_field :name, required: true, title: s_('ClusterIntegration|Cluster name is required.'), + label: s_('ClusterIntegration|Kubernetes cluster name'), label_class: 'label-bold' - if has_multiple_clusters? - .form-group - = field.label :environment_scope, s_('ClusterIntegration|Environment scope'), class: 'label-bold' - = field.text_field :environment_scope, class: 'form-control', placeholder: s_('ClusterIntegration|Environment scope') - .form-text.text-muted= s_("ClusterIntegration|Choose which of your environments will use this cluster.") + = field.form_group :environment_scope, label: { text: s_('ClusterIntegration|Environment scope'), + class: 'label-bold' } do + = field.text_field :environment_scope, required: true, + title: 'Environment scope is required.', wrapper: false + .form-text.text-muted + = s_("ClusterIntegration|Choose which of your environments will use this cluster.") = field.fields_for :platform_kubernetes, @user_cluster.platform_kubernetes do |platform_kubernetes_field| - .form-group - = platform_kubernetes_field.label :api_url, s_('ClusterIntegration|API URL'), class: 'label-bold' - = platform_kubernetes_field.text_field :api_url, class: 'form-control', placeholder: s_('ClusterIntegration|API URL') - - .form-group - = platform_kubernetes_field.label :ca_cert, s_('ClusterIntegration|CA Certificate'), class: 'label-bold' - = platform_kubernetes_field.text_area :ca_cert, class: 'form-control', placeholder: s_('ClusterIntegration|Certificate Authority bundle (PEM format)') - - .form-group - = platform_kubernetes_field.label :token, s_('ClusterIntegration|Token'), class: 'label-bold' - = platform_kubernetes_field.text_field :token, class: 'form-control', placeholder: s_('ClusterIntegration|Service token'), autocomplete: 'off' + = platform_kubernetes_field.url_field :api_url, required: true, + title: s_('ClusterIntegration|API URL must use http or https protocol.'), + label: s_('ClusterIntegration|API URL'), label_class: 'label-bold' + = platform_kubernetes_field.text_area :ca_cert, + placeholder: s_('ClusterIntegration|Certificate Authority bundle (PEM format)'), + label: s_('ClusterIntegration|CA Certificate'), label_class: 'label-bold' + = platform_kubernetes_field.text_field :token, required: true, + title: s_('ClusterIntegration|Service token is required.'), label: s_('ClusterIntegration|Service Token'), + autocomplete: 'off', label_class: 'label-bold' - if @user_cluster.allow_user_defined_namespace? - .form-group - = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)'), class: 'label-bold' - = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace') + = platform_kubernetes_field.text_field :namespace, + label: s_('ClusterIntegration|Project namespace (optional, unique)'), label_class: 'label-bold' - .form-group - .form-check - = platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input qa-rbac-checkbox' }, 'rbac', 'abac' - = platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster'), class: 'form-check-label label-bold' - .form-text.text-muted - = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).') - = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.') - = link_to _('More information'), help_page_path('user/project/clusters/index.md', anchor: 'role-based-access-control-rbac-core-only'), target: '_blank' + = platform_kubernetes_field.form_group :authorization_type do + = platform_kubernetes_field.check_box :authorization_type, + { class: 'qa-rbac-checkbox', label: s_('ClusterIntegration|RBAC-enabled cluster'), + label_class: 'label-bold', inline: true }, 'rbac', 'abac' + .form-text.text-muted + = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).') + = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.') + = link_to _('More information'), help_page_path('user/project/clusters/index.md', + anchor: 'role-based-access-control-rbac-core-only'), target: '_blank' .form-group = field.submit s_('ClusterIntegration|Add Kubernetes cluster'), class: 'btn btn-success' diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 1e6f04c7815..aac6a6d22be 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -1710,6 +1710,9 @@ msgstr "" msgid "ClusterIntegration|API URL" msgstr "" +msgid "ClusterIntegration|API URL must use http or https protocol." +msgstr "" + msgid "ClusterIntegration|Add Kubernetes cluster" msgstr "" @@ -1770,6 +1773,9 @@ msgstr "" msgid "ClusterIntegration|Choose which of your environments will use this cluster." msgstr "" +msgid "ClusterIntegration|Cluster name is required." +msgstr "" + msgid "ClusterIntegration|Clusters are utilized by selecting the nearest ancestor with a matching environment scope. For example, project clusters will override group clusters." msgstr "" @@ -1983,6 +1989,9 @@ msgstr "" msgid "ClusterIntegration|Number of nodes" msgstr "" +msgid "ClusterIntegration|Number of nodes must be a numerical value." +msgstr "" + msgid "ClusterIntegration|Please enter access information for your Kubernetes cluster. If you need help, you can read our %{link_to_help_page} on Kubernetes" msgstr "" @@ -2064,7 +2073,10 @@ msgstr "" msgid "ClusterIntegration|Select zone to choose machine type" msgstr "" -msgid "ClusterIntegration|Service token" +msgid "ClusterIntegration|Service Token" +msgstr "" + +msgid "ClusterIntegration|Service token is required." msgstr "" msgid "ClusterIntegration|Show" -- GitLab From 119a5f0c86efbc2b3e853623880a1c49f9e7d352 Mon Sep 17 00:00:00 2001 From: Enrique Alcantara Date: Tue, 26 Mar 2019 23:05:50 -0400 Subject: [PATCH 021/154] Specify new validation message selector Update QA selector to specify that API URL is a input type=url field --- qa/qa/page/project/operations/kubernetes/add_existing.rb | 2 +- spec/features/groups/clusters/user_spec.rb | 2 +- spec/features/projects/clusters/gcp_spec.rb | 2 +- spec/features/projects/clusters/user_spec.rb | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/qa/qa/page/project/operations/kubernetes/add_existing.rb b/qa/qa/page/project/operations/kubernetes/add_existing.rb index ffd5b36e1ae..63a230fbdb2 100644 --- a/qa/qa/page/project/operations/kubernetes/add_existing.rb +++ b/qa/qa/page/project/operations/kubernetes/add_existing.rb @@ -6,7 +6,7 @@ module QA class AddExisting < Page::Base view 'app/views/clusters/clusters/user/_form.html.haml' do element :cluster_name, 'text_field :name' # rubocop:disable QA/ElementWithPattern - element :api_url, 'text_field :api_url' # rubocop:disable QA/ElementWithPattern + element :api_url, 'url_field :api_url' # rubocop:disable QA/ElementWithPattern element :ca_certificate, 'text_area :ca_cert' # rubocop:disable QA/ElementWithPattern element :token, 'text_field :token' # rubocop:disable QA/ElementWithPattern element :add_cluster_button, "submit s_('ClusterIntegration|Add Kubernetes cluster')" # rubocop:disable QA/ElementWithPattern diff --git a/spec/features/groups/clusters/user_spec.rb b/spec/features/groups/clusters/user_spec.rb index 2410cd92e3f..b661b5cbaef 100644 --- a/spec/features/groups/clusters/user_spec.rb +++ b/spec/features/groups/clusters/user_spec.rb @@ -69,7 +69,7 @@ describe 'User Cluster', :js do end it 'user sees a validation error' do - expect(page).to have_css('#error_explanation') + expect(page).to have_css('.gl-field-error') end end end diff --git a/spec/features/projects/clusters/gcp_spec.rb b/spec/features/projects/clusters/gcp_spec.rb index 9322e29d744..83e582c34f0 100644 --- a/spec/features/projects/clusters/gcp_spec.rb +++ b/spec/features/projects/clusters/gcp_spec.rb @@ -92,7 +92,7 @@ describe 'Gcp Cluster', :js do end it 'user sees a validation error' do - expect(page).to have_css('#error_explanation') + expect(page).to have_css('.gl-field-error') end end end diff --git a/spec/features/projects/clusters/user_spec.rb b/spec/features/projects/clusters/user_spec.rb index 1f2f7592d8b..fe4f737a7da 100644 --- a/spec/features/projects/clusters/user_spec.rb +++ b/spec/features/projects/clusters/user_spec.rb @@ -53,7 +53,7 @@ describe 'User Cluster', :js do end it 'user sees a validation error' do - expect(page).to have_css('#error_explanation') + expect(page).to have_css('.gl-field-error') end end end -- GitLab From 0a2edb292bba3efe665efc45cdf03e41c346f938 Mon Sep 17 00:00:00 2001 From: Enrique Alcantara Date: Tue, 26 Mar 2019 23:09:10 -0400 Subject: [PATCH 022/154] Changelog entry added --- changelogs/unreleased/57602-create-cluster-validations.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 changelogs/unreleased/57602-create-cluster-validations.yml diff --git a/changelogs/unreleased/57602-create-cluster-validations.yml b/changelogs/unreleased/57602-create-cluster-validations.yml new file mode 100644 index 00000000000..35349c1e9f4 --- /dev/null +++ b/changelogs/unreleased/57602-create-cluster-validations.yml @@ -0,0 +1,5 @@ +--- +title: Display cluster form validation error messages inline +merge_request: 26502 +author: +type: changed -- GitLab From 842203ff3393ec451159c41ac02addac79214da8 Mon Sep 17 00:00:00 2001 From: Enrique Alcantara Date: Mon, 1 Apr 2019 18:40:31 -0400 Subject: [PATCH 023/154] Improve API URL validation message --- app/views/clusters/clusters/user/_form.html.haml | 2 +- locale/gitlab.pot | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/clusters/clusters/user/_form.html.haml b/app/views/clusters/clusters/user/_form.html.haml index bcd7f00bd7c..27b11e8469f 100644 --- a/app/views/clusters/clusters/user/_form.html.haml +++ b/app/views/clusters/clusters/user/_form.html.haml @@ -12,7 +12,7 @@ = field.fields_for :platform_kubernetes, @user_cluster.platform_kubernetes do |platform_kubernetes_field| = platform_kubernetes_field.url_field :api_url, required: true, - title: s_('ClusterIntegration|API URL must use http or https protocol.'), + title: s_('ClusterIntegration|API URL should be a valid http/https url.'), label: s_('ClusterIntegration|API URL'), label_class: 'label-bold' = platform_kubernetes_field.text_area :ca_cert, placeholder: s_('ClusterIntegration|Certificate Authority bundle (PEM format)'), diff --git a/locale/gitlab.pot b/locale/gitlab.pot index aac6a6d22be..48a77b6f257 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -1710,7 +1710,7 @@ msgstr "" msgid "ClusterIntegration|API URL" msgstr "" -msgid "ClusterIntegration|API URL must use http or https protocol." +msgid "ClusterIntegration|API URL should be a valid http/https url." msgstr "" msgid "ClusterIntegration|Add Kubernetes cluster" -- GitLab From 9580c93b829ce53ce51d48ff2495eff7f97e17e4 Mon Sep 17 00:00:00 2001 From: Enrique Alcantara Date: Wed, 3 Apr 2019 10:40:11 -0400 Subject: [PATCH 024/154] Fix cluster details form validation --- .../platforms/kubernetes/_form.html.haml | 89 +++++++++---------- locale/gitlab.pot | 8 +- 2 files changed, 42 insertions(+), 55 deletions(-) diff --git a/app/views/clusters/platforms/kubernetes/_form.html.haml b/app/views/clusters/platforms/kubernetes/_form.html.haml index b5ddca7ccb9..f9f8097cb38 100644 --- a/app/views/clusters/platforms/kubernetes/_form.html.haml +++ b/app/views/clusters/platforms/kubernetes/_form.html.haml @@ -1,58 +1,51 @@ -= form_for cluster, url: update_cluster_url_path, as: :cluster do |field| - = form_errors(cluster) - - .form-group - - if cluster.read_only_kubernetes_platform_fields? - %label.append-bottom-10{ for: 'cluster-name' } - = s_('ClusterIntegration|Kubernetes cluster name') - .input-group - %input.form-control.cluster-name.js-select-on-focus{ value: cluster.name, readonly: true } - %span.input-group-append - = clipboard_button(text: cluster.name, title: s_('ClusterIntegration|Copy Kubernetes cluster name'), class: 'input-group-text btn-default') - - else - = field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-bold' - .input-group - = field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name') += bootstrap_form_for cluster, url: update_cluster_url_path, html: { class: 'gl-show-field-errors' }, + as: :cluster do |field| + - copy_name_btn = clipboard_button(text: cluster.name, title: s_('ClusterIntegration|Copy Kubernetes cluster name'), + class: 'input-group-text btn-default') unless !cluster.read_only_kubernetes_platform_fields? + = field.text_field :name, class: 'js-select-on-focus cluster-name', required: true, + title: s_('ClusterIntegration|Cluster name is required.'), + readonly: cluster.read_only_kubernetes_platform_fields?, + label: s_('ClusterIntegration|Kubernetes cluster name'), label_class: 'label-bold', + input_group_class: 'gl-field-error-anchor', append: copy_name_btn = field.fields_for :platform_kubernetes, platform do |platform_field| - .form-group - = platform_field.label :api_url, s_('ClusterIntegration|API URL') - .input-group - = platform_field.text_field :api_url, class: 'form-control js-select-on-focus', placeholder: s_('ClusterIntegration|API URL'), readonly: cluster.read_only_kubernetes_platform_fields? - - if cluster.read_only_kubernetes_platform_fields? - %span.input-group-append - = clipboard_button(text: platform.api_url, title: s_('ClusterIntegration|Copy API URL'), class: 'input-group-text btn-default') + - copy_api_url = clipboard_button(text: platform.api_url, title: s_('ClusterIntegration|Copy API URL'), + class: 'input-group-text btn-default') unless !cluster.read_only_kubernetes_platform_fields? + = platform_field.text_field :api_url, class: 'js-select-on-focus', required: true, + title: s_('ClusterIntegration|API URL should be a valid http/https url.'), + readonly: cluster.read_only_kubernetes_platform_fields?, + label: s_('ClusterIntegration|API URL'), label_class: 'label-bold', + input_group_class: 'gl-field-error-anchor', append: copy_api_url + + - copy_ca_cert_btn = clipboard_button(text: platform.ca_cert, title: s_('ClusterIntegration|Copy CA Certificate'), + class: 'input-group-text btn-default') unless !cluster.read_only_kubernetes_platform_fields? + = platform_field.text_area :ca_cert, class: 'js-select-on-focus', rows: '5', + readonly: cluster.read_only_kubernetes_platform_fields?, + placeholder: s_('ClusterIntegration|Certificate Authority bundle (PEM format)'), + label: s_('ClusterIntegration|CA Certificate'), label_class: 'label-bold', + input_group_class: 'gl-field-error-anchor', append: copy_ca_cert_btn - .form-group - = platform_field.label :ca_cert, s_('ClusterIntegration|CA Certificate') - .input-group - = platform_field.text_area :ca_cert, class: 'form-control js-select-on-focus', placeholder: s_('ClusterIntegration|Certificate Authority bundle (PEM format)'), readonly: cluster.read_only_kubernetes_platform_fields? - - if cluster.read_only_kubernetes_platform_fields? - %span.input-group-append.clipboard-addon - = clipboard_button(text: platform.ca_cert, title: s_('ClusterIntegration|Copy CA Certificate'), class: 'input-group-text btn-blank') + - show_token_btn = (platform_field.button s_('ClusterIntegration|Show'), + type: 'button', class: 'js-show-cluster-token btn btn-default') + - copy_token_btn = clipboard_button(text: platform.token, title: s_('ClusterIntegration|Copy Service Token'), + class: 'input-group-text btn-default') unless !cluster.read_only_kubernetes_platform_fields? - .form-group - = platform_field.label :token, s_('ClusterIntegration|Token') - .input-group - = platform_field.text_field :token, class: 'form-control js-cluster-token js-select-on-focus', type: 'password', placeholder: s_('ClusterIntegration|Token'), readonly: cluster.read_only_kubernetes_platform_fields? - %span.input-group-append - %button.btn.btn-default.input-group-text.js-show-cluster-token{ type: 'button' } - = s_('ClusterIntegration|Show') - - if cluster.read_only_kubernetes_platform_fields? - = clipboard_button(text: platform.token, title: s_('ClusterIntegration|Copy Token'), class: 'btn-default') + = platform_field.text_field :token, type: 'password', class: 'js-select-on-focus js-cluster-token', + required: true, title: s_('ClusterIntegration|Service token is required.'), + readonly: cluster.read_only_kubernetes_platform_fields?, + label: s_('ClusterIntegration|Service Token'), label_class: 'label-bold', + input_group_class: 'gl-field-error-anchor', append: show_token_btn + copy_token_btn - if cluster.allow_user_defined_namespace? - .form-group - = platform_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)') - = platform_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace') + = platform_field.text_field :namespace, label: s_('ClusterIntegration|Project namespace (optional, unique)'), + label_class: 'label-bold' - .form-group - .form-check - = platform_field.check_box :authorization_type, { class: 'form-check-input', disabled: true }, 'rbac', 'abac' - = platform_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster'), class: 'form-check-label label-bold' - .form-text.text-muted - = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).') - = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.') + = platform_field.form_group :authorization_type do + = platform_field.check_box :authorization_type, { disabled: true, label: s_('ClusterIntegration|RBAC-enabled cluster'), + label_class: 'label-bold', inline: true }, 'rbac', 'abac' + .form-text.text-muted + = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).') + = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.') .form-group = field.submit s_('ClusterIntegration|Save changes'), class: 'btn btn-success' diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 48a77b6f257..b5a3191e20a 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -1797,7 +1797,7 @@ msgstr "" msgid "ClusterIntegration|Copy Kubernetes cluster name" msgstr "" -msgid "ClusterIntegration|Copy Token" +msgid "ClusterIntegration|Copy Service Token" msgstr "" msgid "ClusterIntegration|Create Kubernetes cluster" @@ -2004,9 +2004,6 @@ msgstr "" msgid "ClusterIntegration|Project cluster" msgstr "" -msgid "ClusterIntegration|Project namespace" -msgstr "" - msgid "ClusterIntegration|Project namespace (optional, unique)" msgstr "" @@ -2109,9 +2106,6 @@ msgstr "" msgid "ClusterIntegration|Toggle Kubernetes cluster" msgstr "" -msgid "ClusterIntegration|Token" -msgstr "" - msgid "ClusterIntegration|Update failed. Please check the logs and try again." msgstr "" -- GitLab From 6b7a9b7498a4c91ca9c7324a0e31d8a142fddeef Mon Sep 17 00:00:00 2001 From: Ash McKenzie Date: Thu, 28 Mar 2019 12:33:45 +1100 Subject: [PATCH 025/154] Allow console messages be sent to gitlab-shell Currently a no-op for CE --- lib/api/internal.rb | 3 ++- lib/gitlab/git_access.rb | 6 ++++- lib/gitlab/git_access_result/success.rb | 5 ++++ spec/requests/api/internal_spec.rb | 34 +++++++++++++++++++++++++ 4 files changed, 46 insertions(+), 2 deletions(-) diff --git a/lib/api/internal.rb b/lib/api/internal.rb index cb9aa849eeb..9c7b9146c8f 100644 --- a/lib/api/internal.rb +++ b/lib/api/internal.rb @@ -87,7 +87,8 @@ module API gl_id: Gitlab::GlId.gl_id(user), gl_username: user&.username, git_config_options: [], - gitaly: gitaly_payload(params[:action]) + gitaly: gitaly_payload(params[:action]), + gl_console_messages: check_result.console_messages } # Custom option for git-receive-pack command diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb index 010bd0e520c..cb80ed64eff 100644 --- a/lib/gitlab/git_access.rb +++ b/lib/gitlab/git_access.rb @@ -85,7 +85,7 @@ module Gitlab check_push_access! end - ::Gitlab::GitAccessResult::Success.new + ::Gitlab::GitAccessResult::Success.new(console_messages: check_for_console_messages(cmd)) end def guest_can_download_code? @@ -116,6 +116,10 @@ module Gitlab nil end + def check_for_console_messages(cmd) + [] + end + def check_valid_actor! return unless actor.is_a?(Key) diff --git a/lib/gitlab/git_access_result/success.rb b/lib/gitlab/git_access_result/success.rb index 7bb9f24cb0e..e950d727e2e 100644 --- a/lib/gitlab/git_access_result/success.rb +++ b/lib/gitlab/git_access_result/success.rb @@ -3,6 +3,11 @@ module Gitlab module GitAccessResult class Success + attr_reader :console_messages + + def initialize(console_messages: []) + @console_messages = console_messages + end end end end diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index 537194b8e11..6640ce2b07e 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -498,6 +498,40 @@ describe API::Internal do end end + context "console message" do + before do + project.add_developer(user) + end + + context "git pull" do + context "with no console message" do + it "has the correct payload" do + pull(key, project) + + expect(response).to have_gitlab_http_status(200) + expect(json_response['gl_console_messages']).to eq([]) + end + end + + context "with a console message" do + let(:console_messages) { ['message for the console'] } + + it "has the correct payload" do + expect_next_instance_of(Gitlab::GitAccess) do |access| + expect(access).to receive(:check_for_console_messages) + .with('git-upload-pack') + .and_return(console_messages) + end + + pull(key, project) + + expect(response).to have_gitlab_http_status(200) + expect(json_response['gl_console_messages']).to eq(console_messages) + end + end + end + end + context "blocked user" do let(:personal_project) { create(:project, namespace: user.namespace) } -- GitLab From 97ab8539968d52b709cd61c2dd4229d404903e14 Mon Sep 17 00:00:00 2001 From: Rajat Jain Date: Tue, 2 Apr 2019 12:46:21 +0200 Subject: [PATCH 026/154] [frontend] backport of scoped labels Scoped labels in EE require additional changes in CE code. --- app/assets/javascripts/boards/models/issue.js | 12 +- app/assets/javascripts/labels_select.js | 95 +++++++++++++- .../pages/groups/labels/edit/index.js | 2 +- .../pages/groups/labels/new/index.js | 2 +- .../pages/projects/labels/edit/index.js | 2 +- .../pages/projects/labels/new/index.js | 2 +- .../components/sidebar/labels_select/base.vue | 19 ++- .../sidebar/labels_select/dropdown_button.vue | 12 ++ .../sidebar/labels_select/dropdown_value.vue | 55 ++++++--- .../dropdown_value_regular_label.vue | 35 ++++++ .../dropdown_value_scoped_label.vue | 47 +++++++ app/assets/stylesheets/pages/issuable.scss | 10 ++ app/assets/stylesheets/pages/labels.scss | 36 ++++++ locale/gitlab.pot | 3 + spec/frontend/labels_select_spec.js | 116 ++++++++++++++---- spec/javascripts/boards/issue_spec.js | 2 + .../labels_select/dropdown_button_spec.js | 15 ++- .../dropdown_value_collapsed_spec.js | 20 ++- .../labels_select/dropdown_value_spec.js | 40 +++++- .../sidebar/labels_select/mock_data.js | 7 ++ 20 files changed, 467 insertions(+), 65 deletions(-) create mode 100644 app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_regular_label.vue create mode 100644 app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_scoped_label.vue diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js index dd92d3c8552..2edb6723ada 100644 --- a/app/assets/javascripts/boards/models/issue.js +++ b/app/assets/javascripts/boards/models/issue.js @@ -119,7 +119,17 @@ class ListIssue { } const projectPath = this.project ? this.project.path : ''; - return Vue.http.patch(`${this.path}.json`, data); + return Vue.http.patch(`${this.path}.json`, data).then(({ body = {} } = {}) => { + /** + * Since post implementation of Scoped labels, server can reject + * same key-ed labels. To keep the UI and server Model consistent, + * we're just assigning labels that server echo's back to us when we + * PATCH the said object. + */ + if (body) { + this.labels = body.labels; + } + }); } } diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index cca4927c115..b021dcc1853 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -11,6 +11,7 @@ import CreateLabelDropdown from './create_label'; import flash from './flash'; import ModalStore from './boards/stores/modal_store'; import boardsStore from './boards/stores/boards_store'; +import { isEE } from '~/lib/utils/common_utils'; export default class LabelsSelect { constructor(els, options = {}) { @@ -86,8 +87,9 @@ export default class LabelsSelect { return this.value; }) .get(); + const scopedLabels = $dropdown.data('scopedLabels'); + const scopedLabelsDocumentationLink = $dropdown.data('scopedLabelsDocumentationLink'); const { handleClick } = options; - $sidebarLabelTooltip.tooltip(); if ($dropdown.closest('.dropdown').find('.dropdown-new-label').length) { @@ -132,8 +134,48 @@ export default class LabelsSelect { template = LabelsSelect.getLabelTemplate({ labels: data.labels, issueUpdateURL, + enableScopedLabels: scopedLabels, + scopedLabelsDocumentationLink, }); labelCount = data.labels.length; + + // EE Specific + if (isEE) { + /** + * For Scoped labels, the last label selected with the + * same key will be applied to the current issueable. + * + * If these are the labels - priority::1, priority::2; and if + * we apply them in the same order, only priority::2 will stick + * with the issuable. + * + * In the current dropdown implementation, we keep track of all + * the labels selected via a hidden DOM element. Since a User + * can select priority::1 and priority::2 at the same time, the + * DOM will have 2 hidden input and the dropdown will show both + * the items selected but in reality server only applied + * priority::2. + * + * We find all the labels then find all the labels server accepted + * and then remove the excess ones. + */ + const toRemoveIds = Array.from( + $form.find("input[type='hidden'][name='" + fieldName + "']"), + ) + .map(el => el.value) + .map(Number); + + data.labels.forEach(label => { + const index = toRemoveIds.indexOf(label.id); + toRemoveIds.splice(index, 1); + }); + + toRemoveIds.forEach(id => { + $form + .find("input[type='hidden'][name='" + fieldName + "'][value='" + id + "']") + .remove(); + }); + } } else { template = 'None'; } @@ -358,6 +400,7 @@ export default class LabelsSelect { } else { if (!$dropdown.hasClass('js-filter-bulk-update')) { saveLabelData(); + $dropdown.data('glDropdown').clearMenu(); } } } @@ -471,19 +514,61 @@ export default class LabelsSelect { // so best approach is to use traditional way of // concatenation // see: http://2ality.com/2016/05/template-literal-whitespace.html#joining-arrays - const tpl = _.template( + + const labelTemplate = _.template( [ - '<% _.each(labels, function(label){ %>', '?label_name[]=<%- encodeURIComponent(label.title) %>">', - '', + ' title="<%= tooltipTitleTemplate({ label, isScopedLabel, enableScopedLabels }) %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;">', '<%- label.title %>', '', '', + ].join(''), + ); + + const infoIconTemplate = _.template( + [ + '', + '', + '', + ].join(''), + ); + + const tooltipTitleTemplate = _.template( + [ + '<% if (isScopedLabel(label) && enableScopedLabels) { %>', + "Scoped label", + '
', + '<%- label.description %>', + '<% } else { %>', + '<%- label.description %>', + '<% } %>', + ].join(''), + ); + + const isScopedLabel = label => label.title.indexOf('::') !== -1; + + const tpl = _.template( + [ + '<% _.each(labels, function(label){ %>', + '<% if (isScopedLabel(label) && enableScopedLabels) { %>', + '', + '<%= labelTemplate({ label, issueUpdateURL, isScopedLabel, enableScopedLabels, tooltipTitleTemplate, linkAttrs: \'data-html="true"\' }) %>', + '<%= infoIconTemplate({ label,scopedLabelsDocumentationLink }) %>', + '', + '<% } else { %>', + '<%= labelTemplate({ label, issueUpdateURL, isScopedLabel, enableScopedLabels, tooltipTitleTemplate, linkAttrs: "" }) %>', + '<% } %>', '<% }); %>', ].join(''), ); - return tpl(tplData); + return tpl({ + ...tplData, + labelTemplate, + infoIconTemplate, + tooltipTitleTemplate, + isScopedLabel, + }); } bindEvents() { diff --git a/app/assets/javascripts/pages/groups/labels/edit/index.js b/app/assets/javascripts/pages/groups/labels/edit/index.js index fa81ad914ba..83d6ac9fd14 100644 --- a/app/assets/javascripts/pages/groups/labels/edit/index.js +++ b/app/assets/javascripts/pages/groups/labels/edit/index.js @@ -1,3 +1,3 @@ -import Labels from '~/labels'; +import Labels from 'ee_else_ce/labels'; document.addEventListener('DOMContentLoaded', () => new Labels()); diff --git a/app/assets/javascripts/pages/groups/labels/new/index.js b/app/assets/javascripts/pages/groups/labels/new/index.js index fa81ad914ba..83d6ac9fd14 100644 --- a/app/assets/javascripts/pages/groups/labels/new/index.js +++ b/app/assets/javascripts/pages/groups/labels/new/index.js @@ -1,3 +1,3 @@ -import Labels from '~/labels'; +import Labels from 'ee_else_ce/labels'; document.addEventListener('DOMContentLoaded', () => new Labels()); diff --git a/app/assets/javascripts/pages/projects/labels/edit/index.js b/app/assets/javascripts/pages/projects/labels/edit/index.js index fa81ad914ba..83d6ac9fd14 100644 --- a/app/assets/javascripts/pages/projects/labels/edit/index.js +++ b/app/assets/javascripts/pages/projects/labels/edit/index.js @@ -1,3 +1,3 @@ -import Labels from '~/labels'; +import Labels from 'ee_else_ce/labels'; document.addEventListener('DOMContentLoaded', () => new Labels()); diff --git a/app/assets/javascripts/pages/projects/labels/new/index.js b/app/assets/javascripts/pages/projects/labels/new/index.js index fa81ad914ba..83d6ac9fd14 100644 --- a/app/assets/javascripts/pages/projects/labels/new/index.js +++ b/app/assets/javascripts/pages/projects/labels/new/index.js @@ -1,3 +1,3 @@ -import Labels from '~/labels'; +import Labels from 'ee_else_ce/labels'; document.addEventListener('DOMContentLoaded', () => new Labels()); diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue index f66e81b1e08..9c258c4651f 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue @@ -75,6 +75,16 @@ export default { required: false, default: false, }, + enableScopedLabels: { + type: Boolean, + require: false, + default: false, + }, + scopedLabelsDocumentationLink: { + type: String, + require: false, + default: '#', + }, }, computed: { hiddenInputName() { @@ -123,7 +133,12 @@ export default { @onValueClick="handleCollapsedValueClick" /> - +