Commit 92db820a authored by James Fargher's avatar James Fargher

Merge branch 'master' into '338949-remove-subtransactions-in-user'

# Conflicts:
#   app/services/users/migrate_to_ghost_user_service.rb
parents 0fdfb125 13646226
......@@ -201,9 +201,9 @@ Dangerfile @gl-quality/eng-prod
/lib/gitlab/auth/ldap/ @dblessing @mkozono
[Templates]
/lib/gitlab/ci/templates/ @nolith @shinya.maeda @matteeyah
/lib/gitlab/ci/templates/ @gitlab-org/maintainers/cicd-templates
/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml @DylanGriffith @mayra-cabrera @tkuah
/lib/gitlab/ci/templates/Security/ @gonzoyumo @twoodham @sethgitlab @thiagocsf
/lib/gitlab/ci/templates/Security/ @gonzoyumo @twoodham @sethgitlab @thiagocsf
/lib/gitlab/ci/templates/Security/Container-Scanning.*.yml @gitlab-org/protect/container-security-backend
[Project Alias]
......
<!--
Follow the documentation workflow https://docs.gitlab.com/ee/development/documentation/workflow.html
Additional information is located at https://docs.gitlab.com/ee/development/documentation/
To find the designated Tech Writer for the stage/group, see
https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
Mention "documentation" or "docs" in the MR title
For changing documentation location use the Change Documentation Location.md template
-->
## What does this MR do?
<!-- Briefly describe what this MR is about. -->
......@@ -18,12 +8,13 @@
## Author's checklist
- Consider taking [the GitLab Technical Writing Fundamentals course](https://gitlab.edcast.com/pathways/ECL-02528ee2-c334-4e16-abf3-e9d8b8260de4)
- [ ] Consider taking [the GitLab Technical Writing Fundamentals course](https://gitlab.edcast.com/pathways/ECL-02528ee2-c334-4e16-abf3-e9d8b8260de4)
- [ ] Follow the:
- [Documentation Guidelines](https://docs.gitlab.com/ee/development/documentation/).
- [Documentation process](https://docs.gitlab.com/ee/development/documentation/workflow.html).
- [Documentation guidelines](https://docs.gitlab.com/ee/development/documentation/).
- [Style Guide](https://docs.gitlab.com/ee/development/documentation/styleguide/).
- [ ] Ensure that the [product tier badge](https://docs.gitlab.com/ee/development/documentation/styleguide/index.html#product-tier-badges) is added to topic's `h1`.
- [ ] [Request a review](https://docs.gitlab.com/ee/development/code_review.html#dogfooding-the-reviewers-feature) based on the:
- [ ] [Request a review](https://docs.gitlab.com/ee/development/code_review.html#dogfooding-the-reviewers-feature) based on:
- The documentation page's [metadata](https://docs.gitlab.com/ee/development/documentation/#metadata).
- The [associated Technical Writer](https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments).
......
......@@ -712,3 +712,8 @@ QA/SelectorUsage:
- 'ee/spec/**/*.rb'
Exclude:
- 'spec/rubocop/**/*_spec.rb'
Performance/ActiveRecordSubtransactions:
Exclude:
- 'spec/**/*.rb'
- 'ee/spec/**/*.rb'
......@@ -2,6 +2,17 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 14.2.1 (2021-08-23)
### Fixed (1 change)
- [Drop un-used db/ci_migrate symlink](gitlab-org/gitlab@1154311625345e120407c0c397c7d4a27848a739) ([merge request](gitlab-org/gitlab!68723))
### Changed (2 changes)
- [Reorder vuln check criteria](gitlab-org/gitlab@9bbb20db46362a859632e7bb88deba985318ca2c) ([merge request](gitlab-org/gitlab!68723)) **GitLab Enterprise Edition**
- [Don't override vulnerability feedback UUID anymore](gitlab-org/gitlab@5f8372fb782c9416ae5ab582009a4399cb7d3750) ([merge request](gitlab-org/gitlab!68723)) **GitLab Enterprise Edition**
## 14.2.0 (2021-08-20)
### Added (128 changes)
......
48d7984d9912c935a2c2abba3b55593cf0be2d8e
bb2e3f4a916f031f38c9fb1c4fc955f50f0e4275
......@@ -6,7 +6,7 @@ import UsageTrendsApp from './components/app.vue';
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
defaultClient: createDefaultClient({}, { assumeImmutableResults: true }),
});
export default () => {
......
......@@ -22,6 +22,7 @@ export default {
<img
data-testid="image"
class="gl-max-w-full gl-h-auto"
:title="node.attrs.title"
:class="{ 'gl-opacity-5': node.attrs.uploading }"
:src="node.attrs.src"
/>
......
......@@ -50,6 +50,16 @@ export default Image.extend({
};
},
},
title: {
default: null,
parseHTML: (element) => {
const img = resolveImageEl(element);
return {
title: img.getAttribute('title'),
};
},
},
};
},
parseHTML() {
......
......@@ -42,6 +42,14 @@ export default Link.extend({
};
},
},
title: {
title: null,
parseHTML: (element) => {
return {
title: element.getAttribute('title'),
};
},
},
canonicalSrc: {
default: null,
parseHTML: (element) => {
......
......@@ -232,7 +232,7 @@ export function insertMarkdownText({
.join('\n');
}
} else if (tag.indexOf(textPlaceholder) > -1) {
textToInsert = tag.replace(textPlaceholder, () => selected);
textToInsert = tag.replace(textPlaceholder, () => selected.replace(/\\n/g, '\n'));
} else {
textToInsert = String(startChar) + tag + selected + (wrap ? tag : '');
}
......
......@@ -417,43 +417,6 @@ export const urlParamsToArray = (path = '') =>
export const getUrlParamsArray = () => urlParamsToArray(window.location.search);
/**
* Accepts encoding string which includes query params being
* sent to URL.
*
* @param {string} path Query param string
*
* @returns {object} Query params object containing key-value pairs
* with both key and values decoded into plain string.
*
* @deprecated Please use `queryToObject(query, { gatherArrays: true });` instead. See https://gitlab.com/gitlab-org/gitlab/-/issues/328845
*/
export const urlParamsToObject = (path = '') =>
splitPath(path).reduce((dataParam, filterParam) => {
if (filterParam === '') {
return dataParam;
}
const data = dataParam;
let [key, value] = filterParam.split('=');
key = /%\w+/g.test(key) ? decodeURIComponent(key) : key;
const isArray = key.includes('[]');
key = key.replace('[]', '');
value = decodeURIComponent(value.replace(/\+/g, ' '));
if (isArray) {
if (!data[key]) {
data[key] = [];
}
data[key].push(value);
} else {
data[key] = value;
}
return data;
}, {});
/**
* Convert search query into an object
*
......
import LinkedTabs from '~/lib/utils/bootstrap_linked_tabs';
import storageCounter from '~/projects/storage_counter';
import initSearchSettings from '~/search_settings';
const initLinkedTabs = () => {
if (!document.querySelector('.js-usage-quota-tabs')) {
return false;
}
return new LinkedTabs({
defaultAction: '#storage-quota-tab',
parentEl: '.js-usage-quota-tabs',
hashedTabs: true,
});
};
const initVueApp = () => {
storageCounter('js-project-storage-count-app');
};
initVueApp();
initLinkedTabs();
initSearchSettings();
......@@ -56,7 +56,11 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(resolvers, { typeDefs, useGet: true }),
defaultClient: createDefaultClient(resolvers, {
typeDefs,
useGet: true,
assumeImmutableResults: true,
}),
});
const { cache } = apolloProvider.clients.defaultClient;
......
......@@ -134,7 +134,7 @@ export default {
update(data) {
const { ciConfig } = data || {};
const stageNodes = ciConfig?.stages?.nodes || [];
const stages = unwrapStagesWithNeeds(stageNodes);
const stages = unwrapStagesWithNeeds(JSON.parse(JSON.stringify(stageNodes)));
return { ...ciConfig, stages };
},
......
import { get } from 'lodash';
import { REST, GRAPHQL } from './constants';
const accessors = {
[REST]: {
detailsPath: 'details_path',
groupId: 'id',
hasDetails: 'has_details',
pipelineStatus: ['details', 'status'],
sourceJob: ['source_job', 'name'],
},
[GRAPHQL]: {
detailsPath: 'detailsPath',
groupId: 'name',
hasDetails: 'hasDetails',
pipelineStatus: 'status',
sourceJob: ['sourceJob', 'name'],
},
};
const accessValue = (dataMethod, prop, item) => {
return get(item, accessors[dataMethod][prop]);
};
export { accessors, accessValue };
......@@ -8,9 +8,6 @@ export const UPSTREAM = 'upstream';
*/
export const ONE_COL_WIDTH = 180;
export const REST = 'rest';
export const GRAPHQL = 'graphql';
export const STAGE_VIEW = 'stage';
export const LAYER_VIEW = 'layer';
export const VIEW_TYPE_KEY = 'pipeline_graph_view_type';
......
......@@ -7,8 +7,7 @@ import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { reportToSentry } from '../../utils';
import ActionComponent from '../jobs_shared/action_component.vue';
import JobNameComponent from '../jobs_shared/job_name_component.vue';
import { accessValue } from './accessors';
import { REST, SINGLE_JOB } from './constants';
import { SINGLE_JOB } from './constants';
/**
* Renders the badge for the pipeline graph and the job's dropdown.
......@@ -47,11 +46,6 @@ export default {
GlTooltip: GlTooltipDirective,
},
mixins: [delayedJobMixin],
inject: {
dataMethod: {
default: REST,
},
},
props: {
job: {
type: Object,
......@@ -111,10 +105,10 @@ export default {
return this.pipelineId > -1 ? `${this.job.name}-${this.pipelineId}` : '';
},
detailsPath() {
return accessValue(this.dataMethod, 'detailsPath', this.status);
return this.status.detailsPath;
},
hasDetails() {
return accessValue(this.dataMethod, 'hasDetails', this.status);
return this.status.hasDetails;
},
isSingleItem() {
return this.type === SINGLE_JOB;
......@@ -189,7 +183,7 @@ export default {
if (this.isSingleItem) {
/*
This is so the jobDropdown still toggles. Issue to refactor:
https://gitlab.com/gitlab-org/gitlab/-/issues/267117
https://gitlab.com/gitlab-org/gitlab/-/issues/267117
*/
evt.stopPropagation();
}
......
......@@ -4,8 +4,7 @@ import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { __, sprintf } from '~/locale';
import CiStatus from '~/vue_shared/components/ci_icon.vue';
import { reportToSentry } from '../../utils';
import { accessValue } from './accessors';
import { DOWNSTREAM, REST, UPSTREAM } from './constants';
import { DOWNSTREAM, UPSTREAM } from './constants';
export default {
directives: {
......@@ -18,11 +17,6 @@ export default {
GlLoadingIcon,
GlBadge,
},
inject: {
dataMethod: {
default: REST,
},
},
props: {
columnTitle: {
type: String,
......@@ -40,20 +34,9 @@ export default {
type: String,
required: true,
},
/*
The next two props will be removed or required
once the graph transition is done.
See: https://gitlab.com/gitlab-org/gitlab/-/issues/291043
*/
isLoading: {
type: Boolean,
required: false,
default: false,
},
projectId: {
type: Number,
required: false,
default: -1,
required: true,
},
},
computed: {
......@@ -65,7 +48,7 @@ export default {
return `js-linked-pipeline-${this.pipeline.id}`;
},
pipelineStatus() {
return accessValue(this.dataMethod, 'pipelineStatus', this.pipeline);
return this.pipeline.status;
},
projectName() {
return this.pipeline.project.name;
......@@ -97,12 +80,10 @@ export default {
return this.type === UPSTREAM;
},
isSameProject() {
return this.projectId > -1
? this.projectId === this.pipeline.project.id
: !this.pipeline.multiproject;
return !this.pipeline.multiproject;
},
sourceJobName() {
return accessValue(this.dataMethod, 'sourceJob', this.pipeline);
return this.pipeline.sourceJob?.name ?? '';
},
sourceJobInfo() {
return this.isDownstream ? sprintf(__('Created by %{job}'), { job: this.sourceJobName }) : '';
......
......@@ -4,8 +4,6 @@ import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { reportToSentry } from '../../utils';
import MainGraphWrapper from '../graph_shared/main_graph_wrapper.vue';
import ActionComponent from '../jobs_shared/action_component.vue';
import { accessValue } from './accessors';
import { GRAPHQL } from './constants';
import JobGroupDropdown from './job_group_dropdown.vue';
import JobItem from './job_item.vue';
......@@ -97,7 +95,7 @@ export default {
},
methods: {
getGroupId(group) {
return accessValue(GRAPHQL, 'groupId', group);
return group.name;
},
groupId(group) {
return `ci-badge-${escape(group.name)}`;
......
import Vue from 'vue';
import createFlash from '~/flash';
import { parseBoolean } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
import Translate from '~/vue_shared/translate';
import TestReports from './components/test_reports/test_reports.vue';
import createDagApp from './pipeline_details_dag';
import { createPipelinesDetailApp } from './pipeline_details_graph';
import { createPipelineHeaderApp } from './pipeline_details_header';
import { apolloProvider } from './pipeline_shared_client';
import createTestReportsStore from './stores/test_reports';
Vue.use(Translate);
import { createTestDetails } from './pipeline_test_details';
const SELECTORS = {
PIPELINE_DETAILS: '.js-pipeline-details-vue',
......@@ -19,33 +13,6 @@ const SELECTORS = {
PIPELINE_TESTS: '#js-pipeline-tests-detail',
};
const createTestDetails = () => {
const el = document.querySelector(SELECTORS.PIPELINE_TESTS);
const { blobPath, emptyStateImagePath, hasTestReport, summaryEndpoint, suiteEndpoint } =
el?.dataset || {};
const testReportsStore = createTestReportsStore({
blobPath,
summaryEndpoint,
suiteEndpoint,
});
// eslint-disable-next-line no-new
new Vue({
el,
components: {
TestReports,
},
provide: {
emptyStateImagePath,
hasTestReport: parseBoolean(hasTestReport),
},
store: testReportsStore,
render(createElement) {
return createElement('test-reports');
},
});
};
export default async function initPipelineDetailsBundle() {
const { dataset } = document.querySelector(SELECTORS.PIPELINE_DETAILS);
......@@ -65,6 +32,27 @@ export default async function initPipelineDetailsBundle() {
});
}
createDagApp(apolloProvider);
createTestDetails();
try {
createPipelineHeaderApp(SELECTORS.PIPELINE_HEADER, apolloProvider, dataset.graphqlResourceEtag);
} catch {
createFlash({
message: __('An error occurred while loading a section of this page.'),
});
}
try {
createDagApp(apolloProvider);
} catch {
createFlash({
message: __('An error occurred while loading the Needs tab.'),
});
}
try {
createTestDetails(SELECTORS.PIPELINE_TESTS);
} catch {
createFlash({
message: __('An error occurred while loading the Test Reports tab.'),
});
}
}
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { GRAPHQL } from './components/graph/constants';
import PipelineGraphWrapper from './components/graph/graph_component_wrapper.vue';
import { reportToSentry } from './utils';
......@@ -23,7 +22,6 @@ const createPipelinesDetailApp = (
pipelineProjectPath,
pipelineIid,
graphqlResourceEtag,
dataMethod: GRAPHQL,
},
errorCaptured(err, _vm, info) {
reportToSentry('pipeline_details_graph', `error: ${err}, info: ${info}`);
......
import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import Translate from '~/vue_shared/translate';
import TestReports from './components/test_reports/test_reports.vue';
import createTestReportsStore from './stores/test_reports';
Vue.use(Translate);
export const createTestDetails = (selector) => {
const el = document.querySelector(selector);
const { blobPath, emptyStateImagePath, hasTestReport, summaryEndpoint, suiteEndpoint } =
el?.dataset || {};
const testReportsStore = createTestReportsStore({
blobPath,
summaryEndpoint,
suiteEndpoint,
});
// eslint-disable-next-line no-new
new Vue({
el,
components: {
TestReports,
},
provide: {
emptyStateImagePath,
hasTestReport: parseBoolean(hasTestReport),
},
store: testReportsStore,
render(createElement) {
return createElement('test-reports');
},
});
};
......@@ -5,7 +5,12 @@ import createDefaultClient from '~/lib/graphql';
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
defaultClient: createDefaultClient(
{},
{
assumeImmutableResults: true,
},
),
});
export const initCommitPipelineMiniGraph = async (selector = '.js-commit-pipeline-mini-graph') => {
......
<script>
import { s__ } from '~/locale';
export default {
name: 'StorageCounterApp',
i18n: {
placeholder: s__('UsageQuota|Usage'),
},
};
</script>
<template>
<div>{{ $options.i18n.placeholder }}</div>
</template>
import Vue from 'vue';
import StorageCounterApp from './components/app.vue';
export default (containerId = 'js-project-storage-count-app') => {
const el = document.getElementById(containerId);
if (!el) {
return false;
}
return new Vue({
el,
render(createElement) {
return createElement(StorageCounterApp);
},
});
};
......@@ -2,12 +2,16 @@
import createFlash from '~/flash';
import { fetchPolicies } from '~/lib/graphql';
import { updateHistory } from '~/lib/utils/url_utility';
import { formatNumber, sprintf, __ } from '~/locale';
import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue';
import RunnerList from '../components/runner_list.vue';
import RunnerManualSetupHelp from '../components/runner_manual_setup_help.vue';
import RunnerPagination from '../components/runner_pagination.vue';
import RunnerTypeHelp from '../components/runner_type_help.vue';
import { INSTANCE_TYPE, I18N_FETCH_ERROR } from '../constants';
import { statusTokenConfig } from '../components/search_tokens/status_token_config';
import { tagTokenConfig } from '../components/search_tokens/tag_token_config';
import { typeTokenConfig } from '../components/search_tokens/type_token_config';
import { ADMIN_FILTERED_SEARCH_NAMESPACE, INSTANCE_TYPE, I18N_FETCH_ERROR } from '../constants';
import getRunnersQuery from '../graphql/get_runners.query.graphql';
import {
fromUrlQueryToSearch,
......@@ -78,6 +82,21 @@ export default {
noRunnersFound() {
return !this.runnersLoading && !this.runners.items.length;
},
activeRunnersMessage() {
return sprintf(__('Runners currently online: %{active_runners_count}'), {
active_runners_count: formatNumber(this.activeRunnersCount),
});
},
searchTokens() {
return [
statusTokenConfig,
typeTokenConfig,
{
...tagTokenConfig,
recentTokenValuesStorageKey: `${this.$options.filteredSearchNamespace}-recent-tags`,
},
];
},
},
watch: {
search: {
......@@ -99,6 +118,7 @@ export default {
captureException({ error, component: this.$options.name });
},
},
filteredSearchNamespace: ADMIN_FILTERED_SEARCH_NAMESPACE,
INSTANCE_TYPE,
};
</script>
......@@ -118,9 +138,13 @@ export default {
<runner-filtered-search-bar
v-model="search"
namespace="admin_runners"
:active-runners-count="activeRunnersCount"
/>
:tokens="searchTokens"
:namespace="$options.filteredSearchNamespace"
>
<template #runner-count>
{{ activeRunnersMessage }}
</template>
</runner-filtered-search-bar>
<div v-if="noRunnersFound" class="gl-text-center gl-p-5">
{{ __('No runners found') }}
......
<script>
import { cloneDeep } from 'lodash';
import { formatNumber, sprintf, __, s__ } from '~/locale';
import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
import { __ } from '~/locale';
import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
import {
STATUS_ACTIVE,
STATUS_PAUSED,
STATUS_ONLINE,
STATUS_OFFLINE,
STATUS_NOT_CONNECTED,
INSTANCE_TYPE,
GROUP_TYPE,
PROJECT_TYPE,
CREATED_DESC,
CREATED_ASC,
CONTACTED_DESC,
CONTACTED_ASC,
PARAM_KEY_STATUS,
PARAM_KEY_RUNNER_TYPE,
PARAM_KEY_TAG,
} from '../constants';
import TagToken from './search_tokens/tag_token.vue';
import { CREATED_DESC, CREATED_ASC, CONTACTED_DESC, CONTACTED_ASC } from '../constants';
const sortOptions = [
{
......@@ -58,10 +39,6 @@ export default {
type: String,
required: true,
},
activeRunnersCount: {
type: Number,
required: true,
},
},
data() {
// filtered_search_bar_root.vue may mutate the inital
......@@ -73,62 +50,6 @@ export default {
initialSortBy: sort,
};
},
computed: {
searchTokens() {
return [
{
icon: 'status',
title: __('Status'),
type: PARAM_KEY_STATUS,
token: BaseToken,
unique: true,
options: [
{ value: STATUS_ACTIVE, title: s__('Runners|Active') },
{ value: STATUS_PAUSED, title: s__('Runners|Paused') },
{ value: STATUS_ONLINE, title: s__('Runners|Online') },
{ value: STATUS_OFFLINE, title: s__('Runners|Offline') },
// Added extra quotes in this title to avoid splitting this value:
// see: https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1438
{ value: STATUS_NOT_CONNECTED, title: `"${s__('Runners|Not connected')}"` },
],
// TODO In principle we could support more complex search rules,
// this can be added to a separate issue.
operators: OPERATOR_IS_ONLY,
},
{
icon: 'file-tree',
title: __('Type'),
type: PARAM_KEY_RUNNER_TYPE,
token: BaseToken,
unique: true,
options: [
{ value: INSTANCE_TYPE, title: s__('Runners|instance') },
{ value: GROUP_TYPE, title: s__('Runners|group') },
{ value: PROJECT_TYPE, title: s__('Runners|project') },
],
// TODO We should support more complex search rules,
// search for multiple states (OR) or have NOT operators
operators: OPERATOR_IS_ONLY,
},
{
icon: 'tag',
title: s__('Runners|Tags'),
type: PARAM_KEY_TAG,
token: TagToken,
recentTokenValuesStorageKey: `${this.namespace}-recent-tags`,
operators: OPERATOR_IS_ONLY,
},
];
},
activeRunnersMessage() {
return sprintf(__('Runners currently online: %{active_runners_count}'), {
active_runners_count: formatNumber(this.activeRunnersCount),
});
},
},
methods: {
onFilter(filters) {
const { sort } = this.value;
......@@ -161,12 +82,13 @@ export default {
:sort-options="$options.sortOptions"
:initial-filter-value="initialFilterValue"
:initial-sort-by="initialSortBy"
:tokens="searchTokens"
:search-input-placeholder="__('Search or filter results...')"
data-testid="runners-filtered-search"
@onFilter="onFilter"
@onSort="onSort"
/>
<div class="gl-text-right" data-testid="active-runners-message">{{ activeRunnersMessage }}</div>
<div class="gl-text-right" data-testid="runner-count">
<slot name="runner-count"></slot>
</div>
</div>
</template>
import { __, s__ } from '~/locale';
import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
import {
STATUS_ACTIVE,
STATUS_PAUSED,
STATUS_ONLINE,
STATUS_OFFLINE,
STATUS_NOT_CONNECTED,
PARAM_KEY_STATUS,
} from '../../constants';
export const statusTokenConfig = {
icon: 'status',
title: __('Status'),
type: PARAM_KEY_STATUS,
token: BaseToken,
unique: true,
options: [
{ value: STATUS_ACTIVE, title: s__('Runners|Active') },
{ value: STATUS_PAUSED, title: s__('Runners|Paused') },
{ value: STATUS_ONLINE, title: s__('Runners|Online') },
{ value: STATUS_OFFLINE, title: s__('Runners|Offline') },
// Added extra quotes in this title to avoid splitting this value:
// see: https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1438
{ value: STATUS_NOT_CONNECTED, title: `"${s__('Runners|Not connected')}"` },
],
// TODO In principle we could support more complex search rules,
// this can be added to a separate issue.
operators: OPERATOR_IS_ONLY,
};
......@@ -33,6 +33,7 @@ export default {
// The API should
// 1) scope to the rights of the user
// 2) stay up to date to the removal of old tags
// 3) consider the scope of search, like searching within the tags of a group
// See: https://gitlab.com/gitlab-org/gitlab/-/issues/333796
return axios
.get(TAG_SUGGESTIONS_PATH, {
......
import { s__ } from '~/locale';
import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
import { PARAM_KEY_TAG } from '../../constants';
import TagToken from './tag_token.vue';
export const tagTokenConfig = {
icon: 'tag',
title: s__('Runners|Tags'),
type: PARAM_KEY_TAG,
token: TagToken,
operators: OPERATOR_IS_ONLY,
};
import { __, s__ } from '~/locale';
import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE, PARAM_KEY_RUNNER_TYPE } from '../../constants';
export const typeTokenConfig = {
icon: 'file-tree',
title: __('Type'),
type: PARAM_KEY_RUNNER_TYPE,
token: BaseToken,
unique: true,
options: [
{ value: INSTANCE_TYPE, title: s__('Runners|instance') },
{ value: GROUP_TYPE, title: s__('Runners|group') },
{ value: PROJECT_TYPE, title: s__('Runners|project') },
],
// TODO We should support more complex search rules,
// search for multiple states (OR) or have NOT operators
operators: OPERATOR_IS_ONLY,
};
......@@ -2,6 +2,7 @@ import { s__ } from '~/locale';
export const RUNNER_PAGE_SIZE = 20;
export const RUNNER_JOB_COUNT_LIMIT = 1000;
export const GROUP_RUNNER_COUNT_LIMIT = 1000;
export const I18N_FETCH_ERROR = s__('Runners|Something went wrong while fetching runner data.');
export const I18N_DETAILS_TITLE = s__('Runners|Runner #%{runner_id}');
......@@ -50,3 +51,8 @@ export const CONTACTED_DESC = 'CONTACTED_DESC'; // TODO Add this to the API
export const CONTACTED_ASC = 'CONTACTED_ASC';
export const DEFAULT_SORT = CREATED_DESC;
// Local storage namespaces
export const ADMIN_FILTERED_SEARCH_NAMESPACE = 'admin_runners';
export const GROUP_FILTERED_SEARCH_NAMESPACE = 'group_runners';
#import "~/runner/graphql/runner_node.fragment.graphql"
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
query getGroupRunners(
$groupFullPath: ID!
$before: String
$after: String
$first: Int
$last: Int
$status: CiRunnerStatus
$type: CiRunnerType
$search: String
$sort: CiRunnerSort
) {
group(fullPath: $groupFullPath) {
runners(
membership: DESCENDANTS
before: $before
after: $after
first: $first
last: $last
status: $status
type: $type
search: $search
sort: $sort
) {
nodes {
...RunnerNode
}
pageInfo {
...PageInfo
}
}
}
}
<script>
import createFlash from '~/flash';
import { fetchPolicies } from '~/lib/graphql';
import { updateHistory } from '~/lib/utils/url_utility';
import { formatNumber, sprintf, s__ } from '~/locale';
import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue';
import RunnerList from '../components/runner_list.vue';
import RunnerManualSetupHelp from '../components/runner_manual_setup_help.vue';
import RunnerPagination from '../components/runner_pagination.vue';
import RunnerTypeHelp from '../components/runner_type_help.vue';
import { GROUP_TYPE } from '../constants';
import { statusTokenConfig } from '../components/search_tokens/status_token_config';
import { typeTokenConfig } from '../components/search_tokens/type_token_config';
import {
I18N_FETCH_ERROR,
GROUP_FILTERED_SEARCH_NAMESPACE,
GROUP_TYPE,
GROUP_RUNNER_COUNT_LIMIT,
} from '../constants';
import getGroupRunnersQuery from '../graphql/get_group_runners.query.graphql';
import {
fromUrlQueryToSearch,
fromSearchToUrl,
fromSearchToVariables,
} from '../runner_search_utils';
import { captureException } from '../sentry_utils';
export default {
name: 'GroupRunnersApp',
components: {
RunnerFilteredSearchBar,
RunnerList,
RunnerManualSetupHelp,
RunnerTypeHelp,
RunnerPagination,
},
props: {
registrationToken: {
type: String,
required: true,
},
groupFullPath: {
type: String,
required: true,
},
groupRunnersLimitedCount: {
type: Number,
required: true,
},
},
data() {
return {
search: fromUrlQueryToSearch(),
runners: {
items: [],
pageInfo: {},
},
};
},
apollo: {
runners: {
query: getGroupRunnersQuery,
// Runners can be updated by users directly in this list.
// A "cache and network" policy prevents outdated filtered
// results.
fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
variables() {
return this.variables;
},
update(data) {
const { runners } = data?.group || {};
return {
items: runners?.nodes || [],
pageInfo: runners?.pageInfo || {},
};
},
error(error) {
createFlash({ message: I18N_FETCH_ERROR });
this.reportToSentry(error);
},
},
},
computed: {
variables() {
return {
...fromSearchToVariables(this.search),
groupFullPath: this.groupFullPath,
};
},
runnersLoading() {
return this.$apollo.queries.runners.loading;
},
noRunnersFound() {
return !this.runnersLoading && !this.runners.items.length;
},
groupRunnersCount() {
if (this.groupRunnersLimitedCount > GROUP_RUNNER_COUNT_LIMIT) {
return `${formatNumber(GROUP_RUNNER_COUNT_LIMIT)}+`;
}
return formatNumber(this.groupRunnersLimitedCount);
},
runnerCountMessage() {
return sprintf(s__('Runners|Runners in this group: %{groupRunnersCount}'), {
groupRunnersCount: this.groupRunnersCount,
});
},
searchTokens() {
return [statusTokenConfig, typeTokenConfig];
},
filteredSearchNamespace() {
return `${GROUP_FILTERED_SEARCH_NAMESPACE}/${this.groupFullPath}`;
},
},
watch: {
search: {
deep: true,
handler() {
// TODO Implement back button reponse using onpopstate
updateHistory({
url: fromSearchToUrl(this.search),
title: document.title,
});
},
},
},
errorCaptured(error) {
this.reportToSentry(error);
},
methods: {
reportToSentry(error) {
captureException({ error, component: this.$options.name });
},
},
GROUP_TYPE,
};
......@@ -31,5 +148,23 @@ export default {
/>
</div>
</div>
<runner-filtered-search-bar
v-model="search"
:tokens="searchTokens"
:namespace="filteredSearchNamespace"
>
<template #runner-count>
{{ runnerCountMessage }}
</template>
</runner-filtered-search-bar>
<div v-if="noRunnersFound" class="gl-text-center gl-p-5">
{{ __('No runners found') }}
</div>
<template v-else>
<runner-list :runners="runners.items" :loading="runnersLoading" />
<runner-pagination v-model="search.pagination" :page-info="runners.pageInfo" />
</template>
</div>
</template>
......@@ -12,7 +12,13 @@ export const initGroupRunners = (selector = '#js-group-runners') => {
return null;
}
const { registrationToken, groupId } = el.dataset;
const {
registrationToken,
runnerInstallHelpPage,
groupId,
groupFullPath,
groupRunnersLimitedCount,
} = el.dataset;
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(
......@@ -27,12 +33,15 @@ export const initGroupRunners = (selector = '#js-group-runners') => {
el,
apolloProvider,
provide: {
runnerInstallHelpPage,
groupId,
},
render(h) {
return h(GroupRunnersApp, {
props: {
registrationToken,
groupFullPath,
groupRunnersLimitedCount: parseInt(groupRunnersLimitedCount, 10),
},
});
},
......
......@@ -250,6 +250,10 @@
.commit-row-description {
display: none;
flex: 1;
a {
color: $blue-600;
}
}
&.inline-commit {
......
......@@ -29,9 +29,16 @@ class Admin::BackgroundMigrationsController < Admin::ApplicationController
redirect_back fallback_location: { action: 'index' }
end
def retry
migration = batched_migration_class.find(params[:id])
migration.retry_failed_jobs! if migration.failed?
redirect_back fallback_location: { action: 'index' }
end
private
def batched_migration_class
Gitlab::Database::BackgroundMigration::BatchedMigration
@batched_migration_class ||= Gitlab::Database::BackgroundMigration::BatchedMigration
end
end
......@@ -10,6 +10,8 @@ class Groups::RunnersController < Groups::ApplicationController
feature_category :runner
def index
finder = Ci::RunnersFinder.new(current_user: current_user, params: { group: @group })
@group_runners_limited_count = finder.execute.except(:limit, :offset).page.total_count_with_limit(:all, limit: 1000)
end
def runner_list_group_view_vue_ui_enabled
......@@ -59,7 +61,7 @@ class Groups::RunnersController < Groups::ApplicationController
private
def runner
@runner ||= Ci::RunnersFinder.new(current_user: current_user, group: @group, params: {}).execute
@runner ||= Ci::RunnersFinder.new(current_user: current_user, params: { group: @group }).execute
.except(:limit, :offset)
.find(params[:id])
end
......
......@@ -17,7 +17,7 @@ module Groups
NUMBER_OF_RUNNERS_PER_PAGE = 4
def show
runners_finder = Ci::RunnersFinder.new(current_user: current_user, group: @group, params: params)
runners_finder = Ci::RunnersFinder.new(current_user: current_user, params: params.merge({ group: @group }))
# We need all runners for count
@all_group_runners = runners_finder.execute.except(:limit, :offset)
@group_runners = runners_finder.execute.page(params[:page]).per(NUMBER_OF_RUNNERS_PER_PAGE)
......
# frozen_string_literal: true
class Projects::UsageQuotasController < Projects::ApplicationController
before_action :authorize_admin_project!
before_action :verify_usage_quotas_enabled!
layout "project_settings"
feature_category :utilization
private
def verify_usage_quotas_enabled!
render_404 unless Feature.enabled?(:project_storage_ui, project&.group, default_enabled: :yaml)
end
end
......@@ -7,9 +7,9 @@ module Ci
ALLOWED_SORTS = %w[contacted_asc contacted_desc created_at_asc created_at_desc created_date].freeze
DEFAULT_SORT = 'created_at_desc'
def initialize(current_user:, group: nil, params:)
def initialize(current_user:, params:)
@params = params
@group = group
@group = params.delete(:group)
@current_user = current_user
end
......@@ -48,10 +48,16 @@ module Ci
def group_runners
raise Gitlab::Access::AccessDeniedError unless can?(@current_user, :admin_group, @group)
# Getting all runners from the group itself and all its descendants
descendant_projects = Project.for_group_and_its_subgroups(@group)
@runners = Ci::Runner.belonging_to_group_or_project(@group.self_and_descendants, descendant_projects)
@runners = case @params[:membership]
when :direct
Ci::Runner.belonging_to_group(@group.id)
when :descendants, nil
# Getting all runners from the group itself and all its descendant groups/projects
descendant_projects = Project.for_group_and_its_subgroups(@group)
Ci::Runner.belonging_to_group_or_project(@group.self_and_descendants, descendant_projects)
else
raise ArgumentError, 'Invalid membership filter'
end
end
def filter_by_status!
......
# frozen_string_literal: true
# Groups::UserGroupsFinder
#
# Used to filter Groups where a user is member
#
# Arguments:
# current_user - user requesting group info on target user
# target_user - user for which groups will be found
# params:
# permissions: string (see Types::Groups::UserPermissionsEnum)
# search: string used for search on path and group name
#
# Initially created to filter user groups and descendants where the user can create projects
module Groups
class UserGroupsFinder
def initialize(current_user, target_user, params = {})
@current_user = current_user
@target_user = target_user
@params = params
end
def execute
return Group.none unless current_user&.can?(:read_user_groups, target_user)
return Group.none if target_user.blank?
items = by_permission_scope
items = by_search(items)
sort(items)
end
private
attr_reader :current_user, :target_user, :params
def sort(items)
items.order(path: :asc, id: :asc) # rubocop: disable CodeReuse/ActiveRecord
end
def by_search(items)
return items if params[:search].blank?
items.search(params[:search])
end
def by_permission_scope
if permission_scope_create_projects?
target_user.manageable_groups(include_groups_with_developer_maintainer_access: true)
else
target_user.groups
end
end
def permission_scope_create_projects?
params[:permission_scope] == :create_projects &&
Feature.enabled?(:paginatable_namespace_drop_down_for_project_creation, current_user, default_enabled: :yaml)
end
end
end
......@@ -12,11 +12,16 @@ module Packages
end
def execute
base.npm
.with_name(@package_name)
.installable
.last_of_each_version
.preload_files
results = base.npm
.with_name(@package_name)
.installable
.last_of_each_version
unless Feature.enabled?(:npm_presenter_queries_tuning)
results = results.preload_files
end
results
end
private
......
......@@ -124,6 +124,16 @@ module Resolvers
[args[:iid], args[:iids]].any? ? 0 : 0.01
end
def self.before_connection_authorization(&block)
@before_connection_authorization_block = block
end
# rubocop: disable Style/TrivialAccessors
def self.before_connection_authorization_block
@before_connection_authorization_block
end
# rubocop: enable Style/TrivialAccessors
def offset_pagination(relation)
::Gitlab::Graphql::Pagination::OffsetPaginatedRelation.new(relation)
end
......
# frozen_string_literal: true
module Resolvers
module Ci
class GroupRunnersResolver < RunnersResolver
type Types::Ci::RunnerType.connection_type, null: true
argument :membership, ::Types::Ci::RunnerMembershipFilterEnum,
required: false,
default_value: :descendants,
description: 'Control which runners to include in the results.'
protected
def runners_finder_params(params)
super(params).merge(membership: params[:membership])
end
def parent_param
raise 'Expected group missing' unless parent.is_a?(Group)
{ group: parent }
end
end
end
end
......@@ -34,7 +34,7 @@ module Resolvers
.execute)
end
private
protected
def runners_finder_params(params)
{
......@@ -47,6 +47,19 @@ module Resolvers
tag_name: node_selection&.selects?(:tag_list)
}
}.compact
.merge(parent_param)
end
def parent_param
return {} unless parent
raise "Unexpected parent type: #{parent.class}"
end
private
def parent
object.respond_to?(:sync) ? object.sync : object
end
end
end
......
# frozen_string_literal: true
module Resolvers
module Users
class GroupsResolver < BaseResolver
include Gitlab::Graphql::Authorize::AuthorizeResource
include LooksAhead
type Types::GroupType.connection_type, null: true
authorize :read_user_groups
authorizes_object!
argument :search, GraphQL::Types::String,
required: false,
description: 'Search by group name or path.'
argument :permission_scope,
::Types::PermissionTypes::GroupEnum,
required: false,
description: 'Filter by permissions the user has on groups.'
before_connection_authorization do |nodes, current_user|
Preloaders::UserMaxAccessLevelInGroupsPreloader.new(nodes, current_user).execute
end
def resolve_with_lookahead(**args)
return unless Feature.enabled?(:paginatable_namespace_drop_down_for_project_creation, current_user, default_enabled: :yaml)
apply_lookahead(Groups::UserGroupsFinder.new(current_user, object, args).execute)
end
private
def preloads
{
path: [:route],
full_path: [:route]
}
end
end
end
end
Resolvers::Users::GroupsResolver.prepend_mod_with('Resolvers::Users::GroupsResolver')
# frozen_string_literal: true
module Types
module Ci
class RunnerMembershipFilterEnum < BaseEnum
graphql_name 'RunnerMembershipFilter'
description 'Values for filtering runners in namespaces.'
value 'DIRECT',
description: "Include runners that have a direct relationship.",
value: :direct
value 'DESCENDANTS',
description: "Include runners that have either a direct relationship or a relationship with descendants. These can be project runners or group runners (in the case where group is queried).",
value: :descendants
end
end
end
......@@ -155,6 +155,12 @@ module Types
complexity: 5,
resolver: Resolvers::GroupsResolver
field :runners, Types::Ci::RunnerType.connection_type,
null: true,
resolver: Resolvers::Ci::GroupRunnersResolver,
description: "Find runners visible to the current user.",
feature_flag: :runner_graphql_query
def avatar_url
object.avatar_url(only_path: false)
end
......
......@@ -5,7 +5,7 @@ module Types
class Group < BasePermissionType
graphql_name 'GroupPermissions'
abilities :read_group
abilities :read_group, :create_projects
end
end
end
# frozen_string_literal: true
module Types
module PermissionTypes
class GroupEnum < BaseEnum
graphql_name 'GroupPermission'
description 'User permission on groups'
value 'CREATE_PROJECTS', value: :create_projects, description: 'Groups where the user can create projects.'
end
end
end
......@@ -59,6 +59,9 @@ module Types
type: Types::GroupMemberType.connection_type,
null: true,
description: 'Group memberships of the user.'
field :groups,
resolver: Resolvers::Users::GroupsResolver,
description: 'Groups where the user has access.'
field :group_count,
resolver: Resolvers::Users::GroupCountResolver,
description: 'Group count for the user.'
......
......@@ -16,7 +16,6 @@ module Ci
"ci-config-path": project.ci_config_path_or_default,
"ci-examples-help-page-path" => help_page_path('ci/examples/index'),
"ci-help-page-path" => help_page_path('ci/index'),
"commit-sha" => commit_sha,
"default-branch" => project.default_branch_or_main,
"empty-state-illustration-path" => image_path('illustrations/empty-state/empty-dag-md.svg'),
"initial-branch-name" => initial_branch,
......
......@@ -174,7 +174,11 @@ module IssuesHelper
end
def issue_header_actions_data(project, issuable, current_user)
new_issuable_params = ({ issuable_template: 'incident', issue: { issue_type: 'incident' } } if issuable.incident?)
new_issuable_params = { issue: { description: _('Related to #%{issue_id}.') % { issue_id: issuable.iid } + "\n\n" } }
if issuable.incident?
new_issuable_params[:issuable_template] = 'incident'
new_issuable_params[:issue][:issue_type] = 'incident'
end
{
can_create_issue: show_new_issue_link?(project).to_s,
......
......@@ -31,7 +31,7 @@ class ApplicationRecord < ActiveRecord::Base
end
def self.safe_ensure_unique(retries: 0)
transaction(requires_new: true) do
transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
yield
end
rescue ActiveRecord::RecordNotUnique
......@@ -55,7 +55,7 @@ class ApplicationRecord < ActiveRecord::Base
# currently one third of the default 15-second timeout
def self.with_fast_read_statement_timeout(timeout_ms = 5000)
::Gitlab::Database::LoadBalancing::Session.current.fallback_to_replicas_for_ambiguous_queries do
transaction(requires_new: true) do
transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
connection.exec_query("SET LOCAL statement_timeout = #{timeout_ms}")
yield
......@@ -80,7 +80,7 @@ class ApplicationRecord < ActiveRecord::Base
#
# When calling this method on an association, just calling `self.create` would call `ActiveRecord::Persistence.create`
# and that skips some code that adds the newly created record to the association.
transaction(requires_new: true) { all.create(*args, &block) }
transaction(requires_new: true) { all.create(*args, &block) } # rubocop:disable Performance/ActiveRecordSubtransactions
rescue ActiveRecord::RecordNotUnique
find_by(*args)
end
......
......@@ -622,7 +622,7 @@ class ApplicationSetting < ApplicationRecord
def self.create_from_defaults
check_schema!
transaction(requires_new: true) do
transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
super
end
rescue ActiveRecord::RecordNotUnique
......
......@@ -10,6 +10,9 @@ module Clusters
has_many :agent_tokens, class_name: 'Clusters::AgentToken'
has_many :last_used_agent_tokens, -> { order_last_used_at_desc }, class_name: 'Clusters::AgentToken', inverse_of: :agent
has_many :group_authorizations, class_name: 'Clusters::Agents::GroupAuthorization'
has_many :authorized_groups, class_name: '::Group', through: :group_authorizations, source: :group
scope :ordered_by_name, -> { order(:name) }
scope :with_name, -> (name) { where(name: name) }
......
# frozen_string_literal: true
module Clusters
module Agents
class GroupAuthorization < ApplicationRecord
self.table_name = 'agent_group_authorizations'
belongs_to :agent, class_name: 'Clusters::Agent', optional: false
belongs_to :group, class_name: '::Group', optional: false
validates :config, json_schema: { filename: 'cluster_agent_authorization_configuration' }
end
end
end
......@@ -13,7 +13,7 @@ class InstanceConfiguration
{ ssh_algorithms_hashes: ssh_algorithms_hashes,
host: host,
gitlab_pages: gitlab_pages,
gitlab_ci: gitlab_ci,
size_limits: size_limits,
package_file_size_limits: package_file_size_limits,
rate_limits: rate_limits }.deep_symbolize_keys
end
......@@ -38,11 +38,16 @@ class InstanceConfiguration
rescue Resolv::ResolvError
end
def gitlab_ci
Settings.gitlab_ci
.to_h
.merge(artifacts_max_size: { value: Gitlab::CurrentSettings.max_artifacts_size.megabytes,
default: 100.megabytes })
def size_limits
{
max_attachment_size: application_settings[:max_attachment_size].megabytes,
receive_max_input_size: application_settings[:receive_max_input_size]&.megabytes,
max_import_size: application_settings[:max_import_size] > 0 ? application_settings[:max_import_size].megabytes : nil,
diff_max_patch_bytes: application_settings[:diff_max_patch_bytes].bytes,
max_artifacts_size: application_settings[:max_artifacts_size].megabytes,
max_pages_size: application_settings[:max_pages_size] > 0 ? application_settings[:max_pages_size].megabytes : nil,
snippet_size_limit: application_settings[:snippet_size_limit]&.bytes
}
end
def package_file_size_limits
......
......@@ -201,30 +201,55 @@ class InternalId < ApplicationRecord
InternalId.find_by(**scope, usage: usage_value)
end
def initial_value(subject, scope)
raise ArgumentError, 'Cannot initialize without init!' unless init
# `init` computes the maximum based on actual records. We use the
# primary to make sure we have up to date results
Gitlab::Database::LoadBalancing::Session.current.use_primary do
instance = subject.is_a?(::Class) ? nil : subject
init.call(instance, scope) || 0
end
end
def usage_value
@usage_value ||= InternalId.usages[usage.to_s]
end
# Create InternalId record for (scope, usage) combination, if it doesn't exist
#
# We blindly insert without synchronization. If another process
# was faster in doing this, we'll realize once we hit the unique key constraint
# violation. We can safely roll-back the nested transaction and perform
# a lookup instead to retrieve the record.
# We blindly insert ignoring conflicts on the unique key constraint.
# If another process was faster in doing this, we'll end up with that record
# when we do the lookup after the insert.
def create_record
raise ArgumentError, 'Cannot initialize without init!' unless init
instance = subject.is_a?(::Class) ? nil : subject
if Feature.enabled?(:use_insert_all_in_internal_id, default_enabled: :yaml)
scope[:project].save! if scope[:project] && !scope[:project].persisted?
scope[:namespace].save! if scope[:namespace] && !scope[:namespace].persisted?
subject.transaction(requires_new: true) do
InternalId.create!(
**scope,
attributes = {
project_id: scope[:project]&.id || scope[:project_id],
namespace_id: scope[:namespace]&.id || scope[:namespace_id],
usage: usage_value,
last_value: init.call(instance, scope) || 0
)
last_value: initial_value(subject, scope)
}
InternalId.insert_all([attributes])
lookup
else
begin
subject.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
InternalId.create!(
**scope,
usage: usage_value,
last_value: initial_value(subject, scope)
)
end
rescue ActiveRecord::RecordNotUnique
lookup
end
end
rescue ActiveRecord::RecordNotUnique
lookup
end
end
......@@ -247,6 +272,8 @@ class InternalId < ApplicationRecord
# init: Proc that accepts the subject and the scope and returns Integer|NilClass
attr_reader :subject, :scope, :scope_attrs, :usage, :init
RecordAlreadyExists = Class.new(StandardError)
def initialize(subject, scope, usage, init = nil)
@subject = subject
@scope = scope
......@@ -270,10 +297,8 @@ class InternalId < ApplicationRecord
return next_iid if next_iid
create_record!(subject, scope, usage, init) do |iid|
iid.last_value += 1
end
rescue ActiveRecord::RecordNotUnique
create_record!(subject, scope, usage, initial_value(subject, scope) + 1)
rescue RecordAlreadyExists
retry
end
......@@ -302,10 +327,8 @@ class InternalId < ApplicationRecord
next_iid = update_record!(subject, scope, usage, function)
return next_iid if next_iid
create_record!(subject, scope, usage, init) do |object|
object.last_value = [object.last_value, new_value].max
end
rescue ActiveRecord::RecordNotUnique
create_record!(subject, scope, usage, [initial_value(subject, scope), new_value].max)
rescue RecordAlreadyExists
retry
end
......@@ -317,27 +340,56 @@ class InternalId < ApplicationRecord
stmt.set(arel_table[:last_value] => new_value)
stmt.wheres = InternalId.filter_by(scope, usage).arel.constraints
ActiveRecord::Base.connection.insert(stmt, 'Update InternalId', 'last_value') # rubocop: disable Database/MultipleDatabases
InternalId.connection.insert(stmt, 'Update InternalId', 'last_value')
end
def create_record!(subject, scope, usage, init)
raise ArgumentError, 'Cannot initialize without init!' unless init
def create_record!(subject, scope, usage, value)
if Feature.enabled?(:use_insert_all_in_internal_id, default_enabled: :yaml)
scope[:project].save! if scope[:project] && !scope[:project].persisted?
scope[:namespace].save! if scope[:namespace] && !scope[:namespace].persisted?
instance = subject.is_a?(::Class) ? nil : subject
attributes = {
project_id: scope[:project]&.id || scope[:project_id],
namespace_id: scope[:namespace]&.id || scope[:namespace_id],
usage: usage_value,
last_value: value
}
subject.transaction(requires_new: true) do
last_value = init.call(instance, scope) || 0
result = InternalId.insert_all([attributes])
internal_id = InternalId.create!(**scope, usage: usage, last_value: last_value) do |subject|
yield subject if block_given?
end
raise RecordAlreadyExists if result.empty?
internal_id.last_value
value
else
begin
subject.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
internal_id = InternalId.create!(**scope, usage: usage, last_value: value)
internal_id.last_value
end
rescue ActiveRecord::RecordNotUnique
raise RecordAlreadyExists
end
end
end
def arel_table
InternalId.arel_table
end
def initial_value(subject, scope)
raise ArgumentError, 'Cannot initialize without init!' unless init
# `init` computes the maximum based on actual records. We use the
# primary to make sure we have up to date results
Gitlab::Database::LoadBalancing::Session.current.use_primary do
instance = subject.is_a?(::Class) ? nil : subject
init.call(instance, scope) || 0
end
end
def usage_value
@usage_value ||= InternalId.usages[usage.to_s]
end
end
end
# frozen_string_literal: true
class Packages::Package < ApplicationRecord
include EachBatch
include Sortable
include Gitlab::SQL::Pattern
include UsageStatistics
......@@ -104,6 +105,7 @@ class Packages::Package < ApplicationRecord
scope :including_build_info, -> { includes(pipelines: :user) }
scope :including_project_route, -> { includes(project: { namespace: :route }) }
scope :including_tags, -> { includes(:tags) }
scope :including_dependency_links, -> { includes(dependency_links: :dependency) }
scope :with_conan_channel, ->(package_channel) do
joins(:conan_metadatum).where(packages_conan_metadata: { package_channel: package_channel })
......
# frozen_string_literal: true
module Preloaders
# This class preloads the max access level (role) for the user within the given groups and
# stores the values in requests store.
# Will only be able to preload max access level for groups where the user is a direct member
class UserMaxAccessLevelInGroupsPreloader
include BulkMemberAccessLoad
def initialize(groups, user)
@groups = groups
@user = user
end
def execute
group_memberships = GroupMember.active_without_invites_and_requests
.non_minimal_access
.where(user: @user, source_id: @groups)
.group(:source_id)
.maximum(:access_level)
group_memberships.each do |group_id, max_access_level|
merge_value_to_request_store(User, @user.id, group_id, max_access_level)
end
end
end
end
......@@ -2093,6 +2093,10 @@ class Project < ApplicationRecord
# Docker doesn't allow. The proxy expects it to be downcased.
value: "#{Gitlab.host_with_port}/#{namespace.root_ancestor.path.downcase}#{DependencyProxy::URL_SUFFIX}"
)
variables.append(
key: 'CI_DEPENDENCY_PROXY_DIRECT_GROUP_IMAGE_PREFIX',
value: "#{Gitlab.host_with_port}/#{namespace.full_path.downcase}#{DependencyProxy::URL_SUFFIX}"
)
end
end
......
......@@ -25,6 +25,7 @@ class UserPolicy < BasePolicy
enable :update_user_status
enable :read_user_personal_access_tokens
enable :read_group_count
enable :read_user_groups
end
rule { default }.enable :read_user_profile
......
......@@ -7,14 +7,26 @@ module Packages
attr_reader :name, :packages
NPM_VALID_DEPENDENCY_TYPES = %i[dependencies devDependencies bundleDependencies peerDependencies].freeze
def initialize(name, packages)
@name = name
@packages = packages
end
def versions
if queries_tuning?
new_versions
else
legacy_versions
end
end
def dist_tags
build_package_tags.tap { |t| t["latest"] ||= sorted_versions.last }
end
private
def legacy_versions
package_versions = {}
packages.each do |package|
......@@ -28,11 +40,23 @@ module Packages
package_versions
end
def dist_tags
build_package_tags.tap { |t| t["latest"] ||= sorted_versions.last }
end
def new_versions
package_versions = {}
private
packages.each_batch do |relation|
relation.including_dependency_links
.preload_files
.each do |package|
package_file = package.package_files.last
next unless package_file
package_versions[package.version] = build_package_version(package, package_file)
end
end
package_versions
end
def build_package_tags
package_tags.to_h { |tag| [tag.name, tag.package.version] }
......@@ -59,26 +83,38 @@ module Packages
def build_package_dependencies(package)
dependencies = Hash.new { |h, key| h[key] = {} }
dependency_links = package.dependency_links
.with_dependency_type(NPM_VALID_DEPENDENCY_TYPES)
.includes_dependency
dependency_links.find_each do |dependency_link|
dependency = dependency_link.dependency
dependencies[dependency_link.dependency_type][dependency.name] = dependency.version_pattern
if queries_tuning?
package.dependency_links.each do |dependency_link|
dependency = dependency_link.dependency
dependencies[dependency_link.dependency_type][dependency.name] = dependency.version_pattern
end
else
dependency_links = package.dependency_links
.with_dependency_type(%i[dependencies devDependencies bundleDependencies peerDependencies])
.includes_dependency
dependency_links.find_each do |dependency_link|
dependency = dependency_link.dependency
dependencies[dependency_link.dependency_type][dependency.name] = dependency.version_pattern
end
end
dependencies
end
def sorted_versions
versions = packages.map(&:version).compact
versions = packages.pluck_versions.compact
VersionSorter.sort(versions)
end
def package_tags
Packages::Tag.for_packages(packages)
.preload_package
.preload_package
end
def queries_tuning?
Feature.enabled?(:npm_presenter_queries_tuning)
end
end
end
......
# frozen_string_literal: true
module Clusters
module Agents
class RefreshAuthorizationService
include Gitlab::Utils::StrongMemoize
AUTHORIZED_GROUP_LIMIT = 100
delegate :project, to: :agent, private: true
def initialize(agent, config:)
@agent = agent
@config = config
end
def execute
if allowed_group_configurations.present?
group_ids = allowed_group_configurations.map { |config| config.fetch(:group_id) }
agent.with_lock do
agent.group_authorizations.upsert_all(allowed_group_configurations, unique_by: [:agent_id, :group_id])
agent.group_authorizations.where.not(group_id: group_ids).delete_all # rubocop: disable CodeReuse/ActiveRecord
end
else
agent.group_authorizations.delete_all(:delete_all)
end
true
end
private
attr_reader :agent, :config
def allowed_group_configurations
strong_memoize(:allowed_group_configurations) do
group_entries = config.dig('ci_access', 'groups')&.first(AUTHORIZED_GROUP_LIMIT)
if group_entries
groups_by_path = group_entries.index_by { |config| config.delete('id') }
allowed_groups.where_full_path_in(groups_by_path.keys).map do |group|
{ group_id: group.id, config: groups_by_path[group.full_path] }
end
end
end
end
def allowed_groups
if project.root_ancestor.group?
project.root_ancestor.self_and_descendants
else
::Group.none
end
end
end
end
end
......@@ -54,28 +54,10 @@ module Issues
end
handle_assignee_changes(issue, old_assignees)
if issue.previous_changes.include?('confidential')
# don't enqueue immediately to prevent todos removal in case of a mistake
TodosDestroyer::ConfidentialIssueWorker.perform_in(Todo::WAIT_FOR_DELETE, issue.id) if issue.confidential?
create_confidentiality_note(issue)
track_usage_event(:incident_management_incident_change_confidential, current_user.id)
end
added_labels = issue.labels - old_labels
if added_labels.present?
notification_service.async.relabeled_issue(issue, added_labels, current_user)
end
handle_confidential_change(issue)
handle_added_labels(issue, old_labels)
handle_milestone_change(issue)
added_mentions = issue.mentioned_users(current_user) - old_mentioned_users
if added_mentions.present?
notification_service.async.new_mentions_in_issue(issue, added_mentions, current_user)
end
handle_added_mentions(issue, old_mentioned_users)
handle_severity_change(issue, old_severity)
handle_issue_type_change(issue)
end
......@@ -157,6 +139,23 @@ module Issues
MergeRequests::CreateFromIssueService.new(project: project, current_user: current_user, mr_params: create_merge_request_params).execute
end
def handle_confidential_change(issue)
if issue.previous_changes.include?('confidential')
# don't enqueue immediately to prevent todos removal in case of a mistake
TodosDestroyer::ConfidentialIssueWorker.perform_in(Todo::WAIT_FOR_DELETE, issue.id) if issue.confidential?
create_confidentiality_note(issue)
track_usage_event(:incident_management_incident_change_confidential, current_user.id)
end
end
def handle_added_labels(issue, old_labels)
added_labels = issue.labels - old_labels
if added_labels.present?
notification_service.async.relabeled_issue(issue, added_labels, current_user)
end
end
def handle_milestone_change(issue)
return unless issue.previous_changes.include?('milestone_id')
......@@ -185,6 +184,14 @@ module Issues
end
end
def handle_added_mentions(issue, old_mentioned_users)
added_mentions = issue.mentioned_users(current_user) - old_mentioned_users
if added_mentions.present?
notification_service.async.new_mentions_in_issue(issue, added_mentions, current_user)
end
end
def handle_severity_change(issue, old_severity)
return unless old_severity && issue.severity != old_severity
......
......@@ -5,7 +5,7 @@ module Projects
def execute(source_project, remove_remaining_elements: true)
return unless super
Project.transaction(requires_new: true) do
Project.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
move_deploy_keys_projects
remove_remaining_deploy_keys_projects if remove_remaining_elements
......
......@@ -5,7 +5,7 @@ module Projects
def execute(source_project, remove_remaining_elements: true)
return unless super && source_project.fork_network
Project.transaction(requires_new: true) do
Project.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
move_fork_network_members
update_root_project
refresh_forks_count
......
......@@ -5,7 +5,7 @@ module Projects
def execute(source_project, remove_remaining_elements: true)
return unless super
Project.transaction(requires_new: true) do
Project.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
move_lfs_objects_projects
remove_remaining_lfs_objects_project if remove_remaining_elements
......
......@@ -5,7 +5,7 @@ module Projects
def execute(source_project, remove_remaining_elements: true)
return unless super
Project.transaction(requires_new: true) do
Project.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
move_notification_settings
remove_remaining_notification_settings if remove_remaining_elements
......
......@@ -9,7 +9,7 @@ module Projects
def execute(source_project, remove_remaining_elements: true)
return unless super
Project.transaction(requires_new: true) do
Project.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
move_project_authorizations
remove_remaining_authorizations if remove_remaining_elements
......
......@@ -9,7 +9,7 @@ module Projects
def execute(source_project, remove_remaining_elements: true)
return unless super
Project.transaction(requires_new: true) do
Project.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
move_group_links
remove_remaining_project_group_links if remove_remaining_elements
......
......@@ -9,7 +9,7 @@ module Projects
def execute(source_project, remove_remaining_elements: true)
return unless super
Project.transaction(requires_new: true) do
Project.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
move_project_members
remove_remaining_members if remove_remaining_elements
......
......@@ -9,7 +9,7 @@ module Projects
return unless user_stars.any?
Project.transaction(requires_new: true) do
Project.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
user_stars.update_all(project_id: @project.id)
Project.reset_counters @project.id, :users_star_projects
......
{
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "Cluster Agent configuration for an authorized project or group",
"type": "object",
"additionalProperties": true
}
......@@ -17,3 +17,7 @@
= button_to resume_admin_background_migration_path(migration),
class: 'gl-button btn btn-icon has-tooltip', title: _('Resume'), 'aria-label' => _('Resume') do
= sprite_icon('play', css_class: 'gl-button-icon gl-icon')
- elsif migration.failed?
= button_to retry_admin_background_migration_path(migration),
class: 'gl-button btn btn-icon has-tooltip', title: _('Retry'), 'aria-label' => _('Retry') do
= sprite_icon('retry', css_class: 'gl-button-icon gl-icon')
......@@ -3,4 +3,4 @@
%h2.page-title
= s_('Runners|Group Runners')
#js-group-runners{ data: { registration_token: @group.runners_token, group_id: @group.id } }
#js-group-runners{ data: { registration_token: @group.runners_token, runner_install_help_page: 'https://docs.gitlab.com/runner/install/', group_id: @group.id, group_full_path: @group.full_path, group_runners_limited_count: @group_runners_limited_count } }
......@@ -7,7 +7,7 @@
= render 'help/instance_configuration/ssh_info'
= render 'help/instance_configuration/gitlab_pages'
= render 'help/instance_configuration/gitlab_ci'
= render 'help/instance_configuration/size_limits'
= render 'help/instance_configuration/package_registry'
= render 'help/instance_configuration/rate_limits'
%p
......
- content_for :table_content do
%li= link_to _('GitLab CI'), '#gitlab-ci'
- content_for :settings_content do
%h2#gitlab-ci
= _('GitLab CI')
%p
= _('Below are the current settings regarding')
= succeed('.') { link_to(_('GitLab CI'), 'https://about.gitlab.com/gitlab-ci', target: '_blank') }
.table-responsive
%table
%thead
%tr
%th= _('Setting')
%th= instance_configuration_host(@instance_configuration.settings[:host])
%th= _('Default')
%tbody
%tr
- artifacts_size = @instance_configuration.settings[:gitlab_ci][:artifacts_max_size]
%td= _('Artifacts maximum size')
%td= instance_configuration_human_size_cell(artifacts_size[:value])
%td= instance_configuration_human_size_cell(artifacts_size[:default])
......@@ -28,8 +28,3 @@
%td= _('Port')
%td
%code= instance_configuration_cell_html(gitlab_pages[:port])
%br
%p
- link_to_gitlab_ci = link_to(_('GitLab CI'), '#gitlab-ci')
= _("The maximum size of your Pages site is regulated by the artifacts maximum size which is part of %{link_to_gitlab_ci}.").html_safe % { link_to_gitlab_ci: link_to_gitlab_ci }
- size_limits = @instance_configuration.settings[:size_limits]
- content_for :table_content do
- if size_limits.present?
%li= link_to _('Size Limits'), '#size-limits'
- content_for :settings_content do
- if size_limits.present?
%h2#size-limits
= _('Size Limits')
%p
= _('There are several size limits in place.')
.table-responsive
%table
%thead
%tr
%th= _('Setting')
%th= instance_configuration_host(@instance_configuration.settings[:host])
%tbody
%tr
%td= _('Maximum attachment size')
%td= instance_configuration_human_size_cell(size_limits[:max_attachment_size])
%tr
%td= _('Maximum push size')
%td= instance_configuration_human_size_cell(size_limits[:receive_max_input_size])
%tr
%td= _('Maximum import size')
%td= instance_configuration_human_size_cell(size_limits[:max_import_size])
%tr
%td= _('Maximum diff patch size')
%td= instance_configuration_human_size_cell(size_limits[:diff_max_patch_bytes])
%tr
%td= _('Maximum job artifact size')
%td= instance_configuration_human_size_cell(size_limits[:max_artifacts_size])
%tr
%td= _('Maximum page size')
%td= instance_configuration_human_size_cell(size_limits[:max_pages_size])
%tr
%td= _('Maximum snippet size')
%td= instance_configuration_human_size_cell(size_limits[:snippet_size_limit])
- page_title s_("UsageQuota|Usage")
%h3.page-title
= s_('UsageQuota|Usage Quotas')
.row
.col-sm-6
= s_('UsageQuota|Usage of project resources across the %{strong_start}%{project_name}%{strong_end} project').html_safe % { strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe, project_name: @project.name }
.top-area.scrolling-tabs-container.inner-page-scroll-tabs
%ul.nav.nav-tabs.nav-links.scrolling-tabs.separator.js-usage-quota-tabs{ role: 'tablist' }
%li.nav-item
%a.nav-link#storage-quota{ data: { toggle: "tab", action: '#storage-quota-tab' }, href: '#storage-quota-tab', 'aria-controls': '#storage-quota-tab', 'aria-selected': 'true' }
= s_('UsageQuota|Storage')
.tab-content
.tab-pane#storage-quota-tab
#js-project-storage-count-app
---
name: create_vulnerabilities_via_api
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68158
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/338694
milestone: '14.3'
type: development
group: group::threat insights
default_enabled: false
---
name: dast_meta_tag_validation
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/67945
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/337711
name: npm_presenter_queries_tuning
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68275
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/338603
milestone: '14.2'
type: development
group: group::dynamic analysis
default_enabled: true
group: group::package
default_enabled: false
---
name: paginatable_namespace_drop_down_for_project_creation
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/66112
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/338930
milestone: '14.3'
type: development
group: group::project management
default_enabled: false
---
name: dast_runner_site_validation
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/61649
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/331082
milestone: '14.0'
name: project_storage_ui
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68289
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/334889
milestone: '14.2'
type: development
group: group::dynamic analysis
default_enabled: true
group: group::utilization
default_enabled: false
---
name: use_insert_all_in_internal_id
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68617
rollout_issue_url:
milestone: '14.3'
type: development
group: group::project management
default_enabled: false
......@@ -251,6 +251,7 @@ Settings.gitlab_ci['url'] ||= Settings.__send__(:build_gitlab_ci
#
Settings['incoming_email'] ||= Settingslogic.new({})
Settings.incoming_email['enabled'] = false if Settings.incoming_email['enabled'].nil?
Settings.incoming_email['inbox_method'] ||= 'imap'
#
# Service desk email
......
......@@ -93,6 +93,7 @@ namespace :admin do
member do
post :pause
post :resume
post :retry
end
end
......
......@@ -145,6 +145,8 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
resource :packages_and_registries, only: [:show]
end
resources :usage_quotas, only: [:index]
resources :autocomplete_sources, only: [] do
collection do
get 'members'
......
- title: Add pronunciation to GitLab profile page
body: |
You can now add pronunciation to your user profile. In distributed teams where team members are from different countries, it can be difficult to determine how to say someone's name correctly. This will help others know how to pronounce your name.
stage: Manage
self-managed: true
gitlab-com: true
packages: [Free, Premium, Ultimate]
url: 'https://docs.gitlab.com/ee/user/profile/#add-your-pronunciation'
image_url: https://about.gitlab.com/images/14_2/pronounce.png
published_at: 2021-08-22
release: 14.2
- title: View historical CI pipeline minute usage
body: |
Before GitLab 14.2, the CI pipeline minutes usage on the [Usage Quotas](https://docs.gitlab.com/ee/user/admin_area/settings/continuous_integration.html#shared-runners-pipeline-minutes-quota) page only showed the current month's usage. This data would reset every month and there was no way to view activity from the past months for analyzing historical usage.
Now there are two charts that show historical CI pipeline minutes usage by month or by project, so you can make informed decisions about your pipeline usage.
stage: Verify
self-managed: true
gitlab-com: true
packages: [Free, Premium, Ultimate]
url: 'https://docs.gitlab.com/ee/subscriptions/gitlab_com/index.html#ci-pipeline-minutes'
image_url: https://about.gitlab.com/images/14_2/CI_minutes_usage_graph.png
published_at: 2021-08-22
release: 14.2
- title: Edit issue title from an issue board
body: |
Editing an issue in an issue board currently requires many steps and takes you out of your workflow. We've added an easy way to edit an issue's title right in the issue board, without navigating to another page. To edit the title, in the right sidebar, select the issue, then select **Edit**.
stage: Plan
self-managed: true
gitlab-com: true
packages: [Free, Premium, Ultimate]
url: 'https://docs.gitlab.com/ee/user/project/issue_board.html#edit-an-issue'
image_url: https://about.gitlab.com/images/14_2/issue-board-edit-title.gif
published_at: 2021-08-22
release: 14.2
- title: Preview Markdown live while editing
body: |
Markdown is a fast and intuitive syntax for writing rich web content. Until it isn't. Luckily, it's easy to preview the rendered output of Markdown to ensure the accuracy of your markup from the **Preview** tab. Unfortunately, the context switch required to move between the raw source code and the preview can be tedious and disruptive to your flow.
Now, in both the Web IDE and single file editor, Markdown files have a new live preview option available. Right-click the editor and select **Preview Markdown** or use `Command/Control + Shift + P` to toggle a split-screen live preview of your Markdown content. The preview refreshes as you type, so you can be confident that your markup is valid and will render as you intended.
stage: Create
self-managed: true
gitlab-com: true
packages: [Free, Premium, Ultimate]
url: 'https://docs.gitlab.com/ee/user/project/web_ide/#markdown-editing'
image_url: https://about.gitlab.com/images/14_2/create-markdown-live-preview.png
published_at: 2021-08-22
release: 14.2
- title: Stageless pipelines
body: |
Using the [`needs`](https://docs.gitlab.com/ee/ci/yaml/#needs) keyword in your pipeline configuration helps to reduce cycle times by ignoring stage ordering and running jobs without waiting for others to complete. Previously, `needs` could only be used between jobs on different stages.
In this release, we've removed this limitation so you can define a `needs` relationship between any job you want. As a result, you can now create a complete CI/CD pipeline without using stages by including `needs` in every job to implicitly configure the execution order. This lets you define a less verbose pipeline that takes less time to create and can run even faster.
stage: Verify
self-managed: true
gitlab-com: true
packages: [Free, Premium, Ultimate]
url: 'https://docs.gitlab.com/ee/ci/yaml/#needs'
image_url: https://about.gitlab.com/images/14_2/need.png
published_at: 2021-08-22
release: 14.2
- title: New GitLab Kubernetes Agent UI
body: |
The GitLab Kubernetes Agent allows a secure bi-directional connection between GitLab and any Kubernetes cluster. Until now, registering a new Kubernetes Agent required writing GraphQL queries.
As of GitLab 14.2, GitLab ships with a user-friendly user interface and a registration form to help you get started with the Kubernetes Agent with ease.
stage: Configure
self-managed: true
gitlab-com: true
packages: [Premium, Ultimate]
url: 'https://docs.gitlab.com/ee/user/clusters/agent/'
image_url: https://about.gitlab.com/images/14_2/k8s-agent-registration.png
published_at: 2021-08-22
release: 14.2
- title: Create a GitLab branch from a Jira issue
body: |
Users of the [GitLab.com for Jira Cloud](https://marketplace.atlassian.com/apps/1221011/gitlab-com-for-jira-cloud) application can now create GitLab branches directly from a Jira issue's [development panel](https://support.atlassian.com/jira-software-cloud/docs/view-development-information-for-an-issue/). This enables developers to begin work on issues without having to switch tools and lose context.
stage: Create
self-managed: true
gitlab-com: true
packages: [Free, Premium, Ultimate]
url: 'https://docs.gitlab.com/ee/integration/jira/connect-app.html'
image_url: https://about.gitlab.com/images/14_2/jira_dev_panel_jira_setup_3.png
published_at: 2021-08-22
release: 14.2
migrate
\ No newline at end of file
# frozen_string_literal: true
class CreateAgentGroupAuthorizations < ActiveRecord::Migration[6.1]
include Gitlab::Database::MigrationHelpers
def change
create_table :agent_group_authorizations do |t|
t.bigint :group_id, null: false
t.bigint :agent_id, null: false
t.jsonb :config, null: false
t.index :group_id
t.index [:agent_id, :group_id], unique: true
end
end
end
# frozen_string_literal: true
class AddAgentGroupAuthorizationsForeignKeys < ActiveRecord::Migration[6.1]
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
def up
add_concurrent_foreign_key :agent_group_authorizations, :namespaces, column: :group_id
add_concurrent_foreign_key :agent_group_authorizations, :cluster_agents, column: :agent_id
end
def down
with_lock_retries do
remove_foreign_key_if_exists :agent_group_authorizations, column: :group_id
end
with_lock_retries do
remove_foreign_key_if_exists :agent_group_authorizations, column: :agent_id
end
end
end
# frozen_string_literal: true
class AddProjectIdNameVersionIdToNpmPackages < ActiveRecord::Migration[6.1]
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
INDEX_NAME = 'idx_installable_npm_pkgs_on_project_id_name_version_id'
def up
add_concurrent_index :packages_packages, [:project_id, :name, :version, :id], where: 'package_type = 2 AND status = 0', name: INDEX_NAME
end
def down
remove_concurrent_index :packages_packages, [:project_id, :name, :version, :id], where: 'package_type = 2 AND status = 0', name: INDEX_NAME
end
end
# frozen_string_literal: true
class AddDefaultProjectApprovalRulesVulnAllowed < ActiveRecord::Migration[6.1]
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
DEFAULT_VALUE = 0
def up
change_column_default :approval_project_rules, :vulnerabilities_allowed, DEFAULT_VALUE
update_column_in_batches(:approval_project_rules, :vulnerabilities_allowed, DEFAULT_VALUE) do |table, query|
query.where(table[:vulnerabilities_allowed].eq(nil))
end
change_column_null :approval_project_rules, :vulnerabilities_allowed, false
end
def down
change_column_default :approval_project_rules, :vulnerabilities_allowed, nil
change_column_null :approval_project_rules, :vulnerabilities_allowed, true
end
end
......@@ -85,7 +85,7 @@ class DeduplicateEpicIids < ActiveRecord::Migration[6.0]
instance = subject.is_a?(::Class) ? nil : subject
subject.transaction(requires_new: true) do
subject.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
InternalId.create!(
**scope,
usage: usage_value,
......
# frozen_string_literal: true
class BackfillStageEventHash < ActiveRecord::Migration[6.1]
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
BATCH_SIZE = 100
EVENT_ID_IDENTIFIER_MAPPING = {
1 => :issue_created,
2 => :issue_first_mentioned_in_commit,
3 => :issue_closed,
4 => :issue_first_added_to_board,
5 => :issue_first_associated_with_milestone,
7 => :issue_last_edited,
8 => :issue_label_added,
9 => :issue_label_removed,
10 => :issue_deployed_to_production,
100 => :merge_request_created,
101 => :merge_request_first_deployed_to_production,
102 => :merge_request_last_build_finished,
103 => :merge_request_last_build_started,
104 => :merge_request_merged,
105 => :merge_request_closed,
106 => :merge_request_last_edited,
107 => :merge_request_label_added,
108 => :merge_request_label_removed,
109 => :merge_request_first_commit_at,
1000 => :code_stage_start,
1001 => :issue_stage_end,
1002 => :plan_stage_start
}.freeze
LABEL_BASED_EVENTS = Set.new([8, 9, 107, 108]).freeze
class GroupStage < ActiveRecord::Base
include EachBatch
self.table_name = 'analytics_cycle_analytics_group_stages'
end
class ProjectStage < ActiveRecord::Base
include EachBatch
self.table_name = 'analytics_cycle_analytics_project_stages'
end
class StageEventHash < ActiveRecord::Base
self.table_name = 'analytics_cycle_analytics_stage_event_hashes'
end
def up
GroupStage.reset_column_information
ProjectStage.reset_column_information
StageEventHash.reset_column_information
update_stage_table(GroupStage)
update_stage_table(ProjectStage)
add_not_null_constraint :analytics_cycle_analytics_group_stages, :stage_event_hash_id
add_not_null_constraint :analytics_cycle_analytics_project_stages, :stage_event_hash_id
end
def down
remove_not_null_constraint :analytics_cycle_analytics_group_stages, :stage_event_hash_id
remove_not_null_constraint :analytics_cycle_analytics_project_stages, :stage_event_hash_id
end
private
def update_stage_table(klass)
klass.each_batch(of: BATCH_SIZE) do |relation|
klass.transaction do
records = relation.where(stage_event_hash_id: nil).lock!.to_a # prevent concurrent modification (unlikely to happen)
records = delete_invalid_records(records)
next if records.empty?
hashes_by_stage = records.to_h { |stage| [stage, calculate_stage_events_hash(stage)] }
hashes = hashes_by_stage.values.uniq
StageEventHash.insert_all(hashes.map { |hash| { hash_sha256: hash } })
stage_event_hashes_by_hash = StageEventHash.where(hash_sha256: hashes).index_by(&:hash_sha256)
records.each do |stage|
stage.update!(stage_event_hash_id: stage_event_hashes_by_hash[hashes_by_stage[stage]].id)
end
end
end
end
def calculate_stage_events_hash(stage)
start_event_hash = calculate_event_hash(stage.start_event_identifier, stage.start_event_label_id)
end_event_hash = calculate_event_hash(stage.end_event_identifier, stage.end_event_label_id)
Digest::SHA256.hexdigest("#{start_event_hash}-#{end_event_hash}")
end
def calculate_event_hash(event_identifier, label_id = nil)
str = EVENT_ID_IDENTIFIER_MAPPING.fetch(event_identifier).to_s
str << "-#{label_id}" if LABEL_BASED_EVENTS.include?(event_identifier)
Digest::SHA256.hexdigest(str)
end
# Invalid records are safe to delete, since they are not working properly anyway
def delete_invalid_records(records)
to_be_deleted = records.select do |record|
EVENT_ID_IDENTIFIER_MAPPING[record.start_event_identifier].nil? ||
EVENT_ID_IDENTIFIER_MAPPING[record.end_event_identifier].nil?
end
to_be_deleted.each(&:delete)
records - to_be_deleted
end
end
97d968bba0eb2bf6faa19de8a3e4fe93dc03a623b623dc802ab0fe0a4afb0370
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment