summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorChris Baumbauer <cab@cabnetworks.net>2018-11-03 10:13:35 -0700
committerChris Baumbauer <cab@cabnetworks.net>2018-11-03 10:13:35 -0700
commitaa44393e8f6e428bb7159ac8815a143a9e3e1047 (patch)
tree815e91b75359ad33173a667702cecb03cc963535 /app
parentdc078c241765cfea5f49409407b82db7296c132d (diff)
parent4d3ff28a6a0d81f44ccb3eb1602996e5d7c3de1c (diff)
downloadgitlab-ce-aa44393e8f6e428bb7159ac8815a143a9e3e1047.tar.gz
Merge branch 'master' into triggermesh-phase1-knative
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/boards/models/issue.js1
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_table.vue19
-rw-r--r--app/assets/javascripts/commons/gitlab_ui.js3
-rw-r--r--app/assets/javascripts/labels_select.js2
-rw-r--r--app/assets/javascripts/pages/projects/clusters/gcp/new/index.js5
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines.vue38
-rw-r--r--app/assets/javascripts/pipelines/mixins/pipelines.js38
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/header.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue10
-rw-r--r--app/assets/javascripts/vue_shared/mixins/ci_pagination_api_mixin.js9
-rw-r--r--app/controllers/clusters/applications_controller.rb28
-rw-r--r--app/controllers/clusters/base_controller.rb37
-rw-r--r--app/controllers/clusters/clusters_controller.rb218
-rw-r--r--app/controllers/concerns/project_unauthorized.rb10
-rw-r--r--app/controllers/concerns/routable_actions.rb16
-rw-r--r--app/controllers/projects/application_controller.rb3
-rw-r--r--app/controllers/projects/clusters/applications_controller.rb26
-rw-r--r--app/controllers/projects/clusters_controller.rb222
-rw-r--r--app/controllers/projects/commit_controller.rb3
-rw-r--r--app/controllers/projects/merge_requests_controller.rb3
-rw-r--r--app/finders/clusters_finder.rb8
-rw-r--r--app/helpers/clusters_helper.rb5
-rw-r--r--app/helpers/labels_helper.rb2
-rw-r--r--app/mailers/emails/issues.rb14
-rw-r--r--app/mailers/emails/merge_requests.rb14
-rw-r--r--app/mailers/previews/notify_preview.rb20
-rw-r--r--app/models/ci/build.rb3
-rw-r--r--app/models/clusters/cluster.rb8
-rw-r--r--app/models/clusters/kubernetes_namespace.rb35
-rw-r--r--app/models/clusters/platforms/kubernetes.rb36
-rw-r--r--app/models/concerns/token_authenticatable.rb7
-rw-r--r--app/models/concerns/token_authenticatable_strategies/base.rb6
-rw-r--r--app/models/issue.rb3
-rw-r--r--app/models/key.rb4
-rw-r--r--app/models/merge_request.rb9
-rw-r--r--app/models/merge_request_diff.rb6
-rw-r--r--app/models/project.rb2
-rw-r--r--app/models/project_services/kubernetes_service.rb7
-rw-r--r--app/models/user.rb9
-rw-r--r--app/presenters/clusterable_presenter.rb46
-rw-r--r--app/presenters/clusters/cluster_presenter.rb8
-rw-r--r--app/presenters/project_clusterable_presenter.rb15
-rw-r--r--app/serializers/build_action_entity.rb7
-rw-r--r--app/serializers/job_entity.rb1
-rw-r--r--app/services/clusters/create_service.rb22
-rw-r--r--app/services/clusters/gcp/finalize_creation_service.rb23
-rw-r--r--app/services/clusters/gcp/kubernetes.rb11
-rw-r--r--app/services/clusters/gcp/kubernetes/create_or_update_namespace_service.rb54
-rw-r--r--app/services/clusters/gcp/kubernetes/create_service_account_service.rb78
-rw-r--r--app/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service.rb8
-rw-r--r--app/services/issuable_base_service.rb8
-rw-r--r--app/services/issues/update_service.rb14
-rw-r--r--app/services/merge_requests/refresh_service.rb11
-rw-r--r--app/services/merge_requests/reload_diffs_service.rb5
-rw-r--r--app/services/merge_requests/update_service.rb14
-rw-r--r--app/services/milestones/destroy_service.rb2
-rw-r--r--app/services/notification_service.rb41
-rw-r--r--app/views/clusters/clusters/_advanced_settings.html.haml (renamed from app/views/projects/clusters/_advanced_settings.html.haml)2
-rw-r--r--app/views/clusters/clusters/_banner.html.haml (renamed from app/views/projects/clusters/_banner.html.haml)0
-rw-r--r--app/views/clusters/clusters/_cluster.html.haml (renamed from app/views/projects/clusters/_cluster.html.haml)4
-rw-r--r--app/views/clusters/clusters/_empty_state.html.haml (renamed from app/views/projects/clusters/_empty_state.html.haml)4
-rw-r--r--app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml (renamed from app/views/projects/clusters/_gcp_signup_offer_banner.html.haml)0
-rw-r--r--app/views/clusters/clusters/_integration_form.html.haml (renamed from app/views/projects/clusters/_integration_form.html.haml)6
-rw-r--r--app/views/clusters/clusters/_sidebar.html.haml (renamed from app/views/projects/clusters/_sidebar.html.haml)0
-rw-r--r--app/views/clusters/clusters/gcp/_form.html.haml (renamed from app/views/projects/clusters/gcp/_form.html.haml)4
-rw-r--r--app/views/clusters/clusters/gcp/_header.html.haml (renamed from app/views/projects/clusters/gcp/_header.html.haml)0
-rw-r--r--app/views/clusters/clusters/gcp/_show.html.haml (renamed from app/views/projects/clusters/gcp/_show.html.haml)2
-rw-r--r--app/views/clusters/clusters/index.html.haml (renamed from app/views/projects/clusters/index.html.haml)0
-rw-r--r--app/views/clusters/clusters/new.html.haml (renamed from app/views/projects/clusters/new.html.haml)8
-rw-r--r--app/views/clusters/clusters/show.html.haml (renamed from app/views/projects/clusters/show.html.haml)23
-rw-r--r--app/views/clusters/clusters/user/_form.html.haml (renamed from app/views/projects/clusters/user/_form.html.haml)4
-rw-r--r--app/views/clusters/clusters/user/_header.html.haml (renamed from app/views/projects/clusters/user/_header.html.haml)0
-rw-r--r--app/views/clusters/clusters/user/_show.html.haml (renamed from app/views/projects/clusters/user/_show.html.haml)2
-rw-r--r--app/views/notify/changed_milestone_issue_email.html.haml3
-rw-r--r--app/views/notify/changed_milestone_issue_email.text.erb1
-rw-r--r--app/views/notify/changed_milestone_merge_request_email.html.haml3
-rw-r--r--app/views/notify/changed_milestone_merge_request_email.text.erb1
-rw-r--r--app/views/notify/removed_milestone_issue_email.html.haml2
-rw-r--r--app/views/notify/removed_milestone_issue_email.text.erb1
-rw-r--r--app/views/notify/removed_milestone_merge_request_email.html.haml2
-rw-r--r--app/views/notify/removed_milestone_merge_request_email.text.erb1
-rw-r--r--app/views/shared/boards/components/sidebar/_labels.html.haml4
-rw-r--r--app/workers/all_queues.yml1
-rw-r--r--app/workers/cluster_platform_configure_worker.rb22
-rw-r--r--app/workers/cluster_provision_worker.rb2
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