Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-06-11 06:08:22 +00:00
parent 7b0ffedd69
commit 1b3eb6f388
64 changed files with 1455 additions and 240 deletions

View File

@ -230,7 +230,7 @@ export default {
:aria-label="__('Toggle sidebar')"
category="primary"
variant="default"
class="d-sm-none position-absolute toggle-sidebar-mobile-button"
class="d-sm-none gl-absolute toggle-sidebar-mobile-button"
type="button"
@click="toggleSidebar"
>

View File

@ -1,14 +1,18 @@
<script>
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import SidebarHeader from './sidebar/sidebar_header.vue';
import SidebarTodo from './sidebar/sidebar_todo.vue';
import SidebarStatus from './sidebar/sidebar_status.vue';
import SidebarAssignees from './sidebar/sidebar_assignees.vue';
export default {
components: {
SidebarAssignees,
SidebarHeader,
SidebarTodo,
SidebarStatus,
},
mixins: [glFeatureFlagsMixin()],
props: {
sidebarCollapsed: {
type: Boolean,
@ -28,11 +32,6 @@ export default {
return this.sidebarCollapsed ? 'right-sidebar-collapsed' : 'right-sidebar-expanded';
},
},
methods: {
handleAlertSidebarError(errorMessage) {
this.$emit('alert-sidebar-error', errorMessage);
},
},
};
</script>
@ -48,7 +47,14 @@ export default {
:project-path="projectPath"
:alert="alert"
@toggle-sidebar="$emit('toggle-sidebar')"
@alert-sidebar-error="handleAlertSidebarError"
@alert-sidebar-error="$emit('alert-sidebar-error', $event)"
/>
<sidebar-assignees
v-if="glFeatures.alertAssignee"
:project-path="projectPath"
:alert="alert"
@toggle-sidebar="$emit('toggle-sidebar')"
@alert-sidebar-error="$emit('alert-sidebar-error', $event)"
/>
<!-- TODO: Remove after adding extra attribute blocks to sidebar -->
<div class="block"></div>

View File

@ -0,0 +1,51 @@
<script>
import { GlDropdownItem } from '@gitlab/ui';
export default {
components: {
GlDropdownItem,
},
props: {
user: {
type: Object,
required: true,
},
active: {
type: Boolean,
required: true,
},
},
methods: {
isActive(name) {
return this.alert.assignees.nodes.some(({ username }) => username === name);
},
},
};
</script>
<template>
<gl-dropdown-item
:key="user.username"
data-testid="assigneeDropdownItem"
class="assignee-dropdown-item gl-vertical-align-middle"
:active="active"
active-class="is-active"
@click="$emit('update-alert-assignees', user.username)"
>
<span class="gl-relative mr-2">
<img
:alt="user.username"
:src="user.avatar_url"
:width="32"
class="avatar avatar-inline gl-m-0 s32"
data-qa-selector="avatar_image"
/>
</span>
<span class="d-flex gl-flex-direction-column gl-overflow-hidden">
<strong class="dropdown-menu-user-full-name">
{{ user.name }}
</strong>
<span class="dropdown-menu-user-username"> {{ user.username }}</span>
</span>
</gl-dropdown-item>
</template>

View File

@ -0,0 +1,269 @@
<script>
import {
GlIcon,
GlDropdown,
GlDropdownDivider,
GlDropdownHeader,
GlDropdownItem,
GlLoadingIcon,
GlTooltip,
GlButton,
GlSprintf,
} from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
import { s__ } from '~/locale';
import alertSetAssignees from '../../graphql/mutations/alert_set_assignees.graphql';
import SidebarAssignee from './sidebar_assignee.vue';
import { debounce } from 'lodash';
const DATA_REFETCH_DELAY = 250;
export default {
FETCH_USERS_ERROR: s__(
'AlertManagement|There was an error while updating the assignee(s) list. Please try again.',
),
UPDATE_ALERT_ASSIGNEES_ERROR: s__(
'AlertManagement|There was an error while updating the assignee(s) of the alert. Please try again.',
),
components: {
GlIcon,
GlDropdown,
GlDropdownItem,
GlDropdownDivider,
GlDropdownHeader,
GlLoadingIcon,
GlTooltip,
GlButton,
GlSprintf,
SidebarAssignee,
},
props: {
projectPath: {
type: String,
required: true,
},
alert: {
type: Object,
required: true,
},
isEditable: {
type: Boolean,
required: false,
default: true,
},
},
data() {
return {
isDropdownShowing: false,
isDropdownSearching: false,
isUpdating: false,
search: '',
users: [],
};
},
computed: {
assignedUsers() {
return this.alert.assignees.nodes.length > 0
? this.alert.assignees.nodes[0].username
: s__('AlertManagement|Unassigned');
},
dropdownClass() {
return this.isDropdownShowing ? 'show' : 'gl-display-none';
},
userListValid() {
return !this.isDropdownSearching && this.users.length > 0;
},
userListEmpty() {
return !this.isDropdownSearching && this.users.length === 0;
},
},
watch: {
search: debounce(function debouncedUserSearch() {
this.updateAssigneesDropdown();
}, DATA_REFETCH_DELAY),
},
mounted() {
this.updateAssigneesDropdown();
},
methods: {
hideDropdown() {
this.isDropdownShowing = false;
},
toggleFormDropdown() {
this.isDropdownShowing = !this.isDropdownShowing;
const { dropdown } = this.$refs.dropdown.$refs;
if (dropdown && this.isDropdownShowing) {
dropdown.show();
}
},
isActive(name) {
return this.alert.assignees.nodes.some(({ username }) => username === name);
},
buildUrl(urlRoot, url) {
let newUrl;
if (urlRoot != null) {
newUrl = urlRoot.replace(/\/$/, '') + url;
}
return newUrl;
},
updateAssigneesDropdown() {
this.isDropdownSearching = true;
return axios
.get(this.buildUrl(gon.relative_url_root, '/autocomplete/users.json'), {
params: {
search: this.search,
per_page: 20,
active: true,
current_user: true,
project_id: gon.current_project_id,
},
})
.then(({ data }) => {
this.users = data;
})
.catch(() => {
this.$emit('alert-sidebar-error', this.$options.FETCH_USERS_ERROR);
})
.finally(() => {
this.isDropdownSearching = false;
});
},
updateAlertAssignees(assignees) {
this.isUpdating = true;
this.$apollo
.mutate({
mutation: alertSetAssignees,
variables: {
iid: this.alert.iid,
assigneeUsernames: [this.isActive(assignees) ? '' : assignees],
projectPath: this.projectPath,
},
})
.then(() => {
this.hideDropdown();
})
.catch(() => {
this.$emit('alert-sidebar-error', this.$options.UPDATE_ALERT_ASSIGNEES_ERROR);
})
.finally(() => {
this.isUpdating = false;
});
},
},
};
</script>
<template>
<div class="block alert-status">
<div ref="status" class="sidebar-collapsed-icon" @click="$emit('toggle-sidebar')">
<gl-icon name="user" :size="14" />
<gl-loading-icon v-if="isUpdating" />
<p v-else class="collapse-truncated-title px-1">{{ assignedUsers }}</p>
</div>
<gl-tooltip :target="() => $refs.status" boundary="viewport" placement="left">
<gl-sprintf :message="s__('AlertManagement|Alert assignee(s): %{assignees}')">
<template #assignees>
{{ assignedUsers }}
</template>
</gl-sprintf>
</gl-tooltip>
<div class="hide-collapsed">
<p class="title gl-display-flex gl-justify-content-space-between">
{{ s__('AlertManagement|Assignee') }}
<a
v-if="isEditable"
ref="editButton"
class="btn-link"
href="#"
@click="toggleFormDropdown"
@keydown.esc="hideDropdown"
>
{{ s__('AlertManagement|Edit') }}
</a>
</p>
<div class="dropdown dropdown-menu-selectable" :class="dropdownClass">
<gl-dropdown
ref="dropdown"
:text="assignedUsers"
class="w-100"
toggle-class="dropdown-menu-toggle"
variant="outline-default"
@keydown.esc.native="hideDropdown"
@hide="hideDropdown"
>
<div class="dropdown-title">
<span class="alert-title">{{ s__('AlertManagement|Assign Assignees') }}</span>
<gl-button
:aria-label="__('Close')"
variant="link"
class="dropdown-title-button dropdown-menu-close"
icon="close"
@click="hideDropdown"
/>
</div>
<div class="dropdown-input">
<input
v-model.trim="search"
class="dropdown-input-field"
type="search"
:placeholder="__('Search users')"
/>
<gl-icon name="search" class="dropdown-input-search ic-search" data-hidden="true" />
</div>
<div class="dropdown-content dropdown-body">
<template v-if="userListValid">
<gl-dropdown-item @click="updateAlertAssignees('')">
{{ s__('AlertManagement|Unassigned') }}
</gl-dropdown-item>
<gl-dropdown-divider />
<gl-dropdown-header class="mt-0">
{{ s__('AlertManagement|Assignee(s)') }}
</gl-dropdown-header>
<template v-for="user in users">
<sidebar-assignee
v-if="isActive(user.username)"
:key="user.username"
:user="user"
:active="true"
@update-alert-assignees="updateAlertAssignees"
/>
</template>
<gl-dropdown-divider />
<template v-for="user in users">
<sidebar-assignee
v-if="!isActive(user.username)"
:key="user.username"
:user="user"
:active="false"
@update-alert-assignees="updateAlertAssignees"
/>
</template>
</template>
<gl-dropdown-item v-else-if="userListEmpty">
{{ s__('AlertManagement|No Matching Results') }}
</gl-dropdown-item>
<gl-loading-icon v-else />
</div>
</gl-dropdown>
</div>
<gl-loading-icon v-if="isUpdating" :inline="true" />
<p
v-else-if="!isDropdownShowing"
class="value gl-m-0"
:class="{ 'no-value': !alert.assignees.nodes }"
>
<span v-if="alert.assignees.nodes" class="gl-text-gray-700" data-testid="assigned-users">{{
assignedUsers
}}</span>
<span v-else>
{{ s__('AlertManagement|None') }}
</span>
</p>
</div>
</div>
</template>

View File

@ -17,13 +17,13 @@ export default {
</script>
<template>
<div class="block">
<span class="issuable-header-text hide-collapsed float-left">
<div class="block d-flex justify-content-between">
<span class="issuable-header-text hide-collapsed">
{{ __('Quick actions') }}
</span>
<toggle-sidebar
:collapsed="sidebarCollapsed"
css-classes="float-right"
css-classes="ml-auto"
@toggle="$emit('toggle-sidebar')"
/>
<!-- TODO: Implement after or as part of: https://gitlab.com/gitlab-org/gitlab/-/issues/215946 -->

View File

@ -51,7 +51,7 @@ export default {
},
computed: {
dropdownClass() {
return this.isDropdownShowing ? 'show' : 'd-none';
return this.isDropdownShowing ? 'show' : 'gl-display-none';
},
},
methods: {
@ -81,7 +81,6 @@ export default {
})
.then(() => {
this.trackStatusUpdate(status);
this.hideDropdown();
})
.catch(() => {
@ -172,12 +171,15 @@ export default {
<gl-loading-icon v-if="isUpdating" :inline="true" />
<p
v-else-if="!isDropdownShowing"
class="value m-0"
class="value gl-m-0"
:class="{ 'no-value': !$options.statuses[alert.status] }"
>
<span v-if="$options.statuses[alert.status]" class="gl-text-gray-700">{{
$options.statuses[alert.status]
}}</span>
<span
v-if="$options.statuses[alert.status]"
class="gl-text-gray-700"
data-testid="status"
>{{ $options.statuses[alert.status] }}</span
>
<span v-else>
{{ s__('AlertManagement|None') }}
</span>

View File

@ -0,0 +1,15 @@
mutation($projectPath: ID!, $assigneeUsernames: [String!]!, $iid: String!) {
alertSetAssignees(
input: { iid: $iid, assigneeUsernames: $assigneeUsernames, projectPath: $projectPath }
) {
errors
alert {
iid
assignees {
nodes {
username
}
}
}
}
}

View File

@ -1,7 +0,0 @@
import axios from '~/lib/utils/axios_utils';
export default {
getAlertManagementList({ endpoint }) {
return axios.get(endpoint);
},
};

View File

@ -49,4 +49,15 @@
background-color: $white;
}
}
.assignee-dropdown-item {
button {
display: flex;
align-items: center;
&::before {
top: 50% !important;
}
}
}
}

View File

@ -7,7 +7,7 @@ class Clusters::BaseController < ApplicationController
before_action :authorize_read_cluster!
before_action do
push_frontend_feature_flag(:managed_apps_local_tiller)
push_frontend_feature_flag(:managed_apps_local_tiller, clusterable)
end
helper_method :clusterable

View File

@ -0,0 +1,56 @@
# frozen_string_literal: true
module Mutations
module AlertManagement
module Alerts
class SetAssignees < Base
graphql_name 'AlertSetAssignees'
argument :assignee_usernames,
[GraphQL::STRING_TYPE],
required: true,
description: 'The usernames to assign to the alert. Replaces existing assignees by default.'
argument :operation_mode,
Types::MutationOperationModeEnum,
required: false,
description: 'The operation to perform. Defaults to REPLACE.'
def resolve(args)
alert = authorized_find!(project_path: args[:project_path], iid: args[:iid])
result = set_assignees(alert, args[:assignee_usernames], args[:operation_mode])
prepare_response(result)
end
private
def set_assignees(alert, assignee_usernames, operation_mode)
operation_mode ||= Types::MutationOperationModeEnum.enum[:replace]
original_assignees = alert.assignees
target_users = find_target_users(assignee_usernames)
assignees = case Types::MutationOperationModeEnum.enum.key(operation_mode).to_sym
when :replace then target_users.uniq
when :append then (original_assignees + target_users).uniq
when :remove then (original_assignees - target_users)
end
::AlertManagement::Alerts::UpdateService.new(alert, current_user, assignees: assignees).execute
end
def find_target_users(assignee_usernames)
UsersFinder.new(current_user, username: assignee_usernames).execute
end
def prepare_response(result)
{
alert: result.payload[:alert],
errors: result.error? ? [result.message] : []
}
end
end
end
end
end

View File

@ -9,6 +9,7 @@ module Types
mount_mutation Mutations::Admin::SidekiqQueues::DeleteJobs
mount_mutation Mutations::AlertManagement::CreateAlertIssue
mount_mutation Mutations::AlertManagement::UpdateAlertStatus
mount_mutation Mutations::AlertManagement::Alerts::SetAssignees
mount_mutation Mutations::AwardEmojis::Add
mount_mutation Mutations::AwardEmojis::Remove
mount_mutation Mutations::AwardEmojis::Toggle

View File

@ -38,7 +38,8 @@ module Clusters
chart: chart,
files: files.merge(cluster_issuer_file),
preinstall: pre_install_script,
postinstall: post_install_script
postinstall: post_install_script,
local_tiller_enabled: cluster.local_tiller_enabled?
)
end
@ -47,7 +48,8 @@ module Clusters
name: 'certmanager',
rbac: cluster.platform_kubernetes_rbac?,
files: files,
postdelete: post_delete_script
postdelete: post_delete_script,
local_tiller_enabled: cluster.local_tiller_enabled?
)
end

View File

@ -35,7 +35,8 @@ module Clusters
version: VERSION,
rbac: cluster.platform_kubernetes_rbac?,
chart: chart,
files: files
files: files,
local_tiller_enabled: cluster.local_tiller_enabled?
)
end

View File

@ -34,7 +34,8 @@ module Clusters
repository: repository,
files: files,
preinstall: migrate_to_3_script,
postinstall: post_install_script
postinstall: post_install_script,
local_tiller_enabled: cluster.local_tiller_enabled?
)
end
@ -43,7 +44,8 @@ module Clusters
name: 'elastic-stack',
rbac: cluster.platform_kubernetes_rbac?,
files: files,
postdelete: post_delete_script
postdelete: post_delete_script,
local_tiller_enabled: cluster.local_tiller_enabled?
)
end
@ -118,7 +120,8 @@ module Clusters
Gitlab::Kubernetes::Helm::DeleteCommand.new(
name: 'elastic-stack',
rbac: cluster.platform_kubernetes_rbac?,
files: files
files: files,
local_tiller_enabled: cluster.local_tiller_enabled?
).delete_command,
Gitlab::Kubernetes::KubectlCmd.delete("pvc", "--selector", "release=elastic-stack", "--namespace", Gitlab::Kubernetes::Helm::NAMESPACE)
]

