Commit aa44393e authored by Chris Baumbauer's avatar Chris Baumbauer

Merge branch 'master' into triggermesh-phase1-knative

parents dc078c24 4d3ff28a
...@@ -2,6 +2,13 @@ ...@@ -2,6 +2,13 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 11.4.4 (2018-10-30)
### Security (1 change)
- Monkey kubeclient to not follow any redirects.
## 11.4.3 (2018-10-26) ## 11.4.3 (2018-10-26)
- No changes. - No changes.
...@@ -250,6 +257,13 @@ entry. ...@@ -250,6 +257,13 @@ entry.
- Check frozen string in style builds. (gfyoung) - Check frozen string in style builds. (gfyoung)
## 11.3.9 (2018-10-31)
### Security (1 change)
- Monkey kubeclient to not follow any redirects.
## 11.3.8 (2018-10-27) ## 11.3.8 (2018-10-27)
- No changes. - No changes.
...@@ -555,6 +569,13 @@ entry. ...@@ -555,6 +569,13 @@ entry.
- Creates Vue component for artifacts block on job page. - Creates Vue component for artifacts block on job page.
## 11.2.8 (2018-10-31)
### Security (1 change)
- Monkey kubeclient to not follow any redirects.
## 11.2.7 (2018-10-27) ## 11.2.7 (2018-10-27)
- No changes. - No changes.
......
...@@ -30,6 +30,7 @@ class ListIssue { ...@@ -30,6 +30,7 @@ class ListIssue {
this.toggleSubscriptionEndpoint = obj.toggle_subscription_endpoint; this.toggleSubscriptionEndpoint = obj.toggle_subscription_endpoint;
this.milestone_id = obj.milestone_id; this.milestone_id = obj.milestone_id;
this.project_id = obj.project_id; this.project_id = obj.project_id;
this.assignableLabelsEndpoint = obj.assignable_labels_endpoint;
if (obj.project) { if (obj.project) {
this.project = new IssueProject(obj.project); this.project = new IssueProject(obj.project);
......
...@@ -2,9 +2,15 @@ ...@@ -2,9 +2,15 @@
import PipelinesService from '../../pipelines/services/pipelines_service'; import PipelinesService from '../../pipelines/services/pipelines_service';
import PipelineStore from '../../pipelines/stores/pipelines_store'; import PipelineStore from '../../pipelines/stores/pipelines_store';
import pipelinesMixin from '../../pipelines/mixins/pipelines'; 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 { export default {
mixins: [pipelinesMixin], components: {
TablePagination,
},
mixins: [pipelinesMixin, CIPaginationMixin],
props: { props: {
endpoint: { endpoint: {
type: String, type: String,
...@@ -35,6 +41,8 @@ export default { ...@@ -35,6 +41,8 @@ export default {
return { return {
store, store,
state: store.state, state: store.state,
page: getParameterByName('page') || '1',
requestData: {},
}; };
}, },
...@@ -48,11 +56,14 @@ export default { ...@@ -48,11 +56,14 @@ export default {
}, },
created() { created() {
this.service = new PipelinesService(this.endpoint); this.service = new PipelinesService(this.endpoint);
this.requestData = { page: this.page };
}, },
methods: { methods: {
successCallback(resp) { successCallback(resp) {
// depending of the endpoint the response can either bring a `pipelines` key or not. // depending of the endpoint the response can either bring a `pipelines` key or not.
const pipelines = resp.data.pipelines || resp.data; const pipelines = resp.data.pipelines || resp.data;
this.store.storePagination(resp.headers);
this.setCommonData(pipelines); this.setCommonData(pipelines);
const updatePipelinesEvent = new CustomEvent('update-pipelines-count', { const updatePipelinesEvent = new CustomEvent('update-pipelines-count', {
...@@ -97,5 +108,11 @@ export default { ...@@ -97,5 +108,11 @@ export default {
:view-type="viewType" :view-type="viewType"
/> />
</div> </div>
<table-pagination
v-if="shouldRenderPagination"
:change="onChangePage"
:page-info="state.pageInfo"
/>
</div> </div>
</template> </template>
import Vue from 'vue'; 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.component('gl-loading-icon', GlLoadingIcon);
Vue.directive('gl-tooltip', GlTooltipDirective); Vue.directive('gl-tooltip', GlTooltipDirective);
...@@ -59,7 +59,6 @@ export default class LabelsSelect { ...@@ -59,7 +59,6 @@ export default class LabelsSelect {
$toggleText = $dropdown.find('.dropdown-toggle-text'); $toggleText = $dropdown.find('.dropdown-toggle-text');
namespacePath = $dropdown.data('namespacePath'); namespacePath = $dropdown.data('namespacePath');
projectPath = $dropdown.data('projectPath'); projectPath = $dropdown.data('projectPath');
labelUrl = $dropdown.data('labels');
issueUpdateURL = $dropdown.data('issueUpdate'); issueUpdateURL = $dropdown.data('issueUpdate');
selectedLabel = $dropdown.data('selected'); selectedLabel = $dropdown.data('selected');
if (selectedLabel != null && !$dropdown.hasClass('js-multiselect')) { if (selectedLabel != null && !$dropdown.hasClass('js-multiselect')) {
...@@ -168,6 +167,7 @@ export default class LabelsSelect { ...@@ -168,6 +167,7 @@ export default class LabelsSelect {
$dropdown.glDropdown({ $dropdown.glDropdown({
showMenuAbove: showMenuAbove, showMenuAbove: showMenuAbove,
data: function(term, callback) { data: function(term, callback) {
labelUrl = $dropdown.attr('data-labels');
axios axios
.get(labelUrl) .get(labelUrl)
.then(res => { .then(res => {
......
import initGkeDropdowns from '~/projects/gke_cluster_dropdowns';
document.addEventListener('DOMContentLoaded', () => {
initGkeDropdowns();
});
...@@ -155,14 +155,6 @@ export default { ...@@ -155,14 +155,6 @@ export default {
); );
}, },
shouldRenderPagination() {
return (
!this.isLoading &&
this.state.pipelines.length &&
this.state.pageInfo.total > this.state.pageInfo.perPage
);
},
emptyTabMessage() { emptyTabMessage() {
const { scopes } = this.$options; const { scopes } = this.$options;
const possibleScopes = [scopes.pending, scopes.running, scopes.finished]; const possibleScopes = [scopes.pending, scopes.running, scopes.finished];
...@@ -232,36 +224,6 @@ export default { ...@@ -232,36 +224,6 @@ export default {
this.setCommonData(resp.data.pipelines); 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) { handleResetRunnersCache(endpoint) {
this.isResetCacheButtonLoading = true; this.isResetCacheButtonLoading = true;
......
...@@ -23,6 +23,15 @@ export default { ...@@ -23,6 +23,15 @@ export default {
hasMadeRequest: false, hasMadeRequest: false,
}; };
}, },
computed: {
shouldRenderPagination() {
return (
!this.isLoading &&
this.state.pipelines.length &&
this.state.pageInfo.total > this.state.pageInfo.perPage
);
},
},
beforeMount() { beforeMount() {
this.poll = new Poll({ this.poll = new Poll({
resource: this.service, resource: this.service,
...@@ -65,6 +74,35 @@ export default { ...@@ -65,6 +74,35 @@ export default {
this.poll.stop(); this.poll.stop();
}, },
methods: { 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() { updateTable() {
// Cancel ongoing request // Cancel ongoing request
if (this.isMakingRequest) { if (this.isMakingRequest) {
......
<script> <script>
import { parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility'; import { parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility';
import tooltip from '../../../vue_shared/directives/tooltip'; import tooltip from '../../../vue_shared/directives/tooltip';
import { GlProgressBar } from '@gitlab-org/gitlab-ui';
export default { export default {
name: 'TimeTrackingComparisonPane', name: 'TimeTrackingComparisonPane',
components: {
GlProgressBar,
},
directives: { directives: {
tooltip, tooltip,
}, },
......
<script> <script>
import $ from 'jquery'; import $ from 'jquery';
import Tooltip from '../../directives/tooltip'; import { GlTooltipDirective } from '@gitlab-org/gitlab-ui';
import ToolbarButton from './toolbar_button.vue'; import ToolbarButton from './toolbar_button.vue';
import Icon from '../icon.vue'; import Icon from '../icon.vue';
export default { export default {
directives: {
Tooltip,
},
components: { components: {
ToolbarButton, ToolbarButton,
Icon, Icon,
}, },
directives: {
GlTooltip: GlTooltipDirective,
},
props: { props: {
previewMarkdown: { previewMarkdown: {
type: Boolean, type: Boolean,
...@@ -147,7 +147,7 @@ export default { ...@@ -147,7 +147,7 @@ export default {
icon="table" icon="table"
/> />
<button <button
v-tooltip v-gl-tooltip
aria-label="Go full screen" aria-label="Go full screen"
class="toolbar-btn toolbar-fullscreen-btn js-zen-enter" class="toolbar-btn toolbar-fullscreen-btn js-zen-enter"
data-container="body" data-container="body"
......
<script> <script>
import tooltip from '../../directives/tooltip'; import { GlTooltipDirective } from '@gitlab-org/gitlab-ui';
import icon from '../icon.vue'; import Icon from '../icon.vue';
export default { export default {
components: { components: {
icon, Icon,
}, },
directives: { directives: {
tooltip, GlTooltip: GlTooltipDirective,
}, },
props: { props: {
buttonTitle: { buttonTitle: {
...@@ -43,7 +43,7 @@ export default { ...@@ -43,7 +43,7 @@ export default {
<template> <template>
<button <button
v-tooltip v-gl-tooltip
:data-md-tag="tag" :data-md-tag="tag"
:data-md-select="tagSelect" :data-md-select="tagSelect"
:data-md-block="tagBlock" :data-md-block="tagBlock"
......
...@@ -14,7 +14,14 @@ export default { ...@@ -14,7 +14,14 @@ export default {
onChangePage(page) { onChangePage(page) {
/* URLS parameters are strings, we need to parse to match types */ /* 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) { updateInternalState(parameters) {
......
# 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
# 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
# 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
# frozen_string_literal: true
module ProjectUnauthorized
extend ActiveSupport::Concern
# EE would override this
def project_unauthorized_proc
# no-op
end
end
...@@ -3,23 +3,25 @@ ...@@ -3,23 +3,25 @@
module RoutableActions module RoutableActions
extend ActiveSupport::Concern 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?) routable = routable_klass.find_by_full_path(requested_full_path, follow_redirects: request.get?)
if routable_authorized?(routable, extra_authorization_proc) if routable_authorized?(routable, extra_authorization_proc)
ensure_canonical_path(routable, requested_full_path) ensure_canonical_path(routable, requested_full_path)
routable routable
else 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 nil
end end
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) def routable_authorized?(routable, extra_authorization_proc)
return false unless routable
action = :"read_#{routable.class.to_s.underscore}" action = :"read_#{routable.class.to_s.underscore}"
return false unless can?(current_user, action, routable) return false unless can?(current_user, action, routable)
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
class Projects::ApplicationController < ApplicationController class Projects::ApplicationController < ApplicationController
include CookiesHelper include CookiesHelper
include RoutableActions include RoutableActions
include ProjectUnauthorized
include ChecksCollaboration include ChecksCollaboration
skip_before_action :authenticate_user! skip_before_action :authenticate_user!
...@@ -21,7 +22,7 @@ class Projects::ApplicationController < ApplicationController ...@@ -21,7 +22,7 @@ class Projects::ApplicationController < ApplicationController
path = File.join(params[:namespace_id], params[:project_id] || params[:id]) path = File.join(params[:namespace_id], params[:project_id] || params[:id])
auth_proc = ->(project) { !project.pending_delete? } 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 end
def build_canonical_path(project) def build_canonical_path(project)
......
# frozen_string_literal: true # frozen_string_literal: true
class Projects::Clusters::ApplicationsController < Projects::ApplicationController class Projects::Clusters::ApplicationsController < Clusters::ApplicationsController
before_action :cluster include ProjectUnauthorized
before_action :authorize_read_cluster!
before_action :authorize_create_cluster!, only: [:create]
def create prepend_before_action :project
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 private
def cluster def clusterable
@cluster ||= project.clusters.find(params[:id]) || render_404 @clusterable ||= ClusterablePresenter.fabricate(project, current_user: current_user)
end end
def create_cluster_application_params def project
params.permit(:application, :hostname) @project ||= find_routable!(Project, File.join(params[:namespace_id], params[:project_id]), not_found_or_authorized_proc: project_unauthorized_proc)
end end
end end
# frozen_string_literal: true # frozen_string_literal: true
class Projects::ClustersController < Projects::ApplicationController class Projects::ClustersController < Clusters::ClustersController
before_action :cluster, except: [:index, :new, :create_gcp, :create_user] include ProjectUnauthorized
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
STATUS_POLLING_INTERVAL = 10_000 prepend_before_action :project
before_action :repository
def index layout 'project'
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
private private
def cluster def clusterable
@cluster ||= project.clusters.find(params[:id]) @clusterable ||= ClusterablePresenter.fabricate(project, current_user: current_user)
.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)
end end
def authorize_admin_cluster! def project
access_denied! unless can?(current_user, :admin_cluster, cluster) @project ||= find_routable!(Project, File.join(params[:namespace_id], params[:project_id]), not_found_or_authorized_proc: project_unauthorized_proc)
end end
def update_applications_status def repository
@cluster.applications.each(&:schedule_status_update) @repository ||= project.repository
end end
end end
...@@ -43,7 +43,7 @@ class Projects::CommitController < Projects::ApplicationController ...@@ -43,7 +43,7 @@ class Projects::CommitController < Projects::ApplicationController
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def pipelines def pipelines
@pipelines = @commit.pipelines.order(id: :desc) @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| respond_to do |format|
format.html format.html
...@@ -53,6 +53,7 @@ class Projects::CommitController < Projects::ApplicationController ...@@ -53,6 +53,7 @@ class Projects::CommitController < Projects::ApplicationController
render json: { render json: {
pipelines: PipelineSerializer pipelines: PipelineSerializer
.new(project: @project, current_user: @current_user) .new(project: @project, current_user: @current_user)
.with_pagination(request, response)
.represent(@pipelines), .represent(@pipelines),
count: { count: {
all: @pipelines.count all: @pipelines.count
......
...@@ -84,13 +84,14 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -84,13 +84,14 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
end end
def pipelines 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) Gitlab::PollingInterval.set_header(response, interval: 10_000)
render json: { render json: {
pipelines: PipelineSerializer pipelines: PipelineSerializer
.new(project: @project, current_user: @current_user) .new(project: @project, current_user: @current_user)
.with_pagination(request, response)
.represent(@pipelines), .represent(@pipelines),
count: { count: {
all: @pipelines.count all: @pipelines.count
......
# frozen_string_literal: true # frozen_string_literal: true
class ClustersFinder class ClustersFinder
def initialize(project, user, scope) def initialize(clusterable, user, scope)
@project = project @clusterable = clusterable
@user = user @user = user
@scope = scope || :active @scope = scope || :active
end end
def execute def execute
clusters = project.clusters clusters = clusterable.clusters
filter_by_scope(clusters) filter_by_scope(clusters)
end end
private private
attr_reader :project, :user, :scope attr_reader :clusterable, :user, :scope
def filter_by_scope(clusters) def filter_by_scope(clusters)
case scope.to_sym case scope.to_sym
......
# frozen_string_literal: true # frozen_string_literal: true
module ClustersHelper module ClustersHelper
def has_multiple_clusters?(project) # EE overrides this
def has_multiple_clusters?
false false
end end
...@@ -10,7 +11,7 @@ module ClustersHelper ...@@ -10,7 +11,7 @@ module ClustersHelper
return unless show_gcp_signup_offer? return unless show_gcp_signup_offer?
content_tag :section, class: 'no-animate expanded' do 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 end
end end
...@@ -143,7 +143,7 @@ module LabelsHelper ...@@ -143,7 +143,7 @@ module LabelsHelper
def labels_filter_path(options = {}) def labels_filter_path(options = {})
project = @target_project || @project project = @target_project || @project
format = options.delete(:format) || :html format = options.delete(:format)
if project if project
project_labels_path(project, format, options) project_labels_path(project, format, options)
......
...@@ -45,6 +45,20 @@ module Emails ...@@ -45,6 +45,20 @@ module Emails
mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id, reason)) mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id, reason))
end 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) def issue_status_changed_email(recipient_id, issue_id, status, updated_by_user_id, reason = nil)
setup_issue_mail(issue_id, recipient_id) setup_issue_mail(issue_id, recipient_id)
......
...@@ -40,6 +40,20 @@ module Emails ...@@ -40,6 +40,20 @@ module Emails
mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id, reason)) mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id, reason))
end 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) 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) setup_merge_request_mail(merge_request_id, recipient_id)
......
...@@ -68,6 +68,14 @@ class NotifyPreview < ActionMailer::Preview ...@@ -68,6 +68,14 @@ class NotifyPreview < ActionMailer::Preview
Notify.issue_status_changed_email(user.id, issue.id, 'closed', user.id).message Notify.issue_status_changed_email(user.id, issue.id, 'closed', user.id).message
end 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 def closed_merge_request_email
Notify.closed_merge_request_email(user.id, issue.id, user.id).message Notify.closed_merge_request_email(user.id, issue.id, user.id).message
end end
...@@ -80,6 +88,14 @@ class NotifyPreview < ActionMailer::Preview ...@@ -80,6 +88,14 @@ class NotifyPreview < ActionMailer::Preview
Notify.merged_merge_request_email(user.id, merge_request.id, user.id).message Notify.merged_merge_request_email(user.id, merge_request.id, user.id).message
end 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 def member_access_denied_email
Notify.member_access_denied_email('project', project.id, user.id).message Notify.member_access_denied_email('project', project.id, user.id).message
end end
...@@ -143,6 +159,10 @@ class NotifyPreview < ActionMailer::Preview ...@@ -143,6 +159,10 @@ class NotifyPreview < ActionMailer::Preview
@merge_request ||= project.merge_requests.first @merge_request ||= project.merge_requests.first
end end
def milestone
@milestone ||= issue.milestone
end
def pipeline def pipeline
@pipeline = Ci::Pipeline.last @pipeline = Ci::Pipeline.last
end end
......
...@@ -259,8 +259,7 @@ module Ci ...@@ -259,8 +259,7 @@ module Ci
end end
def schedulable? 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 end
def options_scheduled_at def options_scheduled_at
......
...@@ -20,6 +20,7 @@ module Clusters ...@@ -20,6 +20,7 @@ module Clusters
has_many :cluster_projects, class_name: 'Clusters::Project' has_many :cluster_projects, class_name: 'Clusters::Project'
has_many :projects, through: :cluster_projects, class_name: '::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 :cluster_groups, class_name: 'Clusters::Group'
has_many :groups, through: :cluster_groups, class_name: '::Group' has_many :groups, through: :cluster_groups, class_name: '::Group'
...@@ -131,6 +132,13 @@ module Clusters ...@@ -131,6 +132,13 @@ module Clusters
platform_kubernetes.kubeclient if kubernetes? platform_kubernetes.kubeclient if kubernetes?
end 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 private
def restrict_modification def restrict_modification
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
module Clusters module Clusters
class KubernetesNamespace < ActiveRecord::Base class KubernetesNamespace < ActiveRecord::Base
include Gitlab::Kubernetes
self.table_name = 'clusters_kubernetes_namespaces' self.table_name = 'clusters_kubernetes_namespaces'
belongs_to :cluster_project, class_name: 'Clusters::Project' belongs_to :cluster_project, class_name: 'Clusters::Project'
...@@ -12,7 +14,8 @@ module Clusters ...@@ -12,7 +14,8 @@ module Clusters
validates :namespace, presence: true validates :namespace, presence: true
validates :namespace, uniqueness: { scope: :cluster_id } 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, attr_encrypted :service_account_token,
mode: :per_attribute_iv, mode: :per_attribute_iv,
...@@ -23,14 +26,26 @@ module Clusters ...@@ -23,14 +26,26 @@ module Clusters
"#{namespace}-token" "#{namespace}-token"
end 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 Gitlab::Ci::Variables::Collection.new.tap do |variables|
self.namespace ||= default_namespace variables
self.service_account_name ||= default_service_account_name .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 end
def default_namespace private
def kubernetes_or_project_namespace
platform_kubernetes&.namespace.presence || project_namespace platform_kubernetes&.namespace.presence || project_namespace
end end
...@@ -45,5 +60,13 @@ module Clusters ...@@ -45,5 +60,13 @@ module Clusters
def project_slug def project_slug
"#{project.path}-#{project.id}".downcase "#{project.path}-#{project.id}".downcase
end end
def kubeconfig
to_kubeconfig(
url: api_url,
namespace: namespace,
token: service_account_token,
ca_pem: ca_pem)
end
end end
end end
...@@ -6,6 +6,7 @@ module Clusters ...@@ -6,6 +6,7 @@ module Clusters
include Gitlab::Kubernetes include Gitlab::Kubernetes
include ReactiveCaching include ReactiveCaching
include EnumWithNil include EnumWithNil
include AfterCommitQueue
RESERVED_NAMESPACES = %w(gitlab-managed-apps).freeze RESERVED_NAMESPACES = %w(gitlab-managed-apps).freeze
...@@ -44,6 +45,7 @@ module Clusters ...@@ -44,6 +45,7 @@ module Clusters
validate :prevent_modification, on: :update validate :prevent_modification, on: :update
after_save :clear_reactive_cache! after_save :clear_reactive_cache!
after_update :update_kubernetes_namespace
alias_attribute :ca_pem, :ca_cert alias_attribute :ca_pem, :ca_cert
...@@ -68,21 +70,31 @@ module Clusters ...@@ -68,21 +70,31 @@ module Clusters
end end
end end
def predefined_variables def predefined_variables(project:)
config = YAML.dump(kubeconfig)
Gitlab::Ci::Variables::Collection.new.tap do |variables| Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables variables.append(key: 'KUBE_URL', value: api_url)
.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)
if ca_pem.present? if ca_pem.present?
variables variables
.append(key: 'KUBE_CA_PEM', value: ca_pem) .append(key: 'KUBE_CA_PEM', value: ca_pem)
.append(key: 'KUBE_CA_PEM_FILE', value: ca_pem, file: true) .append(key: 'KUBE_CA_PEM_FILE', value: ca_pem, file: true)
end 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
end end
...@@ -205,6 +217,14 @@ module Clusters ...@@ -205,6 +217,14 @@ module Clusters
true true
end end
def update_kubernetes_namespace
return unless namespace_changed?
run_after_commit do
ClusterPlatformConfigureWorker.perform_async(cluster_id)
end
end
end end
end end
end end
...@@ -10,6 +10,7 @@ module TokenAuthenticatable ...@@ -10,6 +10,7 @@ module TokenAuthenticatable
def add_authentication_token_field(token_field, options = {}) def add_authentication_token_field(token_field, options = {})
@token_fields = [] unless @token_fields @token_fields = [] unless @token_fields
unique = options.fetch(:unique, true)
if @token_fields.include?(token_field) if @token_fields.include?(token_field)
raise ArgumentError.new("#{token_field} already configured via add_authentication_token_field") raise ArgumentError.new("#{token_field} already configured via add_authentication_token_field")
...@@ -25,8 +26,10 @@ module TokenAuthenticatable ...@@ -25,8 +26,10 @@ module TokenAuthenticatable
TokenAuthenticatableStrategies::Insecure.new(self, token_field, options) TokenAuthenticatableStrategies::Insecure.new(self, token_field, options)
end end
define_singleton_method("find_by_#{token_field}") do |token| if unique
strategy.find_token_authenticatable(token) define_singleton_method("find_by_#{token_field}") do |token|
strategy.find_token_authenticatable(token)
end
end end
define_method(token_field) do define_method(token_field) do
......
...@@ -43,10 +43,14 @@ module TokenAuthenticatableStrategies ...@@ -43,10 +43,14 @@ module TokenAuthenticatableStrategies
set_token(instance, new_token) set_token(instance, new_token)
end end
def unique
@options.fetch(:unique, true)
end
def generate_available_token def generate_available_token
loop do loop do
token = generate_token token = generate_token
break token unless find_token_authenticatable(token, true) break token unless unique && find_token_authenticatable(token, true)
end end
end end
......
...@@ -240,7 +240,8 @@ class Issue < ActiveRecord::Base ...@@ -240,7 +240,8 @@ class Issue < ActiveRecord::Base
reference_path: issue_reference, reference_path: issue_reference,
real_path: url_helper.project_issue_path(project, self), real_path: url_helper.project_issue_path(project, self),
issue_sidebar_endpoint: url_helper.project_issue_path(project, self, format: :json, serializer: 'sidebar'), 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 end
......
...@@ -34,6 +34,10 @@ class Key < ActiveRecord::Base ...@@ -34,6 +34,10 @@ class Key < ActiveRecord::Base
after_destroy :post_destroy_hook after_destroy :post_destroy_hook
after_destroy :refresh_user_cache after_destroy :refresh_user_cache
def self.regular_keys
where(type: ['Key', nil])
end
def key=(value) def key=(value)
write_attribute(:key, value.present? ? Gitlab::SSHPublicKey.sanitize(value) : nil) write_attribute(:key, value.present? ? Gitlab::SSHPublicKey.sanitize(value) : nil)
......
...@@ -353,6 +353,15 @@ class MergeRequest < ActiveRecord::Base ...@@ -353,6 +353,15 @@ class MergeRequest < ActiveRecord::Base
end end
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 # Calls `MergeWorker` to proceed with the merge process and
# updates `merge_jid` with the MergeWorker#jid. # updates `merge_jid` with the MergeWorker#jid.
# This helps tracking enqueued and ongoing merge jobs. # This helps tracking enqueued and ongoing merge jobs.
......
...@@ -140,6 +140,12 @@ class MergeRequestDiff < ActiveRecord::Base ...@@ -140,6 +140,12 @@ class MergeRequestDiff < ActiveRecord::Base
merge_request_diff_commits.map(&:sha) merge_request_diff_commits.map(&:sha)
end end
def commits_by_shas(shas)
return [] unless shas.present?
merge_request_diff_commits.where(sha: shas)
end
def diff_refs=(new_diff_refs) def diff_refs=(new_diff_refs)
self.base_commit_sha = new_diff_refs&.base_sha self.base_commit_sha = new_diff_refs&.base_sha
self.start_commit_sha = new_diff_refs&.start_sha self.start_commit_sha = new_diff_refs&.start_sha
......
...@@ -1829,7 +1829,7 @@ class Project < ActiveRecord::Base ...@@ -1829,7 +1829,7 @@ class Project < ActiveRecord::Base
end end
def deployment_variables(environment: nil) def deployment_variables(environment: nil)
deployment_platform(environment: environment)&.predefined_variables || [] deployment_platform(environment: environment)&.predefined_variables(project: self) || []
end end
def auto_devops_variables def auto_devops_variables
......
...@@ -104,7 +104,12 @@ class KubernetesService < DeploymentService ...@@ -104,7 +104,12 @@ class KubernetesService < DeploymentService
{ success: false, result: err } { success: false, result: err }
end 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) config = YAML.dump(kubeconfig)
Gitlab::Ci::Variables::Collection.new.tap do |variables| Gitlab::Ci::Variables::Collection.new.tap do |variables|
......
...@@ -88,7 +88,7 @@ class User < ActiveRecord::Base ...@@ -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 has_one :namespace, -> { where(type: nil) }, dependent: :destroy, foreign_key: :owner_id, inverse_of: :owner, autosave: true # rubocop:disable Cop/ActiveRecordDependent
# Profile # 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 :deploy_keys, -> { where(type: 'DeployKey') }, dependent: :nullify # rubocop:disable Cop/ActiveRecordDependent
has_many :gpg_keys has_many :gpg_keys
...@@ -941,12 +941,17 @@ class User < ActiveRecord::Base ...@@ -941,12 +941,17 @@ class User < ActiveRecord::Base
if !Gitlab.config.ldap.enabled if !Gitlab.config.ldap.enabled
false false
elsif ldap_user? 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 else
false false
end end
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 def try_obtain_ldap_lease
# After obtaining this lease LDAP checks will be blocked for 600 seconds # After obtaining this lease LDAP checks will be blocked for 600 seconds
# (10 minutes) for this user. # (10 minutes) for this user.
......
# 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
...@@ -11,5 +11,13 @@ module Clusters ...@@ -11,5 +11,13 @@ module Clusters
def can_toggle_cluster? def can_toggle_cluster?
can?(current_user, :update_cluster, cluster) && created? can?(current_user, :update_cluster, cluster) && created?
end end
def show_path
if cluster.project_type?
project_cluster_path(project, cluster)
else
raise NotImplementedError
end
end
end end
end end
# 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
...@@ -12,7 +12,8 @@ class BuildActionEntity < Grape::Entity ...@@ -12,7 +12,8 @@ class BuildActionEntity < Grape::Entity
end end
expose :playable?, as: :playable 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| expose :unschedule_path, if: -> (build) { build.scheduled? } do |build|
unschedule_project_job_path(build.project, build) unschedule_project_job_path(build.project, build)
...@@ -25,4 +26,8 @@ class BuildActionEntity < Grape::Entity ...@@ -25,4 +26,8 @@ class BuildActionEntity < Grape::Entity
def playable? def playable?
build.playable? && can?(request.current_user, :update_build, build) build.playable? && can?(request.current_user, :update_build, build)
end end
def scheduled?
build.scheduled?
end
end end
...@@ -33,6 +33,7 @@ class JobEntity < Grape::Entity ...@@ -33,6 +33,7 @@ class JobEntity < Grape::Entity
end end
expose :playable?, as: :playable expose :playable?, as: :playable
expose :scheduled?, as: :scheduled
expose :scheduled_at, if: -> (*) { scheduled? } expose :scheduled_at, if: -> (*) { scheduled? }
expose :created_at expose :created_at
expose :updated_at expose :updated_at
......
...@@ -8,10 +8,11 @@ module Clusters ...@@ -8,10 +8,11 @@ module Clusters
@current_user, @params = user, params.dup @current_user, @params = user, params.dup
end end
def execute(project:, access_token: nil) def execute(access_token: nil)
raise ArgumentError, _('Instance does not support multiple Kubernetes clusters') unless can_create_cluster?(project) 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| cluster_params[:provider_gcp_attributes].try do |provider|
provider[:access_token] = access_token provider[:access_token] = access_token
end end
...@@ -27,9 +28,20 @@ module Clusters ...@@ -27,9 +28,20 @@ module Clusters
Clusters::Cluster.create(cluster_params) Clusters::Cluster.create(cluster_params)
end 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 # EE would override this method
def can_create_cluster?(project) def can_create_cluster?
project.clusters.empty? clusterable.clusters.empty?
end end
end end
end end
...@@ -11,8 +11,9 @@ module Clusters ...@@ -11,8 +11,9 @@ module Clusters
configure_provider configure_provider
create_gitlab_service_account! create_gitlab_service_account!
configure_kubernetes configure_kubernetes
cluster.save! cluster.save!
configure_project_service_account
rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e
provider.make_errored!("Failed to request to CloudPlatform; #{e.message}") provider.make_errored!("Failed to request to CloudPlatform; #{e.message}")
rescue Kubeclient::HttpError => e rescue Kubeclient::HttpError => e
...@@ -24,7 +25,10 @@ module Clusters ...@@ -24,7 +25,10 @@ module Clusters
private private
def create_gitlab_service_account! 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 end
def configure_provider def configure_provider
...@@ -44,7 +48,20 @@ module Clusters ...@@ -44,7 +48,20 @@ module Clusters
end end
def request_kubernetes_token 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 end
def authorization_type def authorization_type
......
...@@ -3,11 +3,12 @@ ...@@ -3,11 +3,12 @@
module Clusters module Clusters
module Gcp module Gcp
module Kubernetes module Kubernetes
SERVICE_ACCOUNT_NAME = 'gitlab' GITLAB_SERVICE_ACCOUNT_NAME = 'gitlab'
SERVICE_ACCOUNT_NAMESPACE = 'default' GITLAB_SERVICE_ACCOUNT_NAMESPACE = 'default'
SERVICE_ACCOUNT_TOKEN_NAME = 'gitlab-token' GITLAB_ADMIN_TOKEN_NAME = 'gitlab-token'
CLUSTER_ROLE_BINDING_NAME = 'gitlab-admin' GITLAB_CLUSTER_ROLE_BINDING_NAME = 'gitlab-admin'
CLUSTER_ROLE_NAME = 'cluster-admin' GITLAB_CLUSTER_ROLE_NAME = 'cluster-admin'
PROJECT_CLUSTER_ROLE_NAME = 'edit'
end end
end end
end end
# 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
...@@ -4,46 +4,96 @@ module Clusters ...@@ -4,46 +4,96 @@ module Clusters
module Gcp module Gcp
module Kubernetes module Kubernetes
class CreateServiceAccountService class CreateServiceAccountService
attr_reader :kubeclient, :rbac def initialize(kubeclient, service_account_name:, service_account_namespace:, token_name:, rbac:, namespace_creator: false, role_binding_name: nil)
def initialize(kubeclient, rbac:)
@kubeclient = kubeclient @kubeclient = kubeclient
@service_account_name = service_account_name
@service_account_namespace = service_account_namespace
@token_name = token_name
@rbac = rbac @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 end
def execute def execute
ensure_project_namespace_exists if namespace_creator
kubeclient.create_service_account(service_account_resource) kubeclient.create_service_account(service_account_resource)
kubeclient.create_secret(service_account_token_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 end
private 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 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 end
def service_account_token_resource def service_account_token_resource
Gitlab::Kubernetes::ServiceAccountToken.new( 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 end
def cluster_role_binding_resource def cluster_role_binding_resource
subjects = [{ kind: 'ServiceAccount', name: service_account_name, namespace: service_account_namespace }] subjects = [{ kind: 'ServiceAccount', name: service_account_name, namespace: service_account_namespace }]
Gitlab::Kubernetes::ClusterRoleBinding.new( Gitlab::Kubernetes::ClusterRoleBinding.new(
CLUSTER_ROLE_BINDING_NAME, Clusters::Gcp::Kubernetes::GITLAB_CLUSTER_ROLE_BINDING_NAME,
CLUSTER_ROLE_NAME, Clusters::Gcp::Kubernetes::GITLAB_CLUSTER_ROLE_NAME,
subjects subjects
).generate ).generate
end end
def service_account_name def role_binding_resource
SERVICE_ACCOUNT_NAME Gitlab::Kubernetes::RoleBinding.new(
end name: role_binding_name,
role_name: Clusters::Gcp::Kubernetes::PROJECT_CLUSTER_ROLE_NAME,
def service_account_namespace namespace: service_account_namespace,
SERVICE_ACCOUNT_NAMESPACE service_account_name: service_account_name
).generate
end end
end end
end end
......
...@@ -4,10 +4,12 @@ module Clusters ...@@ -4,10 +4,12 @@ module Clusters
module Gcp module Gcp
module Kubernetes module Kubernetes
class FetchKubernetesTokenService 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 @kubeclient = kubeclient
@service_account_token_name = service_account_token_name
@namespace = namespace
end end
def execute def execute
...@@ -18,7 +20,7 @@ module Clusters ...@@ -18,7 +20,7 @@ module Clusters
private private
def get_secret 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 rescue Kubeclient::HttpError => err
raise err unless err.error_code == 404 raise err unless err.error_code == 404
......
...@@ -3,6 +3,14 @@ ...@@ -3,6 +3,14 @@
class IssuableBaseService < BaseService class IssuableBaseService < BaseService
private 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) def filter_params(issuable)
ability_name = :"admin_#{issuable.to_ability_name}" ability_name = :"admin_#{issuable.to_ability_name}"
......
...@@ -48,6 +48,8 @@ module Issues ...@@ -48,6 +48,8 @@ module Issues
notification_service.async.relabeled_issue(issue, added_labels, current_user) notification_service.async.relabeled_issue(issue, added_labels, current_user)
end end
handle_milestone_change(issue)
added_mentions = issue.mentioned_users - old_mentioned_users added_mentions = issue.mentioned_users - old_mentioned_users
if added_mentions.present? if added_mentions.present?
...@@ -91,6 +93,18 @@ module Issues ...@@ -91,6 +93,18 @@ module Issues
private 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 # rubocop: disable CodeReuse/ActiveRecord
def get_issue_if_allowed(id, board_group_id = nil) def get_issue_if_allowed(id, board_group_id = nil)
return unless id return unless id
......
...@@ -87,11 +87,8 @@ module MergeRequests ...@@ -87,11 +87,8 @@ module MergeRequests
filter_merge_requests(merge_requests).each do |merge_request| filter_merge_requests(merge_requests).each do |merge_request|
if branch_and_project_match?(merge_request) || @push.force_push? if branch_and_project_match?(merge_request) || @push.force_push?
merge_request.reload_diff(current_user) merge_request.reload_diff(current_user)
else elsif merge_request.includes_any_commits?(push_commit_ids)
mr_commit_ids = merge_request.commit_shas merge_request.reload_diff(current_user)
push_commit_ids = @commits.map(&:id)
matches = mr_commit_ids & push_commit_ids
merge_request.reload_diff(current_user) if matches.any?
end end
merge_request.mark_as_unchecked merge_request.mark_as_unchecked
...@@ -104,6 +101,10 @@ module MergeRequests ...@@ -104,6 +101,10 @@ module MergeRequests
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
def push_commit_ids
@push_commit_ids ||= @commits.map(&:id)
end
def branch_and_project_match?(merge_request) def branch_and_project_match?(merge_request)
merge_request.source_project == @project && merge_request.source_project == @project &&
merge_request.source_branch == @push.branch_name merge_request.source_branch == @push.branch_name
......
...@@ -36,7 +36,10 @@ module MergeRequests ...@@ -36,7 +36,10 @@ module MergeRequests
# Remove cache for all diffs on this MR. Do not use the association on the # 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 # model, as that will interfere with other actions happening when
# reloading the diff. # 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 next if merge_request_diff == new_diff
cacheable_collection(merge_request_diff).clear_cache cacheable_collection(merge_request_diff).clear_cache
......
...@@ -58,6 +58,8 @@ module MergeRequests ...@@ -58,6 +58,8 @@ module MergeRequests
merge_request.mark_as_unchecked merge_request.mark_as_unchecked
end end
handle_milestone_change(merge_request)
added_labels = merge_request.labels - old_labels added_labels = merge_request.labels - old_labels
if added_labels.present? if added_labels.present?
notification_service.async.relabeled_merge_request( notification_service.async.relabeled_merge_request(
...@@ -105,6 +107,18 @@ module MergeRequests ...@@ -105,6 +107,18 @@ module MergeRequests
private 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) def create_branch_change_note(issuable, branch_type, old_branch, new_branch)
SystemNoteService.change_branch( SystemNoteService.change_branch(
issuable, issuable.project, current_user, branch_type, issuable, issuable.project, current_user, branch_type,
......
...@@ -4,7 +4,7 @@ module Milestones ...@@ -4,7 +4,7 @@ module Milestones
class DestroyService < Milestones::BaseService class DestroyService < Milestones::BaseService
def execute(milestone) def execute(milestone)
Milestone.transaction do Milestone.transaction do
update_params = { milestone: nil } update_params = { milestone: nil, skip_milestone_email: true }
milestone.issues.each do |issue| milestone.issues.each do |issue|
Issues::UpdateService.new(parent, current_user, update_params).execute(issue) Issues::UpdateService.new(parent, current_user, update_params).execute(issue)
......
...@@ -129,6 +129,14 @@ class NotificationService ...@@ -129,6 +129,14 @@ class NotificationService
relabeled_resource_email(issue, added_labels, current_user, :relabeled_issue_email) relabeled_resource_email(issue, added_labels, current_user, :relabeled_issue_email)
end 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: # When create a merge request we should send an email to:
# #
# * mr author # * mr author
...@@ -138,7 +146,6 @@ class NotificationService ...@@ -138,7 +146,6 @@ class NotificationService
# * users with custom level checked with "new merge request" # * users with custom level checked with "new merge request"
# #
# In EE, approvers of the merge request are also included # In EE, approvers of the merge request are also included
#
def new_merge_request(merge_request, current_user) def new_merge_request(merge_request, current_user)
new_resource_email(merge_request, :new_merge_request_email) new_resource_email(merge_request, :new_merge_request_email)
end end
...@@ -208,6 +215,14 @@ class NotificationService ...@@ -208,6 +215,14 @@ class NotificationService
relabeled_resource_email(merge_request, added_labels, current_user, :relabeled_merge_request_email) relabeled_resource_email(merge_request, added_labels, current_user, :relabeled_merge_request_email)
end 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) def close_mr(merge_request, current_user)
close_resource_email(merge_request, current_user, :closed_merge_request_email) close_resource_email(merge_request, current_user, :closed_merge_request_email)
end end
...@@ -500,6 +515,30 @@ class NotificationService ...@@ -500,6 +515,30 @@ class NotificationService
end end
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) def reopen_resource_email(target, current_user, method, status)
recipients = NotificationRecipientService.build_recipients(target, current_user, action: "reopen") recipients = NotificationRecipientService.build_recipients(target, current_user, action: "reopen")
......
...@@ -12,4 +12,4 @@ ...@@ -12,4 +12,4 @@
= s_('ClusterIntegration|Remove Kubernetes cluster integration') = s_('ClusterIntegration|Remove Kubernetes cluster integration')
%p %p
= s_("ClusterIntegration|Remove this Kubernetes cluster's configuration from this project. This will not delete your actual Kubernetes cluster.") = 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.")})
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
.table-section.section-30 .table-section.section-30
.table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Kubernetes cluster") .table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Kubernetes cluster")
.table-mobile-content .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-section.section-30
.table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Environment scope") .table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Environment scope")
.table-mobile-content= cluster.environment_scope .table-mobile-content= cluster.environment_scope
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
class: "#{'is-checked' if cluster.enabled?} #{'is-disabled' if !cluster.can_toggle_cluster?}", class: "#{'is-checked' if cluster.enabled?} #{'is-disabled' if !cluster.can_toggle_cluster?}",
"aria-label": s_("ClusterIntegration|Toggle Kubernetes Cluster"), "aria-label": s_("ClusterIntegration|Toggle Kubernetes Cluster"),
disabled: !cluster.can_toggle_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? } %input.js-project-feature-toggle-input{ type: "hidden", value: cluster.enabled? }
= icon("spinner spin", class: "loading-icon") = icon("spinner spin", class: "loading-icon")
%span.toggle-icon %span.toggle-icon
......
...@@ -7,6 +7,6 @@ ...@@ -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') - 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} %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 .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'
= 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_errors(@cluster)
.form-group .form-group
%h5= s_('ClusterIntegration|Integration status') %h5= s_('ClusterIntegration|Integration status')
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
= sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked') = 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.') .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 .form-group
%h5= s_('ClusterIntegration|Environment scope') %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') = field.text_field :environment_scope, class: 'col-md-6 form-control js-select-on-focus', placeholder: s_('ClusterIntegration|Environment scope')
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
.form-group .form-group
= field.submit _('Save changes'), class: 'btn btn-success' = field.submit _('Save changes'), class: 'btn btn-success'
- unless has_multiple_clusters?(@project) - unless has_multiple_clusters?
%h5= s_('ClusterIntegration|Environment scope') %h5= s_('ClusterIntegration|Environment scope')
%p %p
%code * %code *
......
...@@ -12,14 +12,14 @@ ...@@ -12,14 +12,14 @@
%p= link_to('Select a different Google account', @authorize_url) %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_errors(@gcp_cluster)
.form-group .form-group
= field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-bold' = field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-bold'
= field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name') = field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name')
.form-group .form-group
= field.label :environment_scope, s_('ClusterIntegration|Environment scope'), class: 'label-bold' = 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| = field.fields_for :provider_gcp, @gcp_cluster.provider_gcp do |provider_gcp_field|
.form-group .form-group
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
%span.input-group-append %span.input-group-append
= clipboard_button(text: @cluster.name, title: s_('ClusterIntegration|Copy Kubernetes cluster name'), class: 'input-group-text btn-default') = 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) = form_errors(@cluster)
= field.fields_for :platform_kubernetes, @cluster.platform_kubernetes do |platform_kubernetes_field| = field.fields_for :platform_kubernetes, @cluster.platform_kubernetes do |platform_kubernetes_field|
......
...@@ -19,9 +19,9 @@ ...@@ -19,9 +19,9 @@
.tab-content.gitlab-tab-content .tab-content.gitlab-tab-content
.tab-pane{ id: 'create-gcp-cluster-pane', class: active_when(active_tab == 'gcp'), role: 'tabpanel' } .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 - if @valid_gcp_token
= render 'projects/clusters/gcp/form' = render 'clusters/clusters/gcp/form'
- elsif @authorize_url - elsif @authorize_url
.signin-with-google .signin-with-google
= link_to(image_tag('auth_buttons/signin_with_google.png', width: '191px'), @authorize_url) = link_to(image_tag('auth_buttons/signin_with_google.png', width: '191px'), @authorize_url)
...@@ -32,5 +32,5 @@ ...@@ -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 } = 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' } .tab-pane{ id: 'add-user-cluster-pane', class: active_when(active_tab == 'user'), role: 'tabpanel' }
= render 'projects/clusters/user/header' = render 'clusters/clusters/user/header'
= render 'projects/clusters/user/form' = render 'clusters/clusters/user/form'
- @content_class = "limit-container-width" unless fluid_layout - @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 - breadcrumb_title @cluster.name
- page_title _("Kubernetes Cluster") - page_title _("Kubernetes Cluster")
- manage_prometheus_path = edit_project_service_path(@cluster.project, 'prometheus') if @project
- expanded = Rails.env.test? - 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, .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_helm_path: clusterable.install_applications_cluster_path(@cluster, :helm),
install_ingress_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :ingress), install_ingress_path: clusterable.install_applications_cluster_path(@cluster, :ingress),
install_prometheus_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :prometheus), install_prometheus_path: clusterable.install_applications_cluster_path(@cluster, :prometheus),
install_runner_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :runner), install_runner_path: clusterable.install_applications_cluster_path(@cluster, :runner),
install_jupyter_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :jupyter), install_jupyter_path: clusterable.install_applications_cluster_path(@cluster, :jupyter),
install_knative_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :knative), install_knative_path: clusterable.install_applications_cluster_path(@cluster, :knative),
toggle_status: @cluster.enabled? ? 'true': 'false', toggle_status: @cluster.enabled? ? 'true': 'false',
cluster_status: @cluster.status_name, cluster_status: @cluster.status_name,
cluster_status_reason: @cluster.status_reason, cluster_status_reason: @cluster.status_reason,
help_path: help_page_path('user/project/clusters/index.md', anchor: 'installing-applications'), 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_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'), 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 .js-cluster-application-notice
.flash-container .flash-container
...@@ -39,9 +40,9 @@ ...@@ -39,9 +40,9 @@
%p= s_('ClusterIntegration|See and edit the details for your Kubernetes cluster') %p= s_('ClusterIntegration|See and edit the details for your Kubernetes cluster')
.settings-content .settings-content
- if @cluster.managed? - if @cluster.managed?
= render 'projects/clusters/gcp/show' = render 'clusters/clusters/gcp/show'
- else - else
= render 'projects/clusters/user/show' = render 'clusters/clusters/user/show'
%section.settings.no-animate#js-cluster-advanced-settings{ class: ('expanded' if expanded) } %section.settings.no-animate#js-cluster-advanced-settings{ class: ('expanded' if expanded) }
.settings-header .settings-header
......
= 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_errors(@user_cluster)
.form-group .form-group
= field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-bold' = field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-bold'
= field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name') = field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name')
- if has_multiple_clusters?(@project) - if has_multiple_clusters?
.form-group .form-group
= field.label :environment_scope, s_('ClusterIntegration|Environment scope'), class: 'label-bold' = field.label :environment_scope, s_('ClusterIntegration|Environment scope'), class: 'label-bold'
= field.text_field :environment_scope, class: 'form-control', placeholder: s_('ClusterIntegration|Environment scope') = field.text_field :environment_scope, class: 'form-control', placeholder: s_('ClusterIntegration|Environment scope')
......
= 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_errors(@cluster)
.form-group .form-group
= field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-bold' = field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-bold'
......
%p
Milestone changed to
%strong= link_to(@milestone.name, @milestone_url)
Milestone changed to <%= @milestone.name %> ( <%= @milestone_url %> )
%p
Milestone changed to
%strong= link_to(@milestone.name, @milestone_url)
Milestone changed to <%= @milestone.name %> ( <%= @milestone_url %> )
...@@ -19,13 +19,13 @@ ...@@ -19,13 +19,13 @@
":value" => "label.id" } ":value" => "label.id" }
.dropdown .dropdown
%button.dropdown-menu-toggle.js-label-select.js-multiselect.js-issue-board-sidebar{ type: "button", %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", data: { toggle: "dropdown",
field_name: "issue[label_names][]", field_name: "issue[label_names][]",
show_no: "true", show_no: "true",
show_any: "true", show_any: "true",
project_id: @project&.try(:id), project_id: @project&.try(:id),
labels: labels_filter_path_with_defaults,
namespace_path: @namespace_path, namespace_path: @namespace_path,
project_path: @project.try(:path) } } project_path: @project.try(:path) } }
%span.dropdown-toggle-text %span.dropdown-toggle-text
......
...@@ -28,6 +28,7 @@ ...@@ -28,6 +28,7 @@
- gcp_cluster:cluster_wait_for_app_installation - gcp_cluster:cluster_wait_for_app_installation
- gcp_cluster:wait_for_cluster_creation - gcp_cluster:wait_for_cluster_creation
- gcp_cluster:cluster_wait_for_ingress_ip_address - gcp_cluster:cluster_wait_for_ingress_ip_address
- gcp_cluster:cluster_platform_configure
- github_import_advance_stage - github_import_advance_stage
- github_importer:github_import_import_diff_note - github_importer:github_import_import_diff_note
......
# 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
...@@ -9,6 +9,8 @@ class ClusterProvisionWorker ...@@ -9,6 +9,8 @@ class ClusterProvisionWorker
cluster.provider.try do |provider| cluster.provider.try do |provider|
Clusters::Gcp::ProvisionService.new.execute(provider) if cluster.gcp? Clusters::Gcp::ProvisionService.new.execute(provider) if cluster.gcp?
end end
ClusterPlatformConfigureWorker.perform_async(cluster.id) if cluster.user?
end end
end end
end end
---
title: Adds pagination to pipelines table in merge request page
merge_request:
author:
type: performance
title: Make Issue Board sidebar show project-specific labels based on selected Issue
merge_request: 22475
author:
type: fixed
---
title: Extend RBAC by having a service account restricted to project's namespace
merge_request: 22011
author:
type: other
---
title: Fix bug when links in tabs of the labels index pages ends with .html
merge_request: 22716
author:
type: fixed
---
title: Add index to find stuck merge requests.
merge_request: 22749
author:
type: performance
---
title: Add scheduled flag to job entity
merge_request: 22710
author:
type: other
---
title: Add email for milestone change
merge_request: 22279
author:
type: added
---
title: Remove gitlab-ui's progress bar from global
merge_request:
author:
type: performance
---
title: Change HELM_HOST in Auto-DevOps template to work behind proxy
merge_request: 22596
author: Sergej Nikolaev <kinolaev@gmail.com>
type: fixed
---
title: Remove `ci_enable_scheduled_build` feature flag
merge_request: 22742
author:
type: other
---
title: Replace tooltip in markdown component with gl-tooltip
merge_request: 21989
author: George Tsiolis
type: other
---
title: Monkey kubeclient to not follow any redirects.
merge_request:
author:
type: security
---
title: Optimize merge request refresh by using the database to check commit SHAs
merge_request: 22731
author:
type: performance
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment