Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-07-21 15:08:52 +00:00
parent 7e5b78ee03
commit a8f5aaa708
203 changed files with 1887 additions and 1258 deletions

View File

@ -41,6 +41,22 @@ nodejs-scan-sast:
semgrep-sast:
rules: !reference [".reports:rules:sast", rules]
gosec-sast:
variables:
GOPATH: "$CI_PROJECT_DIR/vendor/go"
COMPILE: "false"
GOSEC_GO_PKG_PATH: "$CI_PROJECT_DIR"
SECURE_LOG_LEVEL: "debug"
before_script:
- mkdir -p $GOPATH
- cd workhorse
- go get -d ./...
- cd ..
cache:
paths:
- vendor/go
rules: !reference [".reports:rules:sast", rules]
.secret-analyzer:
extends: .default-retry
needs: []

View File

@ -1 +1 @@
499b72a41063d61dbb8a73ed7ffa7aa42f1584fd
996a4adda765e8ced18c72eca0ebd27848afa3c9

View File

@ -0,0 +1,28 @@
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
query getProjects(
$search: String!
$after: String = ""
$first: Int!
$searchNamespaces: Boolean = false
$sort: String
$membership: Boolean = true
) {
projects(
search: $search
after: $after
first: $first
membership: $membership
searchNamespaces: $searchNamespaces
sort: $sort
) {
nodes {
id
name
nameWithNamespace
}
pageInfo {
...PageInfo
}
}
}

View File

