Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-05-05 12:08:03 +00:00
parent 17ef30f3df
commit 3c86701bc8
183 changed files with 1036 additions and 7890 deletions

View File

@ -38,7 +38,7 @@
needs: ["setup-test-env", "retrieve-tests-metadata", "compile-test-assets", "detect-tests"]
script:
- !reference [.base-script, script]
- rspec_paralellized_job "--tag ~quarantine --tag ~geo --tag ~level:migration"
- rspec_paralellized_job "--tag ~quarantine --tag ~level:migration"
.base-artifacts:
artifacts:
@ -61,7 +61,7 @@
- .rails:rules:ee-and-foss-migration
script:
- !reference [.base-script, script]
- rspec_paralellized_job "--tag ~quarantine --tag ~geo --tag level:migration"
- rspec_paralellized_job "--tag ~quarantine --tag level:migration"
.rspec-base-pg11:
extends:
@ -123,33 +123,6 @@
- .rspec-base
- .use-pg13-ee
.rspec-ee-base-geo:
extends: .rspec-base
script:
- !reference [.base-script, script]
- rspec_paralellized_job "--tag ~quarantine --tag geo"
.rspec-ee-base-geo-pg11:
extends:
- .rspec-ee-base-geo
- .use-pg11-ee
.rspec-ee-base-geo-pg12:
extends:
- .rspec-ee-base-geo
- .use-pg12-ee
.rspec-jh-base-geo-pg12:
extends:
- .rspec-jh-base-pg12
script:
- !reference [.rspec-ee-base-geo, script]
.rspec-ee-base-geo-pg13:
extends:
- .rspec-ee-base-geo
- .use-pg13-ee
.db-job-base:
extends:
- .rails-job-base
@ -172,10 +145,7 @@
parallel: 22
.rspec-ee-unit-parallel:
parallel: 14
.rspec-ee-unit-geo-parallel:
parallel: 2
parallel: 16
.rspec-integration-parallel:
parallel: 10
@ -522,9 +492,6 @@ rspec:deprecations:
- rspec-ee unit pg12
- rspec-ee integration pg12
- rspec-ee system pg12
- rspec-ee unit pg12 geo
- rspec-ee integration pg12 geo
- rspec-ee system pg12 geo
variables:
SETUP_DB: "false"
script:
@ -576,14 +543,6 @@ rspec:coverage:
- rspec-ee unit pg12 single-db
- rspec-ee integration pg12 single-db
- rspec-ee system pg12 single-db
# Geo jobs
- rspec-ee unit pg12 geo
- rspec-ee integration pg12 geo
- rspec-ee system pg12 geo
# Geo minimal jobs
- rspec-ee unit pg12 geo minimal
- rspec-ee integration pg12 geo minimal
- rspec-ee system pg12 geo minimal
# Memory jobs
- memory-on-boot
# As-if-FOSS jobs
@ -878,40 +837,6 @@ rspec-ee system pg12 single-db:
- .single-db-rspec
- .rails:rules:single-db
rspec-ee unit pg12 geo:
extends:
- .rspec-ee-base-geo-pg12
- .rails:rules:ee-only-unit
- .rspec-ee-unit-geo-parallel
rspec-ee unit pg12 geo minimal:
extends:
- rspec-ee unit pg12 geo
- .minimal-rspec-tests
- .rails:rules:ee-only-unit:minimal
rspec-ee integration pg12 geo:
extends:
- .rspec-ee-base-geo-pg12
- .rails:rules:ee-only-integration
rspec-ee integration pg12 geo minimal:
extends:
- rspec-ee integration pg12 geo
- .minimal-rspec-tests
- .rails:rules:ee-only-integration:minimal
rspec-ee system pg12 geo:
extends:
- .rspec-ee-base-geo-pg12
- .rails:rules:ee-only-system
rspec-ee system pg12 geo minimal:
extends:
- rspec-ee system pg12 geo
- .minimal-rspec-tests
- .rails:rules:ee-only-system:minimal
rspec-ee migration pg12-as-if-jh:
extends:
- .rspec-jh-base-pg12
@ -937,22 +862,6 @@ rspec-ee system pg12-as-if-jh:
- .rails:rules:as-if-jh-rspec
- .rspec-ee-system-parallel
rspec-ee unit pg12-as-if-jh geo:
extends:
- .rspec-jh-base-geo-pg12
- .rails:rules:as-if-jh-rspec
- .rspec-ee-unit-geo-parallel
rspec-ee integration pg12-as-if-jh geo:
extends:
- .rspec-jh-base-geo-pg12
- .rails:rules:as-if-jh-rspec
rspec-ee system pg12-as-if-jh geo:
extends:
- .rspec-jh-base-geo-pg12
- .rails:rules:as-if-jh-rspec
rspec-jh migration pg12-as-if-jh:
extends:
- .rspec-jh-base-pg12
@ -974,21 +883,6 @@ rspec-jh system pg12-as-if-jh:
- .rspec-jh-base-pg12
- .rails:rules:as-if-jh-rspec
rspec-jh unit pg12-as-if-jh geo:
extends:
- .rspec-jh-base-geo-pg12
- .rails:rules:as-if-jh-rspec
rspec-jh integration pg12-as-if-jh geo:
extends:
- .rspec-jh-base-geo-pg12
- .rails:rules:as-if-jh-rspec
rspec-jh system pg12-as-if-jh geo:
extends:
- .rspec-jh-base-geo-pg12
- .rails:rules:as-if-jh-rspec
db:rollback geo:
extends:
- db:rollback
@ -1086,22 +980,6 @@ rspec-ee system pg11:
- .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only
- .rspec-ee-system-parallel
rspec-ee unit pg11 geo:
extends:
- .rspec-ee-base-geo-pg11
- .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only
- .rspec-ee-unit-geo-parallel
rspec-ee integration pg11 geo:
extends:
- .rspec-ee-base-geo-pg11
- .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only
rspec-ee system pg11 geo:
extends:
- .rspec-ee-base-geo-pg11
- .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only
# PG13
rspec-ee migration pg13:
extends:
@ -1127,22 +1005,6 @@ rspec-ee system pg13:
- .rspec-ee-base-pg13
- .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only
- .rspec-ee-system-parallel
rspec-ee unit pg13 geo:
extends:
- .rspec-ee-base-geo-pg13
- .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only
- .rspec-ee-unit-geo-parallel
rspec-ee integration pg13 geo:
extends:
- .rspec-ee-base-geo-pg13
- .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only
rspec-ee system pg13 geo:
extends:
- .rspec-ee-base-geo-pg13
- .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only
# EE: default branch nightly scheduled jobs #
#####################################

View File

@ -152,4 +152,6 @@ knapsack-report:
- .generate-knapsack-report-base
stage: post-qa
variables:
QA_KNAPSACK_REPORT_FILE_PATTERN: $CI_PROJECT_DIR/tmp/knapsack/*/*.json
# knapsack report upload uses gitlab-qa image with code already there
GIT_STRATEGY: none
QA_KNAPSACK_REPORT_FILE_PATTERN: $CI_PROJECT_DIR/qa/tmp/knapsack/*/*.json

View File

@ -38,9 +38,6 @@ update-tests-metadata:
- rspec-ee unit pg12
- rspec-ee integration pg12
- rspec-ee system pg12
- rspec-ee unit pg12 geo
- rspec-ee integration pg12 geo
- rspec-ee system pg12 geo
script:
- run_timed_command "retry gem install fog-aws mime-types activesupport rspec_profiling postgres-copy --no-document"
- source ./scripts/rspec_helpers.sh

View File

@ -164,7 +164,6 @@ RSpec/AnyInstanceOf:
- spec/controllers/snippets/notes_controller_spec.rb
- spec/controllers/snippets_controller_spec.rb
- spec/features/admin/admin_mode/login_spec.rb
- spec/features/groups/clusters/eks_spec.rb
- spec/features/groups/members/tabs_spec.rb
- spec/features/ide/static_object_external_storage_csp_spec.rb
- spec/features/issuables/issuable_list_spec.rb

View File

@ -1 +1 @@
372599313791cb92e579e0ff02279f33cbcd71b5
1f98d5a94c880e3e556ae3ace095f83e44f002fb

View File

@ -1,6 +1,6 @@
<script>
import { GlLink, GlSprintf } from '@gitlab/ui';
import { mapState } from 'vuex';
import { helpPagePath } from '~/helpers/help_page_helper';
import { s__ } from '~/locale';
export default {
@ -10,13 +10,11 @@ export default {
'ClusterIntegration|Enter details about your cluster. %{linkStart}How do I use a certificate to connect to my cluster?%{linkEnd}',
),
},
clusterConnectHelpPath: helpPagePath('user/project/clusters/add_existing_cluster'),
components: {
GlLink,
GlSprintf,
},
computed: {
...mapState(['clusterConnectHelpPath']),
},
};
</script>
@ -26,7 +24,7 @@ export default {
<p>
<gl-sprintf :message="$options.i18n.information">
<template #link="{ content }">
<gl-link :href="clusterConnectHelpPath" target="_blank">{{ content }}</gl-link>
<gl-link :href="$options.clusterConnectHelpPath">{{ content }}</gl-link>
</template>
</gl-sprintf>
</p>

View File

@ -159,7 +159,7 @@ export default {
)
}}</span>
<template #modal-footer>
<gl-button variant="secondary" @click="handleCancel">{{ __('Cancel') }}</gl-button>
<gl-button @click="handleCancel">{{ __('Cancel') }}</gl-button>
<template v-if="confirmCleanup">
<gl-button
:disabled="!canSubmit"

View File