View File

@ -32,7 +32,8 @@ module Clusters
version: VERSION,
rbac: cluster.platform_kubernetes_rbac?,
chart: chart,
files: files
files: files,
local_tiller_enabled: cluster.local_tiller_enabled?
)
end

View File

@ -52,7 +52,8 @@ module Clusters
Gitlab::Kubernetes::Helm::InitCommand.new(
name: name,
files: files,
rbac: cluster.platform_kubernetes_rbac?
rbac: cluster.platform_kubernetes_rbac?,
local_tiller_enabled: cluster.local_tiller_enabled?
)
end
@ -60,7 +61,8 @@ module Clusters
Gitlab::Kubernetes::Helm::ResetCommand.new(
name: name,
files: files,
rbac: cluster.platform_kubernetes_rbac?
rbac: cluster.platform_kubernetes_rbac?,
local_tiller_enabled: cluster.local_tiller_enabled?
)
end

View File

@ -63,7 +63,8 @@ module Clusters
version: VERSION,
rbac: cluster.platform_kubernetes_rbac?,
chart: chart,
files: files
files: files,
local_tiller_enabled: cluster.local_tiller_enabled?
)
end

View File

@ -45,7 +45,8 @@ module Clusters
rbac: cluster.platform_kubernetes_rbac?,
chart: chart,
files: files,
repository: repository
repository: repository,
local_tiller_enabled: cluster.local_tiller_enabled?
)
end

