Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-08-22 18:10:26 +00:00
parent adaa4599f1
commit a3ffaa242b
88 changed files with 2124 additions and 382 deletions

View File

@ -569,9 +569,6 @@ RSpec/ImplicitSubject:
RSpec/ReceiveNever:
Enabled: false
RSpec/MissingExampleGroupArgument:
Enabled: false
RSpec/UnspecifiedException:
Enabled: false

View File

@ -0,0 +1,16 @@
---
RSpec/MissingExampleGroupArgument:
Exclude:
- 'ee/spec/controllers/groups/audit_events_controller_spec.rb'
- 'ee/spec/services/ee/notification_service_spec.rb'
- 'ee/spec/support/shared_examples/controllers/concerns/description_diff_actions_shared_examples.rb'
- 'spec/controllers/projects/issues_controller_spec.rb'
- 'spec/controllers/projects/merge_requests_controller_spec.rb'
- 'spec/factories/projects/ci_feature_usages.rb'
- 'spec/lib/gitlab/background_migration/backfill_jira_tracker_deployment_type2_spec.rb'
- 'spec/lib/gitlab/git_access_spec.rb'
- 'spec/policies/award_emoji_policy_spec.rb'
- 'spec/requests/api/graphql/mutations/notes/create/diff_note_spec.rb'
- 'spec/requests/api/graphql/mutations/notes/create/image_diff_note_spec.rb'
- 'spec/services/notification_service_spec.rb'
- 'spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb'

View File

@ -66,7 +66,7 @@ export default {
<div v-if="loading && !error" class="text-center loading">
<gl-loading-icon class="mt-5" size="lg" />
</div>
<notebook-lab v-if="!loading && !error" :notebook="json" code-css-class="code white" />
<notebook-lab v-if="!loading && !error" :notebook="json" />
<p v-if="error" class="text-center">
<span v-if="loadError" ref="loadErrorMessage">{{
__('An error occurred while loading the file. Please try again later.')

View File

@ -1,11 +0,0 @@
import Vue from 'vue';
import Panel from './panel.vue';
export default (containerId = '#js-google-cloud-databases') => {
const element = document.querySelector(containerId);
const { ...attrs } = JSON.parse(element.getAttribute('data'));
return new Vue({
el: element,
render: (createElement) => createElement(Panel, { attrs }),
});
};

View File

@ -0,0 +1,11 @@
import Vue from 'vue';
import Panel from './panel.vue';
export default () => {
const element = document.querySelector('#js-google-cloud-databases');
const attrs = JSON.parse(element.getAttribute('data'));
return new Vue({
el: element,
render: (createElement) => createElement(Panel, { attrs }),
});
};

View File

@ -0,0 +1,11 @@
import Vue from 'vue';
import Form from './cloudsql/create_instance_form.vue';
export default () => {
const element = document.querySelector('#js-google-cloud-databases-cloudsql-form');
const attrs = JSON.parse(element.getAttribute('data'));
return new Vue({
el: element,
render: (createElement) => createElement(Form, { attrs }),
});
};

View File

@ -1,11 +1,15 @@
<script>
import GoogleCloudMenu from '../components/google_cloud_menu.vue';
import IncubationBanner from '../components/incubation_banner.vue';
import InstanceTable from './cloudsql/instance_table.vue';
import ServiceTable from './service_table.vue';
export default {
components: {
IncubationBanner,
InstanceTable,
GoogleCloudMenu,
ServiceTable,
},
props: {
configurationUrl: {
@ -20,6 +24,26 @@ export default {
type: String,
required: true,
},
cloudsqlPostgresUrl: {
type: String,
required: true,
},
cloudsqlMysqlUrl: {
type: String,
required: true,
},
cloudsqlSqlserverUrl: {
type: String,
required: true,
},
cloudsqlInstances: {
type: Array,
required: true,
},
emptyIllustrationUrl: {
type: String,
required: true,
},
},
};
</script>
@ -34,5 +58,19 @@ export default {
:deployments-url="deploymentsUrl"
:databases-url="databasesUrl"
/>
<service-table
alloydb-postgres-url="#"
:cloudsql-mysql-url="cloudsqlMysqlUrl"
:cloudsql-postgres-url="cloudsqlPostgresUrl"
:cloudsql-sqlserver-url="cloudsqlSqlserverUrl"
firestore-url="#"
memorystore-redis-url="#"
/>
<instance-table
:cloudsql-instances="cloudsqlInstances"
:empty-illustration-url="emptyIllustrationUrl"
/>
</div>
</template>

View File

@ -13,11 +13,6 @@ export default {
type: Object,
required: true,
},
codeCssClass: {
type: String,
required: false,
default: '',
},
},
computed: {
rawInputCode() {
@ -39,18 +34,12 @@ export default {
<template>
<div class="cell">
<code-output
:raw-code="rawInputCode"
:count="cell.execution_count"
:code-css-class="codeCssClass"
type="input"
/>
<code-output :raw-code="rawInputCode" :count="cell.execution_count" type="input" />
<output-cell
v-if="hasOutput"
:count="cell.execution_count"
:outputs="outputs"
:metadata="cell.metadata"
:code-css-class="codeCssClass"
/>
</div>
</template>

View File

@ -1,10 +1,11 @@
<script>
import Prism from '../../lib/highlight';
import CodeBlockHighlighted from '~/vue_shared/components/code_block_highlighted.vue';
import Prompt from '../prompt.vue';
export default {
name: 'CodeOutput',
components: {
CodeBlockHighlighted,
Prompt,
},
props: {
@ -13,11 +14,6 @@ export default {
required: false,
default: 0,
},
codeCssClass: {
type: String,
required: false,
default: '',
},
type: {
type: String,
required: true,
@ -41,22 +37,21 @@ export default {
return type.charAt(0).toUpperCase() + type.slice(1);
},
cellCssClass() {
return {
[this.codeCssClass]: true,
'jupyter-notebook-scrolled': this.metadata.scrolled,
};
maxHeight() {
return this.metadata.scrolled ? '20rem' : 'initial';
},
},
mounted() {
Prism.highlightElement(this.$refs.code);
},
};
</script>
<template>
<div :class="type">
<prompt :type="promptType" :count="count" />
<pre ref="code" :class="cellCssClass" class="language-python" v-text="code"></pre>
<code-block-highlighted
language="python"
:code="code"
:max-height="maxHeight"
class="gl-border"
/>
</div>
</template>

View File

@ -6,11 +6,6 @@ import LatexOutput from './latex.vue';
export default {
props: {
codeCssClass: {
type: String,
required: false,
default: '',
},
count: {
type: Number,
required: false,
@ -96,7 +91,6 @@ export default {
:index="index"
:raw-code="rawCode(output)"
:metadata="metadata"
:code-css-class="codeCssClass"
/>
</div>
</template>

View File

@ -11,11 +11,6 @@ export default {
type: Object,
required: true,
},
codeCssClass: {
type: String,
required: false,
default: '',
},
},
computed: {
cells() {
@ -52,7 +47,6 @@ export default {
v-for="(cell, index) in cells"
:key="index"
:cell="cell"
:code-css-class="codeCssClass"
/>
</div>
</template>

View File

@ -1,5 +0,0 @@
import Prism from 'prismjs';
import 'prismjs/components/prism-python';
import 'prismjs/themes/prism.css';
export default Prism;

View File

@ -1,3 +0,0 @@
import init from '~/google_cloud/databases/index';
init();

View File

@ -0,0 +1,3 @@
import init from '~/google_cloud/databases/init_index';
init();

View File

@ -0,0 +1,3 @@
import init from '~/google_cloud/databases/init_new';
init();

View File

@ -1,12 +1,18 @@
<script>
import { GlAlert, GlSafeHtmlDirective } from '@gitlab/ui';
import { GlAlert, GlSprintf, GlLink, GlSafeHtmlDirective } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
import { helpPagePath } from '~/helpers/help_page_helper';
import { __, sprintf } from '~/locale';
import ServiceDeskSetting from './service_desk_setting.vue';
export default {
customEmailHelpPath: helpPagePath('/user/project/service_desk.html', {
anchor: 'using-a-custom-email-address',
}),
components: {
GlAlert,
GlSprintf,
GlLink,
ServiceDeskSetting,
},
directives: {
@ -43,6 +49,9 @@ export default {
templates: {
default: [],
},
publicProject: {
default: false,
},
},
data() {
return {
@ -127,6 +136,27 @@ export default {
<template>
<div>
<gl-alert
v-if="publicProject && isEnabled"
class="mb-3"
variant="warning"
data-testid="public-project-alert"
:dismissible="false"
>
<gl-sprintf
:message="
__(
'This project is public. Non-members can guess the Service Desk email address, because it contains the group and project name. %{linkStart}How do I create a custom email address?%{linkEnd}',
)
"
>
<template #link="{ content }">
<gl-link :href="$options.customEmailHelpPath" target="_blank">
{{ content }}
</gl-link>
</template>
</gl-sprintf>
</gl-alert>
<gl-alert v-if="isAlertShowing" class="mb-3" :variant="alertVariant" @dismiss="onDismiss">
<span v-safe-html="alertMessage"></span>
</gl-alert>

View File

@ -20,6 +20,7 @@ export default () => {
selectedTemplate,
selectedFileTemplateProjectId,
templates,
publicProject,
} = el.dataset;
return new Vue({
@ -35,6 +36,7 @@ export default () => {
selectedTemplate,
selectedFileTemplateProjectId: parseInt(selectedFileTemplateProjectId, 10) || null,
templates: JSON.parse(templates),
publicProject: parseBoolean(publicProject),
},
render: (createElement) => createElement(ServiceDeskRoot),
});

View File

@ -1,5 +1,5 @@
<script>
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import { GlButton, GlTooltipDirective, GlLoadingIcon } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import { normalizeHeaders } from '~/lib/utils/common_utils';
import { sprintf, __ } from '~/locale';
@ -8,11 +8,13 @@ import StatusIcon from '../extensions/status_icon.vue';
import { EXTENSION_ICONS } from '../../constants';
const FETCH_TYPE_COLLAPSED = 'collapsed';
const FETCH_TYPE_EXPANDED = 'expanded';
export default {
components: {
StatusIcon,
GlButton,
GlLoadingIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
@ -20,7 +22,7 @@ export default {
props: {
/**
* @param {value.collapsed} Object
* @param {value.extended} Object
* @param {value.expanded} Object
*/
value: {
type: Object,
@ -40,7 +42,7 @@ export default {
type: Function,
required: true,
},
fetchExtendedData: {
fetchExpandedData: {
type: Function,
required: false,
default: undefined,
@ -79,8 +81,10 @@ export default {
},
data() {
return {
isExpandedForTheFirstTime: true,
isCollapsed: true,
isLoading: false,
isLoadingExpandedContent: false,
error: null,
};
},
@ -111,6 +115,22 @@ export default {
methods: {
toggleCollapsed() {
this.isCollapsed = !this.isCollapsed;
if (this.isExpandedForTheFirstTime && typeof this.fetchExpandedData === 'function') {
this.isExpandedForTheFirstTime = false;
this.fetchExpandedContent();
}
},
async fetchExpandedContent() {
this.isLoadingExpandedContent = true;
try {
await this.fetch(this.fetchExpandedData, FETCH_TYPE_EXPANDED);
} catch {
this.error = this.errorText;
}
this.isLoadingExpandedContent = false;
},
fetch(handler, dataType) {
const requests = this.multiPolling ? handler() : [handler];
@ -161,7 +181,6 @@ export default {
<slot v-if="!error" name="summary">{{ isLoading ? loadingText : summary }}</slot>
<span v-else>{{ error }}</span>
</div>
<!-- actions will go here -->
<div
v-if="isCollapsible"
class="gl-border-l-1 gl-border-l-solid gl-border-gray-100 gl-ml-3 gl-pl-3 gl-h-6"
@ -185,7 +204,10 @@ export default {
class="mr-widget-grouped-section gl-relative"
data-testid="widget-extension-collapsed-section"
>
<slot name="content">{{ content }}</slot>
<div v-if="isLoadingExpandedContent" class="report-block-container gl-text-center">
<gl-loading-icon size="sm" inline /> {{ __('Loading...') }}
</div>
<slot v-else name="content">{{ content }}</slot>
</div>
</section>
</template>

View File

@ -0,0 +1,18 @@
import CodeBlock from './code_block.vue';
export default {
component: CodeBlock,
title: 'vue_shared/components/code_block',
};
const Template = (args, { argTypes }) => ({
components: { CodeBlock },
props: Object.keys(argTypes),
template: '<code-block v-bind="$props" />',
});
export const Default = Template.bind({});
Default.args = {
// eslint-disable-next-line @gitlab/require-i18n-strings
code: `git commit -a "Message"\ngit push`,
};

View File

@ -4,7 +4,8 @@ export default {
props: {
code: {
type: String,
required: true,
required: false,
default: '',
},
maxHeight: {
type: String,
@ -32,5 +33,5 @@ export default {
class="code-block rounded code"
:class="$options.userColorScheme"
:style="styleObject"
><code class="d-block">{{ code }}</code></pre>
><slot><code class="d-block">{{ code }}</code></slot></pre>
</template>

View File

@ -0,0 +1,18 @@
import CodeBlockHighlighted from './code_block_highlighted.vue';
export default {
component: CodeBlockHighlighted,
title: 'vue_shared/components/code_block_highlighted',
};
const Template = (args, { argTypes }) => ({
components: { CodeBlockHighlighted },
props: Object.keys(argTypes),
template: '<code-block-highlighted v-bind="$props" />',
});
export const Default = Template.bind({});
Default.args = {
code: `const foo = 1;\nconsole.log(foo + ' yay')`,
language: 'javascript',
};

View File

@ -0,0 +1,72 @@
<script>
import { GlSafeHtmlDirective } from '@gitlab/ui';
import languageLoader from '~/content_editor/services/highlight_js_language_loader';
import CodeBlock from './code_block.vue';
export default {
name: 'CodeBlockHighlighted',
directives: {
SafeHtml: GlSafeHtmlDirective,
},
components: {
CodeBlock,
},
props: {
code: {
type: String,
required: true,
},
language: {
type: String,
required: true,
},
maxHeight: {
type: String,
required: false,
default: 'initial',
},
},
data() {
return {
hljs: null,
languageLoaded: false,
};
},
computed: {
highlighted() {
if (this.hljs && this.languageLoaded) {
return this.hljs.highlight(this.code, { language: this.language }).value;
}
return this.code;
},
},
async mounted() {
this.hljs = await this.loadHighlightJS();
if (this.language) {
await this.loadLanguage();
}
},
methods: {
async loadLanguage() {
try {
const { default: languageDefinition } = await languageLoader[this.language]();
this.hljs.registerLanguage(this.language, languageDefinition);
this.languageLoaded = true;
} catch (e) {
this.$emit('error', e);
}
},
loadHighlightJS() {
return import('highlight.js/lib/core');
},
},
};
</script>
<template>
<code-block :max-height="maxHeight" class="highlight">
<span v-safe-html="highlighted"></span>
</code-block>
</template>

View File

@ -471,11 +471,6 @@ span.idiff {
}
}
.jupyter-notebook-scrolled {
overflow-y: auto;
max-height: 20rem;
}
#js-openapi-viewer {
pre.version,
code {

View File

@ -3,14 +3,138 @@
module Projects
module GoogleCloud
class DatabasesController < Projects::GoogleCloud::BaseController
before_action :validate_gcp_token!
before_action :validate_product, only: :new
def index
js_data = {
configurationUrl: project_google_cloud_configuration_path(project),
deploymentsUrl: project_google_cloud_deployments_path(project),
databasesUrl: project_google_cloud_databases_path(project)
databasesUrl: project_google_cloud_databases_path(project),
cloudsqlPostgresUrl: new_project_google_cloud_database_path(project, :postgres),
cloudsqlMysqlUrl: new_project_google_cloud_database_path(project, :mysql),
cloudsqlSqlserverUrl: new_project_google_cloud_database_path(project, :sqlserver),
cloudsqlInstances: ::GoogleCloud::GetCloudsqlInstancesService.new(project).execute,
emptyIllustrationUrl: ActionController::Base.helpers.image_path('illustrations/pipelines_empty.svg')
}
@js_data = js_data.to_json
track_event('databases#index', 'success', js_data)
track_event('databases#index', 'success', nil)
end
def new
product = permitted_params[:product].to_sym
@title = title(product)
@js_data = {
gcpProjects: gcp_projects,
refs: refs,
cancelPath: project_google_cloud_databases_path(project),
formTitle: form_title(product),
formDescription: description(product),
databaseVersions: Projects::GoogleCloud::CloudsqlHelper::VERSIONS[product],
tiers: Projects::GoogleCloud::CloudsqlHelper::TIERS
}.to_json
render template: 'projects/google_cloud/databases/cloudsql_form', formats: :html
end
def create
enable_response = ::GoogleCloud::EnableCloudsqlService
.new(project, current_user, enable_service_params)
.execute
if enable_response[:status] == :error
track_event('databases#cloudsql_create', 'error_enable_cloudsql_service', enable_response)
flash[:error] = error_message(enable_response[:message])
else
permitted_params = params.permit(:gcp_project, :ref, :database_version, :tier)
create_response = ::GoogleCloud::CreateCloudsqlInstanceService
.new(project, current_user, create_service_params(permitted_params))
.execute
if create_response[:status] == :error
track_event('databases#cloudsql_create', 'error_create_cloudsql_instance', create_response)
flash[:warning] = error_message(create_response[:message])
else
track_event('databases#cloudsql_create', 'success', nil)
flash[:notice] = success_message
end
end
redirect_to project_google_cloud_databases_path(project)
end
private
def enable_service_params
{ google_oauth2_token: token_in_session }
end
def create_service_params(permitted_params)
{
google_oauth2_token: token_in_session,
gcp_project_id: permitted_params[:gcp_project],
environment_name: permitted_params[:ref],
database_version: permitted_params[:database_version],
tier: permitted_params[:tier]
}
end
def error_message(message)
format(s_("CloudSeed|Google Cloud Error - %{message}"), message: message)
end
def success_message
s_('CloudSeed|Cloud SQL instance creation request successful. Expected resolution time is ~5 minutes.')
end
def validate_product
not_found unless permitted_params[:product].in?(%w[postgres mysql sqlserver])
end
def permitted_params
params.permit(:product)
end
def title(product)
case product
when :postgres
s_('CloudSeed|Create Postgres Instance')
when :mysql
s_('CloudSeed|Create MySQL Instance')
else
s_('CloudSeed|Create MySQL Instance')
end
end
def form_title(product)
case product
when :postgres
s_('CloudSeed|Cloud SQL for Postgres')
when :mysql
s_('CloudSeed|Cloud SQL for MySQL')
else
s_('CloudSeed|Cloud SQL for SQL Server')
end
end
def description(product)
case product
when :postgres
s_('CloudSeed|Cloud SQL instances are fully managed, relational PostgreSQL databases. '\
'Google handles replication, patch management, and database management '\
'to ensure availability and performance.')
when :mysql
s_('Cloud SQL instances are fully managed, relational MySQL databases. '\
'Google handles replication, patch management, and database management '\
'to ensure availability and performance.')
else
s_('Cloud SQL instances are fully managed, relational SQL Server databases. ' \
'Google handles replication, patch management, and database management ' \
'to ensure availability and performance.')
end
end
end
end

View File

@ -9,8 +9,8 @@
# updated_before: DateTime
# finished_after: DateTime
# finished_before: DateTime
# environment: String
# status: String (see Deployment.statuses)
# environment: String (name) or Integer (ID)
# status: String or Array<String> (see Deployment.statuses)
# order_by: String (see ALLOWED_SORT_VALUES constant)
# sort: String (asc | desc)
class DeploymentsFinder
@ -33,6 +33,7 @@ class DeploymentsFinder
def initialize(params = {})
@params = params
@params[:status] = Array(@params[:status]).map(&:to_s) if @params[:status]
validate!
end
@ -68,16 +69,25 @@ class DeploymentsFinder
raise error if raise_for_inefficient_updated_at_query?
end
if (filter_by_finished_at? && !order_by_finished_at?) || (!filter_by_finished_at? && order_by_finished_at?)
raise InefficientQueryError, '`finished_at` filter and `finished_at` sorting must be paired'
if filter_by_finished_at? && !order_by_finished_at?
raise InefficientQueryError, '`finished_at` filter requires `finished_at` sort.'
end
if order_by_finished_at? && !(filter_by_finished_at? || filter_by_finished_statuses?)
raise InefficientQueryError,
'`finished_at` sort requires `finished_at` filter or a filter with at least one of the finished statuses.'
end
if filter_by_finished_at? && !filter_by_successful_deployment?
raise InefficientQueryError, '`finished_at` filter must be combined with `success` status filter.'
end
if params[:environment].present? && !params[:project].present?
raise InefficientQueryError, '`environment` filter must be combined with `project` scope.'
if filter_by_environment_name? && !params[:project].present?
raise InefficientQueryError, '`environment` name filter must be combined with `project` scope.'
end
if filter_by_finished_statuses? && filter_by_upcoming_statuses?
raise InefficientQueryError, 'finished statuses and upcoming statuses must be separately queried.'
end
end
@ -86,6 +96,8 @@ class DeploymentsFinder
params[:project].deployments
elsif params[:group].present?
::Deployment.for_projects(params[:group].all_projects)
elsif filter_by_environment_id?
::Deployment.for_environment(params[:environment])
else
::Deployment.none
end
@ -112,7 +124,7 @@ class DeploymentsFinder
end
def by_environment(items)
if params[:project].present? && params[:environment].present?
if params[:project].present? && filter_by_environment_name?
items.for_environment_name(params[:project], params[:environment])
else
items
@ -122,7 +134,7 @@ class DeploymentsFinder
def by_status(items)
return items unless params[:status].present?
unless Deployment.statuses.key?(params[:status])
unless Deployment.statuses.keys.intersection(params[:status]) == params[:status]
raise ArgumentError, "The deployment status #{params[:status]} is invalid"
end
@ -165,7 +177,23 @@ class DeploymentsFinder
end
def filter_by_successful_deployment?
params[:status].to_s == 'success'
params[:status].present? && params[:status].count == 1 && params[:status].first.to_s == 'success'
end
def filter_by_finished_statuses?
params[:status].present? && Deployment::FINISHED_STATUSES.map(&:to_s).intersection(params[:status]).any?
end
def filter_by_upcoming_statuses?
params[:status].present? && Deployment::UPCOMING_STATUSES.map(&:to_s).intersection(params[:status]).any?
end
def filter_by_environment_name?
params[:environment].present? && params[:environment].is_a?(String)
end
def filter_by_environment_id?
params[:environment].present? && params[:environment].is_a?(Integer)
end
def order_by_updated_at?

View File

@ -0,0 +1,20 @@
# frozen_string_literal: true
module Resolvers
class DeploymentResolver < BaseResolver
argument :iid,
GraphQL::Types::ID,
required: true,
description: 'Project-level internal ID of the Deployment.'
type Types::DeploymentType, null: true
alias_method :project, :object
def resolve(iid:)
return unless project.present? && project.is_a?(::Project)
Deployment.for_iid(project, iid)
end
end
end

View File

@ -0,0 +1,39 @@
# frozen_string_literal: true
module Resolvers
class DeploymentsResolver < BaseResolver
argument :statuses, [Types::DeploymentStatusEnum],
description: 'Statuses of the deployments.',
required: false,
as: :status
argument :order_by, Types::DeploymentsOrderByInputType,
description: 'Order by a specified field.',
required: false
type Types::DeploymentType, null: true
alias_method :environment, :object
def resolve(**args)
return unless environment.present? && environment.is_a?(::Environment)
args = transform_args_for_finder(**args)
# GraphQL BatchLoader shouldn't be used here because pagination query will be inefficient
# that fetches thousands of rows before limiting and offsetting.
DeploymentsFinder.new(environment: environment.id, **args).execute
end
private
def transform_args_for_finder(**args)
if (order_by = args.delete(:order_by))
order_by = order_by.to_h.map { |k, v| { order_by: k.to_s, sort: v } }.first
args.merge!(order_by)
end
args
end
end
end

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
module Types
class DeploymentDetailsType < DeploymentType
graphql_name 'DeploymentDetails'
description 'The details of the deployment'
authorize :read_deployment
end
end

View File

@ -0,0 +1,14 @@
# frozen_string_literal: true
module Types
class DeploymentStatusEnum < BaseEnum
graphql_name 'DeploymentStatus'
description 'All deployment statuses.'
::Deployment.statuses.each_key do |status|
value status.upcase,
description: "A deployment that is #{status.tr('_', ' ')}.",
value: status
end
end
end

View File

@ -0,0 +1,54 @@
# frozen_string_literal: true
module Types
# If you're considering to add a new field in DeploymentType, please follow this guideline:
# - If the field is preloadable in batch, define it in DeploymentType.
# In this case, you should extend DeploymentsResolver logic to preload the field. Also, add a new test that
# fetching the specific field for multiple deployments doesn't cause N+1 query problem.
# - If the field is NOT preloadable in batch, define it in DeploymentDetailsType.
# This type can be only fetched for a single deployment, so you don't need to take care of the preloading.
class DeploymentType < BaseObject
graphql_name 'Deployment'
description 'The deployment of an environment'
present_using Deployments::DeploymentPresenter
authorize :read_deployment
field :id,
GraphQL::Types::ID,
description: 'Global ID of the deployment.'
field :iid,
GraphQL::Types::ID,
description: 'Project-level internal ID of the deployment.'
field :ref,
GraphQL::Types::String,
description: 'Git-Ref that the deployment ran on.'
field :tag,
GraphQL::Types::Boolean,
description: 'True or false if the deployment ran on a Git-tag.'
field :sha,
GraphQL::Types::String,
description: 'Git-SHA that the deployment ran on.'
field :created_at,
Types::TimeType,
description: 'When the deployment record was created.'
field :updated_at,
Types::TimeType,
description: 'When the deployment record was updated.'
field :finished_at,
Types::TimeType,
description: 'When the deployment finished.'
field :status,
Types::DeploymentStatusEnum,
description: 'Status of the deployment.'
end
end

View File

@ -0,0 +1,24 @@
# frozen_string_literal: true
module Types
class DeploymentsOrderByInputType < BaseInputObject
graphql_name 'DeploymentsOrderByInput'
description 'Values for ordering deployments by a specific field'
argument :created_at,
Types::SortDirectionEnum,
required: false,
description: 'Order by Created time.'
argument :finished_at,
Types::SortDirectionEnum,
required: false,
description: 'Order by Finished time.'
def prepare
raise GraphQL::ExecutionError, 'orderBy parameter must contain one key-value pair.' unless to_h.size == 1
super
end
end
end

View File

@ -29,5 +29,14 @@ module Types
Types::AlertManagement::AlertType,
null: true,
description: 'Most severe open alert for the environment. If multiple alerts have equal severity, the most recent is returned.'
# Setting high complexity for preventing users from querying deployments for multiple environments,
# which could result in N+1 issue.
field :deployments,
Types::DeploymentType.connection_type,
null: true,
description: 'Deployments of the environment.',
resolver: Resolvers::DeploymentsResolver,
complexity: 150
end
end

View File

@ -179,6 +179,12 @@ module Types
description: 'A single environment of the project.',
resolver: Resolvers::EnvironmentsResolver.single
field :deployment,
Types::DeploymentDetailsType,
null: true,
description: 'Details of the deployment of the project.',
resolver: Resolvers::DeploymentResolver.single
field :issue,
Types::IssueType,
null: true,

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
module Types
class SortDirectionEnum < BaseEnum
graphql_name 'SortDirectionEnum'
description 'Values for sort direction'
value 'ASC', 'Ascending order.', value: 'asc'
value 'DESC', 'Descending order.', value: 'desc'
end
end

View File

@ -0,0 +1,55 @@
# frozen_string_literal: true
module Projects
module GoogleCloud
module CloudsqlHelper
# Sources:
# - https://cloud.google.com/sql/docs/postgres/instance-settings
# - https://cloud.google.com/sql/docs/mysql/instance-settings
# - https://cloud.google.com/sql/docs/sqlserver/instance-settings
TIERS = [
{ value: 'db-custom-1-3840', label: '1 vCPU, 3840 MB RAM - Standard' },
{ value: 'db-custom-2-7680', label: '2 vCPU, 7680 MB RAM - Standard' },
{ value: 'db-custom-2-13312', label: '2 vCPU, 13312 MB RAM - High memory' },
{ value: 'db-custom-4-15360', label: '4 vCPU, 15360 MB RAM - Standard' },
{ value: 'db-custom-4-26624', label: '4 vCPU, 26624 MB RAM - High memory' },
{ value: 'db-custom-8-30720', label: '8 vCPU, 30720 MB RAM - Standard' },
{ value: 'db-custom-8-53248', label: '8 vCPU, 53248 MB RAM - High memory' },
{ value: 'db-custom-16-61440', label: '16 vCPU, 61440 MB RAM - Standard' },
{ value: 'db-custom-16-106496', label: '16 vCPU, 106496 MB RAM - High memory' },
{ value: 'db-custom-32-122880', label: '32 vCPU, 122880 MB RAM - Standard' },
{ value: 'db-custom-32-212992', label: '32 vCPU, 212992 MB RAM - High memory' },
{ value: 'db-custom-64-245760', label: '64 vCPU, 245760 MB RAM - Standard' },
{ value: 'db-custom-64-425984', label: '64 vCPU, 425984 MB RAM - High memory' },
{ value: 'db-custom-96-368640', label: '96 vCPU, 368640 MB RAM - Standard' },
{ value: 'db-custom-96-638976', label: '96 vCPU, 638976 MB RAM - High memory' }
].freeze
VERSIONS = {
postgres: [
{ value: 'POSTGRES_14', label: 'PostgreSQL 14' },
{ value: 'POSTGRES_13', label: 'PostgreSQL 13' },
{ value: 'POSTGRES_12', label: 'PostgreSQL 12' },
{ value: 'POSTGRES_11', label: 'PostgreSQL 11' },
{ value: 'POSTGRES_10', label: 'PostgreSQL 10' },
{ value: 'POSTGRES_9_6', label: 'PostgreSQL 9.6' }
],
mysql: [
{ value: 'MYSQL_8_0', label: 'MySQL 8' },
{ value: 'MYSQL_5_7', label: 'MySQL 5.7' },
{ value: 'MYSQL_5_6', label: 'MySQL 5.6' }
],
sqlserver: [
{ value: 'SQLSERVER_2017_STANDARD', label: 'SQL Server 2017 Standard' },
{ value: 'SQLSERVER_2017_ENTERPRISE', label: 'SQL Server 2017 Enterprise' },
{ value: 'SQLSERVER_2017_EXPRESS', label: 'SQL Server 2017 Express' },
{ value: 'SQLSERVER_2017_WEB', label: 'SQL Server 2017 Web' },
{ value: 'SQLSERVER_2019_STANDARD', label: 'SQL Server 2019 Standard' },
{ value: 'SQLSERVER_2019_ENTERPRISE', label: 'SQL Server 2019 Enterprise' },
{ value: 'SQLSERVER_2019_EXPRESS', label: 'SQL Server 2019 Express' },
{ value: 'SQLSERVER_2019_WEB', label: 'SQL Server 2019 Web' }
]
}.freeze
end
end
end

View File

@ -36,6 +36,7 @@ class Deployment < ApplicationRecord
delegate :name, to: :environment, prefix: true
delegate :kubernetes_namespace, to: :deployment_cluster, allow_nil: true
scope :for_iid, -> (project, iid) { where(project: project, iid: iid) }
scope :for_environment, -> (environment) { where(environment_id: environment) }
scope :for_environment_name, -> (project, name) do
where('deployments.environment_id = (?)',
@ -61,6 +62,7 @@ class Deployment < ApplicationRecord
VISIBLE_STATUSES = %i[running success failed canceled blocked].freeze
FINISHED_STATUSES = %i[success failed canceled].freeze
UPCOMING_STATUSES = %i[created blocked running].freeze
state_machine :status, initial: :created do
event :run do

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
module Deployments
class DeploymentPresenter < Gitlab::View::Presenter::Delegated
presents ::Deployment, as: :deployment
end
end

View File

@ -11,7 +11,7 @@ module GoogleCloud
trigger_instance_setup_worker
success
rescue Google::Apis::Error => err
error(err.to_json)
error(err.message)
end
private

View File

@ -12,6 +12,8 @@ module GoogleCloud
end
success({ gcp_project_ids: unique_gcp_project_ids })
rescue Google::Apis::Error => err
error(err.message)
end
private

View File

@ -17,6 +17,7 @@
selected_file_template_project_id: "#{@project.service_desk_setting&.file_template_project_id}",
outgoing_name: "#{@project.service_desk_setting&.outgoing_name}",
project_key: "#{@project.service_desk_setting&.project_key}",
templates: available_service_desk_templates_for(@project) } }
templates: available_service_desk_templates_for(@project),
public_project: "#{@project.public?}" } }
- elsif show_callout?('promote_service_desk_dismissed')
= render 'shared/promotions/promote_servicedesk'

View File

@ -0,0 +1,9 @@
- add_to_breadcrumbs _('Google Cloud'), project_google_cloud_path(@project)
- add_to_breadcrumbs s_('CloudSeed|Databases'), project_google_cloud_databases_path(@project)
- breadcrumb_title @title
- page_title @title
- @content_class = "limit-container-width" unless fluid_layout
= form_tag project_google_cloud_databases_path(@project), method: 'post' do
#js-google-cloud-databases-cloudsql-form{ data: @js_data }

View File

@ -1,5 +1,5 @@
- add_to_breadcrumbs _('Google Cloud'), project_google_cloud_path(@project)
- breadcrumb_title _('CloudSeed|Regions')
- breadcrumb_title s_('CloudSeed|Regions')
- page_title s_('CloudSeed|Regions')
- @content_class = "limit-container-width" unless fluid_layout

View File

@ -1,7 +1,7 @@
- user = local_assigns.fetch(:user, current_user)
- access = user&.max_member_access_for_group(group.id)
%li.group-row.py-3.gl-align-items-center{ class: "gl-display-flex!#{' no-description' if group.description.blank?}" }
%li.group-row.py-3.gl-align-items-center{ class: "gl-display-flex!" }
.avatar-container.rect-avatar.s40.gl-flex-shrink-0
= link_to group do
= group_icon(group, class: "avatar s40")

View File

@ -8,30 +8,15 @@ module GoogleCloud
feature_category :not_owned # rubocop:disable Gitlab/AvoidFeatureCategoryNotOwned
idempotent!
def perform(user_id, project_id, options = {})
def perform(user_id, project_id, params = {})
user = User.find(user_id)
project = Project.find(project_id)
params = params.with_indifferent_access
google_oauth2_token = options[:google_oauth2_token]
gcp_project_id = options[:gcp_project_id]
instance_name = options[:instance_name]
database_version = options[:database_version]
environment_name = options[:environment_name]
is_protected = options[:is_protected]
params = {
google_oauth2_token: google_oauth2_token,
gcp_project_id: gcp_project_id,
instance_name: instance_name,
database_version: database_version,
environment_name: environment_name,
is_protected: is_protected
}
response = GoogleCloud::SetupCloudsqlInstanceService.new(project, user, params).execute
response = ::GoogleCloud::SetupCloudsqlInstanceService.new(project, user, params).execute
if response[:status] == :error
raise response[:message]
raise "Error SetupCloudsqlInstanceService: #{response.to_json}"
end
end
end

View File

@ -0,0 +1,8 @@
---
name: ci_forked_source_public_cost_factor
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/94870
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/370147
milestone: '15.4'
type: development
group: group::pipeline execution
default_enabled: false

View File

@ -1,7 +1,7 @@
---
name: incubation_5mp_google_cloud
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70715
rollout_issue_url:
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/371332
milestone: '14.3'
type: development
group: group::incubation

View File

@ -312,7 +312,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
get '/deployments/cloud_run', to: 'deployments#cloud_run'
get '/deployments/cloud_storage', to: 'deployments#cloud_storage'
get '/databases', to: 'databases#index'
resources :databases, only: [:index, :create, :new], path_names: { new: 'new/:product' }
end
resources :environments, except: [:destroy] do

View File

@ -0,0 +1,70 @@
- name: "Create tasks in issues"
description: |
Tasks provide a robust way to refine an issue into smaller, discrete work units. Previously in GitLab, you could break down an issue into smaller parts using markdown checklists within the description. However, these checklist items could not be easily assigned, labeled, or managed anywhere outside of the description field.
You can now create tasks within issues from the Child Items widget. Then, you can open the task directly within the issue to quickly update the title, set the weight, or add a description. Tasks break down work within projects for GitLab Free and increase the planning hierarchy for our GitLab Premium customers to three levels (epic, issue, and task). In our next iteration, you will be able to add labels, milestones, and iterations to each task.
Tasks represent our first step toward evolving issues, epics, incidents, requirements, and test cases to [work items](https://docs.gitlab.com/ee/development/work_items.html). If you have feedback or suggestions about tasks, please comment on [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/363613).
stage: plan
self-managed: true
gitlab-com: true
available_in: [Free, Premium, Ultimate]
documentation_link: https://docs.gitlab.com/ee/user/tasks.html
image_url: https://about.gitlab.com/images/unreleased/create-tasks.gif
published_at: 2022-08-22
release: 15.3
- name: "GitOps features are now free"
description: |
When you use GitOps to update a Kubernetes cluster, also called a pull-based deployment, you get an improved security model, better scalability and stability.
The GitLab agent for Kubernetes has supported [GitOps workflows](https://docs.gitlab.com/ee/user/clusters/agent/gitops.html) from its initial release, but until now, the functionality was available only if you had a GitLab Premium or Ultimate subscription. Now if you have a Free subscription, you also get pull-based deployment support. The features available in GitLab Free should serve small, high-trust teams or be suitable to test the agent before upgrading to a higher tier.
In the future, we plan to add [built-in multi-tenant support](https://gitlab.com/gitlab-org/gitlab/-/issues/337904) for Premium subscriptions. This feature would be similar to the impersonation feature already available for the [CI/CD workflow](https://docs.gitlab.com/ee/user/clusters/agent/ci_cd_workflow.html#restrict-project-and-group-access-by-using-impersonation).
stage: configure
self-managed: true
gitlab-com: true
available_in: [Free, Premium, Ultimate]
documentation_link: https://docs.gitlab.com/ee/user/clusters/agent/gitops.html
image_url: https://img.youtube.com/vi/jgVxOnMfOZA/hqdefault.jpg
published_at: 2022-08-22
release: 15.3
- name: "Submit merge request review with summary comment"
description: |
When you finish reviewing a merge request, there are probably some common things that you do, like summarizing your review for others or approving the changes if they look good to you. Those common tasks are now quicker and easier: when you submit your review, you can add a summary comment along with any [quick actions](https://docs.gitlab.com/ee/user/project/quick_actions.html) like `/approve`.
stage: create
self-managed: true
gitlab-com: true
available_in: [Free, Premium, Ultimate]
documentation_link: https://docs.gitlab.com/ee/user/project/merge_requests/reviews/#submit-a-review
image_url: https://about.gitlab.com/images/unreleased/create-mr-review-summary.png
published_at: 2022-08-22
release: 15.3
- name: "Define password complexity requirements"
description: |
GitLab administrators can now define password complexity requirements in addition to minimum password length. For new passwords, you can now require:
- Numbers.
- Uppercase letters.
- Lowercase letters.
- Symbols.
Complex passwords are less likely to be compromised, and the ability to configure password complexity requirements helps administrators enforce their password policies.
stage: manage
self-managed: true
gitlab-com: false
available_in: [Premium, Ultimate]
documentation_link: https://docs.gitlab.com/ee/user/admin_area/settings/sign_up_restrictions.html#password-complexity-requirements
image_url: https://about.gitlab.com/images/unreleased/manage-password-complexity-policy.png
published_at: 2022-08-22
release: 15.3
- name: "Maintain SAML Group Links with API"
description: |
Until now, SAML group links had to be configured in the UI. Now, you can manage SAML group links programmatically using the API so you can automate SAML groups management.
stage: manage
self-managed: true
gitlab-com: true
available_in: [Premium, Ultimate]
documentation_link: https://docs.gitlab.com/ee/api/groups.html#saml-group-links
image_url: https://img.youtube.com/vi/Pft61UFM5LM/hqdefault.jpg
published_at: 2022-08-22
release: 15.3

View File

@ -0,0 +1,8 @@
# frozen_string_literal: true
class AddAutoBanUserToApplicationSettings < Gitlab::Database::Migration[2.0]
def change
add_column :application_settings, :auto_ban_user_on_excessive_projects_download, :boolean,
default: false, null: false
end
end

View File

@ -0,0 +1 @@
a669aca9370ecd086b582164e68366ca459754b26e096301c2dc7121a7e9ab58

View File

@ -11458,6 +11458,7 @@ CREATE TABLE application_settings (
error_tracking_access_token_encrypted text,
package_registry_cleanup_policies_worker_capacity integer DEFAULT 2 NOT NULL,
deactivate_dormant_users_period integer DEFAULT 90 NOT NULL,
auto_ban_user_on_excessive_projects_download boolean DEFAULT false NOT NULL,
CONSTRAINT app_settings_container_reg_cleanup_tags_max_list_size_positive CHECK ((container_registry_cleanup_tags_service_max_list_size >= 0)),
CONSTRAINT app_settings_container_registry_pre_import_tags_rate_positive CHECK ((container_registry_pre_import_tags_rate >= (0)::numeric)),
CONSTRAINT app_settings_dep_proxy_ttl_policies_worker_capacity_positive CHECK ((dependency_proxy_ttl_group_policy_worker_capacity >= 0)),

View File

@ -6984,6 +6984,29 @@ The edge type for [`DependencyProxyManifest`](#dependencyproxymanifest).
| <a id="dependencyproxymanifestedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
| <a id="dependencyproxymanifestedgenode"></a>`node` | [`DependencyProxyManifest`](#dependencyproxymanifest) | The item at the end of the edge. |
#### `DeploymentConnection`
The connection type for [`Deployment`](#deployment).
##### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="deploymentconnectionedges"></a>`edges` | [`[DeploymentEdge]`](#deploymentedge) | A list of edges. |
| <a id="deploymentconnectionnodes"></a>`nodes` | [`[Deployment]`](#deployment) | A list of nodes. |
| <a id="deploymentconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
#### `DeploymentEdge`
The edge type for [`Deployment`](#deployment).
##### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="deploymentedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
| <a id="deploymentedgenode"></a>`node` | [`Deployment`](#deployment) | The item at the end of the edge. |
#### `DesignAtVersionConnection`
The connection type for [`DesignAtVersion`](#designatversion).
@ -10975,6 +10998,42 @@ Group-level Dependency Proxy settings.
| ---- | ---- | ----------- |
| <a id="dependencyproxysettingenabled"></a>`enabled` | [`Boolean!`](#boolean) | Indicates whether the dependency proxy is enabled for the group. |
### `Deployment`
The deployment of an environment.
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="deploymentcreatedat"></a>`createdAt` | [`Time`](#time) | When the deployment record was created. |
| <a id="deploymentfinishedat"></a>`finishedAt` | [`Time`](#time) | When the deployment finished. |
| <a id="deploymentid"></a>`id` | [`ID`](#id) | Global ID of the deployment. |
| <a id="deploymentiid"></a>`iid` | [`ID`](#id) | Project-level internal ID of the deployment. |
| <a id="deploymentref"></a>`ref` | [`String`](#string) | Git-Ref that the deployment ran on. |
| <a id="deploymentsha"></a>`sha` | [`String`](#string) | Git-SHA that the deployment ran on. |
| <a id="deploymentstatus"></a>`status` | [`DeploymentStatus`](#deploymentstatus) | Status of the deployment. |
| <a id="deploymenttag"></a>`tag` | [`Boolean`](#boolean) | True or false if the deployment ran on a Git-tag. |
| <a id="deploymentupdatedat"></a>`updatedAt` | [`Time`](#time) | When the deployment record was updated. |
### `DeploymentDetails`
The details of the deployment.
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="deploymentdetailscreatedat"></a>`createdAt` | [`Time`](#time) | When the deployment record was created. |
| <a id="deploymentdetailsfinishedat"></a>`finishedAt` | [`Time`](#time) | When the deployment finished. |
| <a id="deploymentdetailsid"></a>`id` | [`ID`](#id) | Global ID of the deployment. |
| <a id="deploymentdetailsiid"></a>`iid` | [`ID`](#id) | Project-level internal ID of the deployment. |
| <a id="deploymentdetailsref"></a>`ref` | [`String`](#string) | Git-Ref that the deployment ran on. |
| <a id="deploymentdetailssha"></a>`sha` | [`String`](#string) | Git-SHA that the deployment ran on. |
| <a id="deploymentdetailsstatus"></a>`status` | [`DeploymentStatus`](#deploymentstatus) | Status of the deployment. |
| <a id="deploymentdetailstag"></a>`tag` | [`Boolean`](#boolean) | True or false if the deployment ran on a Git-tag. |
| <a id="deploymentdetailsupdatedat"></a>`updatedAt` | [`Time`](#time) | When the deployment record was updated. |
### `Design`
A single design.
@ -11406,6 +11465,23 @@ Describes where code is deployed for a project.
#### Fields with arguments
##### `Environment.deployments`
Deployments of the environment.
Returns [`DeploymentConnection`](#deploymentconnection).
This field returns a [connection](#connections). It accepts the
four standard [pagination arguments](#connection-pagination-arguments):
`before: String`, `after: String`, `first: Int`, `last: Int`.
###### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="environmentdeploymentsorderby"></a>`orderBy` | [`DeploymentsOrderByInput`](#deploymentsorderbyinput) | Order by a specified field. |
| <a id="environmentdeploymentsstatuses"></a>`statuses` | [`[DeploymentStatus!]`](#deploymentstatus) | Statuses of the deployments. |
##### `Environment.metricsDashboard`
Metrics dashboard schema for the environment.
@ -15838,6 +15914,18 @@ four standard [pagination arguments](#connection-pagination-arguments):
| <a id="projectdastsitevalidationsnormalizedtargeturls"></a>`normalizedTargetUrls` | [`[String!]`](#string) | Normalized URL of the target to be scanned. |
| <a id="projectdastsitevalidationsstatus"></a>`status` | [`DastSiteValidationStatusEnum`](#dastsitevalidationstatusenum) | Status of the site validation. |
##### `Project.deployment`
Details of the deployment of the project.
Returns [`DeploymentDetails`](#deploymentdetails).
###### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="projectdeploymentiid"></a>`iid` | [`ID!`](#id) | Project-level internal ID of the Deployment. |
##### `Project.environment`
A single environment of the project.
@ -19605,6 +19693,20 @@ Weight of the data visualization palette.
| <a id="dependencyproxymanifeststatuspending_destruction"></a>`PENDING_DESTRUCTION` | Dependency proxy manifest has a status of pending_destruction. |
| <a id="dependencyproxymanifeststatusprocessing"></a>`PROCESSING` | Dependency proxy manifest has a status of processing. |
### `DeploymentStatus`
All deployment statuses.
| Value | Description |
| ----- | ----------- |
| <a id="deploymentstatusblocked"></a>`BLOCKED` | A deployment that is blocked. |
| <a id="deploymentstatuscanceled"></a>`CANCELED` | A deployment that is canceled. |
| <a id="deploymentstatuscreated"></a>`CREATED` | A deployment that is created. |
| <a id="deploymentstatusfailed"></a>`FAILED` | A deployment that is failed. |
| <a id="deploymentstatusrunning"></a>`RUNNING` | A deployment that is running. |
| <a id="deploymentstatusskipped"></a>`SKIPPED` | A deployment that is skipped. |
| <a id="deploymentstatussuccess"></a>`SUCCESS` | A deployment that is success. |
### `DeploymentTier`
All environment deployment tiers.
@ -20621,6 +20723,15 @@ Common sort values.
| <a id="sortupdated_asc"></a>`updated_asc` **{warning-solid}** | **Deprecated** in 13.5. This was renamed. Use: `UPDATED_ASC`. |
| <a id="sortupdated_desc"></a>`updated_desc` **{warning-solid}** | **Deprecated** in 13.5. This was renamed. Use: `UPDATED_DESC`. |
### `SortDirectionEnum`
Values for sort direction.
| Value | Description |
| ----- | ----------- |
| <a id="sortdirectionenumasc"></a>`ASC` | Ascending order. |
| <a id="sortdirectionenumdesc"></a>`DESC` | Descending order. |
### `TestCaseStatus`
| Value | Description |
@ -22375,6 +22486,17 @@ Input type for DastSiteProfile authentication.
| <a id="dastsiteprofileauthinputusername"></a>`username` | [`String`](#string) | Username to authenticate with on the target. |
| <a id="dastsiteprofileauthinputusernamefield"></a>`usernameField` | [`String`](#string) | Name of username field at the sign-in HTML form. |
### `DeploymentsOrderByInput`
Values for ordering deployments by a specific field.
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="deploymentsorderbyinputcreatedat"></a>`createdAt` | [`SortDirectionEnum`](#sortdirectionenum) | Order by Created time. |
| <a id="deploymentsorderbyinputfinishedat"></a>`finishedAt` | [`SortDirectionEnum`](#sortdirectionenum) | Order by Finished time. |
### `DiffImagePositionInput`
#### Arguments

View File

@ -816,7 +816,7 @@ You can't delete archived jobs with the API, but you can
## Run a job
Triggers a manual action to start a job.
For a job in manual status, trigger an action to start the job.
```plaintext
POST /projects/:id/jobs/:job_id/play

View File

@ -385,6 +385,7 @@ listed in the descriptions of the relevant settings.
| `max_number_of_repository_downloads` **(ULTIMATE SELF)** | integer | no | Maximum number of unique repositories a user can download in the specified time period before they are banned. Default: 0, Maximum: 10,000 repositories. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/87980) in GitLab 15.1. |
| `max_number_of_repository_downloads_within_time_period` **(ULTIMATE SELF)** | integer | no | Reporting time period (in seconds). Default: 0, Maximum: 864000 seconds (10 days). [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/87980) in GitLab 15.1. |
| `git_rate_limit_users_allowlist` **(ULTIMATE SELF)** | array of strings | no | List of usernames excluded from Git anti-abuse rate limits. Default: `[]`, Maximum: 100 usernames. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/90815) in GitLab 15.2. |
| `auto_ban_user_on_excessive_projects_download` **(ULTIMATE SELF)** | boolean | no | When enabled, users will get automatically banned from the application when they download more than the maximum number of unique projects in the time period specified by `max_number_of_repository_downloads` and `max_number_of_repository_downloads_within_time_period` respectively. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/94153) in GitLab 15.4 |
| `mirror_available` | boolean | no | Allow repository mirroring to configured by project Maintainers. If disabled, only Administrators can configure repository mirroring. |
| `mirror_capacity_threshold` **(PREMIUM)** | integer | no | Minimum capacity to be available before scheduling more mirrors preemptively. |
| `mirror_max_capacity` **(PREMIUM)** | integer | no | Maximum number of mirrors that can be synchronizing at the same time. |

View File

@ -0,0 +1,65 @@
---
stage: Verify
group: Pipeline Authoring
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Downstream pipelines **(FREE)**
A downstream pipeline is any GitLab CI/CD pipeline triggered by another pipeline.
A downstream pipeline can be either:
- A [parent-child pipeline](parent_child_pipelines.md), which is a downstream pipeline triggered
in the same project as the first pipeline.
- A [multi-project pipeline](multi_project_pipelines.md), which is a downstream pipeline triggered
in a different project than the first pipeline.
Parent-child pipelines and multi-project pipelines can sometimes be used for similar purposes,
but there are some key differences.
Parent-child pipelines:
- Run under the same project, ref, and commit SHA as the parent pipeline.
- Affect the overall status of the ref the pipeline runs against. For example,
if a pipeline fails for the main branch, it's common to say that "main is broken".
The status of child pipelines don't directly affect the status of the ref, unless the child
pipeline is triggered with [`strategy:depend`](../yaml/index.md#triggerstrategy).
- Are automatically canceled if the pipeline is configured with [`interruptible`](../yaml/index.md#interruptible)
when a new pipeline is created for the same ref.
- Display only the parent pipelines in the pipeline index page. Child pipelines are
visible when visiting their parent pipeline's page.
- Are limited to 2 levels of nesting. A parent pipeline can trigger multiple child pipelines,
and those child pipeline can trigger multiple child pipelines (`A -> B -> C`).
Multi-project pipelines:
- Are triggered from another pipeline, but the upstream (triggering) pipeline does
not have much control over the downstream (triggered) pipeline. However, it can
choose the ref of the downstream pipeline, and pass CI/CD variables to it.
- Affect the overall status of the ref of the project it runs in, but does not
affect the status of the triggering pipeline's ref, unless it was triggered with
[`strategy:depend`](../yaml/index.md#triggerstrategy).
- Are not automatically canceled in the downstream project when using [`interruptible`](../yaml/index.md#interruptible)
if a new pipeline runs for the same ref in the upstream pipeline. They can be
automatically canceled if a new pipeline is triggered for the same ref on the downstream project.
- Multi-project pipelines are standalone pipelines because they are normal pipelines
that happened to be triggered by an external project. They are all visible on the pipeline index page.
- Are independent, so there are no nesting limits.
## View a downstream pipeline
In the [pipeline graph view](index.md#view-full-pipeline-graph), downstream pipelines display
as a list of cards on the right of the graph.
### Cancel or retry downstream pipelines from the graph view
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/354974) in GitLab 15.0 [with a flag](../../administration/feature_flags.md) named `downstream_retry_action`. Disabled by default.
> - [Generally available and feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/357406) in GitLab 15.1.
To cancel a downstream pipeline that is still running, select **Cancel** (**{cancel}**)
on the pipeline's card.
To retry a failed downstream pipeline, select **Retry** (**{retry}**)
on the pipeline's card.
![downstream pipeline actions](img/downstream_pipeline_actions.png)

View File

@ -62,40 +62,6 @@ Pipelines can be configured in many different ways:
run in the same project and with the same SHA. This pipeline architecture is commonly used for mono-repos.
- [Multi-project pipelines](multi_project_pipelines.md) combine pipelines for different projects together.
### How parent-child pipelines compare to multi-project pipelines
Parent-child pipelines and multi-project pipelines can sometimes be used for similar
purposes, but there are some key differences:
Parent-child pipelines:
- Run under the same project, ref, and commit SHA as the parent pipeline.
- Affect the overall status of the ref the pipeline runs against. For example,
if a pipeline fails for the main branch, it's common to say that "main is broken".
The status of child pipelines don't directly affect the status of the ref, unless the child
pipeline is triggered with [`strategy:depend`](../yaml/index.md#triggerstrategy).
- Are automatically canceled if the pipeline is configured with [`interruptible`](../yaml/index.md#interruptible)
when a new pipeline is created for the same ref.
- Display only the parent pipelines in the pipeline index page. Child pipelines are
visible when visiting their parent pipeline's page.
- Are limited to 2 levels of nesting. A parent pipeline can trigger multiple child pipelines,
and those child pipeline can trigger multiple child pipelines (`A -> B -> C`).
Multi-project pipelines:
- Are triggered from another pipeline, but the upstream (triggering) pipeline does
not have much control over the downstream (triggered) pipeline. However, it can
choose the ref of the downstream pipeline, and pass CI/CD variables to it.
- Affect the overall status of the ref of the project it runs in, but does not
affect the status of the triggering pipeline's ref, unless it was triggered with
[`strategy:depend`](../yaml/index.md#triggerstrategy).
- Are not automatically canceled in the downstream project when using [`interruptible`](../yaml/index.md#interruptible)
if a new pipeline runs for the same ref in the upstream pipeline. They can be
automatically canceled if a new pipeline is triggered for the same ref on the downstream project.
- Multi-project pipelines are standalone pipelines because they are normal pipelines
that happened to be triggered by an external project. They are all visible on the pipeline index page.
- Are independent, so there are no nesting limits.
## Configure a pipeline
Pipelines and their component jobs and stages are defined in the CI/CD pipeline configuration file for each project.
@ -456,25 +422,6 @@ Pipeline analytics are available on the [**CI/CD Analytics** page](../../user/an
Pipeline status and test coverage report badges are available and configurable for each project.
For information on adding pipeline badges to projects, see [Pipeline badges](settings.md#pipeline-badges).
### Downstream pipelines
In the pipeline graph view, downstream pipelines ([Multi-project pipelines](multi_project_pipelines.md)
and [Parent-child pipelines](parent_child_pipelines.md)) display as a list of cards
on the right of the graph.
#### Cancel or retry downstream pipelines from the graph view
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/354974) in GitLab 15.0 [with a flag](../../administration/feature_flags.md) named `downstream_retry_action`. Disabled by default.
> - [Generally available and feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/357406) in GitLab 15.1.
To cancel a downstream pipeline that is still running, select **Cancel** (**{cancel}**)
on the pipeline's card.
To retry a failed downstream pipeline, select **Retry** (**{retry}**)
on the pipeline's card.
![downstream pipeline actions](img/downstream_pipeline_actions.png)
## Pipelines API
GitLab provides API endpoints to:

View File

@ -415,5 +415,5 @@ displays to the right of the mini graph.
If you have permission to trigger pipelines in the downstream project, you can
retry or cancel multi-project pipelines:
- [In the main graph view](index.md#downstream-pipelines).
- [In the main graph view](downstream_pipelines.md#view-a-downstream-pipeline).
- From the downstream pipeline's details page.

View File

@ -224,5 +224,5 @@ multi-project pipelines:
You can retry or cancel child pipelines:
- [In the main graph view](index.md#downstream-pipelines).
- [In the main graph view](downstream_pipelines.md#view-a-downstream-pipeline).
- In the child pipeline's details page.

View File

@ -89,7 +89,7 @@ the paths for the apps that you would like to use in your cluster.
By default, each `helmfile.yaml` in these sub-paths has the attribute `installed: true`. This means that every time
the pipeline runs, Helmfile tries to either install or update your apps according to the current state of your
cluster and Helm releases. If you change this attribute to `installed: false`, Helmfile tries try to uninstall this app
from your cluster. [Read more](https://github.com/roboll/helmfile) about how Helmfile works.
from your cluster. [Read more](https://helmfile.readthedocs.io/en/latest/) about how Helmfile works.
### Built-in applications

View File

@ -8392,6 +8392,12 @@ msgstr ""
msgid "Cloud Run"
msgstr ""
msgid "Cloud SQL instances are fully managed, relational MySQL databases. Google handles replication, patch management, and database management to ensure availability and performance."
msgstr ""
msgid "Cloud SQL instances are fully managed, relational SQL Server databases. Google handles replication, patch management, and database management to ensure availability and performance."
msgstr ""
msgid "Cloud Storage"
msgstr ""
@ -8419,12 +8425,24 @@ msgstr ""
msgid "CloudSeed|Cloud SQL for SQL Server"
msgstr ""
msgid "CloudSeed|Cloud SQL instance creation request successful. Expected resolution time is ~5 minutes."
msgstr ""
msgid "CloudSeed|Cloud SQL instances are fully managed, relational PostgreSQL databases. Google handles replication, patch management, and database management to ensure availability and performance."
msgstr ""
msgid "CloudSeed|CloudSQL Instance"
msgstr ""
msgid "CloudSeed|Configuration"
msgstr ""
msgid "CloudSeed|Create MySQL Instance"
msgstr ""
msgid "CloudSeed|Create Postgres Instance"
msgstr ""
msgid "CloudSeed|Create cluster"
msgstr ""
@ -8479,6 +8497,9 @@ msgstr ""
msgid "CloudSeed|Generated database instance is linked to the selected branch or tag"
msgstr ""
msgid "CloudSeed|Google Cloud Error - %{message}"
msgstr ""
msgid "CloudSeed|Google Cloud Project"
msgstr ""
@ -40354,6 +40375,9 @@ msgstr ""
msgid "This project is not subscribed to any project pipelines."
msgstr ""
msgid "This project is public. Non-members can guess the Service Desk email address, because it contains the group and project name. %{linkStart}How do I create a custom email address?%{linkEnd}"
msgstr ""
msgid "This project manages its dependencies using %{strong_start}%{manager_name}%{strong_end}"
msgstr ""

View File

@ -150,7 +150,6 @@
"popper.js": "^1.16.1",
"portal-vue": "^2.1.7",
"postcss": "8.4.14",
"prismjs": "^1.21.0",
"prosemirror-markdown": "1.9.1",
"prosemirror-model": "^1.18.1",
"prosemirror-state": "^1.4.1",
@ -178,13 +177,13 @@
"url-loader": "^4.1.1",
"uuid": "8.1.0",
"visibilityjs": "^1.2.4",
"vue": "^2.7.8",
"vue": "^2.7.9",
"vue-apollo": "^3.0.7",
"vue-loader": "^15.10",
"vue-observe-visibility": "^1.0.0",
"vue-resize": "^1.0.1",
"vue-router": "3.4.9",
"vue-template-compiler": "^2.7.8",
"vue-template-compiler": "^2.7.9",
"vue-virtual-scroll-list": "^1.4.7",
"vuedraggable": "^2.23.0",
"vuex": "^3.6.2",

View File

@ -32,7 +32,17 @@ RSpec.describe DeploymentsFinder do
it 'raises an error' do
expect { subject }.to raise_error(
described_class::InefficientQueryError,
'`finished_at` filter and `finished_at` sorting must be paired')
'`finished_at` filter requires `finished_at` sort.')
end
end
context 'when running status filter and finished_at sorting' do
let(:params) { { status: :running, order_by: :finished_at } }
it 'raises an error' do
expect { subject }.to raise_error(
described_class::InefficientQueryError,
'`finished_at` sort requires `finished_at` filter or a filter with at least one of the finished statuses.')
end
end
@ -52,7 +62,17 @@ RSpec.describe DeploymentsFinder do
it 'raises an error' do
expect { subject }.to raise_error(
described_class::InefficientQueryError,
'`environment` filter must be combined with `project` scope.')
'`environment` name filter must be combined with `project` scope.')
end
end
context 'when status filter with mixed finished and upcoming statuses' do
let(:params) { { status: [:success, :running] } }
it 'raises an error' do
expect { subject }.to raise_error(
described_class::InefficientQueryError,
'finished statuses and upcoming statuses must be separately queried.')
end
end
end
@ -103,6 +123,24 @@ RSpec.describe DeploymentsFinder do
end
end
context 'when the environment ID is specified' do
let!(:environment1) { create(:environment, project: project) }
let!(:environment2) { create(:environment, project: project) }
let!(:deployment1) do
create(:deployment, project: project, environment: environment1)
end
let!(:deployment2) do
create(:deployment, project: project, environment: environment2)
end
let(:params) { { environment: environment1.id } }
it 'returns deployments for the given environment' do
is_expected.to match_array([deployment1])
end
end
context 'when the deployment status is specified' do
let!(:deployment1) { create(:deployment, :success, project: project) }
let!(:deployment2) { create(:deployment, :failed, project: project) }

View File

@ -2,6 +2,8 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import Panel from '~/google_cloud/databases/panel.vue';
import IncubationBanner from '~/google_cloud/components/incubation_banner.vue';
import GoogleCloudMenu from '~/google_cloud/components/google_cloud_menu.vue';
import ServiceTable from '~/google_cloud/databases/service_table.vue';
import InstanceTable from '~/google_cloud/databases/cloudsql/instance_table.vue';
describe('google_cloud/databases/panel', () => {
let wrapper;
@ -10,6 +12,11 @@ describe('google_cloud/databases/panel', () => {
configurationUrl: 'configuration-url',
deploymentsUrl: 'deployments-url',
databasesUrl: 'databases-url',
cloudsqlPostgresUrl: 'cloudsql-postgres-url',
cloudsqlMysqlUrl: 'cloudsql-mysql-url',
cloudsqlSqlserverUrl: 'cloudsql-sqlserver-url',
cloudsqlInstances: [],
emptyIllustrationUrl: 'empty-illustration-url',
};
beforeEach(() => {
@ -33,4 +40,14 @@ describe('google_cloud/databases/panel', () => {
expect(target.props('deploymentsUrl')).toBe(props.deploymentsUrl);
expect(target.props('databasesUrl')).toBe(props.databasesUrl);
});
it('contains Databases service table', () => {
const target = wrapper.findComponent(ServiceTable);
expect(target.exists()).toBe(true);
});
it('contains CloudSQL instance table', () => {
const target = wrapper.findComponent(InstanceTable);
expect(target.exists()).toBe(true);
});
});

View File

@ -11,7 +11,7 @@ describe('Notebook component', () => {
function buildComponent(notebook) {
return mount(Component, {
propsData: { notebook, codeCssClass: 'js-code-class' },
propsData: { notebook },
provide: { relativeRawPath: '' },
}).vm;
}
@ -46,10 +46,6 @@ describe('Notebook component', () => {
it('renders code cell', () => {
expect(vm.$el.querySelector('pre')).not.toBeNull();
});
it('add code class to code blocks', () => {
expect(vm.$el.querySelector('.js-code-class')).not.toBeNull();
});
});
describe('with worksheets', () => {
@ -72,9 +68,5 @@ describe('Notebook component', () => {
it('renders code cell', () => {
expect(vm.$el.querySelector('pre')).not.toBeNull();
});
it('add code class to code blocks', () => {
expect(vm.$el.querySelector('.js-code-class')).not.toBeNull();
});
});
});

View File

@ -1,15 +0,0 @@
import Prism from '~/notebook/lib/highlight';
describe('Highlight library', () => {
it('imports python language', () => {
expect(Prism.languages.python).toBeDefined();
});
it('uses custom CSS classes', () => {
const el = document.createElement('div');
el.innerHTML = Prism.highlight('console.log("a");', Prism.languages.javascript);
expect(el.querySelector('.string')).not.toBeNull();
expect(el.querySelector('.function')).not.toBeNull();
});
});

View File

@ -1,4 +1,4 @@
import { GlAlert } from '@gitlab/ui';
import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
@ -23,11 +23,16 @@ describe('ServiceDeskRoot', () => {
selectedTemplate: 'Bug',
selectedFileTemplateProjectId: 42,
templates: ['Bug', 'Documentation'],
publicProject: false,
};
const getAlertText = () => wrapper.find(GlAlert).text();
const createComponent = () => shallowMount(ServiceDeskRoot, { provide: provideData });
const createComponent = (customInject = {}) =>
shallowMount(ServiceDeskRoot, {
provide: { ...provideData, ...customInject },
stubs: { GlSprintf },
});
beforeEach(() => {
axiosMock = new AxiosMockAdapter(axios);
@ -60,6 +65,25 @@ describe('ServiceDeskRoot', () => {
});
});
it('shows alert about email inference when current project is public', () => {
wrapper = createComponent({
publicProject: true,
});
const alertEl = wrapper.find('[data-testid="public-project-alert"]');
expect(alertEl.exists()).toBe(true);
expect(alertEl.text()).toContain(
'This project is public. Non-members can guess the Service Desk email address, because it contains the group and project name.',
);
const alertBodyLink = alertEl.findComponent(GlLink);
expect(alertBodyLink.exists()).toBe(true);
expect(alertBodyLink.attributes('href')).toBe(
'/help/user/project/service_desk.html#using-a-custom-email-address',
);
expect(alertBodyLink.text()).toBe('How do I create a custom email address?');
});
describe('toggle event', () => {
describe('when toggling service desk on', () => {
beforeEach(async () => {

View File

@ -197,5 +197,43 @@ describe('MR Widget', () => {
expect(findToggleButton().exists()).toBe(false);
});
it('fetches expanded data when clicked for the first time', async () => {
const mockDataCollapsed = {
headers: {},
status: 200,
data: { vulnerabilities: [{ vuln: 1 }] },
};
const mockDataExpanded = {
headers: {},
status: 200,
data: { vulnerabilities: [{ vuln: 2 }] },
};
createComponent({
propsData: {
isCollapsible: true,
fetchCollapsedData: () => Promise.resolve(mockDataCollapsed),
fetchExpandedData: () => Promise.resolve(mockDataExpanded),
},
});
findToggleButton().vm.$emit('click');
await waitForPromises();
// First fetches the collapsed data
expect(wrapper.emitted('input')[0][0]).toEqual({
collapsed: mockDataCollapsed.data,
expanded: null,
});
// Then fetches the expanded data
expect(wrapper.emitted('input')[1][0]).toEqual({
collapsed: null,
expanded: mockDataExpanded.data,
});
});
});
});

View File

@ -1,26 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Code Block with default props renders correctly 1`] = `
<pre
class="code-block rounded code"
>
<code
class="d-block"
>
test-code
</code>
</pre>
`;
exports[`Code Block with maxHeight set to "200px" renders correctly 1`] = `
<pre
class="code-block rounded code"
style="max-height: 200px; overflow-y: auto;"
>
<code
class="d-block"
>
test-code
</code>
</pre>
`;

View File

@ -0,0 +1,65 @@
import { shallowMount } from '@vue/test-utils';
import CodeBlock from '~/vue_shared/components/code_block_highlighted.vue';
import waitForPromises from 'helpers/wait_for_promises';
describe('Code Block Highlighted', () => {
let wrapper;
const code = 'const foo = 1;';
const createComponent = (propsData = {}) => {
wrapper = shallowMount(CodeBlock, { propsData });
};
afterEach(() => {
wrapper.destroy();
});
it('renders highlighted code if language is supported', async () => {
createComponent({ code, language: 'javascript' });
await waitForPromises();
expect(wrapper.element).toMatchInlineSnapshot(`
<code-block-stub
class="highlight"
code=""
maxheight="initial"
>
<span>
<span
class="hljs-keyword"
>
const
</span>
foo =
<span
class="hljs-number"
>
1
</span>
;
</span>
</code-block-stub>
`);
});
it("renders plain text if language isn't supported", async () => {
createComponent({ code, language: 'foobar' });
await waitForPromises();
expect(wrapper.emitted('error')).toEqual([[expect.any(TypeError)]]);
expect(wrapper.element).toMatchInlineSnapshot(`
<code-block-stub
class="highlight"
code=""
maxheight="initial"
>
<span>
const foo = 1;
</span>
</code-block-stub>
`);
});
});

View File

@ -4,41 +4,77 @@ import CodeBlock from '~/vue_shared/components/code_block.vue';
describe('Code Block', () => {
let wrapper;
const defaultProps = {
code: 'test-code',
};
const code = 'test-code';
const createComponent = (props = {}) => {
const createComponent = (propsData, slots = {}) => {
wrapper = shallowMount(CodeBlock, {
propsData: {
...defaultProps,
...props,
},
slots,
propsData,
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('with default props', () => {
beforeEach(() => {
createComponent();
});
it('overwrites the default slot', () => {
createComponent({}, { default: 'DEFAULT SLOT' });
it('renders correctly', () => {
expect(wrapper.element).toMatchSnapshot();
});
expect(wrapper.element).toMatchInlineSnapshot(`
<pre
class="code-block rounded code"
>
DEFAULT SLOT
</pre>
`);
});
describe('with maxHeight set to "200px"', () => {
beforeEach(() => {
createComponent({ maxHeight: '200px' });
});
it('renders with empty code prop', () => {
createComponent({});
it('renders correctly', () => {
expect(wrapper.element).toMatchSnapshot();
});
expect(wrapper.element).toMatchInlineSnapshot(`
<pre
class="code-block rounded code"
>
<code
class="d-block"
>
</code>
</pre>
`);
});
it('renders code prop when provided', () => {
createComponent({ code });
expect(wrapper.element).toMatchInlineSnapshot(`
<pre
class="code-block rounded code"
>
<code
class="d-block"
>
test-code
</code>
</pre>
`);
});
it('sets maxHeight properly when provided', () => {
createComponent({ code, maxHeight: '200px' });
expect(wrapper.element).toMatchInlineSnapshot(`
<pre
class="code-block rounded code"
style="max-height: 200px; overflow-y: auto;"
>
<code
class="d-block"
>
test-code
</code>
</pre>
`);
});
});

View File

@ -0,0 +1,28 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Resolvers::DeploymentResolver do
include GraphqlHelpers
let_it_be(:project) { create(:project, :repository, :private) }
let_it_be(:environment) { create(:environment, project: project) }
let_it_be(:deployment) { create(:deployment, :created, environment: environment, project: project) }
let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } }
let(:current_user) { developer }
describe '#resolve' do
it 'finds the deployment' do
expect(resolve_deployments(iid: deployment.iid)).to contain_exactly(deployment)
end
it 'does not find the deployment if the IID does not match' do
expect(resolve_deployments(iid: non_existing_record_id)).to be_empty
end
end
def resolve_deployments(args = {}, context = { current_user: current_user })
resolve(described_class, obj: project, args: args, ctx: context)
end
end

View File

@ -0,0 +1,41 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Resolvers::DeploymentsResolver do
include GraphqlHelpers
let_it_be(:project) { create(:project, :repository, :private) }
let_it_be(:environment) { create(:environment, project: project) }
let_it_be(:deployment) { create(:deployment, :created, environment: environment, project: project) }
let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } }
let(:current_user) { developer }
describe '#resolve' do
it 'finds the deployment' do
expect(resolve_deployments).to contain_exactly(deployment)
end
it 'finds the deployment when status matches' do
expect(resolve_deployments(statuses: [:created])).to contain_exactly(deployment)
end
it 'does not find the deployment when status does not match' do
expect(resolve_deployments(statuses: [:success])).to be_empty
end
it 'transforms order_by for finder' do
expect(DeploymentsFinder)
.to receive(:new)
.with(environment: environment.id, status: ['success'], order_by: 'finished_at', sort: 'asc')
.and_call_original
resolve_deployments(statuses: [:success], order_by: { finished_at: :asc })
end
end
def resolve_deployments(args = {}, context = { current_user: current_user })
resolve(described_class, obj: environment, args: args, ctx: context)
end
end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['DeploymentDetails'] do
specify { expect(described_class.graphql_name).to eq('DeploymentDetails') }
it 'has the expected fields' do
expected_fields = %w[
id iid ref tag sha created_at updated_at finished_at status
]
expect(described_class).to have_graphql_fields(*expected_fields)
end
specify { expect(described_class).to require_graphql_authorizations(:read_deployment) }
end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['Deployment'] do
specify { expect(described_class.graphql_name).to eq('Deployment') }
it 'has the expected fields' do
expected_fields = %w[
id iid ref tag sha created_at updated_at finished_at status
]
expect(described_class).to have_graphql_fields(*expected_fields)
end
specify { expect(described_class).to require_graphql_authorizations(:read_deployment) }
end

View File

@ -7,7 +7,7 @@ RSpec.describe GitlabSchema.types['Environment'] do
it 'has the expected fields' do
expected_fields = %w[
name id state metrics_dashboard latest_opened_most_severe_alert path
name id state metrics_dashboard latest_opened_most_severe_alert path deployments
]
expect(described_class).to have_graphql_fields(*expected_fields)

View File

@ -0,0 +1,25 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Projects::GoogleCloud::CloudsqlHelper do
describe '#TIERS' do
it 'is an array' do
expect(described_class::TIERS).to be_an_instance_of(Array)
end
end
describe '#VERSIONS' do
it 'returns versions for :postgres' do
expect(described_class::VERSIONS[:postgres]).to be_an_instance_of(Array)
end
it 'returns versions for :mysql' do
expect(described_class::VERSIONS[:mysql]).to be_an_instance_of(Array)
end
it 'returns versions for :sqlserver' do
expect(described_class::VERSIONS[:sqlserver]).to be_an_instance_of(Array)
end
end
end

View File

@ -74,6 +74,27 @@ RSpec.describe Deployment do
end
end
describe '.for_iid' do
subject { described_class.for_iid(project, iid) }
let_it_be(:project) { create(:project, :repository) }
let_it_be(:deployment) { create(:deployment, project: project) }
let(:iid) { deployment.iid }
it 'finds the deployment' do
is_expected.to contain_exactly(deployment)
end
context 'when iid does not match' do
let(:iid) { non_existing_record_id }
it 'does not find the deployment' do
is_expected.to be_empty
end
end
end
describe '.for_environment_name' do
subject { described_class.for_environment_name(project, environment_name) }

View File

@ -0,0 +1,345 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Environments Deployments query' do
include GraphqlHelpers
let_it_be(:project) { create(:project, :private, :repository) }
let_it_be(:environment) { create(:environment, project: project) }
let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } }
let_it_be(:guest) { create(:user).tap { |u| project.add_guest(u) } }
let(:user) { developer }
subject { GitlabSchema.execute(query, context: { current_user: user }).as_json }
context 'when there are deployments in the environment' do
let_it_be(:finished_deployment_old) do
create(:deployment, :success, environment: environment, project: project, finished_at: 2.days.ago)
end
let_it_be(:finished_deployment_new) do
create(:deployment, :success, environment: environment, project: project, finished_at: 1.day.ago)
end
let_it_be(:upcoming_deployment_old) do
create(:deployment, :created, environment: environment, project: project, created_at: 2.hours.ago)
end
let_it_be(:upcoming_deployment_new) do
create(:deployment, :created, environment: environment, project: project, created_at: 1.hour.ago)
end
let_it_be(:other_environment) { create(:environment, project: project) }
let_it_be(:other_deployment) { create(:deployment, :success, environment: other_environment, project: project) }
let(:query) do
%(
query {
project(fullPath: "#{project.full_path}") {
environment(name: "#{environment.name}") {
deployments {
nodes {
id
iid
ref
tag
sha
createdAt
updatedAt
finishedAt
status
}
}
}
}
}
)
end
it 'returns all deployments of the environment' do
deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes')
expect(deployments.count).to eq(4)
end
context 'when query last deployment' do
let(:query) do
%(
query {
project(fullPath: "#{project.full_path}") {
environment(name: "#{environment.name}") {
deployments(statuses: [SUCCESS], orderBy: { finishedAt: DESC }, first: 1) {
nodes {
iid
}
}
}
}
}
)
end
it 'returns deployment' do
deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes')
expect(deployments.count).to eq(1)
expect(deployments[0]['iid']).to eq(finished_deployment_new.iid.to_s)
end
end
context 'when query latest upcoming deployment' do
let(:query) do
%(
query {
project(fullPath: "#{project.full_path}") {
environment(name: "#{environment.name}") {
deployments(statuses: [CREATED RUNNING BLOCKED], orderBy: { createdAt: DESC }, first: 1) {
nodes {
iid
}
}
}
}
}
)
end
it 'returns deployment' do
deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes')
expect(deployments.count).to eq(1)
expect(deployments[0]['iid']).to eq(upcoming_deployment_new.iid.to_s)
end
end
context 'when query finished deployments in descending order' do
let(:query) do
%(
query {
project(fullPath: "#{project.full_path}") {
environment(name: "#{environment.name}") {
deployments(statuses: [SUCCESS FAILED CANCELED], orderBy: { finishedAt: DESC }) {
nodes {
iid
}
}
}
}
}
)
end
it 'returns deployments' do
deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes')
expect(deployments.count).to eq(2)
expect(deployments[0]['iid']).to eq(finished_deployment_new.iid.to_s)
expect(deployments[1]['iid']).to eq(finished_deployment_old.iid.to_s)
end
end
context 'when query finished deployments in ascending order' do
let(:query) do
%(
query {
project(fullPath: "#{project.full_path}") {
environment(name: "#{environment.name}") {
deployments(statuses: [SUCCESS FAILED CANCELED], orderBy: { finishedAt: ASC }) {
nodes {
iid
}
}
}
}
}
)
end
it 'returns deployments' do
deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes')
expect(deployments.count).to eq(2)
expect(deployments[0]['iid']).to eq(finished_deployment_old.iid.to_s)
expect(deployments[1]['iid']).to eq(finished_deployment_new.iid.to_s)
end
end
context 'when query upcoming deployments in descending order' do
let(:query) do
%(
query {
project(fullPath: "#{project.full_path}") {
environment(name: "#{environment.name}") {
deployments(statuses: [CREATED RUNNING BLOCKED], orderBy: { createdAt: DESC }) {
nodes {
iid
}
}
}
}
}
)
end
it 'returns deployments' do
deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes')
expect(deployments.count).to eq(2)
expect(deployments[0]['iid']).to eq(upcoming_deployment_new.iid.to_s)
expect(deployments[1]['iid']).to eq(upcoming_deployment_old.iid.to_s)
end
end
context 'when query upcoming deployments in ascending order' do
let(:query) do
%(
query {
project(fullPath: "#{project.full_path}") {
environment(name: "#{environment.name}") {
deployments(statuses: [CREATED RUNNING BLOCKED], orderBy: { createdAt: ASC }) {
nodes {
iid
}
}
}
}
}
)
end
it 'returns deployments' do
deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes')
expect(deployments.count).to eq(2)
expect(deployments[0]['iid']).to eq(upcoming_deployment_old.iid.to_s)
expect(deployments[1]['iid']).to eq(upcoming_deployment_new.iid.to_s)
end
end
context 'when query last deployments of multiple environments' do
let(:query) do
%(
query {
project(fullPath: "#{project.full_path}") {
environments {
nodes {
name
deployments(statuses: [SUCCESS], orderBy: { finishedAt: DESC }, first: 1) {
nodes {
iid
}
}
}
}
}
}
)
end
it 'returnes an error for preventing N+1 queries' do
expect(subject['errors'][0]['message']).to include('exceeds max complexity')
end
end
context 'when query finished and upcoming deployments together' do
let(:query) do
%(
query {
project(fullPath: "#{project.full_path}") {
environment(name: "#{environment.name}") {
deployments(statuses: [CREATED SUCCESS]) {
nodes {
iid
}
}
}
}
}
)
end
it 'raises an error' do
expect { subject }.to raise_error(DeploymentsFinder::InefficientQueryError)
end
end
context 'when multiple orderBy input are specified' do
let(:query) do
%(
query {
project(fullPath: "#{project.full_path}") {
environment(name: "#{environment.name}") {
deployments(orderBy: { finishedAt: DESC, createdAt: ASC }) {
nodes {
iid
}
}
}
}
}
)
end
it 'raises an error' do
expect(subject['errors'][0]['message']).to include('orderBy parameter must contain one key-value pair.')
end
end
context 'when user is guest' do
let(:user) { guest }
it 'returns nothing' do
expect(subject['data']['project']['environment']).to be_nil
end
end
describe 'sorting and pagination' do
let(:data_path) { [:project, :environment, :deployments] }
let(:current_user) { user }
def pagination_query(params)
%(
query {
project(fullPath: "#{project.full_path}") {
environment(name: "#{environment.name}") {
deployments(statuses: [SUCCESS], #{params}) {
nodes {
iid
}
pageInfo {
startCursor
endCursor
hasNextPage
hasPreviousPage
}
}
}
}
}
)
end
def pagination_results_data(nodes)
nodes.map { |deployment| deployment['iid'].to_i }
end
context 'when sorting by finished_at in ascending order' do
it_behaves_like 'sorted paginated query' do
let(:sort_argument) { graphql_args(orderBy: { finishedAt: :ASC }) }
let(:first_param) { 2 }
let(:all_records) { [finished_deployment_old.iid, finished_deployment_new.iid] }
end
end
context 'when sorting by finished_at in descending order' do
it_behaves_like 'sorted paginated query' do
let(:sort_argument) { graphql_args(orderBy: { finishedAt: :DESC }) }
let(:first_param) { 2 }
let(:all_records) { [finished_deployment_new.iid, finished_deployment_old.iid] }
end
end
end
end
end

View File

@ -0,0 +1,51 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Project Deployment query' do
let_it_be(:project) { create(:project, :private, :repository) }
let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } }
let_it_be(:guest) { create(:user).tap { |u| project.add_guest(u) } }
let_it_be(:environment) { create(:environment, project: project) }
let_it_be(:deployment) { create(:deployment, environment: environment, project: project) }
subject { GitlabSchema.execute(query, context: { current_user: user }).as_json }
let(:user) { developer }
let(:query) do
%(
query {
project(fullPath: "#{project.full_path}") {
deployment(iid: #{deployment.iid}) {
id
iid
ref
tag
sha
createdAt
updatedAt
finishedAt
status
}
}
}
)
end
it 'returns the deployment of the project' do
deployment_data = subject.dig('data', 'project', 'deployment')
expect(deployment_data['iid']).to eq(deployment.iid.to_s)
end
context 'when user is guest' do
let(:user) { guest }
it 'returns nothing' do
deployment_data = subject.dig('data', 'project', 'deployment')
expect(deployment_data).to be_nil
end
end
end

View File

@ -2,9 +2,6 @@
require 'spec_helper'
# Mock Types
MockGoogleOAuth2Credentials = Struct.new(:app_id, :app_secret)
RSpec.describe Projects::GoogleCloud::ConfigurationController do
let_it_be(:project) { create(:project, :public) }
let_it_be(:url) { project_google_cloud_configuration_path(project) }
@ -56,7 +53,7 @@ RSpec.describe Projects::GoogleCloud::ConfigurationController do
context 'but gitlab instance is not configured for google oauth2' do
it 'returns forbidden' do
unconfigured_google_oauth2 = MockGoogleOAuth2Credentials.new('', '')
unconfigured_google_oauth2 = Struct.new(:app_id, :app_secret).new('', '')
allow(Gitlab::Auth::OAuth::Provider).to receive(:config_for)
.with('google_oauth2')
.and_return(unconfigured_google_oauth2)

View File

@ -2,133 +2,169 @@
require 'spec_helper'
# Mock Types
MockGoogleOAuth2Credentials = Struct.new(:app_id, :app_secret)
RSpec.describe Projects::GoogleCloud::DatabasesController, :snowplow do
shared_examples 'shared examples for database controller endpoints' do
include_examples 'requires `admin_project_google_cloud` role'
RSpec.describe Projects::GoogleCloud::DatabasesController do
let_it_be(:project) { create(:project, :public) }
let_it_be(:url) { project_google_cloud_databases_path(project) }
include_examples 'requires feature flag `incubation_5mp_google_cloud` enabled'
let_it_be(:user_guest) { create(:user) }
let_it_be(:user_developer) { create(:user) }
let_it_be(:user_maintainer) { create(:user) }
include_examples 'requires valid Google OAuth2 configuration'
let_it_be(:unauthorized_members) { [user_guest, user_developer] }
let_it_be(:authorized_members) { [user_maintainer] }
before do
project.add_guest(user_guest)
project.add_developer(user_developer)
project.add_maintainer(user_maintainer)
include_examples 'requires valid Google Oauth2 token' do
let_it_be(:mock_gcp_projects) { [{}, {}, {}] }
let_it_be(:mock_branches) { [] }
let_it_be(:mock_tags) { [] }
end
end
context 'when accessed by unauthorized members' do
it 'returns not found on GET request' do
unauthorized_members.each do |unauthorized_member|
sign_in(unauthorized_member)
context '-/google_cloud/databases' do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
let_it_be(:renders_template) { 'projects/google_cloud/databases/index' }
let_it_be(:redirects_to) { nil }
subject { get project_google_cloud_databases_path(project) }
include_examples 'shared examples for database controller endpoints'
end
context '-/google_cloud/databases/new/postgres' do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
let_it_be(:renders_template) { 'projects/google_cloud/databases/cloudsql_form' }
let_it_be(:redirects_to) { nil }
subject { get new_project_google_cloud_database_path(project, :postgres) }
include_examples 'shared examples for database controller endpoints'
end
context '-/google_cloud/databases/new/mysql' do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
let_it_be(:renders_template) { 'projects/google_cloud/databases/cloudsql_form' }
let_it_be(:redirects_to) { nil }
subject { get new_project_google_cloud_database_path(project, :mysql) }
include_examples 'shared examples for database controller endpoints'
end
context '-/google_cloud/databases/new/sqlserver' do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
let_it_be(:renders_template) { 'projects/google_cloud/databases/cloudsql_form' }
let_it_be(:redirects_to) { nil }
subject { get new_project_google_cloud_database_path(project, :sqlserver) }
include_examples 'shared examples for database controller endpoints'
end
context '-/google_cloud/databases/create' do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
let_it_be(:renders_template) { nil }
let_it_be(:redirects_to) { project_google_cloud_databases_path(project) }
subject { post project_google_cloud_databases_path(project) }
include_examples 'shared examples for database controller endpoints'
context 'when the request is valid' do
before do
project.add_maintainer(user)
sign_in(user)
allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |client|
allow(client).to receive(:validate_token).and_return(true)
allow(client).to receive(:list_projects).and_return(mock_gcp_projects)
end
allow_next_instance_of(BranchesFinder) do |finder|
allow(finder).to receive(:execute).and_return(mock_branches)
end
allow_next_instance_of(TagsFinder) do |finder|
allow(finder).to receive(:execute).and_return(mock_branches)
end
end
subject do
post project_google_cloud_databases_path(project)
end
it 'calls EnableCloudsqlService and redirects on error' do
expect_next_instance_of(::GoogleCloud::EnableCloudsqlService) do |service|
expect(service).to receive(:execute)
.and_return({ status: :error, message: 'error' })
end
subject
expect(response).to redirect_to(project_google_cloud_databases_path(project))
get url
expect_snowplow_event(
category: 'Projects::GoogleCloud',
action: 'admin_project_google_cloud!',
label: 'error_access_denied',
property: 'invalid_user',
action: 'databases#cloudsql_create',
label: 'error_enable_cloudsql_service',
extra: { status: :error, message: 'error' },
project: project,
user: unauthorized_member
user: user
)
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
context 'when accessed by authorized members' do
it 'returns successful' do
authorized_members.each do |authorized_member|
sign_in(authorized_member)
get url
expect(response).to be_successful
expect(response).to render_template('projects/google_cloud/databases/index')
end
end
context 'but gitlab instance is not configured for google oauth2' do
it 'returns forbidden' do
unconfigured_google_oauth2 = MockGoogleOAuth2Credentials.new('', '')
allow(Gitlab::Auth::OAuth::Provider).to receive(:config_for)
.with('google_oauth2')
.and_return(unconfigured_google_oauth2)
authorized_members.each do |authorized_member|
sign_in(authorized_member)
get url
expect(response).to have_gitlab_http_status(:forbidden)
expect_snowplow_event(
category: 'Projects::GoogleCloud',
action: 'google_oauth2_enabled!',
label: 'error_access_denied',
extra: { reason: 'google_oauth2_not_configured',
config: unconfigured_google_oauth2 },
project: project,
user: authorized_member
)
end
end
end
context 'but feature flag is disabled' do
before do
stub_feature_flags(incubation_5mp_google_cloud: false)
end
it 'returns not found' do
authorized_members.each do |authorized_member|
sign_in(authorized_member)
get url
expect(response).to have_gitlab_http_status(:not_found)
expect_snowplow_event(
category: 'Projects::GoogleCloud',
action: 'feature_flag_enabled!',
label: 'error_access_denied',
property: 'feature_flag_not_enabled',
project: project,
user: authorized_member
)
end
end
end
context 'but google oauth2 token is not valid' do
it 'does not return revoke oauth url' do
allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |client|
allow(client).to receive(:validate_token).and_return(false)
context 'when EnableCloudsqlService is successful' do
before do
allow_next_instance_of(::GoogleCloud::EnableCloudsqlService) do |service|
allow(service).to receive(:execute)
.and_return({ status: :success, message: 'success' })
end
end
authorized_members.each do |authorized_member|
sign_in(authorized_member)
it 'calls CreateCloudsqlInstanceService and redirects on error' do
expect_next_instance_of(::GoogleCloud::CreateCloudsqlInstanceService) do |service|
expect(service).to receive(:execute)
.and_return({ status: :error, message: 'error' })
end
get url
subject
expect(response).to redirect_to(project_google_cloud_databases_path(project))
expect(response).to be_successful
expect_snowplow_event(
category: 'Projects::GoogleCloud',
action: 'databases#index',
label: 'success',
extra: {
configurationUrl: project_google_cloud_configuration_path(project),
deploymentsUrl: project_google_cloud_deployments_path(project),
databasesUrl: project_google_cloud_databases_path(project)
},
action: 'databases#cloudsql_create',
label: 'error_create_cloudsql_instance',
extra: { status: :error, message: 'error' },
project: project,
user: authorized_member
user: user
)
end
context 'when CreateCloudsqlInstanceService is successful' do
before do
allow_next_instance_of(::GoogleCloud::CreateCloudsqlInstanceService) do |service|
allow(service).to receive(:execute)
.and_return({ status: :success, message: 'success' })
end
end
it 'redirects as expected' do
subject
expect(response).to redirect_to(project_google_cloud_databases_path(project))
expect_snowplow_event(
category: 'Projects::GoogleCloud',
action: 'databases#cloudsql_create',
label: 'success',
extra: nil,
project: project,
user: user
)
end
end
end
end
end

View File

@ -23,6 +23,11 @@ RSpec.describe GoogleCloud::EnableCloudsqlService do
project.save!
end
after do
project.variables.destroy_all # rubocop:disable Cop/DestroyAll
project.save!
end
it 'enables cloudsql, compute and service networking Google APIs', :aggregate_failures do
expect_next_instance_of(GoogleApi::CloudPlatform::Client) do |instance|
expect(instance).to receive(:enable_cloud_sql_admin).with('prj-prod')
@ -35,5 +40,22 @@ RSpec.describe GoogleCloud::EnableCloudsqlService do
expect(result[:status]).to eq(:success)
end
context 'when Google APIs raise an error' do
it 'returns error result' do
allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |instance|
allow(instance).to receive(:enable_cloud_sql_admin).with('prj-prod')
allow(instance).to receive(:enable_compute).with('prj-prod')
allow(instance).to receive(:enable_service_networking).with('prj-prod')
allow(instance).to receive(:enable_cloud_sql_admin).with('prj-staging')
allow(instance).to receive(:enable_compute).with('prj-staging')
allow(instance).to receive(:enable_service_networking).with('prj-staging')
.and_raise(Google::Apis::Error.new('error'))
end
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq('error')
end
end
end
end

View File

@ -0,0 +1,18 @@
# frozen_string_literal: true
RSpec.shared_examples 'requires feature flag `incubation_5mp_google_cloud` enabled' do
context 'when feature flag is disabled' do
before do
project.add_maintainer(user)
stub_feature_flags(incubation_5mp_google_cloud: false)
end
it 'renders not found' do
sign_in(user)
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
end

View File

@ -0,0 +1,55 @@
# frozen_string_literal: true
RSpec.shared_examples 'requires `admin_project_google_cloud` role' do
shared_examples 'returns not_found' do
it 'returns not found' do
sign_in(user)
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
shared_examples 'redirects to authorize url' do
it 'redirects to authorize url' do
sign_in(user)
subject
expect(response).to redirect_to(assigns(:authorize_url))
end
end
context 'when requested by users with different roles' do
let_it_be(:guest) { create(:user) }
let_it_be(:developer) { create(:user) }
let_it_be(:maintainer) { create(:user) }
before do
project.add_guest(guest)
project.add_developer(developer)
project.add_maintainer(maintainer)
end
context 'for unauthorized users' do
include_examples 'returns not_found' do
let(:user) { guest }
end
include_examples 'returns not_found' do
let(:user) { developer }
end
end
context 'for authorized users' do
include_examples 'redirects to authorize url' do
let(:user) { maintainer }
end
include_examples 'redirects to authorize url' do
let(:user) { project.owner }
end
end
end
end

View File

@ -0,0 +1,22 @@
# frozen_string_literal: true
RSpec.shared_examples 'requires valid Google OAuth2 configuration' do
context 'when GitLab instance does not have valid Google OAuth2 configuration ' do
before do
project.add_maintainer(user)
unconfigured_google_oauth2 = Struct.new(:app_id, :app_secret)
.new('', '')
allow(Gitlab::Auth::OAuth::Provider).to receive(:config_for)
.with('google_oauth2')
.and_return(unconfigured_google_oauth2)
end
it 'renders forbidden' do
sign_in(user)
subject
expect(response).to have_gitlab_http_status(:forbidden)
end
end
end

View File

@ -0,0 +1,47 @@
# frozen_string_literal: true
RSpec.shared_examples 'requires valid Google Oauth2 token' do
context 'when a valid Google OAuth2 token does not exist' do
before do
project.add_maintainer(user)
sign_in(user)
end
it 'triggers Google OAuth2 flow on request' do
subject
expect(response).to redirect_to(assigns(:authorize_url))
end
context 'and a valid Google OAuth2 token gets created' do
before do
allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |client|
allow(client).to receive(:validate_token).and_return(true)
allow(client).to receive(:list_projects).and_return(mock_gcp_projects) if mock_gcp_projects
end
allow_next_instance_of(BranchesFinder) do |finder|
allow(finder).to receive(:execute).and_return(mock_branches) if mock_branches
end
allow_next_instance_of(TagsFinder) do |finder|
allow(finder).to receive(:execute).and_return(mock_branches) if mock_branches
end
end
it 'renders template as expected' do
if renders_template
subject
expect(response).to render_template(renders_template)
end
end
it 'redirects as expected' do
if redirects_to
subject
expect(response).to redirect_to(redirects_to)
end
end
end
end
end

View File

@ -6,13 +6,16 @@ import translateMixin from '~/vue_shared/translate';
const stylesheetsRequireCtx = require.context(
'../../app/assets/stylesheets',
true,
/(application|application_utilities)\.scss$/,
/(application|application_utilities|highlight\/themes\/white)\.scss$/,
);
window.gon = {};
window.gon = {
user_color_scheme: 'white',
};
translateMixin(Vue);
stylesheetsRequireCtx('./application.scss');
stylesheetsRequireCtx('./application_utilities.scss');
stylesheetsRequireCtx('./highlight/themes/white.scss');
export const decorators = [withServer(createMockServer)];

View File

@ -2214,10 +2214,10 @@
semver "^6.3.0"
tsutils "^3.17.1"
"@vue/compiler-sfc@2.7.8":
version "2.7.8"
resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-2.7.8.tgz#731aadd6beafdb9c72fd8614ce189ac6cee87612"
integrity sha512-2DK4YWKfgLnW9VDR9gnju1gcYRk3flKj8UNsms7fsRmFcg35slVTZEkqwBtX+wJBXaamFfn6NxSsZh3h12Ix/Q==
"@vue/compiler-sfc@2.7.9":
version "2.7.9"
resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-2.7.9.tgz#aa31813c94de39f4977e4b924eb261c2199218ad"
integrity sha512-TD2FvT0fPUezw5RVP4tfwTZnKHP0QjeEUb39y7tORvOJQTjbOuHJEk4GPHUPsRaTeQ8rjuKjntyrYcEIx+ODxg==
dependencies:
"@babel/parser" "^7.18.4"
postcss "^8.4.14"
@ -3429,7 +3429,7 @@ clean-stack@^2.0.0:
resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b"
integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==
clipboard@^2.0.0, clipboard@^2.0.8:
clipboard@^2.0.8:
version "2.0.8"
resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-2.0.8.tgz#ffc6c103dd2967a83005f3f61976aa4655a4cdba"
integrity sha512-Y6WO0unAIQp5bLmk1zdThRhgJt/x3ks6f30s3oE3H1mgIEU33XyQjEf8gsf6DxC7NPX8Y1SsNWjUjL/ywLnnbQ==
@ -9731,13 +9731,6 @@ pretty@^2.0.0:
extend-shallow "^2.0.1"
js-beautify "^1.6.12"
prismjs@^1.21.0:
version "1.21.0"
resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.21.0.tgz#36c086ec36b45319ec4218ee164c110f9fc015a3"
integrity sha512-uGdSIu1nk3kej2iZsLyDoJ7e9bnPzIgY0naW/HdknGj61zScaprVEVGHrPoXqI+M9sP0NDnTK2jpkvmldpuqDw==
optionalDependencies:
clipboard "^2.0.0"
process-nextick-args@~2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
@ -12023,10 +12016,10 @@ vue-style-loader@^4.1.0:
hash-sum "^1.0.2"
loader-utils "^1.0.2"
vue-template-compiler@^2.7.8:
version "2.7.8"
resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.7.8.tgz#eadd54ed8fbff55b7deb07093a976c07f451a1dc"
integrity sha512-eQqdcUpJKJpBRPDdxCNsqUoT0edNvdt1jFjtVnVS/LPPmr0BU2jWzXlrf6BVMeODtdLewB3j8j3WjNiB+V+giw==
vue-template-compiler@^2.7.9:
version "2.7.9"
resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.7.9.tgz#ffbeb1769ae6af65cd405a6513df6b1e20e33616"
integrity sha512-NPJxt6OjVlzmkixYg0SdIN2Lw/rMryQskObY89uAMcM9flS/HrmLK5LaN1ReBTuhBgnYuagZZEkSS6FESATQUQ==
dependencies:
de-indent "^1.0.2"
he "^1.2.0"
@ -12041,12 +12034,12 @@ vue-virtual-scroll-list@^1.4.7:
resolved "https://registry.yarnpkg.com/vue-virtual-scroll-list/-/vue-virtual-scroll-list-1.4.7.tgz#12ee26833885f5bb4d37dc058085ccf3ce5b5a74"
integrity sha512-R8bk+k7WMGGoFQ9xF0krGCAlZhQjbJOkDUX+YZD2J+sHQWTzDtmTLS6kiIJToOHK1d/8QPGiD8fd9w0lDP4arg==
vue@^2.7.8:
version "2.7.8"
resolved "https://registry.yarnpkg.com/vue/-/vue-2.7.8.tgz#34e06553137611d8cecc4b0cd78b7a59f80b1299"
integrity sha512-ncwlZx5qOcn754bCu5/tS/IWPhXHopfit79cx+uIlLMyt3vCMGcXai5yCG5y+I6cDmEj4ukRYyZail9FTQh7lQ==
vue@^2.7.9:
version "2.7.9"
resolved "https://registry.yarnpkg.com/vue/-/vue-2.7.9.tgz#ac3ccb4a4ac5dd31a8eec4c68a094efe7f83dfbb"
integrity sha512-GeWCvAUkjzD5q4A3vgi8ka5r9bM6g8fmNmx/9VnHDKCaEzBcoVw+7UcQktZHrJ2jhlI+Zv8L57pMCIwM4h4MWg==
dependencies:
"@vue/compiler-sfc" "2.7.8"
"@vue/compiler-sfc" "2.7.9"
csstype "^3.1.0"
vuedraggable@^2.23.0: