Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
7e5b78ee03
commit
a8f5aaa708
|
@ -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: []
|
||||
|
|
|
@ -1 +1 @@
|
|||
499b72a41063d61dbb8a73ed7ffa7aa42f1584fd
|
||||
996a4adda765e8ced18c72eca0ebd27848afa3c9
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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)">
|
||||
{{ 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>
|
|
@ -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';
|
|
@ -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,
|
||||
},
|
||||
),
|
||||
});
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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',
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
|
@ -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',
|
||||
];
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)}
|
||||
(?:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 = {}
|
||||
|
||||
|
|
|
@ -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?
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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 > 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 }
|
|
@ -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'
|
|
@ -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
|
|
@ -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"
|
||||
|
|
|
@ -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 > 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')
|
|
@ -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) }
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
a97ac46a042b7f049f27db4f4916b8b0dbf527ba3c34fc9cc577da7807a88d32
|
|
@ -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,
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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. |
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
@ -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:
|
||||
|
|
|
@ -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)**
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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) }
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -15,6 +15,10 @@ module Gitlab
|
|||
@time_frame = time_frame
|
||||
@options = options
|
||||
end
|
||||
|
||||
def instrumentation
|
||||
value
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,7 +5,7 @@ module Gitlab
|
|||
module Metrics
|
||||
module Instrumentations
|
||||
class CollectedDataCategoriesMetric < GenericMetric
|
||||
def value
|
||||
value do
|
||||
::ServicePing::PermitDataCategoriesService.new.execute
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 > 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 > 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 ""
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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>
|
||||
`;
|
|
@ -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,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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
|
||||
|
|
|
@ -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? }
|
||||
|
||||
|
|
|
@ -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}"
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"')
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) }
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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 '^\./\.'
|
||||
)
|
||||
)
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue