Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
21341457a8
commit
c52b72f577
|
@ -859,7 +859,7 @@ Rails/SaveBang:
|
|||
- 'ee/spec/models/operations/feature_flag_spec.rb'
|
||||
- 'ee/spec/models/operations/feature_flags/strategy_spec.rb'
|
||||
- 'ee/spec/models/operations/feature_flags/user_list_spec.rb'
|
||||
- 'ee/spec/models/packages/package_spec.rb'
|
||||
- 'spec/models/packages/package_spec.rb'
|
||||
- 'ee/spec/models/project_ci_cd_setting_spec.rb'
|
||||
- 'ee/spec/models/project_services/github_service_spec.rb'
|
||||
- 'ee/spec/models/project_services/jenkins_service_spec.rb'
|
||||
|
|
|
@ -9,6 +9,7 @@ const Api = {
|
|||
groupsPath: '/api/:version/groups.json',
|
||||
groupPath: '/api/:version/groups/:id',
|
||||
groupMembersPath: '/api/:version/groups/:id/members',
|
||||
groupMilestonesPath: '/api/:version/groups/:id/milestones',
|
||||
subgroupsPath: '/api/:version/groups/:id/subgroups',
|
||||
namespacesPath: '/api/:version/namespaces.json',
|
||||
groupProjectsPath: '/api/:version/groups/:id/projects.json',
|
||||
|
@ -98,6 +99,14 @@ const Api = {
|
|||
return axios.get(url).then(({ data }) => data);
|
||||
},
|
||||
|
||||
groupMilestones(groupId, params = {}) {
|
||||
const url = Api.buildUrl(Api.groupMilestonesPath).replace(':id', encodeURIComponent(groupId));
|
||||
|
||||
return axios.get(url, {
|
||||
params,
|
||||
});
|
||||
},
|
||||
|
||||
// Return namespaces list. Filtered by query
|
||||
namespaces(query, callback) {
|
||||
const url = Api.buildUrl(Api.namespacesPath);
|
||||
|
@ -262,10 +271,12 @@ const Api = {
|
|||
});
|
||||
},
|
||||
|
||||
projectMilestones(id) {
|
||||
projectMilestones(id, params = {}) {
|
||||
const url = Api.buildUrl(Api.projectMilestonesPath).replace(':id', encodeURIComponent(id));
|
||||
|
||||
return axios.get(url);
|
||||
return axios.get(url, {
|
||||
params,
|
||||
});
|
||||
},
|
||||
|
||||
mergeRequests(params = {}) {
|
||||
|
|
|
@ -54,7 +54,7 @@ export default {
|
|||
<div>
|
||||
<div
|
||||
v-if="!isSwimlanesOn"
|
||||
class="boards-list w-100 py-3 px-2 text-nowrap"
|
||||
class="boards-list gl-w-full gl-py-5 gl-px-3 gl-white-space-nowrap"
|
||||
data-qa-selector="boards_list"
|
||||
>
|
||||
<board-column
|
||||
|
|
|
@ -25,10 +25,6 @@ export default {
|
|||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
milestonePath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
labelsPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
|
@ -201,7 +197,6 @@ export default {
|
|||
:collapse-scope="isNewForm"
|
||||
:board="board"
|
||||
:can-admin-board="canAdminBoard"
|
||||
:milestone-path="milestonePath"
|
||||
:labels-path="labelsPath"
|
||||
:enable-scoped-labels="enableScopedLabels"
|
||||
:project-id="projectId"
|
||||
|
|
|
@ -36,10 +36,6 @@ export default {
|
|||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
milestonePath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
throttleDuration: {
|
||||
type: Number,
|
||||
default: 200,
|
||||
|
@ -335,7 +331,6 @@ export default {
|
|||
|
||||
<board-form
|
||||
v-if="currentPage"
|
||||
:milestone-path="milestonePath"
|
||||
:labels-path="labelsPath"
|
||||
:project-id="projectId"
|
||||
:group-id="groupId"
|
||||
|
|
|
@ -17,10 +17,6 @@ export default {
|
|||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
milestonePath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
labelPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
|
|
|
@ -38,10 +38,6 @@ export default {
|
|||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
milestonePath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
labelPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
|
@ -149,11 +145,7 @@ export default {
|
|||
class="add-issues-modal d-flex position-fixed position-top-0 position-bottom-0 position-left-0 position-right-0 h-100"
|
||||
>
|
||||
<div class="add-issues-container d-flex flex-column m-auto rounded">
|
||||
<modal-header
|
||||
:project-id="projectId"
|
||||
:milestone-path="milestonePath"
|
||||
:label-path="labelPath"
|
||||
/>
|
||||
<modal-header :project-id="projectId" :label-path="labelPath" />
|
||||
<modal-list
|
||||
v-if="!loading && showList && !filterLoading"
|
||||
:issue-link-base="issueLinkBase"
|
||||
|
|
|
@ -27,7 +27,7 @@ export default () => {
|
|||
hasMissingBoards: parseBoolean(dataset.hasMissingBoards),
|
||||
canAdminBoard: parseBoolean(dataset.canAdminBoard),
|
||||
multipleIssueBoardsAvailable: parseBoolean(dataset.multipleIssueBoardsAvailable),
|
||||
projectId: Number(dataset.projectId),
|
||||
projectId: dataset.projectId ? Number(dataset.projectId) : 0,
|
||||
groupId: Number(dataset.groupId),
|
||||
scopedIssueBoardFeatureEnabled: parseBoolean(dataset.scopedIssueBoardFeatureEnabled),
|
||||
weights: JSON.parse(dataset.weights),
|
||||
|
|
|
@ -4,10 +4,11 @@
|
|||
|
||||
import $ from 'jquery';
|
||||
import { template, escape } from 'lodash';
|
||||
import { __ } from '~/locale';
|
||||
import { __, sprintf } from '~/locale';
|
||||
import '~/gl_dropdown';
|
||||
import Api from '~/api';
|
||||
import axios from './lib/utils/axios_utils';
|
||||
import { timeFor } from './lib/utils/datetime_utility';
|
||||
import { timeFor, parsePikadayDate, dateInWords } from './lib/utils/datetime_utility';
|
||||
import ModalStore from './boards/stores/modal_store';
|
||||
import boardsStore, {
|
||||
boardStoreIssueSet,
|
||||
|
@ -34,10 +35,10 @@ export default class MilestoneSelect {
|
|||
$els.each((i, dropdown) => {
|
||||
let milestoneLinkNoneTemplate,
|
||||
milestoneLinkTemplate,
|
||||
milestoneExpiredLinkTemplate,
|
||||
selectedMilestone,
|
||||
selectedMilestoneDefault;
|
||||
const $dropdown = $(dropdown);
|
||||
const milestonesUrl = $dropdown.data('milestones');
|
||||
const issueUpdateURL = $dropdown.data('issueUpdate');
|
||||
const showNo = $dropdown.data('showNo');
|
||||
const showAny = $dropdown.data('showAny');
|
||||
|
@ -63,58 +64,101 @@ export default class MilestoneSelect {
|
|||
milestoneLinkTemplate = template(
|
||||
'<a href="<%- web_url %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>',
|
||||
);
|
||||
milestoneExpiredLinkTemplate = template(
|
||||
'<a href="<%- web_url %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %> (Past due)</a>',
|
||||
);
|
||||
milestoneLinkNoneTemplate = `<span class="no-value">${__('None')}</span>`;
|
||||
}
|
||||
return $dropdown.glDropdown({
|
||||
showMenuAbove,
|
||||
data: (term, callback) =>
|
||||
axios.get(milestonesUrl).then(({ data }) => {
|
||||
const extraOptions = [];
|
||||
if (showAny) {
|
||||
extraOptions.push({
|
||||
id: null,
|
||||
name: null,
|
||||
title: __('Any milestone'),
|
||||
});
|
||||
}
|
||||
if (showNo) {
|
||||
extraOptions.push({
|
||||
id: -1,
|
||||
name: __('No milestone'),
|
||||
title: __('No milestone'),
|
||||
});
|
||||
}
|
||||
if (showUpcoming) {
|
||||
extraOptions.push({
|
||||
id: -2,
|
||||
name: '#upcoming',
|
||||
title: __('Upcoming'),
|
||||
});
|
||||
}
|
||||
if (showStarted) {
|
||||
extraOptions.push({
|
||||
id: -3,
|
||||
name: '#started',
|
||||
title: __('Started'),
|
||||
});
|
||||
}
|
||||
if (extraOptions.length) {
|
||||
extraOptions.push({ type: 'divider' });
|
||||
}
|
||||
data: (term, callback) => {
|
||||
let contextId = $dropdown.get(0).dataset.projectId;
|
||||
let getMilestones = Api.projectMilestones;
|
||||
|
||||
callback(extraOptions.concat(data));
|
||||
if (showMenuAbove) {
|
||||
$dropdown.data('glDropdown').positionMenuAbove();
|
||||
}
|
||||
$(`[data-milestone-id="${escape(selectedMilestone)}"] > a`).addClass('is-active');
|
||||
}),
|
||||
renderRow: milestone => `
|
||||
<li data-milestone-id="${escape(milestone.name)}">
|
||||
if (!contextId) {
|
||||
contextId = $dropdown.get(0).dataset.groupId;
|
||||
getMilestones = Api.groupMilestones;
|
||||
}
|
||||
|
||||
// We don't use $.data() as it caches initial value and never updates!
|
||||
return getMilestones(contextId, { state: 'active' })
|
||||
.then(({ data }) =>
|
||||
data
|
||||
.map(m => ({
|
||||
...m,
|
||||
// Public API includes `title` instead of `name`.
|
||||
name: m.title,
|
||||
}))
|
||||
.sort((mA, mB) => {
|
||||
// Move all expired milestones to the bottom.
|
||||
if (mA.expired) {
|
||||
return 1;
|
||||
}
|
||||
if (mB.expired) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}),
|
||||
)
|
||||
.then(data => {
|
||||
const extraOptions = [];
|
||||
if (showAny) {
|
||||
extraOptions.push({
|
||||
id: null,
|
||||
name: null,
|
||||
title: __('Any milestone'),
|
||||
});
|
||||
}
|
||||
if (showNo) {
|
||||
extraOptions.push({
|
||||
id: -1,
|
||||
name: __('No milestone'),
|
||||
title: __('No milestone'),
|
||||
});
|
||||
}
|
||||
if (showUpcoming) {
|
||||
extraOptions.push({
|
||||
id: -2,
|
||||
name: '#upcoming',
|
||||
title: __('Upcoming'),
|
||||
});
|
||||
}
|
||||
if (showStarted) {
|
||||
extraOptions.push({
|
||||
id: -3,
|
||||
name: '#started',
|
||||
title: __('Started'),
|
||||
});
|
||||
}
|
||||
if (extraOptions.length) {
|
||||
extraOptions.push({ type: 'divider' });
|
||||
}
|
||||
|
||||
callback(extraOptions.concat(data));
|
||||
if (showMenuAbove) {
|
||||
$dropdown.data('glDropdown').positionMenuAbove();
|
||||
}
|
||||
$(`[data-milestone-id="${selectedMilestone}"] > a`).addClass('is-active');
|
||||
});
|
||||
},
|
||||
renderRow: milestone => {
|
||||
const milestoneName = milestone.title || milestone.name;
|
||||
let milestoneDisplayName = escape(milestoneName);
|
||||
|
||||
if (milestone.expired) {
|
||||
milestoneDisplayName = sprintf(__('%{milestone} (expired)'), {
|
||||
milestone: milestoneDisplayName,
|
||||
});
|
||||
}
|
||||
|
||||
return `
|
||||
<li data-milestone-id="${escape(milestoneName)}">
|
||||
<a href='#' class='dropdown-menu-milestone-link'>
|
||||
${escape(milestone.title)}
|
||||
${milestoneDisplayName}
|
||||
</a>
|
||||
</li>
|
||||
`,
|
||||
`;
|
||||
},
|
||||
filterable: true,
|
||||
search: {
|
||||
fields: ['title'],
|
||||
|
@ -149,7 +193,7 @@ export default class MilestoneSelect {
|
|||
selectedMilestone = $dropdown[0].dataset.selected || selectedMilestoneDefault;
|
||||
}
|
||||
$('a.is-active', $el).removeClass('is-active');
|
||||
$(`[data-milestone-id="${escape(selectedMilestone)}"] > a`, $el).addClass('is-active');
|
||||
$(`[data-milestone-id="${selectedMilestone}"] > a`, $el).addClass('is-active');
|
||||
},
|
||||
vue: $dropdown.hasClass('js-issue-board-sidebar'),
|
||||
clicked: clickEvent => {
|
||||
|
@ -237,7 +281,16 @@ export default class MilestoneSelect {
|
|||
if (data.milestone != null) {
|
||||
data.milestone.remaining = timeFor(data.milestone.due_date);
|
||||
data.milestone.name = data.milestone.title;
|
||||
$value.html(milestoneLinkTemplate(data.milestone));
|
||||
$value.html(
|
||||
data.milestone.expired
|
||||
? milestoneExpiredLinkTemplate({
|
||||
...data.milestone,
|
||||
remaining: sprintf(__('%{due_date} (Past due)'), {
|
||||
due_date: dateInWords(parsePikadayDate(data.milestone.due_date)),
|
||||
}),
|
||||
})
|
||||
: milestoneLinkTemplate(data.milestone),
|
||||
);
|
||||
return $sidebarCollapsedValue
|
||||
.attr(
|
||||
'data-original-title',
|
||||
|
|
|
@ -5,6 +5,8 @@ import EditHeader from './edit_header.vue';
|
|||
import UnsavedChangesConfirmDialog from './unsaved_changes_confirm_dialog.vue';
|
||||
import parseSourceFile from '~/static_site_editor/services/parse_source_file';
|
||||
import { EDITOR_TYPES } from '~/vue_shared/components/rich_content_editor/constants';
|
||||
import { DEFAULT_IMAGE_UPLOAD_PATH } from '../constants';
|
||||
import imageRepository from '../image_repository';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -31,6 +33,12 @@ export default {
|
|||
required: false,
|
||||
default: '',
|
||||
},
|
||||
imageRoot: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: DEFAULT_IMAGE_UPLOAD_PATH,
|
||||
validator: prop => prop.endsWith('/'),
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -40,6 +48,7 @@ export default {
|
|||
isModified: false,
|
||||
};
|
||||
},
|
||||
imageRepository: imageRepository(),
|
||||
computed: {
|
||||
editableContent() {
|
||||
return this.parsedSource.content(this.isWysiwygMode);
|
||||
|
@ -57,8 +66,14 @@ export default {
|
|||
this.editorMode = mode;
|
||||
this.$refs.editor.resetInitialValue(this.editableContent);
|
||||
},
|
||||
onUploadImage({ file, imageUrl }) {
|
||||
this.$options.imageRepository.add(file, imageUrl);
|
||||
},
|
||||
onSubmit() {
|
||||
this.$emit('submit', { content: this.parsedSource.content() });
|
||||
this.$emit('submit', {
|
||||
content: this.parsedSource.content(),
|
||||
images: this.$options.imageRepository.getAll(),
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -70,9 +85,11 @@ export default {
|
|||
ref="editor"
|
||||
:content="editableContent"
|
||||
:initial-edit-type="editorMode"
|
||||
:image-root="imageRoot"
|
||||
class="mb-9 h-100"
|
||||
@modeChange="onModeChange"
|
||||
@input="onInputChange"
|
||||
@uploadImage="onUploadImage"
|
||||
/>
|
||||
<unsaved-changes-confirm-dialog :modified="isModified" />
|
||||
<publish-toolbar
|
||||
|
|
|
@ -19,3 +19,5 @@ export const DEFAULT_HEADING = s__('StaticSiteEditor|Static site editor');
|
|||
export const TRACKING_ACTION_CREATE_COMMIT = 'create_commit';
|
||||
export const TRACKING_ACTION_CREATE_MERGE_REQUEST = 'create_merge_request';
|
||||
export const TRACKING_ACTION_INITIALIZE_EDITOR = 'initialize_editor';
|
||||
|
||||
export const DEFAULT_IMAGE_UPLOAD_PATH = 'source/images/uploads/';
|
||||
|
|
|
@ -3,10 +3,10 @@ import savedContentMetaQuery from '../queries/saved_content_meta.query.graphql';
|
|||
|
||||
const submitContentChangesResolver = (
|
||||
_,
|
||||
{ input: { project: projectId, username, sourcePath, content } },
|
||||
{ input: { project: projectId, username, sourcePath, content, images } },
|
||||
{ cache },
|
||||
) => {
|
||||
return submitContentChanges({ projectId, username, sourcePath, content }).then(
|
||||
return submitContentChanges({ projectId, username, sourcePath, content, images }).then(
|
||||
savedContentMeta => {
|
||||
cache.writeQuery({
|
||||
query: savedContentMetaQuery,
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
import { __ } from '~/locale';
|
||||
import Flash from '~/flash';
|
||||
import { getBinary } from './services/image_service';
|
||||
|
||||
const imageRepository = () => {
|
||||
const images = new Map();
|
||||
const flash = message => new Flash(message);
|
||||
|
||||
const add = (file, url) => {
|
||||
getBinary(file)
|
||||
.then(content => images.set(url, content))
|
||||
.catch(() => flash(__('Something went wrong while inserting your image. Please try again.')));
|
||||
};
|
||||
|
||||
const getAll = () => images;
|
||||
|
||||
return { add, getAll };
|
||||
};
|
||||
|
||||
export default imageRepository;
|
|
@ -67,11 +67,11 @@ export default {
|
|||
onDismissError() {
|
||||
this.submitChangesError = null;
|
||||
},
|
||||
onSubmit({ content }) {
|
||||
onSubmit({ content, images }) {
|
||||
this.content = content;
|
||||
this.submitChanges();
|
||||
this.submitChanges(images);
|
||||
},
|
||||
submitChanges() {
|
||||
submitChanges(images) {
|
||||
this.isSavingChanges = true;
|
||||
|
||||
this.$apollo
|
||||
|
@ -83,6 +83,7 @@ export default {
|
|||
username: this.appData.username,
|
||||
sourcePath: this.appData.sourcePath,
|
||||
content: this.content,
|
||||
images,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const getBinary = file => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.onload = () => resolve(reader.result.split(',')[1]);
|
||||
reader.onerror = error => reject(error);
|
||||
});
|
||||
};
|
|
@ -21,7 +21,32 @@ const createBranch = (projectId, branch) =>
|
|||
throw new Error(SUBMIT_CHANGES_BRANCH_ERROR);
|
||||
});
|
||||
|
||||
const commitContent = (projectId, message, branch, sourcePath, content) => {
|
||||
const createImageActions = (images, markdown) => {
|
||||
const actions = [];
|
||||
|
||||
if (!markdown) {
|
||||
return actions;
|
||||
}
|
||||
|
||||
images.forEach((imageContent, filePath) => {
|
||||
const imageExistsInMarkdown = path => new RegExp(`!\\[([^[\\]\\n]*)\\](\\(${path})\\)`); // matches the image markdown syntax: ![<any-string-except-newline>](<path>)
|
||||
|
||||
if (imageExistsInMarkdown(filePath).test(markdown)) {
|
||||
actions.push(
|
||||
convertObjectPropsToSnakeCase({
|
||||
encoding: 'base64',
|
||||
action: 'create',
|
||||
content: imageContent,
|
||||
filePath,
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return actions;
|
||||
};
|
||||
|
||||
const commitContent = (projectId, message, branch, sourcePath, content, images) => {
|
||||
Tracking.event(document.body.dataset.page, TRACKING_ACTION_CREATE_COMMIT);
|
||||
|
||||
return Api.commitMultiple(
|
||||
|
@ -35,6 +60,7 @@ const commitContent = (projectId, message, branch, sourcePath, content) => {
|
|||
filePath: sourcePath,
|
||||
content,
|
||||
}),
|
||||
...createImageActions(images, content),
|
||||
],
|
||||
}),
|
||||
).catch(() => {
|
||||
|
@ -62,7 +88,7 @@ const createMergeRequest = (
|
|||
});
|
||||
};
|
||||
|
||||
const submitContentChanges = ({ username, projectId, sourcePath, content }) => {
|
||||
const submitContentChanges = ({ username, projectId, sourcePath, content, images }) => {
|
||||
const branch = generateBranchName(username);
|
||||
const mergeRequestTitle = sprintf(s__(`StaticSiteEditor|Update %{sourcePath} file`), {
|
||||
sourcePath,
|
||||
|
@ -73,7 +99,7 @@ const submitContentChanges = ({ username, projectId, sourcePath, content }) => {
|
|||
.then(({ data: { web_url: url } }) => {
|
||||
Object.assign(meta, { branch: { label: branch, url } });
|
||||
|
||||
return commitContent(projectId, mergeRequestTitle, branch, sourcePath, content);
|
||||
return commitContent(projectId, mergeRequestTitle, branch, sourcePath, content, images);
|
||||
})
|
||||
.then(({ data: { short_id: label, web_url: url } }) => {
|
||||
Object.assign(meta, { commit: { label, url } });
|
||||
|
|
|
@ -68,6 +68,7 @@ export default {
|
|||
:is-applying-batch="suggestion.is_applying_batch"
|
||||
:batch-suggestions-count="batchSuggestionsCount"
|
||||
:help-page-path="helpPagePath"
|
||||
:inapplicable-reason="suggestion.inapplicable_reason"
|
||||
@apply="applySuggestion"
|
||||
@applyBatch="applySuggestionBatch"
|
||||
@addToBatch="addSuggestionToBatch"
|
||||
|
|
|
@ -38,6 +38,11 @@ export default {
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
inapplicableReason: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -52,9 +57,7 @@ export default {
|
|||
return this.isApplyingSingle || this.isApplyingBatch;
|
||||
},
|
||||
tooltipMessage() {
|
||||
return this.canApply
|
||||
? __('This also resolves this thread')
|
||||
: __("Can't apply as this line has changed or the suggestion already matches its content.");
|
||||
return this.canApply ? __('This also resolves this thread') : this.inapplicableReason;
|
||||
},
|
||||
isDisableButton() {
|
||||
return this.isApplying || !this.canApply;
|
||||
|
|
|
@ -16,8 +16,15 @@ export default {
|
|||
GlTab,
|
||||
},
|
||||
mixins: [glFeatureFlagMixin()],
|
||||
props: {
|
||||
imageRoot: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
file: null,
|
||||
urlError: null,
|
||||
imageUrl: null,
|
||||
description: null,
|
||||
|
@ -38,6 +45,7 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
show() {
|
||||
this.file = null;
|
||||
this.urlError = null;
|
||||
this.imageUrl = null;
|
||||
this.description = null;
|
||||
|
@ -66,7 +74,9 @@ export default {
|
|||
return;
|
||||
}
|
||||
|
||||
this.$emit('addImage', { file, altText: altText || file.name });
|
||||
const imageUrl = `${this.imageRoot}${file.name}`;
|
||||
|
||||
this.$emit('addImage', { imageUrl, file, altText: altText || file.name });
|
||||
},
|
||||
submitURL(event) {
|
||||
if (!this.validateUrl()) {
|
||||
|
|
|
@ -19,8 +19,6 @@ import {
|
|||
getMarkdown,
|
||||
} from './services/editor_service';
|
||||
|
||||
import { getUrl } from './services/image_service';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ToastEditor: () =>
|
||||
|
@ -54,6 +52,11 @@ export default {
|
|||
required: false,
|
||||
default: EDITOR_PREVIEW_STYLE,
|
||||
},
|
||||
imageRoot: {
|
||||
type: String,
|
||||
required: true,
|
||||
validator: prop => prop.endsWith('/'),
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -104,10 +107,8 @@ export default {
|
|||
const image = { imageUrl, altText };
|
||||
|
||||
if (file) {
|
||||
image.imageUrl = getUrl(file);
|
||||
// TODO - persist images locally (local image repository)
|
||||
this.$emit('uploadImage', { file, imageUrl });
|
||||
// TODO - ensure that the actual repo URL for the image is used in Markdown mode
|
||||
// TODO - upload images to the project repository (on submit)
|
||||
}
|
||||
|
||||
addImage(this.editorInstance, image);
|
||||
|
@ -130,6 +131,6 @@ export default {
|
|||
@change="onContentChanged"
|
||||
@load="onLoad"
|
||||
/>
|
||||
<add-image-modal ref="addImageModal" @addImage="onAddImage" />
|
||||
<add-image-modal ref="addImageModal" :image-root="imageRoot" @addImage="onAddImage" />
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const getUrl = file => URL.createObjectURL(file);
|
|
@ -94,7 +94,8 @@
|
|||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.boards-list {
|
||||
.boards-list,
|
||||
.board-swimlanes {
|
||||
height: calc(100vh - #{$header-height + $breadcrumb-min-height + $performance-bar-height + $system-footer-height + $gl-padding-32});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,7 +45,8 @@
|
|||
}
|
||||
}
|
||||
|
||||
.boards-list {
|
||||
.boards-list,
|
||||
.board-swimlanes {
|
||||
height: calc(100vh - #{$issue-board-list-difference-xs});
|
||||
overflow-x: scroll;
|
||||
min-height: 200px;
|
||||
|
@ -576,29 +577,8 @@
|
|||
}
|
||||
}
|
||||
|
||||
.board-epics-swimlanes {
|
||||
.board-swimlanes {
|
||||
overflow-x: auto;
|
||||
min-height: calc(100vh - #{$issue-board-list-difference-xs});
|
||||
|
||||
@include media-breakpoint-only(sm) {
|
||||
min-height: calc(100vh - #{$issue-board-list-difference-sm});
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
min-height: calc(100vh - #{$issue-board-list-difference-md});
|
||||
}
|
||||
|
||||
.with-performance-bar & {
|
||||
min-height: calc(100vh - #{$issue-board-list-difference-xs} - #{$performance-bar-height});
|
||||
|
||||
@include media-breakpoint-only(sm) {
|
||||
min-height: calc(100vh - #{$issue-board-list-difference-sm} - #{$performance-bar-height});
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
min-height: calc(100vh - #{$issue-board-list-difference-md} - #{$performance-bar-height});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.board-header-collapsed-info-icon:hover {
|
||||
|
|
|
@ -108,12 +108,3 @@
|
|||
.gl-transition-property-stroke {
|
||||
transition-property: stroke;
|
||||
}
|
||||
|
||||
// temporary class till giltab-ui one is merged
|
||||
.gl-border-t-2 {
|
||||
border-top-width: $gl-border-size-2;
|
||||
}
|
||||
|
||||
.gl-border-b-2 {
|
||||
border-bottom-width: $gl-border-size-2;
|
||||
}
|
||||
|
|
|
@ -52,7 +52,7 @@ module Resolvers
|
|||
end
|
||||
|
||||
def group_parameters(args)
|
||||
return { group_ids: parent.id } unless include_descendants?(args)
|
||||
return { group_ids: parent.id } unless args[:include_descendants].present?
|
||||
|
||||
{
|
||||
group_ids: parent.self_and_descendants.public_or_visible_to_user(current_user).select(:id),
|
||||
|
@ -60,10 +60,6 @@ module Resolvers
|
|||
}
|
||||
end
|
||||
|
||||
def include_descendants?(args)
|
||||
args[:include_descendants].present? && Feature.enabled?(:group_milestone_descendants, parent)
|
||||
end
|
||||
|
||||
def group_projects
|
||||
GroupProjectsFinder.new(
|
||||
group: parent,
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ClustersHelper
|
||||
# EE overrides this
|
||||
def has_multiple_clusters?
|
||||
false
|
||||
true
|
||||
end
|
||||
|
||||
def create_new_cluster_label(provider: nil)
|
||||
|
@ -95,5 +94,3 @@ module ClustersHelper
|
|||
can?(user, :admin_cluster, cluster)
|
||||
end
|
||||
end
|
||||
|
||||
ClustersHelper.prepend_if_ee('EE::ClustersHelper')
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
module Clusters
|
||||
class Cluster < ApplicationRecord
|
||||
prepend HasEnvironmentScope
|
||||
include Presentable
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
include FromUnion
|
||||
|
@ -81,6 +82,7 @@ module Clusters
|
|||
validate :no_groups, unless: :group_type?
|
||||
validate :no_projects, unless: :project_type?
|
||||
validate :unique_management_project_environment_scope
|
||||
validate :unique_environment_scope
|
||||
|
||||
after_save :clear_reactive_cache!
|
||||
|
||||
|
@ -354,6 +356,12 @@ module Clusters
|
|||
end
|
||||
end
|
||||
|
||||
def unique_environment_scope
|
||||
if clusterable.present? && clusterable.clusters.where(environment_scope: environment_scope).where.not(id: id).exists?
|
||||
errors.add(:environment_scope, 'cannot add duplicated environment scope')
|
||||
end
|
||||
end
|
||||
|
||||
def managed_namespace(environment)
|
||||
Clusters::KubernetesNamespaceFinder.new(
|
||||
self,
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module DeploymentPlatform
|
||||
# EE would override this and utilize environment argument
|
||||
# rubocop:disable Gitlab/ModuleWithInstanceVariables
|
||||
def deployment_platform(environment: nil)
|
||||
@deployment_platform ||= {}
|
||||
|
@ -20,16 +19,27 @@ module DeploymentPlatform
|
|||
find_instance_cluster_platform_kubernetes(environment: environment)
|
||||
end
|
||||
|
||||
# EE would override this and utilize environment argument
|
||||
def find_platform_kubernetes_with_cte(_environment)
|
||||
Clusters::ClustersHierarchy.new(self, include_management_project: cluster_management_project_enabled?).base_and_ancestors
|
||||
def find_platform_kubernetes_with_cte(environment)
|
||||
if environment
|
||||
::Clusters::ClustersHierarchy.new(self, include_management_project: cluster_management_project_enabled?)
|
||||
.base_and_ancestors
|
||||
.enabled
|
||||
.on_environment(environment, relevant_only: true)
|
||||
.first&.platform_kubernetes
|
||||
else
|
||||
Clusters::ClustersHierarchy.new(self, include_management_project: cluster_management_project_enabled?).base_and_ancestors
|
||||
.enabled.default_environment
|
||||
.first&.platform_kubernetes
|
||||
end
|
||||
end
|
||||
|
||||
# EE would override this and utilize environment argument
|
||||
def find_instance_cluster_platform_kubernetes(environment: nil)
|
||||
Clusters::Instance.new.clusters.enabled.default_environment
|
||||
if environment
|
||||
::Clusters::Instance.new.clusters.enabled.on_environment(environment, relevant_only: true)
|
||||
.first&.platform_kubernetes
|
||||
else
|
||||
Clusters::Instance.new.clusters.enabled.default_environment
|
||||
.first&.platform_kubernetes
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
module Packages
|
||||
def self.table_name_prefix
|
||||
'packages_'
|
||||
end
|
||||
end
|
|
@ -0,0 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Packages::BuildInfo < ApplicationRecord
|
||||
belongs_to :package, inverse_of: :build_info
|
||||
belongs_to :pipeline, class_name: 'Ci::Pipeline'
|
||||
end
|
|
@ -0,0 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Packages
|
||||
module Composer
|
||||
class Metadatum < ApplicationRecord
|
||||
self.table_name = 'packages_composer_metadata'
|
||||
self.primary_key = :package_id
|
||||
|
||||
belongs_to :package, -> { where(package_type: :composer) }, inverse_of: :composer_metadatum
|
||||
|
||||
validates :package, :target_sha, :composer_json, presence: true
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
module Packages
|
||||
module Conan
|
||||
def self.table_name_prefix
|
||||
'packages_conan_'
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,32 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Packages::Conan::FileMetadatum < ApplicationRecord
|
||||
belongs_to :package_file, inverse_of: :conan_file_metadatum
|
||||
|
||||
validates :package_file, presence: true
|
||||
|
||||
validates :recipe_revision,
|
||||
presence: true,
|
||||
format: { with: Gitlab::Regex.conan_revision_regex }
|
||||
|
||||
validates :package_revision, absence: true, if: :recipe_file?
|
||||
validates :package_revision, format: { with: Gitlab::Regex.conan_revision_regex }, if: :package_file?
|
||||
|
||||
validates :conan_package_reference, absence: true, if: :recipe_file?
|
||||
validates :conan_package_reference, format: { with: Gitlab::Regex.conan_package_reference_regex }, if: :package_file?
|
||||
validate :conan_package_type
|
||||
|
||||
enum conan_file_type: { recipe_file: 1, package_file: 2 }
|
||||
|
||||
RECIPE_FILES = ::Gitlab::Regex::Packages::CONAN_RECIPE_FILES
|
||||
PACKAGE_FILES = ::Gitlab::Regex::Packages::CONAN_PACKAGE_FILES
|
||||
PACKAGE_BINARY = 'conan_package.tgz'
|
||||
|
||||
private
|
||||
|
||||
def conan_package_type
|
||||
unless package_file&.package&.conan?
|
||||
errors.add(:base, _('Package type must be Conan'))
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,41 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Packages::Conan::Metadatum < ApplicationRecord
|
||||
belongs_to :package, -> { where(package_type: :conan) }, inverse_of: :conan_metadatum
|
||||
|
||||
validates :package, presence: true
|
||||
|
||||
validates :package_username,
|
||||
presence: true,
|
||||
format: { with: Gitlab::Regex.conan_recipe_component_regex }
|
||||
|
||||
validates :package_channel,
|
||||
presence: true,
|
||||
format: { with: Gitlab::Regex.conan_recipe_component_regex }
|
||||
|
||||
validate :conan_package_type
|
||||
|
||||
def recipe
|
||||
"#{package.name}/#{package.version}@#{package_username}/#{package_channel}"
|
||||
end
|
||||
|
||||
def recipe_path
|
||||
recipe.tr('@', '/')
|
||||
end
|
||||
|
||||
def self.package_username_from(full_path:)
|
||||
full_path.tr('/', '+')
|
||||
end
|
||||
|
||||
def self.full_path_from(package_username:)
|
||||
package_username.tr('+', '/')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def conan_package_type
|
||||
unless package&.conan?
|
||||
errors.add(:base, _('Package type must be Conan'))
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,47 @@
|
|||
# frozen_string_literal: true
|
||||
class Packages::Dependency < ApplicationRecord
|
||||
has_many :dependency_links, class_name: 'Packages::DependencyLink'
|
||||
|
||||
validates :name, :version_pattern, presence: true
|
||||
|
||||
validates :name, uniqueness: { scope: :version_pattern }
|
||||
|
||||
NAME_VERSION_PATTERN_TUPLE_MATCHING = '(name, version_pattern) = (?, ?)'.freeze
|
||||
MAX_STRING_LENGTH = 255.freeze
|
||||
MAX_CHUNKED_QUERIES_COUNT = 10.freeze
|
||||
|
||||
def self.ids_for_package_names_and_version_patterns(names_and_version_patterns = {}, chunk_size = 50, max_rows_limit = 200)
|
||||
names_and_version_patterns.reject! { |key, value| key.size > MAX_STRING_LENGTH || value.size > MAX_STRING_LENGTH }
|
||||
raise ArgumentError, 'Too many names_and_version_patterns' if names_and_version_patterns.size > MAX_CHUNKED_QUERIES_COUNT * chunk_size
|
||||
|
||||
matched_ids = []
|
||||
names_and_version_patterns.each_slice(chunk_size) do |tuples|
|
||||
where_statement = Array.new(tuples.size, NAME_VERSION_PATTERN_TUPLE_MATCHING)
|
||||
.join(' OR ')
|
||||
ids = where(where_statement, *tuples.flatten)
|
||||
.limit(max_rows_limit + 1)
|
||||
.pluck(:id)
|
||||
matched_ids.concat(ids)
|
||||
|
||||
raise ArgumentError, 'Too many Dependencies selected' if matched_ids.size > max_rows_limit
|
||||
end
|
||||
|
||||
matched_ids
|
||||
end
|
||||
|
||||
def self.for_package_names_and_version_patterns(names_and_version_patterns = {}, chunk_size = 50, max_rows_limit = 200)
|
||||
ids = ids_for_package_names_and_version_patterns(names_and_version_patterns, chunk_size, max_rows_limit)
|
||||
|
||||
return none if ids.empty?
|
||||
|
||||
id_in(ids)
|
||||
end
|
||||
|
||||
def self.pluck_ids_and_names
|
||||
pluck(:id, :name)
|
||||
end
|
||||
|
||||
def orphaned?
|
||||
self.dependency_links.empty?
|
||||
end
|
||||
end
|
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
class Packages::DependencyLink < ApplicationRecord
|
||||
belongs_to :package, inverse_of: :dependency_links
|
||||
belongs_to :dependency, inverse_of: :dependency_links, class_name: 'Packages::Dependency'
|
||||
has_one :nuget_metadatum, inverse_of: :dependency_link, class_name: 'Packages::Nuget::DependencyLinkMetadatum'
|
||||
|
||||
validates :package, :dependency, presence: true
|
||||
|
||||
validates :dependency_type,
|
||||
uniqueness: { scope: %i[package_id dependency_id] }
|
||||
|
||||
enum dependency_type: { dependencies: 1, devDependencies: 2, bundleDependencies: 3, peerDependencies: 4 }
|
||||
|
||||
scope :with_dependency_type, ->(dependency_type) { where(dependency_type: dependency_type) }
|
||||
scope :includes_dependency, -> { includes(:dependency) }
|
||||
scope :for_package, ->(package) { where(package_id: package.id) }
|
||||
scope :preload_dependency, -> { preload(:dependency) }
|
||||
scope :preload_nuget_metadatum, -> { preload(:nuget_metadatum) }
|
||||
end
|
|
@ -0,0 +1,93 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Packages
|
||||
module Go
|
||||
class Module
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
attr_reader :project, :name, :path
|
||||
|
||||
def initialize(project, name, path)
|
||||
@project = project
|
||||
@name = name
|
||||
@path = path
|
||||
end
|
||||
|
||||
def versions
|
||||
strong_memoize(:versions) { Packages::Go::VersionFinder.new(self).execute }
|
||||
end
|
||||
|
||||
def version_by(ref: nil, commit: nil)
|
||||
raise ArgumentError.new 'no filter specified' unless ref || commit
|
||||
raise ArgumentError.new 'ref and commit are mutually exclusive' if ref && commit
|
||||
|
||||
if commit
|
||||
return version_by_sha(commit) if commit.is_a? String
|
||||
|
||||
return version_by_commit(commit)
|
||||
end
|
||||
|
||||
return version_by_name(ref) if ref.is_a? String
|
||||
|
||||
version_by_ref(ref)
|
||||
end
|
||||
|
||||
def path_valid?(major)
|
||||
m = /\/v(\d+)$/i.match(@name)
|
||||
|
||||
case major
|
||||
when 0, 1
|
||||
m.nil?
|
||||
else
|
||||
!m.nil? && m[1].to_i == major
|
||||
end
|
||||
end
|
||||
|
||||
def gomod_valid?(gomod)
|
||||
if Feature.enabled?(:go_proxy_disable_gomod_validation, @project)
|
||||
return gomod&.start_with?("module ")
|
||||
end
|
||||
|
||||
gomod&.split("\n", 2)&.first == "module #{@name}"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def version_by_name(name)
|
||||
# avoid a Gitaly call if possible
|
||||
if strong_memoized?(:versions)
|
||||
v = versions.find { |v| v.name == ref }
|
||||
return v if v
|
||||
end
|
||||
|
||||
ref = @project.repository.find_tag(name) || @project.repository.find_branch(name)
|
||||
return unless ref
|
||||
|
||||
version_by_ref(ref)
|
||||
end
|
||||
|
||||
def version_by_ref(ref)
|
||||
# reuse existing versions
|
||||
if strong_memoized?(:versions)
|
||||
v = versions.find { |v| v.ref == ref }
|
||||
return v if v
|
||||
end
|
||||
|
||||
commit = ref.dereferenced_target
|
||||
semver = Packages::SemVer.parse(ref.name, prefixed: true)
|
||||
Packages::Go::ModuleVersion.new(self, :ref, commit, ref: ref, semver: semver)
|
||||
end
|
||||
|
||||
def version_by_sha(sha)
|
||||
commit = @project.commit_by(oid: sha)
|
||||
return unless ref
|
||||
|
||||
version_by_commit(commit)
|
||||
end
|
||||
|
||||
def version_by_commit(commit)
|
||||
Packages::Go::ModuleVersion.new(self, :commit, commit)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,115 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Packages
|
||||
module Go
|
||||
class ModuleVersion
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
VALID_TYPES = %i[ref commit pseudo].freeze
|
||||
|
||||
attr_reader :mod, :type, :ref, :commit
|
||||
|
||||
delegate :major, to: :@semver, allow_nil: true
|
||||
delegate :minor, to: :@semver, allow_nil: true
|
||||
delegate :patch, to: :@semver, allow_nil: true
|
||||
delegate :prerelease, to: :@semver, allow_nil: true
|
||||
delegate :build, to: :@semver, allow_nil: true
|
||||
|
||||
def initialize(mod, type, commit, name: nil, semver: nil, ref: nil)
|
||||
raise ArgumentError.new("invalid type '#{type}'") unless VALID_TYPES.include? type
|
||||
raise ArgumentError.new("mod is required") unless mod
|
||||
raise ArgumentError.new("commit is required") unless commit
|
||||
|
||||
if type == :ref
|
||||
raise ArgumentError.new("ref is required") unless ref
|
||||
elsif type == :pseudo
|
||||
raise ArgumentError.new("name is required") unless name
|
||||
raise ArgumentError.new("semver is required") unless semver
|
||||
end
|
||||
|
||||
@mod = mod
|
||||
@type = type
|
||||
@commit = commit
|
||||
@name = name if name
|
||||
@semver = semver if semver
|
||||
@ref = ref if ref
|
||||
end
|
||||
|
||||
def name
|
||||
@name || @ref&.name
|
||||
end
|
||||
|
||||
def full_name
|
||||
"#{mod.name}@#{name || commit.sha}"
|
||||
end
|
||||
|
||||
def gomod
|
||||
strong_memoize(:gomod) do
|
||||
if strong_memoized?(:blobs)
|
||||
blob_at(@mod.path + '/go.mod')
|
||||
elsif @mod.path.empty?
|
||||
@mod.project.repository.blob_at(@commit.sha, 'go.mod')&.data
|
||||
else
|
||||
@mod.project.repository.blob_at(@commit.sha, @mod.path + '/go.mod')&.data
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def archive
|
||||
suffix_len = @mod.path == '' ? 0 : @mod.path.length + 1
|
||||
|
||||
Zip::OutputStream.write_buffer do |zip|
|
||||
files.each do |file|
|
||||
zip.put_next_entry "#{full_name}/#{file[suffix_len...]}"
|
||||
zip.write blob_at(file)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def files
|
||||
strong_memoize(:files) do
|
||||
ls_tree.filter { |e| !excluded.any? { |n| e.start_with? n } }
|
||||
end
|
||||
end
|
||||
|
||||
def excluded
|
||||
strong_memoize(:excluded) do
|
||||
ls_tree
|
||||
.filter { |f| f.end_with?('/go.mod') && f != @mod.path + '/go.mod' }
|
||||
.map { |f| f[0..-7] }
|
||||
end
|
||||
end
|
||||
|
||||
def valid?
|
||||
@mod.path_valid?(major) && @mod.gomod_valid?(gomod)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def blob_at(path)
|
||||
return if path.nil? || path.empty?
|
||||
|
||||
path = path[1..] if path.start_with? '/'
|
||||
|
||||
blobs.find { |x| x.path == path }&.data
|
||||
end
|
||||
|
||||
def blobs
|
||||
strong_memoize(:blobs) { @mod.project.repository.batch_blobs(files.map { |x| [@commit.sha, x] }) }
|
||||
end
|
||||
|
||||
def ls_tree
|
||||
strong_memoize(:ls_tree) do
|
||||
path =
|
||||
if @mod.path.empty?
|
||||
'.'
|
||||
else
|
||||
@mod.path
|
||||
end
|
||||
|
||||
@mod.project.repository.gitaly_repository_client.search_files_by_name(@commit.sha, path)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
module Packages
|
||||
module Maven
|
||||
def self.table_name_prefix
|
||||
'packages_maven_'
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,28 @@
|
|||
# frozen_string_literal: true
|
||||
class Packages::Maven::Metadatum < ApplicationRecord
|
||||
belongs_to :package, -> { where(package_type: :maven) }
|
||||
|
||||
validates :package, presence: true
|
||||
|
||||
validates :path,
|
||||
presence: true,
|
||||
format: { with: Gitlab::Regex.maven_path_regex }
|
||||
|
||||
validates :app_group,
|
||||
presence: true,
|
||||
format: { with: Gitlab::Regex.maven_app_group_regex }
|
||||
|
||||
validates :app_name,
|
||||
presence: true,
|
||||
format: { with: Gitlab::Regex.maven_app_name_regex }
|
||||
|
||||
validate :maven_package_type
|
||||
|
||||
private
|
||||
|
||||
def maven_package_type
|
||||
unless package&.maven?
|
||||
errors.add(:base, _('Package type must be Maven'))
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
module Packages
|
||||
module Nuget
|
||||
def self.table_name_prefix
|
||||
'packages_nuget_'
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Packages::Nuget::DependencyLinkMetadatum < ApplicationRecord
|
||||
self.primary_key = :dependency_link_id
|
||||
|
||||
belongs_to :dependency_link, inverse_of: :nuget_metadatum
|
||||
|
||||
validates :dependency_link, :target_framework, presence: true
|
||||
|
||||
validate :ensure_nuget_package_type
|
||||
|
||||
private
|
||||
|
||||
def ensure_nuget_package_type
|
||||
return if dependency_link&.package&.nuget?
|
||||
|
||||
errors.add(:base, _('Package type must be NuGet'))
|
||||
end
|
||||
end
|
|
@ -0,0 +1,27 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Packages::Nuget::Metadatum < ApplicationRecord
|
||||
belongs_to :package, -> { where(package_type: :nuget) }, inverse_of: :nuget_metadatum
|
||||
|
||||
validates :package, presence: true
|
||||
validates :license_url, public_url: { allow_blank: true }
|
||||
validates :project_url, public_url: { allow_blank: true }
|
||||
validates :icon_url, public_url: { allow_blank: true }
|
||||
|
||||
validate :ensure_at_least_one_field_supplied
|
||||
validate :ensure_nuget_package_type
|
||||
|
||||
private
|
||||
|
||||
def ensure_at_least_one_field_supplied
|
||||
return if license_url? || project_url? || icon_url?
|
||||
|
||||
errors.add(:base, _('Nuget metadatum must have at least license_url, project_url or icon_url set'))
|
||||
end
|
||||
|
||||
def ensure_nuget_package_type
|
||||
return if package&.nuget?
|
||||
|
||||
errors.add(:base, _('Package type must be NuGet'))
|
||||
end
|
||||
end
|
|
@ -0,0 +1,195 @@
|
|||
# frozen_string_literal: true
|
||||
class Packages::Package < ApplicationRecord
|
||||
include Sortable
|
||||
include Gitlab::SQL::Pattern
|
||||
include UsageStatistics
|
||||
|
||||
belongs_to :project
|
||||
# package_files must be destroyed by ruby code in order to properly remove carrierwave uploads and update project statistics
|
||||
has_many :package_files, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
|
||||
has_many :dependency_links, inverse_of: :package, class_name: 'Packages::DependencyLink'
|
||||
has_many :tags, inverse_of: :package, class_name: 'Packages::Tag'
|
||||
has_one :conan_metadatum, inverse_of: :package, class_name: 'Packages::Conan::Metadatum'
|
||||
has_one :pypi_metadatum, inverse_of: :package, class_name: 'Packages::Pypi::Metadatum'
|
||||
has_one :maven_metadatum, inverse_of: :package, class_name: 'Packages::Maven::Metadatum'
|
||||
has_one :nuget_metadatum, inverse_of: :package, class_name: 'Packages::Nuget::Metadatum'
|
||||
has_one :composer_metadatum, inverse_of: :package, class_name: 'Packages::Composer::Metadatum'
|
||||
has_one :build_info, inverse_of: :package
|
||||
|
||||
accepts_nested_attributes_for :conan_metadatum
|
||||
accepts_nested_attributes_for :maven_metadatum
|
||||
|
||||
delegate :recipe, :recipe_path, to: :conan_metadatum, prefix: :conan
|
||||
|
||||
validates :project, presence: true
|
||||
validates :name, presence: true
|
||||
|
||||
validates :name, format: { with: Gitlab::Regex.package_name_regex }, unless: :conan?
|
||||
|
||||
validates :name,
|
||||
uniqueness: { scope: %i[project_id version package_type] }, unless: :conan?
|
||||
|
||||
validate :valid_conan_package_recipe, if: :conan?
|
||||
validate :valid_npm_package_name, if: :npm?
|
||||
validate :valid_composer_global_name, if: :composer?
|
||||
validate :package_already_taken, if: :npm?
|
||||
validates :version, format: { with: Gitlab::Regex.semver_regex }, if: -> { npm? || nuget? }
|
||||
validates :name, format: { with: Gitlab::Regex.conan_recipe_component_regex }, if: :conan?
|
||||
validates :version, format: { with: Gitlab::Regex.conan_recipe_component_regex }, if: :conan?
|
||||
validates :version, format: { with: Gitlab::Regex.maven_version_regex }, if: -> { version? && maven? }
|
||||
|
||||
enum package_type: { maven: 1, npm: 2, conan: 3, nuget: 4, pypi: 5, composer: 6 }
|
||||
|
||||
scope :with_name, ->(name) { where(name: name) }
|
||||
scope :with_name_like, ->(name) { where(arel_table[:name].matches(name)) }
|
||||
scope :search_by_name, ->(query) { fuzzy_search(query, [:name], use_minimum_char_limit: false) }
|
||||
scope :with_version, ->(version) { where(version: version) }
|
||||
scope :without_version_like, -> (version) { where.not(arel_table[:version].matches(version)) }
|
||||
scope :with_package_type, ->(package_type) { where(package_type: package_type) }
|
||||
|
||||
scope :with_conan_channel, ->(package_channel) do
|
||||
joins(:conan_metadatum).where(packages_conan_metadata: { package_channel: package_channel })
|
||||
end
|
||||
scope :with_conan_username, ->(package_username) do
|
||||
joins(:conan_metadatum).where(packages_conan_metadata: { package_username: package_username })
|
||||
end
|
||||
|
||||
scope :with_composer_target, -> (target) do
|
||||
includes(:composer_metadatum)
|
||||
.joins(:composer_metadatum)
|
||||
.where(Packages::Composer::Metadatum.table_name => { target_sha: target })
|
||||
end
|
||||
scope :preload_composer, -> { preload(:composer_metadatum) }
|
||||
|
||||
scope :without_nuget_temporary_name, -> { where.not(name: Packages::Nuget::CreatePackageService::TEMPORARY_PACKAGE_NAME) }
|
||||
|
||||
scope :has_version, -> { where.not(version: nil) }
|
||||
scope :processed, -> do
|
||||
where.not(package_type: :nuget).or(
|
||||
where.not(name: Packages::Nuget::CreatePackageService::TEMPORARY_PACKAGE_NAME)
|
||||
)
|
||||
end
|
||||
scope :preload_files, -> { preload(:package_files) }
|
||||
scope :last_of_each_version, -> { where(id: all.select('MAX(id) AS id').group(:version)) }
|
||||
scope :limit_recent, ->(limit) { order_created_desc.limit(limit) }
|
||||
scope :select_distinct_name, -> { select(:name).distinct }
|
||||
|
||||
# Sorting
|
||||
scope :order_created, -> { reorder('created_at ASC') }
|
||||
scope :order_created_desc, -> { reorder('created_at DESC') }
|
||||
scope :order_name, -> { reorder('name ASC') }
|
||||
scope :order_name_desc, -> { reorder('name DESC') }
|
||||
scope :order_version, -> { reorder('version ASC') }
|
||||
scope :order_version_desc, -> { reorder('version DESC') }
|
||||
scope :order_type, -> { reorder('package_type ASC') }
|
||||
scope :order_type_desc, -> { reorder('package_type DESC') }
|
||||
scope :order_project_name, -> { joins(:project).reorder('projects.name ASC') }
|
||||
scope :order_project_name_desc, -> { joins(:project).reorder('projects.name DESC') }
|
||||
scope :order_project_path, -> { joins(:project).reorder('projects.path ASC, id ASC') }
|
||||
scope :order_project_path_desc, -> { joins(:project).reorder('projects.path DESC, id DESC') }
|
||||
|
||||
def self.for_projects(projects)
|
||||
return none unless projects.any?
|
||||
|
||||
where(project_id: projects)
|
||||
end
|
||||
|
||||
def self.only_maven_packages_with_path(path)
|
||||
joins(:maven_metadatum).where(packages_maven_metadata: { path: path })
|
||||
end
|
||||
|
||||
def self.by_name_and_file_name(name, file_name)
|
||||
with_name(name)
|
||||
.joins(:package_files)
|
||||
.where(packages_package_files: { file_name: file_name }).last!
|
||||
end
|
||||
|
||||
def self.by_file_name_and_sha256(file_name, sha256)
|
||||
joins(:package_files)
|
||||
.where(packages_package_files: { file_name: file_name, file_sha256: sha256 }).last!
|
||||
end
|
||||
|
||||
def self.pluck_names
|
||||
pluck(:name)
|
||||
end
|
||||
|
||||
def self.pluck_versions
|
||||
pluck(:version)
|
||||
end
|
||||
|
||||
def self.sort_by_attribute(method)
|
||||
case method.to_s
|
||||
when 'created_asc' then order_created
|
||||
when 'created_at_asc' then order_created
|
||||
when 'name_asc' then order_name
|
||||
when 'name_desc' then order_name_desc
|
||||
when 'version_asc' then order_version
|
||||
when 'version_desc' then order_version_desc
|
||||
when 'type_asc' then order_type
|
||||
when 'type_desc' then order_type_desc
|
||||
when 'project_name_asc' then order_project_name
|
||||
when 'project_name_desc' then order_project_name_desc
|
||||
when 'project_path_asc' then order_project_path
|
||||
when 'project_path_desc' then order_project_path_desc
|
||||
else
|
||||
order_created_desc
|
||||
end
|
||||
end
|
||||
|
||||
def versions
|
||||
project.packages
|
||||
.with_name(name)
|
||||
.where.not(version: version)
|
||||
.with_package_type(package_type)
|
||||
.order(:version)
|
||||
end
|
||||
|
||||
def pipeline
|
||||
build_info&.pipeline
|
||||
end
|
||||
|
||||
def tag_names
|
||||
tags.pluck(:name)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def valid_conan_package_recipe
|
||||
recipe_exists = project.packages
|
||||
.conan
|
||||
.includes(:conan_metadatum)
|
||||
.with_name(name)
|
||||
.with_version(version)
|
||||
.with_conan_channel(conan_metadatum.package_channel)
|
||||
.with_conan_username(conan_metadatum.package_username)
|
||||
.id_not_in(id)
|
||||
.exists?
|
||||
|
||||
errors.add(:base, _('Package recipe already exists')) if recipe_exists
|
||||
end
|
||||
|
||||
def valid_composer_global_name
|
||||
# .default_scoped is required here due to a bug in rails that leaks
|
||||
# the scope and adds `self` to the query incorrectly
|
||||
# See https://github.com/rails/rails/pull/35186
|
||||
if Packages::Package.default_scoped.composer.with_name(name).where.not(project_id: project_id).exists?
|
||||
errors.add(:name, 'is already taken by another project')
|
||||
end
|
||||
end
|
||||
|
||||
def valid_npm_package_name
|
||||
return unless project&.root_namespace
|
||||
|
||||
unless name =~ %r{\A@#{project.root_namespace.path}/[^/]+\z}
|
||||
errors.add(:name, 'is not valid')
|
||||
end
|
||||
end
|
||||
|
||||
def package_already_taken
|
||||
return unless project
|
||||
|
||||
if project.package_already_taken?(name)
|
||||
errors.add(:base, _('Package already exists'))
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,56 @@
|
|||
# frozen_string_literal: true
|
||||
class Packages::PackageFile < ApplicationRecord
|
||||
include UpdateProjectStatistics
|
||||
|
||||
delegate :project, :project_id, to: :package
|
||||
delegate :conan_file_type, to: :conan_file_metadatum
|
||||
|
||||
belongs_to :package
|
||||
|
||||
has_one :conan_file_metadatum, inverse_of: :package_file, class_name: 'Packages::Conan::FileMetadatum'
|
||||
|
||||
accepts_nested_attributes_for :conan_file_metadatum
|
||||
|
||||
validates :package, presence: true
|
||||
validates :file, presence: true
|
||||
validates :file_name, presence: true
|
||||
|
||||
scope :recent, -> { order(id: :desc) }
|
||||
scope :with_file_name, ->(file_name) { where(file_name: file_name) }
|
||||
scope :with_file_name_like, ->(file_name) { where(arel_table[:file_name].matches(file_name)) }
|
||||
scope :with_files_stored_locally, -> { where(file_store: ::Packages::PackageFileUploader::Store::LOCAL) }
|
||||
scope :preload_conan_file_metadata, -> { preload(:conan_file_metadatum) }
|
||||
|
||||
scope :with_conan_file_type, ->(file_type) do
|
||||
joins(:conan_file_metadatum)
|
||||
.where(packages_conan_file_metadata: { conan_file_type: ::Packages::Conan::FileMetadatum.conan_file_types[file_type] })
|
||||
end
|
||||
|
||||
scope :with_conan_package_reference, ->(conan_package_reference) do
|
||||
joins(:conan_file_metadatum)
|
||||
.where(packages_conan_file_metadata: { conan_package_reference: conan_package_reference })
|
||||
end
|
||||
|
||||
mount_uploader :file, Packages::PackageFileUploader
|
||||
|
||||
after_save :update_file_metadata, if: :saved_change_to_file?
|
||||
|
||||
update_project_statistics project_statistics_name: :packages_size
|
||||
|
||||
def update_file_metadata
|
||||
# The file.object_store is set during `uploader.store!`
|
||||
# which happens after object is inserted/updated
|
||||
self.update_column(:file_store, file.object_store)
|
||||
self.update_column(:size, file.size) unless file.size == self.size
|
||||
end
|
||||
|
||||
def download_path
|
||||
Gitlab::Routing.url_helpers.download_project_package_file_path(project, self)
|
||||
end
|
||||
|
||||
def local?
|
||||
file_store == ::Packages::PackageFileUploader::Store::LOCAL
|
||||
end
|
||||
end
|
||||
|
||||
Packages::PackageFile.prepend_if_ee('EE::Packages::PackageFileGeo')
|
|
@ -0,0 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
module Packages
|
||||
module Pypi
|
||||
def self.table_name_prefix
|
||||
'packages_pypi_'
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Packages::Pypi::Metadatum < ApplicationRecord
|
||||
self.primary_key = :package_id
|
||||
|
||||
belongs_to :package, -> { where(package_type: :pypi) }, inverse_of: :pypi_metadatum
|
||||
|
||||
validates :package, presence: true
|
||||
|
||||
validate :pypi_package_type
|
||||
|
||||
private
|
||||
|
||||
def pypi_package_type
|
||||
unless package&.pypi?
|
||||
errors.add(:base, _('Package type must be PyPi'))
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,54 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Packages::SemVer
|
||||
attr_accessor :major, :minor, :patch, :prerelease, :build
|
||||
|
||||
def initialize(major = 0, minor = 0, patch = 0, prerelease = nil, build = nil, prefixed: false)
|
||||
@major = major
|
||||
@minor = minor
|
||||
@patch = patch
|
||||
@prerelease = prerelease
|
||||
@build = build
|
||||
@prefixed = prefixed
|
||||
end
|
||||
|
||||
def prefixed?
|
||||
@prefixed
|
||||
end
|
||||
|
||||
def ==(other)
|
||||
self.class == other.class &&
|
||||
self.major == other.major &&
|
||||
self.minor == other.minor &&
|
||||
self.patch == other.patch &&
|
||||
self.prerelease == other.prerelease &&
|
||||
self.build == other.build
|
||||
end
|
||||
|
||||
def to_s
|
||||
s = "#{prefixed? ? 'v' : ''}#{major || 0}.#{minor || 0}.#{patch || 0}"
|
||||
s += "-#{prerelease}" if prerelease
|
||||
s += "+#{build}" if build
|
||||
|
||||
s
|
||||
end
|
||||
|
||||
def self.match(str, prefixed: false)
|
||||
return unless str&.start_with?('v') == prefixed
|
||||
|
||||
str = str[1..] if prefixed
|
||||
|
||||
Gitlab::Regex.semver_regex.match(str)
|
||||
end
|
||||
|
||||
def self.match?(str, prefixed: false)
|
||||
!match(str, prefixed: prefixed).nil?
|
||||
end
|
||||
|
||||
def self.parse(str, prefixed: false)
|
||||
m = match str, prefixed: prefixed
|
||||
return unless m
|
||||
|
||||
new(m[1].to_i, m[2].to_i, m[3].to_i, m[4], m[5], prefixed: prefixed)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,18 @@
|
|||
# frozen_string_literal: true
|
||||
class Packages::Tag < ApplicationRecord
|
||||
belongs_to :package, inverse_of: :tags
|
||||
|
||||
validates :package, :name, presence: true
|
||||
|
||||
FOR_PACKAGES_TAGS_LIMIT = 200.freeze
|
||||
NUGET_TAGS_SEPARATOR = ' ' # https://docs.microsoft.com/en-us/nuget/reference/nuspec#tags
|
||||
|
||||
scope :preload_package, -> { preload(:package) }
|
||||
scope :with_name, -> (name) { where(name: name) }
|
||||
|
||||
def self.for_packages(packages)
|
||||
where(package_id: packages.select(:id))
|
||||
.order(updated_at: :desc)
|
||||
.limit(FOR_PACKAGES_TAGS_LIMIT)
|
||||
end
|
||||
end
|
|
@ -190,6 +190,10 @@ class Project < ApplicationRecord
|
|||
has_many :forks, through: :forked_to_members, source: :project, inverse_of: :forked_from_project
|
||||
has_many :fork_network_projects, through: :fork_network, source: :projects
|
||||
|
||||
# Packages
|
||||
has_many :packages, class_name: 'Packages::Package'
|
||||
has_many :package_files, through: :packages, class_name: 'Packages::PackageFile'
|
||||
|
||||
has_one :import_state, autosave: true, class_name: 'ProjectImportState', inverse_of: :project
|
||||
has_one :import_export_upload, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
|
||||
has_many :export_jobs, class_name: 'ProjectExportJob'
|
||||
|
@ -1700,10 +1704,10 @@ class Project < ApplicationRecord
|
|||
|
||||
def pages_url
|
||||
url = pages_group_url
|
||||
url_path = full_path.partition('/').last.downcase
|
||||
url_path = full_path.partition('/').last
|
||||
|
||||
# If the project path is the same as host, we serve it as group page
|
||||
return url if url == "#{Settings.pages.protocol}://#{url_path}"
|
||||
return url if url == "#{Settings.pages.protocol}://#{url_path}".downcase
|
||||
|
||||
"#{url}/#{url_path}"
|
||||
end
|
||||
|
@ -2421,6 +2425,22 @@ class Project < ApplicationRecord
|
|||
end
|
||||
alias_method :service_desk_enabled?, :service_desk_enabled
|
||||
|
||||
def root_namespace
|
||||
if namespace.has_parent?
|
||||
namespace.root_ancestor
|
||||
else
|
||||
namespace
|
||||
end
|
||||
end
|
||||
|
||||
def package_already_taken?(package_name)
|
||||
namespace.root_ancestor.all_projects
|
||||
.joins(:packages)
|
||||
.where.not(id: id)
|
||||
.merge(Packages::Package.with_name(package_name))
|
||||
.exists?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_service(services, name)
|
||||
|
|
|
@ -11,6 +11,7 @@ class PrometheusMetric < ApplicationRecord
|
|||
validates :group, presence: true
|
||||
validates :y_label, presence: true
|
||||
validates :unit, presence: true
|
||||
validates :identifier, uniqueness: { scope: :project_id }, allow_nil: true
|
||||
|
||||
validates :project, presence: true, unless: :common?
|
||||
validates :project, absence: true, if: :common?
|
||||
|
|
|
@ -13,8 +13,7 @@ class ClusterablePresenter < Gitlab::View::Presenter::Delegated
|
|||
end
|
||||
|
||||
def can_add_cluster?
|
||||
can?(current_user, :add_cluster, clusterable) &&
|
||||
(has_no_clusters? || multiple_clusters_available?)
|
||||
can?(current_user, :add_cluster, clusterable)
|
||||
end
|
||||
|
||||
def can_create_cluster?
|
||||
|
@ -81,17 +80,6 @@ class ClusterablePresenter < Gitlab::View::Presenter::Delegated
|
|||
def learn_more_link
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Overridden on EE module
|
||||
def multiple_clusters_available?
|
||||
false
|
||||
end
|
||||
|
||||
def has_no_clusters?
|
||||
clusterable.clusters.empty?
|
||||
end
|
||||
end
|
||||
|
||||
ClusterablePresenter.prepend_if_ee('EE::ClusterablePresenter')
|
||||
|
|
|
@ -19,10 +19,6 @@ module Clusters
|
|||
|
||||
cluster = Clusters::Cluster.new(cluster_params)
|
||||
|
||||
unless can_create_cluster?
|
||||
cluster.errors.add(:base, _('Instance does not support multiple Kubernetes clusters'))
|
||||
end
|
||||
|
||||
validate_management_project_permissions(cluster)
|
||||
|
||||
return cluster if cluster.errors.present?
|
||||
|
@ -55,16 +51,9 @@ module Clusters
|
|||
end
|
||||
end
|
||||
|
||||
# EE would override this method
|
||||
def can_create_cluster?
|
||||
clusterable.clusters.empty?
|
||||
end
|
||||
|
||||
def validate_management_project_permissions(cluster)
|
||||
Clusters::Management::ValidateManagementProjectPermissionsService.new(current_user)
|
||||
.execute(cluster, params[:management_project_id])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Clusters::CreateService.prepend_if_ee('EE::Clusters::CreateService')
|
||||
|
|
|
@ -10,29 +10,29 @@ module Metrics
|
|||
|
||||
ALLOWED_FILE_TYPE = '.yml'
|
||||
USER_DASHBOARDS_DIR = ::Metrics::Dashboard::CustomDashboardService::DASHBOARD_ROOT
|
||||
SEQUENCES = {
|
||||
::Metrics::Dashboard::SystemDashboardService::DASHBOARD_PATH => [
|
||||
::Gitlab::Metrics::Dashboard::Stages::CommonMetricsInserter,
|
||||
::Gitlab::Metrics::Dashboard::Stages::CustomMetricsInserter,
|
||||
::Gitlab::Metrics::Dashboard::Stages::Sorter
|
||||
].freeze,
|
||||
|
||||
::Metrics::Dashboard::SelfMonitoringDashboardService::DASHBOARD_PATH => [
|
||||
::Gitlab::Metrics::Dashboard::Stages::CustomMetricsInserter
|
||||
].freeze,
|
||||
|
||||
::Metrics::Dashboard::ClusterDashboardService::DASHBOARD_PATH => [
|
||||
::Gitlab::Metrics::Dashboard::Stages::CommonMetricsInserter,
|
||||
::Gitlab::Metrics::Dashboard::Stages::Sorter
|
||||
].freeze
|
||||
}.freeze
|
||||
|
||||
steps :check_push_authorized,
|
||||
:check_branch_name,
|
||||
:check_file_type,
|
||||
:check_dashboard_template,
|
||||
:create_file,
|
||||
:refresh_repository_method_caches
|
||||
|
||||
class << self
|
||||
def sequences
|
||||
@sequences ||= {
|
||||
::Metrics::Dashboard::SystemDashboardService::DASHBOARD_PATH => [
|
||||
::Gitlab::Metrics::Dashboard::Stages::CommonMetricsInserter,
|
||||
::Gitlab::Metrics::Dashboard::Stages::CustomMetricsInserter,
|
||||
::Gitlab::Metrics::Dashboard::Stages::Sorter
|
||||
].freeze,
|
||||
|
||||
::Metrics::Dashboard::SelfMonitoringDashboardService::DASHBOARD_PATH => [
|
||||
::Gitlab::Metrics::Dashboard::Stages::CustomMetricsInserter
|
||||
].freeze
|
||||
}.freeze
|
||||
end
|
||||
end
|
||||
:check_branch_name,
|
||||
:check_file_type,
|
||||
:check_dashboard_template,
|
||||
:create_file,
|
||||
:refresh_repository_method_caches
|
||||
|
||||
def execute
|
||||
execute_steps
|
||||
|
@ -173,10 +173,8 @@ module Metrics
|
|||
end
|
||||
|
||||
def sequence
|
||||
self.class.sequences[dashboard_template] || []
|
||||
SEQUENCES[dashboard_template] || []
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Metrics::Dashboard::CloneDashboardService.prepend_if_ee('EE::Metrics::Dashboard::CloneDashboardService')
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Fetches the system metrics dashboard and formats the output.
|
||||
# Use Gitlab::Metrics::Dashboard::Finder to retrive dashboards.
|
||||
module Metrics
|
||||
module Dashboard
|
||||
class ClusterDashboardService < ::Metrics::Dashboard::PredefinedDashboardService
|
||||
DASHBOARD_PATH = 'config/prometheus/cluster_metrics.yml'
|
||||
DASHBOARD_NAME = 'Cluster'
|
||||
|
||||
SEQUENCE = [
|
||||
STAGES::ClusterEndpointInserter,
|
||||
STAGES::PanelIdsInserter,
|
||||
STAGES::Sorter
|
||||
].freeze
|
||||
|
||||
class << self
|
||||
def valid_params?(params)
|
||||
# support selecting this service by cluster id via .find
|
||||
# Use super to support selecting this service by dashboard_path via .find_raw
|
||||
(params[:cluster].present? && params[:embedded] != 'true') || super
|
||||
end
|
||||
end
|
||||
|
||||
# Permissions are handled at the controller level
|
||||
def allowed?
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,37 @@
|
|||
# frozen_string_literal: true
|
||||
#
|
||||
module Metrics
|
||||
module Dashboard
|
||||
class ClusterMetricsEmbedService < Metrics::Dashboard::DynamicEmbedService
|
||||
class << self
|
||||
def valid_params?(params)
|
||||
[
|
||||
params[:cluster],
|
||||
embedded?(params[:embedded]),
|
||||
params[:group].present?,
|
||||
params[:title].present?,
|
||||
params[:y_label].present?
|
||||
].all?
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Permissions are handled at the controller level
|
||||
def allowed?
|
||||
true
|
||||
end
|
||||
|
||||
def dashboard_path
|
||||
::Metrics::Dashboard::ClusterDashboardService::DASHBOARD_PATH
|
||||
end
|
||||
|
||||
def sequence
|
||||
[
|
||||
STAGES::ClusterEndpointInserter,
|
||||
STAGES::PanelIdsInserter
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,31 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Packages
|
||||
module Conan
|
||||
class CreatePackageFileService
|
||||
attr_reader :package, :file, :params
|
||||
|
||||
def initialize(package, file, params)
|
||||
@package = package
|
||||
@file = file
|
||||
@params = params
|
||||
end
|
||||
|
||||
def execute
|
||||
package.package_files.create!(
|
||||
file: file,
|
||||
size: params['file.size'],
|
||||
file_name: params[:file_name],
|
||||
file_sha1: params['file.sha1'],
|
||||
file_md5: params['file.md5'],
|
||||
conan_file_metadatum_attributes: {
|
||||
recipe_revision: params[:recipe_revision],
|
||||
package_revision: params[:package_revision],
|
||||
conan_package_reference: params[:conan_package_reference],
|
||||
conan_file_type: params[:conan_file_type]
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Packages
|
||||
module Conan
|
||||
class CreatePackageService < BaseService
|
||||
def execute
|
||||
project.packages.create!(
|
||||
name: params[:package_name],
|
||||
version: params[:package_version],
|
||||
package_type: :conan,
|
||||
conan_metadatum_attributes: {
|
||||
package_username: params[:package_username],
|
||||
package_channel: params[:package_channel]
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,58 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Packages
|
||||
module Conan
|
||||
class SearchService < BaseService
|
||||
include ActiveRecord::Sanitization::ClassMethods
|
||||
|
||||
WILDCARD = '*'
|
||||
RECIPE_SEPARATOR = '@'
|
||||
|
||||
def initialize(user, params)
|
||||
super(nil, user, params)
|
||||
end
|
||||
|
||||
def execute
|
||||
ServiceResponse.success(payload: { results: search_results })
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def search_results
|
||||
return [] if wildcard_query?
|
||||
|
||||
return search_for_single_package(sanitized_query) if params[:query].include?(RECIPE_SEPARATOR)
|
||||
|
||||
search_packages(build_query)
|
||||
end
|
||||
|
||||
def wildcard_query?
|
||||
params[:query] == WILDCARD
|
||||
end
|
||||
|
||||
def build_query
|
||||
return "#{sanitized_query}%" if params[:query].end_with?(WILDCARD)
|
||||
|
||||
sanitized_query
|
||||
end
|
||||
|
||||
def search_packages(query)
|
||||
::Packages::Conan::PackageFinder.new(current_user, query: query).execute.map(&:conan_recipe)
|
||||
end
|
||||
|
||||
def search_for_single_package(query)
|
||||
name, version, username, _ = query.split(/[@\/]/)
|
||||
full_path = Packages::Conan::Metadatum.full_path_from(package_username: username)
|
||||
project = Project.find_by_full_path(full_path)
|
||||
return unless current_user.can?(:read_package, project)
|
||||
|
||||
result = project.packages.with_name(name).with_version(version).order_created.last
|
||||
[result&.conan_recipe].compact
|
||||
end
|
||||
|
||||
def sanitized_query
|
||||
@sanitized_query ||= sanitize_sql_like(params[:query].delete(WILDCARD))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,82 @@
|
|||
# frozen_string_literal: true
|
||||
module Packages
|
||||
class CreateDependencyService < BaseService
|
||||
attr_reader :package, :dependencies
|
||||
|
||||
def initialize(package, dependencies)
|
||||
@package = package
|
||||
@dependencies = dependencies
|
||||
end
|
||||
|
||||
def execute
|
||||
Packages::DependencyLink.dependency_types.each_key do |type|
|
||||
create_dependency(type)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_dependency(type)
|
||||
return unless dependencies[type].is_a?(Hash)
|
||||
|
||||
names_and_version_patterns = dependencies[type]
|
||||
existing_ids, existing_names = find_existing_ids_and_names(names_and_version_patterns)
|
||||
dependencies_to_insert = names_and_version_patterns
|
||||
|
||||
if existing_names.any?
|
||||
dependencies_to_insert = names_and_version_patterns.reject { |k, _| k.in?(existing_names) }
|
||||
end
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
inserted_ids = bulk_insert_package_dependencies(dependencies_to_insert)
|
||||
bulk_insert_package_dependency_links(type, (existing_ids + inserted_ids))
|
||||
end
|
||||
end
|
||||
|
||||
def find_existing_ids_and_names(names_and_version_patterns)
|
||||
ids_and_names = Packages::Dependency.for_package_names_and_version_patterns(names_and_version_patterns)
|
||||
.pluck_ids_and_names
|
||||
ids = ids_and_names.map(&:first) || []
|
||||
names = ids_and_names.map(&:second) || []
|
||||
[ids, names]
|
||||
end
|
||||
|
||||
def bulk_insert_package_dependencies(names_and_version_patterns)
|
||||
return [] if names_and_version_patterns.empty?
|
||||
|
||||
rows = names_and_version_patterns.map do |name, version_pattern|
|
||||
{
|
||||
name: name,
|
||||
version_pattern: version_pattern
|
||||
}
|
||||
end
|
||||
|
||||
ids = database.bulk_insert(Packages::Dependency.table_name, rows, return_ids: true, on_conflict: :do_nothing)
|
||||
return ids if ids.size == names_and_version_patterns.size
|
||||
|
||||
Packages::Dependency.uncached do
|
||||
# The bulk_insert statement above do not dirty the query cache. To make
|
||||
# sure that the results are fresh from the database and not from a stalled
|
||||
# and potentially wrong cache, this query has to be done with the query
|
||||
# chache disabled.
|
||||
Packages::Dependency.ids_for_package_names_and_version_patterns(names_and_version_patterns)
|
||||
end
|
||||
end
|
||||
|
||||
def bulk_insert_package_dependency_links(type, dependency_ids)
|
||||
rows = dependency_ids.map do |dependency_id|
|
||||
{
|
||||
package_id: package.id,
|
||||
dependency_id: dependency_id,
|
||||
dependency_type: Packages::DependencyLink.dependency_types[type.to_s]
|
||||
}
|
||||
end
|
||||
|
||||
database.bulk_insert(Packages::DependencyLink.table_name, rows)
|
||||
end
|
||||
|
||||
def database
|
||||
::Gitlab::Database
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,22 @@
|
|||
# frozen_string_literal: true
|
||||
module Packages
|
||||
class CreatePackageFileService
|
||||
attr_reader :package, :params
|
||||
|
||||
def initialize(package, params)
|
||||
@package = package
|
||||
@params = params
|
||||
end
|
||||
|
||||
def execute
|
||||
package.package_files.create!(
|
||||
file: params[:file],
|
||||
size: params[:size],
|
||||
file_name: params[:file_name],
|
||||
file_sha1: params[:file_sha1],
|
||||
file_sha256: params[:file_sha256],
|
||||
file_md5: params[:file_md5]
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,28 @@
|
|||
# frozen_string_literal: true
|
||||
module Packages
|
||||
module Maven
|
||||
class CreatePackageService < BaseService
|
||||
def execute
|
||||
app_group, _, app_name = params[:name].rpartition('/')
|
||||
app_group.tr!('/', '.')
|
||||
|
||||
package = project.packages.create!(
|
||||
name: params[:name],
|
||||
version: params[:version],
|
||||
package_type: :maven,
|
||||
maven_metadatum_attributes: {
|
||||
path: params[:path],
|
||||
app_group: app_group,
|
||||
app_name: app_name,
|
||||
app_version: params[:version]
|
||||
}
|
||||
)
|
||||
|
||||
build = params[:build]
|
||||
package.create_build_info!(pipeline: build.pipeline) if build.present?
|
||||
|
||||
package
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,41 @@
|
|||
# frozen_string_literal: true
|
||||
module Packages
|
||||
module Maven
|
||||
class FindOrCreatePackageService < BaseService
|
||||
MAVEN_METADATA_FILE = 'maven-metadata.xml'.freeze
|
||||
|
||||
def execute
|
||||
package = ::Packages::Maven::PackageFinder
|
||||
.new(params[:path], current_user, project: project).execute
|
||||
|
||||
unless package
|
||||
if params[:file_name] == MAVEN_METADATA_FILE
|
||||
# Maven uploads several files during `mvn deploy` in next order:
|
||||
# - my-company/my-app/1.0-SNAPSHOT/my-app.jar
|
||||
# - my-company/my-app/1.0-SNAPSHOT/my-app.pom
|
||||
# - my-company/my-app/1.0-SNAPSHOT/maven-metadata.xml
|
||||
# - my-company/my-app/maven-metadata.xml
|
||||
#
|
||||
# The last xml file does not have VERSION in URL because it contains
|
||||
# information about all versions.
|
||||
package_name, version = params[:path], nil
|
||||
else
|
||||
package_name, _, version = params[:path].rpartition('/')
|
||||
end
|
||||
|
||||
package_params = {
|
||||
name: package_name,
|
||||
path: params[:path],
|
||||
version: version,
|
||||
build: params[:build]
|
||||
}
|
||||
|
||||
package = ::Packages::Maven::CreatePackageService
|
||||
.new(project, current_user, package_params).execute
|
||||
end
|
||||
|
||||
package
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,91 @@
|
|||
# frozen_string_literal: true
|
||||
module Packages
|
||||
module Npm
|
||||
class CreatePackageService < BaseService
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
def execute
|
||||
return error('Version is empty.', 400) if version.blank?
|
||||
return error('Package already exists.', 403) if current_package_exists?
|
||||
|
||||
ActiveRecord::Base.transaction { create_package! }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_package!
|
||||
package = project.packages.create!(
|
||||
name: name,
|
||||
version: version,
|
||||
package_type: 'npm'
|
||||
)
|
||||
|
||||
if build.present?
|
||||
package.create_build_info!(pipeline: build.pipeline)
|
||||
end
|
||||
|
||||
::Packages::CreatePackageFileService.new(package, file_params).execute
|
||||
::Packages::CreateDependencyService.new(package, package_dependencies).execute
|
||||
::Packages::Npm::CreateTagService.new(package, dist_tag).execute
|
||||
|
||||
package
|
||||
end
|
||||
|
||||
def current_package_exists?
|
||||
project.packages
|
||||
.npm
|
||||
.with_name(name)
|
||||
.with_version(version)
|
||||
.exists?
|
||||
end
|
||||
|
||||
def name
|
||||
params[:name]
|
||||
end
|
||||
|
||||
def version
|
||||
strong_memoize(:version) do
|
||||
params[:versions].each_key.first
|
||||
end
|
||||
end
|
||||
|
||||
def version_data
|
||||
params[:versions][version]
|
||||
end
|
||||
|
||||
def build
|
||||
params[:build]
|
||||
end
|
||||
|
||||
def dist_tag
|
||||
params['dist-tags'].each_key.first
|
||||
end
|
||||
|
||||
def package_file_name
|
||||
strong_memoize(:package_file_name) do
|
||||
"#{name}-#{version}.tgz"
|
||||
end
|
||||
end
|
||||
|
||||
def attachment
|
||||
strong_memoize(:attachment) do
|
||||
params['_attachments'][package_file_name]
|
||||
end
|
||||
end
|
||||
|
||||
def file_params
|
||||
{
|
||||
file: CarrierWaveStringFile.new(Base64.decode64(attachment['data'])),
|
||||
size: attachment['length'],
|
||||
file_sha1: version_data[:dist][:shasum],
|
||||
file_name: package_file_name
|
||||
}
|
||||
end
|
||||
|
||||
def package_dependencies
|
||||
_version, versions_data = params[:versions].first
|
||||
versions_data
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,34 @@
|
|||
# frozen_string_literal: true
|
||||
module Packages
|
||||
module Npm
|
||||
class CreateTagService
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
attr_reader :package, :tag_name
|
||||
|
||||
def initialize(package, tag_name)
|
||||
@package = package
|
||||
@tag_name = tag_name
|
||||
end
|
||||
|
||||
def execute
|
||||
if existing_tag.present?
|
||||
existing_tag.update_column(:package_id, package.id)
|
||||
existing_tag
|
||||
else
|
||||
package.tags.create!(name: tag_name)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def existing_tag
|
||||
strong_memoize(:existing_tag) do
|
||||
Packages::TagsFinder
|
||||
.new(package.project, package.name, package_type: package.package_type)
|
||||
.find_by_name(tag_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,71 @@
|
|||
# frozen_string_literal: true
|
||||
module Packages
|
||||
module Nuget
|
||||
class CreateDependencyService < BaseService
|
||||
def initialize(package, dependencies = [])
|
||||
@package = package
|
||||
@dependencies = dependencies
|
||||
end
|
||||
|
||||
def execute
|
||||
return if @dependencies.empty?
|
||||
|
||||
@package.transaction do
|
||||
create_dependency_links
|
||||
create_dependency_link_metadata
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_dependency_links
|
||||
::Packages::CreateDependencyService
|
||||
.new(@package, dependencies_for_create_dependency_service)
|
||||
.execute
|
||||
end
|
||||
|
||||
def create_dependency_link_metadata
|
||||
inserted_links = ::Packages::DependencyLink.preload_dependency
|
||||
.for_package(@package)
|
||||
|
||||
return if inserted_links.empty?
|
||||
|
||||
rows = inserted_links.map do |dependency_link|
|
||||
raw_dependency = raw_dependency_for(dependency_link.dependency)
|
||||
|
||||
next if raw_dependency[:target_framework].blank?
|
||||
|
||||
{
|
||||
dependency_link_id: dependency_link.id,
|
||||
target_framework: raw_dependency[:target_framework]
|
||||
}
|
||||
end
|
||||
|
||||
::Gitlab::Database.bulk_insert(::Packages::Nuget::DependencyLinkMetadatum.table_name, rows.compact)
|
||||
end
|
||||
|
||||
def raw_dependency_for(dependency)
|
||||
name = dependency.name
|
||||
version = dependency.version_pattern.presence
|
||||
|
||||
@dependencies.find do |raw_dependency|
|
||||
raw_dependency[:name] == name && raw_dependency[:version] == version
|
||||
end
|
||||
end
|
||||
|
||||
def dependencies_for_create_dependency_service
|
||||
names_and_versions = @dependencies.map do |dependency|
|
||||
[dependency[:name], version_or_empty_string(dependency[:version])]
|
||||
end.to_h
|
||||
|
||||
{ 'dependencies' => names_and_versions }
|
||||
end
|
||||
|
||||
def version_or_empty_string(version)
|
||||
return '' if version.blank?
|
||||
|
||||
version
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,23 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Packages
|
||||
module Nuget
|
||||
class CreatePackageService < BaseService
|
||||
TEMPORARY_PACKAGE_NAME = 'NuGet.Temporary.Package'
|
||||
PACKAGE_VERSION = '0.0.0'
|
||||
|
||||
def execute
|
||||
project.packages.nuget.create!(
|
||||
name: TEMPORARY_PACKAGE_NAME,
|
||||
version: "#{PACKAGE_VERSION}-#{uuid}"
|
||||
)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def uuid
|
||||
SecureRandom.uuid
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,106 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Packages
|
||||
module Nuget
|
||||
class MetadataExtractionService
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
ExtractionError = Class.new(StandardError)
|
||||
|
||||
XPATHS = {
|
||||
package_name: '//xmlns:package/xmlns:metadata/xmlns:id',
|
||||
package_version: '//xmlns:package/xmlns:metadata/xmlns:version',
|
||||
license_url: '//xmlns:package/xmlns:metadata/xmlns:licenseUrl',
|
||||
project_url: '//xmlns:package/xmlns:metadata/xmlns:projectUrl',
|
||||
icon_url: '//xmlns:package/xmlns:metadata/xmlns:iconUrl'
|
||||
}.freeze
|
||||
|
||||
XPATH_DEPENDENCIES = '//xmlns:package/xmlns:metadata/xmlns:dependencies/xmlns:dependency'
|
||||
XPATH_DEPENDENCY_GROUPS = '//xmlns:package/xmlns:metadata/xmlns:dependencies/xmlns:group'
|
||||
XPATH_TAGS = '//xmlns:package/xmlns:metadata/xmlns:tags'
|
||||
|
||||
MAX_FILE_SIZE = 4.megabytes.freeze
|
||||
|
||||
def initialize(package_file_id)
|
||||
@package_file_id = package_file_id
|
||||
end
|
||||
|
||||
def execute
|
||||
raise ExtractionError.new('invalid package file') unless valid_package_file?
|
||||
|
||||
extract_metadata(nuspec_file)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def package_file
|
||||
strong_memoize(:package_file) do
|
||||
::Packages::PackageFile.find_by_id(@package_file_id)
|
||||
end
|
||||
end
|
||||
|
||||
def valid_package_file?
|
||||
package_file &&
|
||||
package_file.package&.nuget? &&
|
||||
package_file.file.size.positive?
|
||||
end
|
||||
|
||||
def extract_metadata(file)
|
||||
doc = Nokogiri::XML(file)
|
||||
|
||||
XPATHS.transform_values { |query| doc.xpath(query).text.presence }
|
||||
.compact
|
||||
.tap do |metadata|
|
||||
metadata[:package_dependencies] = extract_dependencies(doc)
|
||||
metadata[:package_tags] = extract_tags(doc)
|
||||
end
|
||||
end
|
||||
|
||||
def extract_dependencies(doc)
|
||||
dependencies = []
|
||||
|
||||
doc.xpath(XPATH_DEPENDENCIES).each do |node|
|
||||
dependencies << extract_dependency(node)
|
||||
end
|
||||
|
||||
doc.xpath(XPATH_DEPENDENCY_GROUPS).each do |group_node|
|
||||
target_framework = group_node.attr("targetFramework")
|
||||
|
||||
group_node.xpath("xmlns:dependency").each do |node|
|
||||
dependencies << extract_dependency(node).merge(target_framework: target_framework)
|
||||
end
|
||||
end
|
||||
|
||||
dependencies
|
||||
end
|
||||
|
||||
def extract_dependency(node)
|
||||
{
|
||||
name: node.attr('id'),
|
||||
version: node.attr('version')
|
||||
}.compact
|
||||
end
|
||||
|
||||
def extract_tags(doc)
|
||||
tags = doc.xpath(XPATH_TAGS).text
|
||||
|
||||
return [] if tags.blank?
|
||||
|
||||
tags.split(::Packages::Tag::NUGET_TAGS_SEPARATOR)
|
||||
end
|
||||
|
||||
def nuspec_file
|
||||
package_file.file.use_file do |file_path|
|
||||
Zip::File.open(file_path) do |zip_file|
|
||||
entry = zip_file.glob('*.nuspec').first
|
||||
|
||||
raise ExtractionError.new('nuspec file not found') unless entry
|
||||
raise ExtractionError.new('nuspec file too big') if entry.size > MAX_FILE_SIZE
|
||||
|
||||
entry.get_input_stream.read
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,101 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Packages
|
||||
module Nuget
|
||||
class SearchService < BaseService
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
include ActiveRecord::ConnectionAdapters::Quoting
|
||||
|
||||
MAX_PER_PAGE = 30
|
||||
MAX_VERSIONS_PER_PACKAGE = 10
|
||||
PRE_RELEASE_VERSION_MATCHING_TERM = '%-%'
|
||||
|
||||
DEFAULT_OPTIONS = {
|
||||
include_prerelease_versions: true,
|
||||
per_page: Kaminari.config.default_per_page,
|
||||
padding: 0
|
||||
}.freeze
|
||||
|
||||
def initialize(project, search_term, options = {})
|
||||
@project = project
|
||||
@search_term = search_term
|
||||
@options = DEFAULT_OPTIONS.merge(options)
|
||||
|
||||
raise ArgumentError, 'negative per_page' if per_page.negative?
|
||||
raise ArgumentError, 'negative padding' if padding.negative?
|
||||
end
|
||||
|
||||
def execute
|
||||
OpenStruct.new(
|
||||
total_count: package_names.total_count,
|
||||
results: search_packages
|
||||
)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def search_packages
|
||||
# custom query to get package names and versions as expected from the nuget search api
|
||||
# See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/24182#technical-notes
|
||||
# and https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource
|
||||
subquery_name = :partition_subquery
|
||||
arel_table = Arel::Table.new(:partition_subquery)
|
||||
column_names = Packages::Package.column_names.map do |cn|
|
||||
"#{subquery_name}.#{quote_column_name(cn)}"
|
||||
end
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
pkgs = Packages::Package.select(column_names.join(','))
|
||||
.from(package_names_partition, subquery_name)
|
||||
.where(arel_table[:row_number].lteq(MAX_VERSIONS_PER_PACKAGE))
|
||||
|
||||
return pkgs if include_prerelease_versions?
|
||||
|
||||
# we can't use pkgs.without_version_like since we have a custom from
|
||||
pkgs.where.not(arel_table[:version].matches(PRE_RELEASE_VERSION_MATCHING_TERM))
|
||||
end
|
||||
|
||||
def package_names_partition
|
||||
table_name = quote_table_name(Packages::Package.table_name)
|
||||
name_column = "#{table_name}.#{quote_column_name('name')}"
|
||||
created_at_column = "#{table_name}.#{quote_column_name('created_at')}"
|
||||
select_sql = "ROW_NUMBER() OVER (PARTITION BY #{name_column} ORDER BY #{created_at_column} DESC) AS row_number, #{table_name}.*"
|
||||
|
||||
@project.packages
|
||||
.select(select_sql)
|
||||
.nuget
|
||||
.has_version
|
||||
.without_nuget_temporary_name
|
||||
.with_name(package_names)
|
||||
end
|
||||
|
||||
def package_names
|
||||
strong_memoize(:package_names) do
|
||||
pkgs = @project.packages
|
||||
.nuget
|
||||
.has_version
|
||||
.without_nuget_temporary_name
|
||||
.order_name
|
||||
.select_distinct_name
|
||||
pkgs = pkgs.without_version_like(PRE_RELEASE_VERSION_MATCHING_TERM) unless include_prerelease_versions?
|
||||
pkgs = pkgs.search_by_name(@search_term) if @search_term.present?
|
||||
pkgs.page(0) # we're using a padding
|
||||
.per(per_page)
|
||||
.padding(padding)
|
||||
end
|
||||
end
|
||||
|
||||
def include_prerelease_versions?
|
||||
@options[:include_prerelease_versions]
|
||||
end
|
||||
|
||||
def padding
|
||||
@options[:padding]
|
||||
end
|
||||
|
||||
def per_page
|
||||
[@options[:per_page], MAX_PER_PAGE].min
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,50 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Packages
|
||||
module Nuget
|
||||
class SyncMetadatumService
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
def initialize(package, metadata)
|
||||
@package = package
|
||||
@metadata = metadata
|
||||
end
|
||||
|
||||
def execute
|
||||
if blank_metadata?
|
||||
metadatum.destroy! if metadatum.persisted?
|
||||
else
|
||||
metadatum.update!(
|
||||
license_url: license_url,
|
||||
project_url: project_url,
|
||||
icon_url: icon_url
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def metadatum
|
||||
strong_memoize(:metadatum) do
|
||||
@package.nuget_metadatum || @package.build_nuget_metadatum
|
||||
end
|
||||
end
|
||||
|
||||
def blank_metadata?
|
||||
project_url.blank? && license_url.blank? && icon_url.blank?
|
||||
end
|
||||
|
||||
def project_url
|
||||
@metadata[:project_url]
|
||||
end
|
||||
|
||||
def license_url
|
||||
@metadata[:license_url]
|
||||
end
|
||||
|
||||
def icon_url
|
||||
@metadata[:icon_url]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,125 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Packages
|
||||
module Nuget
|
||||
class UpdatePackageFromMetadataService
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
include ExclusiveLeaseGuard
|
||||
|
||||
# used by ExclusiveLeaseGuard
|
||||
DEFAULT_LEASE_TIMEOUT = 1.hour.to_i.freeze
|
||||
|
||||
InvalidMetadataError = Class.new(StandardError)
|
||||
|
||||
def initialize(package_file)
|
||||
@package_file = package_file
|
||||
end
|
||||
|
||||
def execute
|
||||
raise InvalidMetadataError.new('package name and/or package version not found in metadata') unless valid_metadata?
|
||||
|
||||
try_obtain_lease do
|
||||
@package_file.transaction do
|
||||
package = existing_package ? link_to_existing_package : update_linked_package
|
||||
|
||||
update_package(package)
|
||||
|
||||
# Updating file_name updates the path where the file is stored.
|
||||
# We must pass the file again so that CarrierWave can handle the update
|
||||
@package_file.update!(
|
||||
file_name: package_filename,
|
||||
file: @package_file.file
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def update_package(package)
|
||||
::Packages::Nuget::SyncMetadatumService
|
||||
.new(package, metadata.slice(:project_url, :license_url, :icon_url))
|
||||
.execute
|
||||
::Packages::UpdateTagsService
|
||||
.new(package, package_tags)
|
||||
.execute
|
||||
rescue => e
|
||||
raise InvalidMetadataError, e.message
|
||||
end
|
||||
|
||||
def valid_metadata?
|
||||
package_name.present? && package_version.present?
|
||||
end
|
||||
|
||||
def link_to_existing_package
|
||||
package_to_destroy = @package_file.package
|
||||
# Updating package_id updates the path where the file is stored.
|
||||
# We must pass the file again so that CarrierWave can handle the update
|
||||
@package_file.update!(
|
||||
package_id: existing_package.id,
|
||||
file: @package_file.file
|
||||
)
|
||||
package_to_destroy.destroy!
|
||||
existing_package
|
||||
end
|
||||
|
||||
def update_linked_package
|
||||
@package_file.package.update!(
|
||||
name: package_name,
|
||||
version: package_version
|
||||
)
|
||||
|
||||
::Packages::Nuget::CreateDependencyService.new(@package_file.package, package_dependencies)
|
||||
.execute
|
||||
@package_file.package
|
||||
end
|
||||
|
||||
def existing_package
|
||||
strong_memoize(:existing_package) do
|
||||
@package_file.project.packages
|
||||
.nuget
|
||||
.with_name(package_name)
|
||||
.with_version(package_version)
|
||||
.first
|
||||
end
|
||||
end
|
||||
|
||||
def package_name
|
||||
metadata[:package_name]
|
||||
end
|
||||
|
||||
def package_version
|
||||
metadata[:package_version]
|
||||
end
|
||||
|
||||
def package_dependencies
|
||||
metadata.fetch(:package_dependencies, [])
|
||||
end
|
||||
|
||||
def package_tags
|
||||
metadata.fetch(:package_tags, [])
|
||||
end
|
||||
|
||||
def metadata
|
||||
strong_memoize(:metadata) do
|
||||
::Packages::Nuget::MetadataExtractionService.new(@package_file.id).execute
|
||||
end
|
||||
end
|
||||
|
||||
def package_filename
|
||||
"#{package_name.downcase}.#{package_version.downcase}.nupkg"
|
||||
end
|
||||
|
||||
# used by ExclusiveLeaseGuard
|
||||
def lease_key
|
||||
package_id = existing_package ? existing_package.id : @package_file.package_id
|
||||
"packages:nuget:update_package_from_metadata_service:package:#{package_id}"
|
||||
end
|
||||
|
||||
# used by ExclusiveLeaseGuard
|
||||
def lease_timeout
|
||||
DEFAULT_LEASE_TIMEOUT
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,40 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Packages
|
||||
module Pypi
|
||||
class CreatePackageService < BaseService
|
||||
include ::Gitlab::Utils::StrongMemoize
|
||||
|
||||
def execute
|
||||
::Packages::Package.transaction do
|
||||
Packages::Pypi::Metadatum.upsert(
|
||||
package_id: created_package.id,
|
||||
required_python: params[:requires_python]
|
||||
)
|
||||
|
||||
::Packages::CreatePackageFileService.new(created_package, file_params).execute
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def created_package
|
||||
strong_memoize(:created_package) do
|
||||
project
|
||||
.packages
|
||||
.pypi
|
||||
.safe_find_or_create_by!(name: params[:name], version: params[:version])
|
||||
end
|
||||
end
|
||||
|
||||
def file_params
|
||||
{
|
||||
file: params[:content],
|
||||
file_name: params[:content].original_filename,
|
||||
file_md5: params[:md5_digest],
|
||||
file_sha256: params[:sha256_digest]
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
module Packages
|
||||
class RemoveTagService < BaseService
|
||||
attr_reader :package_tag
|
||||
|
||||
def initialize(package_tag)
|
||||
raise ArgumentError, "Package tag must be set" if package_tag.blank?
|
||||
|
||||
@package_tag = package_tag
|
||||
end
|
||||
|
||||
def execute
|
||||
package_tag.delete
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,41 @@
|
|||
# frozen_string_literal: true
|
||||
module Packages
|
||||
class UpdateTagsService
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
def initialize(package, tags = [])
|
||||
@package = package
|
||||
@tags = tags
|
||||
end
|
||||
|
||||
def execute
|
||||
return if @tags.empty?
|
||||
|
||||
tags_to_destroy = existing_tags - @tags
|
||||
tags_to_create = @tags - existing_tags
|
||||
|
||||
@package.tags.with_name(tags_to_destroy).delete_all if tags_to_destroy.any?
|
||||
::Gitlab::Database.bulk_insert(Packages::Tag.table_name, rows(tags_to_create)) if tags_to_create.any?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def existing_tags
|
||||
strong_memoize(:existing_tags) do
|
||||
@package.tag_names
|
||||
end
|
||||
end
|
||||
|
||||
def rows(tags)
|
||||
now = Time.zone.now
|
||||
tags.map do |tag|
|
||||
{
|
||||
package_id: @package.id,
|
||||
name: tag,
|
||||
created_at: now,
|
||||
updated_at: now
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,30 @@
|
|||
# frozen_string_literal: true
|
||||
class Packages::PackageFileUploader < GitlabUploader
|
||||
extend Workhorse::UploadPath
|
||||
include ObjectStorage::Concern
|
||||
|
||||
storage_options Gitlab.config.packages
|
||||
|
||||
after :store, :schedule_background_upload
|
||||
|
||||
alias_method :upload, :model
|
||||
|
||||
def filename
|
||||
model.file_name
|
||||
end
|
||||
|
||||
def store_dir
|
||||
dynamic_segment
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def dynamic_segment
|
||||
File.join(disk_hash[0..1], disk_hash[2..3], disk_hash,
|
||||
'packages', model.package.id.to_s, 'files', model.id.to_s)
|
||||
end
|
||||
|
||||
def disk_hash
|
||||
@disk_hash ||= Digest::SHA2.hexdigest(model.package.project_id.to_s)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,6 @@
|
|||
- autodevops_help_url = help_page_path('topics/autodevops/index.md', anchor: 'using-multiple-kubernetes-clusters')
|
||||
- help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe
|
||||
- help_link_end = '</a>'.html_safe
|
||||
|
||||
%p
|
||||
= s_('ClusterIntegration|If you are setting up multiple clusters and are using Auto DevOps, %{help_link_start}read this first%{help_link_end}.').html_safe % { help_link_start: help_link_start % { url: autodevops_help_url }, help_link_end: help_link_end }
|
|
@ -5,4 +5,4 @@
|
|||
%p
|
||||
= clusterable.learn_more_link
|
||||
|
||||
= render_if_exists 'clusters/multiple_clusters_message'
|
||||
= render 'clusters/clusters/multiple_clusters_message'
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#content
|
||||
= email_default_heading("#{sanitize_name(@resource.user.name)}, you've added an additional email!")
|
||||
= email_default_heading("#{sanitize_name(@resource.user.name)}, confirm your email address now!")
|
||||
%p Click the link below to confirm your email address (#{@resource.email})
|
||||
#cta
|
||||
= link_to 'Confirm your email address', confirmation_url(@resource, confirmation_token: @token)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<%= @resource.user.name %>, you've added an additional email!
|
||||
<%= @resource.user.name %>, confirm your email address now!
|
||||
|
||||
Use the link below to confirm your email address (<%= @resource.email %>)
|
||||
|
||||
|
|
|
@ -18,7 +18,8 @@
|
|||
.dropdown
|
||||
%button.dropdown-menu-toggle.js-milestone-select.js-issue-board-sidebar{ type: "button", data: { toggle: "dropdown", show_no: "true", field_name: "issue[milestone_id]", milestones: milestones_filter_path(format: :json), ability_name: "issue", use_id: "true", default_no: "true" },
|
||||
":data-selected" => "milestoneTitle",
|
||||
":data-issuable-id" => "issue.iid" }
|
||||
":data-issuable-id" => "issue.iid",
|
||||
":data-project-id" => "issue.project_id" }
|
||||
= _("Milestone")
|
||||
= icon("chevron-down")
|
||||
.dropdown-menu.dropdown-select.dropdown-menu-selectable
|
||||
|
|
|
@ -45,7 +45,8 @@
|
|||
= link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right', data: { qa_selector: "edit_milestone_link", track_label: "right_sidebar", track_property: "milestone", track_event: "click_edit_button", track_value: "" }
|
||||
.value.hide-collapsed
|
||||
- if milestone.present?
|
||||
= link_to milestone[:title], milestone[:web_url], class: "bold has-tooltip", title: sidebar_milestone_remaining_days(milestone), data: { container: "body", html: 'true', boundary: 'viewport', qa_selector: 'milestone_link', qa_title: milestone[:title] }
|
||||
- milestone_title = milestone[:expired] ? _("%{milestone_name} (Past due)").html_safe % { milestone_name: milestone[:title] } : milestone[:title]
|
||||
= link_to milestone_title, milestone[:web_url], class: "bold has-tooltip", title: sidebar_milestone_remaining_days(milestone), data: { container: "body", html: 'true', boundary: 'viewport', qa_selector: 'milestone_link', qa_title: milestone[:title] }
|
||||
- else
|
||||
%span.no-value
|
||||
= _('None')
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Show expired milestones at the bottom of the list within dropdown
|
||||
merge_request: 35595
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: 'Multiple Kubernetes clusters now available in GitLab core'
|
||||
merge_request: 35094
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add inapplicable reason in MR suggestion Tooltip
|
||||
merge_request: 35276
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix Project#pages_url not to downcase url path
|
||||
merge_request: 36183
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix routing for paths starting with help and projects
|
||||
merge_request: 36048
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Enforce prometheus metric uniqueness across project scope
|
||||
merge_request: 35566
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Include project and subgroup milestones on Roadmap page
|
||||
merge_request: 35973
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Unconfirm wrongfully verified email addresses and user accounts
|
||||
merge_request: 35492
|
||||
author:
|
||||
type: security
|
|
@ -347,12 +347,10 @@ Settings.uploads['object_store']['remote_directory'] ||= 'uploads'
|
|||
#
|
||||
# Packages
|
||||
#
|
||||
Gitlab.ee do
|
||||
Settings['packages'] ||= Settingslogic.new({})
|
||||
Settings.packages['enabled'] = true if Settings.packages['enabled'].nil?
|
||||
Settings.packages['storage_path'] = Settings.absolute(Settings.packages['storage_path'] || File.join(Settings.shared['path'], "packages"))
|
||||
Settings.packages['object_store'] = ObjectStoreSettings.legacy_parse(Settings.packages['object_store'])
|
||||
end
|
||||
Settings['packages'] ||= Settingslogic.new({})
|
||||
Settings.packages['enabled'] = true if Settings.packages['enabled'].nil?
|
||||
Settings.packages['storage_path'] = Settings.absolute(Settings.packages['storage_path'] || File.join(Settings.shared['path'], "packages"))
|
||||
Settings.packages['object_store'] = ObjectStoreSettings.legacy_parse(Settings.packages['object_store'])
|
||||
|
||||
#
|
||||
# Dependency Proxy
|
||||
|
|
|
@ -1,63 +1,40 @@
|
|||
dashboard: 'Cluster health'
|
||||
priority: 1
|
||||
panel_groups:
|
||||
- group: Cluster Health
|
||||
priority: 1
|
||||
metrics:
|
||||
priority: 10
|
||||
panels:
|
||||
- title: "CPU Usage"
|
||||
type: "area-chart"
|
||||
y_label: "CPU (cores)"
|
||||
required_metrics: ['container_cpu_usage_seconds_total']
|
||||
weight: 1
|
||||
queries:
|
||||
- query_range: 'avg(sum(rate(container_cpu_usage_seconds_total{id="/"}[15m])) by (job)) without (job)'
|
||||
label: Usage (cores)
|
||||
unit: "cores"
|
||||
appearance:
|
||||
line:
|
||||
width: 2
|
||||
area:
|
||||
opacity: 0
|
||||
- query_range: 'sum(kube_pod_container_resource_requests_cpu_cores{kubernetes_namespace="gitlab-managed-apps"})'
|
||||
label: Requested (cores)
|
||||
unit: "cores"
|
||||
appearance:
|
||||
line:
|
||||
width: 2
|
||||
area:
|
||||
opacity: 0
|
||||
- query_range: 'sum(kube_node_status_capacity_cpu_cores{kubernetes_namespace="gitlab-managed-apps"})'
|
||||
label: Capacity (cores)
|
||||
unit: "cores"
|
||||
appearance:
|
||||
line:
|
||||
type: 'dashed'
|
||||
width: 2
|
||||
area:
|
||||
opacity: 0
|
||||
- title: "Memory usage"
|
||||
metrics:
|
||||
- id: cluster_health_cpu_usage
|
||||
query_range: 'avg(sum(rate(container_cpu_usage_seconds_total{id="/"}[15m])) by (job)) without (job)'
|
||||
unit: cores
|
||||
label: Usage (cores)
|
||||
- id: cluster_health_cpu_requested
|
||||
query_range: 'sum(kube_pod_container_resource_requests_cpu_cores{kubernetes_namespace="gitlab-managed-apps"})'
|
||||
unit: cores
|
||||
label: Requested (cores)
|
||||
- id: cluster_health_cpu_capacity
|
||||
query_range: 'sum(kube_node_status_capacity_cpu_cores{kubernetes_namespace="gitlab-managed-apps"})'
|
||||
unit: cores
|
||||
label: Capacity (cores)
|
||||
- title: "Memory Usage"
|
||||
type: "area-chart"
|
||||
y_label: "Memory (GiB)"
|
||||
required_metrics: ['container_memory_usage_bytes']
|
||||
weight: 1
|
||||
queries:
|
||||
- query_range: 'avg(sum(container_memory_usage_bytes{id="/"}) by (job)) without (job) / 2^30'
|
||||
label: Usage (GiB)
|
||||
unit: "GiB"
|
||||
appearance:
|
||||
line:
|
||||
width: 2
|
||||
area:
|
||||
opacity: 0
|
||||
- query_range: 'sum(kube_pod_container_resource_requests_memory_bytes{kubernetes_namespace="gitlab-managed-apps"})/2^30'
|
||||
label: Requested (GiB)
|
||||
unit: "GiB"
|
||||
appearance:
|
||||
line:
|
||||
width: 2
|
||||
area:
|
||||
opacity: 0
|
||||
- query_range: 'sum(kube_node_status_capacity_memory_bytes{kubernetes_namespace="gitlab-managed-apps"})/2^30'
|
||||
label: Capacity (GiB)
|
||||
unit: "GiB"
|
||||
appearance:
|
||||
line:
|
||||
type: 'dashed'
|
||||
width: 2
|
||||
area:
|
||||
opacity: 0
|
||||
metrics:
|
||||
- id: cluster_health_memory_usage
|
||||
query_range: 'avg(sum(container_memory_usage_bytes{id="/"}) by (job)) without (job) / 2^30'
|
||||
unit: GiB
|
||||
label: Usage (GiB)
|
||||
- id: cluster_health_memory_requested
|
||||
query_range: 'sum(kube_pod_container_resource_requests_memory_bytes{kubernetes_namespace="gitlab-managed-apps"})/2^30'
|
||||
unit: GiB
|
||||
label: Requested (GiB)
|
||||
- id: cluster_health_memory_capacity
|
||||
query_range: 'sum(kube_node_status_capacity_memory_bytes{kubernetes_namespace="gitlab-managed-apps"})/2^30'
|
||||
unit: GiB
|
||||
label: Capacity (GiB)
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
# most likely this file can be removed, but until we are sure and have capacity to tackle that I've
|
||||
# only moved it and added https://gitlab.com/gitlab-org/gitlab/-/issues/225869 to track work need to clean up codebase.
|
||||
- group: Cluster Health
|
||||
priority: 1
|
||||
metrics:
|
||||
- title: "CPU Usage"
|
||||
y_label: "CPU (cores)"
|
||||
required_metrics: ['container_cpu_usage_seconds_total']
|
||||
weight: 1
|
||||
queries:
|
||||
- query_range: 'avg(sum(rate(container_cpu_usage_seconds_total{id="/"}[15m])) by (job)) without (job)'
|
||||
label: Usage (cores)
|
||||
unit: "cores"
|
||||
appearance:
|
||||
line:
|
||||
width: 2
|
||||
area:
|
||||
opacity: 0
|
||||
- query_range: 'sum(kube_pod_container_resource_requests_cpu_cores{kubernetes_namespace="gitlab-managed-apps"})'
|
||||
label: Requested (cores)
|
||||
unit: "cores"
|
||||
appearance:
|
||||
line:
|
||||
width: 2
|
||||
area:
|
||||
opacity: 0
|
||||
- query_range: 'sum(kube_node_status_capacity_cpu_cores{kubernetes_namespace="gitlab-managed-apps"})'
|
||||
label: Capacity (cores)
|
||||
unit: "cores"
|
||||
appearance:
|
||||
line:
|
||||
type: 'dashed'
|
||||
width: 2
|
||||
area:
|
||||
opacity: 0
|
||||
- title: "Memory usage"
|
||||
y_label: "Memory (GiB)"
|
||||
required_metrics: ['container_memory_usage_bytes']
|
||||
weight: 1
|
||||
queries:
|
||||
- query_range: 'avg(sum(container_memory_usage_bytes{id="/"}) by (job)) without (job) / 2^30'
|
||||
label: Usage (GiB)
|
||||
unit: "GiB"
|
||||
appearance:
|
||||
line:
|
||||
width: 2
|
||||
area:
|
||||
opacity: 0
|
||||
- query_range: 'sum(kube_pod_container_resource_requests_memory_bytes{kubernetes_namespace="gitlab-managed-apps"})/2^30'
|
||||
label: Requested (GiB)
|
||||
unit: "GiB"
|
||||
appearance:
|
||||
line:
|
||||
width: 2
|
||||
area:
|
||||
opacity: 0
|
||||
- query_range: 'sum(kube_node_status_capacity_memory_bytes{kubernetes_namespace="gitlab-managed-apps"})/2^30'
|
||||
label: Capacity (GiB)
|
||||
unit: "GiB"
|
||||
appearance:
|
||||
line:
|
||||
type: 'dashed'
|
||||
width: 2
|
||||
area:
|
||||
opacity: 0
|
|
@ -242,6 +242,8 @@ Rails.application.routes.draw do
|
|||
post :preview_markdown
|
||||
end
|
||||
|
||||
draw :group
|
||||
|
||||
resources :projects, only: [:index, :new, :create]
|
||||
|
||||
get '/projects/:id' => 'projects#resolve'
|
||||
|
@ -258,7 +260,6 @@ Rails.application.routes.draw do
|
|||
draw :admin
|
||||
draw :profile
|
||||
draw :dashboard
|
||||
draw :group
|
||||
draw :user
|
||||
draw :project
|
||||
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddUniqIndexOnMetricIdentifierAndProjectId < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_concurrent_index :prometheus_metrics, [:identifier, :project_id], unique: true
|
||||
end
|
||||
|
||||
def down
|
||||
remove_concurrent_index :prometheus_metrics, [:identifier, :project_id]
|
||||
end
|
||||
end
|
|
@ -0,0 +1,31 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class UnconfirmWrongfullyVerifiedEmails < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
INTERVAL = 5.minutes.to_i
|
||||
BATCH_SIZE = 1000
|
||||
MIGRATION = 'WrongfullyConfirmedEmailUnconfirmer'
|
||||
EMAIL_INDEX_NAME = 'tmp_index_for_email_unconfirmation_migration'
|
||||
|
||||
class Email < ActiveRecord::Base
|
||||
include EachBatch
|
||||
end
|
||||
|
||||
def up
|
||||
add_concurrent_index :emails, :id, where: 'confirmed_at IS NOT NULL', name: EMAIL_INDEX_NAME
|
||||
|
||||
queue_background_migration_jobs_by_range_at_intervals(Email,
|
||||
MIGRATION,
|
||||
INTERVAL,
|
||||
batch_size: BATCH_SIZE)
|
||||
end
|
||||
|
||||
def down
|
||||
remove_concurrent_index_by_name(:emails, EMAIL_INDEX_NAME)
|
||||
end
|
||||
end
|
|
@ -19929,6 +19929,8 @@ CREATE INDEX index_prometheus_metrics_on_group ON public.prometheus_metrics USIN
|
|||
|
||||
CREATE UNIQUE INDEX index_prometheus_metrics_on_identifier ON public.prometheus_metrics USING btree (identifier);
|
||||
|
||||
CREATE UNIQUE INDEX index_prometheus_metrics_on_identifier_and_project_id ON public.prometheus_metrics USING btree (identifier, project_id);
|
||||
|
||||
CREATE INDEX index_prometheus_metrics_on_project_id ON public.prometheus_metrics USING btree (project_id);
|
||||
|
||||
CREATE INDEX index_protected_branch_merge_access ON public.protected_branch_merge_access_levels USING btree (protected_branch_id);
|
||||
|
@ -20493,6 +20495,8 @@ CREATE INDEX tmp_index_ci_pipelines_lock_version ON public.ci_pipelines USING bt
|
|||
|
||||
CREATE INDEX tmp_index_ci_stages_lock_version ON public.ci_stages USING btree (id) WHERE (lock_version IS NULL);
|
||||
|
||||
CREATE INDEX tmp_index_for_email_unconfirmation_migration ON public.emails USING btree (id) WHERE (confirmed_at IS NOT NULL);
|
||||
|
||||
CREATE UNIQUE INDEX unique_merge_request_metrics_by_merge_request_id ON public.merge_request_metrics USING btree (merge_request_id);
|
||||
|
||||
CREATE UNIQUE INDEX users_security_dashboard_projects_unique_index ON public.users_security_dashboard_projects USING btree (project_id, user_id);
|
||||
|
@ -23538,6 +23542,7 @@ COPY "schema_migrations" (version) FROM STDIN;
|
|||
20200610130002
|
||||
20200613104045
|
||||
20200615083635
|
||||
20200615111857
|
||||
20200615121217
|
||||
20200615123055
|
||||
20200615193524
|
||||
|
@ -23579,6 +23584,7 @@ COPY "schema_migrations" (version) FROM STDIN;
|
|||
20200625190458
|
||||
20200626060151
|
||||
20200626130220
|
||||
20200629192638
|
||||
20200630110826
|
||||
20200701093859
|
||||
20200702123805
|
||||
|
|
|
@ -316,11 +316,11 @@ The following documentation relates to the DevOps **Configure** stage:
|
|||
| [GitLab ChatOps](ci/chatops/README.md) | Interact with CI/CD jobs through chat services. |
|
||||
| [Installing Applications](user/project/clusters/index.md#installing-applications) | Install Helm charts such as Ingress and Prometheus on Kubernetes. |
|
||||
| [Mattermost slash commands](user/project/integrations/mattermost_slash_commands.md) | Enable and use slash commands from within Mattermost. |
|
||||
| [Multiple Kubernetes Clusters](user/project/clusters/index.md#multiple-kubernetes-clusters-premium) **(PREMIUM)** | Associate more than one Kubernetes clusters to your project. |
|
||||
| [Multiple Kubernetes Clusters](user/project/clusters/index.md#multiple-kubernetes-clusters) | Associate more than one Kubernetes clusters to your project. |
|
||||
| [Protected variables](ci/variables/README.md#protect-a-custom-variable) | Restrict variables to protected branches and tags. |
|
||||
| [Serverless](user/project/clusters/serverless/index.md) | Run serverless workloads on Kubernetes. |
|
||||
| [Slack slash commands](user/project/integrations/slack_slash_commands.md) | Enable and use slash commands from within Slack. |
|
||||
| [Manage your infrastructure with Terraform](user/infrastructure/index.md) | Manage your infrastructure as you run your CI/CD pipeline. |
|
||||
| [Manage your infrastructure with Terraform](user/infrastructure/index.md) | Manage your infrastructure as you run your CI/CD pipeline. |
|
||||
|
||||
<div align="right">
|
||||
<a type="button" class="btn btn-default" href="#overview">
|
||||
|
|
|
@ -261,6 +261,9 @@ Beyond that, you will want to review the error. If it is:
|
|||
- Specifically from the indexer, this could be a bug/issue and should be escalated to
|
||||
GitLab support.
|
||||
- An OS issue, you will want to reach out to your systems administrator.
|
||||
- A `Faraday::TimeoutError (execution expired)` error **and** you're using a proxy,
|
||||
[set a custom `gitlab_rails['env']` environment variable, called `no_proxy`](https://docs.gitlab.com/omnibus/settings/environment-variables.html)
|
||||
with the IP address of your Elasticsearch host.
|
||||
|
||||
### Troubleshooting performance
|
||||
|
||||
|
|
|
@ -54,6 +54,7 @@ Example Response:
|
|||
"state": "active",
|
||||
"updated_at": "2013-10-02T09:24:18Z",
|
||||
"created_at": "2013-10-02T09:24:18Z",
|
||||
"expired": false,
|
||||
"web_url": "https://gitlab.com/groups/gitlab-org/-/milestones/42"
|
||||
}
|
||||
]
|
||||
|
|
|
@ -51,7 +51,8 @@ Example Response:
|
|||
"start_date": "2013-11-10",
|
||||
"state": "active",
|
||||
"updated_at": "2013-10-02T09:24:18Z",
|
||||
"created_at": "2013-10-02T09:24:18Z"
|
||||
"created_at": "2013-10-02T09:24:18Z",
|
||||
"expired": false
|
||||
}
|
||||
]
|
||||
```
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue