提交 c52b72f5 编写于 作者: G GitLab Bot

Add latest changes from gitlab-org/gitlab@master

上级 21341457
......@@ -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,
......
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,
},
},
})
......
// 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>
// 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,
......
# 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,
......
# 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
# frozen_string_literal: true
module Packages
def self.table_name_prefix
'packages_'
end
end
# frozen_string_literal: true
class Packages::BuildInfo < ApplicationRecord
belongs_to :package, inverse_of: :build_info
belongs_to :pipeline, class_name: 'Ci::Pipeline'
end
# 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
# frozen_string_literal: true
module Packages
module Conan
def self.table_name_prefix
'packages_conan_'
end
end
end
# 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
# 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
# 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
# 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
# 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
# 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
# frozen_string_literal: true
module Packages
module Maven
def self.table_name_prefix
'packages_maven_'
end
end
end
# 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
# frozen_string_literal: true
module Packages
module Nuget
def self.table_name_prefix
'packages_nuget_'
end
end
end
# 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
# 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
# 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
# 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')
# frozen_string_literal: true
module Packages
module Pypi
def self.table_name_prefix
'packages_pypi_'
end
end
end
# 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
# 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
# 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')
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
- 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'
#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)
......
<%= @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')
......
---
title: Show expired milestones at the bottom of the list within dropdown
merge_request: 35595
author:
type: changed
---
title: 'Multiple Kubernetes clusters now available in GitLab core'
merge_request: 35094
author:
type: changed
---
title: Add inapplicable reason in MR suggestion Tooltip
merge_request: 35276
author:
type: changed
---
title: Fix Project#pages_url not to downcase url path
merge_request: 36183
author:
type: fixed
---
title: Fix routing for paths starting with help and projects
merge_request: 36048
author:
type: fixed
---
title: Enforce prometheus metric uniqueness across project scope
merge_request: 35566
author:
type: fixed
---
title: Include project and subgroup milestones on Roadmap page
merge_request: 35973
author:
type: added
---
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
......
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)
# 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
......
# 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
# 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
}
]
```
......
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册