View File

@ -77,7 +77,8 @@ module Clusters
chart: chart,
files: files,
repository: REPOSITORY,
postinstall: install_knative_metrics
postinstall: install_knative_metrics,
local_tiller_enabled: cluster.local_tiller_enabled?
)
end
@ -99,7 +100,8 @@ module Clusters
rbac: cluster.platform_kubernetes_rbac?,
files: files,
predelete: delete_knative_services_and_metrics,
postdelete: delete_knative_istio_leftovers
postdelete: delete_knative_istio_leftovers,
local_tiller_enabled: cluster.local_tiller_enabled?
)
end

View File

@ -66,7 +66,8 @@ module Clusters
rbac: cluster.platform_kubernetes_rbac?,
chart: chart,
files: files,
postinstall: install_knative_metrics
postinstall: install_knative_metrics,
local_tiller_enabled: cluster.local_tiller_enabled?
)
end
@ -76,7 +77,8 @@ module Clusters
version: version,
rbac: cluster.platform_kubernetes_rbac?,
chart: chart,
files: files_with_replaced_values(values)
files: files_with_replaced_values(values),
local_tiller_enabled: cluster.local_tiller_enabled?
)
end
@ -85,7 +87,8 @@ module Clusters
name: name,
rbac: cluster.platform_kubernetes_rbac?,
files: files,
predelete: delete_knative_istio_metrics
predelete: delete_knative_istio_metrics,
local_tiller_enabled: cluster.local_tiller_enabled?
)
end

View File

@ -36,7 +36,8 @@ module Clusters
rbac: cluster.platform_kubernetes_rbac?,
chart: chart,
files: files,
repository: repository
repository: repository,
local_tiller_enabled: cluster.local_tiller_enabled?
)
end

View File

@ -326,6 +326,10 @@ module Clusters
end
end
def local_tiller_enabled?
Feature.enabled?(:managed_apps_local_tiller, clusterable, default_enabled: false)
end
private
def unique_management_project_environment_scope

View File

@ -15,7 +15,7 @@ module Clusters
def set_initial_status
return unless not_installable?
self.status = status_states[:installable] if cluster&.application_helm_available? || ::Gitlab::Kubernetes::Helm.local_tiller_enabled?
self.status = status_states[:installable] if cluster&.application_helm_available? || cluster&.local_tiller_enabled?
end
def can_uninstall?

View File

@ -7,7 +7,8 @@ module Clusters
Gitlab::Kubernetes::Helm::DeleteCommand.new(
name: name,
rbac: cluster.platform_kubernetes_rbac?,
files: files
files: files,
local_tiller_enabled: cluster.local_tiller_enabled?
)
end
@ -32,7 +33,7 @@ module Clusters
private
def use_tiller_ssl?
return false if ::Gitlab::Kubernetes::Helm.local_tiller_enabled?
return false if cluster.local_tiller_enabled?
cluster.application_helm.has_ssl?
end

View File

@ -98,7 +98,7 @@ module Clusters
end
before_transition any => [:installed, :updated] do |application, transition|
unless ::Gitlab::Kubernetes::Helm.local_tiller_enabled? || application.is_a?(Clusters::Applications::Helm)
unless application.cluster.local_tiller_enabled? || application.is_a?(Clusters::Applications::Helm)
if transition.event == :make_externally_installed
# If an application is externally installed
# We assume the helm application is externally installed too

View File

@ -0,0 +1,60 @@
# frozen_string_literal: true
module AlertManagement
module Alerts
class UpdateService
# @param alert [AlertManagement::Alert]
# @param current_user [User]
# @param params [Hash] Attributes of the alert
def initialize(alert, current_user, params)
@alert = alert
@current_user = current_user
@params = params
end
def execute
return error_no_permissions unless allowed?
return error_no_updates if params.empty?
filter_assignees
if alert.update(params)
success
else
error(alert.errors.full_messages.to_sentence)
end
end
private
attr_reader :alert, :current_user, :params
def allowed?
current_user.can?(:update_alert_management_alert, alert)
end
def filter_assignees
return if params[:assignees].nil?
# Take first assignee while multiple are not currently supported
params[:assignees] = Array(params[:assignees].first)
end
def success
ServiceResponse.success(payload: { alert: alert })
end
def error(message)
ServiceResponse.error(payload: { alert: alert }, message: message)
end
def error_no_permissions
error(_('You have no permissions'))
end
def error_no_updates
error(_('Please provide attributes to update'))
end
end
end
end

View File

@ -0,0 +1,5 @@
---
title: Allow the assignment of alerts to users from the alert detail view
merge_request: 33122
author:
type: added

View File

@ -495,6 +495,61 @@ enum AlertManagementStatus {
TRIGGERED
}
"""
Autogenerated input type of AlertSetAssignees
"""
input AlertSetAssigneesInput {
"""
The usernames to assign to the alert. Replaces existing assignees by default.
"""
assigneeUsernames: [String!]!
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
The iid of the alert to mutate
"""
iid: String!
"""
The operation to perform. Defaults to REPLACE.
"""
operationMode: MutationOperationMode
"""
The project the alert to mutate is in
"""
projectPath: ID!
}
"""
Autogenerated return type of AlertSetAssignees
"""
type AlertSetAssigneesPayload {
"""
The alert after mutation
"""
alert: AlertManagementAlert
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Errors encountered during execution of the mutation.
"""
errors: [String!]!
"""
The issue created after mutation
"""
issue: Issue
}
"""
An emoji awarded by a user.
"""
@ -7451,6 +7506,7 @@ type Mutation {
addAwardEmoji(input: AddAwardEmojiInput!): AddAwardEmojiPayload
addProjectToSecurityDashboard(input: AddProjectToSecurityDashboardInput!): AddProjectToSecurityDashboardPayload
adminSidekiqQueuesDeleteJobs(input: AdminSidekiqQueuesDeleteJobsInput!): AdminSidekiqQueuesDeleteJobsPayload
alertSetAssignees(input: AlertSetAssigneesInput!): AlertSetAssigneesPayload
boardListUpdateLimitMetrics(input: BoardListUpdateLimitMetricsInput!): BoardListUpdateLimitMetricsPayload
commitCreate(input: CommitCreateInput!): CommitCreatePayload
createAlertIssue(input: CreateAlertIssueInput!): CreateAlertIssuePayload

View File

@ -1175,6 +1175,168 @@
],
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "AlertSetAssigneesInput",
"description": "Autogenerated input type of AlertSetAssignees",
"fields": null,
"inputFields": [
{
"name": "projectPath",
"description": "The project the alert to mutate is in",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "iid",
"description": "The iid of the alert to mutate",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "assigneeUsernames",
"description": "The usernames to assign to the alert. Replaces existing assignees by default.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
}
},
"defaultValue": null
},
{
"name": "operationMode",
"description": "The operation to perform. Defaults to REPLACE.",
"type": {
"kind": "ENUM",
"name": "MutationOperationMode",
"ofType": null
},
"defaultValue": null
},
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "AlertSetAssigneesPayload",
"description": "Autogenerated return type of AlertSetAssignees",
"fields": [
{
"name": "alert",
"description": "The alert after mutation",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "AlertManagementAlert",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "errors",
"description": "Errors encountered during execution of the mutation.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "issue",
"description": "The issue created after mutation",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "Issue",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "AwardEmoji",
@ -20963,6 +21125,33 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "alertSetAssignees",
"description": null,
"args": [
{
"name": "input",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "AlertSetAssigneesInput",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "AlertSetAssigneesPayload",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "boardListUpdateLimitMetrics",
"description": null,

View File

@ -90,6 +90,17 @@ Represents total number of alerts for the represented categories
| `resolved` | Int | Number of alerts with status RESOLVED for the project |
| `triggered` | Int | Number of alerts with status TRIGGERED for the project |
## AlertSetAssigneesPayload
Autogenerated return type of AlertSetAssignees
| Name | Type | Description |
| --- | ---- | ---------- |
| `alert` | AlertManagementAlert | The alert after mutation |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `issue` | Issue | The issue created after mutation |
## AwardEmoji
An emoji awarded by a user.

View File

@ -59,7 +59,7 @@ To enable pipelines for merged results for your project:
1. [Configure your CI/CD configuration file](../index.md#configuring-pipelines-for-merge-requests)
so that the pipeline or individual jobs run for merge requests.
1. Visit your project's **Settings > General** and expand **Merge requests**.
1. Check **Merge pipelines will try to validate the post-merge result prior to merging**.
1. Check **Enable merge trains and pipelines for merged results**.
1. Click **Save changes**.
CAUTION: **Caution:**

View File

@ -82,7 +82,7 @@ To enable merge trains for your project:
1. [Configure your CI/CD configuration file](../../index.md#configuring-pipelines-for-merge-requests)
so that the pipeline or individual jobs run for merge requests.
1. Visit your project's **Settings > General** and expand **Merge requests**.
1. Check **Merge pipelines will try to validate the post-merge result prior to merging**.
1. Check **Enable merge trains and pipelines for merged results**.
1. Click **Save changes**.
CAUTION: **Caution:**
@ -187,7 +187,7 @@ run a new successful pipeline before you can re-add a merge request to a merge t
Merge trains ensure that each pipeline has succeeded before a merge happens, so
you can clear the **Pipelines must succeed** check box and keep
**Merge pipelines will try to validate the post-merge result prior to merging** (merge trains) enabled.
**Enable merge trains and pipelines for merged results** (merge trains) enabled.
If you want to keep the **Pipelines must succeed** option enabled along with Merge
Trains, you can create a new pipeline for merged results when this error occurs by

View File

@ -12,8 +12,7 @@ projects.
## Cluster precedence
GitLab will try [to match](../../../ci/environments/index.md#scoping-environments-with-specs) clusters in
the following order:
GitLab will try to match clusters in the following order:
- Project-level clusters.
- Group-level clusters.

View File

@ -148,7 +148,7 @@ To create and add a new Kubernetes cluster to your project, group, or instance:
- **VPC** - Select a [VPC](https://docs.aws.amazon.com/vpc/latest/userguide/what-is-amazon-vpc.html)
to use for your EKS Cluster resources.
- **Subnets** - Choose the [subnets](https://docs.aws.amazon.com/vpc/latest/userguide/VPC_Subnets.html)
in your VPC where your worker nodes will run.
in your VPC where your worker nodes will run. You must select at least two.
- **Security group** - Choose the [security group](https://docs.aws.amazon.com/vpc/latest/userguide/VPC_SecurityGroups.html)
to apply to the EKS-managed Elastic Network Interfaces that are created in your worker node subnets.
- **Instance type** - The [instance type](https://aws.amazon.com/ec2/instance-types/) of your worker nodes.

View File

@ -24,8 +24,6 @@ requirements are met:
Starting from [GitLab 12.4](https://gitlab.com/gitlab-org/gitlab/-/issues/25925), all the GKE clusters
provisioned by GitLab are [VPC-native](https://cloud.google.com/kubernetes-engine/docs/how-to/alias-ips).
### Important notes
Note the following:
- The [Google authentication integration](../../../integration/google.md) must be enabled in GitLab
@ -38,9 +36,10 @@ Note the following:
cluster's pod address IP range will be set to /16 instead of the regular /14. /16 is a CIDR
notation.
- GitLab requires basic authentication enabled and a client certificate issued for the cluster to
set up an [initial service account](add_remove_clusters.md#access-controls). Starting from [GitLab
11.10](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/58208), the cluster creation process will
explicitly request that basic authentication and client certificate is enabled.
set up an [initial service account](add_remove_clusters.md#access-controls). In [GitLab versions
11.10 and later](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/58208), the cluster creation process
explicitly requests GKE to create clusters with basic authentication enabled and a client
certificate.
### Creating the cluster on GKE

View File

@ -10,12 +10,6 @@ module Gitlab
SERVICE_ACCOUNT = 'tiller'
CLUSTER_ROLE_BINDING = 'tiller-admin'
CLUSTER_ROLE = 'cluster-admin'
MANAGED_APPS_LOCAL_TILLER_FEATURE_FLAG = :managed_apps_local_tiller
def self.local_tiller_enabled?
Feature.enabled?(MANAGED_APPS_LOCAL_TILLER_FEATURE_FLAG)
end
end
end
end

View File

@ -3,7 +3,24 @@
module Gitlab
module Kubernetes
module Helm
module BaseCommand
class BaseCommand
attr_reader :name, :files
def initialize(rbac:, name:, files:, local_tiller_enabled:)
@rbac = rbac
@name = name
@files = files
@local_tiller_enabled = local_tiller_enabled
end
def rbac?
@rbac
end
def local_tiller_enabled?
@local_tiller_enabled
end
def pod_resource
pod_service_account_name = rbac? ? service_account_name : nil
@ -46,18 +63,6 @@ module Gitlab
files.keys
end
def name
raise "Not implemented"
end
def rbac?
raise "Not implemented"
end
def files
raise "Not implemented"
end
private
def files_dir

View File

@ -57,10 +57,6 @@ module Gitlab
'--tls-key', "#{files_dir}/key.pem"
]
end
def local_tiller_enabled?
::Gitlab::Kubernetes::Helm.local_tiller_enabled?
end
end
end
end

View File

@ -3,17 +3,13 @@
module Gitlab
module Kubernetes
module Helm
class DeleteCommand
include BaseCommand
class DeleteCommand < BaseCommand
include ClientCommand
attr_reader :predelete, :postdelete
attr_accessor :name, :files
def initialize(name:, rbac:, files:, predelete: nil, postdelete: nil)
@name = name
@files = files
@rbac = rbac
def initialize(predelete: nil, postdelete: nil, **args)
super(**args)
@predelete = predelete
@postdelete = postdelete
end
@ -32,10 +28,6 @@ module Gitlab
"uninstall-#{name}"
end
def rbac?
@rbac
end
def delete_command
command = ['helm', 'delete', '--purge', name] + tls_flags_if_remote_tiller

View File

@ -3,27 +3,13 @@
module Gitlab
module Kubernetes
module Helm
class InitCommand
include BaseCommand
attr_reader :name, :files
def initialize(name:, files:, rbac:)
@name = name
@files = files
@rbac = rbac
end
class InitCommand < BaseCommand
def generate_script
super + [
init_helm_command
].join("\n")
end
def rbac?
@rbac
end
private
def init_helm_command

View File

@ -3,19 +3,16 @@
module Gitlab
module Kubernetes
module Helm
class InstallCommand
include BaseCommand
class InstallCommand < BaseCommand
include ClientCommand
attr_reader :name, :files, :chart, :repository, :preinstall, :postinstall
attr_reader :chart, :repository, :preinstall, :postinstall
attr_accessor :version
def initialize(name:, chart:, files:, rbac:, version: nil, repository: nil, preinstall: nil, postinstall: nil)
@name = name
def initialize(chart:, version: nil, repository: nil, preinstall: nil, postinstall: nil, **args)
super(**args)
@chart = chart
@version = version
@rbac = rbac
@files = files
@repository = repository
@preinstall = preinstall
@postinstall = postinstall
@ -33,10 +30,6 @@ module Gitlab
].compact.join("\n")
end
def rbac?
@rbac
end
private
# Uses `helm upgrade --install` which means we can use this for both

View File

@ -5,23 +5,21 @@
module Gitlab
module Kubernetes
module Helm
class PatchCommand
include BaseCommand
class PatchCommand < BaseCommand
include ClientCommand
attr_reader :name, :files, :chart, :repository
attr_reader :chart, :repository
attr_accessor :version
def initialize(name:, chart:, files:, rbac:, version:, repository: nil)
def initialize(chart:, version:, repository: nil, **args)
super(**args)
# version is mandatory to prevent chart mismatches
# we do not want our values interpreted in the context of the wrong version
raise ArgumentError, 'version is required' if version.blank?
@name = name
@chart = chart
@version = version
@rbac = rbac
@files = files
@repository = repository
end
@ -35,10 +33,6 @@ module Gitlab
].compact.join("\n")
end
def rbac?
@rbac
end
private
def upgrade_command

View File

@ -3,18 +3,9 @@
module Gitlab
module Kubernetes
module Helm
class ResetCommand
include BaseCommand
class ResetCommand < BaseCommand
include ClientCommand
attr_reader :name, :files
def initialize(name:, rbac:, files:)
@name = name
@files = files
@rbac = rbac
end
def generate_script
super + [
reset_helm_command,
@ -23,10 +14,6 @@ module Gitlab
].join("\n")
end
def rbac?
@rbac
end
def pod_name
"uninstall-#{name}"
end

View File

@ -1846,6 +1846,9 @@ msgstr ""
msgid "AlertManagement|Alert"
msgstr ""
msgid "AlertManagement|Alert assignee(s): %{assignees}"
msgstr ""
msgid "AlertManagement|Alert detail"
msgstr ""
@ -1861,9 +1864,18 @@ msgstr ""
msgid "AlertManagement|All alerts"
msgstr ""
msgid "AlertManagement|Assign Assignees"
msgstr ""
msgid "AlertManagement|Assign status"
msgstr ""
msgid "AlertManagement|Assignee"
msgstr ""
msgid "AlertManagement|Assignee(s)"
msgstr ""
msgid "AlertManagement|Assignees"
msgstr ""
@ -1903,6 +1915,9 @@ msgstr ""
msgid "AlertManagement|More information"
msgstr ""
msgid "AlertManagement|No Matching Results"
msgstr ""
msgid "AlertManagement|No alert data to display."
msgstr ""
@ -1951,6 +1966,12 @@ msgstr ""
msgid "AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear."
msgstr ""
msgid "AlertManagement|There was an error while updating the assignee(s) list. Please try again."
msgstr ""
msgid "AlertManagement|There was an error while updating the assignee(s) of the alert. Please try again."
msgstr ""
msgid "AlertManagement|There was an error while updating the status of the alert. Please try again."
msgstr ""
@ -16318,6 +16339,9 @@ msgstr ""
msgid "Please provide a valid email address."
msgstr ""
msgid "Please provide attributes to update"
msgstr ""
msgid "Please refer to <a href=\"%{docs_url}\">%{docs_url}</a>"
msgstr ""
@ -17221,6 +17245,9 @@ msgstr ""
msgid "ProjectSettings|Enable 'Delete source branch' option by default"
msgstr ""
msgid "ProjectSettings|Enable merge trains and pipelines for merged results"
msgstr ""
msgid "ProjectSettings|Every merge creates a merge commit"
msgstr ""
@ -17287,9 +17314,6 @@ msgstr ""
msgid "ProjectSettings|Merge options"
msgstr ""
msgid "ProjectSettings|Merge pipelines will try to validate the post-merge result prior to merging"
msgstr ""
msgid "ProjectSettings|Merge requests"
msgstr ""
@ -17317,6 +17341,9 @@ msgstr ""
msgid "ProjectSettings|Pipelines"
msgstr ""
msgid "ProjectSettings|Pipelines for merge requests must be enabled in the CI/CD configuration file, or pipelines could be unresolvable or dropped"
msgstr ""
msgid "ProjectSettings|Pipelines must succeed"
msgstr ""

View File

@ -6,6 +6,7 @@ module QA
extend Forwardable
attr_reader :title
attr_accessor :expires_at
attribute :id
@ -53,13 +54,27 @@ module QA
def api_post_body
{
title: title,
key: public_key
key: public_key,
expires_at: expires_at
}
end
def api_delete_path
"/user/keys/#{id}"
end
def replicated?
api_client = Runtime::API::Client.new(:geo_secondary)
QA::Runtime::Logger.debug('Checking for SSH key replication')
Support::Retrier.retry_until(max_duration: QA::EE::Runtime::Geo.max_db_replication_time, sleep_interval: 3) do
response = get Runtime::API::Request.new(api_client, api_get_path).url
response.code == QA::Support::Api::HTTP_STATUS_OK &&
parse_body(response)[:title].include?(title)
end
end
end
end
end

View File

@ -1,11 +1,16 @@
# frozen_string_literal: true
module QA
context 'Monitor', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/217705', type: :flaky } do
describe 'with Prometheus Gitlab-managed cluster', :orchestrated, :kubernetes, :docker, :runner do
context 'Monitor' do
describe 'with Prometheus in a Gitlab-managed cluster', :orchestrated, :kubernetes do
before :all do
Flow::Login.sign_in
@project, @runner = deploy_project_with_prometheus
@cluster = Service::KubernetesCluster.new.create!
@project = Resource::Project.fabricate_via_api! do |project|
project.name = 'monitoring-project'
project.auto_devops_enabled = true
end
deploy_project_with_prometheus
end
before do
@ -14,7 +19,6 @@ module QA
end
after :all do
@runner.remove_via_api!
@cluster.remove!
end
@ -27,81 +31,94 @@ module QA
it 'duplicates to create dashboard to custom' do
Page::Project::Menu.perform(&:go_to_operations_metrics)
Page::Project::Operations::Metrics::Show.perform do |dashboard|
dashboard.duplicate_dashboard
Page::Project::Operations::Metrics::Show.perform do |on_dashboard|
on_dashboard.duplicate_dashboard
expect(dashboard).to have_metrics
expect(dashboard).to have_edit_dashboard_enabled
expect(on_dashboard).to have_metrics
expect(on_dashboard).to have_edit_dashboard_enabled
end
end
it 'verifies data on filtered deployed environment' do
Page::Project::Menu.perform(&:go_to_operations_metrics)
Page::Project::Operations::Metrics::Show.perform do |dashboard|
dashboard.filter_environment
Page::Project::Operations::Metrics::Show.perform do |on_dashboard|
on_dashboard.filter_environment
expect(dashboard).to have_metrics
expect(on_dashboard).to have_metrics
end
end
it 'filters using the quick range' do
Page::Project::Menu.perform(&:go_to_operations_metrics)
Page::Project::Operations::Metrics::Show.perform do |dashboard|
dashboard.show_last('30 minutes')
expect(dashboard).to have_metrics
Page::Project::Operations::Metrics::Show.perform do |on_dashboard|
on_dashboard.show_last('30 minutes')
expect(on_dashboard).to have_metrics
dashboard.show_last('3 hours')
expect(dashboard).to have_metrics
on_dashboard.show_last('3 hours')
expect(on_dashboard).to have_metrics
dashboard.show_last('1 day')
expect(dashboard).to have_metrics
on_dashboard.show_last('1 day')
expect(on_dashboard).to have_metrics
end
end
private
def deploy_project_with_prometheus
project = Resource::Project.fabricate_via_api! do |project|
project.name = 'cluster-with-prometheus'
project.description = 'Cluster with Prometheus'
%w[
CODE_QUALITY_DISABLED TEST_DISABLED LICENSE_MANAGEMENT_DISABLED
SAST_DISABLED DAST_DISABLED DEPENDENCY_SCANNING_DISABLED
CONTAINER_SCANNING_DISABLED PERFORMANCE_DISABLED
].each do |key|
Resource::CiVariable.fabricate_via_api! do |resource|
resource.project = @project
resource.key = key
resource.value = '1'
resource.masked = false
end
end
runner = Resource::Runner.fabricate_via_api! do |runner|
runner.project = project
runner.name = project.name
end
Flow::Login.sign_in
@cluster = Service::KubernetesCluster.new.create!
cluster_props = Resource::KubernetesCluster::ProjectCluster.fabricate! do |cluster_settings|
cluster_settings.project = project
Resource::KubernetesCluster::ProjectCluster.fabricate! do |cluster_settings|
cluster_settings.project = @project
cluster_settings.cluster = @cluster
cluster_settings.install_helm_tiller = true
cluster_settings.install_runner = true
cluster_settings.install_ingress = true
cluster_settings.install_prometheus = true
end
Resource::CiVariable.fabricate_via_api! do |ci_variable|
ci_variable.project = project
ci_variable.key = 'AUTO_DEVOPS_DOMAIN'
ci_variable.value = cluster_props.ingress_ip
ci_variable.masked = false
end
Resource::Repository::ProjectPush.fabricate! do |push|
push.project = project
push.project = @project
push.directory = Pathname
.new(__dir__)
.join('../../../../fixtures/monitored_auto_devops')
.join('../../../../fixtures/auto_devops_rack')
push.commit_message = 'Create AutoDevOps compatible Project for Monitoring'
end
Page::Project::Menu.perform(&:click_ci_cd_pipelines)
Page::Project::Pipeline::Index.perform(&:wait_for_latest_pipeline_success_or_retry)
Page::Project::Pipeline::Index.perform(&:click_on_latest_pipeline)
[project, runner]
Page::Project::Pipeline::Show.perform do |pipeline|
pipeline.click_job('build')
end
Page::Project::Job::Show.perform do |job|
expect(job).to be_successful(timeout: 600)
job.click_element(:pipeline_path)
end
Page::Project::Pipeline::Show.perform do |pipeline|
pipeline.click_job('production')
end
Page::Project::Job::Show.perform do |job|
expect(job).to be_successful(timeout: 1200)
job.click_element(:pipeline_path)
end
end
def verify_add_custom_metric
@ -115,8 +132,8 @@ module QA
Page::Project::Menu.perform(&:go_to_operations_metrics)
Page::Project::Operations::Metrics::Show.perform do |dashboard|
expect(dashboard).to have_custom_metric('HTTP Requests Total')
Page::Project::Operations::Metrics::Show.perform do |on_dashboard|
expect(on_dashboard).to have_custom_metric('HTTP Requests Total')
end
end
@ -130,8 +147,8 @@ module QA
Page::Project::Menu.perform(&:go_to_operations_metrics)
Page::Project::Operations::Metrics::Show.perform do |dashboard|
expect(dashboard).to have_custom_metric('Throughput')
Page::Project::Operations::Metrics::Show.perform do |on_dashboard|
expect(on_dashboard).to have_custom_metric('Throughput')
end
end
@ -146,8 +163,8 @@ module QA
Page::Project::Menu.perform(&:go_to_operations_metrics)
Page::Project::Operations::Metrics::Show.perform do |dashboard|
expect(dashboard).not_to have_custom_metric('Throughput')
Page::Project::Operations::Metrics::Show.perform do |on_dashboard|
expect(on_dashboard).not_to have_custom_metric('Throughput')
end
end
end

View File

@ -1,5 +1,7 @@
import { mount, shallowMount } from '@vue/test-utils';
import { GlAlert, GlLoadingIcon, GlTable } from '@gitlab/ui';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import AlertDetails from '~/alert_management/components/alert_details.vue';
import createIssueQuery from '~/alert_management/graphql/mutations/create_issue_from_alert.graphql';
import { joinPaths } from '~/lib/utils/url_utility';
@ -14,6 +16,7 @@ const mockAlert = mockAlerts[0];
describe('AlertDetails', () => {
let wrapper;
let mock;
const projectPath = 'root/alerts';
const projectIssuesPath = 'root/alerts/-/issues';
@ -43,12 +46,17 @@ describe('AlertDetails', () => {
});
}
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
if (wrapper) {
if (wrapper) {
wrapper.destroy();
}
}
mock.restore();
});
const findCreateIssueBtn = () => wrapper.find('[data-testid="createIssueBtn"]');

View File

@ -0,0 +1,133 @@
import { shallowMount } from '@vue/test-utils';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { GlDropdownItem } from '@gitlab/ui';
import SidebarAssignee from '~/alert_management/components/sidebar/sidebar_assignee.vue';
import SidebarAssignees from '~/alert_management/components/sidebar/sidebar_assignees.vue';
import AlertSetAssignees from '~/alert_management/graphql/mutations/alert_set_assignees.graphql';
import mockAlerts from '../mocks/alerts.json';
const mockAlert = mockAlerts[0];
describe('Alert Details Sidebar Assignees', () => {
let wrapper;
let mock;
function mountComponent({
data,
users = [],
isDropdownSearching = false,
sidebarCollapsed = true,
loading = false,
stubs = {},
} = {}) {
wrapper = shallowMount(SidebarAssignees, {
data() {
return {
users,
isDropdownSearching,
};
},
propsData: {
alert: { ...mockAlert },
...data,
sidebarCollapsed,
projectPath: 'projectPath',
},
mocks: {
$apollo: {
mutate: jest.fn(),
queries: {
alert: {
loading,
},
},
},
},
stubs,
});
}
afterEach(() => {
if (wrapper) {
wrapper.destroy();
}
mock.restore();
});
describe('updating the alert status', () => {
const mockUpdatedMutationResult = {
data: {
updateAlertStatus: {
errors: [],
alert: {
assigneeUsernames: ['root'],
},
},
},
};
beforeEach(() => {
mock = new MockAdapter(axios);
const path = '/autocomplete/users.json';
const users = [
{
avatar_url:
'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
id: 1,
name: 'User 1',
username: 'root',
},
{
avatar_url:
'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
id: 2,
name: 'User 2',
username: 'not-root',
},
];
mock.onGet(path).replyOnce(200, users);
mountComponent({
data: { alert: mockAlert },
sidebarCollapsed: false,
loading: false,
users,
stubs: {
SidebarAssignee,
},
});
});
it('renders a unassigned option', () => {
wrapper.setData({ isDropdownSearching: false });
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.find(GlDropdownItem).text()).toBe('Unassigned');
});
});
it('calls `$apollo.mutate` with `AlertSetAssignees` mutation and variables containing `iid`, `assigneeUsernames`, & `projectPath`', () => {
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockUpdatedMutationResult);
wrapper.setData({ isDropdownSearching: false });
return wrapper.vm.$nextTick().then(() => {
wrapper.find(SidebarAssignee).vm.$emit('update-alert-assignees', 'root');
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
mutation: AlertSetAssignees,
variables: {
iid: '1527542',
assigneeUsernames: ['root'],
projectPath: 'projectPath',
},
});
});
});
it('stops updating and cancels loading when the request fails', () => {
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockReturnValue(Promise.reject(new Error()));
wrapper.vm.updateAlertAssignees('root');
expect(wrapper.find('[data-testid="assigned-users"]').text()).toBe('Unassigned');
});
});
});

View File

@ -1,20 +1,32 @@
import { shallowMount } from '@vue/test-utils';
import { shallowMount, mount } from '@vue/test-utils';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import AlertSidebar from '~/alert_management/components/alert_sidebar.vue';
import SidebarAssignees from '~/alert_management/components/sidebar/sidebar_assignees.vue';
import mockAlerts from '../mocks/alerts.json';
const mockAlert = mockAlerts[0];
describe('Alert Details Sidebar', () => {
let wrapper;
let mock;
function mountComponent({
sidebarCollapsed = true,
mountMethod = shallowMount,
alertAssignee = false,
stubs = {},
alert = {},
} = {}) {
wrapper = mountMethod(AlertSidebar, {
propsData: {
alert: {},
alert,
sidebarCollapsed,
projectPath: 'projectPath',
},
provide: {
glFeatures: { alertAssignee },
},
stubs,
});
}
@ -23,15 +35,30 @@ describe('Alert Details Sidebar', () => {
if (wrapper) {
wrapper.destroy();
}
mock.restore();
});
describe('the sidebar renders', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
mountComponent();
});
it('open as default', () => {
expect(wrapper.props('sidebarCollapsed')).toBe(true);
});
it('should not render side bar assignee dropdown by default', () => {
expect(wrapper.find(SidebarAssignees).exists()).toBe(false);
});
it('should render side bar assignee dropdown if feature flag enabled', () => {
mountComponent({
mountMethod: mount,
alertAssignee: true,
alert: mockAlert,
});
expect(wrapper.find(SidebarAssignees).exists()).toBe(true);
});
});
});

View File

@ -13,14 +13,8 @@ describe('Alert Details Sidebar Status', () => {
const findStatusDropdownItem = () => wrapper.find(GlDropdownItem);
const findStatusLoadingIcon = () => wrapper.find(GlLoadingIcon);
function mountComponent({
data,
sidebarCollapsed = true,
loading = false,
mountMethod = shallowMount,
stubs = {},
} = {}) {
wrapper = mountMethod(AlertSidebarStatus, {
function mountComponent({ data, sidebarCollapsed = true, loading = false, stubs = {} } = {}) {
wrapper = shallowMount(AlertSidebarStatus, {
propsData: {
alert: { ...mockAlert },
...data,
@ -85,7 +79,7 @@ describe('Alert Details Sidebar Status', () => {
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockReturnValue(Promise.reject(new Error()));
findStatusDropdownItem().vm.$emit('click');
expect(findStatusLoadingIcon().exists()).toBe(false);
expect(wrapper.find('.gl-text-gray-700').text()).toBe('Triggered');
expect(wrapper.find('[data-testid="status"]').text()).toBe('Triggered');
});
});

View File

@ -0,0 +1,167 @@
# frozen_string_literal: true
require 'spec_helper'
describe Mutations::AlertManagement::Alerts::SetAssignees do
let_it_be(:starting_assignee) { create(:user) }
let_it_be(:unassigned_user) { create(:user) }
let_it_be(:alert) { create(:alert_management_alert, assignees: [starting_assignee]) }
let_it_be(:project) { alert.project }
let(:current_user) { starting_assignee }
let(:assignee_usernames) { [unassigned_user.username] }
let(:operation_mode) { nil }
let(:args) do
{
project_path: project.full_path,
iid: alert.iid,
assignee_usernames: assignee_usernames,
operation_mode: operation_mode
}
end
before_all do
project.add_developer(starting_assignee)
project.add_developer(unassigned_user)
end
specify { expect(described_class).to require_graphql_authorizations(:update_alert_management_alert) }
describe '#resolve' do
let(:expected_assignees) { [unassigned_user] }
subject(:resolve) { mutation_for(project, current_user).resolve(args) }
shared_examples 'successful resolution' do
after do
alert.assignees = [starting_assignee]
end
it 'successfully resolves' do
expect(resolve).to eq(alert: alert.reload, errors: [])
expect(alert.assignees).to eq(expected_assignees)
end
end
shared_examples 'noop' do
it 'makes no changes' do
original_assignees = alert.assignees
expect(resolve).to eq(alert: alert.reload, errors: [])
expect(alert.assignees).to eq(original_assignees)
end
end
context 'when operation mode is not specified' do
it_behaves_like 'successful resolution'
end
context 'when user does not have permission to update alerts' do
let(:current_user) { create(:user) }
it 'raises an error if the resource is not accessible to the user' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'for APPEND operation' do
let(:operation_mode) { Types::MutationOperationModeEnum.enum[:append] }
# Only allow a single assignee
context 'when a different user is already assigned' do
it_behaves_like 'noop'
end
context 'when no users are specified' do
let(:assignee_usernames) { [] }
it_behaves_like 'noop'
end
context 'when a user is specified and no user is assigned' do
before do
alert.assignees = []
end
it_behaves_like 'successful resolution'
end
context 'when the specified user is already assigned to the alert' do
let(:assignee_usernames) { [starting_assignee.username] }
it_behaves_like 'noop'
end
end
context 'for REPLACE operation' do
let(:operation_mode) { Types::MutationOperationModeEnum.enum[:replace] }
context 'when a different user is already assigned' do
it_behaves_like 'successful resolution'
end
context 'when no users are specified' do
let(:assignee_usernames) { [] }
let(:expected_assignees) { [] }
it_behaves_like 'successful resolution'
end
context 'when a user is specified and no user is assigned' do
before do
alert.assignees = []
end
it_behaves_like 'successful resolution'
end
context 'when the specified user is already assigned to the alert' do
let(:assignee_usernames) { [starting_assignee.username] }
it_behaves_like 'noop'
end
context 'when multiple users are specified' do
let(:assignees) { [starting_assignee, unassigned_user] }
let(:assignee_usernames) { assignees.map(&:username) }
let(:expected_assignees) { [assignees.last] }
it_behaves_like 'successful resolution'
end
end
context 'for REMOVE operation' do
let(:operation_mode) { Types::MutationOperationModeEnum.enum[:remove] }
context 'when a different user is already assigned' do
it_behaves_like 'noop'
end
context 'when no users are specified' do
let(:assignee_usernames) { [] }
it_behaves_like 'noop'
end
context 'when a user is specified and no user is assigned' do
before do
alert.assignees = []
end
it_behaves_like 'noop'
end
context 'when the specified user is already assigned to the alert' do
let(:assignee_usernames) { [starting_assignee.username] }
let(:expected_assignees) { [] }
it_behaves_like 'successful resolution'
end
end
end
def mutation_for(project, user)
described_class.new(object: project, context: { current_user: user }, field: nil)
end
end

View File

@ -17,7 +17,8 @@ describe Gitlab::Kubernetes::Helm::API do
name: application_name,
chart: 'chart-name',
rbac: rbac,
files: files
files: files,
local_tiller_enabled: true
)
end
@ -142,7 +143,7 @@ describe Gitlab::Kubernetes::Helm::API do
end
context 'with a service account' do
let(:command) { Gitlab::Kubernetes::Helm::InitCommand.new(name: application_name, files: files, rbac: rbac) }
let(:command) { Gitlab::Kubernetes::Helm::InitCommand.new(name: application_name, files: files, rbac: rbac, local_tiller_enabled: true) }
context 'rbac-enabled cluster' do
let(:rbac) { true }

View File

@ -11,25 +11,14 @@ describe Gitlab::Kubernetes::Helm::BaseCommand do
let(:rbac) { false }
let(:test_class) do
Class.new do
include Gitlab::Kubernetes::Helm::BaseCommand
Class.new(Gitlab::Kubernetes::Helm::BaseCommand) do
def initialize(rbac)
@rbac = rbac
end
def name
"test-class-name"
end
def rbac?
@rbac
end
def files
{
some: 'value'
}
super(
name: 'test-class-name',
rbac: rbac,
files: { some: 'value' },
local_tiller_enabled: false
)
end
end
end

View File

@ -3,11 +3,12 @@
require 'spec_helper'
describe Gitlab::Kubernetes::Helm::DeleteCommand do
subject(:delete_command) { described_class.new(name: app_name, rbac: rbac, files: files) }
subject(:delete_command) { described_class.new(name: app_name, rbac: rbac, files: files, local_tiller_enabled: local_tiller_enabled) }
let(:app_name) { 'app-name' }
let(:rbac) { true }
let(:files) { {} }
let(:local_tiller_enabled) { true }
it_behaves_like 'helm command generator' do
let(:commands) do
@ -21,9 +22,7 @@ describe Gitlab::Kubernetes::Helm::DeleteCommand do
end
context 'tillerless feature disabled' do
before do
stub_feature_flags(managed_apps_local_tiller: false)
end
let(:local_tiller_enabled) { false }
it_behaves_like 'helm command generator' do
let(:commands) do

View File

@ -3,7 +3,7 @@
require 'spec_helper'
describe Gitlab::Kubernetes::Helm::InitCommand do
subject(:init_command) { described_class.new(name: application.name, files: files, rbac: rbac) }
subject(:init_command) { described_class.new(name: application.name, files: files, rbac: rbac, local_tiller_enabled: false) }
let(:application) { create(:clusters_applications_helm) }
let(:rbac) { false }

View File

@ -12,7 +12,8 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
version: version,
repository: repository,
preinstall: preinstall,
postinstall: postinstall
postinstall: postinstall,
local_tiller_enabled: local_tiller_enabled
)
end
@ -22,6 +23,7 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
let(:version) { '1.2.3' }
let(:preinstall) { nil }
let(:postinstall) { nil }
let(:local_tiller_enabled) { true }
it_behaves_like 'helm command generator' do
let(:commands) do
@ -51,9 +53,7 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
end
context 'tillerless feature disabled' do
before do
stub_feature_flags(managed_apps_local_tiller: false)
end
let(:local_tiller_enabled) { false }
let(:tls_flags) do
<<~EOS.squish

View File

@ -7,6 +7,7 @@ describe Gitlab::Kubernetes::Helm::PatchCommand do
let(:repository) { 'https://repository.example.com' }
let(:rbac) { false }
let(:version) { '1.2.3' }
let(:local_tiller_enabled) { true }
subject(:patch_command) do
described_class.new(
@ -15,14 +16,13 @@ describe Gitlab::Kubernetes::Helm::PatchCommand do
rbac: rbac,
files: files,
version: version,
repository: repository
repository: repository,
local_tiller_enabled: local_tiller_enabled
)
end
context 'when local tiller feature is disabled' do
before do
stub_feature_flags(managed_apps_local_tiller: false)
end
let(:local_tiller_enabled) { false }
let(:tls_flags) do
<<~EOS.squish

View File

@ -3,7 +3,7 @@
require 'spec_helper'
describe Gitlab::Kubernetes::Helm::ResetCommand do
subject(:reset_command) { described_class.new(name: name, rbac: rbac, files: files) }
subject(:reset_command) { described_class.new(name: name, rbac: rbac, files: files, local_tiller_enabled: false) }
let(:rbac) { true }
let(:name) { 'helm' }

View File

@ -0,0 +1,65 @@
# frozen_string_literal: true
require 'spec_helper'
describe 'Setting assignees of an alert' do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
let_it_be(:current_user) { create(:user) }
let_it_be(:alert) { create(:alert_management_alert, project: project) }
let(:input) { { assignee_usernames: [current_user.username] } }
let(:mutation) do
graphql_mutation(
:alert_set_assignees,
{ project_path: project.full_path, iid: alert.iid.to_s }.merge(input),
<<~QL
clientMutationId
errors
alert {
assignees {
nodes {
username
}
}
}
QL
)
end
let(:mutation_response) { graphql_mutation_response(:alert_set_assignees) }
before_all do
project.add_developer(current_user)
end
it 'updates the assignee of the alert' do
post_graphql_mutation(mutation, current_user: current_user)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_response['alert']['assignees']['nodes'].first['username']).to eq(current_user.username)
expect(alert.reload.assignees).to contain_exactly(current_user)
end
context 'with operation_mode specified' do
let(:input) do
{
assignee_usernames: [current_user.username],
operation_mode: Types::MutationOperationModeEnum.enum[:remove]
}
end
before do
alert.assignees = [current_user]
end
it 'updates the assignee of the alert' do
post_graphql_mutation(mutation, current_user: current_user)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_response['alert']['assignees']['nodes']).to be_empty
expect(alert.reload.assignees).to be_empty
end
end
end

View File

@ -0,0 +1,81 @@
# frozen_string_literal: true
require 'spec_helper'
describe AlertManagement::Alerts::UpdateService do
let_it_be(:user_with_permissions) { create(:user) }
let_it_be(:user_without_permissions) { create(:user) }
let_it_be(:alert, reload: true) { create(:alert_management_alert) }
let_it_be(:project) { alert.project }
let(:current_user) { user_with_permissions }
let(:params) { {} }
let(:service) { described_class.new(alert, current_user, params) }
before_all do
project.add_developer(user_with_permissions)
end
describe '#execute' do
subject(:response) { service.execute }
context 'when user does not have permission to update alerts' do
let(:current_user) { user_without_permissions }
it 'results in an error' do
expect(response).to be_error
expect(response.message).to eq('You have no permissions')
end
end
context 'when no parameters are included' do
it 'results in an error' do
expect(response).to be_error
expect(response.message).to eq('Please provide attributes to update')
end
end
context 'when an error occures during update' do
let(:params) { { title: nil } }
it 'results in an error' do
expect(response).to be_error
expect(response.message).to eq("Title can't be blank")
end
end
context 'when a model attribute is included' do
let(:params) { { title: 'This is an updated alert.' } }
it 'updates the attribute' do
original_title = alert.title
expect { response }.to change { alert.title }.from(original_title).to(params[:title])
expect(response).to be_success
end
end
context 'when assignees are included' do
let(:params) { { assignees: [user_with_permissions] } }
after do
alert.assignees = []
end
it 'assigns the user' do
expect { response }.to change { alert.reload.assignees }.from([]).to(params[:assignees])
expect(response).to be_success
end
context 'with multiple users included' do
let(:params) { { assignees: [user_with_permissions, user_without_permissions] } }
it 'assigns the first permissioned user' do
expect { response }.to change { alert.reload.assignees }.from([]).to([user_with_permissions])
expect(response).to be_success
end
end
end
end
end

View File

@ -54,7 +54,7 @@ RSpec.shared_examples 'cluster application helm specs' do |application_name|
context 'managed_apps_local_tiller feature flag is enabled' do
before do
stub_feature_flags(managed_apps_local_tiller: true)
stub_feature_flags(managed_apps_local_tiller: application.cluster.clusterable)
end
it 'does not include cert files' do

View File

@ -18,7 +18,7 @@ RSpec.shared_examples 'cluster application initial status specs' do
context 'local tiller feature flag is enabled' do
before do
stub_feature_flags(managed_apps_local_tiller: true)
stub_feature_flags(managed_apps_local_tiller: cluster.clusterable)
end
it 'sets a default status' do

View File

@ -66,7 +66,7 @@ RSpec.shared_examples 'cluster application status specs' do |application_name|
context 'managed_apps_local_tiller feature flag enabled' do
before do
stub_feature_flags(managed_apps_local_tiller: true)
stub_feature_flags(managed_apps_local_tiller: subject.cluster.clusterable)
end
it 'does not update the helm version' do