@ -1,17 +1,15 @@
import Vue from 'vue';
import NewCluster from './components/new_cluster.vue';
import { createStore } from './stores/new_cluster';
export default () => {
const entryPoint = document.querySelector('#js-cluster-new');
const el = document.querySelector('#js-cluster-new');
if (!entryPoint) {
if (!el) {
return null;
}
return new Vue({
el: '#js-cluster-new',
store: createStore(entryPoint.dataset),
el,
render(createElement) {
return createElement(NewCluster);
},

View File

@ -1,12 +0,0 @@
import Vue from 'vue';
import Vuex from 'vuex';
import state from './state';
Vue.use(Vuex);
export const createStore = (initialState) =>
new Vuex.Store({
state: state(initialState),
});
export default createStore;

View File

@ -1,3 +0,0 @@
export default (initialState = {}) => ({
clusterConnectHelpPath: initialState.clusterConnectHelpPath,
});

View File

@ -1,5 +1,5 @@
<script>
import { GlDropdown, GlDropdownItem, GlModalDirective, GlTooltip } from '@gitlab/ui';
import { GlButton, GlDropdown, GlDropdownItem, GlModalDirective, GlTooltip } from '@gitlab/ui';
import { INSTALL_AGENT_MODAL_ID, CLUSTERS_ACTIONS } from '../constants';
@ -7,6 +7,7 @@ export default {
i18n: CLUSTERS_ACTIONS,
INSTALL_AGENT_MODAL_ID,
components: {
GlButton,
GlDropdown,
GlDropdownItem,
GlTooltip,
@ -15,7 +16,6 @@ export default {
GlModalDirective,
},
inject: [
'newClusterPath',
'addClusterPath',
'newClusterDocsPath',
'canAddCluster',
@ -42,20 +42,59 @@ export default {
}
return this.addClusterPath;
},
actionItems() {
const createCluster = {
href: this.newClusterDocsPath,
title: this.$options.i18n.createCluster,
testid: 'create-cluster-link',
};
const connectCluster = {
href: this.addClusterPath,
title: this.$options.i18n.connectClusterCertificate,
testid: 'connect-cluster-link',
};
const actions = [];
if (this.displayClusterAgents) {
actions.push(createCluster);
}
if (this.displayClusterAgents && this.certificateBasedClustersEnabled) {
actions.push(connectCluster);
}
return actions;
},
},
methods: {
getTooltipTarget() {
return this.actionItems.length ? this.$refs.actions.$el : this.$refs.actionsContainer;
},
},
};
</script>
<template>
<div class="nav-controls gl-ml-auto">
<div ref="actionsContainer" class="nav-controls gl-ml-auto">
<gl-tooltip
v-if="!canAddCluster"
:target="() => $refs.dropdown.$el"
:title="$options.i18n.dropdownDisabledHint"
:target="() => getTooltipTarget()"
:title="$options.i18n.actionsDisabledHint"
/>
<gl-button
v-if="!actionItems.length"
data-qa-selector="clusters_actions_button"
category="primary"
variant="confirm"
:disabled="!canAddCluster"
:href="defaultActionUrl"
>
{{ defaultActionText }}
</gl-button>
<gl-dropdown
ref="dropdown"
v-else
ref="actions"
v-gl-modal-directive="shouldTriggerModal && $options.INSTALL_AGENT_MODAL_ID"
data-qa-selector="clusters_actions_button"
category="primary"
@ -67,31 +106,13 @@ export default {
right
>
<gl-dropdown-item
v-if="displayClusterAgents"
:href="newClusterDocsPath"
data-testid="create-cluster-link"
v-for="action in actionItems"
:key="action.title"
:href="action.href"
:data-testid="action.testid"
@click.stop
>
{{ $options.i18n.createCluster }}
</gl-dropdown-item>
<template v-if="displayClusterAgents && certificateBasedClustersEnabled">
<gl-dropdown-item :href="newClusterPath" data-testid="new-cluster-link" @click.stop>
{{ $options.i18n.createClusterCertificate }}
</gl-dropdown-item>
<gl-dropdown-item :href="addClusterPath" data-testid="connect-cluster-link" @click.stop>
{{ $options.i18n.connectClusterCertificate }}
</gl-dropdown-item>
</template>
<gl-dropdown-item
v-if="certificateBasedClustersEnabled && !displayClusterAgents"
:href="newClusterPath"
data-testid="new-cluster-link"
@click.stop
>
{{ $options.i18n.createClusterDeprecated }}
{{ action.title }}
</gl-dropdown-item>
</gl-dropdown>
</div>

View File

@ -234,11 +234,9 @@ export const CLUSTERS_ACTIONS = {
connectCluster: s__('ClusterAgents|Connect a cluster'),
connectWithAgent: s__('ClusterAgents|Connect a cluster (agent)'),
connectClusterDeprecated: s__('ClusterAgents|Connect a cluster (deprecated)'),
createClusterDeprecated: s__('ClusterAgents|Create a cluster (deprecated)'),
createCluster: s__('ClusterAgents|Create a cluster'),
createClusterCertificate: s__('ClusterAgents|Create a cluster (certificate - deprecated)'),
connectClusterCertificate: s__('ClusterAgents|Connect a cluster (certificate - deprecated)'),
dropdownDisabledHint: s__(
actionsDisabledHint: s__(
'ClusterAgents|Requires a Maintainer or greater role to perform these actions',
),
};

View File

@ -23,7 +23,6 @@ export default () => {
defaultBranchName,
projectPath,
kasAddress,
newClusterPath,
addClusterPath,
newClusterDocsPath,
emptyStateHelpText,
@ -42,7 +41,6 @@ export default () => {
emptyStateImage,
projectPath,
kasAddress,
newClusterPath,
addClusterPath,
newClusterDocsPath,
emptyStateHelpText,

View File

@ -1,246 +0,0 @@
<script>
import { GlIcon } from '@gitlab/ui';
import $ from 'jquery';
import { isNil } from 'lodash';
import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
import DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue';
import DropdownSearchInput from '~/vue_shared/components/dropdown/dropdown_search_input.vue';
const toArray = (value) => (isNil(value) ? [] : [].concat(value));
const itemsProp = (items, prop) => items.map((item) => item[prop]);
const defaultSearchFn = (searchQuery, labelProp) => (item) =>
item[labelProp].toLowerCase().indexOf(searchQuery) > -1;
export default {
components: {
DropdownButton,
DropdownSearchInput,
DropdownHiddenInput,
GlIcon,
},
props: {
fieldName: {
type: String,
required: false,
default: '',
},
placeholder: {
type: String,
required: false,
default: '',
},
defaultValue: {
type: String,
required: false,
default: '',
},
value: {
type: [Object, Array, String],
required: false,
default: () => null,
},
labelProperty: {
type: String,
required: false,
default: 'name',
},
valueProperty: {
type: String,
required: false,
default: 'value',
},
items: {
type: Array,
required: false,
default: () => [],
},
loading: {
type: Boolean,
required: false,
default: false,
},
disabled: {
type: Boolean,
required: false,
default: false,
},
loadingText: {
type: String,
required: false,
default: '',
},
disabledText: {
type: String,
required: false,
default: '',
},
hasErrors: {
type: Boolean,
required: false,
default: false,
},
multiple: {
type: Boolean,
required: false,
default: false,
},
errorMessage: {
type: String,
required: false,
default: '',
},
searchFieldPlaceholder: {
type: String,
required: false,
default: '',
},
emptyText: {
type: String,
required: false,
default: '',
},
searchFn: {
type: Function,
required: false,
default: defaultSearchFn,
},
},
data() {
return {
searchQuery: '',
focusOnSearch: false,
};
},
computed: {
toggleText() {
if (this.loading && this.loadingText) {
return this.loadingText;
}
if (this.disabled && this.disabledText) {
return this.disabledText;
}
if (!this.selectedItems.length) {
return this.placeholder;
}
return this.selectedItemsLabels;
},
results() {
return this.getItemsOrEmptyList().filter(this.searchFn(this.searchQuery, this.labelProperty));
},
selectedItems() {
const valueProp = this.valueProperty;
const valueList = toArray(this.value);
const items = this.getItemsOrEmptyList();
return items.filter((item) => valueList.some((value) => item[valueProp] === value));
},
selectedItemsLabels() {
return itemsProp(this.selectedItems, this.labelProperty).join(', ');
},
selectedItemsValues() {
return itemsProp(this.selectedItems, this.valueProperty).join(', ');
},
},
mounted() {
$(this.$refs.dropdown)
.on('shown.bs.dropdown', () => {
this.focusOnSearch = true;
})
.on('hidden.bs.dropdown', () => {
this.focusOnSearch = false;
});
},
beforeDestroy() {
// eslint-disable-next-line @gitlab/no-global-event-off
$(this.$refs.dropdown).off();
},
methods: {
getItemsOrEmptyList() {
return this.items || [];
},
selectSingle(item) {
this.$emit('input', item[this.valueProperty]);
},
selectMultiple(item) {
const value = toArray(this.value);
const itemValue = item[this.valueProperty];
const itemValueIndex = value.indexOf(itemValue);
if (itemValueIndex > -1) {
value.splice(itemValueIndex, 1);
} else {
value.push(itemValue);
}
this.$emit('input', value);
},
isSelected(item) {
return this.selectedItems.includes(item);
},
},
};
</script>
<template>
<div>
<div ref="dropdown" class="dropdown">
<dropdown-hidden-input :name="fieldName" :value="selectedItemsValues" />
<dropdown-button
:class="{ 'border-danger': hasErrors }"
:is-disabled="disabled"
:is-loading="loading"
:toggle-text="toggleText"
/>
<div class="dropdown-menu dropdown-select">
<dropdown-search-input
v-model="searchQuery"
:focused="focusOnSearch"
:placeholder-text="searchFieldPlaceholder"
/>
<div class="dropdown-content">
<ul>
<li v-if="!results.length">
<span class="js-empty-text menu-item">{{ emptyText }}</span>
</li>
<li v-for="item in results" :key="item.id">
<button
v-if="multiple"
class="js-dropdown-item d-flex align-items-center"
type="button"
@click.stop.prevent="selectMultiple(item)"
>
<gl-icon
:class="[{ invisible: !isSelected(item) }, 'mr-1']"
name="mobile-issue-close"
/>
<slot name="item" :item="item">{{ item.name }}</slot>
</button>
<button
v-else
class="js-dropdown-item"
type="button"
@click.prevent="selectSingle(item)"
>
<slot name="item" :item="item">{{ item.name }}</slot>
</button>
</li>
</ul>
</div>
</div>
</div>
<span
v-if="hasErrors && errorMessage"
:class="[
'form-text js-eks-dropdown-error-message',
{
'text-danger': hasErrors,
'text-muted': !hasErrors,
},
]"
>{{ errorMessage }}</span
>
</div>
</template>

View File

@ -1,58 +0,0 @@
<script>
import { mapState } from 'vuex';
import EksClusterConfigurationForm from './eks_cluster_configuration_form.vue';
import ServiceCredentialsForm from './service_credentials_form.vue';
export default {
components: {
ServiceCredentialsForm,
EksClusterConfigurationForm,
},
props: {
gitlabManagedClusterHelpPath: {
type: String,
required: true,
},
namespacePerEnvironmentHelpPath: {
type: String,
required: true,
},
kubernetesIntegrationHelpPath: {
type: String,
required: true,
},
accountAndExternalIdsHelpPath: {
type: String,
required: true,
},
createRoleArnHelpPath: {
type: String,
required: true,
},
externalLinkIcon: {
type: String,
required: true,
},
},
computed: {
...mapState(['hasCredentials']),
},
};
</script>
<template>
<div class="js-create-eks-cluster">
<eks-cluster-configuration-form
v-if="hasCredentials"
:gitlab-managed-cluster-help-path="gitlabManagedClusterHelpPath"
:namespace-per-environment-help-path="namespacePerEnvironmentHelpPath"
:kubernetes-integration-help-path="kubernetesIntegrationHelpPath"
:external-link-icon="externalLinkIcon"
/>
<service-credentials-form
v-else
:create-role-arn-help-path="createRoleArnHelpPath"
:account-and-external-ids-help-path="accountAndExternalIdsHelpPath"
:external-link-icon="externalLinkIcon"
/>
</div>
</template>

View File

@ -1,530 +0,0 @@
<script>
import {
GlFormGroup,
GlFormInput,
GlFormCheckbox,
GlIcon,
GlLink,
GlSprintf,
GlButton,
} from '@gitlab/ui';
import { createNamespacedHelpers, mapState, mapActions, mapGetters } from 'vuex';
import ClusterFormDropdown from '~/create_cluster/components/cluster_form_dropdown.vue';
import { s__ } from '~/locale';
import { KUBERNETES_VERSIONS } from '../constants';
const { mapState: mapRolesState, mapActions: mapRolesActions } = createNamespacedHelpers('roles');
const { mapState: mapKeyPairsState, mapActions: mapKeyPairsActions } = createNamespacedHelpers(
'keyPairs',
);
const { mapState: mapVpcsState, mapActions: mapVpcActions } = createNamespacedHelpers('vpcs');
const { mapState: mapSubnetsState, mapActions: mapSubnetActions } = createNamespacedHelpers(
'subnets',
);
const {
mapState: mapSecurityGroupsState,
mapActions: mapSecurityGroupsActions,
} = createNamespacedHelpers('securityGroups');
const { mapState: mapInstanceTypesState } = createNamespacedHelpers('instanceTypes');
export default {
components: {
ClusterFormDropdown,
GlFormCheckbox,
GlFormGroup,
GlFormInput,
GlIcon,
GlLink,
GlSprintf,
GlButton,
},
props: {
gitlabManagedClusterHelpPath: {
type: String,
required: true,
},
namespacePerEnvironmentHelpPath: {
type: String,
required: true,
},
kubernetesIntegrationHelpPath: {
type: String,
required: true,
},
externalLinkIcon: {
type: String,
required: true,
},
},
i18n: {
kubernetesIntegrationHelpText: s__(
'ClusterIntegration|Read our %{linkStart}help page%{linkEnd} on Kubernetes cluster integration.',
),
roleDropdownHelpText: s__(
'ClusterIntegration|Your service role is distinct from the provision role used when authenticating. It will allow Amazon EKS and the Kubernetes control plane to manage AWS resources on your behalf. To use a new role, first create one on %{linkStart}Amazon Web Services%{linkEnd}.',
),
roleDropdownHelpPath:
'https://docs.aws.amazon.com/eks/latest/userguide/service_IAM_role.html#create-service-role',
regionInputLabel: s__('ClusterIntegration|Cluster Region'),
regionHelpText: s__(
'ClusterIntegration|The region the new cluster will be created in. You must reauthenticate to change regions.',
),
keyPairDropdownHelpText: s__(
'ClusterIntegration|Select the key pair name that will be used to create EC2 nodes. To use a new key pair name, first create one on %{linkStart}Amazon Web Services%{linkEnd}.',
),
keyPairDropdownHelpPath:
'https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-key-pairs.html#having-ec2-create-your-key-pair',
vpcDropdownHelpText: s__(
'ClusterIntegration|Select a VPC to use for your EKS Cluster resources. To use a new VPC, first create one on %{linkStart}Amazon Web Services %{linkEnd}.',
),
vpcDropdownHelpPath:
'https://docs.aws.amazon.com/eks/latest/userguide/getting-started-console.html#vpc-create',
subnetDropdownHelpText: s__(
'ClusterIntegration|Choose the %{linkStart}subnets %{linkEnd} in your VPC where your worker nodes will run.',
),
subnetDropdownHelpPath: 'https://console.aws.amazon.com/vpc/home?#subnets',
securityGroupDropdownHelpText: s__(
'ClusterIntegration|Choose the %{linkStart}security group%{linkEnd} to apply to the EKS-managed Elastic Network Interfaces that are created in your worker node subnets.',
),
securityGroupDropdownHelpPath: 'https://console.aws.amazon.com/vpc/home?#securityGroups',
instanceTypesDropdownHelpText: s__(
'ClusterIntegration|Choose the worker node %{linkStart}instance type%{linkEnd}.',
),
instanceTypesDropdownHelpPath: 'https://aws.amazon.com/ec2/instance-types',
gitlabManagedClusterHelpText: s__(
'ClusterIntegration|Allow GitLab to manage namespace and service accounts for this cluster. %{linkStart}More information%{linkEnd}',
),
namespacePerEnvironmentHelpText: s__(
'ClusterIntegration|Deploy each environment to its own namespace. Otherwise, environments within a project share a project-wide namespace. Note that anyone who can trigger a deployment of a namespace can read its secrets. If modified, existing environments will use their current namespaces until the cluster cache is cleared. %{linkStart}More information%{linkEnd}',
),
},
computed: {
...mapState([
'clusterName',
'environmentScope',
'kubernetesVersion',
'selectedRegion',
'selectedKeyPair',
'selectedVpc',
'selectedSubnet',
'selectedRole',
'selectedSecurityGroup',
'selectedInstanceType',
'nodeCount',
'gitlabManagedCluster',
'namespacePerEnvironment',
'isCreatingCluster',
]),
...mapGetters(['subnetValid']),
...mapRolesState({
roles: 'items',
isLoadingRoles: 'isLoadingItems',
loadingRolesError: 'loadingItemsError',
}),
...mapKeyPairsState({
keyPairs: 'items',
isLoadingKeyPairs: 'isLoadingItems',
loadingKeyPairsError: 'loadingItemsError',
}),
...mapVpcsState({
vpcs: 'items',
isLoadingVpcs: 'isLoadingItems',
loadingVpcsError: 'loadingItemsError',
}),
...mapSubnetsState({
subnets: 'items',
isLoadingSubnets: 'isLoadingItems',
loadingSubnetsError: 'loadingItemsError',
}),
...mapSecurityGroupsState({
securityGroups: 'items',
isLoadingSecurityGroups: 'isLoadingItems',
loadingSecurityGroupsError: 'loadingItemsError',
}),
...mapInstanceTypesState({
instanceTypes: 'items',
isLoadingInstanceTypes: 'isLoadingItems',
loadingInstanceTypesError: 'loadingItemsError',
}),
kubernetesVersions() {
return KUBERNETES_VERSIONS;
},
vpcDropdownDisabled() {
return !this.selectedRegion;
},
keyPairDropdownDisabled() {
return !this.selectedRegion;
},
subnetDropdownDisabled() {
return !this.selectedVpc;
},
securityGroupDropdownDisabled() {
return !this.selectedVpc;
},
createClusterButtonDisabled() {
return (
!this.clusterName ||
!this.environmentScope ||
!this.kubernetesVersion ||
!this.selectedRegion ||
!this.selectedKeyPair ||
!this.selectedVpc ||
!this.subnetValid ||
!this.selectedRole ||
!this.selectedSecurityGroup ||
!this.selectedInstanceType ||
!this.nodeCount ||
this.isCreatingCluster
);
},
displaySubnetError() {
return Boolean(this.loadingSubnetsError) || this.selectedSubnet?.length === 1;
},
createClusterButtonLabel() {
return this.isCreatingCluster
? s__('ClusterIntegration|Creating Kubernetes cluster')
: s__('ClusterIntegration|Create Kubernetes cluster');
},
subnetValidationErrorText() {
if (this.loadingSubnetsError) {
return s__('ClusterIntegration|Could not load subnets for the selected VPC');
}
return s__('ClusterIntegration|You should select at least two subnets');
},
},
mounted() {
this.fetchRoles();
this.setRegionAndFetchVpcsAndKeyPairs();
},
methods: {
...mapActions([
'createCluster',
'setClusterName',
'setEnvironmentScope',
'setKubernetesVersion',
'setRegion',
'setVpc',
'setSubnet',
'setRole',
'setKeyPair',
'setSecurityGroup',
'setInstanceType',
'setNodeCount',
'setGitlabManagedCluster',
'setNamespacePerEnvironment',
]),
...mapVpcActions({ fetchVpcs: 'fetchItems' }),
...mapSubnetActions({ fetchSubnets: 'fetchItems' }),
...mapRolesActions({ fetchRoles: 'fetchItems' }),
...mapKeyPairsActions({ fetchKeyPairs: 'fetchItems' }),
...mapSecurityGroupsActions({ fetchSecurityGroups: 'fetchItems' }),
setRegionAndFetchVpcsAndKeyPairs() {
this.setVpc({ vpc: null });
this.setKeyPair({ keyPair: null });
this.setSubnet({ subnet: [] });
this.setSecurityGroup({ securityGroup: null });
this.fetchVpcs({ region: this.selectedRegion });
this.fetchKeyPairs({ region: this.selectedRegion });
},
setVpcAndFetchSubnets(vpc) {
this.setVpc({ vpc });
this.setSubnet({ subnet: [] });
this.setSecurityGroup({ securityGroup: null });
this.fetchSubnets({ vpc, region: this.selectedRegion });
this.fetchSecurityGroups({ vpc, region: this.selectedRegion });
},
},
};
</script>
<template>
<form name="eks-cluster-configuration-form">
<h4>
{{ s__('ClusterIntegration|Enter the details for your Amazon EKS Kubernetes cluster') }}
</h4>
<div class="mb-3">
<gl-sprintf :message="$options.i18n.kubernetesIntegrationHelpText">
<template #link="{ content }">
<gl-link :href="kubernetesIntegrationHelpPath">
{{ content }}
</gl-link>
</template>
</gl-sprintf>
</div>
<div class="form-group">
<label class="label-bold" for="eks-cluster-name">{{
s__('ClusterIntegration|Kubernetes cluster name')
}}</label>
<gl-form-input
id="eks-cluster-name"
:value="clusterName"
@input="setClusterName({ clusterName: $event })"
/>
</div>
<div class="form-group">
<label class="label-bold" for="eks-environment-scope">{{
s__('ClusterIntegration|Environment scope')
}}</label>
<gl-form-input
id="eks-environment-scope"
:value="environmentScope"
@input="setEnvironmentScope({ environmentScope: $event })"
/>
</div>
<div class="form-group">
<label class="label-bold" for="eks-kubernetes-version">{{
s__('ClusterIntegration|Kubernetes version')
}}</label>
<cluster-form-dropdown
field-id="eks-kubernetes-version"
field-name="eks-kubernetes-version"
:value="kubernetesVersion"
:items="kubernetesVersions"
:empty-text="s__('ClusterIntegration|Kubernetes version not found')"
@input="setKubernetesVersion({ kubernetesVersion: $event })"
/>
</div>
<div class="form-group">
<label class="label-bold" for="eks-role">{{ s__('ClusterIntegration|Service role') }}</label>
<cluster-form-dropdown
field-id="eks-role"
field-name="eks-role"
:value="selectedRole"
:items="roles"
:loading="isLoadingRoles"
:loading-text="s__('ClusterIntegration|Loading IAM Roles')"
:placeholder="s__('ClusterIntegration|Select service role')"
:search-field-placeholder="s__('ClusterIntegration|Search IAM Roles')"
:empty-text="s__('ClusterIntegration|No IAM Roles found')"
:has-errors="Boolean(loadingRolesError)"
:error-message="s__('ClusterIntegration|Could not load IAM roles')"
@input="setRole({ role: $event })"
/>
<p class="form-text text-muted">
<gl-sprintf :message="$options.i18n.roleDropdownHelpText">
<template #link="{ content }">
<gl-link :href="$options.i18n.roleDropdownHelpPath" target="_blank">
{{ content }}
<gl-icon name="external-link" class="gl-vertical-align-middle" />
</gl-link>
</template>
</gl-sprintf>
</p>
</div>
<gl-form-group
:label="$options.i18n.regionInputLabel"
:description="$options.i18n.regionHelpText"
>
<gl-form-input id="eks-region" :value="selectedRegion" type="text" readonly />
</gl-form-group>
<div class="form-group">
<label class="label-bold" for="eks-key-pair">{{
s__('ClusterIntegration|Key pair name')
}}</label>
<cluster-form-dropdown
field-id="eks-key-pair"
field-name="eks-key-pair"
:value="selectedKeyPair"
:items="keyPairs"
:disabled="keyPairDropdownDisabled"
:disabled-text="s__('ClusterIntegration|Select a region to choose a Key Pair')"
:loading="isLoadingKeyPairs"
:loading-text="s__('ClusterIntegration|Loading Key Pairs')"
:placeholder="s__('ClusterIntegration|Select key pair')"
:search-field-placeholder="s__('ClusterIntegration|Search Key Pairs')"
:empty-text="s__('ClusterIntegration|No Key Pairs found')"
:has-errors="Boolean(loadingKeyPairsError)"
:error-message="s__('ClusterIntegration|Could not load Key Pairs')"
@input="setKeyPair({ keyPair: $event })"
/>
<p class="form-text text-muted">
<gl-sprintf :message="$options.i18n.keyPairDropdownHelpText">
<template #link="{ content }">
<gl-link :href="$options.i18n.keyPairDropdownHelpPath" target="_blank">
{{ content }}
<gl-icon name="external-link" class="gl-vertical-align-middle" />
</gl-link>
</template>
</gl-sprintf>
</p>
</div>
<div class="form-group">
<label class="label-bold" for="eks-vpc">{{ s__('ClusterIntegration|VPC') }}</label>
<cluster-form-dropdown
field-id="eks-vpc"
field-name="eks-vpc"
:value="selectedVpc"
:items="vpcs"
:loading="isLoadingVpcs"
:disabled="vpcDropdownDisabled"
:disabled-text="s__('ClusterIntegration|Select a region to choose a VPC')"
:loading-text="s__('ClusterIntegration|Loading VPCs')"
:placeholder="s__('ClusterIntegration|Select a VPC')"
:search-field-placeholder="s__('ClusterIntegration|Search VPCs')"
:empty-text="s__('ClusterIntegration|No VPCs found')"
:has-errors="Boolean(loadingVpcsError)"
:error-message="s__('ClusterIntegration|Could not load VPCs for the selected region')"
@input="setVpcAndFetchSubnets($event)"
/>
<p class="form-text text-muted">
<gl-sprintf :message="$options.i18n.vpcDropdownHelpText">
<template #link="{ content }">
<gl-link :href="$options.i18n.vpcDropdownHelpPath" target="_blank">
{{ content }}
<gl-icon name="external-link" class="gl-vertical-align-middle" />
</gl-link>
</template>
</gl-sprintf>
</p>
</div>
<div class="form-group">
<label class="label-bold" for="eks-role">{{ s__('ClusterIntegration|Subnets') }}</label>
<cluster-form-dropdown
field-id="eks-subnet"
field-name="eks-subnet"
multiple
:value="selectedSubnet"
:items="subnets"
:loading="isLoadingSubnets"
:disabled="subnetDropdownDisabled"
:disabled-text="s__('ClusterIntegration|Select a VPC to choose a subnet')"
:loading-text="s__('ClusterIntegration|Loading subnets')"
:placeholder="s__('ClusterIntegration|Select a subnet')"
:search-field-placeholder="s__('ClusterIntegration|Search subnets')"
:empty-text="s__('ClusterIntegration|No subnet found')"
:has-errors="displaySubnetError"
:error-message="subnetValidationErrorText"
@input="setSubnet({ subnet: $event })"
/>
<p class="form-text text-muted">
<gl-sprintf :message="$options.i18n.subnetDropdownHelpText">
<template #link="{ content }">
<gl-link :href="$options.i18n.subnetDropdownHelpPath" target="_blank">
{{ content }}
<gl-icon name="external-link" class="gl-vertical-align-middle" />
</gl-link>
</template>
</gl-sprintf>
</p>
</div>
<div class="form-group">
<label class="label-bold" for="eks-security-group">{{
s__('ClusterIntegration|Security group')
}}</label>
<cluster-form-dropdown
field-id="eks-security-group"
field-name="eks-security-group"
:value="selectedSecurityGroup"
:items="securityGroups"
:loading="isLoadingSecurityGroups"
:disabled="securityGroupDropdownDisabled"
:disabled-text="s__('ClusterIntegration|Select a VPC to choose a security group')"
:loading-text="s__('ClusterIntegration|Loading security groups')"
:placeholder="s__('ClusterIntegration|Select a security group')"
:search-field-placeholder="s__('ClusterIntegration|Search security groups')"
:empty-text="s__('ClusterIntegration|No security group found')"
:has-errors="Boolean(loadingSecurityGroupsError)"
:error-message="
s__('ClusterIntegration|Could not load security groups for the selected VPC')
"
@input="setSecurityGroup({ securityGroup: $event })"
/>
<p class="form-text text-muted">
<gl-sprintf :message="$options.i18n.securityGroupDropdownHelpText">
<template #link="{ content }">
<gl-link :href="$options.i18n.securityGroupDropdownHelpPath" target="_blank">
{{ content }}
<gl-icon name="external-link" class="gl-vertical-align-middle" />
</gl-link>
</template>
</gl-sprintf>
</p>
</div>
<div class="form-group">
<label class="label-bold" for="eks-instance-type">{{
s__('ClusterIntegration|Instance type')
}}</label>
<cluster-form-dropdown
field-id="eks-instance-type"
field-name="eks-instance-type"
:value="selectedInstanceType"
:items="instanceTypes"
:loading="isLoadingInstanceTypes"
:loading-text="s__('ClusterIntegration|Loading instance types')"
:placeholder="s__('ClusterIntegration|Select an instance type')"
:search-field-placeholder="s__('ClusterIntegration|Search instance types')"
:empty-text="s__('ClusterIntegration|No instance type found')"
:has-errors="Boolean(loadingInstanceTypesError)"
:error-message="s__('ClusterIntegration|Could not load instance types')"
@input="setInstanceType({ instanceType: $event })"
/>
<p class="form-text text-muted">
<gl-sprintf :message="$options.i18n.instanceTypesDropdownHelpText">
<template #link="{ content }">
<gl-link :href="$options.i18n.instanceTypesDropdownHelpPath" target="_blank">
{{ content }}
<gl-icon name="external-link" class="gl-vertical-align-middle" />
</gl-link>
</template>
</gl-sprintf>
</p>
</div>
<div class="form-group">
<label class="label-bold" for="eks-node-count">{{
s__('ClusterIntegration|Number of nodes')
}}</label>
<gl-form-input
id="eks-node-count"
type="number"
min="1"
step="1"
:value="nodeCount"
@input="setNodeCount({ nodeCount: $event })"
/>
</div>
<div class="form-group">
<gl-form-checkbox
:checked="gitlabManagedCluster"
@input="setGitlabManagedCluster({ gitlabManagedCluster: $event })"
>{{ s__('ClusterIntegration|GitLab-managed cluster') }}</gl-form-checkbox
>
<p class="form text text-muted">
<gl-sprintf :message="$options.i18n.gitlabManagedClusterHelpText">
<template #link="{ content }">
<gl-link :href="gitlabManagedClusterHelpPath" target="_blank">
{{ content }}
</gl-link>
</template>
</gl-sprintf>
</p>
</div>
<div class="form-group">
<gl-form-checkbox
:checked="namespacePerEnvironment"
@input="setNamespacePerEnvironment({ namespacePerEnvironment: $event })"
>{{ s__('ClusterIntegration|Namespace per environment') }}</gl-form-checkbox
>
<p class="form text text-muted">
<gl-sprintf :message="$options.i18n.namespacePerEnvironmentHelpText">
<template #link="{ content }">
<gl-link :href="namespacePerEnvironmentHelpPath" target="_blank">
{{ content }}
</gl-link>
</template>
</gl-sprintf>
</p>
</div>
<div class="form-group">
<gl-button
variant="success"
category="primary"
class="js-create-cluster"
:disabled="createClusterButtonDisabled"
:loading="isCreatingCluster"
@click="createCluster()"
>
{{ createClusterButtonLabel }}
</gl-button>
</div>
</form>
</template>

View File

@ -1,182 +0,0 @@
<script>
import { GlButton, GlFormGroup, GlFormInput, GlIcon, GlLink, GlSprintf, GlAlert } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
import { s__, __ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import { DEFAULT_REGION } from '../constants';
export default {
components: {
GlButton,
GlFormGroup,
GlFormInput,
GlIcon,
GlLink,
GlSprintf,
ClipboardButton,
GlAlert,
},
props: {
accountAndExternalIdsHelpPath: {
type: String,
required: true,
},
createRoleArnHelpPath: {
type: String,
required: true,
},
externalLinkIcon: {
type: String,
required: true,
},
},
i18n: {
regionInputLabel: s__('ClusterIntegration|Cluster Region'),
regionHelpPath: 'https://aws.amazon.com/about-aws/global-infrastructure/regions_az/',
regionHelpText: s__(
'ClusterIntegration|Select the region you want to create the new cluster in. Make sure you have access to this region for your role to be able to authenticate. If no region is selected, we will use %{codeStart}DEFAULT_REGION%{codeEnd}. Learn more about %{linkStart}Regions%{linkEnd}.',
),
accountAndExternalIdsHelpText: s__(
'ClusterIntegration|The Amazon Resource Name (ARN) associated with your role. If you do not have a provisioned role, first create one on %{awsLinkStart}Amazon Web Services %{awsLinkEnd} using the above account and external IDs. %{moreInfoStart}More information%{moreInfoEnd}',
),
regionHelpTextDefaultRegion: DEFAULT_REGION,
},
data() {
return {
roleArn: this.$store.state.roleArn,
selectedRegion: this.$store.state.selectedRegion,
};
},
computed: {
...mapState(['accountId', 'externalId', 'isCreatingRole', 'createRoleError']),
submitButtonDisabled() {
return this.isCreatingRole || !this.roleArn;
},
submitButtonLabel() {
return this.isCreatingRole
? __('Authenticating')
: s__('ClusterIntegration|Authenticate with AWS');
},
awsHelpLink() {
return 'https://console.aws.amazon.com/iam/home?#roles';
},
},
methods: {
...mapActions(['createRole']),
},
};
</script>
<template>
<form name="service-credentials-form">
<h4>{{ s__('ClusterIntegration|Authenticate with Amazon Web Services') }}</h4>
<p>
{{
s__(
'ClusterIntegration|You must grant access to your organizations AWS resources in order to create a new EKS cluster. To grant access, create a provision role using the account and external ID below and provide us the ARN.',
)
}}
</p>
<gl-alert
v-if="createRoleError"
class="js-invalid-credentials gl-mb-5"
variant="danger"
:dismissible="false"
>
{{ createRoleError }}
</gl-alert>
<div class="form-row">
<div class="form-group col-md-6">
<label for="gitlab-account-id">{{ __('Account ID') }}</label>
<div class="input-group">
<gl-form-input id="gitlab-account-id" type="text" readonly :value="accountId" />
<div class="input-group-append">
<clipboard-button
:text="accountId"
:title="__('Copy Account ID to clipboard')"
class="input-group-text js-copy-account-id-button"
/>
</div>
</div>
</div>
<div class="form-group col-md-6">
<label for="eks-external-id">{{ __('External ID') }}</label>
<div class="input-group">
<gl-form-input id="eks-external-id" type="text" readonly :value="externalId" />
<div class="input-group-append">
<clipboard-button
:text="externalId"
:title="__('Copy External ID to clipboard')"
class="input-group-text js-copy-external-id-button"
/>
</div>
</div>
</div>
<div class="col-12 mb-3 mt-n3">
<p class="form-text text-muted">
<gl-sprintf :message="$options.i18n.accountAndExternalIdsHelpText">
<template #awsLink="{ content }">
<gl-link :href="awsHelpLink" target="_blank">
{{ content }}
<gl-icon name="external-link" class="gl-vertical-align-middle" />
</gl-link>
</template>
<template #moreInfo="{ content }">
<gl-link :href="accountAndExternalIdsHelpPath" target="_blank">
{{ content }}
</gl-link>
</template>
</gl-sprintf>
</p>
</div>
</div>
<div class="form-group">
<label for="eks-provision-role-arn">{{ s__('ClusterIntegration|Provision Role ARN') }}</label>
<gl-form-input id="eks-provision-role-arn" v-model="roleArn" />
<p class="form-text text-muted">
<gl-sprintf :message="$options.i18n.accountAndExternalIdsHelpText">
<template #awsLink="{ content }">
<gl-link :href="awsHelpLink" target="_blank">
{{ content }}
<gl-icon name="external-link" class="gl-vertical-align-middle" />
</gl-link>
</template>
<template #moreInfo="{ content }">
<gl-link :href="accountAndExternalIdsHelpPath" target="_blank">
{{ content }}
</gl-link>
</template>
</gl-sprintf>
</p>
</div>
<gl-form-group :label="$options.i18n.regionInputLabel">
<gl-form-input id="eks-region" v-model="selectedRegion" type="text" />
<template #description>
<gl-sprintf :message="$options.i18n.regionHelpText">
<template #code>
<code>{{ $options.i18n.regionHelpTextDefaultRegion }}</code>
</template>
<template #link="{ content }">
<gl-link :href="$options.i18n.regionHelpPath" target="_blank">
{{ content }}
<gl-icon name="external-link" />
</gl-link>
</template>
</gl-sprintf>
</template>
</gl-form-group>
<gl-button
variant="success"
category="primary"
type="submit"
:disabled="submitButtonDisabled"
:loading="isCreatingRole"
@click.prevent="createRole({ roleArn, selectedRegion, externalId })"
>
{{ submitButtonLabel }}
</gl-button>
</form>
</template>

View File

@ -1,9 +0,0 @@
export const DEFAULT_REGION = 'us-east-2';
export const KUBERNETES_VERSIONS = [
{ name: '1.16', value: '1.16' },
{ name: '1.17', value: '1.17' },
{ name: '1.18', value: '1.18' },
{ name: '1.19', value: '1.19' },
{ name: '1.20', value: '1.20', default: true },
];

View File

@ -1,55 +0,0 @@
import Vue from 'vue';
import Vuex from 'vuex';
import { parseBoolean } from '~/lib/utils/common_utils';
import CreateEksCluster from './components/create_eks_cluster.vue';
import createStore from './store';
Vue.use(Vuex);
export default (el) => {
const {
gitlabManagedClusterHelpPath,
namespacePerEnvironmentHelpPath,
kubernetesIntegrationHelpPath,
accountAndExternalIdsHelpPath,
createRoleArnHelpPath,
externalId,
accountId,
instanceTypes,
hasCredentials,
createRolePath,
createClusterPath,
externalLinkIcon,
roleArn,
} = el.dataset;
return new Vue({
el,
store: createStore({
initialState: {
hasCredentials: parseBoolean(hasCredentials),
externalId,
accountId,
instanceTypes: JSON.parse(instanceTypes),
createRolePath,
createClusterPath,
roleArn,
},
}),
components: {
CreateEksCluster,
},
render(createElement) {
return createElement('create-eks-cluster', {
props: {
gitlabManagedClusterHelpPath,
namespacePerEnvironmentHelpPath,
kubernetesIntegrationHelpPath,
accountAndExternalIdsHelpPath,
createRoleArnHelpPath,
externalLinkIcon,
},
});
},
});
};

View File

@ -1,79 +0,0 @@
import EC2 from 'aws-sdk/clients/ec2';
import IAM from 'aws-sdk/clients/iam';
import AWS from 'aws-sdk/global';
const lookupVpcName = ({ Tags: tags, VpcId: id }) => {
const nameTag = tags.find(({ Key: key }) => key === 'Name');
return nameTag ? nameTag.Value : id;
};
export const setAWSConfig = ({ awsCredentials }) => {
AWS.config = awsCredentials;
};
export const fetchRoles = () => {
const iam = new IAM();
return iam
.listRoles()
.promise()
.then(({ Roles: roles }) => roles.map(({ RoleName: name, Arn: value }) => ({ name, value })));
};
export const fetchKeyPairs = ({ region }) => {
const ec2 = new EC2({ region });
return ec2
.describeKeyPairs()
.promise()
.then(({ KeyPairs: keyPairs }) => keyPairs.map(({ KeyName: name }) => ({ name, value: name })));
};
export const fetchVpcs = ({ region }) => {
const ec2 = new EC2({ region });
return ec2
.describeVpcs()
.promise()
.then(({ Vpcs: vpcs }) =>
vpcs.map((vpc) => ({
value: vpc.VpcId,
name: lookupVpcName(vpc),
})),
);
};
export const fetchSubnets = ({ vpc, region }) => {
const ec2 = new EC2({ region });
return ec2
.describeSubnets({
Filters: [
{
Name: 'vpc-id',
Values: [vpc],
},
],
})
.promise()
.then(({ Subnets: subnets }) => subnets.map(({ SubnetId: id }) => ({ value: id, name: id })));
};
export const fetchSecurityGroups = ({ region, vpc }) => {
const ec2 = new EC2({ region });
return ec2
.describeSecurityGroups({
Filters: [
{
Name: 'vpc-id',
Values: [vpc],
},
],
})
.promise()
.then(({ SecurityGroups: securityGroups }) =>
securityGroups.map(({ GroupName: name, GroupId: value }) => ({ name, value })),
);
};

View File

@ -1,148 +0,0 @@
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { DEFAULT_REGION } from '../constants';
import { setAWSConfig } from '../services/aws_services_facade';
import * as types from './mutation_types';
const getErrorMessage = (data) => {
const errorKey = Object.keys(data)[0];
return data[errorKey][0];
};
export const setClusterName = ({ commit }, payload) => {
commit(types.SET_CLUSTER_NAME, payload);
};
export const setEnvironmentScope = ({ commit }, payload) => {
commit(types.SET_ENVIRONMENT_SCOPE, payload);
};
export const setKubernetesVersion = ({ commit }, payload) => {
commit(types.SET_KUBERNETES_VERSION, payload);
};
export const createRole = ({ dispatch, state: { createRolePath } }, payload) => {
dispatch('requestCreateRole');
const region = payload.selectedRegion || DEFAULT_REGION;
return axios
.post(createRolePath, {
role_arn: payload.roleArn,
role_external_id: payload.externalId,
region,
})
.then(({ data }) => {
const awsData = {
...convertObjectPropsToCamelCase(data),
region,
};
dispatch('createRoleSuccess', awsData);
})
.catch((error) => {
let message = error;
if (error?.response?.data?.message) {
message = error.response.data.message;
}
dispatch('createRoleError', { error: message });
});
};
export const requestCreateRole = ({ commit }) => {
commit(types.REQUEST_CREATE_ROLE);
};
export const createRoleSuccess = ({ dispatch, commit }, awsCredentials) => {
dispatch('setRegion', { region: awsCredentials.region });
setAWSConfig({ awsCredentials });
commit(types.CREATE_ROLE_SUCCESS);
};
export const createRoleError = ({ commit }, payload) => {
commit(types.CREATE_ROLE_ERROR, payload);
};
export const createCluster = ({ dispatch, state }) => {
dispatch('requestCreateCluster');
return axios
.post(state.createClusterPath, {
name: state.clusterName,
environment_scope: state.environmentScope,
managed: state.gitlabManagedCluster,
namespace_per_environment: state.namespacePerEnvironment,
provider_aws_attributes: {
kubernetes_version: state.kubernetesVersion,
region: state.selectedRegion,
vpc_id: state.selectedVpc,
subnet_ids: state.selectedSubnet,
role_arn: state.selectedRole,
key_name: state.selectedKeyPair,
security_group_id: state.selectedSecurityGroup,
instance_type: state.selectedInstanceType,
num_nodes: state.nodeCount,
},
})
.then(({ headers: { location } }) => dispatch('createClusterSuccess', location))
.catch(({ response: { data } }) => {
dispatch('createClusterError', data);
});
};
export const requestCreateCluster = ({ commit }) => {
commit(types.REQUEST_CREATE_CLUSTER);
};
export const createClusterSuccess = (_, location) => {
window.location.assign(location);
};
export const createClusterError = ({ commit }, error) => {
commit(types.CREATE_CLUSTER_ERROR, error);
createFlash({
message: getErrorMessage(error),
});
};
export const setRegion = ({ commit }, payload) => {
commit(types.SET_REGION, payload);
};
export const setKeyPair = ({ commit }, payload) => {
commit(types.SET_KEY_PAIR, payload);
};
export const setVpc = ({ commit }, payload) => {
commit(types.SET_VPC, payload);
};
export const setSubnet = ({ commit }, payload) => {
commit(types.SET_SUBNET, payload);
};
export const setRole = ({ commit }, payload) => {
commit(types.SET_ROLE, payload);
};
export const setSecurityGroup = ({ commit }, payload) => {
commit(types.SET_SECURITY_GROUP, payload);
};
export const setGitlabManagedCluster = ({ commit }, payload) => {
commit(types.SET_GITLAB_MANAGED_CLUSTER, payload);
};
export const setNamespacePerEnvironment = ({ commit }, payload) => {
commit(types.SET_NAMESPACE_PER_ENVIRONMENT, payload);
};
export const setInstanceType = ({ commit }, payload) => {
commit(types.SET_INSTANCE_TYPE, payload);
};
export const setNodeCount = ({ commit }, payload) => {
commit(types.SET_NODE_COUNT, payload);
};

View File

@ -1,2 +0,0 @@
export const subnetValid = ({ selectedSubnet }) =>
Array.isArray(selectedSubnet) && selectedSubnet.length >= 2;

View File

@ -1,49 +0,0 @@
import Vuex from 'vuex';
import clusterDropdownStore from '~/create_cluster/store/cluster_dropdown';
import {
fetchRoles,
fetchKeyPairs,
fetchVpcs,
fetchSubnets,
fetchSecurityGroups,
} from '../services/aws_services_facade';
import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
import state from './state';
const createStore = ({ initialState }) =>
new Vuex.Store({
actions,
getters,
mutations,
state: Object.assign(state(), initialState),
modules: {
roles: {
namespaced: true,
...clusterDropdownStore({ fetchFn: fetchRoles }),
},
keyPairs: {
namespaced: true,
...clusterDropdownStore({ fetchFn: fetchKeyPairs }),
},
vpcs: {
namespaced: true,
...clusterDropdownStore({ fetchFn: fetchVpcs }),
},
subnets: {
namespaced: true,
...clusterDropdownStore({ fetchFn: fetchSubnets }),
},
securityGroups: {
namespaced: true,
...clusterDropdownStore({ fetchFn: fetchSecurityGroups }),
},
instanceTypes: {
namespaced: true,
...clusterDropdownStore({ initialState: { items: initialState.instanceTypes } }),
},
},
});
export default createStore;

View File

@ -1,19 +0,0 @@
export const SET_CLUSTER_NAME = 'SET_CLUSTER_NAME';
export const SET_ENVIRONMENT_SCOPE = 'SET_ENVIRONMENT_SCOPE';
export const SET_KUBERNETES_VERSION = 'SET_KUBERNETES_VERSION';
export const SET_REGION = 'SET_REGION';
export const SET_VPC = 'SET_VPC';
export const SET_KEY_PAIR = 'SET_KEY_PAIR';
export const SET_SUBNET = 'SET_SUBNET';
export const SET_ROLE = 'SET_ROLE';
export const SET_SECURITY_GROUP = 'SET_SECURITY_GROUP';
export const SET_INSTANCE_TYPE = 'SET_INSTANCE_TYPE';
export const SET_NODE_COUNT = 'SET_NODE_COUNT';
export const SET_GITLAB_MANAGED_CLUSTER = 'SET_GITLAB_MANAGED_CLUSTER';
export const SET_NAMESPACE_PER_ENVIRONMENT = 'SET_NAMESPACE_PER_ENVIRONMENT';
export const REQUEST_CREATE_ROLE = 'REQUEST_CREATE_ROLE';
export const CREATE_ROLE_SUCCESS = 'CREATE_ROLE_SUCCESS';
export const CREATE_ROLE_ERROR = 'CREATE_ROLE_ERROR';
export const REQUEST_CREATE_CLUSTER = 'REQUEST_CREATE_CLUSTER';
export const CREATE_CLUSTER_SUCCESS = 'CREATE_CLUSTER_SUCCESS';
export const CREATE_CLUSTER_ERROR = 'CREATE_CLUSTER_ERROR';

View File

@ -1,66 +0,0 @@
import * as types from './mutation_types';
export default {
[types.SET_CLUSTER_NAME](state, { clusterName }) {
state.clusterName = clusterName;
},
[types.SET_ENVIRONMENT_SCOPE](state, { environmentScope }) {
state.environmentScope = environmentScope;
},
[types.SET_KUBERNETES_VERSION](state, { kubernetesVersion }) {
state.kubernetesVersion = kubernetesVersion;
},
[types.SET_REGION](state, { region }) {
state.selectedRegion = region;
},
[types.SET_KEY_PAIR](state, { keyPair }) {
state.selectedKeyPair = keyPair;
},
[types.SET_VPC](state, { vpc }) {
state.selectedVpc = vpc;
},
[types.SET_SUBNET](state, { subnet }) {
state.selectedSubnet = subnet;
},
[types.SET_ROLE](state, { role }) {
state.selectedRole = role;
},
[types.SET_SECURITY_GROUP](state, { securityGroup }) {
state.selectedSecurityGroup = securityGroup;
},
[types.SET_INSTANCE_TYPE](state, { instanceType }) {
state.selectedInstanceType = instanceType;
},
[types.SET_NODE_COUNT](state, { nodeCount }) {
state.nodeCount = nodeCount;
},
[types.SET_GITLAB_MANAGED_CLUSTER](state, { gitlabManagedCluster }) {
state.gitlabManagedCluster = gitlabManagedCluster;
},
[types.SET_NAMESPACE_PER_ENVIRONMENT](state, { namespacePerEnvironment }) {
state.namespacePerEnvironment = namespacePerEnvironment;
},
[types.REQUEST_CREATE_ROLE](state) {
state.isCreatingRole = true;
state.createRoleError = null;
state.hasCredentials = false;
},
[types.CREATE_ROLE_SUCCESS](state) {
state.isCreatingRole = false;
state.createRoleError = null;
state.hasCredentials = true;
},
[types.CREATE_ROLE_ERROR](state, { error }) {
state.isCreatingRole = false;
state.createRoleError = error;
state.hasCredentials = false;
},
[types.REQUEST_CREATE_CLUSTER](state) {
state.isCreatingCluster = true;
state.createClusterError = null;
},
[types.CREATE_CLUSTER_ERROR](state, { error }) {
state.isCreatingCluster = false;
state.createClusterError = error;
},
};

View File

@ -1,34 +0,0 @@
import { KUBERNETES_VERSIONS } from '../constants';
const kubernetesVersion = KUBERNETES_VERSIONS.find((version) => version.default).value;
export default () => ({
createRolePath: null,
isCreatingRole: false,
roleCreated: false,
createRoleError: false,
accountId: '',
externalId: '',
roleArn: '',
clusterName: '',
environmentScope: '*',
kubernetesVersion,
selectedRegion: '',
selectedRole: '',
selectedKeyPair: '',
selectedVpc: '',
selectedSubnet: [],
selectedSecurityGroup: '',
selectedInstanceType: 'm5.large',
nodeCount: '3',
isCreatingCluster: false,
createClusterError: false,
gitlabManagedCluster: true,
namespacePerEnvironment: true,
});

View File

@ -1,70 +0,0 @@
import { GlLoadingIcon } from '@gitlab/ui';
import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
import DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue';
import DropdownSearchInput from '~/vue_shared/components/dropdown/dropdown_search_input.vue';
import store from '../store';
export default {
store,
components: {
DropdownButton,
DropdownSearchInput,
DropdownHiddenInput,
GlLoadingIcon,
},
props: {
fieldId: {
type: String,
required: true,
},
fieldName: {
type: String,
required: true,
},
defaultValue: {
type: String,
required: false,
default: '',
},
},
data() {
return {
isLoading: false,
hasErrors: false,
searchQuery: '',
gapiError: '',
};
},
computed: {
results() {
if (!this.items) {
return [];
}
return this.items.filter((item) => item.name.toLowerCase().indexOf(this.searchQuery) > -1);
},
},
methods: {
fetchSuccessHandler() {
if (this.defaultValue) {
const itemToSelect = this.items.find((item) => item.name === this.defaultValue);
if (itemToSelect) {
this.setItem(itemToSelect.name);
}
}
this.isLoading = false;
this.hasErrors = false;
},
fetchFailureHandler(resp) {
this.isLoading = false;
this.hasErrors = true;
if (resp.result && resp.result.error) {
this.gapiError = resp.result.error.message;
}
},
},
};

View File

@ -1,112 +0,0 @@
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
import { sprintf, s__ } from '~/locale';
import gkeDropdownMixin from './gke_dropdown_mixin';
export default {
name: 'GkeMachineTypeDropdown',
mixins: [gkeDropdownMixin],
computed: {
...mapState([
'isValidatingProjectBilling',
'projectHasBillingEnabled',
'selectedZone',
'selectedMachineType',
]),
...mapState({ items: 'machineTypes' }),
...mapGetters(['hasZone', 'hasMachineType']),
isDisabled() {
return (
this.isLoading ||
this.isValidatingProjectBilling ||
!this.projectHasBillingEnabled ||
!this.hasZone
);
},
toggleText() {
if (this.isLoading) {
return s__('ClusterIntegration|Fetching machine types');
}
if (this.selectedMachineType) {
return this.selectedMachineType;
}
if (!this.projectHasBillingEnabled && !this.hasZone) {
return s__('ClusterIntegration|Select project and zone to choose machine type');
}
return !this.hasZone
? s__('ClusterIntegration|Select zone to choose machine type')
: s__('ClusterIntegration|Select machine type');
},
errorMessage() {
return sprintf(
s__(
'ClusterIntegration|An error occurred while trying to fetch zone machine types: %{error}',
),
{ error: this.gapiError },
);
},
},
watch: {
selectedZone() {
this.hasErrors = false;
if (this.hasZone) {
this.isLoading = true;
this.fetchMachineTypes().then(this.fetchSuccessHandler).catch(this.fetchFailureHandler);
}
},
},
methods: {
...mapActions(['fetchMachineTypes']),
...mapActions({ setItem: 'setMachineType' }),
},
};
</script>
<template>
<div>
<div class="js-gcp-machine-type-dropdown dropdown">
<dropdown-hidden-input :name="fieldName" :value="selectedMachineType" />
<dropdown-button
:class="{ 'border-danger': hasErrors }"
:is-disabled="isDisabled"
:is-loading="isLoading"
:toggle-text="toggleText"
/>
<div class="dropdown-menu dropdown-select">
<dropdown-search-input
v-model="searchQuery"
:placeholder-text="s__('ClusterIntegration|Search machine types')"
/>
<div class="dropdown-content">
<ul>
<li v-show="!results.length">
<span class="menu-item">
{{ s__('ClusterIntegration|No machine types matched your search') }}
</span>
</li>
<li v-for="result in results" :key="result.id">
<button type="button" @click.prevent="setItem(result.name)">{{ result.name }}</button>
</li>
</ul>
</div>
<div class="dropdown-loading"><gl-loading-icon size="sm" /></div>
</div>
</div>
<span
v-if="hasErrors"
:class="{
'text-danger': hasErrors,
'text-muted': !hasErrors,
}"
class="form-text"
>
{{ errorMessage }}
</span>
</div>
</template>

View File

@ -1,53 +0,0 @@
<script>
import { createNamespacedHelpers, mapState, mapGetters, mapActions } from 'vuex';
import ClusterFormDropdown from '~/create_cluster/components/cluster_form_dropdown.vue';
const { mapState: mapDropdownState } = createNamespacedHelpers('networks');
const { mapActions: mapSubnetworkActions } = createNamespacedHelpers('subnetworks');
export default {
components: {
ClusterFormDropdown,
},
props: {
fieldName: {
type: String,
required: true,
},
},
computed: {
...mapState(['selectedNetwork']),
...mapDropdownState(['items', 'isLoadingItems', 'loadingItemsError']),
...mapGetters(['hasZone', 'projectId', 'region']),
},
methods: {
...mapActions(['setNetwork', 'setSubnetwork']),
...mapSubnetworkActions({ fetchSubnetworks: 'fetchItems' }),
setNetworkAndFetchSubnetworks(network) {
const { projectId: project, region } = this;
this.setSubnetwork('');
this.setNetwork(network);
this.fetchSubnetworks({ project, region, network: network.selfLink });
},
},
};
</script>
<template>
<cluster-form-dropdown
:field-name="fieldName"
:value="selectedNetwork"
:items="items"
:disabled="!hasZone"
:loading="isLoadingItems"
:has-errors="Boolean(loadingItemsError)"
:loading-text="s__('ClusterIntegration|Loading networks')"
:placeholder="s__('ClusterIntegration|Select a network')"
:search-field-placeholder="s__('ClusterIntegration|Search networks')"
:empty-text="s__('ClusterIntegration|No networks found')"
:error-message="s__('ClusterIntegration|Could not load networks')"
:disabled-text="s__('ClusterIntegration|Select a zone to choose a network')"
@input="setNetworkAndFetchSubnetworks"
/>
</template>

View File

@ -1,194 +0,0 @@
<script>
import { GlSprintf, GlLink, GlIcon } from '@gitlab/ui';
import { mapState, mapGetters, mapActions } from 'vuex';
import { s__ } from '~/locale';
import gkeDropdownMixin from './gke_dropdown_mixin';
export default {
name: 'GkeProjectIdDropdown',
components: {
GlSprintf,
GlLink,
GlIcon,
},
mixins: [gkeDropdownMixin],
props: {
docsUrl: {
type: String,
required: true,
},
},
computed: {
...mapState(['selectedProject', 'isValidatingProjectBilling', 'projectHasBillingEnabled']),
...mapState({ items: 'projects' }),
...mapGetters(['hasProject']),
hasOneProject() {
return this.items && this.items.length === 1;
},
isDisabled() {
return (
this.isLoading || this.isValidatingProjectBilling || (this.items && this.items.length < 2)
);
},
toggleText() {
if (this.isValidatingProjectBilling) {
return s__('ClusterIntegration|Validating project billing status');
}
if (this.isLoading) {
return s__('ClusterIntegration|Fetching projects');
}
if (this.hasProject) {
return this.selectedProject.name;
}
if (!this.items) {
return s__('ClusterIntegration|No projects found');
}
return s__('ClusterIntegration|Select project');
},
helpText() {
if (this.hasErrors) {
return this.errorMessage;
}
if (!this.items) {
return s__(
'ClusterIntegration|We were unable to fetch any projects. Ensure that you have a project on %{docsLinkStart}Google Cloud Platform%{docsLinkEnd}.',
);
}
return this.items.length
? s__(
'ClusterIntegration|To use a new project, first create one on %{docsLinkStart}Google Cloud Platform%{docsLinkEnd}.',
)
: s__(
'ClusterIntegration|To create a cluster, first create a project on %{docsLinkStart}Google Cloud Platform%{docsLinkEnd}.',
);
},
errorMessage() {
if (!this.projectHasBillingEnabled) {
if (this.gapiError) {
return s__(
'ClusterIntegration|We could not verify that one of your projects on GCP has billing enabled. Please try again.',
);
}
return s__(
'ClusterIntegration|This project does not have billing enabled. To create a cluster, %{linkToBillingStart}enable billing%{linkToBillingEnd} and try again.',
);
}
return s__(
'ClusterIntegration|An error occurred while trying to fetch your projects: %{error}',
);
},
},
watch: {
selectedProject() {
this.setIsValidatingProjectBilling(true);
this.validateProjectBilling()
.then(this.validateProjectBillingSuccessHandler)
.catch(this.validateProjectBillingFailureHandler);
},
},
created() {
this.isLoading = true;
this.fetchProjects().then(this.fetchSuccessHandler).catch(this.fetchFailureHandler);
},
methods: {
...mapActions(['fetchProjects', 'setIsValidatingProjectBilling', 'validateProjectBilling']),
...mapActions({ setItem: 'setProject' }),
fetchSuccessHandler() {
if (this.defaultValue) {
const projectToSelect = this.items.find((item) => item.projectId === this.defaultValue);
if (projectToSelect) {
this.setItem(projectToSelect);
}
} else if (this.items.length === 1) {
this.setItem(this.items[0]);
}
this.isLoading = false;
this.hasErrors = false;
},
validateProjectBillingSuccessHandler() {
this.hasErrors = !this.projectHasBillingEnabled;
},
validateProjectBillingFailureHandler(resp) {
this.hasErrors = true;
this.gapiError = resp.result ? resp.result.error.message : resp;
},
},
};
</script>
<template>
<div>
<div class="js-gcp-project-id-dropdown dropdown">
<dropdown-hidden-input :name="fieldName" :value="selectedProject.projectId" />
<dropdown-button
:class="{
'border-danger': hasErrors,
'read-only': hasOneProject,
}"
:is-disabled="isDisabled"
:is-loading="isLoading"
:toggle-text="toggleText"
/>
<div class="dropdown-menu dropdown-select">
<dropdown-search-input
v-model="searchQuery"
:placeholder-text="s__('ClusterIntegration|Search projects')"
/>
<div class="dropdown-content">
<ul>
<li v-show="!results.length">
<span class="menu-item">
{{ s__('ClusterIntegration|No projects matched your search') }}
</span>
</li>
<li v-for="result in results" :key="result.project_number">
<button type="button" @click.prevent="setItem(result)">{{ result.name }}</button>
</li>
</ul>
</div>
<div class="dropdown-loading"><gl-loading-icon size="sm" /></div>
</div>
</div>
<span
:class="{
'text-danger': hasErrors,
'text-muted': !hasErrors,
}"
class="form-text"
>
<gl-sprintf :message="helpText">
<template #linkToBilling="{ content }">
<gl-link
:href="'https://console.cloud.google.com/freetrial?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral'"
target="_blank"
>{{ content }} <gl-icon name="external-link"
/></gl-link>
</template>
<template #docsLink="{ content }">
<gl-link :href="docsUrl" target="_blank"
>{{ content }} <gl-icon name="external-link"
/></gl-link>
</template>
<template #error>
{{ gapiError }}
</template>
</gl-sprintf>
</span>
</div>
</template>

View File

@ -1,18 +0,0 @@
<script>
import { mapGetters } from 'vuex';
export default {
computed: {
...mapGetters(['hasValidData']),
},
};
</script>
<template>
<button
type="submit"
:disabled="!hasValidData"
class="js-gke-cluster-creation-submit btn btn-success"
>
{{ s__('ClusterIntegration|Create Kubernetes cluster') }}
</button>
</template>

View File

@ -1,44 +0,0 @@
<script>
import { createNamespacedHelpers, mapState, mapGetters, mapActions } from 'vuex';
import ClusterFormDropdown from '~/create_cluster/components/cluster_form_dropdown.vue';
const { mapState: mapDropdownState } = createNamespacedHelpers('subnetworks');
export default {
components: {
ClusterFormDropdown,
},
props: {
fieldName: {
type: String,
required: true,
},
},
computed: {
...mapState(['selectedSubnetwork']),
...mapDropdownState(['items', 'isLoadingItems', 'loadingItemsError']),
...mapGetters(['hasNetwork']),
},
methods: {
...mapActions(['setSubnetwork']),
},
};
</script>
<template>
<cluster-form-dropdown
:field-name="fieldName"
:value="selectedSubnetwork"
:items="items"
:disabled="!hasNetwork"
:loading="isLoadingItems"
:has-errors="Boolean(loadingItemsError)"
:loading-text="s__('ClusterIntegration|Loading subnetworks')"
:placeholder="s__('ClusterIntegration|Select a subnetwork')"
:search-field-placeholder="s__('ClusterIntegration|Search subnetworks')"
:empty-text="s__('ClusterIntegration|No subnetworks found')"
:error-message="s__('ClusterIntegration|Could not load subnetworks')"
:disabled-text="s__('ClusterIntegration|Select a network to choose a subnetwork')"
@input="setSubnetwork"
/>
</template>

View File

@ -1,101 +0,0 @@
<script>
import { mapState, mapActions } from 'vuex';
import { sprintf, s__ } from '~/locale';
import gkeDropdownMixin from './gke_dropdown_mixin';
export default {
name: 'GkeZoneDropdown',
mixins: [gkeDropdownMixin],
computed: {
...mapState([
'selectedProject',
'selectedZone',
'projects',
'isValidatingProjectBilling',
'projectHasBillingEnabled',
]),
...mapState({ items: 'zones' }),
isDisabled() {
return this.isLoading || this.isValidatingProjectBilling || !this.projectHasBillingEnabled;
},
toggleText() {
if (this.isLoading) {
return s__('ClusterIntegration|Fetching zones');
}
if (this.selectedZone) {
return this.selectedZone;
}
return !this.projectHasBillingEnabled
? s__('ClusterIntegration|Select project to choose zone')
: s__('ClusterIntegration|Select zone');
},
errorMessage() {
return sprintf(
s__('ClusterIntegration|An error occurred while trying to fetch project zones: %{error}'),
{ error: this.gapiError },
);
},
},
watch: {
isValidatingProjectBilling(isValidating) {
this.hasErrors = false;
if (!isValidating && this.projectHasBillingEnabled) {
this.isLoading = true;
this.fetchZones().then(this.fetchSuccessHandler).catch(this.fetchFailureHandler);
}
},
},
methods: {
...mapActions(['fetchZones']),
...mapActions({ setItem: 'setZone' }),
},
};
</script>
<template>
<div>
<div class="js-gcp-zone-dropdown dropdown">
<dropdown-hidden-input :name="fieldName" :value="selectedZone" />
<dropdown-button
:class="{ 'border-danger': hasErrors }"
:is-disabled="isDisabled"
:is-loading="isLoading"
:toggle-text="toggleText"
/>
<div class="dropdown-menu dropdown-select">
<dropdown-search-input
v-model="searchQuery"
:placeholder-text="s__('ClusterIntegration|Search zones')"
/>
<div class="dropdown-content">
<ul>
<li v-show="!results.length">
<span class="menu-item">
{{ s__('ClusterIntegration|No zones matched your search') }}
</span>
</li>
<li v-for="result in results" :key="result.id">
<button type="button" @click.prevent="setItem(result.name)">{{ result.name }}</button>
</li>
</ul>
</div>
<div class="dropdown-loading"><gl-loading-icon size="sm" /></div>
</div>
</div>
<span
v-if="hasErrors"
:class="{
'text-danger': hasErrors,
'text-muted': !hasErrors,
}"
class="form-text"
>
{{ errorMessage }}
</span>
</div>
</template>

View File

@ -1,11 +0,0 @@
import { s__ } from '~/locale';
export const GCP_API_ERROR = s__(
'ClusterIntegration|An error occurred when trying to contact the Google Cloud API. Please try again later.',
);
export const GCP_API_CLOUD_BILLING_ENDPOINT =
'https://www.googleapis.com/discovery/v1/apis/cloudbilling/v1/rest';
export const GCP_API_CLOUD_RESOURCE_MANAGER_ENDPOINT =
'https://www.googleapis.com/discovery/v1/apis/cloudresourcemanager/v1/rest';
export const GCP_API_COMPUTE_ENDPOINT =
'https://www.googleapis.com/discovery/v1/apis/compute/v1/rest';

View File

@ -1,24 +0,0 @@
// This is a helper module to lazily import the google APIs for the GKE cluster
// integration without introducing an indirect global dependency on an
// initialized window.gapi object.
export default () => {
if (window.gapiPromise === undefined) {
// first time loading the module
window.gapiPromise = new Promise((resolve, reject) => {
// this callback is set as a query param to script.src URL
window.onGapiLoad = () => {
resolve(window.gapi);
};
const script = document.createElement('script');
// do not use script.onload, because gapi continues to load after the initial script load
script.type = 'text/javascript';
script.async = true;
script.src = 'https://apis.google.com/js/api.js?onload=onGapiLoad';
script.onerror = reject;
document.head.appendChild(script);
});
}
return window.gapiPromise;
};

View File

@ -1,95 +0,0 @@
import Vue from 'vue';
import createFlash from '~/flash';
import GkeMachineTypeDropdown from './components/gke_machine_type_dropdown.vue';
import GkeProjectIdDropdown from './components/gke_project_id_dropdown.vue';
import GkeSubmitButton from './components/gke_submit_button.vue';
import GkeZoneDropdown from './components/gke_zone_dropdown.vue';
import * as CONSTANTS from './constants';
import gapiLoader from './gapi_loader';
import store from './store';
const mountComponent = (entryPoint, component, componentName, extraProps = {}) => {
const el = document.querySelector(entryPoint);
if (!el) return false;
const hiddenInput = el.querySelector('input');
return new Vue({
el,
store,
components: {
[componentName]: component,
},
render: (createElement) =>
createElement(componentName, {
props: {
fieldName: hiddenInput.getAttribute('name'),
fieldId: hiddenInput.getAttribute('id'),
defaultValue: hiddenInput.value,
...extraProps,
},
}),
});
};
const mountGkeProjectIdDropdown = () => {
const entryPoint = '.js-gcp-project-id-dropdown-entry-point';
const el = document.querySelector(entryPoint);
mountComponent(entryPoint, GkeProjectIdDropdown, 'gke-project-id-dropdown', {
docsUrl: el.dataset.docsurl,
});
};
const mountGkeZoneDropdown = () => {
mountComponent('.js-gcp-zone-dropdown-entry-point', GkeZoneDropdown, 'gke-zone-dropdown');
};
const mountGkeMachineTypeDropdown = () => {
mountComponent(
'.js-gcp-machine-type-dropdown-entry-point',
GkeMachineTypeDropdown,
'gke-machine-type-dropdown',
);
};
const mountGkeSubmitButton = () => {
mountComponent('.js-gke-cluster-creation-submit-container', GkeSubmitButton, 'gke-submit-button');
};
const gkeDropdownErrorHandler = () => {
createFlash({
message: CONSTANTS.GCP_API_ERROR,
});
};
const initializeGapiClient = (gapi) => () => {
const el = document.querySelector('.js-gke-cluster-creation');
if (!el) return false;
return gapi.client
.init({
discoveryDocs: [
CONSTANTS.GCP_API_CLOUD_BILLING_ENDPOINT,
CONSTANTS.GCP_API_CLOUD_RESOURCE_MANAGER_ENDPOINT,
CONSTANTS.GCP_API_COMPUTE_ENDPOINT,
],
})
.then(() => {
gapi.client.setToken({ access_token: el.dataset.token });
mountGkeProjectIdDropdown();
mountGkeZoneDropdown();
mountGkeMachineTypeDropdown();
mountGkeSubmitButton();
})
.catch(gkeDropdownErrorHandler);
};
const initGkeDropdowns = () =>
gapiLoader()
.then((gapi) => gapi.load('client', initializeGapiClient(gapi)))
.catch(gkeDropdownErrorHandler);
export default initGkeDropdowns;

View File

@ -1,99 +0,0 @@
import gapiLoader from '../gapi_loader';
import * as types from './mutation_types';
const gapiResourceListRequest = ({ resource, params, commit, mutation, payloadKey }) =>
new Promise((resolve, reject) => {
const request = resource.list(params);
return request.then(
(resp) => {
const { result } = resp;
commit(mutation, result[payloadKey]);
resolve();
},
(resp) => {
reject(resp);
},
);
});
export const setProject = ({ commit }, selectedProject) => {
commit(types.SET_PROJECT, selectedProject);
};
export const setZone = ({ commit }, selectedZone) => {
commit(types.SET_ZONE, selectedZone);
};
export const setMachineType = ({ commit }, selectedMachineType) => {
commit(types.SET_MACHINE_TYPE, selectedMachineType);
};
export const setIsValidatingProjectBilling = ({ commit }, isValidatingProjectBilling) => {
commit(types.SET_IS_VALIDATING_PROJECT_BILLING, isValidatingProjectBilling);
};
export const fetchProjects = ({ commit }) =>
gapiLoader().then((gapi) =>
gapiResourceListRequest({
resource: gapi.client.cloudresourcemanager.projects,
params: {},
commit,
mutation: types.SET_PROJECTS,
payloadKey: 'projects',
}),
);
export const validateProjectBilling = ({ dispatch, commit, state }) =>
gapiLoader()
.then((gapi) => {
const request = gapi.client.cloudbilling.projects.getBillingInfo({
name: `projects/${state.selectedProject.projectId}`,
});
commit(types.SET_ZONE, '');
commit(types.SET_MACHINE_TYPE, '');
return request;
})
.then(
(resp) => {
const { billingEnabled } = resp.result;
commit(types.SET_PROJECT_BILLING_STATUS, Boolean(billingEnabled));
dispatch('setIsValidatingProjectBilling', false);
},
(errorResp) => {
dispatch('setIsValidatingProjectBilling', false);
return errorResp;
},
);
export const fetchZones = ({ commit, state }) =>
gapiLoader().then((gapi) =>
gapiResourceListRequest({
resource: gapi.client.compute.zones,
params: {
project: state.selectedProject.projectId,
},
commit,
mutation: types.SET_ZONES,
payloadKey: 'items',
}),
);
export const fetchMachineTypes = ({ commit, state }) =>
gapiLoader().then((gapi) =>
gapiResourceListRequest({
resource: gapi.client.compute.machineTypes,
params: {
project: state.selectedProject.projectId,
zone: state.selectedZone,
},
commit,
mutation: types.SET_MACHINE_TYPES,
payloadKey: 'items',
}),
);

View File

@ -1,5 +0,0 @@
export const hasProject = (state) => Boolean(state.selectedProject.projectId);
export const hasZone = (state) => Boolean(state.selectedZone);
export const hasMachineType = (state) => Boolean(state.selectedMachineType);
export const hasValidData = (state, getters) =>
Boolean(state.projectHasBillingEnabled) && getters.hasZone && getters.hasMachineType;

View File

@ -1,18 +0,0 @@
import Vue from 'vue';
import Vuex from 'vuex';
import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
import createState from './state';
Vue.use(Vuex);
export const createStore = () =>
new Vuex.Store({
actions,
getters,
mutations,
state: createState(),
});
export default createStore();

View File

@ -1,8 +0,0 @@
export const SET_PROJECT = 'SET_PROJECT';
export const SET_PROJECT_BILLING_STATUS = 'SET_PROJECT_BILLING_STATUS';
export const SET_IS_VALIDATING_PROJECT_BILLING = 'SET_IS_VALIDATING_PROJECT_BILLING';
export const SET_ZONE = 'SET_ZONE';
export const SET_MACHINE_TYPE = 'SET_MACHINE_TYPE';
export const SET_PROJECTS = 'SET_PROJECTS';
export const SET_ZONES = 'SET_ZONES';
export const SET_MACHINE_TYPES = 'SET_MACHINE_TYPES';

View File

@ -1,28 +0,0 @@
import * as types from './mutation_types';
export default {
[types.SET_PROJECT](state, selectedProject) {
Object.assign(state, { selectedProject });
},
[types.SET_IS_VALIDATING_PROJECT_BILLING](state, isValidatingProjectBilling) {
Object.assign(state, { isValidatingProjectBilling });
},
[types.SET_PROJECT_BILLING_STATUS](state, projectHasBillingEnabled) {
Object.assign(state, { projectHasBillingEnabled });
},
[types.SET_ZONE](state, selectedZone) {
Object.assign(state, { selectedZone });
},
[types.SET_MACHINE_TYPE](state, selectedMachineType) {
Object.assign(state, { selectedMachineType });
},
[types.SET_PROJECTS](state, projects) {
Object.assign(state, { projects });
},
[types.SET_ZONES](state, zones) {
Object.assign(state, { zones });
},
[types.SET_MACHINE_TYPES](state, machineTypes) {
Object.assign(state, { machineTypes });
},
};

View File

@ -1,13 +0,0 @@
export default () => ({
selectedProject: {
projectId: '',
name: '',
},
selectedZone: '',
selectedMachineType: '',
isValidatingProjectBilling: null,
projectHasBillingEnabled: null,
projects: [],
zones: [],
machineTypes: [],
});

View File

@ -1,35 +0,0 @@
import PersistentUserCallout from '~/persistent_user_callout';
import initGkeDropdowns from './gke_cluster';
import initGkeNamespace from './gke_cluster_namespace';
const newClusterViews = [':clusters:new', ':clusters:create_gcp', ':clusters:create_user'];
const isProjectLevelCluster = (page) => page.startsWith('project:clusters');
export default (document) => {
const { page } = document.body.dataset;
const isNewClusterView = newClusterViews.some((view) => page.endsWith(view));
if (!isNewClusterView) {
return;
}
const callout = document.querySelector('.gcp-signup-offer');
PersistentUserCallout.factory(callout);
initGkeDropdowns();
if (isProjectLevelCluster(page)) {
initGkeNamespace();
}
import(/* webpackChunkName: 'eks_cluster' */ '~/create_cluster/eks_cluster')
.then(({ default: initCreateEKSCluster }) => {
const el = document.querySelector('.js-create-eks-cluster-form-container');
if (el) {
initCreateEKSCluster(el);
}
})
.catch(() => {});
};

View File

@ -1,14 +0,0 @@
import * as types from './mutation_types';
export default (fetchItems) => ({
requestItems: ({ commit }) => commit(types.REQUEST_ITEMS),
receiveItemsSuccess: ({ commit }, payload) => commit(types.RECEIVE_ITEMS_SUCCESS, payload),
receiveItemsError: ({ commit }, payload) => commit(types.RECEIVE_ITEMS_ERROR, payload),
fetchItems: ({ dispatch }, payload) => {
dispatch('requestItems');
return fetchItems(payload)
.then((items) => dispatch('receiveItemsSuccess', { items }))
.catch((error) => dispatch('receiveItemsError', { error }));
},
});

View File

@ -1,13 +0,0 @@
import actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
import state from './state';
const createStore = ({ fetchFn, initialState }) => ({
actions: actions(fetchFn),
getters,
mutations,
state: Object.assign(state(), initialState || {}),
});
export default createStore;

View File

@ -1,3 +0,0 @@
export const REQUEST_ITEMS = 'REQUEST_ITEMS';
export const RECEIVE_ITEMS_SUCCESS = 'REQUEST_ITEMS_SUCCESS';
export const RECEIVE_ITEMS_ERROR = 'RECEIVE_ITEMS_ERROR';

View File

@ -1,16 +0,0 @@
import * as types from './mutation_types';
export default {
[types.REQUEST_ITEMS](state) {
state.isLoadingItems = true;
state.loadingItemsError = null;
},
[types.RECEIVE_ITEMS_SUCCESS](state, { items }) {
state.isLoadingItems = false;
state.items = items;
},
[types.RECEIVE_ITEMS_ERROR](state, { error }) {
state.isLoadingItems = false;
state.loadingItemsError = error;
},
};

View File

@ -1,5 +0,0 @@
export default () => ({
isLoadingItems: false,
items: [],
loadingItemsError: null,
});

View File

@ -45,6 +45,8 @@ export default {
copyToClipboardText: s__('EnableReviewApp|Copy snippet text'),
title: s__('ReviewApp|Enable Review App'),
},
visualReviewsDocs: helpPagePath('ci/review_apps/index.md', { anchor: 'visual-reviews' }),
connectClusterDocs: helpPagePath('user/clusters/agent/index'),
data() {
const modalInfoCopyId = uniqueId('enable-review-app-copy-string-');
@ -64,9 +66,6 @@ export default {
except:
- ${this.defaultBranchName}`;
},
visualReviewsDocs() {
return helpPagePath('ci/review_apps/index.md', { anchor: 'visual-reviews' });
},
},
};
</script>
@ -88,11 +87,7 @@ export default {
<strong>{{ content }}</strong>
</template>
<template #link="{ content }">
<gl-link
href="https://docs.gitlab.com/ee/user/project/clusters/add_remove_clusters.html"
target="_blank"
>{{ content }}</gl-link
>
<gl-link :href="$options.connectClusterDocs" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
</p>
@ -134,7 +129,7 @@ export default {
<strong>{{ content }}</strong>
</template>
<template #link="{ content }">
<gl-link :href="visualReviewsDocs" target="_blank">{{ content }}</gl-link>
<gl-link :href="$options.visualReviewsDocs" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
</p>

View File

@ -44,6 +44,11 @@ const STATUS_MAP = {
text: __('Failed'),
variant: 'danger',
},
[STATUSES.TIMEOUT]: {
icon: 'status-failed',
text: __('Timeout'),
variant: 'danger',
},
[STATUSES.CANCELLED]: {
icon: 'status-stopped',
text: __('Cancelled'),

View File

@ -10,4 +10,5 @@ export const STATUSES = {
NONE: 'none',
SCHEDULING: 'scheduling',
CANCELLED: 'cancelled',
TIMEOUT: 'timeout',
};

View File

@ -10,7 +10,7 @@ export function getInvalidNameValidationMessage(importTarget) {
}
export function isFinished(group) {
return [STATUSES.FINISHED, STATUSES.FAILED].includes(group.progress?.status);
return [STATUSES.FINISHED, STATUSES.FAILED, STATUSES.TIMEOUT].includes(group.progress?.status);
}
export function isAvailableForImport(group) {

View File

@ -1,3 +0,0 @@
import initCreateCluster from '~/create_cluster/init_create_cluster';
initCreateCluster(document, gon);

View File

@ -1,5 +1,3 @@
import initIntegrationForm from '~/clusters/forms/show/index';
import initCreateCluster from '~/create_cluster/init_create_cluster';
initCreateCluster(document, gon);
initIntegrationForm();

View File

@ -1,3 +0,0 @@
import initCreateCluster from '~/create_cluster/init_create_cluster';
initCreateCluster(document, gon);

View File

@ -1,6 +1,6 @@
import ClustersBundle from '~/clusters/clusters_bundle';
import initIntegrationForm from '~/clusters/forms/show';
import initGkeNamespace from '~/create_cluster/gke_cluster_namespace';
import initGkeNamespace from '~/clusters/gke_cluster_namespace';
import initClusterHealth from './cluster_health';
new ClustersBundle(); // eslint-disable-line no-new

View File

@ -19,7 +19,7 @@ export default {
type: String,
required: true,
},
runnerUrl: {
runnerPath: {
type: String,
required: false,
default: null,
@ -66,7 +66,7 @@ export default {
<runner-update-form
:loading="loading"
:runner="runner"
:runner-url="runnerUrl"
:runner-path="runnerPath"
class="gl-my-5"
/>
</div>

View File

@ -12,7 +12,7 @@ export const initAdminRunnerEdit = (selector = '#js-admin-runner-edit') => {
return null;
}
const { runnerId, runnerUrl } = el.dataset;
const { runnerId, runnerPath } = el.dataset;
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
@ -25,7 +25,7 @@ export const initAdminRunnerEdit = (selector = '#js-admin-runner-edit') => {
return h(AdminRunnerEditApp, {
props: {
runnerId,
runnerUrl,
runnerPath,
},
});
},

View File

@ -1,11 +1,14 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import { showAlertFromLocalStorage } from '../local_storage_alert/show_alert_from_local_storage';
import AdminRunnerShowApp from './admin_runner_show_app.vue';
Vue.use(VueApollo);
export const initAdminRunnerShow = (selector = '#js-admin-runner-show') => {
showAlertFromLocalStorage();
const el = document.querySelector(selector);
if (!el) {

View File

@ -14,10 +14,12 @@ import {
runnerToModel,
} from 'ee_else_ce/runner/runner_update_form_utils';
import { createAlert, VARIANT_SUCCESS } from '~/flash';
import { redirectTo } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import { captureException } from '~/runner/sentry_utils';
import { ACCESS_LEVEL_NOT_PROTECTED, ACCESS_LEVEL_REF_PROTECTED, PROJECT_TYPE } from '../constants';
import runnerUpdateMutation from '../graphql/details/runner_update.mutation.graphql';
import { saveAlertToLocalStorage } from '../local_storage_alert/save_alert_to_local_storage';
export default {
name: 'RunnerUpdateForm',
@ -46,7 +48,7 @@ export default {
required: false,
default: false,
},
runnerUrl: {
runnerPath: {
type: String,
required: false,
default: null,
@ -85,24 +87,23 @@ export default {
});
if (errors?.length) {
// Validation errors need not be thrown
createAlert({ message: errors[0] });
return;
this.onError(errors[0]);
} else {
this.onSuccess();
}
this.onSuccess();
} catch (error) {
const { message } = error;
createAlert({ message });
this.onError(message);
captureException({ error, component: this.$options.name });
} finally {
this.saving = false;
}
},
onSuccess() {
createAlert({ message: __('Changes saved.'), variant: VARIANT_SUCCESS });
this.model = runnerToModel(this.runner);
saveAlertToLocalStorage({ message: __('Changes saved.'), variant: VARIANT_SUCCESS });
redirectTo(this.runnerPath);
},
onError(message) {
this.saving = false;
createAlert({ message });
},
},
ACCESS_LEVEL_NOT_PROTECTED,
@ -210,7 +211,7 @@ export default {
>
{{ __('Save changes') }}
</gl-button>
<gl-button :href="runnerUrl">
<gl-button :href="runnerPath">
{{ __('Cancel') }}
</gl-button>
</div>

View File

@ -1,18 +0,0 @@
<script>
export default {
props: {
name: {
type: String,
required: true,
},
value: {
type: [Number, String],
required: true,
},
},
};
</script>
<template>
<input :name="name" :value="value" type="hidden" />
</template>

View File

@ -1,49 +0,0 @@
<script>
import { GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
export default {
components: {
GlIcon,
},
props: {
placeholderText: {
type: String,
required: true,
default: __('Search'),
},
focused: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return { searchQuery: this.value };
},
watch: {
searchQuery(query) {
this.$emit('input', query);
},
focused(val) {
if (val) {
this.$refs.searchInput.focus();
}
},
},
};
</script>
<template>
<div class="dropdown-input">
<input
ref="searchInput"
v-model="searchQuery"
:placeholder="placeholderText"
class="dropdown-input-field"
type="search"
autocomplete="off"
/>
<gl-icon name="search" class="dropdown-input-search" data-hidden="true" />
</div>
</template>

View File

@ -6,12 +6,9 @@ class Clusters::ClustersController < Clusters::BaseController
include MetricsDashboard
before_action :cluster, only: [:cluster_status, :show, :update, :destroy, :clear_cache]
before_action :generate_gcp_authorize_url, only: [:new]
before_action :validate_gcp_token, only: [:new]
before_action :gcp_cluster, only: [:new]
before_action :user_cluster, only: [:new, :connect]
before_action :user_cluster, only: [:connect]
before_action :authorize_read_cluster!, only: [:show, :index]
before_action :authorize_create_cluster!, only: [:new, :connect, :authorize_aws_role]
before_action :authorize_create_cluster!, only: [:connect, :authorize_aws_role]
before_action :authorize_update_cluster!, only: [:update]
before_action :update_applications_status, only: [:cluster_status]
before_action :ensure_feature_enabled!, except: [:index, :new_cluster_docs]
@ -46,16 +43,6 @@ class Clusters::ClustersController < Clusters::BaseController
end
end
def new
if params[:provider] == 'aws'
@aws_role = Aws::Role.create_or_find_by!(user: current_user)
@instance_types = load_instance_types.to_json
elsif params[:provider] == 'gcp'
redirect_to @authorize_url if @authorize_url && !@valid_gcp_token
end
end
# Overridding ActionController::Metal#status is NOT a good idea
def cluster_status
respond_to do |format|
@ -108,24 +95,6 @@ class Clusters::ClustersController < Clusters::BaseController
redirect_to clusterable.index_path, status: :found
end
def create_gcp
@gcp_cluster = ::Clusters::CreateService
.new(current_user, create_gcp_cluster_params)
.execute(access_token: token_in_session)
.present(current_user: current_user)
if @gcp_cluster.persisted?
redirect_to @gcp_cluster.show_path
else
generate_gcp_authorize_url
validate_gcp_token
user_cluster
params[:provider] = 'gcp'
render :new, locals: { active_tab: 'create' }
end
end
def create_aws
@aws_cluster = ::Clusters::CreateService
.new(current_user, create_aws_cluster_params)
@ -235,24 +204,6 @@ class Clusters::ClustersController < Clusters::BaseController
end
end
def create_gcp_cluster_params
params.require(:cluster).permit(
*base_permitted_cluster_params,
:name,
provider_gcp_attributes: [
:gcp_project_id,
:zone,
:num_nodes,
:machine_type,
:cloud_run,
:legacy_abac
]).merge(
provider_type: :gcp,
platform_type: :kubernetes,
clusterable: clusterable.__subject__
)
end
def create_aws_cluster_params
params.require(:cluster).permit(
*base_permitted_cluster_params,
@ -296,10 +247,10 @@ class Clusters::ClustersController < Clusters::BaseController
end
def generate_gcp_authorize_url
new_path = clusterable.new_path(provider: :gcp).to_s
error_path = @project ? project_clusters_path(@project) : new_path
connect_path = clusterable.connect_path().to_s
error_path = @project ? project_clusters_path(@project) : connect_path
state = generate_session_key_redirect(new_path, error_path)
state = generate_session_key_redirect(connect_path, error_path)
@authorize_url = GoogleApi::CloudPlatform::Client.new(
nil, callback_google_api_auth_url,

View File

@ -17,7 +17,6 @@ module ClustersHelper
clusters_empty_state_image: image_path('illustrations/empty-state/empty-state-clusters.svg'),
empty_state_image: image_path('illustrations/empty-state/empty-state-agents.svg'),
empty_state_help_text: clusterable.empty_state_help_text,
new_cluster_path: clusterable.new_path,
add_cluster_path: clusterable.connect_path,
new_cluster_docs_path: clusterable.new_cluster_docs_path,
can_add_cluster: clusterable.can_add_cluster?.to_s,
@ -43,12 +42,6 @@ module ClustersHelper
}
end
def js_cluster_new
{
cluster_connect_help_path: help_page_path('user/project/clusters/add_remove_clusters', anchor: 'add-existing-cluster')
}
end
def render_gcp_signup_offer
return if Gitlab::CurrentSettings.current_application_settings.hide_third_party_offers?
return unless show_gcp_signup_offer?

View File

@ -155,6 +155,20 @@ module Emails
end
end
def approved_merge_request_email(recipient_id, merge_request_id, approved_by_user_id, reason = nil)
setup_merge_request_mail(merge_request_id, recipient_id)
@approved_by = User.find(approved_by_user_id)
mail_answer_thread(@merge_request, merge_request_thread_options(approved_by_user_id, reason))
end
def unapproved_merge_request_email(recipient_id, merge_request_id, unapproved_by_user_id, reason = nil)
setup_merge_request_mail(merge_request_id, recipient_id)
@unapproved_by = User.find(unapproved_by_user_id)
mail_answer_thread(@merge_request, merge_request_thread_options(unapproved_by_user_id, reason))
end
private
def setup_merge_request_mail(merge_request_id, recipient_id, present: false)

View File

@ -14,9 +14,16 @@ module Integrations
logger.error(message)
end
def log_exception(error, params = {})
Gitlab::ExceptionLogFormatter.format!(error, params)
log_error(params[:message] || error.message, params)
end
def build_message(message, params = {})
{
integration_class: self.class.name,
integration_id: id,
project_id: project&.id,
project_path: project&.full_path,
message: message

View File

@ -352,16 +352,7 @@ module Integrations
true
rescue StandardError => error
log_error(
"Issue transition failed",
error: {
exception_class: error.class.name,
exception_message: error.message,
exception_backtrace: Gitlab::BacktraceCleaner.clean_backtrace(error.backtrace)
},
client_url: client_url
)
log_exception(error, message: 'Issue transition failed', client_url: client_url)
false
end
@ -538,9 +529,7 @@ module Integrations
yield
rescue StandardError => error
@error = error
payload = { client_url: client_url }
Gitlab::ExceptionLogFormatter.format!(error, payload)
log_error("Error sending message", payload)
log_exception(error, message: 'Error sending message', client_url: client_url)
nil
end

View File

@ -28,10 +28,6 @@ class ClusterablePresenter < Gitlab::View::Presenter::Delegated
polymorphic_path([clusterable, :clusters], options)
end
def new_path(options = {})
new_polymorphic_path([clusterable, :cluster], options)
end
def connect_path
polymorphic_path([clusterable, :clusters], action: :connect)
end
@ -40,22 +36,10 @@ class ClusterablePresenter < Gitlab::View::Presenter::Delegated
polymorphic_path([clusterable, :clusters], action: :new_cluster_docs)
end
def authorize_aws_role_path
polymorphic_path([clusterable, :clusters], action: :authorize_aws_role)
end
def create_user_clusters_path
polymorphic_path([clusterable, :clusters], action: :create_user)
end
def create_gcp_clusters_path
polymorphic_path([clusterable, :clusters], action: :create_gcp)
end
def create_aws_clusters_path
polymorphic_path([clusterable, :clusters], action: :create_aws)
end
def cluster_status_cluster_path(cluster, params = {})
raise NotImplementedError
end

View File

@ -18,11 +18,6 @@ class InstanceClusterablePresenter < ClusterablePresenter
admin_clusters_path(options)
end
override :new_path
def new_path(options = {})
new_admin_cluster_path(options)
end
override :cluster_status_cluster_path
def cluster_status_cluster_path(cluster, params = {})
cluster_status_admin_cluster_path(cluster, params)
@ -53,21 +48,6 @@ class InstanceClusterablePresenter < ClusterablePresenter
create_user_admin_clusters_path
end
override :create_gcp_clusters_path
def create_gcp_clusters_path
create_gcp_admin_clusters_path
end
override :create_aws_clusters_path
def create_aws_clusters_path
create_aws_admin_clusters_path
end
override :authorize_aws_role_path
def authorize_aws_role_path
authorize_aws_role_admin_clusters_path
end
override :empty_state_help_text
def empty_state_help_text
s_('ClusterIntegration|Adding an integration will share the cluster across all projects.')

View File

@ -3,8 +3,6 @@
module Jira
module Requests
class Base
include Integrations::Loggable
JIRA_API_VERSION = 2
# Limit the size of the JSON error message we will attempt to parse, as the JSON is external input.
JIRA_ERROR_JSON_SIZE_LIMIT = 5_000
@ -54,17 +52,13 @@ module Jira
def request
response = client.get(url)
build_service_response(response)
rescue *ALL_ERRORS => e
log_error('Error sending message',
client_url: client.options[:site],
error: {
exception_class: e.class.name,
exception_message: e.message,
exception_backtrace: Gitlab::BacktraceCleaner.clean_backtrace(e.backtrace)
}
rescue *ALL_ERRORS => error
jira_integration.log_exception(error,
message: 'Error sending message',
client_url: client.options[:site]
)
ServiceResponse.error(message: error_message(e))
ServiceResponse.error(message: error_message(error))
end
def auth_docs_link_start

View File

@ -33,6 +33,8 @@ module MergeRequests
def execute_approval_hooks(merge_request, current_user)
# Only one approval is required for a merge request to be approved
notification_service.async.approve_mr(merge_request, current_user)
execute_hooks(merge_request, 'approved')
end

View File

@ -33,6 +33,7 @@ module MergeRequests
def trigger_approval_hooks(merge_request)
yield
notification_service.async.unapprove_mr(merge_request, current_user)
execute_hooks(merge_request, 'unapproved')
end

View File

@ -761,6 +761,14 @@ class NotificationService
mailer.in_product_marketing_email(user_id, group_id, track, series).deliver_later
end
def approve_mr(merge_request, current_user)
approve_mr_email(merge_request, merge_request.target_project, current_user)
end
def unapprove_mr(merge_request, current_user)
unapprove_mr_email(merge_request, merge_request.target_project, current_user)
end
protected
def new_resource_email(target, current_user, method)
@ -866,6 +874,22 @@ class NotificationService
private
def approve_mr_email(merge_request, project, current_user)
recipients = ::NotificationRecipients::BuildService.build_recipients(merge_request, current_user, action: 'approve')
recipients.each do |recipient|
mailer.approved_merge_request_email(recipient.user.id, merge_request.id, current_user.id).deliver_later
end
end
def unapprove_mr_email(merge_request, project, current_user)
recipients = ::NotificationRecipients::BuildService.build_recipients(merge_request, current_user, action: 'unapprove')
recipients.each do |recipient|
mailer.unapproved_merge_request_email(recipient.user.id, merge_request.id, current_user.id).deliver_later
end
end
def pipeline_notification_status(ref_status, pipeline)
if Ci::Ref.failing_state?(ref_status)
'failed'

View File

@ -8,7 +8,7 @@
- breadcrumb_title runner_name
- page_title runner_name
#js-admin-runner-edit{ data: {runner_id: @runner.id, runner_url: admin_runner_path(@runner) } }
#js-admin-runner-edit{ data: {runner_id: @runner.id, runner_path: admin_runner_path(@runner) } }
- if @runner.project_type?
.gl-overflow-auto

View File

@ -1,5 +1,4 @@
- is_connect_page = local_assigns.fetch(:is_connect_page, false)
- docs_mode = local_assigns.fetch(:docs_mode, false)
- title = is_connect_page ? s_('ClusterIntegration|Connect a Kubernetes cluster') : s_('ClusterIntegration|Create a Kubernetes cluster')
%h3
@ -7,7 +6,7 @@
%p
= clusterable.sidebar_text
- if !docs_mode
- if is_connect_page
%p
= clusterable.learn_more_link

View File

@ -1,17 +0,0 @@
- if !Gitlab::CurrentSettings.eks_integration_enabled?
- documentation_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/project/clusters/add_eks_clusters.md',
anchor: 'additional-requirements-for-self-managed-instances') }
= s_('Amazon authentication is not %{link_start}correctly configured%{link_end}. Ask your GitLab administrator if you want to use this service.').html_safe % { link_start: documentation_link_start, link_end: '<a/>'.html_safe }
- else
.js-create-eks-cluster-form-container{ data: { 'gitlab-managed-cluster-help-path' => help_page_path('user/project/clusters/gitlab_managed_clusters.md'),
'namespace-per-environment-help-path' => help_page_path('user/project/clusters/deploy_to_cluster.md', anchor: 'custom-namespace'),
'create-role-path' => clusterable.authorize_aws_role_path,
'create-cluster-path' => clusterable.create_aws_clusters_path,
'account-id' => Gitlab::CurrentSettings.eks_account_id,
'external-id' => @aws_role.role_external_id,
'role-arn' => @aws_role.role_arn,
'instance-types' => @instance_types,
'kubernetes-integration-help-path' => help_page_path('user/infrastructure/clusters/index.md'),
'account-and-external-ids-help-path' => help_page_path('user/project/clusters/add_eks_clusters.md', anchor: 'how-to-create-a-new-cluster-on-eks-through-cluster-certificates-deprecated'),
'create-role-arn-help-path' => help_page_path('user/project/clusters/add_eks_clusters.md', anchor: 'how-to-create-a-new-cluster-on-eks-through-cluster-certificates-deprecated'),
'external-link-icon' => sprite_icon('external-link') } }

View File

@ -1,15 +1,11 @@
- provider = local_assigns.fetch(:provider)
- is_current_provider = provider == params[:provider]
- logo_path = local_assigns.fetch(:logo_path)
- help_path = local_assigns.fetch(:help_path)
- label = local_assigns.fetch(:label)
- last = local_assigns.fetch(:last, false)
- docs_mode = local_assigns.fetch(:docs_mode, false)
- classes = ["btn btn-confirm gl-button btn-confirm-secondary gl-flex-direction-column gl-w-half"]
- conditional_classes = [("gl-mr-5" unless last), ("active" if is_current_provider && !docs_mode), ("js-create-#{provider}-cluster-button" if !docs_mode)]
- link = docs_mode ? help_path : clusterable.new_path(provider: provider)
- conditional_classes = [("gl-mr-5" unless last)]
= link_to link, class: classes + conditional_classes do
= link_to help_path, class: classes + conditional_classes do
.svg-content.gl-p-3= image_tag logo_path, alt: label, class: "gl-w-64 gl-h-64"
%span
= label

View File

@ -3,13 +3,12 @@
- create_cluster_label = s_('ClusterIntegration|Where do you want to create a cluster?')
- eks_help_path = help_page_path('user/infrastructure/clusters/connect/new_eks_cluster')
- gke_help_path = help_page_path('user/infrastructure/clusters/connect/new_gke_cluster')
- docs_mode = local_assigns.fetch(:docs_mode, false)
.gl-p-5
%h4.gl-mb-5
= create_cluster_label
.gl-display-flex
= render partial: 'clusters/clusters/cloud_providers/cloud_provider_button',
locals: { provider: 'aws', label: eks_label, logo_path: 'illustrations/logos/amazon_eks.svg', help_path: eks_help_path, docs_mode: docs_mode }
locals: { label: eks_label, logo_path: 'illustrations/logos/amazon_eks.svg', help_path: eks_help_path }
= render partial: 'clusters/clusters/cloud_providers/cloud_provider_button',
locals: { provider: 'gcp', label: gke_label, logo_path: 'illustrations/logos/google_gke.svg', help_path: gke_help_path, docs_mode: docs_mode, last: true }
locals: { label: gke_label, logo_path: 'illustrations/logos/google_gke.svg', help_path: gke_help_path, last: true }

View File

@ -9,5 +9,5 @@
.gl-w-quarter.gl-xs-w-full.gl-flex-shrink-0.gl-md-mr-5
= render 'sidebar', is_connect_page: true
.gl-w-full
#js-cluster-new{ data: js_cluster_new }
#js-cluster-new
= render 'clusters/clusters/user/form'

View File

@ -1,87 +0,0 @@
- external_link_icon = sprite_icon('external-link')
- zones_link_url = 'https://cloud.google.com/compute/docs/regions-zones/regions-zones'
- machine_type_link_url = 'https://cloud.google.com/compute/docs/machine-types'
- pricing_link_url = 'https://cloud.google.com/compute/pricing#machinetype'
- kubernetes_integration_url = help_page_path('user/infrastructure/clusters/index.md')
- help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe
- help_link_end = ' %{external_link_icon}</a>'.html_safe % { external_link_icon: external_link_icon }
%p
= s_('ClusterIntegration|Read our %{link_start}help page%{link_end} on Kubernetes cluster integration.').html_safe % { link_start: help_link_start % { url: kubernetes_integration_url }, link_end: '</a>'.html_safe }
%p= link_to('Select a different Google account', @authorize_url)
= bootstrap_form_for @gcp_cluster, html: { class: 'gl-show-field-errors js-gke-cluster-creation prepend-top-20',
data: { token: token_in_session } }, url: clusterable.create_gcp_clusters_path, as: :cluster do |field|
= field.text_field :name, required: true, title: s_('ClusterIntegration|Cluster name is required.'),
label: s_('ClusterIntegration|Kubernetes cluster name'), label_class: 'label-bold'
= field.form_group :environment_scope, label: { text: s_('ClusterIntegration|Environment scope'),
class: 'label-bold' } do
= field.text_field :environment_scope, required: true, class: 'form-control',
title: 'Environment scope is required.', wrapper: false
.form-text.text-muted= s_("ClusterIntegration|Choose which of your environments will use this cluster.")
= field.fields_for :provider_gcp, @gcp_cluster.provider_gcp do |provider_gcp_field|
.form-group
= provider_gcp_field.label :gcp_project_id, s_('ClusterIntegration|Google Cloud Platform project'),
class: 'label-bold'
.js-gcp-project-id-dropdown-entry-point{ data: { docsUrl: 'https://console.cloud.google.com/home/dashboard' } }
= provider_gcp_field.hidden_field :gcp_project_id
.dropdown
%button.dropdown-menu-toggle.dropdown-menu-full-width{ type: 'button', disabled: true }
%span.dropdown-toggle-text
= _('Select project')
= sprite_icon('chevron-down', css_class: 'dropdown-menu-toggle-icon gl-top-3')
%span.form-text.text-muted &nbsp;
.form-group
= provider_gcp_field.label :zone, s_('ClusterIntegration|Zone'), class: 'label-bold'
.js-gcp-zone-dropdown-entry-point
= provider_gcp_field.hidden_field :zone
.dropdown
%button.dropdown-menu-toggle.dropdown-menu-full-width{ type: 'button', disabled: true }
%span.dropdown-toggle-text
= _('Select project to choose zone')
= sprite_icon('chevron-down', css_class: 'dropdown-menu-toggle-icon gl-top-3')
%p.form-text.text-muted
= s_('ClusterIntegration|Learn more about %{help_link_start}zones%{help_link_end}.').html_safe % { help_link_start: help_link_start % { url: zones_link_url }, help_link_end: help_link_end }
= provider_gcp_field.number_field :num_nodes, required: true, placeholder: '3',
title: s_('ClusterIntegration|Number of nodes must be a numerical value.'),
label: s_('ClusterIntegration|Number of nodes'), label_class: 'label-bold'
.form-group
= provider_gcp_field.label :machine_type, s_('ClusterIntegration|Machine type'), class: 'label-bold'
.js-gcp-machine-type-dropdown-entry-point
= provider_gcp_field.hidden_field :machine_type
.dropdown
%button.dropdown-menu-toggle.dropdown-menu-full-width{ type: 'button', disabled: true }
%span.dropdown-toggle-text
= _('Select project and zone to choose machine type')
= sprite_icon('chevron-down', css_class: 'dropdown-menu-toggle-icon gl-top-3')
%p.form-text.text-muted
= s_('ClusterIntegration|Learn more about %{help_link_start_machine_type}machine types%{help_link_end} and %{help_link_start_pricing}pricing%{help_link_end}.').html_safe % { help_link_start_machine_type: help_link_start % { url: machine_type_link_url }, help_link_start_pricing: help_link_start % { url: pricing_link_url }, help_link_end: help_link_end }
.form-group
= provider_gcp_field.check_box :cloud_run, { label: s_('ClusterIntegration|Enable Cloud Run for Anthos'),
label_class: 'label-bold' }
.form-text.text-muted
= s_('ClusterIntegration|Uses the Cloud Run, Istio, and HTTP Load Balancing addons for this cluster.')
= link_to _('Learn more.'), help_page_path('user/project/clusters/add_gke_clusters.md', anchor: 'cloud-run-for-anthos'), target: '_blank', rel: 'noopener noreferrer'
.form-group
= field.check_box :managed, { label: s_('ClusterIntegration|GitLab-managed cluster'),
label_class: 'label-bold' }
.form-text.text-muted
= s_('ClusterIntegration|Allow GitLab to manage namespaces and service accounts for this cluster.')
= link_to _('Learn more.'), help_page_path('user/project/clusters/gitlab_managed_clusters.md'), target: '_blank', rel: 'noopener noreferrer'
.form-group
= field.check_box :namespace_per_environment, { label: s_('ClusterIntegration|Namespace per environment'), label_class: 'label-bold' }
.form-text.text-muted
= s_('ClusterIntegration|Deploy each environment to its own namespace. Otherwise, environments within a project share a project-wide namespace. Note that anyone who can trigger a deployment of a namespace can read its secrets. If modified, existing environments will use their current namespaces until the cluster cache is cleared.')
= link_to _('Learn more.'), help_page_path('user/project/clusters/deploy_to_cluster.md', anchor: 'custom-namespace'), target: '_blank', rel: 'noopener noreferrer'
.form-group.js-gke-cluster-creation-submit-container
= field.submit s_('ClusterIntegration|Create Kubernetes cluster'),
class: 'js-gke-cluster-creation-submit gl-button btn btn-confirm', disabled: true

View File

@ -1,3 +0,0 @@
- documentation_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path("integration/google") }
- link_end = '<a/>'.html_safe
= s_('Google authentication is not %{link_start}properly configured%{link_end}. Ask your GitLab administrator if you want to use this service.').html_safe % { link_start: documentation_link_start, link_end: link_end }

View File

@ -1,14 +0,0 @@
%h4
= s_('ClusterIntegration|Enter the details for your Kubernetes cluster')
%p
= s_('ClusterIntegration|Please make sure that your Google account meets the following requirements:')
%ul
%li
- link_to_kubernetes_engine = link_to(s_('ClusterIntegration|access to Google Kubernetes Engine'), 'https://console.cloud.google.com/freetrial?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral', target: '_blank', rel: 'noopener noreferrer')
= s_('ClusterIntegration|Your account must have %{link_to_kubernetes_engine}').html_safe % { link_to_kubernetes_engine: link_to_kubernetes_engine }
%li
- link_to_requirements = link_to(s_('ClusterIntegration|meets the requirements'), 'https://cloud.google.com/kubernetes-engine/docs/quickstart?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral', target: '_blank', rel: 'noopener noreferrer')
= s_('ClusterIntegration|Make sure your account %{link_to_requirements} to create Kubernetes clusters').html_safe % { link_to_requirements: link_to_requirements }
%li
- link_to_container_project = link_to(s_('ClusterIntegration|Google Kubernetes Engine project'), 'https://console.cloud.google.com/home/dashboard?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral', target: '_blank', rel: 'noopener noreferrer')
= s_('ClusterIntegration|This account must have permissions to create a Kubernetes cluster in the %{link_to_container_project} specified below').html_safe % { link_to_container_project: link_to_container_project }

View File

@ -1,5 +0,0 @@
= render 'clusters/clusters/gcp/header'
- if @valid_gcp_token
= render 'clusters/clusters/gcp/form'
- else
= render 'clusters/clusters/gcp/gcp_not_configured'

View File

@ -1,19 +0,0 @@
- @content_class = 'limit-container-width' unless fluid_layout
- add_to_breadcrumbs _('Kubernetes Clusters'), clusterable.index_path
- breadcrumb_title _('Create a cluster')
- page_title _('Create a Kubernetes cluster')
- provider = params[:provider]
= render 'deprecation_alert'
= render_gcp_signup_offer
.gl-md-display-flex.gl-mt-3
.gl-w-quarter.gl-xs-w-full.gl-flex-shrink-0.gl-md-mr-5
= render 'sidebar', is_connect_page: false
.gl-w-full
= render 'clusters/clusters/cloud_providers/cloud_provider_selector'
- if ['aws', 'gcp'].include?(provider)
.gl-p-5.gl-border-1.gl-border-t-solid.gl-border-gray-100
= render "clusters/clusters/#{provider}/new"

View File

@ -2,12 +2,11 @@
- add_to_breadcrumbs _('Kubernetes Clusters'), clusterable.index_path
- breadcrumb_title _('Create a cluster')
- page_title _('Create a Kubernetes cluster')
- docs_mode = true
= render_gcp_signup_offer
.gl-md-display-flex.gl-mt-3
.gl-w-quarter.gl-xs-w-full.gl-flex-shrink-0.gl-md-mr-5
= render 'sidebar', docs_mode: docs_mode, is_connect_page: false
= render 'sidebar', is_connect_page: false
.gl-w-full
= render 'clusters/clusters/cloud_providers/cloud_provider_selector', docs_mode: docs_mode
= render 'clusters/clusters/cloud_providers/cloud_provider_selector'

View File

@ -1,7 +1,5 @@
- more_info_link = link_to _('Learn more.'), help_page_path('user/project/clusters/add_remove_clusters.md',
anchor: 'add-existing-cluster'), target: '_blank', rel: 'noopener noreferrer'
- rbac_help_link = link_to _('Learn more.'), help_page_path('user/project/clusters/add_remove_clusters.md',
anchor: 'access-controls'), target: '_blank', rel: 'noopener noreferrer'
- more_info_link = link_to _('Learn more.'), help_page_path('user/project/clusters/add_existing_cluster'), target: '_blank', rel: 'noopener noreferrer'
- rbac_help_link = link_to _('Learn more.'), help_page_path('user/project/clusters/cluster_access'), target: '_blank', rel: 'noopener noreferrer'
- api_url_help_text = s_('ClusterIntegration|The URL used to access the Kubernetes API.')
- ca_cert_help_text = s_('ClusterIntegration|The Kubernetes certificate used to authenticate to the cluster.')

View File

@ -0,0 +1,157 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
%html{ lang: "en" }
%head
%meta{ content: "text/html; charset=UTF-8", "http-equiv" => "Content-Type" }/
%meta{ content: "width=device-width, initial-scale=1", name: "viewport" }/
%meta{ content: "IE=edge", "http-equiv" => "X-UA-Compatible" }/
%title= message.subject
:css
/* CLIENT-SPECIFIC STYLES */
body, table, td, a { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
img { -ms-interpolation-mode: bicubic; }
/* iOS BLUE LINKS */
a[x-apple-data-detectors] {
color: inherit !important;
text-decoration: none !important;
font-size: inherit !important;
font-family: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
}
/* ANDROID MARGIN HACK */
body { margin:0 !important; }
div[style*="margin: 16px 0"] { margin:0 !important; }
@media only screen and (max-width: 639px) {
body, #body {
min-width: 320px !important;
}
table.wrapper {
width: 100% !important;
min-width: 320px !important;
}
table.wrapper > tbody > tr > td {
border-left: 0 !important;
border-right: 0 !important;
border-radius: 0 !important;
padding-left: 10px !important;
padding-right: 10px !important;
}
}
ul.users-list {
list-style: none;
padding: 0px;
display: block;
margin-top: 0px;
}
ul.users-list li {
display: inline-block;
padding-right: 12px;
padding-top: 8px;
}
%body{ style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;height:100%;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" }
%table#body{ border: "0", cellpadding: "0", cellspacing: "0", style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;" }
%tbody
%tr.line
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#6b4fbb;height:4px;font-size:4px;line-height:4px;" }
%tr.header
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" }
%img{ alt: "GitLab", height: "55", src: image_url('mailers/gitlab_logo.png'), width: "55" }/
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" }
%table.wrapper{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:640px;margin:0 auto;border-collapse:separate;border-spacing:0;" }
%tbody
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#ffffff;text-align:left;padding:18px 25px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" }
%table.content{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:separate;border-spacing:0;" }
%tbody
%tr.success
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:10px;border-radius:3px;font-size:14px;line-height:1.3;text-align:center;overflow:hidden;color:#ffffff;background-color:#31af64;" }
%table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;" }
%tbody
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;padding-right:5px;" }
%img{ alt: "✓", height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-check-green-inverted.gif'), style: "display:block;", width: "13" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;" }
- if @merge_request.respond_to? :approvals_required
%span Merge request was approved (#{@merge_request.approvals.count}/#{@merge_request.approvals_required})
- else
%span Merge request was approved
%tr.spacer
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" }
&nbsp;
%tr.section
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;line-height:1.4;text-align:center;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" }
%table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;width:100%;" }
%tbody
%tr{ style: 'width:100%;' }
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;text-align:center;" }
%img{ src: image_url('mailers/approval/icon-merge-request-gray.gif'), style: "height:18px;width:18px;margin-bottom:-4px;", alt: "Merge request icon" }
%span{ style: "font-weight: 600;color:#333333;" } Merge request
%a{ href: merge_request_url(@merge_request), style: "font-weight: 600;color:#3777b0;text-decoration:none" }= @merge_request.to_reference
%span was approved by
%img.avatar{ height: "24", src: avatar_icon_for_user(@approved_by, 24, only_path: false), style: "border-radius:12px;margin:-7px 0 -7px 3px;", width: "24", alt: "Avatar" }/
%a.muted{ href: user_url(@approved_by), style: "color:#333333;text-decoration:none;" }
= @approved_by.name
%tr.spacer
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" }
&nbsp;
%tr.section
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" }
%table.info{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;" }
%tbody
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;" } Project
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;" }
- namespace_name = @project.group ? @project.group.name : @project.namespace.owner.name
- namespace_url = @project.group ? group_url(@project.group) : user_url(@project.namespace.owner)
%a.muted{ href: namespace_url, style: "color:#333333;text-decoration:none;" }
= namespace_name
\/
%a.muted{ href: project_url(@project), style: "color:#333333;text-decoration:none;" }
= @project.name
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Branch
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
%table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
%tbody
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
%img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13", alt: "Branch icon" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
%span.muted{ style: "color:#333333;text-decoration:none;" }
= @merge_request.source_branch
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Author
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
%table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
%tbody
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
%img.avatar{ height: "24", src: avatar_icon_for_user(@merge_request.author, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
%a.muted{ href: user_url(@merge_request.author), style: "color:#333333;text-decoration:none;" }
= @merge_request.author.name
- if @merge_request.assignees.any?
= render 'users_list', users: @merge_request.assignees, user_label: assignees_label(@merge_request, include_value: false)
- if @merge_request.reviewers.any?
= render 'users_list', users: @merge_request.reviewers, user_label: reviewers_label(@merge_request, include_value: false)
- if Gitlab.ee?
-# EE-specific start
= render 'layouts/mailer/additional_text'
-# EE-specific end
%tr.footer
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" }
%img{ alt: "GitLab", src: image_url('mailers/gitlab_logo_black_text.png'), style: "display:block;margin:0 auto 1em;", width: "90" }/
%div
- manage_notifications_link = link_to(_("Manage all notifications"), profile_notifications_url, style: "color:#3777b0;text-decoration:none;")
- help_link = link_to(_("Help"), help_url, style: "color:#3777b0;text-decoration:none;")
= _("You're receiving this email because of your account on %{host}. %{manage_notifications_link} &middot; %{help_link}").html_safe % { host: Gitlab.config.gitlab.host, manage_notifications_link: manage_notifications_link, help_link: help_link }

View File

@ -0,0 +1,9 @@
Merge request #{@merge_request.to_reference} was approved by #{sanitize_name(@approved_by.name)}
Merge request URL: #{project_merge_request_url(@merge_request.target_project, @merge_request)}
= merge_path_description(@merge_request, 'to')
Author: #{sanitize_name(@merge_request.author_name)}
= assignees_label(@merge_request)
= reviewers_label(@merge_request)

View File

@ -0,0 +1,156 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
%html{ lang: "en" }
%head
%meta{ content: "text/html; charset=UTF-8", "http-equiv" => "Content-Type" }/
%meta{ content: "width=device-width, initial-scale=1", name: "viewport" }/
%meta{ content: "IE=edge", "http-equiv" => "X-UA-Compatible" }/
%title= message.subject
:css
/* CLIENT-SPECIFIC STYLES */
body, table, td, a { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
img { -ms-interpolation-mode: bicubic; }
/* iOS BLUE LINKS */
a[x-apple-data-detectors] {
color: inherit !important;
text-decoration: none !important;
font-size: inherit !important;
font-family: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
}
/* ANDROID MARGIN HACK */
body { margin:0 !important; }
div[style*="margin: 16px 0"] { margin:0 !important; }
@media only screen and (max-width: 639px) {
body, #body {
min-width: 320px !important;
}
table.wrapper {
width: 100% !important;
min-width: 320px !important;
}
table.wrapper > tbody > tr > td {
border-left: 0 !important;
border-right: 0 !important;
border-radius: 0 !important;
padding-left: 10px !important;
padding-right: 10px !important;
}
}
ul.users-list {
list-style: none;
padding: 0px;
display: block;
margin-top: 0px;
}
ul.users-list li {
display: inline-block;
padding-right: 12px;
padding-top: 8px;
}
%body{ style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;height:100%;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" }
%table#body{ border: "0", cellpadding: "0", cellspacing: "0", style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;" }
%tbody
%tr.line
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#6b4fbb;height:4px;font-size:4px;line-height:4px;" }
%tr.header
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" }
%img{ alt: "GitLab", height: "55", src: image_url('mailers/gitlab_logo.png'), width: "55" }/
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" }
%table.wrapper{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:640px;margin:0 auto;border-collapse:separate;border-spacing:0;" }
%tbody
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#ffffff;text-align:left;padding:18px 25px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" }
%table.content{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:separate;border-spacing:0;" }
%tbody
%tr.success
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:10px;border-radius:3px;font-size:14px;line-height:1.3;text-align:center;overflow:hidden;color:#ffffff;background-color:#FC6D26;" }
%table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;" }
%tbody
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;padding-right:5px;" }
%img{ alt: "✗", height: "13", src: image_url('mailers/approval/icon-x-orange-inverted.gif'), style: "display:block;", width: "13" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;" }
- if @merge_request.respond_to? :approvals_required
%span Merge request was unapproved (#{@merge_request.approvals.count}/#{@merge_request.approvals_required})
- else
%span Merge request was unapproved
%tr.spacer
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" }
&nbsp;
%tr.section
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;line-height:1.4;text-align:center;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" }
%table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;width:100%;" }
%tbody
%tr{ style: 'width:100%;' }
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;text-align:center;" }
%img{ src: image_url('mailers/approval/icon-merge-request-gray.gif'), style: "height:18px;width:18px;margin-bottom:-4px;", alt: "Merge request icon" }
%span{ style: "font-weight: 600;color:#333333;" } Merge request
%a{ href: merge_request_url(@merge_request), style: "font-weight: 600;color:#3777b0;text-decoration:none" }= @merge_request.to_reference
%span was unapproved by
%img.avatar{ height: "24", src: avatar_icon_for_user(@unapproved_by, 24), style: "border-radius:12px;margin:-7px 0 -7px 3px;", width: "24", alt: "Avatar" }/
%a.muted{ href: user_url(@unapproved_by), style: "color:#333333;text-decoration:none;" }
= @unapproved_by.name
%tr.spacer
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" }
&nbsp;
%tr.section
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" }
%table.info{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;" }
%tbody
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;" } Project
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;" }
- namespace_name = @project.group ? @project.group.name : @project.namespace.owner.name
- namespace_url = @project.group ? group_url(@project.group) : user_url(@project.namespace.owner)
%a.muted{ href: namespace_url, style: "color:#333333;text-decoration:none;" }
= namespace_name
\/
%a.muted{ href: project_url(@project), style: "color:#333333;text-decoration:none;" }
= @project.name
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Branch
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
%table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
%tbody
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
%img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13", alt: "Branch icon" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
%span.muted{ style: "color:#333333;text-decoration:none;" }
= @merge_request.source_branch
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Author
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
%table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
%tbody
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
%img.avatar{ height: "24", src: avatar_icon_for_user(@merge_request.author, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
%a.muted{ href: user_url(@merge_request.author), style: "color:#333333;text-decoration:none;" }
= @merge_request.author.name
- if @merge_request.assignees.any?
= render 'users_list', users: @merge_request.assignees, user_label: assignees_label(@merge_request, include_value: false)
- if @merge_request.reviewers.any?
= render 'users_list', users: @merge_request.reviewers, user_label: reviewers_label(@merge_request, include_value: false)
- if Gitlab.ee?
-# EE-specific start
= render 'layouts/mailer/additional_text'
-# EE-specific end
%tr.footer
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" }
%img{ alt: "GitLab", src: image_url('mailers/gitlab_logo_black_text.png'), style: "display:block;margin:0 auto 1em;", width: "90" }/
%div
- manage_notifications_link = link_to(_("Manage all notifications"), profile_notifications_url, style: "color:#3777b0;text-decoration:none;")
- help_link = link_to(_("Help"), help_url, style: "color:#3777b0;text-decoration:none;")
= _("You're receiving this email because of your account on %{host}. %{manage_notifications_link} &middot; %{help_link}").html_safe % { host: Gitlab.config.gitlab.host, manage_notifications_link: manage_notifications_link, help_link: help_link }

View File

@ -0,0 +1,9 @@
Merge request #{@merge_request.to_reference} was unapproved by #{@unapproved_by.name}
Merge request URL: #{project_merge_request_url(@merge_request.target_project, @merge_request)}
= merge_path_description(@merge_request, 'to')
Author: #{sanitize_name(@merge_request.author_name)}
= assignees_label(@merge_request)
= reviewers_label(@merge_request)

View File

@ -13,10 +13,13 @@ class ProjectServiceWorker # rubocop:disable Scalability/IdempotentWorker
def perform(hook_id, data)
data = data.with_indifferent_access
integration = Integration.find(hook_id)
integration.execute(data)
rescue StandardError => error
integration_class = integration&.class&.name || "Not Found"
Gitlab::ErrorTracking.log_exception(error, integration_class: integration_class)
integration = Integration.find_by_id(hook_id)
return unless integration
begin
integration.execute(data)
rescue StandardError => error
integration.log_exception(error)
end
end
end

View File

@ -234,12 +234,11 @@ Rails.application.routes.draw do
# End of the /-/ scope.
concern :clusterable do
resources :clusters, only: [:index, :new, :show, :update, :destroy] do
resources :clusters, only: [:index, :show, :update, :destroy] do
collection do
get :connect
get :new_cluster_docs
post :create_user
post :create_gcp
post :create_aws
post :authorize_aws_role
end

View File

@ -0,0 +1,28 @@
# frozen_string_literal: true
class RemoveRequirementsManagementTestReportsRequirementId < Gitlab::Database::Migration[2.0]
disable_ddl_transaction!
TARGET_TABLE = :requirements_management_test_reports
CONSTRAINT_NAME = 'fk_rails_fb3308ad55'
def up
with_lock_retries do
remove_column TARGET_TABLE, :requirement_id
end
end
def down
unless column_exists?(TARGET_TABLE, :requirement_id)
with_lock_retries do
add_column TARGET_TABLE, :requirement_id, :bigint, after: :created_at
end
end
add_concurrent_index TARGET_TABLE, :requirement_id,
name: :index_requirements_management_test_reports_on_requirement_id
add_concurrent_foreign_key TARGET_TABLE, :requirements,
column: :requirement_id, name: CONSTRAINT_NAME, on_delete: :cascade
end
end

View File

@ -0,0 +1 @@
0e38608a14abd18ab257531f11e31e0a5d7d3801d9725ae02731b6b5ce881db7

View File

@ -20111,7 +20111,6 @@ ALTER SEQUENCE requirements_id_seq OWNED BY requirements.id;
CREATE TABLE requirements_management_test_reports (
id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
requirement_id bigint,
author_id bigint,
state smallint NOT NULL,
build_id bigint,
@ -29021,8 +29020,6 @@ CREATE INDEX index_requirements_management_test_reports_on_build_id ON requireme
CREATE INDEX index_requirements_management_test_reports_on_issue_id ON requirements_management_test_reports USING btree (issue_id);
CREATE INDEX index_requirements_management_test_reports_on_requirement_id ON requirements_management_test_reports USING btree (requirement_id);
CREATE INDEX index_requirements_on_author_id ON requirements USING btree (author_id);
CREATE INDEX index_requirements_on_created_at ON requirements USING btree (created_at);
@ -33519,9 +33516,6 @@ ALTER TABLE ONLY merge_requests_closing_issues
ALTER TABLE ONLY banned_users
ADD CONSTRAINT fk_rails_fa5bb598e5 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
ALTER TABLE ONLY requirements_management_test_reports
ADD CONSTRAINT fk_rails_fb3308ad55 FOREIGN KEY (requirement_id) REFERENCES requirements(id) ON DELETE CASCADE;
ALTER TABLE ONLY operations_feature_flags_issues
ADD CONSTRAINT fk_rails_fb4d2a7cb1 FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE;

View File

@ -463,6 +463,9 @@ gitlab_rails['incoming_email_host'] = "exchange.example.com"
gitlab_rails['incoming_email_port'] = 993
# Whether the IMAP server uses SSL
gitlab_rails['incoming_email_ssl'] = true
# Whether to expunge (permanently remove) messages from the mailbox when they are deleted after delivery
gitlab_rails['incoming_email_expunge_deleted'] = true
```
Example for source installs:
@ -491,6 +494,9 @@ incoming_email:
port: 993
# Whether the IMAP server uses SSL
ssl: true
# Whether to expunge (permanently remove) messages from the mailbox when they are deleted after delivery
expunge_deleted: true
```
##### Dedicated email address
@ -521,6 +527,9 @@ gitlab_rails['incoming_email_host'] = "exchange.example.com"
gitlab_rails['incoming_email_port'] = 993
# Whether the IMAP server uses SSL
gitlab_rails['incoming_email_ssl'] = true
# Whether to expunge (permanently remove) messages from the mailbox when they are deleted after delivery
gitlab_rails['incoming_email_expunge_deleted'] = true
```
Example for source installs:
@ -545,6 +554,9 @@ incoming_email:
port: 993
# Whether the IMAP server uses SSL
ssl: true
# Whether to expunge (permanently remove) messages from the mailbox when they are deleted after delivery
expunge_deleted: true
```
#### Microsoft Office 365
@ -599,6 +611,9 @@ gitlab_rails['incoming_email_host'] = "outlook.office365.com"
gitlab_rails['incoming_email_port'] = 993
# Whether the IMAP server uses SSL
gitlab_rails['incoming_email_ssl'] = true
# Whether to expunge (permanently remove) messages from the mailbox when they are deleted after delivery
gitlab_rails['incoming_email_expunge_deleted'] = true
```
This example for source installs assumes the mailbox `incoming@office365.example.com`:
@ -626,6 +641,9 @@ incoming_email:
port: 993
# Whether the IMAP server uses SSL
ssl: true
# Whether to expunge (permanently remove) messages from the mailbox when they are deleted after delivery
expunge_deleted: true
```
##### Catch-all mailbox
@ -654,6 +672,9 @@ gitlab_rails['incoming_email_host'] = "outlook.office365.com"
gitlab_rails['incoming_email_port'] = 993
# Whether the IMAP server uses SSL
gitlab_rails['incoming_email_ssl'] = true
# Whether to expunge (permanently remove) messages from the mailbox when they are deleted after delivery
gitlab_rails['incoming_email_expunge_deleted'] = true
```
This example for source installs assumes the catch-all mailbox `incoming@office365.example.com`:
@ -681,6 +702,9 @@ incoming_email:
port: 993
# Whether the IMAP server uses SSL
ssl: true
# Whether to expunge (permanently remove) messages from the mailbox when they are deleted after delivery
expunge_deleted: true
```
##### Dedicated email address
@ -708,6 +732,9 @@ gitlab_rails['incoming_email_host'] = "outlook.office365.com"
gitlab_rails['incoming_email_port'] = 993
# Whether the IMAP server uses SSL
gitlab_rails['incoming_email_ssl'] = true
# Whether to expunge (permanently remove) messages from the mailbox when they are deleted after delivery
gitlab_rails['incoming_email_expunge_deleted'] = true
```
This example for source installs assumes the dedicated email address `incoming@office365.example.com`:
@ -730,6 +757,9 @@ incoming_email:
port: 993
# Whether the IMAP server uses SSL
ssl: true
# Whether to expunge (permanently remove) messages from the mailbox when they are deleted after delivery
expunge_deleted: true
```
#### Microsoft Graph

View File

@ -4359,7 +4359,7 @@ Input type: `SecurityPolicyProjectAssignInput`
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationsecuritypolicyprojectassignclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationsecuritypolicyprojectassignfullpath"></a>`fullPath` | [`String`](#string) | Full path of the project. |
| <a id="mutationsecuritypolicyprojectassignfullpath"></a>`fullPath` | [`String`](#string) | Full path of the project or group. |
| <a id="mutationsecuritypolicyprojectassignprojectpath"></a>`projectPath` **{warning-solid}** | [`ID`](#id) | **Deprecated:** Use `fullPath`. Deprecated in 14.10. |
| <a id="mutationsecuritypolicyprojectassignsecuritypolicyprojectid"></a>`securityPolicyProjectId` | [`ProjectID!`](#projectid) | ID of the security policy project. |
@ -4381,7 +4381,7 @@ Input type: `SecurityPolicyProjectCreateInput`
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationsecuritypolicyprojectcreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationsecuritypolicyprojectcreatefullpath"></a>`fullPath` | [`String`](#string) | Full path of the project. |
| <a id="mutationsecuritypolicyprojectcreatefullpath"></a>`fullPath` | [`String`](#string) | Full path of the project or group. |
| <a id="mutationsecuritypolicyprojectcreateprojectpath"></a>`projectPath` **{warning-solid}** | [`ID`](#id) | **Deprecated:** Use `fullPath`. Deprecated in 14.10. |
#### Fields
@ -4403,7 +4403,7 @@ Input type: `SecurityPolicyProjectUnassignInput`
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationsecuritypolicyprojectunassignclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationsecuritypolicyprojectunassignfullpath"></a>`fullPath` | [`String`](#string) | Full path of the project. |
| <a id="mutationsecuritypolicyprojectunassignfullpath"></a>`fullPath` | [`String`](#string) | Full path of the project or group. |
| <a id="mutationsecuritypolicyprojectunassignprojectpath"></a>`projectPath` **{warning-solid}** | [`ID`](#id) | **Deprecated:** Use `fullPath`. Deprecated in 14.10. |
#### Fields

View File

@ -302,7 +302,7 @@ Parameters:
NOTE:
`name`, `api_url`, `ca_cert` and `token` can only be updated if the cluster was added
through the ["Add existing Kubernetes cluster"](../user/project/clusters/add_remove_clusters.md#add-existing-cluster) option or
through the ["Add existing Kubernetes cluster"](../user/project/clusters/add_existing_cluster.md) option or
through the ["Add existing cluster to project"](#add-existing-cluster-to-project) endpoint.
Example request:

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