diff --git a/app/assets/javascripts/alert_management/components/alert_details.vue b/app/assets/javascripts/alert_management/components/alert_details.vue
index 99194007551..6abdbc6be98 100644
--- a/app/assets/javascripts/alert_management/components/alert_details.vue
+++ b/app/assets/javascripts/alert_management/components/alert_details.vue
@@ -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"
>
diff --git a/app/assets/javascripts/alert_management/components/alert_sidebar.vue b/app/assets/javascripts/alert_management/components/alert_sidebar.vue
index 2d647ad296a..e1fd3e7fd24 100644
--- a/app/assets/javascripts/alert_management/components/alert_sidebar.vue
+++ b/app/assets/javascripts/alert_management/components/alert_sidebar.vue
@@ -1,14 +1,18 @@
@@ -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)"
+ />
+
{{ assignedUsers }}
++ {{ s__('AlertManagement|Assignee') }} + + {{ s__('AlertManagement|Edit') }} + +
+ ++ {{ + assignedUsers + }} + + {{ s__('AlertManagement|None') }} + +
+- {{ - $options.statuses[alert.status] - }} + {{ $options.statuses[alert.status] }} {{ s__('AlertManagement|None') }} diff --git a/app/assets/javascripts/alert_management/graphql/mutations/alert_set_assignees.graphql b/app/assets/javascripts/alert_management/graphql/mutations/alert_set_assignees.graphql new file mode 100644 index 00000000000..efeaf8fa372 --- /dev/null +++ b/app/assets/javascripts/alert_management/graphql/mutations/alert_set_assignees.graphql @@ -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 + } + } + } + } +} diff --git a/app/assets/javascripts/alert_management/services/index.js b/app/assets/javascripts/alert_management/services/index.js deleted file mode 100644 index 787603d3e7a..00000000000 --- a/app/assets/javascripts/alert_management/services/index.js +++ /dev/null @@ -1,7 +0,0 @@ -import axios from '~/lib/utils/axios_utils'; - -export default { - getAlertManagementList({ endpoint }) { - return axios.get(endpoint); - }, -}; diff --git a/app/assets/stylesheets/pages/alert_management/details.scss b/app/assets/stylesheets/pages/alert_management/details.scss index 5a7bde1a3bc..c96127f05d4 100644 --- a/app/assets/stylesheets/pages/alert_management/details.scss +++ b/app/assets/stylesheets/pages/alert_management/details.scss @@ -49,4 +49,15 @@ background-color: $white; } } + + .assignee-dropdown-item { + button { + display: flex; + align-items: center; + + &::before { + top: 50% !important; + } + } + } } diff --git a/app/controllers/clusters/base_controller.rb b/app/controllers/clusters/base_controller.rb index 8c13cc67be2..6b83400971d 100644 --- a/app/controllers/clusters/base_controller.rb +++ b/app/controllers/clusters/base_controller.rb @@ -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 diff --git a/app/graphql/mutations/alert_management/alerts/set_assignees.rb b/app/graphql/mutations/alert_management/alerts/set_assignees.rb new file mode 100644 index 00000000000..1e0c9fdeeaf --- /dev/null +++ b/app/graphql/mutations/alert_management/alerts/set_assignees.rb @@ -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 diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index 8d4368cc30a..8874c56dfdb 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -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 diff --git a/app/models/clusters/applications/cert_manager.rb b/app/models/clusters/applications/cert_manager.rb index 1efa44c39c5..53c90fa56d5 100644 --- a/app/models/clusters/applications/cert_manager.rb +++ b/app/models/clusters/applications/cert_manager.rb @@ -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 diff --git a/app/models/clusters/applications/crossplane.rb b/app/models/clusters/applications/crossplane.rb index 420e56c1742..2e5a8210b3c 100644 --- a/app/models/clusters/applications/crossplane.rb +++ b/app/models/clusters/applications/crossplane.rb @@ -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 diff --git a/app/models/clusters/applications/elastic_stack.rb b/app/models/clusters/applications/elastic_stack.rb index 0d029aabc3b..679a4d4e447 100644 --- a/app/models/clusters/applications/elastic_stack.rb +++ b/app/models/clusters/applications/elastic_stack.rb @@ -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) ] diff --git a/app/models/clusters/applications/fluentd.rb b/app/models/clusters/applications/fluentd.rb index 3fd6e870edc..1bcd39618f6 100644 --- a/app/models/clusters/applications/fluentd.rb +++ b/app/models/clusters/applications/fluentd.rb @@ -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 diff --git a/app/models/clusters/applications/helm.rb b/app/models/clusters/applications/helm.rb index 4a1bcac4bb7..226a9c26db0 100644 --- a/app/models/clusters/applications/helm.rb +++ b/app/models/clusters/applications/helm.rb @@ -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 diff --git a/app/models/clusters/applications/ingress.rb b/app/models/clusters/applications/ingress.rb index dd354198910..a44450ec7a9 100644 --- a/app/models/clusters/applications/ingress.rb +++ b/app/models/clusters/applications/ingress.rb @@ -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 diff --git a/app/models/clusters/applications/jupyter.rb b/app/models/clusters/applications/jupyter.rb index 056ea355de6..b737f0f962f 100644 --- a/app/models/clusters/applications/jupyter.rb +++ b/app/models/clusters/applications/jupyter.rb @@ -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 diff --git a/app/models/clusters/applications/knative.rb b/app/models/clusters/applications/knative.rb index 3047da12dd9..b55fc3c45fc 100644 --- a/app/models/clusters/applications/knative.rb +++ b/app/models/clusters/applications/knative.rb @@ -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 diff --git a/app/models/clusters/applications/prometheus.rb b/app/models/clusters/applications/prometheus.rb index 46ca0d13b65..24bb1df6d22 100644 --- a/app/models/clusters/applications/prometheus.rb +++ b/app/models/clusters/applications/prometheus.rb @@ -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 diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb index 2aa4ca719a5..6d3b6c4ed8f 100644 --- a/app/models/clusters/applications/runner.rb +++ b/app/models/clusters/applications/runner.rb @@ -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 diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index 81107bb00e3..65dfc108d49 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -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 diff --git a/app/models/clusters/concerns/application_core.rb b/app/models/clusters/concerns/application_core.rb index 297d00aa281..c1f63758906 100644 --- a/app/models/clusters/concerns/application_core.rb +++ b/app/models/clusters/concerns/application_core.rb @@ -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? diff --git a/app/models/clusters/concerns/application_data.rb b/app/models/clusters/concerns/application_data.rb index 77c606553d2..ade27e69642 100644 --- a/app/models/clusters/concerns/application_data.rb +++ b/app/models/clusters/concerns/application_data.rb @@ -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 diff --git a/app/models/clusters/concerns/application_status.rb b/app/models/clusters/concerns/application_status.rb index 79d1849b05e..86d74ed7b1c 100644 --- a/app/models/clusters/concerns/application_status.rb +++ b/app/models/clusters/concerns/application_status.rb @@ -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 diff --git a/app/services/alert_management/alerts/update_service.rb b/app/services/alert_management/alerts/update_service.rb new file mode 100644 index 00000000000..76397d2fb48 --- /dev/null +++ b/app/services/alert_management/alerts/update_service.rb @@ -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 diff --git a/changelogs/unreleased/alert-assignee-dropdown.yml b/changelogs/unreleased/alert-assignee-dropdown.yml new file mode 100644 index 00000000000..3ad54932780 --- /dev/null +++ b/changelogs/unreleased/alert-assignee-dropdown.yml @@ -0,0 +1,5 @@ +--- +title: Allow the assignment of alerts to users from the alert detail view +merge_request: 33122 +author: +type: added diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index f7b0855a044..9c478f2d56e 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -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 diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index e4654b97b6e..90aaeac3c27 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -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, diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 34f7a4c621e..7387dfaaa55 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -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. diff --git a/doc/ci/merge_request_pipelines/pipelines_for_merged_results/index.md b/doc/ci/merge_request_pipelines/pipelines_for_merged_results/index.md index ac79c2152c0..a43085c8e7a 100644 --- a/doc/ci/merge_request_pipelines/pipelines_for_merged_results/index.md +++ b/doc/ci/merge_request_pipelines/pipelines_for_merged_results/index.md @@ -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:** diff --git a/doc/ci/merge_request_pipelines/pipelines_for_merged_results/merge_trains/index.md b/doc/ci/merge_request_pipelines/pipelines_for_merged_results/merge_trains/index.md index c4d16d73243..fc8e6368d72 100644 --- a/doc/ci/merge_request_pipelines/pipelines_for_merged_results/merge_trains/index.md +++ b/doc/ci/merge_request_pipelines/pipelines_for_merged_results/merge_trains/index.md @@ -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 diff --git a/doc/user/instance/clusters/index.md b/doc/user/instance/clusters/index.md index 4a8b45f6643..8059b8ca642 100644 --- a/doc/user/instance/clusters/index.md +++ b/doc/user/instance/clusters/index.md @@ -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. diff --git a/doc/user/project/clusters/add_eks_clusters.md b/doc/user/project/clusters/add_eks_clusters.md index 34dde49deae..4d9604bf467 100644 --- a/doc/user/project/clusters/add_eks_clusters.md +++ b/doc/user/project/clusters/add_eks_clusters.md @@ -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. diff --git a/doc/user/project/clusters/add_gke_clusters.md b/doc/user/project/clusters/add_gke_clusters.md index e056b4d942f..2746076befe 100644 --- a/doc/user/project/clusters/add_gke_clusters.md +++ b/doc/user/project/clusters/add_gke_clusters.md @@ -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 diff --git a/lib/gitlab/kubernetes/helm.rb b/lib/gitlab/kubernetes/helm.rb index 00ab7109267..9507f7bc117 100644 --- a/lib/gitlab/kubernetes/helm.rb +++ b/lib/gitlab/kubernetes/helm.rb @@ -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 diff --git a/lib/gitlab/kubernetes/helm/base_command.rb b/lib/gitlab/kubernetes/helm/base_command.rb index 31cd21f17e0..f27ad05599e 100644 --- a/lib/gitlab/kubernetes/helm/base_command.rb +++ b/lib/gitlab/kubernetes/helm/base_command.rb @@ -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 diff --git a/lib/gitlab/kubernetes/helm/client_command.rb b/lib/gitlab/kubernetes/helm/client_command.rb index e7ade7e4d39..24458e1b4b3 100644 --- a/lib/gitlab/kubernetes/helm/client_command.rb +++ b/lib/gitlab/kubernetes/helm/client_command.rb @@ -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 diff --git a/lib/gitlab/kubernetes/helm/delete_command.rb b/lib/gitlab/kubernetes/helm/delete_command.rb index 771444ee9ee..3bb41d09994 100644 --- a/lib/gitlab/kubernetes/helm/delete_command.rb +++ b/lib/gitlab/kubernetes/helm/delete_command.rb @@ -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 diff --git a/lib/gitlab/kubernetes/helm/init_command.rb b/lib/gitlab/kubernetes/helm/init_command.rb index 058f38f2c9c..e4844e255c5 100644 --- a/lib/gitlab/kubernetes/helm/init_command.rb +++ b/lib/gitlab/kubernetes/helm/init_command.rb @@ -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 diff --git a/lib/gitlab/kubernetes/helm/install_command.rb b/lib/gitlab/kubernetes/helm/install_command.rb index 3784aecccb5..cf6d993cad4 100644 --- a/lib/gitlab/kubernetes/helm/install_command.rb +++ b/lib/gitlab/kubernetes/helm/install_command.rb @@ -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 diff --git a/lib/gitlab/kubernetes/helm/patch_command.rb b/lib/gitlab/kubernetes/helm/patch_command.rb index ed7a5c2b2d6..1a5fab116bd 100644 --- a/lib/gitlab/kubernetes/helm/patch_command.rb +++ b/lib/gitlab/kubernetes/helm/patch_command.rb @@ -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 diff --git a/lib/gitlab/kubernetes/helm/reset_command.rb b/lib/gitlab/kubernetes/helm/reset_command.rb index 13176360227..f1f7938039c 100644 --- a/lib/gitlab/kubernetes/helm/reset_command.rb +++ b/lib/gitlab/kubernetes/helm/reset_command.rb @@ -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 diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 88e707836ce..afeecab9644 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -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 %{docs_url}" 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 "" diff --git a/qa/qa/resource/ssh_key.rb b/qa/qa/resource/ssh_key.rb index b948bf3969b..317d70ef2c3 100644 --- a/qa/qa/resource/ssh_key.rb +++ b/qa/qa/resource/ssh_key.rb @@ -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 diff --git a/qa/qa/specs/features/browser_ui/8_monitor/all_monitor_core_features_spec.rb b/qa/qa/specs/features/browser_ui/8_monitor/all_monitor_core_features_spec.rb index 45273655bb6..a9ed6651069 100644 --- a/qa/qa/specs/features/browser_ui/8_monitor/all_monitor_core_features_spec.rb +++ b/qa/qa/specs/features/browser_ui/8_monitor/all_monitor_core_features_spec.rb @@ -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 diff --git a/spec/frontend/alert_management/components/alert_management_detail_spec.js b/spec/frontend/alert_management/components/alert_management_detail_spec.js index bbf6c5428be..14e45a4f563 100644 --- a/spec/frontend/alert_management/components/alert_management_detail_spec.js +++ b/spec/frontend/alert_management/components/alert_management_detail_spec.js @@ -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"]'); diff --git a/spec/frontend/alert_management/components/alert_managment_sidebar_assignees_spec.js b/spec/frontend/alert_management/components/alert_managment_sidebar_assignees_spec.js new file mode 100644 index 00000000000..c1b9746c308 --- /dev/null +++ b/spec/frontend/alert_management/components/alert_managment_sidebar_assignees_spec.js @@ -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'); + }); + }); +}); diff --git a/spec/frontend/alert_management/components/alert_sidebar_spec.js b/spec/frontend/alert_management/components/alert_sidebar_spec.js index 1d581e8e2f1..5fa6048de1b 100644 --- a/spec/frontend/alert_management/components/alert_sidebar_spec.js +++ b/spec/frontend/alert_management/components/alert_sidebar_spec.js @@ -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); + }); }); }); diff --git a/spec/frontend/alert_management/components/alert_sidebar_status_spec.js b/spec/frontend/alert_management/components/alert_sidebar_status_spec.js index d8d6634a46e..94643966a43 100644 --- a/spec/frontend/alert_management/components/alert_sidebar_status_spec.js +++ b/spec/frontend/alert_management/components/alert_sidebar_status_spec.js @@ -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'); }); }); diff --git a/spec/graphql/mutations/alert_management/alerts/set_assignees_spec.rb b/spec/graphql/mutations/alert_management/alerts/set_assignees_spec.rb new file mode 100644 index 00000000000..a025b3d344a --- /dev/null +++ b/spec/graphql/mutations/alert_management/alerts/set_assignees_spec.rb @@ -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 diff --git a/spec/lib/gitlab/kubernetes/helm/api_spec.rb b/spec/lib/gitlab/kubernetes/helm/api_spec.rb index 1f925fd45af..0e4179d5887 100644 --- a/spec/lib/gitlab/kubernetes/helm/api_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/api_spec.rb @@ -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 } diff --git a/spec/lib/gitlab/kubernetes/helm/base_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/base_command_spec.rb index 2a4a911cf38..f9bcb8abdb1 100644 --- a/spec/lib/gitlab/kubernetes/helm/base_command_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/base_command_spec.rb @@ -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 diff --git a/spec/lib/gitlab/kubernetes/helm/delete_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/delete_command_spec.rb index 95d60c18d56..2bf8b294821 100644 --- a/spec/lib/gitlab/kubernetes/helm/delete_command_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/delete_command_spec.rb @@ -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 diff --git a/spec/lib/gitlab/kubernetes/helm/init_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/init_command_spec.rb index 05d9b63d12b..61b8eb30b42 100644 --- a/spec/lib/gitlab/kubernetes/helm/init_command_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/init_command_spec.rb @@ -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 } diff --git a/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb index abd29e97505..6fc91300f5b 100644 --- a/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb @@ -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 diff --git a/spec/lib/gitlab/kubernetes/helm/patch_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/patch_command_spec.rb index eee842fa7d6..8d965a25f84 100644 --- a/spec/lib/gitlab/kubernetes/helm/patch_command_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/patch_command_spec.rb @@ -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 diff --git a/spec/lib/gitlab/kubernetes/helm/reset_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/reset_command_spec.rb index 981bb4e4abf..3773c428713 100644 --- a/spec/lib/gitlab/kubernetes/helm/reset_command_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/reset_command_spec.rb @@ -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' } diff --git a/spec/requests/api/graphql/mutations/alert_management/alerts/set_assignees_spec.rb b/spec/requests/api/graphql/mutations/alert_management/alerts/set_assignees_spec.rb new file mode 100644 index 00000000000..6663281e093 --- /dev/null +++ b/spec/requests/api/graphql/mutations/alert_management/alerts/set_assignees_spec.rb @@ -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 diff --git a/spec/services/alert_management/alerts/update_service_spec.rb b/spec/services/alert_management/alerts/update_service_spec.rb new file mode 100644 index 00000000000..055b2f6585a --- /dev/null +++ b/spec/services/alert_management/alerts/update_service_spec.rb @@ -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 diff --git a/spec/support/shared_examples/models/cluster_application_helm_cert_shared_examples.rb b/spec/support/shared_examples/models/cluster_application_helm_cert_shared_examples.rb index fa6b0c3afdd..239588d3b2f 100644 --- a/spec/support/shared_examples/models/cluster_application_helm_cert_shared_examples.rb +++ b/spec/support/shared_examples/models/cluster_application_helm_cert_shared_examples.rb @@ -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 diff --git a/spec/support/shared_examples/models/cluster_application_initial_status_shared_examples.rb b/spec/support/shared_examples/models/cluster_application_initial_status_shared_examples.rb index 0b21e9a3aa7..7f0c60d4204 100644 --- a/spec/support/shared_examples/models/cluster_application_initial_status_shared_examples.rb +++ b/spec/support/shared_examples/models/cluster_application_initial_status_shared_examples.rb @@ -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 diff --git a/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb b/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb index a259c47ff86..0efa5e56199 100644 --- a/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb +++ b/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb @@ -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