diff options
43 files changed, 1281 insertions, 101 deletions
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 4dc4ce543e9..8a6395b42b5 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -47,6 +47,7 @@ const Api = { adminStatisticsPath: '/api/:version/application/statistics', pipelineSinglePath: '/api/:version/projects/:id/pipelines/:pipeline_id', lsifPath: '/api/:version/projects/:id/commits/:commit_id/lsif/info', + environmentsPath: '/api/:version/projects/:id/environments', group(groupId, callback) { const url = Api.buildUrl(Api.groupPath).replace(':id', groupId); @@ -483,6 +484,11 @@ const Api = { return axios.get(url, { params: { path } }); }, + environments(id) { + const url = Api.buildUrl(this.environmentsPath).replace(':id', encodeURIComponent(id)); + return axios.get(url); + }, + buildUrl(url) { return joinPaths(gon.relative_url_root || '', url.replace(':version', gon.api_version)); }, diff --git a/app/assets/javascripts/ci_variable_list/store/actions.js b/app/assets/javascripts/ci_variable_list/store/actions.js new file mode 100644 index 00000000000..f3a629b84ee --- /dev/null +++ b/app/assets/javascripts/ci_variable_list/store/actions.js @@ -0,0 +1,155 @@ +import * as types from './mutation_types'; +import axios from '~/lib/utils/axios_utils'; +import Api from '~/api'; +import createFlash from '~/flash'; +import { __ } from '~/locale'; +import { prepareDataForApi, prepareDataForDisplay, prepareEnvironments } from './utils'; + +export const toggleValues = ({ commit }, valueState) => { + commit(types.TOGGLE_VALUES, valueState); +}; + +export const clearModal = ({ commit }) => { + commit(types.CLEAR_MODAL); +}; + +export const resetEditing = ({ commit, dispatch }) => { + // fetch variables again if modal is being edited and then hidden + // without saving changes, to cover use case of reactivity in the table + dispatch('fetchVariables'); + commit(types.RESET_EDITING); +}; + +export const requestAddVariable = ({ commit }) => { + commit(types.REQUEST_ADD_VARIABLE); +}; + +export const receiveAddVariableSuccess = ({ commit }) => { + commit(types.RECEIVE_ADD_VARIABLE_SUCCESS); +}; + +export const receiveAddVariableError = ({ commit }, error) => { + commit(types.RECEIVE_ADD_VARIABLE_ERROR, error); +}; + +export const addVariable = ({ state, dispatch }) => { + dispatch('requestAddVariable'); + + return axios + .patch(state.endpoint, { + variables_attributes: [prepareDataForApi(state.variable)], + }) + .then(() => { + dispatch('receiveAddVariableSuccess'); + dispatch('fetchVariables'); + }) + .catch(error => { + createFlash(error.response.data[0]); + dispatch('receiveAddVariableError', error); + }); +}; + +export const requestUpdateVariable = ({ commit }) => { + commit(types.REQUEST_UPDATE_VARIABLE); +}; + +export const receiveUpdateVariableSuccess = ({ commit }) => { + commit(types.RECEIVE_UPDATE_VARIABLE_SUCCESS); +}; + +export const receiveUpdateVariableError = ({ commit }, error) => { + commit(types.RECEIVE_UPDATE_VARIABLE_ERROR, error); +}; + +export const updateVariable = ({ state, dispatch }, variable) => { + dispatch('requestUpdateVariable'); + + const updatedVariable = prepareDataForApi(variable); + updatedVariable.secrect_value = updateVariable.value; + + return axios + .patch(state.endpoint, { variables_attributes: [updatedVariable] }) + .then(() => { + dispatch('receiveUpdateVariableSuccess'); + dispatch('fetchVariables'); + }) + .catch(error => { + createFlash(error.response.data[0]); + dispatch('receiveUpdateVariableError', error); + }); +}; + +export const editVariable = ({ commit }, variable) => { + const variableToEdit = variable; + variableToEdit.secret_value = variableToEdit.value; + commit(types.VARIABLE_BEING_EDITED, variableToEdit); +}; + +export const requestVariables = ({ commit }) => { + commit(types.REQUEST_VARIABLES); +}; +export const receiveVariablesSuccess = ({ commit }, variables) => { + commit(types.RECEIVE_VARIABLES_SUCCESS, variables); +}; + +export const fetchVariables = ({ dispatch, state }) => { + dispatch('requestVariables'); + + return axios + .get(state.endpoint) + .then(({ data }) => { + dispatch('receiveVariablesSuccess', prepareDataForDisplay(data.variables)); + }) + .catch(() => { + createFlash(__('There was an error fetching the variables.')); + }); +}; + +export const requestDeleteVariable = ({ commit }) => { + commit(types.REQUEST_DELETE_VARIABLE); +}; + +export const receiveDeleteVariableSuccess = ({ commit }) => { + commit(types.RECEIVE_DELETE_VARIABLE_SUCCESS); +}; + +export const receiveDeleteVariableError = ({ commit }, error) => { + commit(types.RECEIVE_DELETE_VARIABLE_ERROR, error); +}; + +export const deleteVariable = ({ dispatch, state }, variable) => { + dispatch('requestDeleteVariable'); + + const destroy = true; + + return axios + .patch(state.endpoint, { variables_attributes: [prepareDataForApi(variable, destroy)] }) + .then(() => { + dispatch('receiveDeleteVariableSuccess'); + dispatch('fetchVariables'); + }) + .catch(error => { + createFlash(error.response.data[0]); + dispatch('receiveDeleteVariableError', error); + }); +}; + +export const requestEnvironments = ({ commit }) => { + commit(types.REQUEST_ENVIRONMENTS); +}; + +export const receiveEnvironmentsSuccess = ({ commit }, environments) => { + commit(types.RECEIVE_ENVIRONMENTS_SUCCESS, environments); +}; + +export const fetchEnvironments = ({ dispatch, state }) => { + dispatch('requestEnvironments'); + + return Api.environments(state.projectId) + .then(res => { + dispatch('receiveEnvironmentsSuccess', prepareEnvironments(res.data)); + }) + .catch(() => { + createFlash(__('There was an error fetching the environments information.')); + }); +}; diff --git a/app/assets/javascripts/ci_variable_list/store/index.js b/app/assets/javascripts/ci_variable_list/store/index.js new file mode 100644 index 00000000000..db4ba95b3c2 --- /dev/null +++ b/app/assets/javascripts/ci_variable_list/store/index.js @@ -0,0 +1,17 @@ +import Vue from 'vue'; +import Vuex from 'vuex'; +import * as actions from './actions'; +import mutations from './mutations'; +import state from './state'; + +Vue.use(Vuex); + +export default (initialState = {}) => + new Vuex.Store({ + actions, + mutations, + state: { + ...state(), + ...initialState, + }, + }); diff --git a/app/assets/javascripts/ci_variable_list/store/mutation_types.js b/app/assets/javascripts/ci_variable_list/store/mutation_types.js new file mode 100644 index 00000000000..240066d0f22 --- /dev/null +++ b/app/assets/javascripts/ci_variable_list/store/mutation_types.js @@ -0,0 +1,22 @@ +export const TOGGLE_VALUES = 'TOGGLE_VALUES'; +export const VARIABLE_BEING_EDITED = 'VARIABLE_BEING_EDITED'; +export const RESET_EDITING = 'RESET_EDITING'; +export const CLEAR_MODAL = 'CLEAR_MODAL'; + +export const REQUEST_VARIABLES = 'REQUEST_VARIABLES'; +export const RECEIVE_VARIABLES_SUCCESS = 'RECEIVE_VARIABLES_SUCCESS'; + +export const REQUEST_DELETE_VARIABLE = 'REQUEST_DELETE_VARIABLE'; +export const RECEIVE_DELETE_VARIABLE_SUCCESS = 'RECEIVE_DELETE_VARIABLE_SUCCESS'; +export const RECEIVE_DELETE_VARIABLE_ERROR = 'RECEIVE_DELETE_VARIABLE_ERROR'; + +export const REQUEST_ADD_VARIABLE = 'REQUEST_ADD_VARIABLE'; +export const RECEIVE_ADD_VARIABLE_SUCCESS = 'RECEIVE_ADD_VARIABLE_SUCCESS'; +export const RECEIVE_ADD_VARIABLE_ERROR = 'RECEIVE_ADD_VARIABLE_ERROR'; + +export const REQUEST_UPDATE_VARIABLE = 'REQUEST_UPDATE_VARIABLE'; +export const RECEIVE_UPDATE_VARIABLE_SUCCESS = 'RECEIVE_UPDATE_VARIABLE_SUCCESS'; +export const RECEIVE_UPDATE_VARIABLE_ERROR = 'RECEIVE_UPDATE_VARIABLE_ERROR'; + +export const REQUEST_ENVIRONMENTS = 'REQUEST_ENVIRONMENTS'; +export const RECEIVE_ENVIRONMENTS_SUCCESS = 'RECEIVE_ENVIRONMENTS_SUCCESS'; diff --git a/app/assets/javascripts/ci_variable_list/store/mutations.js b/app/assets/javascripts/ci_variable_list/store/mutations.js new file mode 100644 index 00000000000..74e2bcfa2db --- /dev/null +++ b/app/assets/javascripts/ci_variable_list/store/mutations.js @@ -0,0 +1,86 @@ +import * as types from './mutation_types'; +import { __ } from '~/locale'; + +export default { + [types.REQUEST_VARIABLES](state) { + state.isLoading = true; + }, + + [types.RECEIVE_VARIABLES_SUCCESS](state, variables) { + state.isLoading = false; + state.variables = variables; + }, + + [types.REQUEST_DELETE_VARIABLE](state) { + state.isDeleting = true; + }, + + [types.RECEIVE_DELETE_VARIABLE_SUCCESS](state) { + state.isDeleting = false; + }, + + [types.RECEIVE_DELETE_VARIABLE_ERROR](state, error) { + state.isDeleting = false; + state.error = error; + }, + + [types.REQUEST_ADD_VARIABLE](state) { + state.isLoading = true; + }, + + [types.RECEIVE_ADD_VARIABLE_SUCCESS](state) { + state.isLoading = false; + }, + + [types.RECEIVE_ADD_VARIABLE_ERROR](state, error) { + state.isLoading = false; + state.error = error; + }, + + [types.REQUEST_UPDATE_VARIABLE](state) { + state.isLoading = true; + }, + + [types.RECEIVE_UPDATE_VARIABLE_SUCCESS](state) { + state.isLoading = false; + }, + + [types.RECEIVE_UPDATE_VARIABLE_ERROR](state, error) { + state.isLoading = false; + state.error = error; + }, + + [types.TOGGLE_VALUES](state, valueState) { + state.valuesHidden = valueState; + }, + + [types.REQUEST_ENVIRONMENTS](state) { + state.isLoading = true; + }, + + [types.RECEIVE_ENVIRONMENTS_SUCCESS](state, environments) { + state.isLoading = false; + state.environments = environments; + state.environments.unshift(__('All environments')); + }, + + [types.VARIABLE_BEING_EDITED](state, variable) { + state.variableBeingEdited = variable; + }, + + [types.CLEAR_MODAL](state) { + state.variable = { + variable_type: __('Variable'), + key: '', + secret_value: '', + protected: false, + masked: false, + environment_scope: __('All environments'), + }; + }, + + [types.RESET_EDITING](state) { + state.variableBeingEdited = null; + state.showInputValue = false; + }, +}; diff --git a/app/assets/javascripts/ci_variable_list/store/state.js b/app/assets/javascripts/ci_variable_list/store/state.js new file mode 100644 index 00000000000..c5e0bbfdbf4 --- /dev/null +++ b/app/assets/javascripts/ci_variable_list/store/state.js @@ -0,0 +1,24 @@ +import { __ } from '~/locale'; + +export default () => ({ + endpoint: null, + projectId: null, + isGroup: null, + maskableRegex: null, + isLoading: false, + isDeleting: false, + variable: { + variable_type: __('Variable'), + key: '', + secret_value: '', + protected: false, + masked: false, + environment_scope: __('All environments'), + }, + variables: null, + valuesHidden: true, + error: null, + environments: [], + typeOptions: [__('Variable'), __('File')], + variableBeingEdited: null, +}); diff --git a/app/assets/javascripts/ci_variable_list/store/utils.js b/app/assets/javascripts/ci_variable_list/store/utils.js new file mode 100644 index 00000000000..44807e03dad --- /dev/null +++ b/app/assets/javascripts/ci_variable_list/store/utils.js @@ -0,0 +1,44 @@ +import { __ } from '~/locale'; + +const variableType = 'env_var'; +const fileType = 'file'; + +const variableTypeHandler = type => (type === 'Variable' ? variableType : fileType); + +export const prepareDataForDisplay = variables => { + const variablesToDisplay = []; + variables.forEach(variable => { + const variableCopy = variable; + if (variableCopy.variable_type === variableType) { + variableCopy.variable_type = __('Variable'); + } else { + variableCopy.variable_type = __('File'); + } + + if (variableCopy.environment_scope === '*') { + variableCopy.environment_scope = __('All environments'); + } + variablesToDisplay.push(variableCopy); + }); + return variablesToDisplay; +}; + +export const prepareDataForApi = (variable, destroy = false) => { + const variableCopy = variable; + variableCopy.protected.toString(); + variableCopy.masked.toString(); + variableCopy.variable_type = variableTypeHandler(variableCopy.variable_type); + + if (variableCopy.environment_scope === __('All environments')) { + variableCopy.environment_scope = __('*'); + } + + if (destroy) { + // eslint-disable-next-line + variableCopy._destroy = destroy; + } + + return variableCopy; +}; + +export const prepareEnvironments = environments => environments.map(e => e.name); diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 6cd69fe75ce..a1bfa03a5ac 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -102,7 +102,6 @@ padding-bottom: 0.3em; border-bottom: 1px solid $white-dark; color: $gl-text-color; - overflow: hidden; &:first-child { margin-top: 0; @@ -116,7 +115,6 @@ padding-bottom: 0.3em; border-bottom: 1px solid $white-dark; color: $gl-text-color; - overflow: hidden; } h3 { diff --git a/app/workers/admin_email_worker.rb b/app/workers/admin_email_worker.rb index a7cc4fb0d11..9388b1014ac 100644 --- a/app/workers/admin_email_worker.rb +++ b/app/workers/admin_email_worker.rb @@ -7,7 +7,7 @@ class AdminEmailWorker include CronjobQueue # rubocop:enable Scalability/CronWorkerContext - feature_category_not_owned! + feature_category :source_code_management def perform send_repository_check_mail if Gitlab::CurrentSettings.repository_checks_enabled diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index f6daab73689..459872965f8 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -58,7 +58,7 @@ :resource_boundary: :unknown :weight: 1 - :name: cronjob:admin_email - :feature_category: :not_owned + :feature_category: :source_code_management :has_external_dependencies: :latency_sensitive: :resource_boundary: :unknown @@ -88,7 +88,7 @@ :resource_boundary: :unknown :weight: 1 - :name: cronjob:gitlab_usage_ping - :feature_category: :not_owned + :feature_category: :collection :has_external_dependencies: :latency_sensitive: :resource_boundary: :unknown @@ -142,7 +142,7 @@ :resource_boundary: :cpu :weight: 1 - :name: cronjob:prune_old_events - :feature_category: :not_owned + :feature_category: :users :has_external_dependencies: :latency_sensitive: :resource_boundary: :unknown diff --git a/app/workers/gitlab_usage_ping_worker.rb b/app/workers/gitlab_usage_ping_worker.rb index bf0dc0fdd59..002542e26f4 100644 --- a/app/workers/gitlab_usage_ping_worker.rb +++ b/app/workers/gitlab_usage_ping_worker.rb @@ -9,7 +9,7 @@ class GitlabUsagePingWorker include CronjobQueue # rubocop:enable Scalability/CronWorkerContext - feature_category_not_owned! + feature_category :collection # Retry for up to approximately three hours then give up. sidekiq_options retry: 10, dead: false diff --git a/app/workers/prune_old_events_worker.rb b/app/workers/prune_old_events_worker.rb index 835c51ec846..541a17c0fcf 100644 --- a/app/workers/prune_old_events_worker.rb +++ b/app/workers/prune_old_events_worker.rb @@ -7,7 +7,7 @@ class PruneOldEventsWorker include CronjobQueue # rubocop:enable Scalability/CronWorkerContext - feature_category_not_owned! + feature_category :users DELETE_LIMIT = 10_000 diff --git a/changelogs/unreleased/27142-divider-in-readme-is-overlaying-with-image.yml b/changelogs/unreleased/27142-divider-in-readme-is-overlaying-with-image.yml deleted file mode 100644 index dd246a35640..00000000000 --- a/changelogs/unreleased/27142-divider-in-readme-is-overlaying-with-image.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Do not draw heading borders over floated images in markdown -merge_request: -author: Gwen_ -type: fixed diff --git a/doc/administration/packages/container_registry.md b/doc/administration/packages/container_registry.md index 87be9d500fb..26c83ab6034 100644 --- a/doc/administration/packages/container_registry.md +++ b/doc/administration/packages/container_registry.md @@ -619,7 +619,7 @@ provided by `gitlab-ctl`. Consider the following example, where you first build the image: -```bash +```shell # This builds a image with content of sha256:111111 docker build -t my.registry.com/my.group/my.project:latest . docker push my.registry.com/my.group/my.project:latest @@ -627,7 +627,7 @@ docker push my.registry.com/my.group/my.project:latest Now, you do overwrite `:latest` with a new version: -```bash +```shell # This builds a image with content of sha256:222222 docker build -t my.registry.com/my.group/my.project:latest . docker push my.registry.com/my.group/my.project:latest @@ -774,7 +774,7 @@ once a week. Create a file under `/etc/cron.d/registry-garbage-collect`: -```bash +```shell SHELL=/bin/sh PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin diff --git a/doc/administration/server_hooks.md b/doc/administration/server_hooks.md index 4c72634d4ff..37fffcdce22 100644 --- a/doc/administration/server_hooks.md +++ b/doc/administration/server_hooks.md @@ -88,11 +88,10 @@ pattern (`*~`). The hooks are searched and executed in this order: -1. `gitlab-shell/hooks` directory as known to Gitaly. -1. `<project>.git/hooks/<hook_name>` - executed by `git` itself, this is symlinked to `gitlab-shell/hooks/<hook_name>`. +1. Built-in GitLab server hooks (not user-customizable). 1. `<project>.git/custom_hooks/<hook_name>` - per-project hook (this was kept as the already existing behavior). 1. `<project>.git/custom_hooks/<hook_name>.d/*` - per-project hooks. -1. `<project>.git/hooks/<hook_name>.d/*` OR `<custom_hooks_dir>/<hook_name.d>/*` - global hooks: all executable files (except editor backup files). +1. `<custom_hooks_dir>/<hook_name>.d/*` - global hooks: all executable files (except editor backup files). The hooks of the same type are executed in order and execution stops on the first script exiting with a non-zero value. diff --git a/doc/administration/troubleshooting/kubernetes_cheat_sheet.md b/doc/administration/troubleshooting/kubernetes_cheat_sheet.md index 48d415c6bdf..4ffce11aed0 100644 --- a/doc/administration/troubleshooting/kubernetes_cheat_sheet.md +++ b/doc/administration/troubleshooting/kubernetes_cheat_sheet.md @@ -74,7 +74,7 @@ and they will assist you with any issues you are having. - How to get cronjobs configured on a cluster - ```bash + ```shell kubectl get cronjobs ``` diff --git a/doc/api/protected_environments.md b/doc/api/protected_environments.md index 7d4e62a8ff5..852a5ae6e71 100644 --- a/doc/api/protected_environments.md +++ b/doc/api/protected_environments.md @@ -17,7 +17,7 @@ Currently, these levels are recognized: Gets a list of protected environments from a project: -```bash +```shell GET /projects/:id/protected_environments ``` @@ -25,7 +25,7 @@ GET /projects/:id/protected_environments | --------- | ---- | -------- | ----------- | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. | -```bash +```shell curl --header "PRIVATE-TOKEN: <your_access_token>" 'https://gitlab.example.com/api/v4/projects/5/protected_environments/' ``` @@ -51,7 +51,7 @@ Example response: Gets a single protected environment: -```bash +```shell GET /projects/:id/protected_environments/:name ``` @@ -60,7 +60,7 @@ GET /projects/:id/protected_environments/:name | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | | `name` | string | yes | The name of the protected environment | -```bash +```shell curl --header "PRIVATE-TOKEN: <your_access_token>" 'https://gitlab.example.com/api/v4/projects/5/protected_environments/production' ``` @@ -84,11 +84,11 @@ Example response: Protects a single environment: -```bash +```shell POST /projects/:id/protected_environments ``` -```bash +```shell curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" 'https://gitlab.example.com/api/v4/projects/5/protected_environments?name=staging&deploy_access_levels%5B%5D%5Buser_id%5D=1' ``` @@ -122,11 +122,11 @@ Example response: Unprotects the given protected environment: -```bash +```shell DELETE /projects/:id/protected_environments/:name ``` -```bash +```shell curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" 'https://gitlab.example.com/api/v4/projects/5/protected_environments/staging' ``` diff --git a/doc/ci/README.md b/doc/ci/README.md index f25a0ade42a..5206807cf7a 100644 --- a/doc/ci/README.md +++ b/doc/ci/README.md @@ -50,6 +50,7 @@ To get started with GitLab CI/CD, we recommend you read through the following documents: - [How GitLab CI/CD works](introduction/index.md#how-gitlab-cicd-works). +- [Fundamental pipeline architectures](pipelines/pipeline_architectures.md). - [GitLab CI/CD basic workflow](introduction/index.md#basic-cicd-workflow). - [Step-by-step guide for writing `.gitlab-ci.yml` for the first time](../user/project/pages/getting_started_part_four.md). diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md index 621b679de73..08419685388 100644 --- a/doc/ci/docker/using_docker_images.md +++ b/doc/ci/docker/using_docker_images.md @@ -397,7 +397,7 @@ Before the new extended Docker configuration options, you would need to create your own image based on the `super/sql:latest` image, add the default command, and then use it in job's configuration, like: -```Dockerfile +```dockerfile # my-super-sql:latest image's Dockerfile FROM super/sql:latest diff --git a/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md b/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md index f59401c6f87..848808f65ea 100644 --- a/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md +++ b/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md @@ -63,7 +63,7 @@ Next, we'll create a small subset of tests that exemplify most of the states I e this `Weapon` class to go through. To get started, create a folder called `lib/tests` and add the following code to a new file `weaponTests.ts`: -```ts +```typescript import { expect } from 'chai'; import { Weapon, BulletFactory } from '../lib/weapon'; @@ -114,7 +114,7 @@ describe('Weapon', () => { To build and run these tests using gulp, let's also add the following gulp functions to the existing `gulpfile.js` file: -```ts +```typescript gulp.task('build-test', function () { return gulp.src('src/tests/**/*.ts', { read: false }) .pipe(tap(function (file) { @@ -140,7 +140,7 @@ to trigger the weapon. In the `src/lib` folder create a `weapon.ts` file. We'll to it: `Weapon` and `BulletFactory` which will encapsulate Phaser's **sprite** and **group** objects, and the logic specific to our game. -```ts +```typescript export class Weapon { private isTriggered: boolean = false; private currentTimer: number = 0; @@ -210,7 +210,7 @@ export class BulletFactory { Lastly, we'll redo our entry point, `game.ts`, to tie together both `Player` and `Weapon` objects as well as add them to the update loop. Here is what the updated `game.ts` file looks like: -```ts +```typescript import { Player } from "./player"; import { Weapon, BulletFactory } from "./weapon"; diff --git a/doc/ci/pipelines/pipeline_architectures.md b/doc/ci/pipelines/pipeline_architectures.md new file mode 100644 index 00000000000..0e6745a59eb --- /dev/null +++ b/doc/ci/pipelines/pipeline_architectures.md @@ -0,0 +1,269 @@ +--- +type: reference +--- + +# Pipeline Architecture + +Pipelines are the fundamental building blocks for CI/CD in GitLab. This page documents +some of the important concepts related to them. + +There are three main ways to structure your pipelines, each with their +own advantages. These methods can be mixed and matched if needed: + +- [Basic](#basic-pipelines): Good for straightforward projects where all the configuration is in one easy to find place. +- [Directed Acylic Graph](#directed-acyclic-graph-pipelines): Good for large, complex projects that need efficient execution. +- [Child/Parent Pipelines](#child--parent-pipelines): Good for monorepos and projects with lots of independently defined components. + +For more details about +any of the keywords used below, check out our [CI YAML reference](../yaml/) for details. + +## Basic Pipelines + +This is the simplest pipeline in GitLab. It will run everything in the build stage concurrently, +and once all of those finish, it will run everything in the test stage the same way, and so on. +It's not the most efficient, and if you have lots of steps it can grow quite complex, but it's +easier to maintain: + +```mermaid +graph LR + subgraph deploy stage + deploy --> deploy_a + deploy --> deploy_b + end + subgraph test stage + test --> test_a + test --> test_b + end + subgraph build stage + build --> build_a + build --> build_b + end + build_a -.-> test + build_b -.-> test + test_a -.-> deploy + test_b -.-> deploy +``` + +Example basic `/.gitlab-ci.yml` pipeline configuration matching the diagram: + +```yaml +stages: + - build + - test + - deploy + +image: alpine + +build_a: + stage: build + script: + - echo "This job builds something." + +build_b: + stage: build + script: + - echo "This job builds something else." + +test_a: + stage: test + script: + - echo "This job tests something. It will only run when all jobs in the" + - echo "build stage are complete." + +test_b: + stage: test + script: + - echo "This job tests something else. It will only run when all jobs in the" + - echo "build stage are complete too. It will start at about the same time as test_a." + +deploy_a: + stage: deploy + script: + - echo "This job deploys something. It will only run when all jobs in the" + - echo "test stage complete." + +deploy_b: + stage: deploy + script: + - echo "This job deploys something else. It will only run when all jobs in the" + - echo "test stage complete. It will start at about the same time as deploy_a." +``` + +## Directed Acyclic Graph Pipelines + +If efficiency is important to you and you want everything to run as quickly as possible, +you can use [Directed Acylic Graphs (DAG)](../directed_acyclic_graph/index.md). Use the +[`needs` keyword](../yaml/README.md#needs) to define dependency relationships between +your jobs. When GitLab knows the relationships between your jobs, it can run everything +as fast as possible, and even skips into subsequent stages when possible. + +In the example below, if `build_a` and `test_a` are much faster than `build_b` and +`test_b`, GitLab will start `deploy_a` even if `build_b` is still running. + +```mermaid +graph LR + subgraph Pipeline using DAG + build_a --> test_a --> deploy_a + build_b --> test_b --> deploy_b + end +``` + +Example DAG `/.gitlab-ci.yml` configuration matching the diagram: + +```yaml +stages: + - build + - test + - deploy + +image: alpine + +build_a: + stage: build + script: + - echo "This job builds something quickly." + +build_b: + stage: build + script: + - echo "This job builds something else slowly." + +test_a: + stage: test + needs: build_a + script: + - echo "This test job will start as soon as build_a finishes." + - echo "It will not wait for build_b, or other jobs in the build stage, to finish." + +test_b: + stage: test + needs: build_b + script: + - echo "This test job will start as soon as build_b finishes." + - echo "It will not wait for other jobs in the build stage to finish." + +deploy_a: + stage: deploy + needs: test_a + script: + - echo "Since build_a and test_a run quickly, this deploy job can run much earlier." + - echo "It does not need to wait for build_b or test_b." + +deploy_b: + stage: deploy + needs: test_b + script: + - echo "Since build_b and test_b run slowly, this deploy job will run much later." +``` + +## Child / Parent Pipelines + +In the examples above, it's clear we've got two types of things that could be built independently. +This is an ideal case for using [Child / Parent Pipelines](../parent_child_pipelines.md)) via +the [`trigger` keyword](../yaml/README.md#trigger). It will separate out the configuration +into multiple files, keeping things very simple. You can also combine this with: + +- The [`rules` keyword](../yaml/README.md#rules): For example, have the child pipelines triggered only + when there are changes to that area. +- The [`include` keyword](../yaml/README.md#include): Bring in common behaviors, ensuring + you are not repeating yourself. +- [DAG pipelines](#directed-acyclic-graph-pipelines) inside of child pipelines, achieving the benefits of both. + +```mermaid +graph LR + subgraph Parent pipeline + trigger_a -.-> build_a + trigger_b -.-> build_b + subgraph child pipeline B + build_b --> test_b --> deploy_b + end + + subgraph child pipeline A + build_a --> test_a --> deploy_a + end + end +``` + +Example `/.gitlab-ci.yml` configuration for the parent pipeline matching the diagram: + +```yaml +stages: + - triggers + +trigger_a: + stage: triggers + trigger: + include: a/.gitlab-ci.yml + rules: + - changes: + - a/* + +trigger_b: + stage: triggers + trigger: + include: b/.gitlab-ci.yml + rules: + - changes: + - b/* +``` + +Example child `a` pipeline configuration, located in `/a/.gitlab-ci.yml`, making +use of the DAG `needs:` keyword: + +```yaml +stages: + - build + - test + - deploy + +image: alpine + +build_a: + stage: build + script: + - echo "This job builds something." + +test_a: + stage: test + needs: build_a + script: + - echo "This job tests something." + +deploy_a: + stage: deploy + needs: test_a + script: + - echo "This job deploys something." +``` + +Example child `b` pipeline configuration, located in `/b/.gitlab-ci.yml`, making +use of the DAG `needs:` keyword: + +```yaml +stages: + - build + - test + - deploy + +image: alpine + +build_b: + stage: build + script: + - echo "This job builds something else." + +test_b: + stage: test + needs: build_b + script: + - echo "This job tests something else." + +deploy_b: + stage: deploy + needs: test_b + script: + - echo "This job deploys something else." +``` + +It's also possible to set jobs to run before or after triggering child pipelines, +for example if you have common setup steps or a unified deployment at the end. diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md index 6de3eaf8831..83b3ec44314 100644 --- a/doc/ci/quick_start/README.md +++ b/doc/ci/quick_start/README.md @@ -17,6 +17,11 @@ NOTE: **Note:** Coming over to GitLab from Jenkins? Check out our [reference](../jenkins/index.md) for converting your pre-existing pipelines over to our format. +NOTE: **Note:** +There are a few different [basic pipeline architectures](../pipelines/pipeline_architectures.md) +that you can consider for use in your project. You may want to familiarize +yourself with these prior to getting started. + GitLab offers a [continuous integration](https://about.gitlab.com/stages-devops-lifecycle/continuous-integration/) service. For each commit or push to trigger your CI [pipeline](../pipelines.md), you must: diff --git a/doc/ci/services/postgres.md b/doc/ci/services/postgres.md index a137f10949f..235f237ba94 100644 --- a/doc/ci/services/postgres.md +++ b/doc/ci/services/postgres.md @@ -17,12 +17,12 @@ First, in your `.gitlab-ci.yml` add: ```yaml services: - - postgres:latest + - postgres:12.2-alpine variables: POSTGRES_DB: nice_marmot POSTGRES_USER: runner - POSTGRES_PASSWORD: "" + POSTGRES_PASSWORD: "runner-password" ``` NOTE: **Note:** @@ -37,7 +37,7 @@ And then configure your application to use the database, for example: ```yaml Host: postgres User: runner -Password: +Password: runner-password Database: nice_marmot ``` diff --git a/doc/development/code_comments.md b/doc/development/code_comments.md index c1d58c1bd4b..a71e2b3c792 100644 --- a/doc/development/code_comments.md +++ b/doc/development/code_comments.md @@ -7,7 +7,7 @@ check if a comment is still relevant and what needs to be done to address it. Examples: -```rb +```ruby # Deprecated scope until code_owner column has been migrated to rule_type. # To be removed with https://gitlab.com/gitlab-org/gitlab/issues/11834. scope :code_owner, -> { where(code_owner: true).or(where(rule_type: :code_owner)) } diff --git a/doc/development/creating_enums.md b/doc/development/creating_enums.md index 64385a2ea79..79ed465b121 100644 --- a/doc/development/creating_enums.md +++ b/doc/development/creating_enums.md @@ -8,7 +8,7 @@ To use this type, add `limit: 2` to the migration that creates the column. Example: -```rb +```ruby def change add_column :ci_job_artifacts, :file_format, :integer, limit: 2 end diff --git a/doc/development/documentation/styleguide.md b/doc/development/documentation/styleguide.md index e2f84e1200e..de7a437a1d6 100644 --- a/doc/development/documentation/styleguide.md +++ b/doc/development/documentation/styleguide.md @@ -783,33 +783,64 @@ nicely on different mobile devices. - When providing a shell command and its output, prefix the shell command with `$` and leave a blank line between the command and the output. - When providing a command without output, don't prefix the shell command with `$`. +- If you need to include triple backticks inside a code block, use four backticks + for the codeblock fences instead of three. - For regular code blocks, always use a highlighting class corresponding to the language for better readability. Examples: - ~~~md + ````markdown ```ruby Ruby code ``` - ```js + ```javascript JavaScript code ``` - ```md + ```markdown [Markdown code example](example.md) ``` - ```text + ```plaintext Code or text for which no specific highlighting class is available. ``` - ~~~ - -- To display raw Markdown instead of rendered Markdown, you can use triple backticks - with `md`, like the `Markdown code` example above, unless you want to include triple - backticks in the code block as well. In that case, use triple tildes (`~~~`) instead. -- [Syntax highlighting for code blocks](https://github.com/rouge-ruby/rouge/wiki/List-of-supported-languages-and-lexers) - is available for many languages. Use `shell` instead of `bash` or `sh` for shell output. -- For a complete reference on code blocks, check the [Kramdown guide](https://about.gitlab.com/handbook/product/technical-writing/markdown-guide/#code-blocks). + ```` + +Syntax highlighting is required for code blocks added to the GitLab documentation. +Refer to the table below for the most common language classes, or check the +[complete list](https://github.com/rouge-ruby/rouge/wiki/List-of-supported-languages-and-lexers) +of language classes available. + +| Preferred language tags | Language aliases and notes | +|-------------------------|------------------------------------------------------------------------------| +| `asciidoc` | | +| `dockerfile` | Alias: `docker`. | +| `elixir` | | +| `erb` | | +| `golang` | Alias: `go`. | +| `graphql` | | +| `haml` | | +| `html` | | +| `ini` | For some simple config files that are not in TOML format. | +| `javascript` | Alias `js`. | +| `json` | | +| `markdown` | Alias: `md`. | +| `mermaid` | | +| `nginx` | | +| `perl` | | +| `php` | | +| `plaintext` | Examples with no defined language, such as output from shell commands or API calls. If a codeblock has no language, it defaults to `plaintext`. Alias: `text`. | +| `prometheus` | Prometheus configuration examples. | +| `python` | | +| `ruby` | Alias: `rb`. | +| `shell` | Aliases: `bash` or `sh`. | +| `sql` | | +| `toml` | Runner configuration examples, and other toml formatted configuration files. | +| `typescript` | Alias: `ts`. | +| `xml` | | +| `yaml` | Alias: `yml`. | + +For a complete reference on code blocks, check the [Kramdown guide](https://about.gitlab.com/handbook/product/technical-writing/markdown-guide/#code-blocks). ## GitLab SVG icons diff --git a/doc/development/gitaly.md b/doc/development/gitaly.md index 32017a284d5..b275a265cc6 100644 --- a/doc/development/gitaly.md +++ b/doc/development/gitaly.md @@ -240,13 +240,13 @@ Here are the steps to gate a new feature in Gitaly behind a feature flag. 1. Create a package scoped flag name: - ```go + ```golang var findAllTagsFeatureFlag = "go-find-all-tags" ``` 1. Create a switch in the code using the `featureflag` package: - ```go + ```golang if featureflag.IsEnabled(ctx, findAllTagsFeatureFlag) { // go implementation } else { @@ -256,7 +256,7 @@ Here are the steps to gate a new feature in Gitaly behind a feature flag. 1. Create Prometheus metrics: - ```go + ```golang var findAllTagsRequests = prometheus.NewCounterVec( prometheus.CounterOpts{ Name: "gitaly_find_all_tags_requests_total", @@ -280,7 +280,7 @@ Here are the steps to gate a new feature in Gitaly behind a feature flag. 1. Set headers in tests: - ```go + ```golang import ( "google.golang.org/grpc/metadata" diff --git a/doc/development/go_guide/index.md b/doc/development/go_guide/index.md index 73a1dd8ad8a..ae215026f56 100644 --- a/doc/development/go_guide/index.md +++ b/doc/development/go_guide/index.md @@ -195,7 +195,7 @@ When comparing expected and actual values in tests, use and others to improve readability when comparing structs, errors, large portions of text, or JSON documents: -```go +```golang type TestData struct { // ... } diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md index dcd822705f9..aa210f3550f 100644 --- a/doc/topics/autodevops/index.md +++ b/doc/topics/autodevops/index.md @@ -918,12 +918,12 @@ instead of the default `ruby:latest`: 1. Set `AUTO_DEVOPS_BUILD_IMAGE_EXTRA_ARGS` to `--build-arg=RUBY_VERSION=alpine`. 1. Add the following to a custom `Dockerfile`: - ```docker - ARG RUBY_VERSION=latest - FROM ruby:$RUBY_VERSION + ```dockerfile + ARG RUBY_VERSION=latest + FROM ruby:$RUBY_VERSION - # ... put your stuff here - ``` + # ... put your stuff here + ``` NOTE: **Note:** Passing in complex values (newlines and spaces, for example) will likely @@ -955,14 +955,14 @@ In projects: 1. Activate the experimental `Dockerfile` syntax by adding the following to the top of the file: - ```docker + ```dockerfile # syntax = docker/dockerfile:experimental ``` 1. To make secrets available in any `RUN $COMMAND` in the `Dockerfile`, mount the secret file and source it prior to running `$COMMAND`: - ```docker + ```dockerfile RUN --mount=type=secret,id=auto-devops-build-secrets . /run/secrets/auto-devops-build-secrets && $COMMAND ``` diff --git a/doc/topics/web_application_firewall/quick_start_guide.md b/doc/topics/web_application_firewall/quick_start_guide.md index e3cf0bcd498..96c2da4e963 100644 --- a/doc/topics/web_application_firewall/quick_start_guide.md +++ b/doc/topics/web_application_firewall/quick_start_guide.md @@ -80,11 +80,11 @@ under which this application will be deployed. ![Google auth](../autodevops/img/guide_google_auth_v12_3.png) 1. The last step is to provide the cluster details. - 1. Give it a name, leave the environment scope as is, and choose the GCP project under which the cluster - will be created (per the instructions to [configure your Google account](#configuring-your-google-account), a project should have already been created for you). - 1. Choose the [region/zone](https://cloud.google.com/compute/docs/regions-zones/) under which the cluster will be created. - 1. Enter the number of nodes you want it to have. - 1. Choose the [machine type](https://cloud.google.com/compute/docs/machine-types). + 1. Give it a name, leave the environment scope as is, and choose the GCP project under which the cluster + will be created (per the instructions to [configure your Google account](#configuring-your-google-account), a project should have already been created for you). + 1. Choose the [region/zone](https://cloud.google.com/compute/docs/regions-zones/) under which the cluster will be created. + 1. Enter the number of nodes you want it to have. + 1. Choose the [machine type](https://cloud.google.com/compute/docs/machine-types). ![GitLab GKE cluster details](../autodevops/img/guide_gitlab_gke_details_v12_3.png) @@ -180,40 +180,40 @@ your cluster either using [Cloud Shell](https://cloud.google.com/shell/) or the 1. After connecting to your cluster, check if the Ingress-NGINX controller is running and ModSecurity is enabled. - This is done by running the following commands: + This is done by running the following commands: - ```bash - $ kubectl get pods -n gitlab-managed-apps | grep 'ingress-controller' - ingress-nginx-ingress-controller-55f9cf6584-dxljn 2/2 Running + ```shell + $ kubectl get pods -n gitlab-managed-apps | grep 'ingress-controller' + ingress-nginx-ingress-controller-55f9cf6584-dxljn 2/2 Running - $ kubectl -n gitlab-managed-apps exec -it $(kubectl get pods -n gitlab-managed-apps | grep 'ingress-controller' | awk '{print $1}') -- cat /etc/nginx/nginx.conf | grep 'modsecurity on;' - modsecurity on; - ``` + $ kubectl -n gitlab-managed-apps exec -it $(kubectl get pods -n gitlab-managed-apps | grep 'ingress-controller' | awk '{print $1}') -- cat /etc/nginx/nginx.conf | grep 'modsecurity on;' + modsecurity on; + ``` 1. Verify the Rails application has been installed properly. - ```bash - $ kubectl get ns - auto-devv-2-16730183-production Active + ```shell + $ kubectl get ns + auto-devv-2-16730183-production Active - $ kubectl get pods -n auto-devv-2-16730183-production - NAME READY STATUS RESTARTS - production-5778cfcfcd-nqjcm 1/1 Running 0 - production-postgres-6449f8cc98-r7xgg 1/1 Running 0 - ``` + $ kubectl get pods -n auto-devv-2-16730183-production + NAME READY STATUS RESTARTS + production-5778cfcfcd-nqjcm 1/1 Running 0 + production-postgres-6449f8cc98-r7xgg 1/1 Running 0 + ``` 1. To make sure the Rails application is responding, send a request to it by running: - ```bash - $ kubectl get ing -n auto-devv-2-16730183-production - NAME HOSTS PORTS - production-auto-deploy fjdiaz-auto-devv-2.34.68.60.207.nip.io,le-16730183.34.68.60.207.nip.io 80, 443 + ```shell + $ kubectl get ing -n auto-devv-2-16730183-production + NAME HOSTS PORTS + production-auto-deploy fjdiaz-auto-devv-2.34.68.60.207.nip.io,le-16730183.34.68.60.207.nip.io 80, 443 - $ curl --location --insecure fjdiaz-auto-devv-2.34.68.60.207.nip.io | grep 'Rails!' --after 2 --before 2 - <body> - <p>You're on Rails!</p> - </body> - ``` + $ curl --location --insecure fjdiaz-auto-devv-2.34.68.60.207.nip.io | grep 'Rails!' --after 2 --before 2 + <body> + <p>You're on Rails!</p> + </body> + ``` Now that we have confirmed our system is properly setup, we can go ahead and test the WAF with OWASP CRS! @@ -223,7 +223,7 @@ the WAF with OWASP CRS! Now let's send a potentially malicious request, as if we were a scanner, checking for vulnerabilities within our application and examine the modsecurity logs: -```bash +```shell $ curl --location --insecure fjdiaz-auto-devv-2.34.68.60.207.nip.io --header "User-Agent: absinthe" | grep 'Rails!' --after 2 --before 2 <body> <p>You're on Rails!</p> diff --git a/doc/user/project/integrations/img/prometheus_dashboard_environments_v12_8.png b/doc/user/project/integrations/img/prometheus_dashboard_environments_v12_8.png Binary files differnew file mode 100644 index 00000000000..467deb86881 --- /dev/null +++ b/doc/user/project/integrations/img/prometheus_dashboard_environments_v12_8.png diff --git a/doc/user/project/integrations/img/prometheus_monitoring_dashboard_v12_8.png b/doc/user/project/integrations/img/prometheus_monitoring_dashboard_v12_8.png Binary files differnew file mode 100644 index 00000000000..8899852ed04 --- /dev/null +++ b/doc/user/project/integrations/img/prometheus_monitoring_dashboard_v12_8.png diff --git a/doc/user/project/integrations/prometheus.md b/doc/user/project/integrations/prometheus.md index e703f15b4d3..2f6c77c8217 100644 --- a/doc/user/project/integrations/prometheus.md +++ b/doc/user/project/integrations/prometheus.md @@ -55,6 +55,17 @@ will help you to quickly create a deployment: 1. Navigate to your project's **CI/CD > Pipelines** page, and run a pipeline on any branch. 1. When the pipeline has run successfully, graphs will be available on the **Operations > Metrics** page. +![Monitoring Dashboard](img/prometheus_monitoring_dashboard_v12_8.png) + +#### Using the Metrics Dashboard + +##### Select an environment + +The **Environment** dropdown box above the dashboard displays the list of all [environments](#monitoring-cicd-environments). +It enables you to search as you type through all environments and select the one you're looking for. + +![Monitoring Dashboard Environments](img/prometheus_dashboard_environments_v12_8.png) + #### About managed Prometheus deployments Prometheus is deployed into the `gitlab-managed-apps` namespace, using the [official Helm chart](https://github.com/helm/charts/tree/master/stable/prometheus). Prometheus is only accessible within the cluster, with GitLab communicating through the [Kubernetes API](https://kubernetes.io/docs/concepts/overview/kubernetes-api/). @@ -428,6 +439,29 @@ Note the following properties: ![single stat panel type](img/prometheus_dashboard_single_stat_panel_type.png) +###### Percentile based results + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/201946) in GitLab 12.8. + +Query results sometimes need to be represented as a percentage value out of 100. You can use the `max_value` property at the root of the panel definition: + +```yaml +dashboard: 'Dashboard Title' +panel_groups: + - group: 'Group Title' + panels: + - title: "Single Stat" + type: "single-stat" + max_value: 100 + metrics: + - id: 10 + query: 'max(go_memstats_alloc_bytes{job="prometheus"})' + unit: '%' + label: "Total" +``` + +For example, if you have a query value of `53.6`, adding `%` as the unit results in a single stat value of `53.6%`, but if the maximum expected value of the query is `120`, the value would be `44.6%`. Adding the `max_value` causes the correct percentage value to display. + ##### Heatmaps > [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/30581) in GitLab 12.5. diff --git a/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml index c3ca44eea9e..20063cf6a69 100644 --- a/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml @@ -1,12 +1,10 @@ performance: stage: performance - # pin to a version matching the dind service, just to be safe image: docker:19.03.5 allow_failure: true variables: DOCKER_TLS_CERTDIR: "" services: - # pin to a known working version until https://gitlab.com/gitlab-org/gitlab-runner/issues/6697 is fixed - docker:19.03.5-dind script: - | diff --git a/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml index 488945ffa3e..bb0de9df8bf 100644 --- a/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml @@ -4,7 +4,6 @@ build: variables: DOCKER_TLS_CERTDIR: "" services: - # pin to a known working version until https://gitlab.com/gitlab-org/gitlab-runner/issues/6697 is fixed - docker:19.03.5-dind script: - | diff --git a/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml index dd5144e28a7..5b8d41bbd10 100644 --- a/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml @@ -1,10 +1,8 @@ code_quality: stage: test - # pin to a version matching the dind service, just to be safe image: docker:19.03.5 allow_failure: true services: - # pin to a known working version until https://gitlab.com/gitlab-org/gitlab-runner/issues/6697 is fixed - docker:19.03.5-dind variables: DOCKER_DRIVER: overlay2 diff --git a/lib/quality/kubernetes_client.rb b/lib/quality/kubernetes_client.rb index 453b9d21adb..f83652e117f 100644 --- a/lib/quality/kubernetes_client.rb +++ b/lib/quality/kubernetes_client.rb @@ -48,7 +48,8 @@ module Quality resource_names = raw_resource_names command = [ 'delete', - %(--namespace "#{namespace}") + %(--namespace "#{namespace}"), + '--ignore-not-found' ] Array(release_name).each do |release| diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 70945366c6c..b492921680b 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -534,6 +534,9 @@ msgstr "" msgid "(removed)" msgstr "" +msgid "*" +msgstr "" + msgid "+ %{amount} more" msgstr "" @@ -1544,6 +1547,9 @@ msgstr "" msgid "All email addresses will be used to identify your commits." msgstr "" +msgid "All environments" +msgstr "" + msgid "All features are enabled for blank projects, from templates, or when importing, but you can disable them afterward in the project settings." msgstr "" @@ -8385,6 +8391,9 @@ msgstr "" msgid "Fetching licenses failed. You are not permitted to perform this action." msgstr "" +msgid "File" +msgstr "" + msgid "File Hooks" msgstr "" @@ -19425,6 +19434,12 @@ msgstr "" msgid "There was an error fetching the Designs" msgstr "" +msgid "There was an error fetching the environments information." +msgstr "" + +msgid "There was an error fetching the variables." +msgstr "" + msgid "There was an error fetching value stream analytics stages." msgstr "" @@ -21367,6 +21382,9 @@ msgstr "" msgid "Value Stream Analytics gives an overview of how much time it takes to go from idea to production in your project." msgstr "" +msgid "Variable" +msgstr "" + msgid "Variables" msgstr "" diff --git a/spec/frontend/ci_variable_list/services/mock_data.js b/spec/frontend/ci_variable_list/services/mock_data.js new file mode 100644 index 00000000000..89473b57af9 --- /dev/null +++ b/spec/frontend/ci_variable_list/services/mock_data.js @@ -0,0 +1,90 @@ +export default { + mockVariables: [ + { + environment_scope: 'All environments', + id: 113, + key: 'test_var', + masked: false, + protected: false, + value: 'test_val', + variable_type: 'Variable', + }, + { + environment_scope: 'All environments', + id: 114, + key: 'test_var_2', + masked: false, + protected: false, + value: 'test_val_2', + variable_type: 'Variable', + }, + { + environment_scope: 'All environments', + id: 115, + key: 'test_var_3', + masked: false, + protected: false, + value: 'test_val_3', + variable_type: 'Variable', + }, + ], + + mockVariablesApi: [ + { + environment_scope: '*', + id: 113, + key: 'test_var', + masked: false, + protected: false, + value: 'test_val', + variable_type: 'env_var', + }, + { + environment_scope: '*', + id: 114, + key: 'test_var_2', + masked: false, + protected: false, + value: 'test_val_2', + variable_type: 'file', + }, + ], + + mockVariablesDisplay: [ + { + environment_scope: 'All environments', + id: 113, + key: 'test_var', + masked: false, + protected: false, + value: 'test_val', + variable_type: 'Variable', + }, + { + environment_scope: 'All environments', + id: 114, + key: 'test_var_2', + masked: false, + protected: false, + value: 'test_val_2', + variable_type: 'File', + }, + ], + + mockEnvironments: [ + { + id: 28, + name: 'staging', + slug: 'staging', + external_url: 'https://staging.example.com', + state: 'available', + }, + { + id: 29, + name: 'production', + slug: 'production', + external_url: 'https://production.example.com', + state: 'available', + }, + ], +}; diff --git a/spec/frontend/ci_variable_list/store/actions_spec.js b/spec/frontend/ci_variable_list/store/actions_spec.js new file mode 100644 index 00000000000..84455612f0c --- /dev/null +++ b/spec/frontend/ci_variable_list/store/actions_spec.js @@ -0,0 +1,279 @@ +import Api from '~/api'; +import MockAdapter from 'axios-mock-adapter'; +import testAction from 'helpers/vuex_action_helper'; +import axios from '~/lib/utils/axios_utils'; +import createFlash from '~/flash'; +import getInitialState from '~/ci_variable_list/store/state'; +import * as actions from '~/ci_variable_list/store/actions'; +import * as types from '~/ci_variable_list/store/mutation_types'; +import mockData from '../services/mock_data'; +import { prepareDataForDisplay, prepareEnvironments } from '~/ci_variable_list/store/utils'; + +jest.mock('~/api.js'); +jest.mock('~/flash.js'); + +describe('CI variable list store actions', () => { + let mock; + let state; + const mockVariable = { + environment_scope: '*', + id: 63, + key: 'test_var', + masked: false, + protected: false, + value: 'test_val', + variable_type: 'env_var', + _destory: true, + }; + const payloadError = new Error('Request failed with status code 500'); + + beforeEach(() => { + mock = new MockAdapter(axios); + state = getInitialState(); + state.endpoint = '/variables'; + }); + + afterEach(() => { + mock.restore(); + }); + + describe('toggleValues', () => { + const valuesHidden = false; + it('commits TOGGLE_VALUES mutation', () => { + testAction(actions.toggleValues, valuesHidden, {}, [ + { + type: types.TOGGLE_VALUES, + payload: valuesHidden, + }, + ]); + }); + }); + + describe('clearModal', () => { + it('commits CLEAR_MODAL mutation', () => { + testAction(actions.clearModal, {}, {}, [ + { + type: types.CLEAR_MODAL, + }, + ]); + }); + }); + + describe('resetEditing', () => { + it('commits RESET_EDITING mutation', () => { + testAction( + actions.resetEditing, + {}, + {}, + [ + { + type: types.RESET_EDITING, + }, + ], + [{ type: 'fetchVariables' }], + ); + }); + }); + + describe('deleteVariable', () => { + it('dispatch correct actions on successful deleted variable', done => { + mock.onPatch(state.endpoint).reply(200); + + testAction( + actions.deleteVariable, + mockVariable, + state, + [], + [ + { type: 'requestDeleteVariable' }, + { type: 'receiveDeleteVariableSuccess' }, + { type: 'fetchVariables' }, + ], + () => { + done(); + }, + ); + }); + + it('should show flash error and set error in state on delete failure', done => { + mock.onPatch(state.endpoint).reply(500, ''); + + testAction( + actions.deleteVariable, + mockVariable, + state, + [], + [ + { type: 'requestDeleteVariable' }, + { + type: 'receiveDeleteVariableError', + payload: payloadError, + }, + ], + () => { + expect(createFlash).toHaveBeenCalled(); + done(); + }, + ); + }); + }); + + describe('updateVariable', () => { + it('dispatch correct actions on successful updated variable', done => { + mock.onPatch(state.endpoint).reply(200); + + testAction( + actions.updateVariable, + mockVariable, + state, + [], + [ + { type: 'requestUpdateVariable' }, + { type: 'receiveUpdateVariableSuccess' }, + { type: 'fetchVariables' }, + ], + () => { + done(); + }, + ); + }); + + it('should show flash error and set error in state on update failure', done => { + mock.onPatch(state.endpoint).reply(500, ''); + + testAction( + actions.updateVariable, + mockVariable, + state, + [], + [ + { type: 'requestUpdateVariable' }, + { + type: 'receiveUpdateVariableError', + payload: payloadError, + }, + ], + () => { + expect(createFlash).toHaveBeenCalled(); + done(); + }, + ); + }); + }); + + describe('addVariable', () => { + it('dispatch correct actions on successful added variable', done => { + mock.onPatch(state.endpoint).reply(200); + + testAction( + actions.addVariable, + {}, + state, + [], + [ + { type: 'requestAddVariable' }, + { type: 'receiveAddVariableSuccess' }, + { type: 'fetchVariables' }, + ], + () => { + done(); + }, + ); + }); + + it('should show flash error and set error in state on add failure', done => { + mock.onPatch(state.endpoint).reply(500, ''); + + testAction( + actions.addVariable, + {}, + state, + [], + [ + { type: 'requestAddVariable' }, + { + type: 'receiveAddVariableError', + payload: payloadError, + }, + ], + () => { + expect(createFlash).toHaveBeenCalled(); + done(); + }, + ); + }); + }); + + describe('fetchVariables', () => { + it('dispatch correct actions on fetchVariables', done => { + mock.onGet(state.endpoint).reply(200, { variables: mockData.mockVariables }); + + testAction( + actions.fetchVariables, + {}, + state, + [], + [ + { type: 'requestVariables' }, + { + type: 'receiveVariablesSuccess', + payload: prepareDataForDisplay(mockData.mockVariables), + }, + ], + () => { + done(); + }, + ); + }); + + it('should show flash error and set error in state on fetch variables failure', done => { + mock.onGet(state.endpoint).reply(500); + + testAction(actions.fetchVariables, {}, state, [], [{ type: 'requestVariables' }], () => { + expect(createFlash).toHaveBeenCalledWith('There was an error fetching the variables.'); + done(); + }); + }); + }); + + describe('fetchEnvironments', () => { + it('dispatch correct actions on fetchEnvironments', done => { + Api.environments = jest.fn().mockResolvedValue({ data: mockData.mockEnvironments }); + + testAction( + actions.fetchEnvironments, + {}, + state, + [], + [ + { type: 'requestEnvironments' }, + { + type: 'receiveEnvironmentsSuccess', + payload: prepareEnvironments(mockData.mockEnvironments), + }, + ], + () => { + done(); + }, + ); + }); + + it('should show flash error and set error in state on fetch environments failure', done => { + Api.environments = jest.fn().mockRejectedValue(); + + testAction( + actions.fetchEnvironments, + {}, + state, + [], + [{ type: 'requestEnvironments' }], + () => { + expect(createFlash).toHaveBeenCalledWith( + 'There was an error fetching the environments information.', + ); + done(); + }, + ); + }); + }); +}); diff --git a/spec/frontend/ci_variable_list/store/mutations_spec.js b/spec/frontend/ci_variable_list/store/mutations_spec.js new file mode 100644 index 00000000000..1bb34e88cf5 --- /dev/null +++ b/spec/frontend/ci_variable_list/store/mutations_spec.js @@ -0,0 +1,64 @@ +import state from '~/ci_variable_list/store/state'; +import mutations from '~/ci_variable_list/store/mutations'; +import * as types from '~/ci_variable_list/store/mutation_types'; + +describe('CI variable list mutations', () => { + let stateCopy; + + beforeEach(() => { + stateCopy = state(); + }); + + describe('TOGGLE_VALUES', () => { + it('should toggle state', () => { + const valuesHidden = false; + + mutations[types.TOGGLE_VALUES](stateCopy, valuesHidden); + + expect(stateCopy.valuesHidden).toEqual(valuesHidden); + }); + }); + + describe('VARIABLE_BEING_EDITED', () => { + it('should set variable that is being edited', () => { + const variableBeingEdited = { + environment_scope: '*', + id: 63, + key: 'test_var', + masked: false, + protected: false, + value: 'test_val', + variable_type: 'env_var', + }; + + mutations[types.VARIABLE_BEING_EDITED](stateCopy, variableBeingEdited); + + expect(stateCopy.variableBeingEdited).toEqual(variableBeingEdited); + }); + }); + + describe('RESET_EDITING', () => { + it('should reset variableBeingEdited to null', () => { + mutations[types.RESET_EDITING](stateCopy); + + expect(stateCopy.variableBeingEdited).toEqual(null); + }); + }); + + describe('CLEAR_MODAL', () => { + it('should clear modal state ', () => { + const modalState = { + variable_type: 'Variable', + key: '', + secret_value: '', + protected: false, + masked: false, + environment_scope: 'All environments', + }; + + mutations[types.CLEAR_MODAL](stateCopy); + + expect(stateCopy.variable).toEqual(modalState); + }); + }); +}); diff --git a/spec/frontend/ci_variable_list/store/utils_spec.js b/spec/frontend/ci_variable_list/store/utils_spec.js new file mode 100644 index 00000000000..9d5dd6b4f29 --- /dev/null +++ b/spec/frontend/ci_variable_list/store/utils_spec.js @@ -0,0 +1,47 @@ +import { + prepareDataForDisplay, + prepareEnvironments, + prepareDataForApi, +} from '~/ci_variable_list/store/utils'; +import mockData from '../services/mock_data'; + +describe('CI variables store utils', () => { + it('prepares ci variables for display', () => { + expect(prepareDataForDisplay(mockData.mockVariablesApi)).toStrictEqual( + mockData.mockVariablesDisplay, + ); + }); + + it('prepares single ci variable for api', () => { + expect(prepareDataForApi(mockData.mockVariablesDisplay[0])).toStrictEqual({ + environment_scope: '*', + id: 113, + key: 'test_var', + masked: false, + protected: false, + value: 'test_val', + variable_type: 'env_var', + }); + + expect(prepareDataForApi(mockData.mockVariablesDisplay[1])).toStrictEqual({ + environment_scope: '*', + id: 114, + key: 'test_var_2', + masked: false, + protected: false, + value: 'test_val_2', + variable_type: 'file', + }); + }); + + it('prepares single ci variable for delete', () => { + expect(prepareDataForApi(mockData.mockVariablesDisplay[0], true)).toHaveProperty( + '_destroy', + true, + ); + }); + + it('prepares environments for display', () => { + expect(prepareEnvironments(mockData.mockEnvironments)).toStrictEqual(['staging', 'production']); + }); +}); diff --git a/spec/lib/quality/kubernetes_client_spec.rb b/spec/lib/quality/kubernetes_client_spec.rb index 3a362dfccbf..1cfee5200f3 100644 --- a/spec/lib/quality/kubernetes_client_spec.rb +++ b/spec/lib/quality/kubernetes_client_spec.rb @@ -38,7 +38,7 @@ RSpec.describe Quality::KubernetesClient do .and_return(Gitlab::Popen::Result.new([], '', '', double(success?: true))) expect(Gitlab::Popen).to receive(:popen_with_detail) - .with([%(kubectl delete --namespace "#{namespace}" #{pod_for_release})]) + .with([%(kubectl delete --namespace "#{namespace}" --ignore-not-found #{pod_for_release})]) .and_return(Gitlab::Popen::Result.new([], '', '', double(success?: true))) # We're not verifying the output here, just silencing it @@ -64,7 +64,7 @@ RSpec.describe Quality::KubernetesClient do .and_return(Gitlab::Popen::Result.new([], '', '', double(success?: true))) expect(Gitlab::Popen).to receive(:popen_with_detail) - .with([%(kubectl delete --namespace "#{namespace}" #{pod_for_release})]) + .with([%(kubectl delete --namespace "#{namespace}" --ignore-not-found #{pod_for_release})]) .and_return(Gitlab::Popen::Result.new([], '', '', double(success?: true))) # We're not verifying the output here, just silencing it @@ -89,7 +89,7 @@ RSpec.describe Quality::KubernetesClient do .and_return(Gitlab::Popen::Result.new([], '', '', double(success?: true))) expect(Gitlab::Popen).to receive(:popen_with_detail) - .with([%(kubectl delete --namespace "#{namespace}" #{pod_for_release})]) + .with([%(kubectl delete --namespace "#{namespace}" --ignore-not-found #{pod_for_release})]) .and_return(Gitlab::Popen::Result.new([], '', '', double(success?: true))) # We're not verifying the output here, just silencing it |