Merge branch 'master' into triggermesh-phase1-knative
This commit is contained in:
commit
aa44393e8f
165 changed files with 2373 additions and 842 deletions
21
CHANGELOG.md
21
CHANGELOG.md
|
@ -2,6 +2,13 @@
|
|||
documentation](doc/development/changelog.md) for instructions on adding your own
|
||||
entry.
|
||||
|
||||
## 11.4.4 (2018-10-30)
|
||||
|
||||
### Security (1 change)
|
||||
|
||||
- Monkey kubeclient to not follow any redirects.
|
||||
|
||||
|
||||
## 11.4.3 (2018-10-26)
|
||||
|
||||
- No changes.
|
||||
|
@ -250,6 +257,13 @@ entry.
|
|||
- 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)
|
||||
|
||||
- No changes.
|
||||
|
@ -555,6 +569,13 @@ entry.
|
|||
- 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)
|
||||
|
||||
- No changes.
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
import initGkeDropdowns from '~/projects/gke_cluster_dropdowns';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initGkeDropdowns();
|
||||
});
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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) {
|
||||
|
|
28
app/controllers/clusters/applications_controller.rb
Normal file
28
app/controllers/clusters/applications_controller.rb
Normal file
|
@ -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
|
37
app/controllers/clusters/base_controller.rb
Normal file
37
app/controllers/clusters/base_controller.rb
Normal file
|
@ -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
|
218
app/controllers/clusters/clusters_controller.rb
Normal file
218
app/controllers/clusters/clusters_controller.rb
Normal file
|
@ -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
|
10
app/controllers/concerns/project_unauthorized.rb
Normal file
10
app/controllers/concerns/project_unauthorized.rb
Normal file
|
@ -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
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
def clusterable
|
||||
@clusterable ||= ClusterablePresenter.fabricate(project, 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
|
||||
def project
|
||||
@project ||= find_routable!(Project, File.join(params[:namespace_id], params[:project_id]), not_found_or_authorized_proc: project_unauthorized_proc)
|
||||
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
|
||||
|
||||
def authorize_admin_cluster!
|
||||
access_denied! unless can?(current_user, :admin_cluster, cluster)
|
||||
end
|
||||
|
||||
def update_applications_status
|
||||
@cluster.applications.each(&:schedule_status_update)
|
||||
def repository
|
||||
@repository ||= project.repository
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 set_namespace_and_service_account_to_default
|
||||
self.namespace ||= default_namespace
|
||||
self.service_account_name ||= default_service_account_name
|
||||
def configure_predefined_credentials
|
||||
self.namespace = kubernetes_or_project_namespace
|
||||
self.service_account_name = default_service_account_name
|
||||
end
|
||||
|
||||
def default_namespace
|
||||
def predefined_variables
|
||||
config = YAML.dump(kubeconfig)
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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|
|
||||
|
|
|
@ -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.
|
||||
|
|
46
app/presenters/clusterable_presenter.rb
Normal file
46
app/presenters/clusterable_presenter.rb
Normal file
|
@ -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
|
|
@ -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
|
||||
|
|
15
app/presenters/project_clusterable_presenter.rb
Normal file
15
app/presenters/project_clusterable_presenter.rb
Normal file
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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}"
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -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.")})
|
|
@ -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
|
|
@ -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'
|
|
@ -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 *
|
|
@ -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
|
|
@ -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|
|
|
@ -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'
|
|
@ -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
|
|
@ -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')
|
|
@ -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'
|
3
app/views/notify/changed_milestone_issue_email.html.haml
Normal file
3
app/views/notify/changed_milestone_issue_email.html.haml
Normal file
|
@ -0,0 +1,3 @@
|
|||
%p
|
||||
Milestone changed to
|
||||
%strong= link_to(@milestone.name, @milestone_url)
|
1
app/views/notify/changed_milestone_issue_email.text.erb
Normal file
1
app/views/notify/changed_milestone_issue_email.text.erb
Normal file
|
@ -0,0 +1 @@
|
|||
Milestone changed to <%= @milestone.name %> ( <%= @milestone_url %> )
|
|
@ -0,0 +1,3 @@
|
|||
%p
|
||||
Milestone changed to
|
||||
%strong= link_to(@milestone.name, @milestone_url)
|
|
@ -0,0 +1 @@
|
|||
Milestone changed to <%= @milestone.name %> ( <%= @milestone_url %> )
|
2
app/views/notify/removed_milestone_issue_email.html.haml
Normal file
2
app/views/notify/removed_milestone_issue_email.html.haml
Normal file
|
@ -0,0 +1,2 @@
|
|||
%p
|
||||
Milestone removed
|
1
app/views/notify/removed_milestone_issue_email.text.erb
Normal file
1
app/views/notify/removed_milestone_issue_email.text.erb
Normal file
|
@ -0,0 +1 @@
|
|||
Milestone removed
|
|
@ -0,0 +1,2 @@
|
|||
%p
|
||||
Milestone removed
|
|
@ -0,0 +1 @@
|
|||
Milestone removed
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
22
app/workers/cluster_platform_configure_worker.rb
Normal file
22
app/workers/cluster_platform_configure_worker.rb
Normal file
|
@ -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
|
|
@ -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
|
||||
|
|
5
changelogs/unreleased/28249-add-pagination.yml
Normal file
5
changelogs/unreleased/28249-add-pagination.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Adds pagination to pipelines table in merge request page
|
||||
merge_request:
|
||||
author:
|
||||
type: performance
|
|
@ -0,0 +1,4 @@
|
|||
title: Make Issue Board sidebar show project-specific labels based on selected Issue
|
||||
merge_request: 22475
|
||||
author:
|
||||
type: fixed
|
5
changelogs/unreleased/51716-create-kube-namespace.yml
Normal file
5
changelogs/unreleased/51716-create-kube-namespace.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Extend RBAC by having a service account restricted to project's namespace
|
||||
merge_request: 22011
|
||||
author:
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix bug when links in tabs of the labels index pages ends with .html
|
||||
merge_request: 22716
|
||||
author:
|
||||
type: fixed
|
5
changelogs/unreleased/ab-45608-stuck-mr-query.yml
Normal file
5
changelogs/unreleased/ab-45608-stuck-mr-query.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add index to find stuck merge requests.
|
||||
merge_request: 22749
|
||||
author:
|
||||
type: performance
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add scheduled flag to job entity
|
||||
merge_request: 22710
|
||||
author:
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add email for milestone change
|
||||
merge_request: 22279
|
||||
author:
|
||||
type: added
|
5
changelogs/unreleased/gl-ui-progress-bar.yml
Normal file
5
changelogs/unreleased/gl-ui-progress-bar.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Remove gitlab-ui's progress bar from global
|
||||
merge_request:
|
||||
author:
|
||||
type: performance
|
5
changelogs/unreleased/kinolaev-master-patch-91872.yml
Normal file
5
changelogs/unreleased/kinolaev-master-patch-91872.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Change HELM_HOST in Auto-DevOps template to work behind proxy
|
||||
merge_request: 22596
|
||||
author: Sergej Nikolaev <kinolaev@gmail.com>
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Remove `ci_enable_scheduled_build` feature flag
|
||||
merge_request: 22742
|
||||
author:
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Replace tooltip in markdown component with gl-tooltip
|
||||
merge_request: 21989
|
||||
author: George Tsiolis
|
||||
type: other
|
5
changelogs/unreleased/security-kubeclient-ssrf.yml
Normal file
5
changelogs/unreleased/security-kubeclient-ssrf.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Monkey kubeclient to not follow any redirects.
|
||||
merge_request:
|
||||
author:
|
||||
type: security
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Optimize merge request refresh by using the database to check commit SHAs
|
||||
merge_request: 22731
|
||||
author:
|
||||
type: performance
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue