diff options
author | Chris Baumbauer <cab@cabnetworks.net> | 2018-11-03 10:13:35 -0700 |
---|---|---|
committer | Chris Baumbauer <cab@cabnetworks.net> | 2018-11-03 10:13:35 -0700 |
commit | aa44393e8f6e428bb7159ac8815a143a9e3e1047 (patch) | |
tree | 815e91b75359ad33173a667702cecb03cc963535 /app | |
parent | dc078c241765cfea5f49409407b82db7296c132d (diff) | |
parent | 4d3ff28a6a0d81f44ccb3eb1602996e5d7c3de1c (diff) | |
download | gitlab-ce-aa44393e8f6e428bb7159ac8815a143a9e3e1047.tar.gz |
Merge branch 'master' into triggermesh-phase1-knative
Diffstat (limited to 'app')
86 files changed, 974 insertions, 399 deletions
diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js index bb3b2865934..669630edcab 100644 --- a/app/assets/javascripts/boards/models/issue.js +++ b/app/assets/javascripts/boards/models/issue.js @@ -30,6 +30,7 @@ class ListIssue { this.toggleSubscriptionEndpoint = obj.toggle_subscription_endpoint; this.milestone_id = obj.milestone_id; this.project_id = obj.project_id; + this.assignableLabelsEndpoint = obj.assignable_labels_endpoint; if (obj.project) { this.project = new IssueProject(obj.project); diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue index a2aa3d197e3..82532539c9c 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_table.vue +++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue @@ -2,9 +2,15 @@ import PipelinesService from '../../pipelines/services/pipelines_service'; import PipelineStore from '../../pipelines/stores/pipelines_store'; import pipelinesMixin from '../../pipelines/mixins/pipelines'; +import TablePagination from '../../vue_shared/components/table_pagination.vue'; +import { getParameterByName } from '../../lib/utils/common_utils'; +import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin'; export default { - mixins: [pipelinesMixin], + components: { + TablePagination, + }, + mixins: [pipelinesMixin, CIPaginationMixin], props: { endpoint: { type: String, @@ -35,6 +41,8 @@ export default { return { store, state: store.state, + page: getParameterByName('page') || '1', + requestData: {}, }; }, @@ -48,11 +56,14 @@ export default { }, created() { this.service = new PipelinesService(this.endpoint); + this.requestData = { page: this.page }; }, methods: { successCallback(resp) { // depending of the endpoint the response can either bring a `pipelines` key or not. const pipelines = resp.data.pipelines || resp.data; + + this.store.storePagination(resp.headers); this.setCommonData(pipelines); const updatePipelinesEvent = new CustomEvent('update-pipelines-count', { @@ -97,5 +108,11 @@ export default { :view-type="viewType" /> </div> + + <table-pagination + v-if="shouldRenderPagination" + :change="onChangePage" + :page-info="state.pageInfo" + /> </div> </template> diff --git a/app/assets/javascripts/commons/gitlab_ui.js b/app/assets/javascripts/commons/gitlab_ui.js index e93e1f5ea2c..82a191d056b 100644 --- a/app/assets/javascripts/commons/gitlab_ui.js +++ b/app/assets/javascripts/commons/gitlab_ui.js @@ -1,7 +1,6 @@ import Vue from 'vue'; -import { GlProgressBar, GlLoadingIcon, GlTooltipDirective } from '@gitlab-org/gitlab-ui'; +import { GlLoadingIcon, GlTooltipDirective } from '@gitlab-org/gitlab-ui'; -Vue.component('gl-progress-bar', GlProgressBar); Vue.component('gl-loading-icon', GlLoadingIcon); Vue.directive('gl-tooltip', GlTooltipDirective); diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index 5457604b3b9..c0a76814102 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -59,7 +59,6 @@ export default class LabelsSelect { $toggleText = $dropdown.find('.dropdown-toggle-text'); namespacePath = $dropdown.data('namespacePath'); projectPath = $dropdown.data('projectPath'); - labelUrl = $dropdown.data('labels'); issueUpdateURL = $dropdown.data('issueUpdate'); selectedLabel = $dropdown.data('selected'); if (selectedLabel != null && !$dropdown.hasClass('js-multiselect')) { @@ -168,6 +167,7 @@ export default class LabelsSelect { $dropdown.glDropdown({ showMenuAbove: showMenuAbove, data: function(term, callback) { + labelUrl = $dropdown.attr('data-labels'); axios .get(labelUrl) .then(res => { diff --git a/app/assets/javascripts/pages/projects/clusters/gcp/new/index.js b/app/assets/javascripts/pages/projects/clusters/gcp/new/index.js deleted file mode 100644 index d4f34e32a48..00000000000 --- a/app/assets/javascripts/pages/projects/clusters/gcp/new/index.js +++ /dev/null @@ -1,5 +0,0 @@ -import initGkeDropdowns from '~/projects/gke_cluster_dropdowns'; - -document.addEventListener('DOMContentLoaded', () => { - initGkeDropdowns(); -}); diff --git a/app/assets/javascripts/pipelines/components/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines.vue index ea526cf1309..fcd8a54c9c1 100644 --- a/app/assets/javascripts/pipelines/components/pipelines.vue +++ b/app/assets/javascripts/pipelines/components/pipelines.vue @@ -155,14 +155,6 @@ export default { ); }, - shouldRenderPagination() { - return ( - !this.isLoading && - this.state.pipelines.length && - this.state.pageInfo.total > this.state.pageInfo.perPage - ); - }, - emptyTabMessage() { const { scopes } = this.$options; const possibleScopes = [scopes.pending, scopes.running, scopes.finished]; @@ -232,36 +224,6 @@ export default { this.setCommonData(resp.data.pipelines); } }, - /** - * Handles URL and query parameter changes. - * When the user uses the pagination or the tabs, - * - update URL - * - Make API request to the server with new parameters - * - Update the polling function - * - Update the internal state - */ - updateContent(parameters) { - this.updateInternalState(parameters); - - // fetch new data - return this.service - .getPipelines(this.requestData) - .then(response => { - this.isLoading = false; - this.successCallback(response); - - // restart polling - this.poll.restart({ data: this.requestData }); - }) - .catch(() => { - this.isLoading = false; - this.errorCallback(); - - // restart polling - this.poll.restart({ data: this.requestData }); - }); - }, - handleResetRunnersCache(endpoint) { this.isResetCacheButtonLoading = true; diff --git a/app/assets/javascripts/pipelines/mixins/pipelines.js b/app/assets/javascripts/pipelines/mixins/pipelines.js index 8929b397f6c..85781f548c6 100644 --- a/app/assets/javascripts/pipelines/mixins/pipelines.js +++ b/app/assets/javascripts/pipelines/mixins/pipelines.js @@ -23,6 +23,15 @@ export default { hasMadeRequest: false, }; }, + computed: { + shouldRenderPagination() { + return ( + !this.isLoading && + this.state.pipelines.length && + this.state.pageInfo.total > this.state.pageInfo.perPage + ); + }, + }, beforeMount() { this.poll = new Poll({ resource: this.service, @@ -65,6 +74,35 @@ export default { this.poll.stop(); }, methods: { + /** + * Handles URL and query parameter changes. + * When the user uses the pagination or the tabs, + * - update URL + * - Make API request to the server with new parameters + * - Update the polling function + * - Update the internal state + */ + updateContent(parameters) { + this.updateInternalState(parameters); + + // fetch new data + return this.service + .getPipelines(this.requestData) + .then(response => { + this.isLoading = false; + this.successCallback(response); + + // restart polling + this.poll.restart({ data: this.requestData }); + }) + .catch(() => { + this.isLoading = false; + this.errorCallback(); + + // restart polling + this.poll.restart({ data: this.requestData }); + }); + }, updateTable() { // Cancel ongoing request if (this.isMakingRequest) { diff --git a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue index e74912d628f..b145e5dc5e2 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue @@ -1,9 +1,13 @@ <script> import { parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility'; import tooltip from '../../../vue_shared/directives/tooltip'; +import { GlProgressBar } from '@gitlab-org/gitlab-ui'; export default { name: 'TimeTrackingComparisonPane', + components: { + GlProgressBar, + }, directives: { tooltip, }, diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue index 3ddb39730c4..27e3f314dd3 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/header.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue @@ -1,17 +1,17 @@ <script> import $ from 'jquery'; -import Tooltip from '../../directives/tooltip'; +import { GlTooltipDirective } from '@gitlab-org/gitlab-ui'; import ToolbarButton from './toolbar_button.vue'; import Icon from '../icon.vue'; export default { - directives: { - Tooltip, - }, components: { ToolbarButton, Icon, }, + directives: { + GlTooltip: GlTooltipDirective, + }, props: { previewMarkdown: { type: Boolean, @@ -147,7 +147,7 @@ export default { icon="table" /> <button - v-tooltip + v-gl-tooltip aria-label="Go full screen" class="toolbar-btn toolbar-fullscreen-btn js-zen-enter" data-container="body" diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue index 3e89e1c1e75..91d0bbfc21c 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue @@ -1,13 +1,13 @@ <script> -import tooltip from '../../directives/tooltip'; -import icon from '../icon.vue'; +import { GlTooltipDirective } from '@gitlab-org/gitlab-ui'; +import Icon from '../icon.vue'; export default { components: { - icon, + Icon, }, directives: { - tooltip, + GlTooltip: GlTooltipDirective, }, props: { buttonTitle: { @@ -43,7 +43,7 @@ export default { <template> <button - v-tooltip + v-gl-tooltip :data-md-tag="tag" :data-md-select="tagSelect" :data-md-block="tagBlock" diff --git a/app/assets/javascripts/vue_shared/mixins/ci_pagination_api_mixin.js b/app/assets/javascripts/vue_shared/mixins/ci_pagination_api_mixin.js index 67a1632269e..f9e3f3df0cc 100644 --- a/app/assets/javascripts/vue_shared/mixins/ci_pagination_api_mixin.js +++ b/app/assets/javascripts/vue_shared/mixins/ci_pagination_api_mixin.js @@ -14,7 +14,14 @@ export default { onChangePage(page) { /* URLS parameters are strings, we need to parse to match types */ - this.updateContent({ scope: this.scope, page: Number(page).toString() }); + const params = { + page: Number(page).toString(), + }; + + if (this.scope) { + params.scope = this.scope; + } + this.updateContent(params); }, updateInternalState(parameters) { diff --git a/app/controllers/clusters/applications_controller.rb b/app/controllers/clusters/applications_controller.rb new file mode 100644 index 00000000000..250f42f3096 --- /dev/null +++ b/app/controllers/clusters/applications_controller.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +class Clusters::ApplicationsController < Clusters::BaseController + before_action :cluster + before_action :authorize_create_cluster!, only: [:create] + + def create + Clusters::Applications::CreateService + .new(@cluster, current_user, create_cluster_application_params) + .execute(request) + + head :no_content + rescue Clusters::Applications::CreateService::InvalidApplicationError + render_404 + rescue StandardError + head :bad_request + end + + private + + def cluster + @cluster ||= clusterable.clusters.find(params[:id]) || render_404 + end + + def create_cluster_application_params + params.permit(:application, :hostname) + end +end diff --git a/app/controllers/clusters/base_controller.rb b/app/controllers/clusters/base_controller.rb new file mode 100644 index 00000000000..ef42f7c4074 --- /dev/null +++ b/app/controllers/clusters/base_controller.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +class Clusters::BaseController < ApplicationController + include RoutableActions + + skip_before_action :authenticate_user! + before_action :authorize_read_cluster! + + helper_method :clusterable + + private + + def cluster + @cluster ||= clusterable.clusters.find(params[:id]) + .present(current_user: current_user) + end + + def authorize_update_cluster! + access_denied! unless can?(current_user, :update_cluster, cluster) + end + + def authorize_admin_cluster! + access_denied! unless can?(current_user, :admin_cluster, cluster) + end + + def authorize_read_cluster! + access_denied! unless can?(current_user, :read_cluster, clusterable) + end + + def authorize_create_cluster! + access_denied! unless can?(current_user, :create_cluster, clusterable) + end + + def clusterable + raise NotImplementedError + end +end diff --git a/app/controllers/clusters/clusters_controller.rb b/app/controllers/clusters/clusters_controller.rb new file mode 100644 index 00000000000..f6f2060ebb5 --- /dev/null +++ b/app/controllers/clusters/clusters_controller.rb @@ -0,0 +1,218 @@ +# frozen_string_literal: true + +class Clusters::ClustersController < Clusters::BaseController + include RoutableActions + + before_action :cluster, except: [:index, :new, :create_gcp, :create_user] + before_action :generate_gcp_authorize_url, only: [:new] + before_action :validate_gcp_token, only: [:new] + before_action :gcp_cluster, only: [:new] + before_action :user_cluster, only: [:new] + before_action :authorize_create_cluster!, only: [:new] + before_action :authorize_update_cluster!, only: [:update] + before_action :authorize_admin_cluster!, only: [:destroy] + before_action :update_applications_status, only: [:cluster_status] + + helper_method :token_in_session + + STATUS_POLLING_INTERVAL = 10_000 + + def index + clusters = ClustersFinder.new(clusterable, current_user, :all).execute + @clusters = clusters.page(params[:page]).per(20) + end + + def new + end + + # Overridding ActionController::Metal#status is NOT a good idea + def cluster_status + respond_to do |format| + format.json do + Gitlab::PollingInterval.set_header(response, interval: STATUS_POLLING_INTERVAL) + + render json: ClusterSerializer + .new(current_user: @current_user) + .represent_status(@cluster) + end + end + end + + def show + end + + def update + Clusters::UpdateService + .new(current_user, update_params) + .execute(cluster) + + if cluster.valid? + respond_to do |format| + format.json do + head :no_content + end + format.html do + flash[:notice] = _('Kubernetes cluster was successfully updated.') + redirect_to cluster.show_path + end + end + else + respond_to do |format| + format.json { head :bad_request } + format.html { render :show } + end + end + end + + def destroy + if cluster.destroy + flash[:notice] = _('Kubernetes cluster integration was successfully removed.') + redirect_to clusterable.index_path, status: :found + else + flash[:notice] = _('Kubernetes cluster integration was not removed.') + render :show + end + end + + def create_gcp + @gcp_cluster = ::Clusters::CreateService + .new(current_user, create_gcp_cluster_params) + .execute(access_token: token_in_session) + .present(current_user: current_user) + + if @gcp_cluster.persisted? + redirect_to @gcp_cluster.show_path + else + generate_gcp_authorize_url + validate_gcp_token + user_cluster + + render :new, locals: { active_tab: 'gcp' } + end + end + + def create_user + @user_cluster = ::Clusters::CreateService + .new(current_user, create_user_cluster_params) + .execute(access_token: token_in_session) + .present(current_user: current_user) + + if @user_cluster.persisted? + redirect_to @user_cluster.show_path + else + generate_gcp_authorize_url + validate_gcp_token + gcp_cluster + + render :new, locals: { active_tab: 'user' } + end + end + + private + + def update_params + if cluster.managed? + params.require(:cluster).permit( + :enabled, + :environment_scope, + platform_kubernetes_attributes: [ + :namespace + ] + ) + else + params.require(:cluster).permit( + :enabled, + :name, + :environment_scope, + platform_kubernetes_attributes: [ + :api_url, + :token, + :ca_cert, + :namespace + ] + ) + end + end + + def create_gcp_cluster_params + params.require(:cluster).permit( + :enabled, + :name, + :environment_scope, + provider_gcp_attributes: [ + :gcp_project_id, + :zone, + :num_nodes, + :machine_type, + :legacy_abac + ]).merge( + provider_type: :gcp, + platform_type: :kubernetes, + clusterable: clusterable.subject + ) + end + + def create_user_cluster_params + params.require(:cluster).permit( + :enabled, + :name, + :environment_scope, + platform_kubernetes_attributes: [ + :namespace, + :api_url, + :token, + :ca_cert, + :authorization_type + ]).merge( + provider_type: :user, + platform_type: :kubernetes, + clusterable: clusterable.subject + ) + end + + def generate_gcp_authorize_url + state = generate_session_key_redirect(clusterable.new_path.to_s) + + @authorize_url = GoogleApi::CloudPlatform::Client.new( + nil, callback_google_api_auth_url, + state: state).authorize_url + rescue GoogleApi::Auth::ConfigMissingError + # no-op + end + + def gcp_cluster + @gcp_cluster = ::Clusters::Cluster.new.tap do |cluster| + cluster.build_provider_gcp + end + end + + def user_cluster + @user_cluster = ::Clusters::Cluster.new.tap do |cluster| + cluster.build_platform_kubernetes + end + end + + def validate_gcp_token + @valid_gcp_token = GoogleApi::CloudPlatform::Client.new(token_in_session, nil) + .validate_token(expires_at_in_session) + end + + def token_in_session + session[GoogleApi::CloudPlatform::Client.session_key_for_token] + end + + def expires_at_in_session + @expires_at_in_session ||= + session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at] + end + + def generate_session_key_redirect(uri) + GoogleApi::CloudPlatform::Client.new_session_key_for_redirect_uri do |key| + session[key] = uri + end + end + + def update_applications_status + @cluster.applications.each(&:schedule_status_update) + end +end diff --git a/app/controllers/concerns/project_unauthorized.rb b/app/controllers/concerns/project_unauthorized.rb new file mode 100644 index 00000000000..f59440dbc59 --- /dev/null +++ b/app/controllers/concerns/project_unauthorized.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module ProjectUnauthorized + extend ActiveSupport::Concern + + # EE would override this + def project_unauthorized_proc + # no-op + end +end diff --git a/app/controllers/concerns/routable_actions.rb b/app/controllers/concerns/routable_actions.rb index 88939b002b2..5624eb3aa45 100644 --- a/app/controllers/concerns/routable_actions.rb +++ b/app/controllers/concerns/routable_actions.rb @@ -3,23 +3,25 @@ module RoutableActions extend ActiveSupport::Concern - def find_routable!(routable_klass, requested_full_path, extra_authorization_proc: nil) + def find_routable!(routable_klass, requested_full_path, extra_authorization_proc: nil, not_found_or_authorized_proc: nil) routable = routable_klass.find_by_full_path(requested_full_path, follow_redirects: request.get?) if routable_authorized?(routable, extra_authorization_proc) ensure_canonical_path(routable, requested_full_path) routable else - handle_not_found_or_authorized(routable) + if not_found_or_authorized_proc + not_found_or_authorized_proc.call(routable) + end + + route_not_found unless performed? + nil end end - # This is overridden in gitlab-ee. - def handle_not_found_or_authorized(_routable) - route_not_found - end - def routable_authorized?(routable, extra_authorization_proc) + return false unless routable + action = :"read_#{routable.class.to_s.underscore}" return false unless can?(current_user, action, routable) diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb index a2bdcaefa9b..e0677ce3fbc 100644 --- a/app/controllers/projects/application_controller.rb +++ b/app/controllers/projects/application_controller.rb @@ -3,6 +3,7 @@ class Projects::ApplicationController < ApplicationController include CookiesHelper include RoutableActions + include ProjectUnauthorized include ChecksCollaboration skip_before_action :authenticate_user! @@ -21,7 +22,7 @@ class Projects::ApplicationController < ApplicationController path = File.join(params[:namespace_id], params[:project_id] || params[:id]) auth_proc = ->(project) { !project.pending_delete? } - @project = find_routable!(Project, path, extra_authorization_proc: auth_proc) + @project = find_routable!(Project, path, extra_authorization_proc: auth_proc, not_found_or_authorized_proc: project_unauthorized_proc) end def build_canonical_path(project) diff --git a/app/controllers/projects/clusters/applications_controller.rb b/app/controllers/projects/clusters/applications_controller.rb index bcea96bce94..c7b6218d007 100644 --- a/app/controllers/projects/clusters/applications_controller.rb +++ b/app/controllers/projects/clusters/applications_controller.rb @@ -1,29 +1,17 @@ # frozen_string_literal: true -class Projects::Clusters::ApplicationsController < Projects::ApplicationController - before_action :cluster - before_action :authorize_read_cluster! - before_action :authorize_create_cluster!, only: [:create] +class Projects::Clusters::ApplicationsController < Clusters::ApplicationsController + include ProjectUnauthorized - def create - Clusters::Applications::CreateService - .new(@cluster, current_user, create_cluster_application_params) - .execute(request) - - head :no_content - rescue Clusters::Applications::CreateService::InvalidApplicationError - render_404 - rescue StandardError - head :bad_request - end + prepend_before_action :project private - def cluster - @cluster ||= project.clusters.find(params[:id]) || render_404 + def clusterable + @clusterable ||= ClusterablePresenter.fabricate(project, current_user: current_user) end - def create_cluster_application_params - params.permit(:application, :hostname) + def project + @project ||= find_routable!(Project, File.join(params[:namespace_id], params[:project_id]), not_found_or_authorized_proc: project_unauthorized_proc) end end diff --git a/app/controllers/projects/clusters_controller.rb b/app/controllers/projects/clusters_controller.rb index 62adc66fb09..feda6deeaa6 100644 --- a/app/controllers/projects/clusters_controller.rb +++ b/app/controllers/projects/clusters_controller.rb @@ -1,224 +1,24 @@ # frozen_string_literal: true -class Projects::ClustersController < Projects::ApplicationController - before_action :cluster, except: [:index, :new, :create_gcp, :create_user] - before_action :authorize_read_cluster! - before_action :generate_gcp_authorize_url, only: [:new] - before_action :validate_gcp_token, only: [:new] - before_action :gcp_cluster, only: [:new] - before_action :user_cluster, only: [:new] - before_action :authorize_create_cluster!, only: [:new] - before_action :authorize_update_cluster!, only: [:update] - before_action :authorize_admin_cluster!, only: [:destroy] - before_action :update_applications_status, only: [:status] - helper_method :token_in_session +class Projects::ClustersController < Clusters::ClustersController + include ProjectUnauthorized - STATUS_POLLING_INTERVAL = 10_000 + prepend_before_action :project + before_action :repository - def index - clusters = ClustersFinder.new(project, current_user, :all).execute - @clusters = clusters.page(params[:page]).per(20) - end - - def new - end - - def status - respond_to do |format| - format.json do - Gitlab::PollingInterval.set_header(response, interval: STATUS_POLLING_INTERVAL) - - render json: ClusterSerializer - .new(project: @project, current_user: @current_user) - .represent_status(@cluster) - end - end - end - - def show - end - - def update - Clusters::UpdateService - .new(current_user, update_params) - .execute(cluster) - - if cluster.valid? - respond_to do |format| - format.json do - head :no_content - end - format.html do - flash[:notice] = _('Kubernetes cluster was successfully updated.') - redirect_to project_cluster_path(project, cluster) - end - end - else - respond_to do |format| - format.json { head :bad_request } - format.html { render :show } - end - end - end - - def destroy - if cluster.destroy - flash[:notice] = _('Kubernetes cluster integration was successfully removed.') - redirect_to project_clusters_path(project), status: :found - else - flash[:notice] = _('Kubernetes cluster integration was not removed.') - render :show - end - end - - def create_gcp - @gcp_cluster = ::Clusters::CreateService - .new(current_user, create_gcp_cluster_params) - .execute(project: project, access_token: token_in_session) - - if @gcp_cluster.persisted? - redirect_to project_cluster_path(project, @gcp_cluster) - else - generate_gcp_authorize_url - validate_gcp_token - user_cluster - - render :new, locals: { active_tab: 'gcp' } - end - end - - def create_user - @user_cluster = ::Clusters::CreateService - .new(current_user, create_user_cluster_params) - .execute(project: project, access_token: token_in_session) - - if @user_cluster.persisted? - redirect_to project_cluster_path(project, @user_cluster) - else - generate_gcp_authorize_url - validate_gcp_token - gcp_cluster - - render :new, locals: { active_tab: 'user' } - end - end + layout 'project' private - def cluster - @cluster ||= project.clusters.find(params[:id]) - .present(current_user: current_user) - end - - def update_params - if cluster.managed? - params.require(:cluster).permit( - :enabled, - :environment_scope, - platform_kubernetes_attributes: [ - :namespace - ] - ) - else - params.require(:cluster).permit( - :enabled, - :name, - :environment_scope, - platform_kubernetes_attributes: [ - :api_url, - :token, - :ca_cert, - :namespace - ] - ) - end - end - - def create_gcp_cluster_params - params.require(:cluster).permit( - :enabled, - :name, - :environment_scope, - provider_gcp_attributes: [ - :gcp_project_id, - :zone, - :num_nodes, - :machine_type, - :legacy_abac - ]).merge( - provider_type: :gcp, - platform_type: :kubernetes - ) - end - - def create_user_cluster_params - params.require(:cluster).permit( - :enabled, - :name, - :environment_scope, - platform_kubernetes_attributes: [ - :namespace, - :api_url, - :token, - :ca_cert, - :authorization_type - ]).merge( - provider_type: :user, - platform_type: :kubernetes - ) - end - - def generate_gcp_authorize_url - state = generate_session_key_redirect(new_project_cluster_path(@project).to_s) - - @authorize_url = GoogleApi::CloudPlatform::Client.new( - nil, callback_google_api_auth_url, - state: state).authorize_url - rescue GoogleApi::Auth::ConfigMissingError - # no-op - end - - def gcp_cluster - @gcp_cluster = ::Clusters::Cluster.new.tap do |cluster| - cluster.build_provider_gcp - end - end - - def user_cluster - @user_cluster = ::Clusters::Cluster.new.tap do |cluster| - cluster.build_platform_kubernetes - end - end - - def validate_gcp_token - @valid_gcp_token = GoogleApi::CloudPlatform::Client.new(token_in_session, nil) - .validate_token(expires_at_in_session) - end - - def token_in_session - session[GoogleApi::CloudPlatform::Client.session_key_for_token] - end - - def expires_at_in_session - @expires_at_in_session ||= - session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at] - end - - def generate_session_key_redirect(uri) - GoogleApi::CloudPlatform::Client.new_session_key_for_redirect_uri do |key| - session[key] = uri - end - end - - def authorize_update_cluster! - access_denied! unless can?(current_user, :update_cluster, cluster) + def clusterable + @clusterable ||= ClusterablePresenter.fabricate(project, current_user: current_user) end - def authorize_admin_cluster! - access_denied! unless can?(current_user, :admin_cluster, cluster) + def project + @project ||= find_routable!(Project, File.join(params[:namespace_id], params[:project_id]), not_found_or_authorized_proc: project_unauthorized_proc) end - def update_applications_status - @cluster.applications.each(&:schedule_status_update) + def repository + @repository ||= project.repository end end diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index 00b63f55710..32fc5140366 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -43,7 +43,7 @@ class Projects::CommitController < Projects::ApplicationController # rubocop: disable CodeReuse/ActiveRecord def pipelines @pipelines = @commit.pipelines.order(id: :desc) - @pipelines = @pipelines.where(ref: params[:ref]) if params[:ref] + @pipelines = @pipelines.where(ref: params[:ref]).page(params[:page]).per(30) if params[:ref] respond_to do |format| format.html @@ -53,6 +53,7 @@ class Projects::CommitController < Projects::ApplicationController render json: { pipelines: PipelineSerializer .new(project: @project, current_user: @current_user) + .with_pagination(request, response) .represent(@pipelines), count: { all: @pipelines.count diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 27b83da4f54..4bdb857b2d9 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -84,13 +84,14 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo end def pipelines - @pipelines = @merge_request.all_pipelines + @pipelines = @merge_request.all_pipelines.page(params[:page]).per(30) Gitlab::PollingInterval.set_header(response, interval: 10_000) render json: { pipelines: PipelineSerializer .new(project: @project, current_user: @current_user) + .with_pagination(request, response) .represent(@pipelines), count: { all: @pipelines.count diff --git a/app/finders/clusters_finder.rb b/app/finders/clusters_finder.rb index b40d6c41b71..0cce493b73e 100644 --- a/app/finders/clusters_finder.rb +++ b/app/finders/clusters_finder.rb @@ -1,20 +1,20 @@ # frozen_string_literal: true class ClustersFinder - def initialize(project, user, scope) - @project = project + def initialize(clusterable, user, scope) + @clusterable = clusterable @user = user @scope = scope || :active end def execute - clusters = project.clusters + clusters = clusterable.clusters filter_by_scope(clusters) end private - attr_reader :project, :user, :scope + attr_reader :clusterable, :user, :scope def filter_by_scope(clusters) case scope.to_sym diff --git a/app/helpers/clusters_helper.rb b/app/helpers/clusters_helper.rb index 19eb763e1de..916dcb1a308 100644 --- a/app/helpers/clusters_helper.rb +++ b/app/helpers/clusters_helper.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true module ClustersHelper - def has_multiple_clusters?(project) + # EE overrides this + def has_multiple_clusters? false end @@ -10,7 +11,7 @@ module ClustersHelper return unless show_gcp_signup_offer? content_tag :section, class: 'no-animate expanded' do - render 'projects/clusters/gcp_signup_offer_banner' + render 'clusters/clusters/gcp_signup_offer_banner' end end end diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb index 76ed8efe2c6..39f661b5f0c 100644 --- a/app/helpers/labels_helper.rb +++ b/app/helpers/labels_helper.rb @@ -143,7 +143,7 @@ module LabelsHelper def labels_filter_path(options = {}) project = @target_project || @project - format = options.delete(:format) || :html + format = options.delete(:format) if project project_labels_path(project, format, options) diff --git a/app/mailers/emails/issues.rb b/app/mailers/emails/issues.rb index 602e5afe26b..93b51fb1774 100644 --- a/app/mailers/emails/issues.rb +++ b/app/mailers/emails/issues.rb @@ -45,6 +45,20 @@ module Emails mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id, reason)) end + def removed_milestone_issue_email(recipient_id, issue_id, updated_by_user_id, reason = nil) + setup_issue_mail(issue_id, recipient_id) + + mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id, reason)) + end + + def changed_milestone_issue_email(recipient_id, issue_id, milestone, updated_by_user_id, reason = nil) + setup_issue_mail(issue_id, recipient_id) + + @milestone = milestone + @milestone_url = milestone_url(@milestone) + mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id, reason)) + end + def issue_status_changed_email(recipient_id, issue_id, status, updated_by_user_id, reason = nil) setup_issue_mail(issue_id, recipient_id) diff --git a/app/mailers/emails/merge_requests.rb b/app/mailers/emails/merge_requests.rb index be085496731..6524d0c2087 100644 --- a/app/mailers/emails/merge_requests.rb +++ b/app/mailers/emails/merge_requests.rb @@ -40,6 +40,20 @@ module Emails mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id, reason)) end + def removed_milestone_merge_request_email(recipient_id, merge_request_id, updated_by_user_id, reason = nil) + setup_merge_request_mail(merge_request_id, recipient_id) + + mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id, reason)) + end + + def changed_milestone_merge_request_email(recipient_id, merge_request_id, milestone, updated_by_user_id, reason = nil) + setup_merge_request_mail(merge_request_id, recipient_id) + + @milestone = milestone + @milestone_url = milestone_url(@milestone) + mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id, reason)) + end + def closed_merge_request_email(recipient_id, merge_request_id, updated_by_user_id, reason = nil) setup_merge_request_mail(merge_request_id, recipient_id) diff --git a/app/mailers/previews/notify_preview.rb b/app/mailers/previews/notify_preview.rb index 2f5b5483e9d..e7e8d96eca4 100644 --- a/app/mailers/previews/notify_preview.rb +++ b/app/mailers/previews/notify_preview.rb @@ -68,6 +68,14 @@ class NotifyPreview < ActionMailer::Preview Notify.issue_status_changed_email(user.id, issue.id, 'closed', user.id).message end + def removed_milestone_issue_email + Notify.removed_milestone_issue_email(user.id, issue.id, user.id) + end + + def changed_milestone_issue_email + Notify.changed_milestone_issue_email(user.id, issue.id, milestone, user.id) + end + def closed_merge_request_email Notify.closed_merge_request_email(user.id, issue.id, user.id).message end @@ -80,6 +88,14 @@ class NotifyPreview < ActionMailer::Preview Notify.merged_merge_request_email(user.id, merge_request.id, user.id).message end + def removed_milestone_merge_request_email + Notify.removed_milestone_merge_request_email(user.id, merge_request.id, user.id) + end + + def changed_milestone_merge_request_email + Notify.changed_milestone_merge_request_email(user.id, merge_request.id, milestone, user.id) + end + def member_access_denied_email Notify.member_access_denied_email('project', project.id, user.id).message end @@ -143,6 +159,10 @@ class NotifyPreview < ActionMailer::Preview @merge_request ||= project.merge_requests.first end + def milestone + @milestone ||= issue.milestone + end + def pipeline @pipeline = Ci::Pipeline.last end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index d73c02ba5d7..bb5d52fc78d 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -259,8 +259,7 @@ module Ci end def schedulable? - Feature.enabled?('ci_enable_scheduled_build', default_enabled: true) && - self.when == 'delayed' && options[:start_in].present? + self.when == 'delayed' && options[:start_in].present? end def options_scheduled_at diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index 1939df9f86e..7b219ec8267 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -20,6 +20,7 @@ module Clusters has_many :cluster_projects, class_name: 'Clusters::Project' has_many :projects, through: :cluster_projects, class_name: '::Project' + has_one :cluster_project, -> { order(id: :desc) }, class_name: 'Clusters::Project' has_many :cluster_groups, class_name: 'Clusters::Group' has_many :groups, through: :cluster_groups, class_name: '::Group' @@ -131,6 +132,13 @@ module Clusters platform_kubernetes.kubeclient if kubernetes? end + def find_or_initialize_kubernetes_namespace(cluster_project) + kubernetes_namespaces.find_or_initialize_by( + project: cluster_project.project, + cluster_project: cluster_project + ) + end + private def restrict_modification diff --git a/app/models/clusters/kubernetes_namespace.rb b/app/models/clusters/kubernetes_namespace.rb index fb5f6b65d9d..ac7f9193b87 100644 --- a/app/models/clusters/kubernetes_namespace.rb +++ b/app/models/clusters/kubernetes_namespace.rb @@ -2,6 +2,8 @@ module Clusters class KubernetesNamespace < ActiveRecord::Base + include Gitlab::Kubernetes + self.table_name = 'clusters_kubernetes_namespaces' belongs_to :cluster_project, class_name: 'Clusters::Project' @@ -12,7 +14,8 @@ module Clusters validates :namespace, presence: true validates :namespace, uniqueness: { scope: :cluster_id } - before_validation :set_namespace_and_service_account_to_default, on: :create + delegate :ca_pem, to: :platform_kubernetes, allow_nil: true + delegate :api_url, to: :platform_kubernetes, allow_nil: true attr_encrypted :service_account_token, mode: :per_attribute_iv, @@ -23,14 +26,26 @@ module Clusters "#{namespace}-token" end - private + def configure_predefined_credentials + self.namespace = kubernetes_or_project_namespace + self.service_account_name = default_service_account_name + end + + def predefined_variables + config = YAML.dump(kubeconfig) - def set_namespace_and_service_account_to_default - self.namespace ||= default_namespace - self.service_account_name ||= default_service_account_name + Gitlab::Ci::Variables::Collection.new.tap do |variables| + variables + .append(key: 'KUBE_SERVICE_ACCOUNT', value: service_account_name) + .append(key: 'KUBE_NAMESPACE', value: namespace) + .append(key: 'KUBE_TOKEN', value: service_account_token, public: false) + .append(key: 'KUBECONFIG', value: config, public: false, file: true) + end end - def default_namespace + private + + def kubernetes_or_project_namespace platform_kubernetes&.namespace.presence || project_namespace end @@ -45,5 +60,13 @@ module Clusters def project_slug "#{project.path}-#{project.id}".downcase end + + def kubeconfig + to_kubeconfig( + url: api_url, + namespace: namespace, + token: service_account_token, + ca_pem: ca_pem) + end end end diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb index d961130d251..d69038be532 100644 --- a/app/models/clusters/platforms/kubernetes.rb +++ b/app/models/clusters/platforms/kubernetes.rb @@ -6,6 +6,7 @@ module Clusters include Gitlab::Kubernetes include ReactiveCaching include EnumWithNil + include AfterCommitQueue RESERVED_NAMESPACES = %w(gitlab-managed-apps).freeze @@ -44,6 +45,7 @@ module Clusters validate :prevent_modification, on: :update after_save :clear_reactive_cache! + after_update :update_kubernetes_namespace alias_attribute :ca_pem, :ca_cert @@ -68,21 +70,31 @@ module Clusters end end - def predefined_variables - config = YAML.dump(kubeconfig) - + def predefined_variables(project:) Gitlab::Ci::Variables::Collection.new.tap do |variables| - variables - .append(key: 'KUBE_URL', value: api_url) - .append(key: 'KUBE_TOKEN', value: token, public: false) - .append(key: 'KUBE_NAMESPACE', value: actual_namespace) - .append(key: 'KUBECONFIG', value: config, public: false, file: true) + variables.append(key: 'KUBE_URL', value: api_url) if ca_pem.present? variables .append(key: 'KUBE_CA_PEM', value: ca_pem) .append(key: 'KUBE_CA_PEM_FILE', value: ca_pem, file: true) end + + if kubernetes_namespace = cluster.kubernetes_namespaces.find_by(project: project) + variables.concat(kubernetes_namespace.predefined_variables) + else + # From 11.5, every Clusters::Project should have at least one + # Clusters::KubernetesNamespace, so once migration has been completed, + # this 'else' branch will be removed. For more information, please see + # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22433 + config = YAML.dump(kubeconfig) + + variables + .append(key: 'KUBE_URL', value: api_url) + .append(key: 'KUBE_TOKEN', value: token, public: false) + .append(key: 'KUBE_NAMESPACE', value: actual_namespace) + .append(key: 'KUBECONFIG', value: config, public: false, file: true) + end end end @@ -205,6 +217,14 @@ module Clusters true end + + def update_kubernetes_namespace + return unless namespace_changed? + + run_after_commit do + ClusterPlatformConfigureWorker.perform_async(cluster_id) + end + end end end end diff --git a/app/models/concerns/token_authenticatable.rb b/app/models/concerns/token_authenticatable.rb index 66db4bd92de..23a43aec677 100644 --- a/app/models/concerns/token_authenticatable.rb +++ b/app/models/concerns/token_authenticatable.rb @@ -10,6 +10,7 @@ module TokenAuthenticatable def add_authentication_token_field(token_field, options = {}) @token_fields = [] unless @token_fields + unique = options.fetch(:unique, true) if @token_fields.include?(token_field) raise ArgumentError.new("#{token_field} already configured via add_authentication_token_field") @@ -25,8 +26,10 @@ module TokenAuthenticatable TokenAuthenticatableStrategies::Insecure.new(self, token_field, options) end - define_singleton_method("find_by_#{token_field}") do |token| - strategy.find_token_authenticatable(token) + if unique + define_singleton_method("find_by_#{token_field}") do |token| + strategy.find_token_authenticatable(token) + end end define_method(token_field) do diff --git a/app/models/concerns/token_authenticatable_strategies/base.rb b/app/models/concerns/token_authenticatable_strategies/base.rb index f0f7107d627..413721d3e6c 100644 --- a/app/models/concerns/token_authenticatable_strategies/base.rb +++ b/app/models/concerns/token_authenticatable_strategies/base.rb @@ -43,10 +43,14 @@ module TokenAuthenticatableStrategies set_token(instance, new_token) end + def unique + @options.fetch(:unique, true) + end + def generate_available_token loop do token = generate_token - break token unless find_token_authenticatable(token, true) + break token unless unique && find_token_authenticatable(token, true) end end diff --git a/app/models/issue.rb b/app/models/issue.rb index 4ace5d3ab97..0de5e434b02 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -240,7 +240,8 @@ class Issue < ActiveRecord::Base reference_path: issue_reference, real_path: url_helper.project_issue_path(project, self), issue_sidebar_endpoint: url_helper.project_issue_path(project, self, format: :json, serializer: 'sidebar'), - toggle_subscription_endpoint: url_helper.toggle_subscription_project_issue_path(project, self) + toggle_subscription_endpoint: url_helper.toggle_subscription_project_issue_path(project, self), + assignable_labels_endpoint: url_helper.project_labels_path(project, format: :json, include_ancestor_groups: true) ) end diff --git a/app/models/key.rb b/app/models/key.rb index bdb83e12793..8f93418b88b 100644 --- a/app/models/key.rb +++ b/app/models/key.rb @@ -34,6 +34,10 @@ class Key < ActiveRecord::Base after_destroy :post_destroy_hook after_destroy :refresh_user_cache + def self.regular_keys + where(type: ['Key', nil]) + end + def key=(value) write_attribute(:key, value.present? ? Gitlab::SSHPublicKey.sanitize(value) : nil) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 7eef08aa6a3..735d9fba966 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -353,6 +353,15 @@ class MergeRequest < ActiveRecord::Base end end + # Returns true if there are commits that match at least one commit SHA. + def includes_any_commits?(shas) + if persisted? + merge_request_diff.commits_by_shas(shas).exists? + else + (commit_shas & shas).present? + end + end + # Calls `MergeWorker` to proceed with the merge process and # updates `merge_jid` with the MergeWorker#jid. # This helps tracking enqueued and ongoing merge jobs. diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index 02c6b650f33..bb6ff8921df 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -140,6 +140,12 @@ class MergeRequestDiff < ActiveRecord::Base merge_request_diff_commits.map(&:sha) end + def commits_by_shas(shas) + return [] unless shas.present? + + merge_request_diff_commits.where(sha: shas) + end + def diff_refs=(new_diff_refs) self.base_commit_sha = new_diff_refs&.base_sha self.start_commit_sha = new_diff_refs&.start_sha diff --git a/app/models/project.rb b/app/models/project.rb index e2e309e8496..fa995b5b061 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1829,7 +1829,7 @@ class Project < ActiveRecord::Base end def deployment_variables(environment: nil) - deployment_platform(environment: environment)&.predefined_variables || [] + deployment_platform(environment: environment)&.predefined_variables(project: self) || [] end def auto_devops_variables diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb index 798944d0c06..3459ded7ccf 100644 --- a/app/models/project_services/kubernetes_service.rb +++ b/app/models/project_services/kubernetes_service.rb @@ -104,7 +104,12 @@ class KubernetesService < DeploymentService { success: false, result: err } end - def predefined_variables + # Project param was added on + # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22011, + # as a way to keep this service compatible with + # Clusters::Platforms::Kubernetes, it won't be used on this method + # as it's only needed for Clusters::Cluster. + def predefined_variables(project:) config = YAML.dump(kubeconfig) Gitlab::Ci::Variables::Collection.new.tap do |variables| diff --git a/app/models/user.rb b/app/models/user.rb index cc2cd1b7723..d3eb7162174 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -88,7 +88,7 @@ class User < ActiveRecord::Base has_one :namespace, -> { where(type: nil) }, dependent: :destroy, foreign_key: :owner_id, inverse_of: :owner, autosave: true # rubocop:disable Cop/ActiveRecordDependent # Profile - has_many :keys, -> { where(type: ['Key', nil]) }, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent + has_many :keys, -> { regular_keys }, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :deploy_keys, -> { where(type: 'DeployKey') }, dependent: :nullify # rubocop:disable Cop/ActiveRecordDependent has_many :gpg_keys @@ -941,12 +941,17 @@ class User < ActiveRecord::Base if !Gitlab.config.ldap.enabled false elsif ldap_user? - !last_credential_check_at || (last_credential_check_at + 1.hour) < Time.now + !last_credential_check_at || (last_credential_check_at + ldap_sync_time) < Time.now else false end end + def ldap_sync_time + # This number resides in this method so it can be redefined in EE. + 1.hour + end + def try_obtain_ldap_lease # After obtaining this lease LDAP checks will be blocked for 600 seconds # (10 minutes) for this user. diff --git a/app/presenters/clusterable_presenter.rb b/app/presenters/clusterable_presenter.rb new file mode 100644 index 00000000000..cff0e74d6ea --- /dev/null +++ b/app/presenters/clusterable_presenter.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +class ClusterablePresenter < Gitlab::View::Presenter::Delegated + presents :clusterable + + def self.fabricate(clusterable, **attributes) + presenter_class = "#{clusterable.class.name}ClusterablePresenter".constantize + attributes_with_presenter_class = attributes.merge(presenter_class: presenter_class) + + Gitlab::View::Presenter::Factory + .new(clusterable, attributes_with_presenter_class) + .fabricate! + end + + def can_create_cluster? + can?(current_user, :create_cluster, clusterable) + end + + def index_path + polymorphic_path([clusterable, :clusters]) + end + + def new_path + new_polymorphic_path([clusterable, :cluster]) + end + + def create_user_clusters_path + polymorphic_path([clusterable, :clusters], action: :create_user) + end + + def create_gcp_clusters_path + polymorphic_path([clusterable, :clusters], action: :create_gcp) + end + + def cluster_status_cluster_path(cluster, params = {}) + raise NotImplementedError + end + + def install_applications_cluster_path(cluster, application) + raise NotImplementedError + end + + def cluster_path(cluster, params = {}) + raise NotImplementedError + end +end diff --git a/app/presenters/clusters/cluster_presenter.rb b/app/presenters/clusters/cluster_presenter.rb index dfdd8e82f97..78d632eb77c 100644 --- a/app/presenters/clusters/cluster_presenter.rb +++ b/app/presenters/clusters/cluster_presenter.rb @@ -11,5 +11,13 @@ module Clusters def can_toggle_cluster? can?(current_user, :update_cluster, cluster) && created? end + + def show_path + if cluster.project_type? + project_cluster_path(project, cluster) + else + raise NotImplementedError + end + end end end diff --git a/app/presenters/project_clusterable_presenter.rb b/app/presenters/project_clusterable_presenter.rb new file mode 100644 index 00000000000..12077b2e735 --- /dev/null +++ b/app/presenters/project_clusterable_presenter.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class ProjectClusterablePresenter < ClusterablePresenter + def cluster_status_cluster_path(cluster, params = {}) + cluster_status_project_cluster_path(clusterable, cluster, params) + end + + def install_applications_cluster_path(cluster, application) + install_applications_project_cluster_path(clusterable, cluster, application) + end + + def cluster_path(cluster, params = {}) + project_cluster_path(clusterable, cluster, params) + end +end diff --git a/app/serializers/build_action_entity.rb b/app/serializers/build_action_entity.rb index 0db7875aa87..95833c3528f 100644 --- a/app/serializers/build_action_entity.rb +++ b/app/serializers/build_action_entity.rb @@ -12,7 +12,8 @@ class BuildActionEntity < Grape::Entity end expose :playable?, as: :playable - expose :scheduled_at, if: -> (build) { build.scheduled? } + expose :scheduled?, as: :scheduled + expose :scheduled_at, if: -> (*) { scheduled? } expose :unschedule_path, if: -> (build) { build.scheduled? } do |build| unschedule_project_job_path(build.project, build) @@ -25,4 +26,8 @@ class BuildActionEntity < Grape::Entity def playable? build.playable? && can?(request.current_user, :update_build, build) end + + def scheduled? + build.scheduled? + end end diff --git a/app/serializers/job_entity.rb b/app/serializers/job_entity.rb index a0a66511b7b..aebbc18e32f 100644 --- a/app/serializers/job_entity.rb +++ b/app/serializers/job_entity.rb @@ -33,6 +33,7 @@ class JobEntity < Grape::Entity end expose :playable?, as: :playable + expose :scheduled?, as: :scheduled expose :scheduled_at, if: -> (*) { scheduled? } expose :created_at expose :updated_at diff --git a/app/services/clusters/create_service.rb b/app/services/clusters/create_service.rb index cd843b8ffa8..270db4a52fd 100644 --- a/app/services/clusters/create_service.rb +++ b/app/services/clusters/create_service.rb @@ -8,10 +8,11 @@ module Clusters @current_user, @params = user, params.dup end - def execute(project:, access_token: nil) - raise ArgumentError, _('Instance does not support multiple Kubernetes clusters') unless can_create_cluster?(project) + def execute(access_token: nil) + raise ArgumentError, 'Unknown clusterable provided' unless clusterable + raise ArgumentError, _('Instance does not support multiple Kubernetes clusters') unless can_create_cluster? - cluster_params = params.merge(user: current_user, cluster_type: :project_type, projects: [project]) + cluster_params = params.merge(user: current_user).merge(clusterable_params) cluster_params[:provider_gcp_attributes].try do |provider| provider[:access_token] = access_token end @@ -27,9 +28,20 @@ module Clusters Clusters::Cluster.create(cluster_params) end + def clusterable + @clusterable ||= params.delete(:clusterable) + end + + def clusterable_params + case clusterable + when ::Project + { cluster_type: :project_type, projects: [clusterable] } + end + end + # EE would override this method - def can_create_cluster?(project) - project.clusters.empty? + def can_create_cluster? + clusterable.clusters.empty? end end end diff --git a/app/services/clusters/gcp/finalize_creation_service.rb b/app/services/clusters/gcp/finalize_creation_service.rb index 6ee63db8eb9..3df43657fa0 100644 --- a/app/services/clusters/gcp/finalize_creation_service.rb +++ b/app/services/clusters/gcp/finalize_creation_service.rb @@ -11,8 +11,9 @@ module Clusters configure_provider create_gitlab_service_account! configure_kubernetes - cluster.save! + configure_project_service_account + rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e provider.make_errored!("Failed to request to CloudPlatform; #{e.message}") rescue Kubeclient::HttpError => e @@ -24,7 +25,10 @@ module Clusters private def create_gitlab_service_account! - Clusters::Gcp::Kubernetes::CreateServiceAccountService.new(kube_client, rbac: create_rbac_cluster?).execute + Clusters::Gcp::Kubernetes::CreateServiceAccountService.gitlab_creator( + kube_client, + rbac: create_rbac_cluster? + ).execute end def configure_provider @@ -44,7 +48,20 @@ module Clusters end def request_kubernetes_token - Clusters::Gcp::Kubernetes::FetchKubernetesTokenService.new(kube_client).execute + Clusters::Gcp::Kubernetes::FetchKubernetesTokenService.new( + kube_client, + Clusters::Gcp::Kubernetes::GITLAB_ADMIN_TOKEN_NAME, + Clusters::Gcp::Kubernetes::GITLAB_SERVICE_ACCOUNT_NAMESPACE + ).execute + end + + def configure_project_service_account + kubernetes_namespace = cluster.find_or_initialize_kubernetes_namespace(cluster.cluster_project) + + Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService.new( + cluster: cluster, + kubernetes_namespace: kubernetes_namespace + ).execute end def authorization_type diff --git a/app/services/clusters/gcp/kubernetes.rb b/app/services/clusters/gcp/kubernetes.rb index d014d73b3e8..90ed529670c 100644 --- a/app/services/clusters/gcp/kubernetes.rb +++ b/app/services/clusters/gcp/kubernetes.rb @@ -3,11 +3,12 @@ module Clusters module Gcp module Kubernetes - SERVICE_ACCOUNT_NAME = 'gitlab' - SERVICE_ACCOUNT_NAMESPACE = 'default' - SERVICE_ACCOUNT_TOKEN_NAME = 'gitlab-token' - CLUSTER_ROLE_BINDING_NAME = 'gitlab-admin' - CLUSTER_ROLE_NAME = 'cluster-admin' + GITLAB_SERVICE_ACCOUNT_NAME = 'gitlab' + GITLAB_SERVICE_ACCOUNT_NAMESPACE = 'default' + GITLAB_ADMIN_TOKEN_NAME = 'gitlab-token' + GITLAB_CLUSTER_ROLE_BINDING_NAME = 'gitlab-admin' + GITLAB_CLUSTER_ROLE_NAME = 'cluster-admin' + PROJECT_CLUSTER_ROLE_NAME = 'edit' end end end diff --git a/app/services/clusters/gcp/kubernetes/create_or_update_namespace_service.rb b/app/services/clusters/gcp/kubernetes/create_or_update_namespace_service.rb new file mode 100644 index 00000000000..a888fab2789 --- /dev/null +++ b/app/services/clusters/gcp/kubernetes/create_or_update_namespace_service.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +module Clusters + module Gcp + module Kubernetes + class CreateOrUpdateNamespaceService + def initialize(cluster:, kubernetes_namespace:) + @cluster = cluster + @kubernetes_namespace = kubernetes_namespace + @platform = cluster.platform + end + + def execute + configure_kubernetes_namespace + create_project_service_account + configure_kubernetes_token + + kubernetes_namespace.save! + rescue ::Kubeclient::HttpError => err + raise err unless err.error_code = 404 + end + + private + + attr_reader :cluster, :kubernetes_namespace, :platform + + def configure_kubernetes_namespace + kubernetes_namespace.configure_predefined_credentials + end + + def create_project_service_account + Clusters::Gcp::Kubernetes::CreateServiceAccountService.namespace_creator( + platform.kubeclient, + service_account_name: kubernetes_namespace.service_account_name, + service_account_namespace: kubernetes_namespace.namespace, + rbac: platform.rbac? + ).execute + end + + def configure_kubernetes_token + kubernetes_namespace.service_account_token = fetch_service_account_token + end + + def fetch_service_account_token + Clusters::Gcp::Kubernetes::FetchKubernetesTokenService.new( + platform.kubeclient, + kubernetes_namespace.token_name, + kubernetes_namespace.namespace + ).execute + end + end + end + end +end diff --git a/app/services/clusters/gcp/kubernetes/create_service_account_service.rb b/app/services/clusters/gcp/kubernetes/create_service_account_service.rb index d17744591e6..dfc4bf7a358 100644 --- a/app/services/clusters/gcp/kubernetes/create_service_account_service.rb +++ b/app/services/clusters/gcp/kubernetes/create_service_account_service.rb @@ -4,46 +4,96 @@ module Clusters module Gcp module Kubernetes class CreateServiceAccountService - attr_reader :kubeclient, :rbac - - def initialize(kubeclient, rbac:) + def initialize(kubeclient, service_account_name:, service_account_namespace:, token_name:, rbac:, namespace_creator: false, role_binding_name: nil) @kubeclient = kubeclient + @service_account_name = service_account_name + @service_account_namespace = service_account_namespace + @token_name = token_name @rbac = rbac + @namespace_creator = namespace_creator + @role_binding_name = role_binding_name + end + + def self.gitlab_creator(kubeclient, rbac:) + self.new( + kubeclient, + service_account_name: Clusters::Gcp::Kubernetes::GITLAB_SERVICE_ACCOUNT_NAME, + service_account_namespace: Clusters::Gcp::Kubernetes::GITLAB_SERVICE_ACCOUNT_NAMESPACE, + token_name: Clusters::Gcp::Kubernetes::GITLAB_ADMIN_TOKEN_NAME, + rbac: rbac + ) + end + + def self.namespace_creator(kubeclient, service_account_name:, service_account_namespace:, rbac:) + self.new( + kubeclient, + service_account_name: service_account_name, + service_account_namespace: service_account_namespace, + token_name: "#{service_account_namespace}-token", + rbac: rbac, + namespace_creator: true, + role_binding_name: "gitlab-#{service_account_namespace}" + ) end def execute + ensure_project_namespace_exists if namespace_creator kubeclient.create_service_account(service_account_resource) kubeclient.create_secret(service_account_token_resource) - kubeclient.create_cluster_role_binding(cluster_role_binding_resource) if rbac + create_role_or_cluster_role_binding if rbac end private + attr_reader :kubeclient, :service_account_name, :service_account_namespace, :token_name, :rbac, :namespace_creator, :role_binding_name + + def ensure_project_namespace_exists + Gitlab::Kubernetes::Namespace.new( + service_account_namespace, + kubeclient + ).ensure_exists! + end + + def create_role_or_cluster_role_binding + if namespace_creator + kubeclient.create_role_binding(role_binding_resource) + else + kubeclient.create_cluster_role_binding(cluster_role_binding_resource) + end + end + def service_account_resource - Gitlab::Kubernetes::ServiceAccount.new(service_account_name, service_account_namespace).generate + Gitlab::Kubernetes::ServiceAccount.new( + service_account_name, + service_account_namespace + ).generate end def service_account_token_resource Gitlab::Kubernetes::ServiceAccountToken.new( - SERVICE_ACCOUNT_TOKEN_NAME, service_account_name, service_account_namespace).generate + token_name, + service_account_name, + service_account_namespace + ).generate end def cluster_role_binding_resource subjects = [{ kind: 'ServiceAccount', name: service_account_name, namespace: service_account_namespace }] Gitlab::Kubernetes::ClusterRoleBinding.new( - CLUSTER_ROLE_BINDING_NAME, - CLUSTER_ROLE_NAME, + Clusters::Gcp::Kubernetes::GITLAB_CLUSTER_ROLE_BINDING_NAME, + Clusters::Gcp::Kubernetes::GITLAB_CLUSTER_ROLE_NAME, subjects ).generate end - def service_account_name - SERVICE_ACCOUNT_NAME - end - - def service_account_namespace - SERVICE_ACCOUNT_NAMESPACE + def role_binding_resource + Gitlab::Kubernetes::RoleBinding.new( + name: role_binding_name, + role_name: Clusters::Gcp::Kubernetes::PROJECT_CLUSTER_ROLE_NAME, + namespace: service_account_namespace, + service_account_name: service_account_name + ).generate end end end diff --git a/app/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service.rb b/app/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service.rb index 9e09345c8dc..277cc4b788d 100644 --- a/app/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service.rb +++ b/app/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service.rb @@ -4,10 +4,12 @@ module Clusters module Gcp module Kubernetes class FetchKubernetesTokenService - attr_reader :kubeclient + attr_reader :kubeclient, :service_account_token_name, :namespace - def initialize(kubeclient) + def initialize(kubeclient, service_account_token_name, namespace) @kubeclient = kubeclient + @service_account_token_name = service_account_token_name + @namespace = namespace end def execute @@ -18,7 +20,7 @@ module Clusters private def get_secret - kubeclient.get_secret(SERVICE_ACCOUNT_TOKEN_NAME, SERVICE_ACCOUNT_NAMESPACE).as_json + kubeclient.get_secret(service_account_token_name, namespace).as_json rescue Kubeclient::HttpError => err raise err unless err.error_code == 404 diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index 3e8b9f84042..c388913ae65 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -3,6 +3,14 @@ class IssuableBaseService < BaseService private + attr_accessor :params, :skip_milestone_email + + def initialize(project, user = nil, params = {}) + super + + @skip_milestone_email = @params.delete(:skip_milestone_email) + end + def filter_params(issuable) ability_name = :"admin_#{issuable.to_ability_name}" diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb index b54b0bf6ef6..fba252b0bae 100644 --- a/app/services/issues/update_service.rb +++ b/app/services/issues/update_service.rb @@ -48,6 +48,8 @@ module Issues notification_service.async.relabeled_issue(issue, added_labels, current_user) end + handle_milestone_change(issue) + added_mentions = issue.mentioned_users - old_mentioned_users if added_mentions.present? @@ -91,6 +93,18 @@ module Issues private + def handle_milestone_change(issue) + return if skip_milestone_email + + return unless issue.previous_changes.include?('milestone_id') + + if issue.milestone.nil? + notification_service.async.removed_milestone_issue(issue, current_user) + else + notification_service.async.changed_milestone_issue(issue, issue.milestone, current_user) + end + end + # rubocop: disable CodeReuse/ActiveRecord def get_issue_if_allowed(id, board_group_id = nil) return unless id diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index f01872b205e..53768ff2cbe 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -87,11 +87,8 @@ module MergeRequests filter_merge_requests(merge_requests).each do |merge_request| if branch_and_project_match?(merge_request) || @push.force_push? merge_request.reload_diff(current_user) - else - mr_commit_ids = merge_request.commit_shas - push_commit_ids = @commits.map(&:id) - matches = mr_commit_ids & push_commit_ids - merge_request.reload_diff(current_user) if matches.any? + elsif merge_request.includes_any_commits?(push_commit_ids) + merge_request.reload_diff(current_user) end merge_request.mark_as_unchecked @@ -104,6 +101,10 @@ module MergeRequests end # rubocop: enable CodeReuse/ActiveRecord + def push_commit_ids + @push_commit_ids ||= @commits.map(&:id) + end + def branch_and_project_match?(merge_request) merge_request.source_project == @project && merge_request.source_branch == @push.branch_name diff --git a/app/services/merge_requests/reload_diffs_service.rb b/app/services/merge_requests/reload_diffs_service.rb index b4d48fe92ad..b47d8f3f63a 100644 --- a/app/services/merge_requests/reload_diffs_service.rb +++ b/app/services/merge_requests/reload_diffs_service.rb @@ -36,7 +36,10 @@ module MergeRequests # Remove cache for all diffs on this MR. Do not use the association on the # model, as that will interfere with other actions happening when # reloading the diff. - MergeRequestDiff.where(merge_request: merge_request).each do |merge_request_diff| + MergeRequestDiff + .where(merge_request: merge_request) + .preload(merge_request: :target_project) + .find_each do |merge_request_diff| next if merge_request_diff == new_diff cacheable_collection(merge_request_diff).clear_cache diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb index b112edbce7f..aacaf10d09c 100644 --- a/app/services/merge_requests/update_service.rb +++ b/app/services/merge_requests/update_service.rb @@ -58,6 +58,8 @@ module MergeRequests merge_request.mark_as_unchecked end + handle_milestone_change(merge_request) + added_labels = merge_request.labels - old_labels if added_labels.present? notification_service.async.relabeled_merge_request( @@ -105,6 +107,18 @@ module MergeRequests private + def handle_milestone_change(merge_request) + return if skip_milestone_email + + return unless merge_request.previous_changes.include?('milestone_id') + + if merge_request.milestone.nil? + notification_service.async.removed_milestone_merge_request(merge_request, current_user) + else + notification_service.async.changed_milestone_merge_request(merge_request, merge_request.milestone, current_user) + end + end + def create_branch_change_note(issuable, branch_type, old_branch, new_branch) SystemNoteService.change_branch( issuable, issuable.project, current_user, branch_type, diff --git a/app/services/milestones/destroy_service.rb b/app/services/milestones/destroy_service.rb index 7cda802c120..87c7a282081 100644 --- a/app/services/milestones/destroy_service.rb +++ b/app/services/milestones/destroy_service.rb @@ -4,7 +4,7 @@ module Milestones class DestroyService < Milestones::BaseService def execute(milestone) Milestone.transaction do - update_params = { milestone: nil } + update_params = { milestone: nil, skip_milestone_email: true } milestone.issues.each do |issue| Issues::UpdateService.new(parent, current_user, update_params).execute(issue) diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 50fa373025b..fb9c18ea75d 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -129,6 +129,14 @@ class NotificationService relabeled_resource_email(issue, added_labels, current_user, :relabeled_issue_email) end + def removed_milestone_issue(issue, current_user) + removed_milestone_resource_email(issue, current_user, :removed_milestone_issue_email) + end + + def changed_milestone_issue(issue, new_milestone, current_user) + changed_milestone_resource_email(issue, new_milestone, current_user, :changed_milestone_issue_email) + end + # When create a merge request we should send an email to: # # * mr author @@ -138,7 +146,6 @@ class NotificationService # * users with custom level checked with "new merge request" # # In EE, approvers of the merge request are also included - # def new_merge_request(merge_request, current_user) new_resource_email(merge_request, :new_merge_request_email) end @@ -208,6 +215,14 @@ class NotificationService relabeled_resource_email(merge_request, added_labels, current_user, :relabeled_merge_request_email) end + def removed_milestone_merge_request(merge_request, current_user) + removed_milestone_resource_email(merge_request, current_user, :removed_milestone_merge_request_email) + end + + def changed_milestone_merge_request(merge_request, new_milestone, current_user) + changed_milestone_resource_email(merge_request, new_milestone, current_user, :changed_milestone_merge_request_email) + end + def close_mr(merge_request, current_user) close_resource_email(merge_request, current_user, :closed_merge_request_email) end @@ -500,6 +515,30 @@ class NotificationService end end + def removed_milestone_resource_email(target, current_user, method) + recipients = NotificationRecipientService.build_recipients( + target, + current_user, + action: 'removed_milestone' + ) + + recipients.each do |recipient| + mailer.send(method, recipient.user.id, target.id, current_user.id).deliver_later + end + end + + def changed_milestone_resource_email(target, milestone, current_user, method) + recipients = NotificationRecipientService.build_recipients( + target, + current_user, + action: 'changed_milestone' + ) + + recipients.each do |recipient| + mailer.send(method, recipient.user.id, target.id, milestone, current_user.id).deliver_later + end + end + def reopen_resource_email(target, current_user, method, status) recipients = NotificationRecipientService.build_recipients(target, current_user, action: "reopen") diff --git a/app/views/projects/clusters/_advanced_settings.html.haml b/app/views/clusters/clusters/_advanced_settings.html.haml index 243e8cd9ba0..7037c80aa6b 100644 --- a/app/views/projects/clusters/_advanced_settings.html.haml +++ b/app/views/clusters/clusters/_advanced_settings.html.haml @@ -12,4 +12,4 @@ = s_('ClusterIntegration|Remove Kubernetes cluster integration') %p = s_("ClusterIntegration|Remove this Kubernetes cluster's configuration from this project. This will not delete your actual Kubernetes cluster.") - = link_to(s_('ClusterIntegration|Remove integration'), namespace_project_cluster_path(@project.namespace, @project, @cluster.id), method: :delete, class: 'btn btn-danger', data: { confirm: s_("ClusterIntegration|Are you sure you want to remove this Kubernetes cluster's integration? This will not delete your actual Kubernetes cluster.")}) + = link_to(s_('ClusterIntegration|Remove integration'), clusterable.cluster_path(@cluster), method: :delete, class: 'btn btn-danger', data: { confirm: s_("ClusterIntegration|Are you sure you want to remove this Kubernetes cluster's integration? This will not delete your actual Kubernetes cluster.")}) diff --git a/app/views/projects/clusters/_banner.html.haml b/app/views/clusters/clusters/_banner.html.haml index 73cfea0ef92..73cfea0ef92 100644 --- a/app/views/projects/clusters/_banner.html.haml +++ b/app/views/clusters/clusters/_banner.html.haml diff --git a/app/views/projects/clusters/_cluster.html.haml b/app/views/clusters/clusters/_cluster.html.haml index 2d7f7c6b1fb..facbcb7fc59 100644 --- a/app/views/projects/clusters/_cluster.html.haml +++ b/app/views/clusters/clusters/_cluster.html.haml @@ -2,7 +2,7 @@ .table-section.section-30 .table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Kubernetes cluster") .table-mobile-content - = link_to cluster.name, namespace_project_cluster_path(@project.namespace, @project, cluster) + = link_to cluster.name, cluster.show_path .table-section.section-30 .table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Environment scope") .table-mobile-content= cluster.environment_scope @@ -16,7 +16,7 @@ class: "#{'is-checked' if cluster.enabled?} #{'is-disabled' if !cluster.can_toggle_cluster?}", "aria-label": s_("ClusterIntegration|Toggle Kubernetes Cluster"), disabled: !cluster.can_toggle_cluster?, - data: { endpoint: namespace_project_cluster_path(@project.namespace, @project, cluster, format: :json) } } + data: { endpoint: clusterable.cluster_path(cluster, format: :json) } } %input.js-project-feature-toggle-input{ type: "hidden", value: cluster.enabled? } = icon("spinner spin", class: "loading-icon") %span.toggle-icon diff --git a/app/views/projects/clusters/_empty_state.html.haml b/app/views/clusters/clusters/_empty_state.html.haml index b8a3556a206..800e76d92ef 100644 --- a/app/views/projects/clusters/_empty_state.html.haml +++ b/app/views/clusters/clusters/_empty_state.html.haml @@ -7,6 +7,6 @@ - link_to_help_page = link_to(_('Learn more about Kubernetes'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer') %p= s_('ClusterIntegration|Kubernetes clusters allow you to use review apps, deploy your applications, run your pipelines, and much more in an easy way. %{link_to_help_page}').html_safe % { link_to_help_page: link_to_help_page} - - if can?(current_user, :create_cluster, @project) + - if clusterable.can_create_cluster? .text-center - = link_to s_('ClusterIntegration|Add Kubernetes cluster'), new_project_cluster_path(@project), class: 'btn btn-success' + = link_to s_('ClusterIntegration|Add Kubernetes cluster'), clusterable.new_path, class: 'btn btn-success' diff --git a/app/views/projects/clusters/_gcp_signup_offer_banner.html.haml b/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml index 73b11d509d3..73b11d509d3 100644 --- a/app/views/projects/clusters/_gcp_signup_offer_banner.html.haml +++ b/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml diff --git a/app/views/projects/clusters/_integration_form.html.haml b/app/views/clusters/clusters/_integration_form.html.haml index d0a553e3414..5e451f60c9d 100644 --- a/app/views/projects/clusters/_integration_form.html.haml +++ b/app/views/clusters/clusters/_integration_form.html.haml @@ -1,4 +1,4 @@ -= form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field| += form_for @cluster, url: clusterable.cluster_path(@cluster), as: :cluster do |field| = form_errors(@cluster) .form-group %h5= s_('ClusterIntegration|Integration status') @@ -13,7 +13,7 @@ = sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked') .form-text.text-muted= s_('ClusterIntegration|Enable or disable GitLab\'s connection to your Kubernetes cluster.') - - if has_multiple_clusters?(@project) + - if has_multiple_clusters? .form-group %h5= s_('ClusterIntegration|Environment scope') = field.text_field :environment_scope, class: 'col-md-6 form-control js-select-on-focus', placeholder: s_('ClusterIntegration|Environment scope') @@ -23,7 +23,7 @@ .form-group = field.submit _('Save changes'), class: 'btn btn-success' - - unless has_multiple_clusters?(@project) + - unless has_multiple_clusters? %h5= s_('ClusterIntegration|Environment scope') %p %code * diff --git a/app/views/projects/clusters/_sidebar.html.haml b/app/views/clusters/clusters/_sidebar.html.haml index 3d10348212f..3d10348212f 100644 --- a/app/views/projects/clusters/_sidebar.html.haml +++ b/app/views/clusters/clusters/_sidebar.html.haml diff --git a/app/views/projects/clusters/gcp/_form.html.haml b/app/views/clusters/clusters/gcp/_form.html.haml index 171ceeceb68..ad842036a62 100644 --- a/app/views/projects/clusters/gcp/_form.html.haml +++ b/app/views/clusters/clusters/gcp/_form.html.haml @@ -12,14 +12,14 @@ %p= link_to('Select a different Google account', @authorize_url) -= form_for @gcp_cluster, html: { class: 'js-gke-cluster-creation prepend-top-20', data: { token: token_in_session } }, url: create_gcp_namespace_project_clusters_path(@project.namespace, @project), as: :cluster do |field| += form_for @gcp_cluster, html: { class: 'js-gke-cluster-creation prepend-top-20', data: { token: token_in_session } }, url: clusterable.create_gcp_clusters_path, as: :cluster do |field| = form_errors(@gcp_cluster) .form-group = field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-bold' = field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name') .form-group = field.label :environment_scope, s_('ClusterIntegration|Environment scope'), class: 'label-bold' - = field.text_field :environment_scope, class: 'form-control', readonly: !has_multiple_clusters?(@project), placeholder: s_('ClusterIntegration|Environment scope') + = field.text_field :environment_scope, class: 'form-control', readonly: !has_multiple_clusters?, placeholder: s_('ClusterIntegration|Environment scope') = field.fields_for :provider_gcp, @gcp_cluster.provider_gcp do |provider_gcp_field| .form-group diff --git a/app/views/projects/clusters/gcp/_header.html.haml b/app/views/clusters/clusters/gcp/_header.html.haml index a2ad3cd64df..a2ad3cd64df 100644 --- a/app/views/projects/clusters/gcp/_header.html.haml +++ b/app/views/clusters/clusters/gcp/_header.html.haml diff --git a/app/views/projects/clusters/gcp/_show.html.haml b/app/views/clusters/clusters/gcp/_show.html.haml index 779c9c245c1..6021b220285 100644 --- a/app/views/projects/clusters/gcp/_show.html.haml +++ b/app/views/clusters/clusters/gcp/_show.html.haml @@ -6,7 +6,7 @@ %span.input-group-append = clipboard_button(text: @cluster.name, title: s_('ClusterIntegration|Copy Kubernetes cluster name'), class: 'input-group-text btn-default') -= form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field| += form_for @cluster, url: clusterable.cluster_path(@cluster), as: :cluster do |field| = form_errors(@cluster) = field.fields_for :platform_kubernetes, @cluster.platform_kubernetes do |platform_kubernetes_field| diff --git a/app/views/projects/clusters/index.html.haml b/app/views/clusters/clusters/index.html.haml index a55de84b5cd..a55de84b5cd 100644 --- a/app/views/projects/clusters/index.html.haml +++ b/app/views/clusters/clusters/index.html.haml diff --git a/app/views/projects/clusters/new.html.haml b/app/views/clusters/clusters/new.html.haml index a38003f5750..eeeef6bd824 100644 --- a/app/views/projects/clusters/new.html.haml +++ b/app/views/clusters/clusters/new.html.haml @@ -19,9 +19,9 @@ .tab-content.gitlab-tab-content .tab-pane{ id: 'create-gcp-cluster-pane', class: active_when(active_tab == 'gcp'), role: 'tabpanel' } - = render 'projects/clusters/gcp/header' + = render 'clusters/clusters/gcp/header' - if @valid_gcp_token - = render 'projects/clusters/gcp/form' + = render 'clusters/clusters/gcp/form' - elsif @authorize_url .signin-with-google = link_to(image_tag('auth_buttons/signin_with_google.png', width: '191px'), @authorize_url) @@ -32,5 +32,5 @@ = s_('Google authentication is not %{link_to_documentation}. Ask your GitLab administrator if you want to use this service.').html_safe % { link_to_documentation: link } .tab-pane{ id: 'add-user-cluster-pane', class: active_when(active_tab == 'user'), role: 'tabpanel' } - = render 'projects/clusters/user/header' - = render 'projects/clusters/user/form' + = render 'clusters/clusters/user/header' + = render 'clusters/clusters/user/form' diff --git a/app/views/projects/clusters/show.html.haml b/app/views/clusters/clusters/show.html.haml index c79d3f67706..7ea85fe43d6 100644 --- a/app/views/projects/clusters/show.html.haml +++ b/app/views/clusters/clusters/show.html.haml @@ -1,25 +1,26 @@ - @content_class = "limit-container-width" unless fluid_layout -- add_to_breadcrumbs "Kubernetes Clusters", project_clusters_path(@project) +- add_to_breadcrumbs "Kubernetes Clusters", clusterable.index_path - breadcrumb_title @cluster.name - page_title _("Kubernetes Cluster") +- manage_prometheus_path = edit_project_service_path(@cluster.project, 'prometheus') if @project - expanded = Rails.env.test? -- status_path = status_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster.id, format: :json) if can?(current_user, :admin_cluster, @cluster) +- status_path = clusterable.cluster_status_cluster_path(@cluster.id, format: :json) if can?(current_user, :admin_cluster, @cluster) .edit-cluster-form.js-edit-cluster-form{ data: { status_path: status_path, - install_helm_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :helm), - install_ingress_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :ingress), - install_prometheus_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :prometheus), - install_runner_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :runner), - install_jupyter_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :jupyter), - install_knative_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :knative), + install_helm_path: clusterable.install_applications_cluster_path(@cluster, :helm), + install_ingress_path: clusterable.install_applications_cluster_path(@cluster, :ingress), + install_prometheus_path: clusterable.install_applications_cluster_path(@cluster, :prometheus), + install_runner_path: clusterable.install_applications_cluster_path(@cluster, :runner), + install_jupyter_path: clusterable.install_applications_cluster_path(@cluster, :jupyter), + install_knative_path: clusterable.install_applications_cluster_path(@cluster, :knative), toggle_status: @cluster.enabled? ? 'true': 'false', cluster_status: @cluster.status_name, cluster_status_reason: @cluster.status_reason, help_path: help_page_path('user/project/clusters/index.md', anchor: 'installing-applications'), ingress_help_path: help_page_path('user/project/clusters/index.md', anchor: 'getting-the-external-ip-address'), ingress_dns_help_path: help_page_path('topics/autodevops/quick_start_guide.md', anchor: 'point-dns-at-cluster-ip'), - manage_prometheus_path: edit_project_service_path(@cluster.project, 'prometheus') } } + manage_prometheus_path: manage_prometheus_path } } .js-cluster-application-notice .flash-container @@ -39,9 +40,9 @@ %p= s_('ClusterIntegration|See and edit the details for your Kubernetes cluster') .settings-content - if @cluster.managed? - = render 'projects/clusters/gcp/show' + = render 'clusters/clusters/gcp/show' - else - = render 'projects/clusters/user/show' + = render 'clusters/clusters/user/show' %section.settings.no-animate#js-cluster-advanced-settings{ class: ('expanded' if expanded) } .settings-header diff --git a/app/views/projects/clusters/user/_form.html.haml b/app/views/clusters/clusters/user/_form.html.haml index 54a6e685bb0..4e6232b69de 100644 --- a/app/views/projects/clusters/user/_form.html.haml +++ b/app/views/clusters/clusters/user/_form.html.haml @@ -1,9 +1,9 @@ -= form_for @user_cluster, url: create_user_namespace_project_clusters_path(@project.namespace, @project), as: :cluster do |field| += form_for @user_cluster, url: clusterable.create_user_clusters_path, as: :cluster do |field| = form_errors(@user_cluster) .form-group = field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-bold' = field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name') - - if has_multiple_clusters?(@project) + - if has_multiple_clusters? .form-group = field.label :environment_scope, s_('ClusterIntegration|Environment scope'), class: 'label-bold' = field.text_field :environment_scope, class: 'form-control', placeholder: s_('ClusterIntegration|Environment scope') diff --git a/app/views/projects/clusters/user/_header.html.haml b/app/views/clusters/clusters/user/_header.html.haml index 749177fa6c1..749177fa6c1 100644 --- a/app/views/projects/clusters/user/_header.html.haml +++ b/app/views/clusters/clusters/user/_header.html.haml diff --git a/app/views/projects/clusters/user/_show.html.haml b/app/views/clusters/clusters/user/_show.html.haml index 5b57f7ceb7d..a871fef0240 100644 --- a/app/views/projects/clusters/user/_show.html.haml +++ b/app/views/clusters/clusters/user/_show.html.haml @@ -1,4 +1,4 @@ -= form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field| += form_for @cluster, url: clusterable.cluster_path(@cluster), as: :cluster do |field| = form_errors(@cluster) .form-group = field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-bold' diff --git a/app/views/notify/changed_milestone_issue_email.html.haml b/app/views/notify/changed_milestone_issue_email.html.haml new file mode 100644 index 00000000000..7d5425fc72d --- /dev/null +++ b/app/views/notify/changed_milestone_issue_email.html.haml @@ -0,0 +1,3 @@ +%p + Milestone changed to + %strong= link_to(@milestone.name, @milestone_url) diff --git a/app/views/notify/changed_milestone_issue_email.text.erb b/app/views/notify/changed_milestone_issue_email.text.erb new file mode 100644 index 00000000000..c5fc0b61518 --- /dev/null +++ b/app/views/notify/changed_milestone_issue_email.text.erb @@ -0,0 +1 @@ +Milestone changed to <%= @milestone.name %> ( <%= @milestone_url %> ) diff --git a/app/views/notify/changed_milestone_merge_request_email.html.haml b/app/views/notify/changed_milestone_merge_request_email.html.haml new file mode 100644 index 00000000000..7d5425fc72d --- /dev/null +++ b/app/views/notify/changed_milestone_merge_request_email.html.haml @@ -0,0 +1,3 @@ +%p + Milestone changed to + %strong= link_to(@milestone.name, @milestone_url) diff --git a/app/views/notify/changed_milestone_merge_request_email.text.erb b/app/views/notify/changed_milestone_merge_request_email.text.erb new file mode 100644 index 00000000000..c5fc0b61518 --- /dev/null +++ b/app/views/notify/changed_milestone_merge_request_email.text.erb @@ -0,0 +1 @@ +Milestone changed to <%= @milestone.name %> ( <%= @milestone_url %> ) diff --git a/app/views/notify/removed_milestone_issue_email.html.haml b/app/views/notify/removed_milestone_issue_email.html.haml new file mode 100644 index 00000000000..7e9205b6491 --- /dev/null +++ b/app/views/notify/removed_milestone_issue_email.html.haml @@ -0,0 +1,2 @@ +%p + Milestone removed diff --git a/app/views/notify/removed_milestone_issue_email.text.erb b/app/views/notify/removed_milestone_issue_email.text.erb new file mode 100644 index 00000000000..0b83ed7a4c5 --- /dev/null +++ b/app/views/notify/removed_milestone_issue_email.text.erb @@ -0,0 +1 @@ +Milestone removed diff --git a/app/views/notify/removed_milestone_merge_request_email.html.haml b/app/views/notify/removed_milestone_merge_request_email.html.haml new file mode 100644 index 00000000000..7e9205b6491 --- /dev/null +++ b/app/views/notify/removed_milestone_merge_request_email.html.haml @@ -0,0 +1,2 @@ +%p + Milestone removed diff --git a/app/views/notify/removed_milestone_merge_request_email.text.erb b/app/views/notify/removed_milestone_merge_request_email.text.erb new file mode 100644 index 00000000000..0b83ed7a4c5 --- /dev/null +++ b/app/views/notify/removed_milestone_merge_request_email.text.erb @@ -0,0 +1 @@ +Milestone removed diff --git a/app/views/shared/boards/components/sidebar/_labels.html.haml b/app/views/shared/boards/components/sidebar/_labels.html.haml index 6138914206b..19159684420 100644 --- a/app/views/shared/boards/components/sidebar/_labels.html.haml +++ b/app/views/shared/boards/components/sidebar/_labels.html.haml @@ -19,13 +19,13 @@ ":value" => "label.id" } .dropdown %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-issue-board-sidebar{ type: "button", - "v-bind:data-selected" => "selectedLabels", + ":data-selected" => "selectedLabels", + ":data-labels" => "issue.assignableLabelsEndpoint", data: { toggle: "dropdown", field_name: "issue[label_names][]", show_no: "true", show_any: "true", project_id: @project&.try(:id), - labels: labels_filter_path_with_defaults, namespace_path: @namespace_path, project_path: @project.try(:path) } } %span.dropdown-toggle-text diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index f21789de37d..a66a6f4c777 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -28,6 +28,7 @@ - gcp_cluster:cluster_wait_for_app_installation - gcp_cluster:wait_for_cluster_creation - gcp_cluster:cluster_wait_for_ingress_ip_address +- gcp_cluster:cluster_platform_configure - github_import_advance_stage - github_importer:github_import_import_diff_note diff --git a/app/workers/cluster_platform_configure_worker.rb b/app/workers/cluster_platform_configure_worker.rb new file mode 100644 index 00000000000..68e8335a09d --- /dev/null +++ b/app/workers/cluster_platform_configure_worker.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +class ClusterPlatformConfigureWorker + include ApplicationWorker + include ClusterQueue + + def perform(cluster_id) + Clusters::Cluster.find_by_id(cluster_id).try do |cluster| + next unless cluster.cluster_project + + kubernetes_namespace = cluster.find_or_initialize_kubernetes_namespace(cluster.cluster_project) + + Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService.new( + cluster: cluster, + kubernetes_namespace: kubernetes_namespace + ).execute + end + + rescue ::Kubeclient::HttpError => err + Rails.logger.error "Failed to create/update Kubernetes Namespace. id: #{kubernetes_namespace.id} message: #{err.message}" + end +end diff --git a/app/workers/cluster_provision_worker.rb b/app/workers/cluster_provision_worker.rb index 59de7903c1c..3d5894b73ec 100644 --- a/app/workers/cluster_provision_worker.rb +++ b/app/workers/cluster_provision_worker.rb @@ -9,6 +9,8 @@ class ClusterProvisionWorker cluster.provider.try do |provider| Clusters::Gcp::ProvisionService.new.execute(provider) if cluster.gcp? end + + ClusterPlatformConfigureWorker.perform_async(cluster.id) if cluster.user? end end end |