@ -14,6 +14,8 @@ import {
GlTabs,
GlSprintf,
} from '@gitlab/ui';
import createFlash from '~/flash';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { objectToQuery } from '~/lib/utils/url_utility';
import { s__, __ } from '~/locale';
@ -24,12 +26,21 @@ import { s__, __ } from '~/locale';
// import PackageHistory from '~/packages/details/components/package_history.vue';
// import PackageListRow from '~/packages/shared/components/package_list_row.vue';
import PackagesListLoader from '~/packages/shared/components/packages_list_loader.vue';
import {
PackageType,
TrackingActions,
SHOW_DELETE_SUCCESS_ALERT,
} from '~/packages/shared/constants';
import { packageTypeToTrackCategory } from '~/packages/shared/utils';
import {
PACKAGE_TYPE_NUGET,
PACKAGE_TYPE_COMPOSER,
DELETE_PACKAGE_TRACKING_ACTION,
REQUEST_DELETE_PACKAGE_TRACKING_ACTION,
CANCEL_DELETE_PACKAGE_TRACKING_ACTION,
PULL_PACKAGE_TRACKING_ACTION,
DELETE_PACKAGE_FILE_TRACKING_ACTION,
REQUEST_DELETE_PACKAGE_FILE_TRACKING_ACTION,
CANCEL_DELETE_PACKAGE_FILE_TRACKING_ACTION,
SHOW_DELETE_SUCCESS_ALERT,
FETCH_PACKAGE_DETAILS_ERROR_MESSAGE,
} from '~/packages_and_registries/package_registry/constants';
import getPackageDetails from '~/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql';
import Tracking from '~/tracking';
export default {
@ -42,7 +53,8 @@ export default {
GlTab,
GlTabs,
GlSprintf,
PackageTitle: () => import('~/packages/details/components/package_title.vue'),
PackageTitle: () =>
import('~/packages_and_registries/package_registry/components/details/package_title.vue'),
TerraformTitle: () =>
import('~/packages_and_registries/infrastructure_registry/components/details_title.vue'),
PackagesListLoader,
@ -59,6 +71,7 @@ export default {
},
mixins: [Tracking.mixin()],
inject: [
'packageId',
'titleComponent',
'projectName',
'canDelete',
@ -68,22 +81,53 @@ export default {
'projectListUrl',
'groupListUrl',
],
trackingActions: { ...TrackingActions },
trackingActions: {
DELETE_PACKAGE_TRACKING_ACTION,
REQUEST_DELETE_PACKAGE_TRACKING_ACTION,
CANCEL_DELETE_PACKAGE_TRACKING_ACTION,
PULL_PACKAGE_TRACKING_ACTION,
DELETE_PACKAGE_FILE_TRACKING_ACTION,
REQUEST_DELETE_PACKAGE_FILE_TRACKING_ACTION,
CANCEL_DELETE_PACKAGE_FILE_TRACKING_ACTION,
},
data() {
return {
fileToDelete: null,
packageEntity: {},
};
},
apollo: {
packageEntity: {
query: getPackageDetails,
variables() {
return this.queryVariables;
},
update(data) {
return data.package;
},
error(error) {
createFlash({
message: FETCH_PACKAGE_DETAILS_ERROR_MESSAGE,
captureError: true,
error,
});
},
},
},
computed: {
queryVariables() {
return {
id: convertToGraphQLId('Packages::Package', this.packageId),
};
},
packageFiles() {
return this.packageEntity.packageFiles;
},
isLoading() {
return false;
return this.$apollo.queries.package;
},
isValidPackage() {
return Boolean(this.packageEntity.name);
return Boolean(this.packageEntity?.name);
},
tracking() {
return {
@ -97,10 +141,10 @@ export default {
return this.packageEntity.dependency_links || [];
},
showDependencies() {
return this.packageEntity.package_type === PackageType.NUGET;
return this.packageEntity.package_type === PACKAGE_TYPE_NUGET;
},
showFiles() {
return this.packageEntity?.package_type !== PackageType.COMPOSER;
return this.packageEntity?.package_type !== PACKAGE_TYPE_COMPOSER;
},
},
methods: {
@ -113,7 +157,7 @@ export default {
}
},
async confirmPackageDeletion() {
this.track(TrackingActions.DELETE_PACKAGE);
this.track(DELETE_PACKAGE_TRACKING_ACTION);
await this.deletePackage();
@ -127,12 +171,12 @@ export default {
window.location.replace(`${returnTo}?${modalQuery}`);
},
handleFileDelete(file) {
this.track(TrackingActions.REQUEST_DELETE_PACKAGE_FILE);
this.track(REQUEST_DELETE_PACKAGE_FILE_TRACKING_ACTION);
this.fileToDelete = { ...file };
this.$refs.deleteFileModal.show();
},
confirmFileDelete() {
this.track(TrackingActions.DELETE_PACKAGE_FILE);
this.track(DELETE_PACKAGE_FILE_TRACKING_ACTION);
// this.deletePackageFile(this.fileToDelete.id);
this.fileToDelete = null;
},
@ -176,7 +220,7 @@ export default {
/>
<div v-else class="packages-app">
<component :is="titleComponent">
<component :is="titleComponent" :package-entity="packageEntity">
<template #delete-button>
<gl-button
v-if="canDelete"

View File

@ -0,0 +1,133 @@
<script>
import { GlIcon, GlSprintf, GlTooltipDirective, GlBadge } from '@gitlab/ui';
import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { __ } from '~/locale';
import PackageTags from '~/packages/shared/components/package_tags.vue';
import { PACKAGE_TYPE_NUGET } from '~/packages_and_registries/package_registry/constants';
import { getPackageTypeLabel } from '~/packages_and_registries/package_registry/utils';
import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
export default {
name: 'PackageTitle',
components: {
TitleArea,
GlIcon,
GlSprintf,
PackageTags,
MetadataItem,
GlBadge,
},
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [timeagoMixin],
i18n: {
packageInfo: __('v%{version} published %{timeAgo}'),
},
props: {
packageEntity: {
type: Object,
required: true,
},
},
data() {
return {
isDesktop: true,
};
},
computed: {
packageTypeDisplay() {
return getPackageTypeLabel(this.packageEntity.packageType);
},
packagePipeline() {
return this.packageEntity.pipelines?.nodes[0];
},
packageIcon() {
if (this.packageEntity.packageType === PACKAGE_TYPE_NUGET) {
return this.packageEntity.metadata?.iconUrl || null;
}
return null;
},
hasTagsToDisplay() {
return Boolean(this.packageEntity.tags?.nodes && this.packageEntity.tags?.nodes.length);
},
totalSize() {
return this.packageEntity.packageFiles
? numberToHumanSize(
this.packageEntity.packageFiles.nodes.reduce((acc, p) => acc + Number(p.size), 0),
)
: '0';
},
},
mounted() {
this.isDesktop = GlBreakpointInstance.isDesktop();
},
methods: {
dynamicSlotName(index) {
return `metadata-tag${index}`;
},
},
};
</script>
<template>
<title-area :title="packageEntity.name" :avatar="packageIcon" data-qa-selector="package_title">
<template #sub-header>
<gl-icon name="eye" class="gl-mr-3" />
<gl-sprintf :message="$options.i18n.packageInfo">
<template #version>
{{ packageEntity.version }}
</template>
<template #timeAgo>
<span v-gl-tooltip :title="tooltipTitle(packageEntity.created_at)">
&nbsp;{{ timeFormatted(packageEntity.created_at) }}
</span>
</template>
</gl-sprintf>
</template>
<template v-if="packageTypeDisplay" #metadata-type>
<metadata-item data-testid="package-type" icon="package" :text="packageTypeDisplay" />
</template>
<template #metadata-size>
<metadata-item data-testid="package-size" icon="disk" :text="totalSize" />
</template>
<template v-if="packagePipeline" #metadata-pipeline>
<metadata-item
data-testid="pipeline-project"
icon="review-list"
:text="packagePipeline.project.name"
:link="packagePipeline.project.webUrl"
/>
</template>
<template v-if="packagePipeline && packagePipeline.ref" #metadata-ref>
<metadata-item data-testid="package-ref" icon="branch" :text="packagePipeline.ref" />
</template>
<template v-if="isDesktop && hasTagsToDisplay" #metadata-tags>
<package-tags :tag-display-limit="2" :tags="packageEntity.tags.nodes" hide-label />
</template>
<!-- we need to duplicate the package tags on mobile to ensure proper styling inside the flex wrap -->
<template
v-for="(tag, index) in packageEntity.tags.nodes"
v-else-if="hasTagsToDisplay"
#[dynamicSlotName(index)]
>
<gl-badge :key="index" class="gl-my-1" data-testid="tag-badge" variant="info" size="sm">
{{ tag.name }}
</gl-badge>
</template>
<template #right-actions>
<slot name="delete-button"></slot>
</template>
</title-area>
</template>

View File

@ -0,0 +1,46 @@
import { __, s__ } from '~/locale';
export const PACKAGE_TYPE_CONAN = 'CONAN';
export const PACKAGE_TYPE_MAVEN = 'MAVEN';
export const PACKAGE_TYPE_NPM = 'NPM';
export const PACKAGE_TYPE_NUGET = 'NUGET';
export const PACKAGE_TYPE_PYPI = 'PYPI';
export const PACKAGE_TYPE_COMPOSER = 'COMPOSER';
export const PACKAGE_TYPE_RUBYGEMS = 'RUBYGEMS';
export const PACKAGE_TYPE_GENERIC = 'GENERIC';
export const PACKAGE_TYPE_DEBIAN = 'DEBIAN';
export const PACKAGE_TYPE_HELM = 'HELM';
export const PACKAGE_TYPE_TERRAFORM = 'terraform_module';
export const DELETE_PACKAGE_TRACKING_ACTION = 'delete_package';
export const REQUEST_DELETE_PACKAGE_TRACKING_ACTION = 'request_delete_package';
export const CANCEL_DELETE_PACKAGE_TRACKING_ACTION = 'cancel_delete_package';
export const PULL_PACKAGE_TRACKING_ACTION = 'pull_package';
export const DELETE_PACKAGE_FILE_TRACKING_ACTION = 'delete_package_file';
export const REQUEST_DELETE_PACKAGE_FILE_TRACKING_ACTION = 'request_delete_package_file';
export const CANCEL_DELETE_PACKAGE_FILE_TRACKING_ACTION = 'cancel_delete_package_file';
export const TrackingCategories = {
[PACKAGE_TYPE_MAVEN]: 'MavenPackages',
[PACKAGE_TYPE_NPM]: 'NpmPackages',
[PACKAGE_TYPE_CONAN]: 'ConanPackages',
};
export const SHOW_DELETE_SUCCESS_ALERT = 'showSuccessDeleteAlert';
export const DELETE_PACKAGE_ERROR_MESSAGE = s__(
'PackageRegistry|Something went wrong while deleting the package.',
);
export const DELETE_PACKAGE_FILE_ERROR_MESSAGE = s__(
__('PackageRegistry|Something went wrong while deleting the package file.'),
);
export const DELETE_PACKAGE_FILE_SUCCESS_MESSAGE = s__(
'PackageRegistry|Package file deleted successfully',
);
export const FETCH_PACKAGE_DETAILS_ERROR_MESSAGE = s__(
'PackageRegistry|Failed to load the package data',
);
export const PACKAGE_ERROR_STATUS = 'error';
export const PACKAGE_DEFAULT_STATUS = 'default';
export const PACKAGE_HIDDEN_STATUS = 'hidden';
export const PACKAGE_PROCESSING_STATUS = 'processing';

View File

@ -0,0 +1,14 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
Vue.use(VueApollo);
export const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(
{},
{
assumeImmutableResults: true,
},
),
});

View File

@ -0,0 +1,35 @@
query getPackageDetails($id: ID!) {
package(id: $id) {
id
name
packageType
version
createdAt
updatedAt
status
tags {
nodes {
id
name
}
}
pipelines(first: 3) {
nodes {
project {
name
webUrl
}
}
}
packageFiles(first: 1000) {
nodes {
id
fileMd5
fileName
fileSha1
fileSha256
size
}
}
}
}

View File

@ -1,7 +1,8 @@
import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import PackagesApp from '~/packages_and_registries/package_registry/components/details/app.vue';
import { apolloProvider } from '~/packages_and_registries/package_registry/graphql/index';
import Translate from '~/vue_shared/translate';
import PackagesApp from '../components/details/app.vue';
Vue.use(Translate);
@ -14,6 +15,7 @@ export default () => {
const { canDelete, ...datasetOptions } = el.dataset;
return new Vue({
el,
apolloProvider,
provide: {
canDelete: parseBoolean(canDelete),
titleComponent: 'PackageTitle',

View File

@ -0,0 +1,40 @@
import { s__ } from '~/locale';
import {
PACKAGE_TYPE_CONAN,
PACKAGE_TYPE_MAVEN,
PACKAGE_TYPE_NPM,
PACKAGE_TYPE_NUGET,
PACKAGE_TYPE_PYPI,
PACKAGE_TYPE_COMPOSER,
PACKAGE_TYPE_RUBYGEMS,
PACKAGE_TYPE_GENERIC,
PACKAGE_TYPE_DEBIAN,
PACKAGE_TYPE_HELM,
} from './constants';
export const getPackageTypeLabel = (packageType) => {
switch (packageType) {
case PACKAGE_TYPE_CONAN:
return s__('PackageRegistry|Conan');
case PACKAGE_TYPE_MAVEN:
return s__('PackageRegistry|Maven');
case PACKAGE_TYPE_NPM:
return s__('PackageRegistry|npm');
case PACKAGE_TYPE_NUGET:
return s__('PackageRegistry|NuGet');
case PACKAGE_TYPE_PYPI:
return s__('PackageRegistry|PyPI');
case PACKAGE_TYPE_RUBYGEMS:
return s__('PackageRegistry|RubyGems');
case PACKAGE_TYPE_COMPOSER:
return s__('PackageRegistry|Composer');
case PACKAGE_TYPE_GENERIC:
return s__('PackageRegistry|Generic');
case PACKAGE_TYPE_DEBIAN:
return s__('PackageRegistry|Debian');
case PACKAGE_TYPE_HELM:
return s__('PackageRegistry|Helm');
default:
return null;
}
};

View File

@ -7,7 +7,6 @@ const PERSISTENT_USER_CALLOUTS = [
'.js-buy-pipeline-minutes-notification-callout',
'.js-token-expiry-callout',
'.js-registration-enabled-callout',
'.js-service-templates-deprecated-callout',
'.js-new-user-signups-cap-reached',
'.js-eoa-bronze-plan-banner',
];

View File

@ -15,6 +15,11 @@ export default {
ProjectListItem,
},
props: {
maxListHeight: {
type: Number,
required: false,
default: 402,
},
projectSearchResults: {
type: Array,
required: true,
@ -101,7 +106,7 @@ export default {
<div class="d-flex flex-column">
<gl-loading-icon v-if="showLoadingIndicator" size="sm" class="py-2 px-4" />
<gl-infinite-scroll
:max-list-height="402"
:max-list-height="maxListHeight"
:fetched-items="projectSearchResults.length"
:total-items="totalResults"
@bottomReached="bottomReached"

View File

@ -1,47 +0,0 @@
# frozen_string_literal: true
class Admin::ServicesController < Admin::ApplicationController
include Integrations::Params
before_action :integration, only: [:edit, :update]
before_action :disable_query_limiting, only: [:index]
feature_category :integrations
def index
@activated_services = Integration.for_template.active.sort_by(&:title)
@existing_instance_types = Integration.for_instance.pluck(:type) # rubocop: disable CodeReuse/ActiveRecord
end
def edit
if integration.nil? || Integration.instance_exists_for?(integration.type)
redirect_to admin_application_settings_services_path,
alert: "Service is unknown or it doesn't exist"
end
end
def update
if integration.update(integration_params[:integration])
PropagateServiceTemplateWorker.perform_async(integration.id) if integration.active? # rubocop:disable CodeReuse/Worker
redirect_to admin_application_settings_services_path,
notice: 'Application settings saved successfully'
else
render :edit
end
end
private
# rubocop: disable CodeReuse/ActiveRecord
def integration
@integration ||= Integration.find_by(id: params[:id], template: true)
@service ||= @integration # TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/329759
end
alias_method :service, :integration
# rubocop: enable CodeReuse/ActiveRecord
def disable_query_limiting
Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/220357')
end
end

View File

@ -56,11 +56,11 @@ module Mutations
issue = authorized_find!(project_path: project_path, iid: iid)
move_params = { id: issue.id, board_id: board.id }.merge(move_arguments(args))
move_issue(board, issue, move_params)
result = move_issue(board, issue, move_params)
{
issue: issue.reset,
errors: issue.errors.full_messages
errors: error_for(result)
}
end
@ -79,6 +79,12 @@ module Mutations
def move_arguments(args)
args.slice(:from_list_id, :to_list_id, :move_after_id, :move_before_id)
end
def error_for(result)
return [] unless result.error?
[result.message]
end
end
end
end

View File

@ -64,9 +64,10 @@ module PackagesHelper
project.container_repositories.exists?
end
def package_details_data(project, package = nil)
def package_details_data(project, package, use_presenter = false)
{
package: package ? package_from_presenter(package) : nil,
package: use_presenter ? package_from_presenter(package) : nil,
package_id: package.id,
can_delete: can?(current_user, :destroy_package, project).to_s,
svg_path: image_path('illustrations/no-packages.svg'),
npm_path: package_registry_instance_url(:npm),

View File

@ -4,7 +4,6 @@ module UserCalloutsHelper
GKE_CLUSTER_INTEGRATION = 'gke_cluster_integration'
GCP_SIGNUP_OFFER = 'gcp_signup_offer'
SUGGEST_POPOVER_DISMISSED = 'suggest_popover_dismissed'
SERVICE_TEMPLATES_DEPRECATED_CALLOUT = 'service_templates_deprecated_callout'
TABS_POSITION_HIGHLIGHT = 'tabs_position_highlight'
CUSTOMIZE_HOMEPAGE = 'customize_homepage'
FEATURE_FLAGS_NEW_VERSION = 'feature_flags_new_version'
@ -35,13 +34,6 @@ module UserCalloutsHelper
!user_dismissed?(SUGGEST_POPOVER_DISMISSED)
end
def show_service_templates_deprecated_callout?
!Gitlab.com? &&
current_user&.admin? &&
Integration.for_template.active.exists? &&
!user_dismissed?(SERVICE_TEMPLATES_DEPRECATED_CALLOUT)
end
def show_customize_homepage_banner?
current_user.default_dashboard? && !user_dismissed?(CUSTOMIZE_HOMEPAGE)
end

View File

@ -86,4 +86,17 @@ class ApplicationRecord < ActiveRecord::Base
values = enum_mod.definition.transform_values { |v| v[:value] }
enum(enum_mod.key => values)
end
def self.transaction(**options, &block)
if options[:requires_new] && track_subtransactions?
::Gitlab::Database::Metrics.subtransactions_increment(self.name)
end
super(**options, &block)
end
def self.track_subtransactions?
::Feature.enabled?(:active_record_subtransactions_counter, type: :ops, default_enabled: :yaml) &&
connection.transaction_open?
end
end

View File

@ -80,7 +80,7 @@ class Group < Namespace
# debian_distributions and associated component_files must be destroyed by ruby code in order to properly remove carrierwave uploads
has_many :debian_distributions, class_name: 'Packages::Debian::GroupDistribution', dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
delegate :prevent_sharing_groups_outside_hierarchy, to: :namespace_settings
delegate :prevent_sharing_groups_outside_hierarchy, :new_user_signups_cap, to: :namespace_settings
accepts_nested_attributes_for :variables, allow_destroy: true

View File

@ -62,15 +62,13 @@ class Integration < ApplicationRecord
belongs_to :group, inverse_of: :integrations
has_one :service_hook, inverse_of: :integration, foreign_key: :service_id
validates :project_id, presence: true, unless: -> { template? || instance_level? || group_level? }
validates :group_id, presence: true, unless: -> { template? || instance_level? || project_level? }
validates :project_id, :group_id, absence: true, if: -> { template? || instance_level? }
validates :project_id, presence: true, unless: -> { instance_level? || group_level? }
validates :group_id, presence: true, unless: -> { instance_level? || project_level? }
validates :project_id, :group_id, absence: true, if: -> { instance_level? }
validates :type, presence: true, exclusion: BASE_CLASSES
validates :type, uniqueness: { scope: :template }, if: :template?
validates :type, uniqueness: { scope: :instance }, if: :instance_level?
validates :type, uniqueness: { scope: :project_id }, if: :project_level?
validates :type, uniqueness: { scope: :group_id }, if: :group_level?
validate :validate_is_instance_or_template
validate :validate_belongs_to_project_or_group
scope :external_issue_trackers, -> { where(category: 'issue_tracker').active }
@ -81,7 +79,6 @@ class Integration < ApplicationRecord
scope :inherit_from_id, -> (id) { where(inherit_from_id: id) }
scope :inherit, -> { where.not(inherit_from_id: nil) }
scope :for_group, -> (group) { where(group_id: group, type: available_integration_types(include_project_specific: false)) }
scope :for_template, -> { where(template: true, type: available_integration_types(include_project_specific: false)) }
scope :for_instance, -> { where(instance: true, type: available_integration_types(include_project_specific: false)) }
scope :push_hooks, -> { where(push_events: true, active: true) }
@ -169,25 +166,10 @@ class Integration < ApplicationRecord
'push'
end
def self.find_or_create_templates
create_nonexistent_templates
for_template
def self.event_description(event)
IntegrationsHelper.integration_event_description(event)
end
def self.create_nonexistent_templates
nonexistent_integrations = build_nonexistent_integrations_for(for_template)
return if nonexistent_integrations.empty?
# Create within a transaction to perform the lowest possible SQL queries.
transaction do
nonexistent_integrations.each do |integration|
integration.template = true
integration.save
end
end
end
private_class_method :create_nonexistent_templates
def self.find_or_initialize_non_project_specific_integration(name, instance: false, group_id: nil)
return unless name.in?(available_integration_names(include_project_specific: false))
@ -275,7 +257,6 @@ class Integration < ApplicationRecord
data_fields.integration = new_integration
end
new_integration.template = false
new_integration.instance = false
new_integration.project_id = project_id
new_integration.group_id = group_id
@ -306,12 +287,11 @@ class Integration < ApplicationRecord
end
private_class_method :instance_level_integration
def self.create_from_active_default_integrations(scope, association, with_templates: false)
def self.create_from_active_default_integrations(scope, association)
group_ids = sorted_ancestors(scope).select(:id)
array = group_ids.to_sql.present? ? "array(#{group_ids.to_sql})" : 'ARRAY[]'
from_union([
with_templates ? active.where(template: true) : none,
active.where(instance: true),
active.where(group_id: group_ids, inherit_from_id: nil)
]).order(Arel.sql("type ASC, array_position(#{array}::bigint[], #{table_name}.group_id), instance DESC")).group_by(&:type).each do |type, records|
@ -384,7 +364,7 @@ class Integration < ApplicationRecord
end
def to_integration_hash
as_json(methods: :type, except: %w[id template instance project_id group_id])
as_json(methods: :type, except: %w[id instance project_id group_id])
end
def to_data_fields_hash
@ -503,10 +483,6 @@ class Integration < ApplicationRecord
end
end
def validate_is_instance_or_template
errors.add(:template, 'The service should be a service template or instance-level integration') if template? && instance_level?
end
def validate_belongs_to_project_or_group
errors.add(:project_id, 'The service cannot belong to both a project and a group') if project_level? && group_level?
end

View File

@ -61,10 +61,38 @@ class Milestone < ApplicationRecord
end
def self.reference_pattern
if Feature.enabled?(:milestone_reference_pattern, default_enabled: :yaml)
new_reference_pattern
else
old_reference_pattern
end
end
def self.new_reference_pattern
# NOTE: The iid pattern only matches when all characters on the expression
# are digits, so it will match %2 but not %2.1 because that's probably a
# milestone name and we want it to be matched as such.
@reference_pattern ||= %r{
@new_reference_pattern ||= %r{
(#{Project.reference_pattern})?
#{Regexp.escape(reference_prefix)}
(?:
(?<milestone_iid>
\d+(?!\S\w)\b # Integer-based milestone iid, or
) |
(?<milestone_name>
[^"\s\<]+\b | # String-based single-word milestone title, or
"[^"]+" # String-based multi-word milestone surrounded in quotes
)
)
}x
end
# Deprecated: https://gitlab.com/gitlab-org/gitlab/-/issues/336268
def self.old_reference_pattern
# NOTE: The iid pattern only matches when all characters on the expression
# are digits, so it will match %2 but not %2.1 because that's probably a
# milestone name and we want it to be matched as such.
@old_reference_pattern ||= %r{
(#{Project.reference_pattern})?
#{Regexp.escape(reference_prefix)}
(?:

View File

@ -1411,7 +1411,7 @@ class Project < ApplicationRecord
def find_or_initialize_integration(name)
return if disabled_integrations.include?(name)
find_integration(integrations, name) || build_from_instance_or_template(name) || build_integration(name)
find_integration(integrations, name) || build_from_instance(name) || build_integration(name)
end
# rubocop: disable CodeReuse/ServiceClass
@ -2673,22 +2673,18 @@ class Project < ApplicationRecord
integrations.find { _1.to_param == name }
end
def build_from_instance_or_template(name)
def build_from_instance(name)
instance = find_integration(integration_instances, name)
return Integration.build_from_integration(instance, project_id: id) if instance
template = find_integration(integration_templates, name)
return Integration.build_from_integration(template, project_id: id) if template
return unless instance
Integration.build_from_integration(instance, project_id: id)
end
def build_integration(name)
Integration.integration_name_to_model(name).new(project_id: id)
end
def integration_templates
@integration_templates ||= Integration.for_template
end
def integration_instances
@integration_instances ||= Integration.for_instance
end

View File

@ -16,7 +16,6 @@ class UserCallout < ApplicationRecord
tabs_position_highlight: 10,
threat_monitoring_info: 11, # EE-only
account_recovery_regular_check: 12, # EE-only
service_templates_deprecated_callout: 14,
web_ide_alert_dismissed: 16, # no longer in use
active_user_count_threshold: 18, # EE-only
buy_pipeline_minutes_notification_dot: 19, # EE-only

View File

@ -5,9 +5,7 @@ module Admin
include PropagateService
def propagate
return unless integration.active?
create_integration_for_projects_without_integration
# TODO: Remove this as part of https://gitlab.com/gitlab-org/gitlab/-/issues/335178
end
end
end

View File

@ -4,13 +4,23 @@ module Boards
class BaseItemMoveService < Boards::BaseService
def execute(issuable)
issuable_modification_params = issuable_params(issuable)
return false if issuable_modification_params.empty?
return if issuable_modification_params.empty?
move_single_issuable(issuable, issuable_modification_params)
return unless move_single_issuable(issuable, issuable_modification_params)
success(issuable)
end
private
def success(issuable)
ServiceResponse.success(payload: { issuable: issuable })
end
def error(issuable, message)
ServiceResponse.error(payload: { issuable: issuable }, message: message)
end
def issuable_params(issuable)
attrs = {}

View File

@ -162,7 +162,7 @@ module Projects
@project.create_or_update_import_data(data: @import_data[:data], credentials: @import_data[:credentials]) if @import_data
if @project.save
Integration.create_from_active_default_integrations(@project, :project_id, with_templates: true)
Integration.create_from_active_default_integrations(@project, :project_id)
@project.create_labels unless @project.gitlab_project_import?

View File

@ -1,9 +0,0 @@
= render "service_templates_deprecated_alert"
%h3.page-title
= @service.title
%p= @service.description
= form_for :service, url: admin_application_settings_service_path, method: :put, html: { class: 'fieldset-form js-integration-settings-form' } do |form|
= render 'shared/service_settings', form: form, integration: @service

View File

@ -1,10 +0,0 @@
- doc_link_start = "<a href=\"#{integrations_help_page_path}\" target='_blank' rel='noopener noreferrer'>".html_safe
- settings_link_start = "<a href=\"#{integrations_admin_application_settings_path}\">".html_safe
.gl-alert.gl-alert-danger.gl-mt-5{ role: 'alert' }
.gl-alert-container
= sprite_icon('error', css_class: 'gl-alert-icon gl-alert-icon-no-title')
.gl-alert-content
%h4.gl-alert-title= s_('AdminSettings|Service templates are deprecated and will be removed in GitLab 14.0.')
.gl-alert-body
= html_escape_once(s_("AdminSettings|You can't add new templates. To migrate or remove a Service template, create a new integration at %{settings_link_start}Settings &gt; Integrations%{link_end}. Learn more about %{doc_link_start}Project integration management%{link_end}.")).html_safe % { settings_link_start: settings_link_start, doc_link_start: doc_link_start, link_end: '</a>'.html_safe }

View File

@ -1,6 +0,0 @@
- add_to_breadcrumbs _("Service Templates"), admin_application_settings_services_path
- page_title @service.title, _("Service Templates")
- breadcrumb_title @service.title
- @content_class = 'limit-container-width' unless fluid_layout
= render 'form'

View File

@ -1,43 +0,0 @@
- page_title _("Service Templates")
- @content_class = 'limit-container-width' unless fluid_layout
= render "service_templates_deprecated_alert"
- if @activated_services.any?
%h3.page-title Service templates
%p= s_('AdminSettings|Service template allows you to set default values for integrations')
%table.table.b-table.gl-table
%colgroup
%col
%col
%col
%col{ width: 135 }
%thead
%tr
%th
%th= _('Service')
%th= _('Description')
%th= _('Last edit')
- @activated_services.each do |service|
- if service.type.in?(@existing_instance_types)
%tr
%td
%td
= link_to edit_admin_application_settings_integration_path(service.to_param), class: 'gl-text-blue-300!' do
%strong.has-tooltip{ title: s_('AdminSettings|Moved to integrations'), data: { container: 'body' } }
= service.title
%td.gl-cursor-default.gl-text-gray-400
= service.description
%td
- else
%tr
%td
= boolean_to_icon service.activated?
%td
= link_to edit_admin_application_settings_service_path(service.id) do
%strong= service.title
%td
= service.description
%td.light
= time_ago_with_tooltip service.updated_at

View File

@ -11,7 +11,6 @@
= render "layouts/broadcast"
= render "layouts/header/read_only_banner"
= render "layouts/header/registration_enabled_callout"
= render "layouts/header/service_templates_deprecation_callout"
= render "layouts/nav/classification_level_banner"
= yield :flash_message
= render "shared/service_ping_consent"

View File

@ -1,21 +0,0 @@
- return unless show_service_templates_deprecated_callout?
- doc_link_start = "<a href=\"#{integrations_help_page_path}\" target='_blank' rel='noopener noreferrer'>".html_safe
- settings_link_start = "<a href=\"#{integrations_admin_application_settings_path}\">".html_safe
%div{ class: [container_class, @content_class, 'gl-pt-5!'] }
.gl-alert.gl-alert-warning.js-service-templates-deprecated-callout{ role: 'alert', data: { feature_id: UserCalloutsHelper::SERVICE_TEMPLATES_DEPRECATED_CALLOUT, dismiss_endpoint: user_callouts_path } }
= sprite_icon('warning', size: 16, css_class: 'gl-alert-icon')
%button.gl-alert-dismiss.js-close{ type: 'button', aria: { label: _('Close') }, data: { testid: 'close-service-templates-deprecated-callout' } }
= sprite_icon('close', size: 16)
.gl-alert-title
= s_('AdminSettings|Service templates are deprecated and will be removed in GitLab 14.0.')
.gl-alert-body
= html_escape_once(s_('AdminSettings|You should migrate to %{doc_link_start}Project integration management%{link_end}, available at %{settings_link_start}Settings &gt; Integrations.%{link_end}')).html_safe % { doc_link_start: doc_link_start, settings_link_start: settings_link_start, link_end: '</a>'.html_safe }
.gl-alert-actions
= link_to admin_application_settings_services_path, class: 'btn gl-alert-action btn-info btn-md gl-button' do
%span.gl-button-text
= s_('AdminSettings|See affected service templates')
= link_to "https://gitlab.com/gitlab-org/gitlab/-/issues/325905", class: 'btn gl-alert-action btn-default btn-md gl-button', target: '_blank', rel: 'noopener noreferrer' do
%span.gl-button-text
= _('Leave feedback')

View File

@ -7,6 +7,6 @@
.row
.col-12
- if Feature.enabled?(:package_details_apollo)
#js-vue-packages-detail-new{ data: package_details_data(@project) }
#js-vue-packages-detail-new{ data: package_details_data(@project, @package) }
- else
#js-vue-packages-detail{ data: package_details_data(@project, @package) }
#js-vue-packages-detail{ data: package_details_data(@project, @package, true) }

View File

@ -0,0 +1,8 @@
---
name: milestone_reference_pattern
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/65847
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/336268
milestone: '14.1'
type: development
group: group::source code
default_enabled: false

View File

@ -0,0 +1,8 @@
---
name: active_record_subtransactions_counter
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/66477
rollout_issue_url:
milestone: '14.1'
type: ops
group: group::pipeline execution
default_enabled: false

View File

@ -13,6 +13,7 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/65336
time_frame: none
data_source: system
data_category: Standard
instrumentation_class: CollectedDataCategoriesMetric
distribution:
- ce
- ee

View File

@ -125,7 +125,6 @@ namespace :admin do
end
resource :application_settings, only: :update do
resources :services, only: [:index, :edit, :update]
resources :integrations, only: [:edit, :update] do
member do
put :test

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
class RemoveNullConstraintFromSecurityFindings < ActiveRecord::Migration[6.1]
def up
change_column_null :security_findings, :project_fingerprint, true
end
def down
# no-op, it can not be reverted due to existing records that might not be valid
end
end

View File

@ -0,0 +1 @@
a97ac46a042b7f049f27db4f4916b8b0dbf527ba3c34fc9cc577da7807a88d32

View File

@ -17896,7 +17896,7 @@ CREATE TABLE security_findings (
scanner_id bigint NOT NULL,
severity smallint NOT NULL,
confidence smallint NOT NULL,
project_fingerprint text NOT NULL,
project_fingerprint text,
deduplicated boolean DEFAULT false NOT NULL,
"position" integer,
uuid uuid,

View File

@ -2088,6 +2088,9 @@ but with smaller performance requirements, several modifications can be consider
- Combining select nodes: Some nodes can be combined to reduce complexity at the cost of some performance:
- GitLab Rails and Sidekiq: Sidekiq nodes can be removed and the component instead enabled on the GitLab Rails nodes.
- PostgreSQL and PgBouncer: PgBouncer nodes can be removed and the component instead enabled on PostgreSQL with the Internal Load Balancer pointing to them instead.
- Reducing the node counts: Some node types do not need consensus and can run with fewer nodes (but more than one for redundancy). Note that this will also lead to reduced performance.
- GitLab Rails and Sidekiq: Stateless services don't have a minimum node count. Two are enough for redundancy.
- Gitaly and Praefect: A quorum is not strictly necessary. Two Gitaly nodes and two Praefect nodes are enough for redundancy.
- Running select components in reputable Cloud PaaS solutions: Select components of the GitLab setup can instead be run on Cloud Provider PaaS solutions. By doing this, additional dependent components can also be removed:
- PostgreSQL: Can be run on reputable Cloud PaaS solutions such as Google Cloud SQL or AWS RDS. In this setup, the PgBouncer and Consul nodes are no longer required:
- Consul may still be desired if [Prometheus](../monitoring/prometheus/index.md) auto discovery is a requirement, otherwise you would need to [manually add scrape configurations](../monitoring/prometheus/index.md#adding-custom-scrape-configurations) for all nodes.

View File

@ -15334,7 +15334,6 @@ Name of the feature that the callout is for.
| <a id="usercalloutfeaturenameenumregistration_enabled_callout"></a>`REGISTRATION_ENABLED_CALLOUT` | Callout feature name for registration_enabled_callout. |
| <a id="usercalloutfeaturenameenumsecurity_configuration_devops_alert"></a>`SECURITY_CONFIGURATION_DEVOPS_ALERT` | Callout feature name for security_configuration_devops_alert. |
| <a id="usercalloutfeaturenameenumsecurity_configuration_upgrade_banner"></a>`SECURITY_CONFIGURATION_UPGRADE_BANNER` | Callout feature name for security_configuration_upgrade_banner. |
| <a id="usercalloutfeaturenameenumservice_templates_deprecated_callout"></a>`SERVICE_TEMPLATES_DEPRECATED_CALLOUT` | Callout feature name for service_templates_deprecated_callout. |
| <a id="usercalloutfeaturenameenumsuggest_pipeline"></a>`SUGGEST_PIPELINE` | Callout feature name for suggest_pipeline. |
| <a id="usercalloutfeaturenameenumsuggest_popover_dismissed"></a>`SUGGEST_POPOVER_DISMISSED` | Callout feature name for suggest_popover_dismissed. |
| <a id="usercalloutfeaturenameenumtabs_position_highlight"></a>`TABS_POSITION_HIGHLIGHT` | Callout feature name for tabs_position_highlight. |

View File

@ -377,7 +377,9 @@ happened over time, such as how many CI pipelines have run. They are monotonic a
Observations are facts collected from one or more GitLab instances and can carry arbitrary data. There are no
general guidelines around how to collect those, due to the individual nature of that data.
There are several types of counters which are all found in `usage_data.rb`:
### Types of counters
There are several types of counters in `usage_data.rb`:
- **Ordinary Batch Counters:** Simple count of a given ActiveRecord_Relation
- **Distinct Batch Counters:** Distinct count of a given ActiveRecord_Relation in a given column
@ -388,6 +390,19 @@ There are several types of counters which are all found in `usage_data.rb`:
NOTE:
Only use the provided counter methods. Each counter method contains a built in fail safe to isolate each counter to avoid breaking the entire Service Ping.
### Using instrumentation classes
We recommend you use [instrumentation classes](metrics_instrumentation.md) in `usage_data.rb` where possible.
For example, we have the following instrumentation class:
`lib/gitlab/usage/metrics/instrumentations/count_boards_metric.rb`.
You should add it to `usage_data.rb` as follows:
```ruby
boards: add_metric('CountBoardsMetric', time_frame: 'all'),
```
### Why batch counting
For large tables, PostgreSQL can take a long time to count rows due to MVCC [(Multi-version Concurrency Control)](https://en.wikipedia.org/wiki/Multiversion_concurrency_control). Batch counting is a counting method where a single large query is broken into multiple smaller queries. For example, instead of a single query querying 1,000,000 records, with batch counting, you can execute 100 queries of 10,000 records each. Batch counting is useful for avoiding database timeouts as each batch query is significantly shorter than one single long running query.

View File

@ -86,6 +86,21 @@ module Gitlab
end
```
## Support for instrumentation classes
There is support for:
- `count`, `distinct_count` for [database metrics](#database-metrics).
- [Redis HLL metrics](#redis-hyperloglog-metrics).
- [Generic metrics](#generic-metrics), which are metrics based on settings or configurations.
Currently, there is no support for:
- `add`, `sum`, `histogram`, `estimate_batch_distinct_count` for database metrics.
- Regular Redis counters.
You can [track the progress to support these](https://gitlab.com/groups/gitlab-org/-/epics/6118).
## Creating a new metric instrumentation class
To create a stub instrumentation for a Service Ping metric, you can use a dedicated [generator](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/generators/gitlab/usage_metric_generator.rb):

View File

@ -63,6 +63,9 @@ any of the following Service Ping files:
Read the [stages file](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/data/stages.yml).
- Check the file location. Consider the time frame, and if the file should be under `ee`.
- Check the tiers.
- Metrics instrumentations
- Recommend to use metrics instrumentation for new metrics addded to service with
[limitations](metrics_instrumentation.md#support-for-instrumentation-classes)
- Approve the MR, and relabel the MR with `~"product intelligence::approved"`.
## Review workload distribution

View File

@ -72,13 +72,21 @@ from the chart version to GitLab version to determine the [upgrade path](#upgrad
## Checking for background migrations before upgrading
Certain major/minor releases may require a set of background migrations to be
finished. The number of remaining migrations jobs can be found by running the
following command:
Certain major/minor releases may require different migrations to be
finished before you update to the newer version.
**For GitLab 14.0 and newer**
To check the status of [batched background migrations](../user/admin_area/monitoring/background_migrations.md):
1. On the top bar, select **Menu >** **{admin}** **Admin**.
1. On the left sidebar, select **Monitoring > Background Migrations**.
![queued batched background migrations table](img/batched_background_migrations_queued_v14_0.png)
**For Omnibus installations**
If using GitLab 12.9 and newer, run:
If using GitLab 12.9 and newer, also run:
```shell
sudo gitlab-rails runner -e production 'puts Gitlab::BackgroundMigration.remaining'
@ -107,12 +115,6 @@ Sidekiq::Queue.new("background_migration").size
Sidekiq::ScheduledSet.new.select { |r| r.klass == 'BackgroundMigrationWorker' }.size
```
### Batched background migrations
Batched background migrations need to finish before you update to a newer version.
Read more about [batched background migrations](../user/admin_area/monitoring/background_migrations.md).
### What do I do if my background migrations are stuck?
WARNING:

View File

@ -23,13 +23,8 @@ prevent integer overflow for some tables.
## Check the status of background migrations **(FREE SELF)**
All migrations must have a `Finished` status before updating GitLab. To check the status of the existing
migrations:
1. On the top bar, select **Menu >** **{admin}** **Admin**.
1. On the left sidebar, select **Monitoring > Background Migrations**.
![queued batched background migrations table](img/batched_background_migrations_queued_v14_0.png)
All migrations must have a `Finished` status before you [upgrade GitLab](../../../update/index.md).
You can [check the status of existing migrations](../../../update/index.md#checking-for-background-migrations-before-upgrading).
## Enable or disable batched background migrations **(FREE SELF)**

View File

@ -28,20 +28,11 @@ module Banzai
@references_per_parent[parent_type] ||= begin
refs = Hash.new { |hash, key| hash[key] = Set.new }
nodes.each do |node|
prepare_node_for_scan(node).scan(regex) do
parent_path = if parent_type == :project
full_project_path($~[:namespace], $~[:project])
else
full_group_path($~[:group])
end
ident = filter.identifier($~)
refs[parent_path] << ident if ident
end
if Feature.enabled?(:milestone_reference_pattern, default_enabled: :yaml)
doc_search(refs)
else
node_search(nodes, refs)
end
refs
end
end
@ -172,6 +163,39 @@ module Banzai
delegate :project, :group, :parent, :parent_type, to: :filter
# Deprecated: https://gitlab.com/gitlab-org/gitlab/-/issues/336268
def node_search(nodes, refs)
nodes.each do |node|
prepare_node_for_scan(node).scan(regex) do
parent_path = if parent_type == :project
full_project_path($~[:namespace], $~[:project])
else
full_group_path($~[:group])
end
ident = filter.identifier($~)
refs[parent_path] << ident if ident
end
end
refs
end
def doc_search(refs)
prepare_doc_for_scan(filter.doc).to_enum(:scan, regex).each do
parent_path = if parent_type == :project
full_project_path($~[:namespace], $~[:project])
else
full_group_path($~[:group])
end
ident = filter.identifier($~)
refs[parent_path] << ident if ident
end
refs
end
def regex
strong_memoize(:regex) do
[
@ -185,6 +209,13 @@ module Banzai
Gitlab::SafeRequestStore["banzai_#{parent_type}_refs".to_sym] ||= {}
end
def prepare_doc_for_scan(doc)
html = doc.to_html
filter.requires_unescaping? ? unescape_html_entities(html) : html
end
# Deprecated: https://gitlab.com/gitlab-org/gitlab/-/issues/336268
def prepare_node_for_scan(node)
html = node.to_html

View File

@ -42,9 +42,9 @@ module Gitlab
# timeout - The time after which the pool should be forcefully
# disconnected.
def disconnect!(timeout = 120)
start_time = Metrics::System.monotonic_time
start_time = ::Gitlab::Metrics::System.monotonic_time
while (Metrics::System.monotonic_time - start_time) <= timeout
while (::Gitlab::Metrics::System.monotonic_time - start_time) <= timeout
break if pool.connections.none?(&:in_use?)
sleep(2)

View File

@ -0,0 +1,26 @@
# frozen_string_literal: true
module Gitlab
module Database
class Metrics
extend ::Gitlab::Utils::StrongMemoize
class << self
def subtransactions_increment(model_name)
subtransactions_counter.increment(model: model_name)
end
private
def subtransactions_counter
strong_memoize(:subtransactions_counter) do
name = :gitlab_active_record_subtransactions_total
comment = 'Total amount of subtransactions created by ActiveRecord'
::Gitlab::Metrics.counter(name, comment)
end
end
end
end
end
end

View File

@ -19,7 +19,12 @@ module Gitlab
end
# Indexes with reindexing support
scope :reindexing_support, -> { where(partitioned: false, exclusion: false, expression: false, type: Gitlab::Database::Reindexing::SUPPORTED_TYPES) }
scope :reindexing_support, -> do
where(partitioned: false, exclusion: false, expression: false, type: Gitlab::Database::Reindexing::SUPPORTED_TYPES)
.not_match("#{Gitlab::Database::Reindexing::ReindexConcurrently::TEMPORARY_INDEX_PATTERN}$")
end
scope :reindexing_leftovers, -> { match("#{Gitlab::Database::Reindexing::ReindexConcurrently::TEMPORARY_INDEX_PATTERN}$") }
scope :not_match, ->(regex) { where("name !~ ?", regex) }

View File

@ -8,6 +8,13 @@ module Gitlab
SUPPORTED_TYPES = %w(btree gist).freeze
# When dropping an index, we acquire a SHARE UPDATE EXCLUSIVE lock,
# which only conflicts with DDL and vacuum. We therefore execute this with a rather
# high lock timeout and a long pause in between retries. This is an alternative to
# setting a high statement timeout, which would lead to a long running query with effects
# on e.g. vacuum.
REMOVE_INDEX_RETRY_CONFIG = [[1.minute, 9.minutes]] * 30
# candidate_indexes: Array of Gitlab::Database::PostgresIndex
def self.perform(candidate_indexes, how_many: DEFAULT_INDEXES_PER_INVOCATION)
IndexSelection.new(candidate_indexes).take(how_many).each do |index|
@ -15,10 +22,22 @@ module Gitlab
end
end
def self.candidate_indexes
Gitlab::Database::PostgresIndex
.not_match("#{ReindexConcurrently::TEMPORARY_INDEX_PATTERN}$")
.reindexing_support
def self.cleanup_leftovers!
PostgresIndex.reindexing_leftovers.each do |index|
Gitlab::AppLogger.info("Removing index #{index.identifier} which is a leftover, temporary index from previous reindexing activity")
retries = Gitlab::Database::WithLockRetriesOutsideTransaction.new(
timing_configuration: REMOVE_INDEX_RETRY_CONFIG,
klass: self.class,
logger: Gitlab::AppLogger
)
retries.run(raise_on_exhaustion: false) do
ApplicationRecord.connection.tap do |conn|
conn.execute("DROP INDEX CONCURRENTLY IF EXISTS #{conn.quote_table_name(index.schema)}.#{conn.quote_table_name(index.name)}")
end
end
end
end
end
end

View File

@ -11,13 +11,6 @@ module Gitlab
STATEMENT_TIMEOUT = 9.hours
PG_MAX_INDEX_NAME_LENGTH = 63
# When dropping an index, we acquire a SHARE UPDATE EXCLUSIVE lock,
# which only conflicts with DDL and vacuum. We therefore execute this with a rather
# high lock timeout and a long pause in between retries. This is an alternative to
# setting a high statement timeout, which would lead to a long running query with effects
# on e.g. vacuum.
REMOVE_INDEX_RETRY_CONFIG = [[1.minute, 9.minutes]] * 30
attr_reader :index, :logger
def initialize(index, logger: Gitlab::AppLogger)

View File

@ -15,6 +15,10 @@ module Gitlab
@time_frame = time_frame
@options = options
end
def instrumentation
value
end
end
end
end

View File

@ -5,7 +5,7 @@ module Gitlab
module Metrics
module Instrumentations
class CollectedDataCategoriesMetric < GenericMetric
def value
value do
::ServicePing::PermitDataCategoriesService.new.execute
end
end

View File

@ -59,6 +59,10 @@ module Gitlab
Gitlab::Usage::Metrics::Query.for(self.class.metric_operation, relation, self.class.column)
end
def instrumentation
to_sql
end
def suggested_name
Gitlab::Usage::Metrics::NameSuggestion.for(
self.class.metric_operation,

View File

@ -15,9 +15,7 @@ module Gitlab
FALLBACK = -1
class << self
attr_reader :metric_operation, :metric_value
@metric_operation = :alt
attr_reader :metric_value
def fallback(custom_fallback = FALLBACK)
return @metric_fallback if defined?(@metric_fallback)
@ -30,6 +28,11 @@ module Gitlab
end
end
def initialize(time_frame: 'none', options: {})
@time_frame = time_frame
@options = options
end
def value
alt_usage_data(fallback: self.class.fallback) do
self.class.metric_value.call
@ -37,9 +40,7 @@ module Gitlab
end
def suggested_name
Gitlab::Usage::Metrics::NameSuggestion.for(
self.class.metric_operation
)
Gitlab::Usage::Metrics::NameSuggestion.for(:alt)
end
end
end

View File

@ -12,11 +12,6 @@ module Gitlab
# events:
# - g_analytics_valuestream
# end
class << self
attr_reader :metric_operation
@metric_operation = :redis
end
def initialize(time_frame:, options: {})
super
@ -36,9 +31,7 @@ module Gitlab
end
def suggested_name
Gitlab::Usage::Metrics::NameSuggestion.for(
self.class.metric_operation
)
Gitlab::Usage::Metrics::NameSuggestion.for(:redis)
end
private

View File

@ -10,6 +10,12 @@ module Gitlab
uncached_data.deep_stringify_keys.dig(*key_path.split('.'))
end
def add_metric(metric, time_frame: 'none')
metric_class = "Gitlab::Usage::Metrics::Instrumentations::#{metric}".constantize
metric_class.new(time_frame: time_frame).suggested_name
end
private
def count(relation, column = nil, batch: true, batch_size: nil, start: nil, finish: nil)

View File

@ -72,8 +72,8 @@ module Gitlab
def license_usage_data
{
recorded_at: recorded_at,
uuid: alt_usage_data { Gitlab::CurrentSettings.uuid },
hostname: alt_usage_data { Gitlab.config.gitlab.host },
uuid: add_metric('UuidMetric'),
hostname: add_metric('HostnameMetric'),
version: alt_usage_data { Gitlab::VERSION },
installation_type: alt_usage_data { installation_type },
active_user_count: count(User.active),
@ -93,7 +93,7 @@ module Gitlab
{
counts: {
assignee_lists: count(List.assignee),
boards: count(Board),
boards: add_metric('CountBoardsMetric', time_frame: 'all'),
ci_builds: count(::Ci::Build),
ci_internal_pipelines: count(::Ci::Pipeline.internal),
ci_external_pipelines: count(::Ci::Pipeline.external),
@ -138,7 +138,7 @@ module Gitlab
in_review_folder: count(::Environment.in_review_folder),
grafana_integrated_projects: count(GrafanaIntegration.enabled),
groups: count(Group),
issues: count(Issue, start: minimum_id(Issue), finish: maximum_id(Issue)),
issues: add_metric('CountIssuesMetric', time_frame: 'all'),
issues_created_from_gitlab_error_tracking_ui: count(SentryIssue),
issues_with_associated_zoom_link: count(ZoomMeeting.added_to_issue),
issues_using_zoom_quick_actions: distinct_count(ZoomMeeting, :issue_id),
@ -257,7 +257,7 @@ module Gitlab
ldap_encrypted_secrets_enabled: alt_usage_data(fallback: nil) { Gitlab::Auth::Ldap::Config.encrypted_secrets.active? },
operating_system: alt_usage_data(fallback: nil) { operating_system },
gitaly_apdex: alt_usage_data { gitaly_apdex },
collected_data_categories: alt_usage_data(fallback: []) { Gitlab::Usage::Metrics::Instrumentations::CollectedDataCategoriesMetric.new(time_frame: 'none').value }
collected_data_categories: add_metric('CollectedDataCategoriesMetric', time_frame: 'none')
}
}
end
@ -644,8 +644,9 @@ module Gitlab
# Omitted because of encrypted properties: `projects_jira_cloud_active`, `projects_jira_server_active`
# rubocop: disable CodeReuse/ActiveRecord
def usage_activity_by_stage_plan(time_period)
time_frame = time_period.present? ? '28d' : 'none'
{
issues: distinct_count(::Issue.where(time_period), :author_id),
issues: add_metric('CountUsersCreatingIssuesMetric', time_frame: time_frame),
notes: distinct_count(::Note.where(time_period), :author_id),
projects: distinct_count(::Project.where(time_period), :creator_id),
todos: distinct_count(::Todo.where(time_period), :author_id),

View File

@ -5,6 +5,12 @@ module Gitlab
SQL_METRIC_DEFAULT = -3
class << self
def add_metric(metric, time_frame: 'none')
metric_class = "Gitlab::Usage::Metrics::Instrumentations::#{metric}".constantize
metric_class.new(time_frame: time_frame).instrumentation
end
def count(relation, column = nil, batch: true, batch_size: nil, start: nil, finish: nil)
SQL_METRIC_DEFAULT
end

View File

@ -5,6 +5,12 @@ module Gitlab
# See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41091
class UsageDataQueries < UsageData
class << self
def add_metric(metric, time_frame: 'none')
metric_class = "Gitlab::Usage::Metrics::Instrumentations::#{metric}".constantize
metric_class.new(time_frame: time_frame).instrumentation
end
def count(relation, column = nil, *args, **kwargs)
Gitlab::Usage::Metrics::Query.for(:count, relation, column)
end

View File

@ -44,6 +44,12 @@ module Gitlab
DISTRIBUTED_HLL_FALLBACK = -2
MAX_BUCKET_SIZE = 100
def add_metric(metric, time_frame: 'none')
metric_class = "Gitlab::Usage::Metrics::Instrumentations::#{metric}".constantize
metric_class.new(time_frame: time_frame).value
end
def count(relation, column = nil, batch: true, batch_size: nil, start: nil, finish: nil)
if batch
Gitlab::Database::BatchCount.batch_count(relation, column, batch_size: batch_size, start: start, finish: finish)

View File

@ -161,7 +161,7 @@ namespace :gitlab do
exit
end
indexes = Gitlab::Database::Reindexing.candidate_indexes
indexes = Gitlab::Database::PostgresIndex.reindexing_support
if identifier = args[:index_name]
raise ArgumentError, "Index name is not fully qualified with a schema: #{identifier}" unless identifier =~ /^\w+\.\w+$/
@ -173,6 +173,9 @@ namespace :gitlab do
ActiveRecord::Base.logger = Logger.new($stdout) if Gitlab::Utils.to_boolean(ENV['LOG_QUERIES_TO_CONSOLE'], default: false)
# Cleanup leftover temporary indexes from previous, possibly aborted runs (if any)
Gitlab::Database::Reindexing.cleanup_leftovers!
Gitlab::Database::Reindexing.perform(indexes)
rescue StandardError => e
Gitlab::AppLogger.error(e)

View File

@ -2389,9 +2389,6 @@ msgstr ""
msgid "AdminSettings|Maximum duration of a session for Git operations when 2FA is enabled."
msgstr ""
msgid "AdminSettings|Moved to integrations"
msgstr ""
msgid "AdminSettings|New CI/CD variables in projects and groups default to protected."
msgstr ""
@ -2404,21 +2401,12 @@ msgstr ""
msgid "AdminSettings|Required pipeline configuration"
msgstr ""
msgid "AdminSettings|See affected service templates"
msgstr ""
msgid "AdminSettings|Select a CI/CD template"
msgstr ""
msgid "AdminSettings|Select a group to use as the source for instance-level project templates."
msgstr ""
msgid "AdminSettings|Service template allows you to set default values for integrations"
msgstr ""
msgid "AdminSettings|Service templates are deprecated and will be removed in GitLab 14.0."
msgstr ""
msgid "AdminSettings|Session duration for Git operations when 2FA is enabled (minutes)"
msgstr ""
@ -2440,12 +2428,6 @@ msgstr ""
msgid "AdminSettings|The template for the required pipeline configuration can be one of the GitLab-provided templates, or a custom template added to an instance template repository. %{link_start}How do I create an instance template repository?%{link_end}"
msgstr ""
msgid "AdminSettings|You can't add new templates. To migrate or remove a Service template, create a new integration at %{settings_link_start}Settings &gt; Integrations%{link_end}. Learn more about %{doc_link_start}Project integration management%{link_end}."
msgstr ""
msgid "AdminSettings|You should migrate to %{doc_link_start}Project integration management%{link_end}, available at %{settings_link_start}Settings &gt; Integrations.%{link_end}"
msgstr ""
msgid "AdminStatistics|Active Users"
msgstr ""
@ -19143,9 +19125,6 @@ msgstr ""
msgid "Last contact"
msgstr ""
msgid "Last edit"
msgstr ""
msgid "Last edited %{date}"
msgstr ""
@ -19425,9 +19404,6 @@ msgstr ""
msgid "Leave edit mode? All unsaved changes will be lost."
msgstr ""
msgid "Leave feedback"
msgstr ""
msgid "Leave group"
msgstr ""
@ -23015,9 +22991,6 @@ msgstr ""
msgid "Only include features new to your current subscription tier."
msgstr ""
msgid "Only owners can update Security Policy Project"
msgstr ""
msgid "Only policy:"
msgstr ""
@ -23099,9 +23072,6 @@ msgstr ""
msgid "Opens in a new window"
msgstr ""
msgid "Operation completed"
msgstr ""
msgid "Operation failed. Check pod logs for %{pod_name} for more details."
msgstr ""
@ -23384,6 +23354,9 @@ msgstr ""
msgid "PackageRegistry|Delete package"
msgstr ""
msgid "PackageRegistry|Failed to load the package data"
msgstr ""
msgid "PackageRegistry|For more information on Composer packages in GitLab, %{linkStart}see the documentation.%{linkEnd}"
msgstr ""
@ -29048,15 +29021,24 @@ msgstr ""
msgid "SecurityConfiguration|You can quickly enable all security scanning tools by enabling %{linkStart}Auto DevOps%{linkEnd}."
msgstr ""
msgid "SecurityOrchestration|A security policy project can be used enforce policies for a given project, group, or instance. It allows you to specify security policies that are important to you and enforce them with every commit."
msgid "SecurityOrchestration|A security policy project can enforce policies for a given project, group, or instance. With a security policy project, you can specify security policies that are important to you and enforce them with every commit. %{linkStart}More information.%{linkEnd}"
msgstr ""
msgid "SecurityOrchestration|An error occurred assigning your security policy project"
msgstr ""
msgid "SecurityOrchestration|Create a policy"
msgstr ""
msgid "SecurityOrchestration|Only owners can update Security Policy Project"
msgstr ""
msgid "SecurityOrchestration|Security policy project"
msgstr ""
msgid "SecurityOrchestration|Security policy project was linked successfully"
msgstr ""
msgid "SecurityPolicies|+%{count} more"
msgstr ""
@ -29732,9 +29714,6 @@ msgstr ""
msgid "Service Desk allows people to create issues in your GitLab instance without their own user account. It provides a unique email address for end users to create issues in a project. Replies can be sent either through the GitLab interface or by email. End users only see threads through email."
msgstr ""
msgid "Service Templates"
msgstr ""
msgid "Service URL"
msgstr ""
@ -29996,6 +29975,9 @@ msgstr ""
msgid "Setting"
msgstr ""
msgid "Setting enforced"
msgstr ""
msgid "Setting this to 0 means using the system default timeout value."
msgstr ""

View File

@ -10,6 +10,8 @@ global:
secretName: review-apps-tls
initialRootPassword:
secret: shared-gitlab-initial-root-password
nodeSelector:
preemptible: "true"
certmanager:
install: false
gitlab:
@ -24,6 +26,8 @@ gitlab:
persistence:
size: 10G
storageClass: ssd
nodeSelector:
preemptible: "false"
gitlab-exporter:
enabled: false
mailroom:
@ -100,6 +104,8 @@ gitlab-runner:
limits:
cpu: 1015m
memory: 150M
nodeSelector:
preemptible: "true"
minio:
resources:
requests:
@ -108,6 +114,8 @@ minio:
limits:
cpu: 15m
memory: 280M
nodeSelector:
preemptible: "true"
nginx-ingress:
controller:
config:
@ -125,6 +133,8 @@ nginx-ingress:
timeoutSeconds: 5
readinessProbe:
timeoutSeconds: 5
nodeSelector:
preemptible: "true"
defaultBackend:
resources:
requests:
@ -133,6 +143,8 @@ nginx-ingress:
limits:
cpu: 10m
memory: 24M
nodeSelector:
preemptible: "true"
postgresql:
metrics:
enabled: false
@ -143,6 +155,9 @@ postgresql:
limits:
cpu: 1300m
memory: 1500M
master:
nodeSelector:
preemptible: "true"
prometheus:
install: false
redis:
@ -155,6 +170,9 @@ redis:
limits:
cpu: 200m
memory: 130M
master:
nodeSelector:
preemptible: "true"
registry:
hpa:
minReplicas: 1
@ -165,3 +183,5 @@ registry:
limits:
cpu: 200m
memory: 45M
nodeSelector:
preemptible: "true"

View File

@ -1,75 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Admin::ServicesController do
let(:admin) { create(:admin) }
before do
sign_in(admin)
end
describe 'GET #edit' do
let(:service) do
create(:jira_integration, :template)
end
it 'successfully displays the template' do
get :edit, params: { id: service.id }
expect(response).to have_gitlab_http_status(:ok)
end
context 'when integration does not exists' do
it 'redirects to the admin application integration page' do
get :edit, params: { id: 'invalid' }
expect(response).to redirect_to(admin_application_settings_services_path)
end
end
context 'when instance integration exists' do
before do
create(:jira_integration, :instance)
end
it 'redirects to the admin application integration page' do
get :edit, params: { id: service.id }
expect(response).to redirect_to(admin_application_settings_services_path)
end
end
end
describe "#update" do
let(:project) { create(:project) }
let!(:service_template) do
Integrations::Redmine.create!(
project: nil,
active: false,
template: true,
properties: {
project_url: 'http://abc',
issues_url: 'http://abc',
new_issue_url: 'http://abc'
}
)
end
it 'calls the propagation worker when service is active' do
expect(PropagateServiceTemplateWorker).to receive(:perform_async).with(service_template.id)
put :update, params: { id: service_template.id, service: { active: true } }
expect(response).to have_gitlab_http_status(:found)
end
it 'does not call the propagation worker when service is not active' do
expect(PropagateServiceTemplateWorker).not_to receive(:perform_async)
put :update, params: { id: service_template.id, service: { properties: {} } }
expect(response).to have_gitlab_http_status(:found)
end
end
end

View File

@ -25,7 +25,6 @@ FactoryBot.define do
create(:service, project: projects[2], type: 'SlackService', active: true)
create(:service, project: projects[2], type: 'MattermostService', active: false)
create(:service, group: group, project: nil, type: 'MattermostService', active: true)
create(:service, :template, type: 'MattermostService', active: true)
mattermost_instance = create(:service, :instance, type: 'MattermostService', active: true)
create(:service, project: projects[1], type: 'MattermostService', active: true, inherit_from_id: mattermost_instance.id)
create(:service, group: group, project: nil, type: 'SlackService', active: true, inherit_from_id: mattermost_instance.id)

View File

@ -1,53 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Admin visits service templates' do
let(:admin) { create(:user, :admin) }
let(:slack_integration) { Integration.for_template.find { |s| s.type == 'SlackService' } }
before do
sign_in(admin)
gitlab_enable_admin_mode_sign_in(admin)
end
context 'without an active service template' do
before do
visit(admin_application_settings_services_path)
end
it 'does not show service template content' do
expect(page).not_to have_content('Service template allows you to set default values for integrations')
end
end
context 'with an active service template' do
before do
create(:integrations_slack, :template, active: true)
visit(admin_application_settings_services_path)
end
it 'shows service template content' do
expect(page).to have_content('Service template allows you to set default values for integrations')
end
context 'without instance-level integration' do
it 'shows a link to service template' do
expect(page).to have_link('Slack', href: edit_admin_application_settings_service_path(slack_integration.id))
expect(page).not_to have_link('Slack', href: edit_admin_application_settings_integration_path(slack_integration))
end
end
context 'with instance-level integration' do
before do
create(:integrations_slack, instance: true, project: nil)
visit(admin_application_settings_services_path)
end
it 'shows a link to instance-level integration' do
expect(page).not_to have_link('Slack', href: edit_admin_application_settings_service_path(slack_integration.id))
expect(page).to have_link('Slack', href: edit_admin_application_settings_integration_path(slack_integration))
end
end
end
end

View File

@ -1,59 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Service templates deprecation callout' do
let_it_be(:admin) { create(:admin) }
let_it_be(:non_admin) { create(:user) }
let_it_be(:callout_content) { 'Service templates are deprecated and will be removed in GitLab 14.0.' }
context 'when a non-admin is logged in' do
before do
sign_in(non_admin)
visit root_dashboard_path
end
it 'does not display callout' do
expect(page).not_to have_content callout_content
end
end
context 'when an admin is logged in' do
before do
sign_in(admin)
gitlab_enable_admin_mode_sign_in(admin)
visit root_dashboard_path
end
context 'with no active service templates' do
it 'does not display callout' do
expect(page).not_to have_content callout_content
end
end
context 'with active service template' do
before do
create(:service, :template, type: 'MattermostService', active: true)
visit root_dashboard_path
end
it 'displays callout' do
expect(page).to have_content callout_content
expect(page).to have_link 'See affected service templates', href: admin_application_settings_services_path
end
context 'when callout is dismissed', :js do
before do
find('[data-testid="close-service-templates-deprecated-callout"]').click
visit root_dashboard_path
end
it 'does not display callout' do
expect(page).not_to have_content callout_content
end
end
end
end
end

View File

@ -0,0 +1,177 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PackageTitle renders with tags 1`] = `
<div
class="gl-display-flex gl-flex-direction-column"
data-qa-selector="package_title"
>
<div
class="gl-display-flex gl-justify-content-space-between gl-py-3"
>
<div
class="gl-flex-direction-column gl-flex-grow-1"
>
<div
class="gl-display-flex"
>
<!---->
<div
class="gl-display-flex gl-flex-direction-column"
>
<h1
class="gl-font-size-h1 gl-mt-3 gl-mb-2"
data-testid="title"
>
@gitlab-org/package-15
</h1>
<div
class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-mt-1"
>
<gl-icon-stub
class="gl-mr-3"
name="eye"
size="16"
/>
<gl-sprintf-stub
message="v%{version} published %{timeAgo}"
/>
</div>
</div>
</div>
<div
class="gl-display-flex gl-flex-wrap gl-align-items-center gl-mt-3"
>
<div
class="gl-display-flex gl-align-items-center gl-mr-5"
>
<metadata-item-stub
data-testid="package-type"
icon="package"
link=""
size="s"
text="npm"
texttooltip=""
/>
</div>
<div
class="gl-display-flex gl-align-items-center gl-mr-5"
>
<metadata-item-stub
data-testid="package-size"
icon="disk"
link=""
size="s"
text="800.00 KiB"
texttooltip=""
/>
</div>
<div
class="gl-display-flex gl-align-items-center gl-mr-5"
>
<package-tags-stub
hidelabel="true"
tagdisplaylimit="2"
tags="[object Object],[object Object],[object Object]"
/>
</div>
</div>
</div>
<!---->
</div>
<p />
</div>
`;
exports[`PackageTitle renders without tags 1`] = `
<div
class="gl-display-flex gl-flex-direction-column"
data-qa-selector="package_title"
>
<div
class="gl-display-flex gl-justify-content-space-between gl-py-3"
>
<div
class="gl-flex-direction-column gl-flex-grow-1"
>
<div
class="gl-display-flex"
>
<!---->
<div
class="gl-display-flex gl-flex-direction-column"
>
<h1
class="gl-font-size-h1 gl-mt-3 gl-mb-2"
data-testid="title"
>
@gitlab-org/package-15
</h1>
<div
class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-mt-1"
>
<gl-icon-stub
class="gl-mr-3"
name="eye"
size="16"
/>
<gl-sprintf-stub
message="v%{version} published %{timeAgo}"
/>
</div>
</div>
</div>
<div
class="gl-display-flex gl-flex-wrap gl-align-items-center gl-mt-3"
>
<div
class="gl-display-flex gl-align-items-center gl-mr-5"
>
<metadata-item-stub
data-testid="package-type"
icon="package"
link=""
size="s"
text="npm"
texttooltip=""
/>
</div>
<div
class="gl-display-flex gl-align-items-center gl-mr-5"
>
<metadata-item-stub
data-testid="package-size"
icon="disk"
link=""
size="s"
text="800.00 KiB"
texttooltip=""
/>
</div>
<div
class="gl-display-flex gl-align-items-center gl-mr-5"
>
<package-tags-stub
hidelabel="true"
tagdisplaylimit="2"
tags="[object Object],[object Object],[object Object]"
/>
</div>
</div>
</div>
<!---->
</div>
<p />
</div>
`;

View File

@ -1,15 +1,36 @@
import { GlEmptyState } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import PackagesApp from '~/packages_and_registries/package_registry/components/details/app.vue';
import PackageTitle from '~/packages_and_registries/package_registry/components/details/package_title.vue';
import { FETCH_PACKAGE_DETAILS_ERROR_MESSAGE } from '~/packages_and_registries/package_registry/constants';
import getPackageDetails from '~/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql';
import { packageDetailsQuery, packageData } from '../../mock_data';
jest.mock('~/flash');
const localVue = createLocalVue();
describe('PackagesApp', () => {
let wrapper;
let apolloProvider;
function createComponent({ resolver = jest.fn().mockResolvedValue(packageDetailsQuery()) } = {}) {
localVue.use(VueApollo);
const requestHandlers = [[getPackageDetails, resolver]];
apolloProvider = createMockApollo(requestHandlers);
function createComponent() {
wrapper = shallowMount(PackagesApp, {
localVue,
apolloProvider,
provide: {
titleComponent: 'titleComponent',
packageId: '111',
titleComponent: 'PackageTitle',
projectName: 'projectName',
canDelete: 'canDelete',
svgPath: 'svgPath',
@ -21,7 +42,8 @@ describe('PackagesApp', () => {
});
}
const emptyState = () => wrapper.findComponent(GlEmptyState);
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findPackageTitle = () => wrapper.findComponent(PackageTitle);
afterEach(() => {
wrapper.destroy();
@ -30,6 +52,29 @@ describe('PackagesApp', () => {
it('renders an empty state component', () => {
createComponent();
expect(emptyState().exists()).toBe(true);
expect(findEmptyState().exists()).toBe(true);
});
it('renders the app and displays the package title', async () => {
createComponent();
await waitForPromises();
expect(findPackageTitle().exists()).toBe(true);
expect(findPackageTitle().props()).toMatchObject({
packageEntity: expect.objectContaining(packageData()),
});
});
it('emits an error message if the load fails', async () => {
createComponent({ resolver: jest.fn().mockRejectedValue() });
await waitForPromises();
expect(createFlash).toHaveBeenCalledWith(
expect.objectContaining({
message: FETCH_PACKAGE_DETAILS_ERROR_MESSAGE,
}),
);
});
});

View File

@ -0,0 +1,176 @@
import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import PackageTags from '~/packages/shared/components/package_tags.vue';
import PackageTitle from '~/packages_and_registries/package_registry/components/details/package_title.vue';
import {
PACKAGE_TYPE_CONAN,
PACKAGE_TYPE_MAVEN,
PACKAGE_TYPE_NPM,
PACKAGE_TYPE_NUGET,
} from '~/packages_and_registries/package_registry/constants';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import { packageData, packageFiles, packageTags, packagePipelines } from '../../mock_data';
const packageWithTags = {
...packageData(),
tags: { nodes: packageTags() },
packageFiles: { nodes: packageFiles() },
};
describe('PackageTitle', () => {
let wrapper;
function createComponent(packageEntity = packageWithTags) {
wrapper = shallowMountExtended(PackageTitle, {
propsData: { packageEntity },
stubs: {
TitleArea,
},
});
return wrapper.vm.$nextTick();
}
const findTitleArea = () => wrapper.findComponent(TitleArea);
const findPackageType = () => wrapper.findByTestId('package-type');
const findPackageSize = () => wrapper.findByTestId('package-size');
const findPipelineProject = () => wrapper.findByTestId('pipeline-project');
const findPackageRef = () => wrapper.findByTestId('package-ref');
const findPackageTags = () => wrapper.findComponent(PackageTags);
const findPackageBadges = () => wrapper.findAllByTestId('tag-badge');
afterEach(() => {
wrapper.destroy();
});
describe('renders', () => {
it('without tags', async () => {
await createComponent();
expect(wrapper.element).toMatchSnapshot();
});
it('with tags', async () => {
await createComponent();
expect(wrapper.element).toMatchSnapshot();
});
it('with tags on mobile', async () => {
jest.spyOn(GlBreakpointInstance, 'isDesktop').mockReturnValue(false);
await createComponent();
await wrapper.vm.$nextTick();
expect(findPackageBadges()).toHaveLength(packageTags().length);
});
});
describe('package title', () => {
it('is correctly bound', async () => {
await createComponent();
expect(findTitleArea().props('title')).toBe(packageData().name);
});
});
describe('package icon', () => {
const iconUrl = 'a-fake-src';
it('shows an icon when present and package type is NUGET', async () => {
await createComponent({
...packageData(),
packageType: PACKAGE_TYPE_NUGET,
metadata: { iconUrl },
});
expect(findTitleArea().props('avatar')).toBe(iconUrl);
});
it('hides the icon when not present', async () => {
await createComponent();
expect(findTitleArea().props('avatar')).toBe(null);
});
});
describe.each`
packageType | text
${PACKAGE_TYPE_CONAN} | ${'Conan'}
${PACKAGE_TYPE_MAVEN} | ${'Maven'}
${PACKAGE_TYPE_NPM} | ${'npm'}
${PACKAGE_TYPE_NUGET} | ${'NuGet'}
`(`package type`, ({ packageType, text }) => {
beforeEach(() => createComponent({ ...packageData, packageType }));
it(`${packageType} should render ${text}`, () => {
expect(findPackageType().props()).toEqual(expect.objectContaining({ text, icon: 'package' }));
});
});
describe('calculates the package size', () => {
it('correctly calculates when there is only 1 file', async () => {
await createComponent({ ...packageData(), packageFiles: { nodes: [packageFiles()[0]] } });
expect(findPackageSize().props()).toMatchObject({ text: '400.00 KiB', icon: 'disk' });
});
it('correctly calculates when there are multiple files', async () => {
await createComponent();
expect(findPackageSize().props('text')).toBe('800.00 KiB');
});
});
describe('package tags', () => {
it('displays the package-tags component when the package has tags', async () => {
await createComponent();
expect(findPackageTags().exists()).toBe(true);
});
it('does not display the package-tags component when there are no tags', async () => {
await createComponent({ ...packageData(), tags: { nodes: [] } });
expect(findPackageTags().exists()).toBe(false);
});
});
describe('package ref', () => {
it('does not display the ref if missing', async () => {
await createComponent();
expect(findPackageRef().exists()).toBe(false);
});
it('correctly shows the package ref if there is one', async () => {
await createComponent({
...packageData(),
pipelines: { nodes: packagePipelines({ ref: 'test' }) },
});
expect(findPackageRef().props()).toMatchObject({
text: 'test',
icon: 'branch',
});
});
});
describe('pipeline project', () => {
it('does not display the project if missing', async () => {
await createComponent();
expect(findPipelineProject().exists()).toBe(false);
});
it('correctly shows the pipeline project if there is one', async () => {
await createComponent({
...packageData(),
pipelines: { nodes: packagePipelines() },
});
expect(findPipelineProject().props()).toMatchObject({
text: packagePipelines()[0].project.name,
icon: 'review-list',
link: packagePipelines()[0].project.webUrl,
});
});
});
});

View File

@ -0,0 +1,70 @@
export const packageTags = () => [
{ id: 'gid://gitlab/Packages::Tag/87', name: 'bananas_9', __typename: 'PackageTag' },
{ id: 'gid://gitlab/Packages::Tag/86', name: 'bananas_8', __typename: 'PackageTag' },
{ id: 'gid://gitlab/Packages::Tag/85', name: 'bananas_7', __typename: 'PackageTag' },
];
export const packagePipelines = (extend) => [
{
project: {
name: 'project14',
webUrl: 'http://gdk.test:3000/namespace14/project14',
__typename: 'Project',
},
...extend,
__typename: 'Pipeline',
},
];
export const packageFiles = () => [
{
id: 'gid://gitlab/Packages::PackageFile/118',
fileMd5: null,
fileName: 'foo-1.0.1.tgz',
fileSha1: 'be93151dc23ac34a82752444556fe79b32c7a1ad',
fileSha256: null,
size: '409600',
__typename: 'PackageFile',
},
{
id: 'gid://gitlab/Packages::PackageFile/119',
fileMd5: null,
fileName: 'foo-1.0.2.tgz',
fileSha1: 'be93151dc23ac34a82752444556fe79b32c7a1ss',
fileSha256: null,
size: '409600',
__typename: 'PackageFile',
},
];
export const packageData = (extend) => ({
id: 'gid://gitlab/Packages::Package/111',
name: '@gitlab-org/package-15',
packageType: 'NPM',
version: '1.0.0',
createdAt: '2020-08-17T14:23:32Z',
updatedAt: '2020-08-17T14:23:32Z',
status: 'DEFAULT',
...extend,
});
export const packageDetailsQuery = () => ({
data: {
package: {
...packageData(),
tags: {
nodes: packageTags(),
__typename: 'PackageTagConnection',
},
pipelines: {
nodes: packagePipelines(),
__typename: 'PipelineConnection',
},
packageFiles: {
nodes: packageFiles(),
__typename: 'PackageFileConnection',
},
__typename: 'PackageDetailsType',
},
},
});

View File

@ -0,0 +1,23 @@
import { getPackageTypeLabel } from '~/packages_and_registries/package_registry/utils';
describe('Packages shared utils', () => {
describe('getPackageTypeLabel', () => {
describe.each`
packageType | expectedResult
${'CONAN'} | ${'Conan'}
${'MAVEN'} | ${'Maven'}
${'NPM'} | ${'npm'}
${'NUGET'} | ${'NuGet'}
${'PYPI'} | ${'PyPI'}
${'RUBYGEMS'} | ${'RubyGems'}
${'COMPOSER'} | ${'Composer'}
${'DEBIAN'} | ${'Debian'}
${'HELM'} | ${'Helm'}
${'FOO'} | ${null}
`(`package type`, ({ packageType, expectedResult }) => {
it(`${packageType} should show as ${expectedResult}`, () => {
expect(getPackageTypeLabel(packageType)).toBe(expectedResult);
});
});
});
});

View File

@ -219,4 +219,25 @@ RSpec.describe PackagesHelper do
it { is_expected.to eq(expected_result) }
end
end
describe '#package_details_data' do
let_it_be(:package) { create(:package) }
before do
allow(helper).to receive(:current_user) { project.owner }
allow(helper).to receive(:can?) { true }
end
it 'when use_presenter is true populate the package key' do
result = helper.package_details_data(project, package, true)
expect(result[:package]).not_to be_nil
end
it 'when use_presenter is false the package key is nil' do
result = helper.package_details_data(project, package, false)
expect(result[:package]).to be_nil
end
end
end

View File

@ -61,34 +61,6 @@ RSpec.describe UserCalloutsHelper do
end
end
describe '.show_service_templates_deprecated_callout?' do
using RSpec::Parameterized::TableSyntax
let_it_be(:admin) { create(:user, :admin) }
let_it_be(:non_admin) { create(:user) }
subject { helper.show_service_templates_deprecated_callout? }
where(:self_managed, :is_admin_user, :has_active_service_template, :callout_dismissed, :should_show_callout) do
true | true | true | false | true
true | true | true | true | false
true | false | true | false | false
false | true | true | false | false
true | true | false | false | false
end
with_them do
before do
allow(::Gitlab).to receive(:com?).and_return(!self_managed)
allow(helper).to receive(:current_user).and_return(is_admin_user ? admin : non_admin)
allow(helper).to receive(:user_dismissed?).with(described_class::SERVICE_TEMPLATES_DEPRECATED_CALLOUT) { callout_dismissed }
create(:service, :template, type: 'MattermostService', active: has_active_service_template)
end
it { is_expected.to be should_show_callout }
end
end
describe '.show_customize_homepage_banner?' do
subject { helper.show_customize_homepage_banner? }

View File

@ -92,6 +92,11 @@ RSpec.describe Banzai::Filter::References::MilestoneReferenceFilter do
expect(doc.to_html).to match(%r(\(<a.+>#{milestone.reference_link_text}</a>\.\)))
end
it 'links with adjacent html tags' do
doc = reference_filter("Milestone <p>#{reference}</p>.")
expect(doc.to_html).to match(%r(<p><a.+>#{milestone.reference_link_text}</a></p>))
end
it 'ignores invalid milestone names' do
exp = act = "Milestone #{Milestone.reference_prefix}#{milestone.name.reverse}"

View File

@ -40,6 +40,37 @@ RSpec.describe Gitlab::Database::PostgresIndex do
expect(types & %w(btree gist)).to eq(types)
end
context 'with leftover indexes' do
before do
ActiveRecord::Base.connection.execute(<<~SQL)
CREATE INDEX foobar_ccnew ON users (id);
CREATE INDEX foobar_ccnew1 ON users (id);
SQL
end
subject { described_class.reindexing_support.map(&:name) }
it 'excludes temporary indexes from reindexing' do
expect(subject).not_to include('foobar_ccnew')
expect(subject).not_to include('foobar_ccnew1')
end
end
end
describe '.reindexing_leftovers' do
subject { described_class.reindexing_leftovers }
before do
ActiveRecord::Base.connection.execute(<<~SQL)
CREATE INDEX foobar_ccnew ON users (id);
CREATE INDEX foobar_ccnew1 ON users (id);
SQL
end
it 'retrieves leftover indexes matching the /_ccnew[0-9]*$/ pattern' do
expect(subject.map(&:name)).to eq(%w(foobar_ccnew foobar_ccnew1))
end
end
describe '.not_match' do

View File

@ -26,14 +26,31 @@ RSpec.describe Gitlab::Database::Reindexing do
end
end
describe '.candidate_indexes' do
subject { described_class.candidate_indexes }
describe '.cleanup_leftovers!' do
subject { described_class.cleanup_leftovers! }
it 'retrieves regular indexes that are no left-overs from previous runs' do
result = double
expect(Gitlab::Database::PostgresIndex).to receive_message_chain('not_match.reindexing_support').with('\_ccnew[0-9]*$').with(no_args).and_return(result)
before do
ApplicationRecord.connection.execute(<<~SQL)
CREATE INDEX foobar_ccnew ON users (id);
CREATE INDEX foobar_ccnew1 ON users (id);
SQL
end
expect(subject).to eq(result)
it 'drops both leftover indexes' do
expect_query("SET lock_timeout TO '60000ms'")
expect_query("DROP INDEX CONCURRENTLY IF EXISTS \"public\".\"foobar_ccnew\"")
expect_query("RESET idle_in_transaction_session_timeout; RESET lock_timeout")
expect_query("SET lock_timeout TO '60000ms'")
expect_query("DROP INDEX CONCURRENTLY IF EXISTS \"public\".\"foobar_ccnew1\"")
expect_query("RESET idle_in_transaction_session_timeout; RESET lock_timeout")
subject
end
def expect_query(sql)
expect(ApplicationRecord.connection).to receive(:execute).ordered.with(sql).and_wrap_original do |method, sql|
method.call(sql.sub(/CONCURRENTLY/, ''))
end
end
end
end

View File

@ -16,6 +16,14 @@ RSpec.describe Gitlab::Usage::Metrics::NamesSuggestions::Generator do
end
end
describe '#add_metric' do
let(:metric) {'CountIssuesMetric' }
it 'computes the suggested name for given metric' do
expect(described_class.add_metric(metric)).to eq('count_issues')
end
end
context 'for count with default column metrics' do
it_behaves_like 'name suggestion' do
# corresponding metric is collected with count(Board)

View File

@ -42,6 +42,10 @@ RSpec.describe Gitlab::UsageDataMetrics do
it 'includes usage_activity_by_stage_monthly keys' do
expect(subject[:usage_activity_by_stage_monthly][:plan]).to include(:issues)
end
it 'includes settings keys' do
expect(subject[:settings]).to include(:collected_data_categories)
end
end
end
end

View File

@ -5,6 +5,14 @@ require 'spec_helper'
RSpec.describe Gitlab::UsageDataNonSqlMetrics do
let(:default_count) { Gitlab::UsageDataNonSqlMetrics::SQL_METRIC_DEFAULT }
describe '#add_metric' do
let(:metric) { 'UuidMetric' }
it 'computes the metric value for given metric' do
expect(described_class.add_metric(metric)).to eq(Gitlab::CurrentSettings.uuid)
end
end
describe '.count' do
it 'returns default value for count' do
expect(described_class.count(User)).to eq(default_count)

View File

@ -7,6 +7,14 @@ RSpec.describe Gitlab::UsageDataQueries do
allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(false)
end
describe '#add_metric' do
let(:metric) { 'CountBoardsMetric' }
it 'builds the query for given metric' do
expect(described_class.add_metric(metric)).to eq('SELECT COUNT("boards"."id") FROM "boards"')
end
end
describe '.count' do
it 'returns the raw SQL' do
expect(described_class.count(User)).to start_with('SELECT COUNT("users"."id") FROM "users"')

View File

@ -568,7 +568,6 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
expect(count_data[:projects_custom_issue_tracker_active]).to eq(1)
expect(count_data[:projects_mattermost_active]).to eq(1)
expect(count_data[:groups_mattermost_active]).to eq(1)
expect(count_data[:templates_mattermost_active]).to eq(1)
expect(count_data[:instances_mattermost_active]).to eq(1)
expect(count_data[:projects_inheriting_mattermost_active]).to eq(1)
expect(count_data[:groups_inheriting_slack_active]).to eq(1)

View File

@ -5,6 +5,14 @@ require 'spec_helper'
RSpec.describe Gitlab::Utils::UsageData do
include Database::DatabaseHelpers
describe '#add_metric' do
let(:metric) { 'UuidMetric'}
it 'computes the metric value for given metric' do
expect(described_class.add_metric(metric)).to eq(Gitlab::CurrentSettings.uuid)
end
end
describe '#count' do
let(:relation) { double(:relation) }

View File

@ -105,6 +105,50 @@ RSpec.describe ApplicationRecord do
end
end
describe '.transaction', :delete do
it 'opens a new transaction' do
expect(described_class.connection.transaction_open?).to be false
Project.transaction do
expect(Project.connection.transaction_open?).to be true
Project.transaction(requires_new: true) do
expect(Project.connection.transaction_open?).to be true
end
end
end
it 'does not increment a counter when a transaction is not nested' do
expect(described_class.connection.transaction_open?).to be false
expect(::Gitlab::Database::Metrics)
.not_to receive(:subtransactions_increment)
Project.transaction do
expect(Project.connection.transaction_open?).to be true
end
Project.transaction(requires_new: true) do
expect(Project.connection.transaction_open?).to be true
end
end
it 'increments a counter when a nested transaction is created' do
expect(described_class.connection.transaction_open?).to be false
expect(::Gitlab::Database::Metrics)
.to receive(:subtransactions_increment)
.with('Project')
.once
Project.transaction do
Project.transaction(requires_new: true) do
expect(Project.connection.transaction_open?).to be true
end
end
end
end
describe '.with_fast_read_statement_timeout' do
context 'when the query runs faster than configured timeout' do
it 'executes the query without error' do

View File

@ -21,38 +21,31 @@ RSpec.describe Integration do
it { is_expected.to validate_presence_of(:type) }
it { is_expected.to validate_exclusion_of(:type).in_array(described_class::BASE_CLASSES) }
where(:project_id, :group_id, :template, :instance, :valid) do
1 | nil | false | false | true
nil | 1 | false | false | true
nil | nil | true | false | true
nil | nil | false | true | true
nil | nil | false | false | false
nil | nil | true | true | false
1 | 1 | false | false | false
1 | nil | true | false | false
1 | nil | false | true | false
nil | 1 | true | false | false
nil | 1 | false | true | false
where(:project_id, :group_id, :instance, :valid) do
1 | nil | false | true
nil | 1 | false | true
nil | nil | true | true
nil | nil | false | false
1 | 1 | false | false
1 | nil | false | true
1 | nil | true | false
nil | 1 | false | true
nil | 1 | true | false
end
with_them do
it 'validates the service' do
expect(build(:service, project_id: project_id, group_id: group_id, template: template, instance: instance).valid?).to eq(valid)
expect(build(:service, project_id: project_id, group_id: group_id, instance: instance).valid?).to eq(valid)
end
end
context 'with existing services' do
before_all do
create(:service, :template)
create(:service, :instance)
create(:service, project: project)
create(:service, group: group, project: nil)
end
it 'allows only one service template per type' do
expect(build(:service, :template)).to be_invalid
end
it 'allows only one instance service per type' do
expect(build(:service, :instance)).to be_invalid
end
@ -263,192 +256,108 @@ RSpec.describe Integration do
end
end
describe 'template' do
shared_examples 'retrieves service templates' do
it 'returns the available service templates' do
expect(Integration.find_or_create_templates.pluck(:type)).to match_array(Integration.available_integration_types(include_project_specific: false))
describe '.build_from_integration' do
context 'when integration is invalid' do
let(:invalid_integration) do
build(:prometheus_integration, :template, active: true, properties: {})
.tap { |integration| integration.save!(validate: false) }
end
it 'sets integration to inactive' do
integration = described_class.build_from_integration(invalid_integration, project_id: project.id)
expect(integration).to be_valid
expect(integration.active).to be false
end
end
describe '.find_or_create_templates' do
it 'creates service templates' do
total = Integration.available_integration_names(include_project_specific: false).size
context 'when integration is an instance-level integration' do
let(:instance_integration) { create(:jira_integration, :instance) }
expect { Integration.find_or_create_templates }.to change(Integration, :count).from(0).to(total)
end
it 'sets inherit_from_id from integration' do
integration = described_class.build_from_integration(instance_integration, project_id: project.id)
it_behaves_like 'retrieves service templates'
context 'with all existing templates' do
before do
Integration.insert_all(
Integration.available_integration_types(include_project_specific: false).map { |type| { template: true, type: type } }
)
end
it 'does not create service templates' do
expect { Integration.find_or_create_templates }.not_to change { Integration.count }
end
it_behaves_like 'retrieves service templates'
context 'with a previous existing service (Previous) and a new service (Asana)' do
before do
Integration.insert({ type: 'PreviousService', template: true })
Integration.delete_by(type: 'AsanaService', template: true)
end
it_behaves_like 'retrieves service templates'
end
end
context 'with a few existing templates' do
before do
create(:jira_integration, :template)
end
it 'creates the rest of the service templates' do
total = Integration.available_integration_names(include_project_specific: false).size
expect { Integration.find_or_create_templates }.to change(Integration, :count).from(1).to(total)
end
it_behaves_like 'retrieves service templates'
expect(integration.inherit_from_id).to eq(instance_integration.id)
end
end
describe '.build_from_integration' do
context 'when integration is invalid' do
let(:template_integration) do
build(:prometheus_integration, :template, active: true, properties: {})
.tap { |integration| integration.save!(validate: false) }
end
context 'when integration is a group-level integration' do
let(:group_integration) { create(:jira_integration, group: group, project: nil) }
it 'sets integration to inactive' do
integration = described_class.build_from_integration(template_integration, project_id: project.id)
it 'sets inherit_from_id from integration' do
integration = described_class.build_from_integration(group_integration, project_id: project.id)
expect(integration).to be_valid
expect(integration.active).to be false
end
end
context 'when integration is an instance-level integration' do
let(:instance_integration) { create(:jira_integration, :instance) }
it 'sets inherit_from_id from integration' do
integration = described_class.build_from_integration(instance_integration, project_id: project.id)
expect(integration.inherit_from_id).to eq(instance_integration.id)
end
end
context 'when integration is a group-level integration' do
let(:group_integration) { create(:jira_integration, group: group, project: nil) }
it 'sets inherit_from_id from integration' do
integration = described_class.build_from_integration(group_integration, project_id: project.id)
expect(integration.inherit_from_id).to eq(group_integration.id)
end
end
describe 'build issue tracker from an integration' do
let(:url) { 'http://jira.example.com' }
let(:api_url) { 'http://api-jira.example.com' }
let(:username) { 'jira-username' }
let(:password) { 'jira-password' }
let(:data_params) do
{
url: url, api_url: api_url,
username: username, password: password
}
end
shared_examples 'service creation from an integration' do
it 'creates a correct service for a project integration' do
service = described_class.build_from_integration(integration, project_id: project.id)
expect(service).to be_active
expect(service.url).to eq(url)
expect(service.api_url).to eq(api_url)
expect(service.username).to eq(username)
expect(service.password).to eq(password)
expect(service.template).to eq(false)
expect(service.instance).to eq(false)
expect(service.project).to eq(project)
expect(service.group).to eq(nil)
end
it 'creates a correct service for a group integration' do
service = described_class.build_from_integration(integration, group_id: group.id)
expect(service).to be_active
expect(service.url).to eq(url)
expect(service.api_url).to eq(api_url)
expect(service.username).to eq(username)
expect(service.password).to eq(password)
expect(service.template).to eq(false)
expect(service.instance).to eq(false)
expect(service.project).to eq(nil)
expect(service.group).to eq(group)
end
end
# this will be removed as part of https://gitlab.com/gitlab-org/gitlab/issues/29404
context 'when data are stored in properties' do
let(:properties) { data_params }
let!(:integration) do
create(:jira_integration, :without_properties_callback, template: true, properties: properties.merge(additional: 'something'))
end
it_behaves_like 'service creation from an integration'
end
context 'when data are stored in separated fields' do
let(:integration) do
create(:jira_integration, :template, data_params.merge(properties: {}))
end
it_behaves_like 'service creation from an integration'
end
context 'when data are stored in both properties and separated fields' do
let(:properties) { data_params }
let(:integration) do
create(:jira_integration, :without_properties_callback, active: true, template: true, properties: properties).tap do |integration|
create(:jira_tracker_data, data_params.merge(integration: integration))
end
end
it_behaves_like 'service creation from an integration'
end
expect(integration.inherit_from_id).to eq(group_integration.id)
end
end
describe "for pushover service" do
let!(:service_template) do
Integrations::Pushover.create!(
template: true,
properties: {
device: 'MyDevice',
sound: 'mic',
priority: 4,
api_key: '123456789'
})
describe 'build issue tracker from an integration' do
let(:url) { 'http://jira.example.com' }
let(:api_url) { 'http://api-jira.example.com' }
let(:username) { 'jira-username' }
let(:password) { 'jira-password' }
let(:data_params) do
{
url: url, api_url: api_url,
username: username, password: password
}
end
describe 'is prefilled for projects pushover service' do
it "has all fields prefilled" do
integration = project.find_or_initialize_integration('pushover')
shared_examples 'service creation from an integration' do
it 'creates a correct service for a project integration' do
service = described_class.build_from_integration(integration, project_id: project.id)
expect(integration).to have_attributes(
template: eq(false),
device: eq('MyDevice'),
sound: eq('mic'),
priority: eq(4),
api_key: eq('123456789')
)
expect(service).to be_active
expect(service.url).to eq(url)
expect(service.api_url).to eq(api_url)
expect(service.username).to eq(username)
expect(service.password).to eq(password)
expect(service.instance).to eq(false)
expect(service.project).to eq(project)
expect(service.group).to eq(nil)
end
it 'creates a correct service for a group integration' do
service = described_class.build_from_integration(integration, group_id: group.id)
expect(service).to be_active
expect(service.url).to eq(url)
expect(service.api_url).to eq(api_url)
expect(service.username).to eq(username)
expect(service.password).to eq(password)
expect(service.instance).to eq(false)
expect(service.project).to eq(nil)
expect(service.group).to eq(group)
end
end
# this will be removed as part of https://gitlab.com/gitlab-org/gitlab/issues/29404
context 'when data is stored in properties' do
let(:properties) { data_params }
let!(:integration) do
create(:jira_integration, :without_properties_callback, properties: properties.merge(additional: 'something'))
end
it_behaves_like 'service creation from an integration'
end
context 'when data are stored in separated fields' do
let(:integration) do
create(:jira_integration, data_params.merge(properties: {}))
end
it_behaves_like 'service creation from an integration'
end
context 'when data are stored in both properties and separated fields' do
let(:properties) { data_params }
let(:integration) do
create(:jira_integration, :without_properties_callback, active: true, properties: properties).tap do |integration|
create(:jira_tracker_data, data_params.merge(integration: integration))
end
end
it_behaves_like 'service creation from an integration'
end
end
end
@ -510,121 +419,109 @@ RSpec.describe Integration do
end
describe '.create_from_active_default_integrations' do
context 'with an active integration template' do
let_it_be(:template_integration) { create(:prometheus_integration, :template, api_url: 'https://prometheus.template.com/') }
context 'with an active instance-level integration' do
let!(:instance_integration) { create(:prometheus_integration, :instance, api_url: 'https://prometheus.instance.com/') }
it 'creates an integration from the template' do
described_class.create_from_active_default_integrations(project, :project_id, with_templates: true)
it 'creates an integration from the instance-level integration' do
described_class.create_from_active_default_integrations(project, :project_id)
expect(project.reload.integrations.size).to eq(1)
expect(project.reload.integrations.first.api_url).to eq(template_integration.api_url)
expect(project.reload.integrations.first.inherit_from_id).to be_nil
expect(project.reload.integrations.first.api_url).to eq(instance_integration.api_url)
expect(project.reload.integrations.first.inherit_from_id).to eq(instance_integration.id)
end
context 'with an active instance-level integration' do
let!(:instance_integration) { create(:prometheus_integration, :instance, api_url: 'https://prometheus.instance.com/') }
context 'passing a group' do
it 'creates an integration from the instance-level integration' do
described_class.create_from_active_default_integrations(project, :project_id, with_templates: true)
described_class.create_from_active_default_integrations(group, :group_id)
expect(group.reload.integrations.size).to eq(1)
expect(group.reload.integrations.first.api_url).to eq(instance_integration.api_url)
expect(group.reload.integrations.first.inherit_from_id).to eq(instance_integration.id)
end
end
context 'with an active group-level integration' do
let!(:group_integration) { create(:prometheus_integration, group: group, project: nil, api_url: 'https://prometheus.group.com/') }
it 'creates an integration from the group-level integration' do
described_class.create_from_active_default_integrations(project, :project_id)
expect(project.reload.integrations.size).to eq(1)
expect(project.reload.integrations.first.api_url).to eq(instance_integration.api_url)
expect(project.reload.integrations.first.inherit_from_id).to eq(instance_integration.id)
expect(project.reload.integrations.first.api_url).to eq(group_integration.api_url)
expect(project.reload.integrations.first.inherit_from_id).to eq(group_integration.id)
end
context 'passing a group' do
it 'creates an integration from the instance-level integration' do
described_class.create_from_active_default_integrations(group, :group_id)
let!(:subgroup) { create(:group, parent: group) }
expect(group.reload.integrations.size).to eq(1)
expect(group.reload.integrations.first.api_url).to eq(instance_integration.api_url)
expect(group.reload.integrations.first.inherit_from_id).to eq(instance_integration.id)
it 'creates an integration from the group-level integration' do
described_class.create_from_active_default_integrations(subgroup, :group_id)
expect(subgroup.reload.integrations.size).to eq(1)
expect(subgroup.reload.integrations.first.api_url).to eq(group_integration.api_url)
expect(subgroup.reload.integrations.first.inherit_from_id).to eq(group_integration.id)
end
end
context 'with an active group-level integration' do
let!(:group_integration) { create(:prometheus_integration, group: group, project: nil, api_url: 'https://prometheus.group.com/') }
context 'with an active subgroup' do
let!(:subgroup_integration) { create(:prometheus_integration, group: subgroup, project: nil, api_url: 'https://prometheus.subgroup.com/') }
let!(:subgroup) { create(:group, parent: group) }
let(:project) { create(:project, group: subgroup) }
it 'creates an integration from the group-level integration' do
described_class.create_from_active_default_integrations(project, :project_id, with_templates: true)
it 'creates an integration from the subgroup-level integration' do
described_class.create_from_active_default_integrations(project, :project_id)
expect(project.reload.integrations.size).to eq(1)
expect(project.reload.integrations.first.api_url).to eq(group_integration.api_url)
expect(project.reload.integrations.first.inherit_from_id).to eq(group_integration.id)
expect(project.reload.integrations.first.api_url).to eq(subgroup_integration.api_url)
expect(project.reload.integrations.first.inherit_from_id).to eq(subgroup_integration.id)
end
context 'passing a group' do
let!(:subgroup) { create(:group, parent: group) }
let!(:sub_subgroup) { create(:group, parent: subgroup) }
it 'creates an integration from the group-level integration' do
described_class.create_from_active_default_integrations(subgroup, :group_id)
context 'traversal queries' do
shared_examples 'correct ancestor order' do
it 'creates an integration from the subgroup-level integration' do
described_class.create_from_active_default_integrations(sub_subgroup, :group_id)
expect(subgroup.reload.integrations.size).to eq(1)
expect(subgroup.reload.integrations.first.api_url).to eq(group_integration.api_url)
expect(subgroup.reload.integrations.first.inherit_from_id).to eq(group_integration.id)
end
end
sub_subgroup.reload
context 'with an active subgroup' do
let!(:subgroup_integration) { create(:prometheus_integration, group: subgroup, project: nil, api_url: 'https://prometheus.subgroup.com/') }
let!(:subgroup) { create(:group, parent: group) }
let(:project) { create(:project, group: subgroup) }
expect(sub_subgroup.integrations.size).to eq(1)
expect(sub_subgroup.integrations.first.api_url).to eq(subgroup_integration.api_url)
expect(sub_subgroup.integrations.first.inherit_from_id).to eq(subgroup_integration.id)
end
it 'creates an integration from the subgroup-level integration' do
described_class.create_from_active_default_integrations(project, :project_id, with_templates: true)
context 'having an integration inheriting settings' do
let!(:subgroup_integration) { create(:prometheus_integration, group: subgroup, project: nil, inherit_from_id: group_integration.id, api_url: 'https://prometheus.subgroup.com/') }
expect(project.reload.integrations.size).to eq(1)
expect(project.reload.integrations.first.api_url).to eq(subgroup_integration.api_url)
expect(project.reload.integrations.first.inherit_from_id).to eq(subgroup_integration.id)
end
context 'passing a group' do
let!(:sub_subgroup) { create(:group, parent: subgroup) }
context 'traversal queries' do
shared_examples 'correct ancestor order' do
it 'creates an integration from the subgroup-level integration' do
it 'creates an integration from the group-level integration' do
described_class.create_from_active_default_integrations(sub_subgroup, :group_id)
sub_subgroup.reload
expect(sub_subgroup.integrations.size).to eq(1)
expect(sub_subgroup.integrations.first.api_url).to eq(subgroup_integration.api_url)
expect(sub_subgroup.integrations.first.inherit_from_id).to eq(subgroup_integration.id)
end
context 'having an integration inheriting settings' do
let!(:subgroup_integration) { create(:prometheus_integration, group: subgroup, project: nil, inherit_from_id: group_integration.id, api_url: 'https://prometheus.subgroup.com/') }
it 'creates an integration from the group-level integration' do
described_class.create_from_active_default_integrations(sub_subgroup, :group_id)
sub_subgroup.reload
expect(sub_subgroup.integrations.size).to eq(1)
expect(sub_subgroup.integrations.first.api_url).to eq(group_integration.api_url)
expect(sub_subgroup.integrations.first.inherit_from_id).to eq(group_integration.id)
end
expect(sub_subgroup.integrations.first.api_url).to eq(group_integration.api_url)
expect(sub_subgroup.integrations.first.inherit_from_id).to eq(group_integration.id)
end
end
end
context 'recursive' do
before do
stub_feature_flags(use_traversal_ids: false)
end
include_examples 'correct ancestor order'
context 'recursive' do
before do
stub_feature_flags(use_traversal_ids: false)
end
context 'linear' do
before do
stub_feature_flags(use_traversal_ids: true)
include_examples 'correct ancestor order'
end
sub_subgroup.reload # make sure traversal_ids are reloaded
end
context 'linear' do
before do
stub_feature_flags(use_traversal_ids: true)
include_examples 'correct ancestor order'
sub_subgroup.reload # make sure traversal_ids are reloaded
end
include_examples 'correct ancestor order'
end
end
end

View File

@ -538,6 +538,15 @@ RSpec.describe Milestone do
it { is_expected.to match('gitlab-org/gitlab-ce%123') }
it { is_expected.to match('gitlab-org/gitlab-ce%"my-milestone"') }
context 'when milestone_reference_pattern feature flag is false' do
before do
stub_feature_flags(milestone_reference_pattern: false)
end
it { is_expected.to match('gitlab-org/gitlab-ce%123') }
it { is_expected.to match('gitlab-org/gitlab-ce%"my-milestone"') }
end
end
describe '.link_reference_pattern' do

View File

@ -5911,10 +5911,9 @@ RSpec.describe Project, factory_default: :keep do
end
end
context 'with an instance-level and template integrations' do
context 'with an instance-level integration' do
before do
create(:prometheus_integration, :instance, api_url: 'https://prometheus.instance.com/')
create(:prometheus_integration, :template, api_url: 'https://prometheus.template.com/')
end
it 'builds the integration from the instance integration' do
@ -5922,17 +5921,7 @@ RSpec.describe Project, factory_default: :keep do
end
end
context 'with a template integration and no instance-level' do
before do
create(:prometheus_integration, :template, api_url: 'https://prometheus.template.com/')
end
it 'builds the integration from the template' do
expect(subject.find_or_initialize_integration('prometheus').api_url).to eq('https://prometheus.template.com/')
end
end
context 'without an exisiting integration, or instance-level or template' do
context 'without an existing integration or instance-level' do
it 'builds the integration' do
expect(subject.find_or_initialize_integration('prometheus')).to be_a(::Integrations::Prometheus)
expect(subject.find_or_initialize_integration('prometheus').api_url).to be_nil

View File

@ -1,60 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Admin::PropagateServiceTemplate do
describe '.propagate' do
let_it_be(:project) { create(:project) }
let!(:service_template) do
Integrations::Pushover.create!(
template: true,
active: true,
push_events: false,
properties: {
device: 'MyDevice',
sound: 'mic',
priority: 4,
user_key: 'asdf',
api_key: '123456789'
}
)
end
it 'calls to PropagateIntegrationProjectWorker' do
expect(PropagateIntegrationProjectWorker).to receive(:perform_async)
.with(service_template.id, project.id, project.id)
described_class.propagate(service_template)
end
context 'with a project that has another service' do
before do
Integrations::Bamboo.create!(
active: true,
project: project,
properties: {
bamboo_url: 'http://gitlab.com',
username: 'mic',
password: 'password',
build_key: 'build'
}
)
end
it 'calls to PropagateIntegrationProjectWorker' do
expect(PropagateIntegrationProjectWorker).to receive(:perform_async)
.with(service_template.id, project.id, project.id)
described_class.propagate(service_template)
end
end
it 'does not create the service if it exists already' do
Integration.build_from_integration(service_template, project_id: project.id).save!
expect { described_class.propagate(service_template) }
.not_to change { Integration.count }
end
end
end

View File

@ -96,18 +96,4 @@ RSpec.describe BulkCreateIntegrationService do
it_behaves_like 'updates inherit_from_id'
end
end
context 'passing a template integration' do
let(:integration) { template_integration }
context 'with a project association' do
let!(:project) { create(:project) }
let(:created_integration) { project.jira_integration }
let(:batch) { Project.where(id: project.id) }
let(:association) { 'project' }
let(:inherit_from_id) { integration.id }
it_behaves_like 'creates integration from batch ids'
end
end
end

View File

@ -607,65 +607,55 @@ RSpec.describe Projects::CreateService, '#execute' do
describe 'create integration for the project' do
subject(:project) { create_project(user, opts) }
context 'with an active integration template' do
let!(:template_integration) { create(:prometheus_integration, :template, api_url: 'https://prometheus.template.com/') }
context 'with an active instance-level integration' do
let!(:instance_integration) { create(:prometheus_integration, :instance, api_url: 'https://prometheus.instance.com/') }
it 'creates an integration from the template' do
it 'creates an integration from the instance-level integration' do
expect(project.integrations.count).to eq(1)
expect(project.integrations.first.api_url).to eq(template_integration.api_url)
expect(project.integrations.first.inherit_from_id).to be_nil
expect(project.integrations.first.api_url).to eq(instance_integration.api_url)
expect(project.integrations.first.inherit_from_id).to eq(instance_integration.id)
end
context 'with an active instance-level integration' do
let!(:instance_integration) { create(:prometheus_integration, :instance, api_url: 'https://prometheus.instance.com/') }
it 'creates an integration from the instance-level integration' do
expect(project.integrations.count).to eq(1)
expect(project.integrations.first.api_url).to eq(instance_integration.api_url)
expect(project.integrations.first.inherit_from_id).to eq(instance_integration.id)
context 'with an active group-level integration' do
let!(:group_integration) { create(:prometheus_integration, group: group, project: nil, api_url: 'https://prometheus.group.com/') }
let!(:group) do
create(:group).tap do |group|
group.add_owner(user)
end
end
context 'with an active group-level integration' do
let!(:group_integration) { create(:prometheus_integration, group: group, project: nil, api_url: 'https://prometheus.group.com/') }
let!(:group) do
create(:group).tap do |group|
group.add_owner(user)
let(:opts) do
{
name: 'GitLab',
namespace_id: group.id
}
end
it 'creates an integration from the group-level integration' do
expect(project.integrations.count).to eq(1)
expect(project.integrations.first.api_url).to eq(group_integration.api_url)
expect(project.integrations.first.inherit_from_id).to eq(group_integration.id)
end
context 'with an active subgroup' do
let!(:subgroup_integration) { create(:prometheus_integration, group: subgroup, project: nil, api_url: 'https://prometheus.subgroup.com/') }
let!(:subgroup) do
create(:group, parent: group).tap do |subgroup|
subgroup.add_owner(user)
end
end
let(:opts) do
{
name: 'GitLab',
namespace_id: group.id
namespace_id: subgroup.id
}
end
it 'creates an integration from the group-level integration' do
it 'creates an integration from the subgroup-level integration' do
expect(project.integrations.count).to eq(1)
expect(project.integrations.first.api_url).to eq(group_integration.api_url)
expect(project.integrations.first.inherit_from_id).to eq(group_integration.id)
end
context 'with an active subgroup' do
let!(:subgroup_integration) { create(:prometheus_integration, group: subgroup, project: nil, api_url: 'https://prometheus.subgroup.com/') }
let!(:subgroup) do
create(:group, parent: group).tap do |subgroup|
subgroup.add_owner(user)
end
end
let(:opts) do
{
name: 'GitLab',
namespace_id: subgroup.id
}
end
it 'creates an integration from the subgroup-level integration' do
expect(project.integrations.count).to eq(1)
expect(project.integrations.first.api_url).to eq(subgroup_integration.api_url)
expect(project.integrations.first.inherit_from_id).to eq(subgroup_integration.id)
end
expect(project.integrations.first.api_url).to eq(subgroup_integration.api_url)
expect(project.integrations.first.inherit_from_id).to eq(subgroup_integration.id)
end
end
end

View File

@ -100,8 +100,8 @@ RSpec.shared_examples 'issues move service' do |group|
create(:labeled_issue, project: project, labels: [bug, development], assignees: [assignee])
end
it 'returns false' do
expect(described_class.new(parent, user, params).execute(issue)).to eq false
it 'returns nil' do
expect(described_class.new(parent, user, params).execute(issue)).to be_nil
end
it 'keeps issues labels' do

View File

@ -201,9 +201,15 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout do
let(:reindex) { double('reindex') }
let(:indexes) { double('indexes') }
it 'cleans up any leftover indexes' do
expect(Gitlab::Database::Reindexing).to receive(:cleanup_leftovers!)
run_rake_task('gitlab:db:reindex')
end
context 'when no index_name is given' do
it 'uses all candidate indexes' do
expect(Gitlab::Database::Reindexing).to receive(:candidate_indexes).and_return(indexes)
expect(Gitlab::Database::PostgresIndex).to receive(:reindexing_support).and_return(indexes)
expect(Gitlab::Database::Reindexing).to receive(:perform).with(indexes)
run_rake_task('gitlab:db:reindex')
@ -214,7 +220,7 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout do
let(:index) { double('index') }
before do
allow(Gitlab::Database::Reindexing).to receive(:candidate_indexes).and_return(indexes)
allow(Gitlab::Database::PostgresIndex).to receive(:reindexing_support).and_return(indexes)
end
it 'calls the index rebuilder with the proper arguments' do

View File

@ -4,9 +4,10 @@ require 'spec_helper'
RSpec.describe PropagateIntegrationWorker do
describe '#perform' do
let(:project) { create(:project) }
let(:integration) do
Integrations::Pushover.create!(
template: true,
project: project,
active: true,
device: 'MyDevice',
sound: 'mic',

View File

@ -1,31 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe PropagateServiceTemplateWorker do
include ExclusiveLeaseHelpers
describe '#perform' do
it 'calls the propagate service with the template' do
template = Integrations::Pushover.create!(
template: true,
active: true,
properties: {
device: 'MyDevice',
sound: 'mic',
priority: 4,
user_key: 'asdf',
api_key: '123456789'
})
stub_exclusive_lease("propagate_service_template_worker:#{template.id}",
timeout: PropagateServiceTemplateWorker::LEASE_TIMEOUT)
expect(Admin::PropagateServiceTemplate)
.to receive(:propagate)
.with(template)
subject.perform(template.id)
end
end
end

View File

@ -1,5 +1,5 @@
PREFIX=/usr/local
PKG := gitlab.com/gitlab-org/gitlab-workhorse
PKG := gitlab.com/gitlab-org/gitlab/workhorse
BUILD_DIR ?= $(CURDIR)
TARGET_DIR ?= $(BUILD_DIR)/_build
TARGET_SETUP := $(TARGET_DIR)/.ok

View File

@ -6,7 +6,7 @@ if [ "x$1" = xcheck ]; then
fi
IMPORT_RESULT=$(
goimports $FLAG -local "gitlab.com/gitlab-org/gitlab-workhorse" -l $(
goimports $FLAG -local "gitlab.com/gitlab-org/gitlab/workhorse" -l $(
find . -type f -name '*.go' | grep -v -e /_ -e /testdata/ -e '^\./\.'
)
)

View File

@ -13,11 +13,11 @@ import (
"github.com/dgrijalva/jwt-go"
"github.com/stretchr/testify/require"
"gitlab.com/gitlab-org/gitlab-workhorse/internal/api"
"gitlab.com/gitlab-org/gitlab-workhorse/internal/helper"
"gitlab.com/gitlab-org/gitlab-workhorse/internal/secret"
"gitlab.com/gitlab-org/gitlab-workhorse/internal/testhelper"
"gitlab.com/gitlab-org/gitlab-workhorse/internal/upstream/roundtripper"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/api"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/helper"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/secret"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/testhelper"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/upstream/roundtripper"
)
func okHandler(w http.ResponseWriter, _ *http.Request, _ *api.Response) {

Some files were not shown because too many files have changed in this diff Show More