From cf0265723479b2e511595312f8d053f737fa3d57 Mon Sep 17 00:00:00 2001 From: Fatih Acet Date: Fri, 12 Apr 2019 22:23:49 +0200 Subject: [PATCH 01/73] Delete leftover code for referenced_merge_requests We rewrote Related MRs widget using Vue. The previous implementation was using Haml templates and calling referenced_merge_requests endpoint which is now deprecated. This MR deletes leftover stuff them. --- app/controllers/projects/issues_controller.rb | 12 -------- .../issues/_merge_requests_status.html.haml | 25 ----------------- config/routes/project.rb | 1 - spec/javascripts/issue_spec.js | 1 - .../_merge_requests_status.html.haml_spec.rb | 28 ------------------- 5 files changed, 67 deletions(-) delete mode 100644 app/views/projects/issues/_merge_requests_status.html.haml delete mode 100644 spec/views/projects/issues/_merge_requests_status.html.haml_spec.rb diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 3d16a368f23..87c224c3c62 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -132,18 +132,6 @@ class Projects::IssuesController < Projects::ApplicationController render_conflict_response end - def referenced_merge_requests - @merge_requests, @closed_by_merge_requests = ::Issues::ReferencedMergeRequestsService.new(project, current_user).execute(issue) - - respond_to do |format| - format.json do - render json: { - html: view_to_html_string('projects/issues/_merge_requests') - } - end - end - end - def related_branches @related_branches = Issues::RelatedBranchesService.new(project, current_user).execute(issue) diff --git a/app/views/projects/issues/_merge_requests_status.html.haml b/app/views/projects/issues/_merge_requests_status.html.haml deleted file mode 100644 index 90838a75214..00000000000 --- a/app/views/projects/issues/_merge_requests_status.html.haml +++ /dev/null @@ -1,25 +0,0 @@ -- time_format = '%b %e, %Y %l:%M%P %Z%z' - -- if merge_request.merged? - - mr_status_date = merge_request.merged_at - - mr_status_title = _('Merged') - - mr_status_icon = 'merge' - - mr_status_class = 'merged' -- elsif merge_request.closed? - - mr_status_date = merge_request.closed_event&.created_at - - mr_status_title = _('Closed') - - mr_status_icon = 'issue-close' - - mr_status_class = 'closed' -- else - - mr_status_date = merge_request.created_at - - mr_status_title = mr_status_date ? _('Opened') : _('Open') - - mr_status_icon = 'issue-open-m' - - mr_status_class = 'open' - -- if mr_status_date - - mr_status_tooltip = "
#{mr_status_title} #{time_ago_in_words(mr_status_date)} ago
#{l(mr_status_date.to_time, format: time_format)}" -- else - - mr_status_tooltip = "
#{mr_status_title}
" - -%span.mr-status-wrapper.suggestion-help-hover{ class: css_class, data: { toggle: 'tooltip', placement: 'bottom', html: 'true', title: mr_status_tooltip } } - = sprite_icon(mr_status_icon, size: 16, css_class: "merge-request-status #{mr_status_class}") diff --git a/config/routes/project.rb b/config/routes/project.rb index 93d168fc595..e05c887aac0 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -360,7 +360,6 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do post :toggle_subscription post :mark_as_spam post :move - get :referenced_merge_requests get :related_branches get :can_create_branch get :realtime_changes diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js index 11ab6c38a55..966aee72abb 100644 --- a/spec/javascripts/issue_spec.js +++ b/spec/javascripts/issue_spec.js @@ -113,7 +113,6 @@ describe('Issue', function() { mock = new MockAdapter(axios); mock.onGet(/(.*)\/related_branches$/).reply(200, {}); - mock.onGet(/(.*)\/referenced_merge_requests$/).reply(200, {}); findElements(isIssueInitiallyOpen); this.issue = new Issue(); diff --git a/spec/views/projects/issues/_merge_requests_status.html.haml_spec.rb b/spec/views/projects/issues/_merge_requests_status.html.haml_spec.rb deleted file mode 100644 index 9424795749d..00000000000 --- a/spec/views/projects/issues/_merge_requests_status.html.haml_spec.rb +++ /dev/null @@ -1,28 +0,0 @@ -# frozen_string_literal: true -require 'spec_helper' - -describe 'projects/issues/_merge_requests_status.html.haml' do - around do |ex| - Timecop.freeze(Date.new(2018, 7, 22)) do - ex.run - end - end - - it 'shows date of status change in tooltip' do - merge_request = create(:merge_request, created_at: 1.month.ago) - - render partial: 'projects/issues/merge_requests_status', - locals: { merge_request: merge_request, css_class: '' } - - expect(rendered).to match("Opened.*about 1 month ago") - end - - it 'shows only status in tooltip if date is not set' do - merge_request = create(:merge_request, state: :closed) - - render partial: 'projects/issues/merge_requests_status', - locals: { merge_request: merge_request, css_class: '' } - - expect(rendered).to match("Closed") - end -end From 303ec92ab92942e067f8bc95de27e85b579580c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C5=82gorzata=20Ksionek?= Date: Tue, 16 Apr 2019 10:20:57 +0200 Subject: [PATCH 02/73] Update projects controller --- app/controllers/projects_controller.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 48f4d7a586d..e88c46144ef 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -36,10 +36,10 @@ class ProjectsController < Projects::ApplicationController # rubocop: disable CodeReuse/ActiveRecord def new - namespace = Namespace.find_by(id: params[:namespace_id]) if params[:namespace_id] - return access_denied! if namespace && !can?(current_user, :create_projects, namespace) + @namespace = Namespace.find_by(id: params[:namespace_id]) if params[:namespace_id] + return access_denied! if @namespace && !can?(current_user, :create_projects, @namespace) - @project = Project.new(namespace_id: namespace&.id) + @project = Project.new(namespace_id: @namespace&.id) end # rubocop: enable CodeReuse/ActiveRecord From f5c7c3b9ae8b91662830f46b595ce1512050be89 Mon Sep 17 00:00:00 2001 From: Brett Walker Date: Wed, 17 Apr 2019 18:43:19 -0500 Subject: [PATCH 03/73] Basic GraphQL for a group Add new query for Groups, with new GroupType and NamespaceType --- app/graphql/resolvers/full_path_resolver.rb | 6 +- app/graphql/resolvers/group_resolver.rb | 13 ++ app/graphql/types/group_type.rb | 21 ++ app/graphql/types/namespace_type.rb | 19 ++ app/graphql/types/permission_types/group.rb | 11 + app/graphql/types/project_type.rb | 3 + app/graphql/types/query_type.rb | 5 + .../unreleased/bw-add-graphql-groups.yml | 5 + spec/graphql/types/group_type_spec.rb | 11 + spec/graphql/types/namespace_type.rb | 7 + spec/graphql/types/query_type_spec.rb | 2 +- spec/requests/api/graphql/group_query_spec.rb | 197 ++++++++++++++++++ 12 files changed, 296 insertions(+), 4 deletions(-) create mode 100644 app/graphql/resolvers/group_resolver.rb create mode 100644 app/graphql/types/group_type.rb create mode 100644 app/graphql/types/namespace_type.rb create mode 100644 app/graphql/types/permission_types/group.rb create mode 100644 changelogs/unreleased/bw-add-graphql-groups.yml create mode 100644 spec/graphql/types/group_type_spec.rb create mode 100644 spec/graphql/types/namespace_type.rb create mode 100644 spec/requests/api/graphql/group_query_spec.rb diff --git a/app/graphql/resolvers/full_path_resolver.rb b/app/graphql/resolvers/full_path_resolver.rb index 0f1a64b6c58..972f318c806 100644 --- a/app/graphql/resolvers/full_path_resolver.rb +++ b/app/graphql/resolvers/full_path_resolver.rb @@ -7,14 +7,14 @@ module Resolvers prepended do argument :full_path, GraphQL::ID_TYPE, required: true, - description: 'The full path of the project or namespace, e.g., "gitlab-org/gitlab-ce"' + description: 'The full path of the project, group or namespace, e.g., "gitlab-org/gitlab-ce"' end def model_by_full_path(model, full_path) BatchLoader.for(full_path).batch(key: model) do |full_paths, loader, args| # `with_route` avoids an N+1 calculating full_path - args[:key].where_full_path_in(full_paths).with_route.each do |project| - loader.call(project.full_path, project) + args[:key].where_full_path_in(full_paths).with_route.each do |model_instance| + loader.call(model_instance.full_path, model_instance) end end end diff --git a/app/graphql/resolvers/group_resolver.rb b/app/graphql/resolvers/group_resolver.rb new file mode 100644 index 00000000000..4260e18829e --- /dev/null +++ b/app/graphql/resolvers/group_resolver.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Resolvers + class GroupResolver < BaseResolver + prepend FullPathResolver + + type Types::GroupType, null: true + + def resolve(full_path:) + model_by_full_path(Group, full_path) + end + end +end diff --git a/app/graphql/types/group_type.rb b/app/graphql/types/group_type.rb new file mode 100644 index 00000000000..a2d615ee732 --- /dev/null +++ b/app/graphql/types/group_type.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Types + class GroupType < NamespaceType + graphql_name 'Group' + + authorize :read_group + + expose_permissions Types::PermissionTypes::Group + + field :web_url, GraphQL::STRING_TYPE, null: true + + field :avatar_url, GraphQL::STRING_TYPE, null: true, resolve: -> (group, args, ctx) do + group.avatar_url(only_path: false) + end + + if ::Group.supports_nested_objects? + field :parent, GroupType, null: true + end + end +end diff --git a/app/graphql/types/namespace_type.rb b/app/graphql/types/namespace_type.rb new file mode 100644 index 00000000000..b1c5da50aa5 --- /dev/null +++ b/app/graphql/types/namespace_type.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Types + class NamespaceType < BaseObject + graphql_name 'Namespace' + + field :id, GraphQL::ID_TYPE, null: false + field :name, GraphQL::STRING_TYPE, null: false + + field :path, GraphQL::STRING_TYPE, null: false + field :description, GraphQL::STRING_TYPE, null: true + field :visibility, GraphQL::STRING_TYPE, null: true + field :lfs_enabled, GraphQL::BOOLEAN_TYPE, null: true, method: :lfs_enabled? + + field :request_access_enabled, GraphQL::BOOLEAN_TYPE, null: true + field :full_path, GraphQL::ID_TYPE, null: false + field :full_name, GraphQL::STRING_TYPE, null: false + end +end diff --git a/app/graphql/types/permission_types/group.rb b/app/graphql/types/permission_types/group.rb new file mode 100644 index 00000000000..29833993ce6 --- /dev/null +++ b/app/graphql/types/permission_types/group.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Types + module PermissionTypes + class Group < BasePermissionType + graphql_name 'GroupPermissions' + + abilities :read_group + end + end +end diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb index fbb4eddd13c..baea6658e05 100644 --- a/app/graphql/types/project_type.rb +++ b/app/graphql/types/project_type.rb @@ -66,6 +66,9 @@ module Types field :only_allow_merge_if_all_discussions_are_resolved, GraphQL::BOOLEAN_TYPE, null: true field :printing_merge_request_link_enabled, GraphQL::BOOLEAN_TYPE, null: true + field :namespace, Types::NamespaceType, null: false + field :group, Types::GroupType, null: true + field :merge_requests, Types::MergeRequestType.connection_type, null: true, diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb index 0f655ab9d03..40d7de1a49a 100644 --- a/app/graphql/types/query_type.rb +++ b/app/graphql/types/query_type.rb @@ -9,6 +9,11 @@ module Types resolver: Resolvers::ProjectResolver, description: "Find a project" + field :group, Types::GroupType, + null: true, + resolver: Resolvers::GroupResolver, + description: "Find a group" + field :metadata, Types::MetadataType, null: true, resolver: Resolvers::MetadataResolver, diff --git a/changelogs/unreleased/bw-add-graphql-groups.yml b/changelogs/unreleased/bw-add-graphql-groups.yml new file mode 100644 index 00000000000..f72ee1cf2b7 --- /dev/null +++ b/changelogs/unreleased/bw-add-graphql-groups.yml @@ -0,0 +1,5 @@ +--- +title: Add initial GraphQL query for Groups +merge_request: 27492 +author: +type: added diff --git a/spec/graphql/types/group_type_spec.rb b/spec/graphql/types/group_type_spec.rb new file mode 100644 index 00000000000..3dd5b602aa2 --- /dev/null +++ b/spec/graphql/types/group_type_spec.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe GitlabSchema.types['Group'] do + it { expect(described_class).to expose_permissions_using(Types::PermissionTypes::Group) } + + it { expect(described_class.graphql_name).to eq('Group') } + + it { expect(described_class).to require_graphql_authorizations(:read_group) } +end diff --git a/spec/graphql/types/namespace_type.rb b/spec/graphql/types/namespace_type.rb new file mode 100644 index 00000000000..7cd6a79ae5d --- /dev/null +++ b/spec/graphql/types/namespace_type.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe GitlabSchema.types['Namespace'] do + it { expect(described_class.graphql_name).to eq('Namespace') } +end diff --git a/spec/graphql/types/query_type_spec.rb b/spec/graphql/types/query_type_spec.rb index 69e3ea8a4a9..b4626955816 100644 --- a/spec/graphql/types/query_type_spec.rb +++ b/spec/graphql/types/query_type_spec.rb @@ -5,7 +5,7 @@ describe GitlabSchema.types['Query'] do expect(described_class.graphql_name).to eq('Query') end - it { is_expected.to have_graphql_fields(:project, :echo, :metadata) } + it { is_expected.to have_graphql_fields(:project, :group, :echo, :metadata) } describe 'project field' do subject { described_class.fields['project'] } diff --git a/spec/requests/api/graphql/group_query_spec.rb b/spec/requests/api/graphql/group_query_spec.rb new file mode 100644 index 00000000000..e4f559e9f10 --- /dev/null +++ b/spec/requests/api/graphql/group_query_spec.rb @@ -0,0 +1,197 @@ +require 'spec_helper' + +# Based on spec/requests/api/groups_spec.rb +# Should follow closely in order to ensure all situations are covered +describe 'getting group information' do + include GraphqlHelpers + include UploadHelpers + + let(:user1) { create(:user, can_create_group: false) } + let(:user2) { create(:user) } + let(:admin) { create(:admin) } + let!(:group1) { create(:group, avatar: File.open(uploaded_image_temp_path)) } + let!(:group2) { create(:group, :private) } + # let!(:project1) { create(:project, namespace: group1) } + # let!(:project2) { create(:project, namespace: group2) } + # let!(:project3) { create(:project, namespace: group1, path: 'test', visibility_level: Gitlab::VisibilityLevel::PRIVATE) } + + before do + group1.add_owner(user1) + group2.add_owner(user2) + end + + # similar to the API "GET /groups/:id" + describe "Query group(fullPath)" do + # Given a group, create one project for each visibility level + # + # group - Group to add projects to + # share_with - If provided, each project will be shared with this Group + # + # Returns a Hash of visibility_level => Project pairs + def add_projects_to_group(group, share_with: nil) + projects = { + public: create(:project, :public, namespace: group), + internal: create(:project, :internal, namespace: group), + private: create(:project, :private, namespace: group) + } + + if share_with + create(:project_group_link, project: projects[:public], group: share_with) + create(:project_group_link, project: projects[:internal], group: share_with) + create(:project_group_link, project: projects[:private], group: share_with) + end + + projects + end + + def response_project_ids(json_response, key) + json_response[key].map do |project| + project['id'].to_i + end + end + + def group_query(group) + graphql_query_for('group', 'fullPath' => group.full_path) + end + + it_behaves_like 'a working graphql query' do + before do + post_graphql(group_query(group1)) + end + end + + context 'when unauthenticated' do + it 'returns nil for a private group' do + post_graphql(group_query(group2)) + + expect(graphql_data['group']).to be_nil + end + + it 'returns a public group' do + post_graphql(group_query(group1)) + + expect(graphql_data['group']).not_to be_nil + end + + # it 'returns only public projects in the group' do + # public_group = create(:group, :public) + # projects = add_projects_to_group(public_group) + # + # get api("/groups/#{public_group.id}") + # + # expect(response_project_ids(json_response, 'projects')) + # .to contain_exactly(projects[:public].id) + # end + + # it 'returns only public projects shared with the group' do + # public_group = create(:group, :public) + # projects = add_projects_to_group(public_group, share_with: group1) + # + # get api("/groups/#{group1.id}") + # + # expect(response_project_ids(json_response, 'shared_projects')) + # .to contain_exactly(projects[:public].id) + # end + end + + context "when authenticated as user" do + it "returns one of user1's groups" do + project = create(:project, namespace: group2, path: 'Foo') + create(:project_group_link, project: project, group: group1) + + post_graphql(group_query(group1), current_user: user1) + + expect(response).to have_gitlab_http_status(200) + expect(graphql_data['group']['id']).to eq(group1.id.to_s) + expect(graphql_data['group']['name']).to eq(group1.name) + expect(graphql_data['group']['path']).to eq(group1.path) + expect(graphql_data['group']['description']).to eq(group1.description) + expect(graphql_data['group']['visibility']).to eq(Gitlab::VisibilityLevel.string_level(group1.visibility_level)) + expect(graphql_data['group']['avatarUrl']).to eq(group1.avatar_url(only_path: false)) + expect(graphql_data['group']['webUrl']).to eq(group1.web_url) + expect(graphql_data['group']['requestAccessEnabled']).to eq(group1.request_access_enabled) + expect(graphql_data['group']['fullName']).to eq(group1.full_name) + expect(graphql_data['group']['fullPath']).to eq(group1.full_path) + expect(graphql_data['group']['parentId']).to eq(group1.parent_id) + # expect(graphql_data['group']['projects']).to be_an Array + # expect(graphql_data['group']['projects'].length).to eq(2) + # expect(graphql_data['group']['sharedProjects']).to be_an Array + # expect(graphql_data['group']['sharedProjects'].length).to eq(1) + # expect(graphql_data['group']['sharedProjects'][0]['id']).to eq(project.id) + end + + # it "returns one of user1's groups without projects when with_projects option is set to false" do + # project = create(:project, namespace: group2, path: 'Foo') + # create(:project_group_link, project: project, group: group1) + # + # get api("/groups/#{group1.id}", user1), params: { with_projects: false } + # + # expect(response).to have_gitlab_http_status(200) + # expect(json_response['projects']).to be_nil + # expect(json_response['shared_projects']).to be_nil + # end + + it "does not return a non existing group" do + query = graphql_query_for('group', 'fullPath' => '1328') + post_graphql(query, current_user: user1) + + expect(graphql_data['group']).to be_nil + end + + it "does not return a group not attached to user1" do + post_graphql(group_query(group2), current_user: user1) + + expect(graphql_data['group']).to be_nil + end + + # it 'returns only public and internal projects in the group' do + # public_group = create(:group, :public) + # projects = add_projects_to_group(public_group) + # + # get api("/groups/#{public_group.id}", user2) + # + # expect(response_project_ids(json_response, 'projects')) + # .to contain_exactly(projects[:public].id, projects[:internal].id) + # end + + # it 'returns only public and internal projects shared with the group' do + # public_group = create(:group, :public) + # projects = add_projects_to_group(public_group, share_with: group1) + # + # get api("/groups/#{group1.id}", user2) + # + # expect(response_project_ids(json_response, 'shared_projects')) + # .to contain_exactly(projects[:public].id, projects[:internal].id) + # end + + it 'avoids N+1 queries' do + post_graphql(group_query(group1), current_user: admin) + + control_count = ActiveRecord::QueryRecorder.new do + post_graphql(group_query(group1), current_user: admin) + end.count + + create(:project, namespace: group1) + + expect do + post_graphql(group_query(group1), current_user: admin) + end.not_to exceed_query_limit(control_count) + end + end + + context "when authenticated as admin" do + it "returns any existing group" do + post_graphql(group_query(group2), current_user: admin) + + expect(graphql_data['group']['name']).to eq(group2.name) + end + + it "does not return a non existing group" do + query = graphql_query_for('group', 'fullPath' => '1328') + post_graphql(query, current_user: admin) + + expect(graphql_data['group']).to be_nil + end + end + end +end From d693c3e5cac0fbe5c5e28e0ade43aa7455ba4876 Mon Sep 17 00:00:00 2001 From: Brett Walker Date: Mon, 22 Apr 2019 14:46:13 -0500 Subject: [PATCH 04/73] Refactor group query spec and removing unnecessary code --- app/graphql/types/namespace_type.rb | 8 +- spec/requests/api/graphql/group_query_spec.rb | 127 ++++-------------- 2 files changed, 28 insertions(+), 107 deletions(-) diff --git a/app/graphql/types/namespace_type.rb b/app/graphql/types/namespace_type.rb index b1c5da50aa5..36d8ee8c878 100644 --- a/app/graphql/types/namespace_type.rb +++ b/app/graphql/types/namespace_type.rb @@ -5,15 +5,15 @@ module Types graphql_name 'Namespace' field :id, GraphQL::ID_TYPE, null: false - field :name, GraphQL::STRING_TYPE, null: false + field :name, GraphQL::STRING_TYPE, null: false field :path, GraphQL::STRING_TYPE, null: false + field :full_name, GraphQL::STRING_TYPE, null: false + field :full_path, GraphQL::ID_TYPE, null: false + field :description, GraphQL::STRING_TYPE, null: true field :visibility, GraphQL::STRING_TYPE, null: true field :lfs_enabled, GraphQL::BOOLEAN_TYPE, null: true, method: :lfs_enabled? - field :request_access_enabled, GraphQL::BOOLEAN_TYPE, null: true - field :full_path, GraphQL::ID_TYPE, null: false - field :full_name, GraphQL::STRING_TYPE, null: false end end diff --git a/spec/requests/api/graphql/group_query_spec.rb b/spec/requests/api/graphql/group_query_spec.rb index e4f559e9f10..8ff95cc9af2 100644 --- a/spec/requests/api/graphql/group_query_spec.rb +++ b/spec/requests/api/graphql/group_query_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' # Based on spec/requests/api/groups_spec.rb @@ -6,95 +8,47 @@ describe 'getting group information' do include GraphqlHelpers include UploadHelpers - let(:user1) { create(:user, can_create_group: false) } - let(:user2) { create(:user) } - let(:admin) { create(:admin) } - let!(:group1) { create(:group, avatar: File.open(uploaded_image_temp_path)) } - let!(:group2) { create(:group, :private) } - # let!(:project1) { create(:project, namespace: group1) } - # let!(:project2) { create(:project, namespace: group2) } - # let!(:project3) { create(:project, namespace: group1, path: 'test', visibility_level: Gitlab::VisibilityLevel::PRIVATE) } - - before do - group1.add_owner(user1) - group2.add_owner(user2) - end + let(:user1) { create(:user, can_create_group: false) } + let(:user2) { create(:user) } + let(:admin) { create(:admin) } + let(:public_group) { create(:group, :public) } + let(:private_group) { create(:group, :private) } # similar to the API "GET /groups/:id" describe "Query group(fullPath)" do - # Given a group, create one project for each visibility level - # - # group - Group to add projects to - # share_with - If provided, each project will be shared with this Group - # - # Returns a Hash of visibility_level => Project pairs - def add_projects_to_group(group, share_with: nil) - projects = { - public: create(:project, :public, namespace: group), - internal: create(:project, :internal, namespace: group), - private: create(:project, :private, namespace: group) - } - - if share_with - create(:project_group_link, project: projects[:public], group: share_with) - create(:project_group_link, project: projects[:internal], group: share_with) - create(:project_group_link, project: projects[:private], group: share_with) - end - - projects - end - - def response_project_ids(json_response, key) - json_response[key].map do |project| - project['id'].to_i - end - end - def group_query(group) graphql_query_for('group', 'fullPath' => group.full_path) end it_behaves_like 'a working graphql query' do before do - post_graphql(group_query(group1)) + post_graphql(group_query(public_group)) end end context 'when unauthenticated' do it 'returns nil for a private group' do - post_graphql(group_query(group2)) + post_graphql(group_query(private_group)) expect(graphql_data['group']).to be_nil end it 'returns a public group' do - post_graphql(group_query(group1)) + post_graphql(group_query(public_group)) expect(graphql_data['group']).not_to be_nil end - - # it 'returns only public projects in the group' do - # public_group = create(:group, :public) - # projects = add_projects_to_group(public_group) - # - # get api("/groups/#{public_group.id}") - # - # expect(response_project_ids(json_response, 'projects')) - # .to contain_exactly(projects[:public].id) - # end - - # it 'returns only public projects shared with the group' do - # public_group = create(:group, :public) - # projects = add_projects_to_group(public_group, share_with: group1) - # - # get api("/groups/#{group1.id}") - # - # expect(response_project_ids(json_response, 'shared_projects')) - # .to contain_exactly(projects[:public].id) - # end end context "when authenticated as user" do + let!(:group1) { create(:group, avatar: File.open(uploaded_image_temp_path)) } + let!(:group2) { create(:group, :private) } + + before do + group1.add_owner(user1) + group2.add_owner(user2) + end + it "returns one of user1's groups" do project = create(:project, namespace: group2, path: 'Foo') create(:project_group_link, project: project, group: group1) @@ -113,57 +67,24 @@ describe 'getting group information' do expect(graphql_data['group']['fullName']).to eq(group1.full_name) expect(graphql_data['group']['fullPath']).to eq(group1.full_path) expect(graphql_data['group']['parentId']).to eq(group1.parent_id) - # expect(graphql_data['group']['projects']).to be_an Array - # expect(graphql_data['group']['projects'].length).to eq(2) - # expect(graphql_data['group']['sharedProjects']).to be_an Array - # expect(graphql_data['group']['sharedProjects'].length).to eq(1) - # expect(graphql_data['group']['sharedProjects'][0]['id']).to eq(project.id) end - # it "returns one of user1's groups without projects when with_projects option is set to false" do - # project = create(:project, namespace: group2, path: 'Foo') - # create(:project_group_link, project: project, group: group1) - # - # get api("/groups/#{group1.id}", user1), params: { with_projects: false } - # - # expect(response).to have_gitlab_http_status(200) - # expect(json_response['projects']).to be_nil - # expect(json_response['shared_projects']).to be_nil - # end - it "does not return a non existing group" do query = graphql_query_for('group', 'fullPath' => '1328') + post_graphql(query, current_user: user1) expect(graphql_data['group']).to be_nil end it "does not return a group not attached to user1" do - post_graphql(group_query(group2), current_user: user1) + private_group.add_owner(user2) + + post_graphql(group_query(private_group), current_user: user1) expect(graphql_data['group']).to be_nil end - # it 'returns only public and internal projects in the group' do - # public_group = create(:group, :public) - # projects = add_projects_to_group(public_group) - # - # get api("/groups/#{public_group.id}", user2) - # - # expect(response_project_ids(json_response, 'projects')) - # .to contain_exactly(projects[:public].id, projects[:internal].id) - # end - - # it 'returns only public and internal projects shared with the group' do - # public_group = create(:group, :public) - # projects = add_projects_to_group(public_group, share_with: group1) - # - # get api("/groups/#{group1.id}", user2) - # - # expect(response_project_ids(json_response, 'shared_projects')) - # .to contain_exactly(projects[:public].id, projects[:internal].id) - # end - it 'avoids N+1 queries' do post_graphql(group_query(group1), current_user: admin) @@ -181,9 +102,9 @@ describe 'getting group information' do context "when authenticated as admin" do it "returns any existing group" do - post_graphql(group_query(group2), current_user: admin) + post_graphql(group_query(private_group), current_user: admin) - expect(graphql_data['group']['name']).to eq(group2.name) + expect(graphql_data['group']['name']).to eq(private_group.name) end it "does not return a non existing group" do From ff2727fc7efa92228b022ea46bace698caceced1 Mon Sep 17 00:00:00 2001 From: Brett Walker Date: Tue, 23 Apr 2019 12:24:48 -0500 Subject: [PATCH 05/73] Mention group query in GraphQL documentation --- doc/api/graphql/index.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/doc/api/graphql/index.md b/doc/api/graphql/index.md index ec48bf4940b..cf02bbd9c92 100644 --- a/doc/api/graphql/index.md +++ b/doc/api/graphql/index.md @@ -29,7 +29,11 @@ curl --data "value=100" --header "PRIVATE-TOKEN: " https://gi ## Available queries -A first iteration of a GraphQL API includes a query for: `project`. Within a project it is also possible to fetch a `mergeRequest` by IID. +A first iteration of a GraphQL API includes the following queries + +1. `project` : Within a project it is also possible to fetch a `mergeRequest` by IID. + +1. `group` : Only basic group information is currently supported. ## GraphiQL From 7b0ee6464de2b40f72885a3f0a00d89dbc6db2cd Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Wed, 24 Apr 2019 11:00:23 +0100 Subject: [PATCH 06/73] Clarify that reporters can manage boards The original issue didn't specify this, but the code always allowed reporters to do everything here. I think that's correct because 1. Developer permissions normally refer to code, and boards are to do with issues. 2. Reporters can manage epics, as well as labels on issues, which are in a similar space. --- doc/user/project/issue_board.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md index ad47b848bea..31020de5208 100644 --- a/doc/user/project/issue_board.md +++ b/doc/user/project/issue_board.md @@ -151,7 +151,7 @@ Create lists for each of your team members and quickly drag-and-drop issues onto ## Permissions -[Developers and up](../permissions.md) can use all the functionality of the +[Reporters and up](../permissions.md) can use all the functionality of the Issue Board, that is, create or delete lists and drag issues from one list to another. ## GitLab Enterprise features for Issue Boards From f48605c796bf9098370a258e00fce41f1770ff74 Mon Sep 17 00:00:00 2001 From: Jonathon Reinhart Date: Sun, 30 Dec 2018 22:31:35 -0500 Subject: [PATCH 07/73] Use all keyword args for DataBuilder::Push.build() --- app/services/git/base_hooks_service.rb | 14 +++++++------- app/services/tags/destroy_service.rb | 11 +++++------ lib/gitlab/data_builder/push.rb | 12 ++++++++++-- spec/lib/gitlab/data_builder/push_spec.rb | 11 +++++++---- .../project_services/hipchat_service_spec.rb | 11 +++++------ .../shared_examples/models/chat_service_spec.rb | 2 +- ...ack_mattermost_notifications_shared_examples.rb | 6 +++--- 7 files changed, 38 insertions(+), 29 deletions(-) diff --git a/app/services/git/base_hooks_service.rb b/app/services/git/base_hooks_service.rb index a8478e3a904..9d371e234ee 100644 --- a/app/services/git/base_hooks_service.rb +++ b/app/services/git/base_hooks_service.rb @@ -73,13 +73,13 @@ module Git def push_data @push_data ||= Gitlab::DataBuilder::Push.build( - project, - current_user, - params[:oldrev], - params[:newrev], - params[:ref], - limited_commits, - event_message, + project: project, + user: current_user, + oldrev: params[:oldrev], + newrev: params[:newrev], + ref: params[:ref], + commits: limited_commits, + message: event_message, commits_count: commits_count, push_options: params[:push_options] || {} ) diff --git a/app/services/tags/destroy_service.rb b/app/services/tags/destroy_service.rb index cab507946b4..4f6ae07be7d 100644 --- a/app/services/tags/destroy_service.rb +++ b/app/services/tags/destroy_service.rb @@ -41,12 +41,11 @@ module Tags def build_push_data(tag) Gitlab::DataBuilder::Push.build( - project, - current_user, - tag.dereferenced_target.sha, - Gitlab::Git::BLANK_SHA, - "#{Gitlab::Git::TAG_REF_PREFIX}#{tag.name}", - []) + project: project, + user: current_user, + oldrev: tag.dereferenced_target.sha, + newrev: Gitlab::Git::BLANK_SHA, + ref: "#{Gitlab::Git::TAG_REF_PREFIX}#{tag.name}") end end end diff --git a/lib/gitlab/data_builder/push.rb b/lib/gitlab/data_builder/push.rb index af385d7d4ca..40bda3410e1 100644 --- a/lib/gitlab/data_builder/push.rb +++ b/lib/gitlab/data_builder/push.rb @@ -58,7 +58,10 @@ module Gitlab # } # # rubocop:disable Metrics/ParameterLists - def build(project, user, oldrev, newrev, ref, commits = [], message = nil, commits_count: nil, push_options: {}) + def build( + project:, user:, ref:, oldrev: nil, newrev: nil, + commits: [], commits_count: nil, message: nil, push_options: {}) + commits = Array(commits) # Total commits count @@ -113,7 +116,12 @@ module Gitlab ref = "#{Gitlab::Git::BRANCH_REF_PREFIX}#{project.default_branch}" commits = project.repository.commits(project.default_branch.to_s, limit: 3) - build(project, user, commits.last&.id, commits.first&.id, ref, commits) + build(project: project, + user: user, + oldrev: commits.last&.id, + newrev: commits.first&.id, + ref: ref, + commits: commits) end def sample_data diff --git a/spec/lib/gitlab/data_builder/push_spec.rb b/spec/lib/gitlab/data_builder/push_spec.rb index 0c4decc6518..46ad674a1eb 100644 --- a/spec/lib/gitlab/data_builder/push_spec.rb +++ b/spec/lib/gitlab/data_builder/push_spec.rb @@ -23,9 +23,12 @@ describe Gitlab::DataBuilder::Push do describe '.build' do let(:data) do - described_class.build(project, user, Gitlab::Git::BLANK_SHA, - '8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b', - 'refs/tags/v1.1.0') + described_class.build( + project: project, + user: user, + oldrev: Gitlab::Git::BLANK_SHA, + newrev: '8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b', + ref: 'refs/tags/v1.1.0') end it { expect(data).to be_a(Hash) } @@ -47,7 +50,7 @@ describe Gitlab::DataBuilder::Push do include_examples 'deprecated repository hook data' it 'does not raise an error when given nil commits' do - expect { described_class.build(spy, spy, spy, spy, 'refs/tags/v1.1.0', nil) } + expect { described_class.build(project: spy, user: spy, ref: 'refs/tags/v1.1.0', commits: nil) } .not_to raise_error end end diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb index fd9e33c1781..a04b984c1f6 100644 --- a/spec/models/project_services/hipchat_service_spec.rb +++ b/spec/models/project_services/hipchat_service_spec.rb @@ -98,12 +98,11 @@ describe HipchatService do context 'tag_push events' do let(:push_sample_data) do Gitlab::DataBuilder::Push.build( - project, - user, - Gitlab::Git::BLANK_SHA, - '1' * 40, - 'refs/tags/test', - []) + project: project, + user: user, + oldrev: Gitlab::Git::BLANK_SHA, + newrev: '1' * 40, + ref: 'refs/tags/test') end it "calls Hipchat API for tag push events" do diff --git a/spec/support/shared_examples/models/chat_service_spec.rb b/spec/support/shared_examples/models/chat_service_spec.rb index cf1d52a9616..2711c23f916 100644 --- a/spec/support/shared_examples/models/chat_service_spec.rb +++ b/spec/support/shared_examples/models/chat_service_spec.rb @@ -64,7 +64,7 @@ shared_examples_for "chat service" do |service_name| context "with not default branch" do let(:sample_data) do - Gitlab::DataBuilder::Push.build(project, user, nil, nil, "not-the-default-branch") + Gitlab::DataBuilder::Push.build(project: project, user: user, ref: "not-the-default-branch") end context "when notify_only_default_branch enabled" do diff --git a/spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb b/spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb index 940c24c8d67..d2492f6cee5 100644 --- a/spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb +++ b/spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb @@ -267,7 +267,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do it 'does not notify push events if they are not for the default branch' do ref = "#{Gitlab::Git::BRANCH_REF_PREFIX}test" - push_sample_data = Gitlab::DataBuilder::Push.build(project, user, nil, nil, ref, []) + push_sample_data = Gitlab::DataBuilder::Push.build(project: project, user: user, ref: ref) chat_service.execute(push_sample_data) @@ -284,7 +284,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do it 'still notifies about pushed tags' do ref = "#{Gitlab::Git::TAG_REF_PREFIX}test" - push_sample_data = Gitlab::DataBuilder::Push.build(project, user, nil, nil, ref, []) + push_sample_data = Gitlab::DataBuilder::Push.build(project: project, user: user, ref: ref) chat_service.execute(push_sample_data) @@ -299,7 +299,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do it 'notifies about all push events' do ref = "#{Gitlab::Git::BRANCH_REF_PREFIX}test" - push_sample_data = Gitlab::DataBuilder::Push.build(project, user, nil, nil, ref, []) + push_sample_data = Gitlab::DataBuilder::Push.build(project: project, user: user, ref: ref) chat_service.execute(push_sample_data) From a26633003c89cc4e79aeb31d0f95452227cee7bb Mon Sep 17 00:00:00 2001 From: Brandon Labuschagne Date: Thu, 25 Apr 2019 16:49:58 +0200 Subject: [PATCH 08/73] Internationalisation of ide directory This is one of many MRs opened in order to improve the overall internationalisation of the GitLab codebase. i18n documentation https://docs.gitlab.com/ee/development/i18n/externalization.html --- app/assets/javascripts/ide/ide_router.js | 3 ++- .../ide/stores/modules/file_templates/getters.js | 5 +++-- locale/gitlab.pot | 9 +++++++++ 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js index 518a9cf7a0f..8c84b98a108 100644 --- a/app/assets/javascripts/ide/ide_router.js +++ b/app/assets/javascripts/ide/ide_router.js @@ -3,6 +3,7 @@ import VueRouter from 'vue-router'; import { joinPaths } from '~/lib/utils/url_utility'; import flash from '~/flash'; import store from './stores'; +import { __ } from '~/locale'; Vue.use(VueRouter); @@ -94,7 +95,7 @@ router.beforeEach((to, from, next) => { }) .catch(e => { flash( - 'Error while loading the project data. Please try again.', + __('Error while loading the project data. Please try again.'), 'alert', document, null, diff --git a/app/assets/javascripts/ide/stores/modules/file_templates/getters.js b/app/assets/javascripts/ide/stores/modules/file_templates/getters.js index 628babe6a01..f10891a8e5b 100644 --- a/app/assets/javascripts/ide/stores/modules/file_templates/getters.js +++ b/app/assets/javascripts/ide/stores/modules/file_templates/getters.js @@ -1,4 +1,5 @@ import { activityBarViews } from '../../../constants'; +import { __ } from '~/locale'; export const templateTypes = () => [ { @@ -10,11 +11,11 @@ export const templateTypes = () => [ key: 'gitignores', }, { - name: 'LICENSE', + name: __('LICENSE'), key: 'licenses', }, { - name: 'Dockerfile', + name: __('Dockerfile'), key: 'dockerfiles', }, ]; diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 06f2f848925..c948c3a3b2d 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -3271,6 +3271,9 @@ msgstr "" msgid "Do you want to customize how Google Code email addresses and usernames are imported into GitLab?" msgstr "" +msgid "Dockerfile" +msgstr "" + msgid "Domain" msgstr "" @@ -3739,6 +3742,9 @@ msgstr "" msgid "Error while loading the merge request. Please try again." msgstr "" +msgid "Error while loading the project data. Please try again." +msgstr "" + msgid "Error while migrating %{upload_id}: %{error_message}" msgstr "" @@ -5165,6 +5171,9 @@ msgstr "" msgid "LFSStatus|Enabled" msgstr "" +msgid "LICENSE" +msgstr "" + msgid "Label" msgstr "" From 4848c32ea00a9a5be587aec0f24214e56724c683 Mon Sep 17 00:00:00 2001 From: Martin Hobert Date: Fri, 26 Apr 2019 14:54:20 +0200 Subject: [PATCH 09/73] refactor(sidebar): Refactored Karma spec files to Jest fix #58830 Added changelog Updated to use jest functions Added mock implementation --- .../vue_shared/directives/tooltip.js | 1 + ...tor-58830-migrate-sidebar-spec-to-jest.yml | 5 ++ .../sidebar/collapsed_calendar_icon_spec.js | 4 +- .../collapsed_grouped_date_picker_spec.js | 4 +- .../components/sidebar/date_picker_spec.js | 8 ++-- .../sidebar/labels_select/base_spec.js | 48 ++++++++++--------- .../labels_select/dropdown_button_spec.js | 8 ++-- .../dropdown_create_label_spec.js | 5 +- .../labels_select/dropdown_footer_spec.js | 5 +- .../labels_select/dropdown_header_spec.js | 2 +- .../dropdown_search_input_spec.js | 2 +- .../labels_select/dropdown_title_spec.js | 2 +- .../dropdown_value_collapsed_spec.js | 7 ++- .../labels_select/dropdown_value_spec.js | 8 ++-- .../components/sidebar/toggle_sidebar_spec.js | 4 +- 15 files changed, 62 insertions(+), 51 deletions(-) create mode 100644 changelogs/unreleased/refactor-58830-migrate-sidebar-spec-to-jest.yml rename spec/{javascripts => frontend}/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js (89%) rename spec/{javascripts => frontend}/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js (94%) rename spec/{javascripts => frontend}/vue_shared/components/sidebar/date_picker_spec.js (94%) rename spec/{javascripts => frontend}/vue_shared/components/sidebar/labels_select/base_spec.js (75%) rename spec/{javascripts => frontend}/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js (94%) rename spec/{javascripts => frontend}/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js (95%) rename spec/{javascripts => frontend}/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js (93%) rename spec/{javascripts => frontend}/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js (93%) rename spec/{javascripts => frontend}/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js (93%) rename spec/{javascripts => frontend}/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js (93%) rename spec/{javascripts => frontend}/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js (92%) rename spec/{javascripts => frontend}/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js (95%) rename spec/{javascripts => frontend}/vue_shared/components/sidebar/toggle_sidebar_spec.js (88%) diff --git a/app/assets/javascripts/vue_shared/directives/tooltip.js b/app/assets/javascripts/vue_shared/directives/tooltip.js index 549d27e96d9..2d1f7a1cfd0 100644 --- a/app/assets/javascripts/vue_shared/directives/tooltip.js +++ b/app/assets/javascripts/vue_shared/directives/tooltip.js @@ -1,4 +1,5 @@ import $ from 'jquery'; +import '~/commons/bootstrap'; export default { bind(el) { diff --git a/changelogs/unreleased/refactor-58830-migrate-sidebar-spec-to-jest.yml b/changelogs/unreleased/refactor-58830-migrate-sidebar-spec-to-jest.yml new file mode 100644 index 00000000000..20a4be8c9ad --- /dev/null +++ b/changelogs/unreleased/refactor-58830-migrate-sidebar-spec-to-jest.yml @@ -0,0 +1,5 @@ +--- +title: 'Refactored Karma spec files to Jest' +merge_request: 27688 +author: Martin Hobert +type: other diff --git a/spec/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js b/spec/frontend/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js similarity index 89% rename from spec/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js rename to spec/frontend/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js index 6bff1521695..691ebe43d6b 100644 --- a/spec/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import collapsedCalendarIcon from '~/vue_shared/components/sidebar/collapsed_calendar_icon.vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import mountComponent from 'helpers/vue_mount_component_helper'; describe('collapsedCalendarIcon', () => { let vm; @@ -26,7 +26,7 @@ describe('collapsedCalendarIcon', () => { }); it('should emit click event when container is clicked', () => { - const click = jasmine.createSpy(); + const click = jest.fn(); vm.$on('click', click); vm.$el.click(); diff --git a/spec/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js b/spec/frontend/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js similarity index 94% rename from spec/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js rename to spec/frontend/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js index c507a97d37e..062ebfa01c9 100644 --- a/spec/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import collapsedGroupedDatePicker from '~/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import mountComponent from 'helpers/vue_mount_component_helper'; describe('collapsedGroupedDatePicker', () => { let vm; @@ -13,7 +13,7 @@ describe('collapsedGroupedDatePicker', () => { describe('toggleCollapse events', () => { beforeEach(done => { - spyOn(vm, 'toggleSidebar'); + jest.spyOn(vm, 'toggleSidebar').mockImplementation(() => {}); vm.minDate = new Date('07/17/2016'); Vue.nextTick(done); }); diff --git a/spec/javascripts/vue_shared/components/sidebar/date_picker_spec.js b/spec/frontend/vue_shared/components/sidebar/date_picker_spec.js similarity index 94% rename from spec/javascripts/vue_shared/components/sidebar/date_picker_spec.js rename to spec/frontend/vue_shared/components/sidebar/date_picker_spec.js index 805ba7b9947..5e2bca6efc9 100644 --- a/spec/javascripts/vue_shared/components/sidebar/date_picker_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/date_picker_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import sidebarDatePicker from '~/vue_shared/components/sidebar/date_picker.vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import mountComponent from 'helpers/vue_mount_component_helper'; describe('sidebarDatePicker', () => { let vm; @@ -13,7 +13,7 @@ describe('sidebarDatePicker', () => { }); it('should emit toggleCollapse when collapsed toggle sidebar is clicked', () => { - const toggleCollapse = jasmine.createSpy(); + const toggleCollapse = jest.fn(); vm.$on('toggleCollapse', toggleCollapse); vm.$el.querySelector('.issuable-sidebar-header .gutter-toggle').click(); @@ -90,7 +90,7 @@ describe('sidebarDatePicker', () => { }); it('should emit saveDate when remove button is clicked', () => { - const saveDate = jasmine.createSpy(); + const saveDate = jest.fn(); vm.$on('saveDate', saveDate); vm.$el.querySelector('.value-content .btn-blank').click(); @@ -110,7 +110,7 @@ describe('sidebarDatePicker', () => { }); it('should emit toggleCollapse when toggle sidebar is clicked', () => { - const toggleCollapse = jasmine.createSpy(); + const toggleCollapse = jest.fn(); vm.$on('toggleCollapse', toggleCollapse); vm.$el.querySelector('.title .gutter-toggle').click(); diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/base_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/base_spec.js similarity index 75% rename from spec/javascripts/vue_shared/components/sidebar/labels_select/base_spec.js rename to spec/frontend/vue_shared/components/sidebar/labels_select/base_spec.js index c44b04009ca..6aee616c324 100644 --- a/spec/javascripts/vue_shared/components/sidebar/labels_select/base_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select/base_spec.js @@ -3,25 +3,35 @@ import Vue from 'vue'; import LabelsSelect from '~/labels_select'; import baseComponent from '~/vue_shared/components/sidebar/labels_select/base.vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; - -import { mockConfig, mockLabels } from './mock_data'; +import { mount } from '@vue/test-utils'; +import { + mockConfig, + mockLabels, +} from '../../../../../javascripts/vue_shared/components/sidebar/labels_select/mock_data'; const createComponent = (config = mockConfig) => { const Component = Vue.extend(baseComponent); - return mountComponent(Component, config); + return mount(Component, { + propsData: config, + sync: false, + }); }; describe('BaseComponent', () => { + let wrapper; let vm; - beforeEach(() => { - vm = createComponent(); + beforeEach(done => { + wrapper = createComponent(); + + ({ vm } = wrapper); + + Vue.nextTick(done); }); afterEach(() => { - vm.$destroy(); + wrapper.destroy(); }); describe('computed', () => { @@ -31,11 +41,9 @@ describe('BaseComponent', () => { }); it('returns correct string when showCreate prop is `false`', () => { - const mockConfigNonEditable = Object.assign({}, mockConfig, { showCreate: false }); - const vmNonEditable = createComponent(mockConfigNonEditable); + wrapper.setProps({ showCreate: false }); - expect(vmNonEditable.hiddenInputName).toBe('label_id[]'); - vmNonEditable.$destroy(); + expect(vm.hiddenInputName).toBe('label_id[]'); }); }); @@ -45,11 +53,9 @@ describe('BaseComponent', () => { }); it('return `Create group label` when `isProject` prop is false', () => { - const mockConfigGroup = Object.assign({}, mockConfig, { isProject: false }); - const vmGroup = createComponent(mockConfigGroup); + wrapper.setProps({ isProject: false }); - expect(vmGroup.createLabelTitle).toBe('Create group label'); - vmGroup.$destroy(); + expect(vm.createLabelTitle).toBe('Create group label'); }); }); @@ -59,11 +65,9 @@ describe('BaseComponent', () => { }); it('return `Manage group labels` when `isProject` prop is false', () => { - const mockConfigGroup = Object.assign({}, mockConfig, { isProject: false }); - const vmGroup = createComponent(mockConfigGroup); + wrapper.setProps({ isProject: false }); - expect(vmGroup.manageLabelsTitle).toBe('Manage group labels'); - vmGroup.$destroy(); + expect(vm.manageLabelsTitle).toBe('Manage group labels'); }); }); }); @@ -71,7 +75,7 @@ describe('BaseComponent', () => { describe('methods', () => { describe('handleClick', () => { it('emits onLabelClick event with label and list of labels as params', () => { - spyOn(vm, '$emit'); + jest.spyOn(vm, '$emit').mockImplementation(() => {}); vm.handleClick(mockLabels[0]); expect(vm.$emit).toHaveBeenCalledWith('onLabelClick', mockLabels[0]); @@ -80,7 +84,7 @@ describe('BaseComponent', () => { describe('handleCollapsedValueClick', () => { it('emits toggleCollapse event on component', () => { - spyOn(vm, '$emit'); + jest.spyOn(vm, '$emit').mockImplementation(() => {}); vm.handleCollapsedValueClick(); expect(vm.$emit).toHaveBeenCalledWith('toggleCollapse'); @@ -89,7 +93,7 @@ describe('BaseComponent', () => { describe('handleDropdownHidden', () => { it('emits onDropdownClose event on component', () => { - spyOn(vm, '$emit'); + jest.spyOn(vm, '$emit').mockImplementation(() => {}); vm.handleDropdownHidden(); expect(vm.$emit).toHaveBeenCalledWith('onDropdownClose'); diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js similarity index 94% rename from spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js rename to spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js index 0689fc1cf1f..bb33dc6ea0f 100644 --- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js @@ -2,9 +2,11 @@ import Vue from 'vue'; import dropdownButtonComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_button.vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; - -import { mockConfig, mockLabels } from './mock_data'; +import mountComponent from 'helpers/vue_mount_component_helper'; +import { + mockConfig, + mockLabels, +} from '../../../../../javascripts/vue_shared/components/sidebar/labels_select/mock_data'; const componentConfig = Object.assign({}, mockConfig, { fieldName: 'label_id[]', diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js similarity index 95% rename from spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js rename to spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js index b8f32f96332..1c25d42682c 100644 --- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js @@ -2,9 +2,8 @@ import Vue from 'vue'; import dropdownCreateLabelComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; - -import { mockSuggestedColors } from './mock_data'; +import mountComponent from 'helpers/vue_mount_component_helper'; +import { mockSuggestedColors } from '../../../../../javascripts/vue_shared/components/sidebar/labels_select/mock_data'; const createComponent = headerTitle => { const Component = Vue.extend(dropdownCreateLabelComponent); diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js similarity index 93% rename from spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js rename to spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js index 3711e9dac8c..989901a0012 100644 --- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js @@ -2,9 +2,8 @@ import Vue from 'vue'; import dropdownFooterComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_footer.vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; - -import { mockConfig } from './mock_data'; +import mountComponent from 'helpers/vue_mount_component_helper'; +import { mockConfig } from '../../../../../javascripts/vue_shared/components/sidebar/labels_select/mock_data'; const createComponent = ( labelsWebUrl = mockConfig.labelsWebUrl, diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js similarity index 93% rename from spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js rename to spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js index 115e21e4f9f..c36a82e1a35 100644 --- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js @@ -2,7 +2,7 @@ import Vue from 'vue'; import dropdownHeaderComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_header.vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import mountComponent from 'helpers/vue_mount_component_helper'; const createComponent = () => { const Component = Vue.extend(dropdownHeaderComponent); diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js similarity index 93% rename from spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js rename to spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js index c30e619e76b..2fffb2e495e 100644 --- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js @@ -2,7 +2,7 @@ import Vue from 'vue'; import dropdownSearchInputComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_search_input.vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import mountComponent from 'helpers/vue_mount_component_helper'; const createComponent = () => { const Component = Vue.extend(dropdownSearchInputComponent); diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js similarity index 93% rename from spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js rename to spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js index 6c84d2e167c..1616e657c81 100644 --- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js @@ -2,7 +2,7 @@ import Vue from 'vue'; import dropdownTitleComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_title.vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import mountComponent from 'helpers/vue_mount_component_helper'; const createComponent = (canEdit = true) => { const Component = Vue.extend(dropdownTitleComponent); diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js similarity index 92% rename from spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js rename to spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js index 4d3de5e474d..517f2c01c46 100644 --- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js @@ -2,9 +2,8 @@ import Vue from 'vue'; import dropdownValueCollapsedComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; - -import { mockLabels } from './mock_data'; +import mountComponent from 'helpers/vue_mount_component_helper'; +import { mockLabels } from '../../../../../javascripts/vue_shared/components/sidebar/labels_select/mock_data'; const createComponent = (labels = mockLabels) => { const Component = Vue.extend(dropdownValueCollapsedComponent); @@ -72,7 +71,7 @@ describe('DropdownValueCollapsedComponent', () => { describe('methods', () => { describe('handleClick', () => { it('emits onValueClick event on component', () => { - spyOn(vm, '$emit'); + jest.spyOn(vm, '$emit').mockImplementation(() => {}); vm.handleClick(); expect(vm.$emit).toHaveBeenCalledWith('onValueClick'); diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js similarity index 95% rename from spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js rename to spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js index 35a9c300953..ec143fec5d9 100644 --- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js @@ -3,9 +3,11 @@ import $ from 'jquery'; import dropdownValueComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_value.vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; - -import { mockConfig, mockLabels } from './mock_data'; +import mountComponent from 'helpers/vue_mount_component_helper'; +import { + mockConfig, + mockLabels, +} from '../../../../../javascripts/vue_shared/components/sidebar/labels_select/mock_data'; const createComponent = ( labels = mockLabels, diff --git a/spec/javascripts/vue_shared/components/sidebar/toggle_sidebar_spec.js b/spec/frontend/vue_shared/components/sidebar/toggle_sidebar_spec.js similarity index 88% rename from spec/javascripts/vue_shared/components/sidebar/toggle_sidebar_spec.js rename to spec/frontend/vue_shared/components/sidebar/toggle_sidebar_spec.js index c911a129173..5cf25ca6f81 100644 --- a/spec/javascripts/vue_shared/components/sidebar/toggle_sidebar_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/toggle_sidebar_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import toggleSidebar from '~/vue_shared/components/sidebar/toggle_sidebar.vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import mountComponent from 'helpers/vue_mount_component_helper'; describe('toggleSidebar', () => { let vm; @@ -23,7 +23,7 @@ describe('toggleSidebar', () => { }); it('should emit toggle event when button clicked', () => { - const toggle = jasmine.createSpy(); + const toggle = jest.fn(); vm.$on('toggle', toggle); vm.$el.click(); From 8703fdaae8243e9934d2456d57859f066020ccc4 Mon Sep 17 00:00:00 2001 From: Enrique Alcantara Date: Fri, 26 Apr 2019 10:20:04 -0400 Subject: [PATCH 10/73] Fix base domain help text update --- .../javascripts/clusters/clusters_bundle.js | 12 +++---- .../frontend/clusters/clusters_bundle_spec.js | 35 ++++--------------- 2 files changed, 11 insertions(+), 36 deletions(-) diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js index 4f47f1b6550..8461e01de7b 100644 --- a/app/assets/javascripts/clusters/clusters_bundle.js +++ b/app/assets/javascripts/clusters/clusters_bundle.js @@ -279,14 +279,10 @@ export default class Clusters { this.store.acknowledgeSuccessfulUpdate(appId); } - toggleIngressDomainHelpText(ingressPreviousState, ingressNewState) { - const { externalIp, status } = ingressNewState; - const helpTextHidden = status !== APPLICATION_STATUS.INSTALLED || !externalIp; - const domainSnippetText = `${externalIp}${INGRESS_DOMAIN_SUFFIX}`; - - if (ingressPreviousState.status !== status) { - this.ingressDomainHelpText.classList.toggle('hide', helpTextHidden); - this.ingressDomainSnippet.textContent = domainSnippetText; + toggleIngressDomainHelpText({ externalIp }, { externalIp: newExternalIp }) { + if (externalIp !== newExternalIp) { + this.ingressDomainHelpText.classList.toggle('hide', !newExternalIp); + this.ingressDomainSnippet.textContent = `${newExternalIp}${INGRESS_DOMAIN_SUFFIX}`; } } diff --git a/spec/frontend/clusters/clusters_bundle_spec.js b/spec/frontend/clusters/clusters_bundle_spec.js index 5103cb4f69f..a61103397eb 100644 --- a/spec/frontend/clusters/clusters_bundle_spec.js +++ b/spec/frontend/clusters/clusters_bundle_spec.js @@ -6,7 +6,7 @@ import { loadHTMLFixture } from 'helpers/fixtures'; import { setTestTimeout } from 'helpers/timeout'; import $ from 'jquery'; -const { INSTALLING, INSTALLABLE, INSTALLED, NOT_INSTALLABLE } = APPLICATION_STATUS; +const { INSTALLING, INSTALLABLE, INSTALLED } = APPLICATION_STATUS; describe('Clusters', () => { setTestTimeout(1000); @@ -317,13 +317,12 @@ describe('Clusters', () => { let ingressNewState; beforeEach(() => { - ingressPreviousState = { status: INSTALLABLE }; - ingressNewState = { status: INSTALLED, externalIp: '127.0.0.1' }; + ingressPreviousState = { externalIp: null }; + ingressNewState = { externalIp: '127.0.0.1' }; }); - describe(`when ingress application new status is ${INSTALLED}`, () => { + describe(`when ingress have an external ip assigned`, () => { beforeEach(() => { - ingressNewState.status = INSTALLED; cluster.toggleIngressDomainHelpText(ingressPreviousState, ingressNewState); }); @@ -338,31 +337,11 @@ describe('Clusters', () => { }); }); - describe(`when ingress application new status is different from ${INSTALLED}`, () => { + describe(`when ingress does not have an external ip assigned`, () => { it('hides custom domain help text', () => { - ingressNewState.status = NOT_INSTALLABLE; - cluster.ingressDomainHelpText.classList.remove('hide'); - - cluster.toggleIngressDomainHelpText(ingressPreviousState, ingressNewState); - - expect(cluster.ingressDomainHelpText.classList.contains('hide')).toEqual(true); - }); - }); - - describe('when ingress application new status and old status are the same', () => { - it('does not display custom domain help text', () => { - ingressPreviousState.status = INSTALLED; - ingressNewState.status = ingressPreviousState.status; - - cluster.toggleIngressDomainHelpText(ingressPreviousState, ingressNewState); - - expect(cluster.ingressDomainHelpText.classList.contains('hide')).toEqual(true); - }); - }); - - describe(`when ingress new status is ${INSTALLED} and there isn’t an ip assigned`, () => { - it('does not display custom domain help text', () => { + ingressPreviousState.externalIp = '127.0.0.1'; ingressNewState.externalIp = null; + cluster.ingressDomainHelpText.classList.remove('hide'); cluster.toggleIngressDomainHelpText(ingressPreviousState, ingressNewState); From 36624f21eee3a3f7a50d0ff0d941d3b055579bf0 Mon Sep 17 00:00:00 2001 From: Enrique Alcantara Date: Fri, 26 Apr 2019 10:23:33 -0400 Subject: [PATCH 11/73] Add changelog entry --- changelogs/unreleased/61036-fix-ingress-base-domain-text.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 changelogs/unreleased/61036-fix-ingress-base-domain-text.yml diff --git a/changelogs/unreleased/61036-fix-ingress-base-domain-text.yml b/changelogs/unreleased/61036-fix-ingress-base-domain-text.yml new file mode 100644 index 00000000000..32f0e023923 --- /dev/null +++ b/changelogs/unreleased/61036-fix-ingress-base-domain-text.yml @@ -0,0 +1,5 @@ +--- +title: Fix base domain help text update +merge_request: 27746 +author: +type: fixed From 558e27e1c472ad13e8814577e8db1a780e2473b9 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Wed, 24 Apr 2019 11:49:31 +0100 Subject: [PATCH 12/73] Fixes EE differences for app/views/projects/new.html.haml --- app/views/projects/new.html.haml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 95027634de2..d7e16dbd40c 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -16,6 +16,7 @@ = _('A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), %{among_other_things_link}.').html_safe % { among_other_things_link: among_other_things_link } %p = _('All features are enabled for blank projects, from templates, or when importing, but you can disable them afterward in the project settings.') + = render_if_exists 'projects/new_ci_cd_banner_external_repo' %p - pages_getting_started_guide = link_to _('Pages getting started guide'), help_page_path("user/project/pages/getting_started_part_two", anchor: "fork-a-project-to-get-started-from"), target: '_blank' = _('Information about additional Pages templates and how to install them can be found in our %{pages_getting_started_guide}.').html_safe % { pages_getting_started_guide: pages_getting_started_guide } @@ -42,6 +43,7 @@ %a.nav-link{ href: '#import-project-pane', id: 'import-project-tab', data: { toggle: 'tab', track_label: 'import_project', track_event: "click_tab" }, role: 'tab' } %span.d-none.d-sm-block Import project %span.d-block.d-sm-none Import + = render_if_exists 'projects/new_ci_cd_only_project_tab', active_tab: active_tab .tab-content.gitlab-tab-content .tab-pane{ id: 'blank-project-pane', class: active_when(active_tab == 'blank'), role: 'tabpanel' } @@ -68,6 +70,8 @@ %h4 No import options available %p Contact an administrator to enable options for importing your project. + = render_if_exists 'projects/new_ci_cd_only_project_pane', active_tab: active_tab + .save-project-loader.d-none .center %h2 From 348d5e3c63c8279005c6af814964df399b617f93 Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Fri, 26 Apr 2019 15:32:56 -0300 Subject: [PATCH 13/73] Fix letter opener gem error Accessing http://localhostrails/letter_opener is thowring an exception, updating the gem fixes it. --- Gemfile | 2 +- Gemfile.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Gemfile b/Gemfile index 65ba7137892..137cae38f6f 100644 --- a/Gemfile +++ b/Gemfile @@ -308,7 +308,7 @@ group :development do gem 'foreman', '~> 0.84.0' gem 'brakeman', '~> 4.2', require: false - gem 'letter_opener_web', '~> 1.3.0' + gem 'letter_opener_web', '~> 1.3.4' gem 'rblineprof', '~> 0.3.6', platform: :mri, require: false # Better errors handler diff --git a/Gemfile.lock b/Gemfile.lock index da8f8db9528..8626a11ad45 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -441,9 +441,9 @@ GEM rest-client (~> 2.0) launchy (2.4.3) addressable (~> 2.3) - letter_opener (1.4.1) + letter_opener (1.7.0) launchy (~> 2.2) - letter_opener_web (1.3.0) + letter_opener_web (1.3.4) actionmailer (>= 3.2) letter_opener (~> 1.0) railties (>= 3.2) @@ -1089,7 +1089,7 @@ DEPENDENCIES kaminari (~> 1.0) knapsack (~> 1.17) kubeclient (~> 4.2.2) - letter_opener_web (~> 1.3.0) + letter_opener_web (~> 1.3.4) license_finder (~> 5.4) licensee (~> 8.9) lograge (~> 0.5) From 71efdd204154c5f50b000de32a72a20cb38056b1 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Thu, 25 Apr 2019 22:07:53 -0700 Subject: [PATCH 14/73] Upgrade Mermaid to 8.0.0 We were using 8.0.0-rc.8 previously, and there appears to be significant new dependencies. --- package.json | 2 +- yarn.lock | 131 +++++++++++++++++++++++++++++++++++++++------------ 2 files changed, 102 insertions(+), 31 deletions(-) diff --git a/package.json b/package.json index d2ee29fe06d..66908eab09a 100644 --- a/package.json +++ b/package.json @@ -90,7 +90,7 @@ "jszip-utils": "^0.0.2", "katex": "^0.10.0", "marked": "^0.3.12", - "mermaid": "^8.0.0-rc.8", + "mermaid": "^8.0.0", "monaco-editor": "^0.15.6", "monaco-editor-webpack-plugin": "^1.7.0", "mousetrap": "^1.4.6", diff --git a/yarn.lock b/yarn.lock index a0446b652ac..6055acc491e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2736,17 +2736,17 @@ cyclist@~0.2.2: resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640" integrity sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA= -d3-array@1, d3-array@1.2.1, d3-array@^1.2.0, d3-array@^1.2.1: +d3-array@1, d3-array@1.2.1, d3-array@^1.1.1, d3-array@^1.2.0, d3-array@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-1.2.1.tgz#d1ca33de2f6ac31efadb8e050a021d7e2396d5dc" integrity sha512-CyINJQ0SOUHojDdFDH4JEM0552vCR1utGyLHegJHyYH0JyCpSeTPxi4OBqHMA2jJZq4NH782LtaJWBImqI/HBw== -d3-axis@1.0.8, d3-axis@^1.0.8: +d3-axis@1, d3-axis@1.0.8, d3-axis@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/d3-axis/-/d3-axis-1.0.8.tgz#31a705a0b535e65759de14173a31933137f18efa" integrity sha1-MacFoLU15ldZ3hQXOjGTMTfxjvo= -d3-brush@1.0.4, d3-brush@^1.0.4: +d3-brush@1, d3-brush@1.0.4, d3-brush@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/d3-brush/-/d3-brush-1.0.4.tgz#00c2f238019f24f6c0a194a26d41a1530ffe7bc4" integrity sha1-AMLyOAGfJPbAoZSibUGhUw/+e8Q= @@ -2757,7 +2757,7 @@ d3-brush@1.0.4, d3-brush@^1.0.4: d3-selection "1" d3-transition "1" -d3-chord@1.0.4: +d3-chord@1, d3-chord@1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/d3-chord/-/d3-chord-1.0.4.tgz#7dec4f0ba886f713fe111c45f763414f6f74ca2c" integrity sha1-fexPC6iG9xP+ERxF92NBT290yiw= @@ -2775,6 +2775,13 @@ d3-color@1, d3-color@1.0.3: resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-1.0.3.tgz#bc7643fca8e53a8347e2fbdaffa236796b58509b" integrity sha1-vHZD/KjlOoNH4vva/6I2eWtYUJs= +d3-contour@1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/d3-contour/-/d3-contour-1.3.2.tgz#652aacd500d2264cb3423cee10db69f6f59bead3" + integrity sha512-hoPp4K/rJCu0ladiH6zmJUEz6+u3lgR+GSm/QdM2BBvDraU39Vr7YdDCicJcxP1z8i9B/2dJLgDC1NcvlF8WCg== + dependencies: + d3-array "^1.1.1" + d3-dispatch@1, d3-dispatch@1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-1.0.3.tgz#46e1491eaa9b58c358fce5be4e8bed626e7871f8" @@ -2802,7 +2809,14 @@ d3-ease@1, d3-ease@1.0.3, d3-ease@^1.0.3: resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-1.0.3.tgz#68bfbc349338a380c44d8acc4fbc3304aa2d8c0e" integrity sha1-aL+8NJM4o4DETYrMT7wzBKotjA4= -d3-force@1.1.0: +d3-fetch@1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/d3-fetch/-/d3-fetch-1.1.2.tgz#957c8fbc6d4480599ba191b1b2518bf86b3e1be2" + integrity sha512-S2loaQCV/ZeyTyIF2oP8D1K9Z4QizUzW7cWeAOAS4U88qOt3Ucf6GsmgthuYSdyB2HyEm4CeGvkQxWsmInsIVA== + dependencies: + d3-dsv "1" + +d3-force@1, d3-force@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/d3-force/-/d3-force-1.1.0.tgz#cebf3c694f1078fcc3d4daf8e567b2fbd70d4ea3" integrity sha512-2HVQz3/VCQs0QeRNZTYb7GxoUCeb6bOzMp/cGcLa87awY9ZsPvXOGeZm0iaGBjXic6I1ysKwMn+g+5jSAdzwcg== @@ -2817,14 +2831,14 @@ d3-format@1, d3-format@1.2.2: resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.2.2.tgz#1a39c479c8a57fe5051b2e67a3bee27061a74e7a" integrity sha512-zH9CfF/3C8zUI47nsiKfD0+AGDEuM8LwBIP7pBVpyR4l/sKkZqITmMtxRp04rwBrlshIZ17XeFAaovN3++wzkw== -d3-geo@1.9.1: +d3-geo@1, d3-geo@1.9.1: version "1.9.1" resolved "https://registry.yarnpkg.com/d3-geo/-/d3-geo-1.9.1.tgz#157e3b0f917379d0f73bebfff3be537f49fa7356" integrity sha512-l9wL/cEQkyZQYXw3xbmLsH3eQ5ij+icNfo4r0GrLa5rOCZR/e/3am45IQ0FvQ5uMsv+77zBRunLc9ufTWSQYFA== dependencies: d3-array "1" -d3-hierarchy@1.1.5: +d3-hierarchy@1, d3-hierarchy@1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-1.1.5.tgz#a1c845c42f84a206bcf1c01c01098ea4ddaa7a26" integrity sha1-ochFxC+Eoga88cAcAQmOpN2qeiY= @@ -2841,7 +2855,7 @@ d3-path@1, d3-path@1.0.5: resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-1.0.5.tgz#241eb1849bd9e9e8021c0d0a799f8a0e8e441764" integrity sha1-JB6xhJvZ6egCHA0KeZ+KDo5EF2Q= -d3-polygon@1.0.3: +d3-polygon@1, d3-polygon@1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/d3-polygon/-/d3-polygon-1.0.3.tgz#16888e9026460933f2b179652ad378224d382c62" integrity sha1-FoiOkCZGCTPysXllKtN4Ik04LGI= @@ -2856,7 +2870,7 @@ d3-queue@3.0.7: resolved "https://registry.yarnpkg.com/d3-queue/-/d3-queue-3.0.7.tgz#c93a2e54b417c0959129d7d73f6cf7d4292e7618" integrity sha1-yTouVLQXwJWRKdfXP2z31Ckudhg= -d3-random@1.1.0: +d3-random@1, d3-random@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/d3-random/-/d3-random-1.1.0.tgz#6642e506c6fa3a648595d2b2469788a8d12529d3" integrity sha1-ZkLlBsb6OmSFldKyRpeIqNElKdM= @@ -2871,6 +2885,14 @@ d3-request@1.0.6: d3-dsv "1" xmlhttprequest "1" +d3-scale-chromatic@1: + version "1.3.3" + resolved "https://registry.yarnpkg.com/d3-scale-chromatic/-/d3-scale-chromatic-1.3.3.tgz#dad4366f0edcb288f490128979c3c793583ed3c0" + integrity sha512-BWTipif1CimXcYfT02LKjAyItX5gKiwxuPRgr4xM58JwlLocWbjPLI7aMEjkcoOQXMkYsmNsvv3d2yl/OKuHHw== + dependencies: + d3-color "1" + d3-interpolate "1" + d3-scale@1.0.7, d3-scale@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-1.0.7.tgz#fa90324b3ea8a776422bd0472afab0b252a0945d" @@ -2884,12 +2906,24 @@ d3-scale@1.0.7, d3-scale@^1.0.7: d3-time "1" d3-time-format "2" +d3-scale@2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-2.2.2.tgz#4e880e0b2745acaaddd3ede26a9e908a9e17b81f" + integrity sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw== + dependencies: + d3-array "^1.2.0" + d3-collection "1" + d3-format "1" + d3-interpolate "1" + d3-time "1" + d3-time-format "2" + d3-selection@1, d3-selection@1.3.0, d3-selection@^1.1.0, d3-selection@^1.2.0: version "1.3.0" resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-1.3.0.tgz#d53772382d3dc4f7507bfb28bcd2d6aed2a0ad6d" integrity sha512-qgpUOg9tl5CirdqESUAu0t9MU/t3O9klYfGfyKsXEmhyxyzLpzpeh08gaxBUTQw1uXIOkr/30Ut2YRjSSxlmHA== -d3-shape@1.2.0, d3-shape@^1.2.0: +d3-shape@1, d3-shape@1.2.0, d3-shape@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-1.2.0.tgz#45d01538f064bafd05ea3d6d2cb748fd8c41f777" integrity sha1-RdAVOPBkuv0F6j1tLLdI/YxB93c= @@ -2925,12 +2959,12 @@ d3-transition@1, d3-transition@1.1.1, d3-transition@^1.1.1: d3-selection "^1.1.0" d3-timer "1" -d3-voronoi@1.1.2: +d3-voronoi@1, d3-voronoi@1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/d3-voronoi/-/d3-voronoi-1.1.2.tgz#1687667e8f13a2d158c80c1480c5a29cb0d8973c" integrity sha1-Fodmfo8TotFYyAwUgMWinLDYlzw= -d3-zoom@1.7.1: +d3-zoom@1, d3-zoom@1.7.1: version "1.7.1" resolved "https://registry.yarnpkg.com/d3-zoom/-/d3-zoom-1.7.1.tgz#02f43b3c3e2db54f364582d7e4a236ccc5506b63" integrity sha512-sZHQ55DGq5BZBFGnRshUT8tm2sfhPHFnOlmPbbwTkAoPeVdRTkB4Xsf9GCY0TSHrTD8PeJPZGmP/TpGicwJDJQ== @@ -2977,6 +3011,43 @@ d3@^4.13.0: d3-voronoi "1.1.2" d3-zoom "1.7.1" +d3@^5.7.0: + version "5.9.2" + resolved "https://registry.yarnpkg.com/d3/-/d3-5.9.2.tgz#64e8a7e9c3d96d9e6e4999d2c8a2c829767e67f5" + integrity sha512-ydrPot6Lm3nTWH+gJ/Cxf3FcwuvesYQ5uk+j/kXEH/xbuYWYWTMAHTJQkyeuG8Y5WM5RSEYB41EctUrXQQytRQ== + dependencies: + d3-array "1" + d3-axis "1" + d3-brush "1" + d3-chord "1" + d3-collection "1" + d3-color "1" + d3-contour "1" + d3-dispatch "1" + d3-drag "1" + d3-dsv "1" + d3-ease "1" + d3-fetch "1" + d3-force "1" + d3-format "1" + d3-geo "1" + d3-hierarchy "1" + d3-interpolate "1" + d3-path "1" + d3-polygon "1" + d3-quadtree "1" + d3-random "1" + d3-scale "2" + d3-scale-chromatic "1" + d3-selection "1" + d3-shape "1" + d3-time "1" + d3-time-format "2" + d3-timer "1" + d3-transition "1" + d3-voronoi "1" + d3-zoom "1" + dagre-d3-renderer@^0.5.8: version "0.5.8" resolved "https://registry.yarnpkg.com/dagre-d3-renderer/-/dagre-d3-renderer-0.5.8.tgz#aa071bb71d3c4d67426925906f3f6ddead49c1a3" @@ -4938,10 +5009,10 @@ hash.js@^1.0.0, hash.js@^1.0.3: inherits "^2.0.3" minimalistic-assert "^1.0.0" -he@^1.1.0, he@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" - integrity sha1-k0EP0hsAlzUVH4howvJx80J+I/0= +he@^1.1.0, he@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== highlight.js@^9.13.1, highlight.js@~9.13.0: version "9.13.1" @@ -7008,19 +7079,19 @@ merge@^1.2.0: resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.1.tgz#38bebf80c3220a8a487b6fcfb3941bb11720c145" integrity sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ== -mermaid@^8.0.0-rc.8: - version "8.0.0-rc.8" - resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-8.0.0-rc.8.tgz#74ed54d0d46e9ee71c4db2730b2d83d516a21e72" - integrity sha512-GbF9jHWfqE7YGx9vQySmBxy2Ahlclxmpk4tJ9ntNyafENl96s96ggUK/NQS5ydYoFab6MavTm4YMTIPKqWVvPQ== +mermaid@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-8.0.0.tgz#8f6c75017e788a8c3997e20c5e5046c2b88d1a8f" + integrity sha512-vUQRykev0A6RtxIVqQT3a9TDxcSbdZbQF5JDyKgidnYuJy8BE8jp6LM+HKDSQuroKm6buu4NlpMO+qhxIP/cTg== dependencies: - d3 "^4.13.0" + d3 "^5.7.0" dagre-d3-renderer "^0.5.8" dagre-layout "^0.8.8" graphlibrary "^2.2.0" - he "^1.1.1" - lodash "^4.17.5" - moment "^2.21.0" - scope-css "^1.0.5" + he "^1.2.0" + lodash "^4.17.11" + moment "^2.23.0" + scope-css "^1.2.1" methods@~1.1.2: version "1.1.2" @@ -7172,10 +7243,10 @@ mkdirp@0.5.x, mkdirp@0.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp dependencies: minimist "0.0.8" -moment@2.x, moment@^2.10.2, moment@^2.21.0: - version "2.23.0" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.23.0.tgz#759ea491ac97d54bac5ad776996e2a58cc1bc225" - integrity sha512-3IE39bHVqFbWWaPOMHZF98Q9c3LDKGTmypMiTM2QygGXXElkFWIH7GxfmlwmY2vwa+wmNsoYZmG2iusf1ZjJoA== +moment@2.x, moment@^2.10.2, moment@^2.23.0: + version "2.24.0" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b" + integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg== monaco-editor-webpack-plugin@^1.7.0: version "1.7.0" @@ -9267,7 +9338,7 @@ schema-utils@^1.0.0: ajv-errors "^1.0.0" ajv-keywords "^3.1.0" -scope-css@^1.0.5: +scope-css@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/scope-css/-/scope-css-1.2.1.tgz#c35768bc900cad030a3e0d663a818c0f6a57f40e" integrity sha512-UjLRmyEYaDNiOS673xlVkZFlVCtckJR/dKgr434VMm7Lb+AOOqXKdAcY7PpGlJYErjXXJzKN7HWo4uRPiZZG0Q== From 3b86728070732cab4854799b4e954bcd16e07a3c Mon Sep 17 00:00:00 2001 From: Bastian Blank Date: Sat, 30 Mar 2019 00:33:13 +0100 Subject: [PATCH 15/73] Format extra help page text like wiki The instance specific help text was not rendered in any way compatible with the look and feel of the rest of the page. Just re-use the wiki style like the rest of the page. --- app/views/help/index.html.haml | 3 ++- changelogs/unreleased/55948-help-text-formatting-wiki.yml | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/55948-help-text-formatting-wiki.yml diff --git a/app/views/help/index.html.haml b/app/views/help/index.html.haml index 916f98a62d1..75e4dc46c9b 100644 --- a/app/views/help/index.html.haml +++ b/app/views/help/index.html.haml @@ -1,6 +1,7 @@ %div - if Gitlab::CurrentSettings.help_page_text.present? - = markdown_field(Gitlab::CurrentSettings.current_application_settings, :help_page_text) + .prepend-top-default.md + = markdown_field(Gitlab::CurrentSettings.current_application_settings, :help_page_text) %hr %h1 diff --git a/changelogs/unreleased/55948-help-text-formatting-wiki.yml b/changelogs/unreleased/55948-help-text-formatting-wiki.yml new file mode 100644 index 00000000000..e1e0475a117 --- /dev/null +++ b/changelogs/unreleased/55948-help-text-formatting-wiki.yml @@ -0,0 +1,5 @@ +--- +title: Format extra help page text like wiki +merge_request: 26782 +author: Bastian Blank +type: fixed From 28f785404a6659d61c69ee4bfdaca915652d1759 Mon Sep 17 00:00:00 2001 From: Shinya Maeda Date: Sun, 28 Apr 2019 10:27:08 +0700 Subject: [PATCH 16/73] Prevent concurrent execution of PipelineScheduleWorker Currently, PipelineScheduleWorker is fired in a short period on our production server. We can stop this behavior by locking the execution thread with in_lock method. --- app/workers/pipeline_schedule_worker.rb | 26 ++++++++++++------- .../lock-pipeline-schedule-worker.yml | 5 ++++ spec/workers/pipeline_schedule_worker_spec.rb | 12 +++++++++ 3 files changed, 33 insertions(+), 10 deletions(-) create mode 100644 changelogs/unreleased/lock-pipeline-schedule-worker.yml diff --git a/app/workers/pipeline_schedule_worker.rb b/app/workers/pipeline_schedule_worker.rb index 02a69ea3b54..8a9ee7808e4 100644 --- a/app/workers/pipeline_schedule_worker.rb +++ b/app/workers/pipeline_schedule_worker.rb @@ -3,20 +3,26 @@ class PipelineScheduleWorker include ApplicationWorker include CronjobQueue + include ::Gitlab::ExclusiveLeaseHelpers + + EXCLUSIVE_LOCK_KEY = 'pipeline_schedules:run:lock' + LOCK_TIMEOUT = 50.minutes # rubocop: disable CodeReuse/ActiveRecord def perform - Ci::PipelineSchedule.active.where("next_run_at < ?", Time.now) - .preload(:owner, :project).find_each do |schedule| + in_lock(EXCLUSIVE_LOCK_KEY, ttl: LOCK_TIMEOUT, retries: 1) do + Ci::PipelineSchedule.active.where("next_run_at < ?", Time.now) + .preload(:owner, :project).find_each do |schedule| - Ci::CreatePipelineService.new(schedule.project, - schedule.owner, - ref: schedule.ref) - .execute!(:schedule, ignore_skip_ci: true, save_on_errors: true, schedule: schedule) - rescue => e - error(schedule, e) - ensure - schedule.schedule_next_run! + schedule.schedule_next_run! + + Ci::CreatePipelineService.new(schedule.project, + schedule.owner, + ref: schedule.ref) + .execute!(:schedule, ignore_skip_ci: true, save_on_errors: true, schedule: schedule) + rescue => e + error(schedule, e) + end end end # rubocop: enable CodeReuse/ActiveRecord diff --git a/changelogs/unreleased/lock-pipeline-schedule-worker.yml b/changelogs/unreleased/lock-pipeline-schedule-worker.yml new file mode 100644 index 00000000000..1b889f01620 --- /dev/null +++ b/changelogs/unreleased/lock-pipeline-schedule-worker.yml @@ -0,0 +1,5 @@ +--- +title: Prevent concurrent execution of PipelineScheduleWorker +merge_request: 27781 +author: +type: performance diff --git a/spec/workers/pipeline_schedule_worker_spec.rb b/spec/workers/pipeline_schedule_worker_spec.rb index f23910d23be..8c604b13297 100644 --- a/spec/workers/pipeline_schedule_worker_spec.rb +++ b/spec/workers/pipeline_schedule_worker_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' describe PipelineScheduleWorker do + include ExclusiveLeaseHelpers + subject { described_class.new.perform } set(:project) { create(:project, :repository) } @@ -39,6 +41,16 @@ describe PipelineScheduleWorker do it_behaves_like 'successful scheduling' + context 'when exclusive lease has already been taken by the other instance' do + before do + stub_exclusive_lease_taken(described_class::EXCLUSIVE_LOCK_KEY, timeout: described_class::LOCK_TIMEOUT) + end + + it 'raises an error and does not start creating pipelines' do + expect { subject }.to raise_error(Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError) + end + end + context 'when the latest commit contains [ci skip]' do before do allow_any_instance_of(Ci::Pipeline) From 6229a458519e246694d277a89e9a5e3fea0adb9f Mon Sep 17 00:00:00 2001 From: Hossein Pursultani Date: Fri, 26 Apr 2019 16:20:20 +1000 Subject: [PATCH 17/73] Change path of bin/sidkiq-cluster in comments `sidekiq-cluster` is moved from `bin/` to `ee/bin` in EE code. This is a corresponding change in CE. --- lib/gitlab/sidekiq_config.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/gitlab/sidekiq_config.rb b/lib/gitlab/sidekiq_config.rb index fb303e3fb0c..c102fa14cfc 100644 --- a/lib/gitlab/sidekiq_config.rb +++ b/lib/gitlab/sidekiq_config.rb @@ -7,7 +7,7 @@ module Gitlab module SidekiqConfig QUEUE_CONFIG_PATHS = %w[app/workers/all_queues.yml ee/app/workers/all_queues.yml].freeze - # This method is called by `bin/sidekiq-cluster` in EE, which runs outside + # This method is called by `ee/bin/sidekiq-cluster` in EE, which runs outside # of bundler/Rails context, so we cannot use any gem or Rails methods. def self.worker_queues(rails_path = Rails.root.to_s) @worker_queues ||= {} @@ -19,7 +19,7 @@ module Gitlab end end - # This method is called by `bin/sidekiq-cluster` in EE, which runs outside + # This method is called by `ee/bin/sidekiq-cluster` in EE, which runs outside # of bundler/Rails context, so we cannot use any gem or Rails methods. def self.expand_queues(queues, all_queues = self.worker_queues) return [] if queues.empty? From fb96143ad361ab87350a1618a6e9123190386986 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Mon, 29 Apr 2019 08:40:25 +0100 Subject: [PATCH 18/73] Fixed boards card dragging styling Closes https://gitlab.com/gitlab-org/gitlab-ce/issues/61050 --- app/assets/javascripts/boards/components/board_card.vue | 2 +- app/assets/stylesheets/pages/boards.scss | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue index c9effa0639b..b8882203cc7 100644 --- a/app/assets/javascripts/boards/components/board_card.vue +++ b/app/assets/javascripts/boards/components/board_card.vue @@ -83,7 +83,7 @@ export default { }" :index="index" :data-issue-id="issue.id" - class="board-card position-relative p-3 rounded" + class="board-card p-3 rounded" @mousedown="mouseDown" @mousemove="mouseMove" @mouseup="showIssue($event)" diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index 0e4b40b2bed..09ff518bbdf 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -210,6 +210,7 @@ border: 1px solid $gray-200; box-shadow: 0 1px 2px $issue-boards-card-shadow; line-height: $gl-padding; + list-style: none; &:not(:last-child) { margin-bottom: $gl-padding-8; From de69a808a015514fa7e4451f406fcc3d73734919 Mon Sep 17 00:00:00 2001 From: Peter Leitzen Date: Sat, 27 Apr 2019 16:35:32 +0200 Subject: [PATCH 19/73] Use correct k8s namespace in Prometheus queries Before this commit the wrong namespace could have been used in Prometheus queries for group-level installations. --- app/models/clusters/platforms/kubernetes.rb | 4 +++ lib/gitlab/prometheus/query_variables.rb | 6 +++- .../gitlab/prometheus/query_variables_spec.rb | 28 ++++++++++++++++--- 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb index a806367a49b..94a6b224113 100644 --- a/app/models/clusters/platforms/kubernetes.rb +++ b/app/models/clusters/platforms/kubernetes.rb @@ -76,6 +76,10 @@ module Clusters end end + def namespace_for(project) + cluster.find_or_initialize_kubernetes_namespace_for_project(project)&.namespace + end + def predefined_variables(project:) Gitlab::Ci::Variables::Collection.new.tap do |variables| variables.append(key: 'KUBE_URL', value: api_url) diff --git a/lib/gitlab/prometheus/query_variables.rb b/lib/gitlab/prometheus/query_variables.rb index 1cc85d4b4a6..dca09aef47d 100644 --- a/lib/gitlab/prometheus/query_variables.rb +++ b/lib/gitlab/prometheus/query_variables.rb @@ -4,9 +4,13 @@ module Gitlab module Prometheus module QueryVariables def self.call(environment) + deployment_platform = environment.deployment_platform + namespace = deployment_platform&.namespace_for(environment.project) || + deployment_platform&.actual_namespace || '' + { ci_environment_slug: environment.slug, - kube_namespace: environment.deployment_platform&.actual_namespace || '', + kube_namespace: namespace, environment_filter: %{container_name!="POD",environment="#{environment.slug}"} } end diff --git a/spec/lib/gitlab/prometheus/query_variables_spec.rb b/spec/lib/gitlab/prometheus/query_variables_spec.rb index 78c74266c61..048f4af6020 100644 --- a/spec/lib/gitlab/prometheus/query_variables_spec.rb +++ b/spec/lib/gitlab/prometheus/query_variables_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' describe Gitlab::Prometheus::QueryVariables do describe '.call' do + let(:project) { environment.project } let(:environment) { create(:environment) } let(:slug) { environment.slug } @@ -21,13 +22,32 @@ describe Gitlab::Prometheus::QueryVariables do end context 'with deployment platform' do - let(:kube_namespace) { environment.deployment_platform.actual_namespace } + context 'with project cluster' do + let(:kube_namespace) { environment.deployment_platform.actual_namespace } - before do - create(:cluster, :provided_by_user, projects: [environment.project]) + before do + create(:cluster, :project, :provided_by_user, projects: [project]) + end + + it { is_expected.to include(kube_namespace: kube_namespace) } end - it { is_expected.to include(kube_namespace: kube_namespace) } + context 'with group cluster' do + let(:cluster) { create(:cluster, :group, :provided_by_user, groups: [group]) } + let(:group) { create(:group) } + let(:project2) { create(:project) } + let(:kube_namespace) { k8s_ns.namespace } + + let!(:k8s_ns) { create(:cluster_kubernetes_namespace, cluster: cluster, project: project) } + let!(:k8s_ns2) { create(:cluster_kubernetes_namespace, cluster: cluster, project: project2) } + + before do + group.projects << project + group.projects << project2 + end + + it { is_expected.to include(kube_namespace: kube_namespace) } + end end end end From 94148297a4bcd994c576487aa46c5433b684dc9a Mon Sep 17 00:00:00 2001 From: Rajat Jain Date: Mon, 29 Apr 2019 13:22:25 +0530 Subject: [PATCH 20/73] Move EE differences for projects issue --- app/views/projects/issues/_issue.html.haml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml index 0d8d7123a01..9293aa1b309 100644 --- a/app/views/projects/issues/_issue.html.haml +++ b/app/views/projects/issues/_issue.html.haml @@ -39,6 +39,8 @@ - presented_labels_sorted_by_title(issue.labels, issue.project).each do |label| = link_to_label(label, css_class: 'label-link') + = render_if_exists "projects/issues/issue_weight", issue: issue + .issuable-meta %ul.controls - if issue.closed? From d80080b5cb6721e8d08d9cb8702c59730215467a Mon Sep 17 00:00:00 2001 From: Walmyr Lima Date: Mon, 29 Apr 2019 13:34:01 +0200 Subject: [PATCH 21/73] Move test method to the bottom The reason for the change is that reading the code it should be si- milar to reading a newspaper, where high-level information is at the top, like the title and summary of the news, and low level, or more specific information, are at the bottom. This improves code readability. --- .../3_create/wiki/create_edit_clone_push_wiki_spec.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb b/qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb index 309ae6cd986..e689ba4c69c 100644 --- a/qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb @@ -3,11 +3,6 @@ module QA context 'Create' do describe 'Wiki management' do - def validate_content(content) - expect(page).to have_content('Wiki was successfully updated') - expect(page).to have_content(/#{content}/) - end - it 'user creates, edits, clones, and pushes to the wiki' do Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.perform(&:sign_in_using_credentials) @@ -38,6 +33,11 @@ module QA expect(page).to have_content('My Third Wiki Content') end + + def validate_content(content) + expect(page).to have_content('Wiki was successfully updated') + expect(page).to have_content(/#{content}/) + end end end end From ed3a2fc8d7fef157706859dd009e1662fdc3d4b5 Mon Sep 17 00:00:00 2001 From: Shinya Maeda Date: Thu, 25 Apr 2019 15:23:39 +0700 Subject: [PATCH 22/73] Fix CI_COMMIT_REF_NAME and SLUG variable With Pipelines for Merge Requests feature, users cannout keep using $CI_COMMIT_REF_NAME and _SLUG predefined variables for dynamic environments. We fix this problem by explicitly looking at the source ref. --- app/models/ci/bridge.rb | 3 +- app/models/ci/build.rb | 3 +- app/models/ci/pipeline.rb | 12 +++++ app/models/concerns/ci/contextable.rb | 8 ++-- app/models/concerns/ci/pipeline_delegator.rb | 21 ++++++++ app/models/concerns/has_ref.rb | 3 ++ .../fix-ci-commit-ref-name-and-slug.yml | 5 ++ spec/models/ci/bridge_spec.rb | 2 + spec/models/ci/build_spec.rb | 18 +++++++ spec/models/ci/pipeline_spec.rb | 48 +++++++++++++++++++ 10 files changed, 115 insertions(+), 8 deletions(-) create mode 100644 app/models/concerns/ci/pipeline_delegator.rb create mode 100644 changelogs/unreleased/fix-ci-commit-ref-name-and-slug.yml diff --git a/app/models/ci/bridge.rb b/app/models/ci/bridge.rb index 0d8d7d95791..644716ba8e7 100644 --- a/app/models/ci/bridge.rb +++ b/app/models/ci/bridge.rb @@ -4,6 +4,7 @@ module Ci class Bridge < CommitStatus include Ci::Processable include Ci::Contextable + include Ci::PipelineDelegator include Importable include AfterCommitQueue include HasRef @@ -13,8 +14,6 @@ module Ci belongs_to :trigger_request validates :ref, presence: true - delegate :merge_request_event?, to: :pipeline - def self.retry(bridge, current_user) raise NotImplementedError end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index e5236051118..5a2ead41578 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -6,6 +6,7 @@ module Ci include Ci::Processable include Ci::Metadatable include Ci::Contextable + include Ci::PipelineDelegator include TokenAuthenticatable include AfterCommitQueue include ObjectStorage::BackgroundMove @@ -49,8 +50,6 @@ module Ci delegate :terminal_specification, to: :runner_session, allow_nil: true delegate :gitlab_deploy_token, to: :project delegate :trigger_short_token, to: :trigger_request, allow_nil: true - delegate :merge_request_event?, :merge_request_ref?, - :legacy_detached_merge_request_pipeline?, to: :pipeline ## # Since Gitlab 11.5, deployments records started being created right after diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index bbd21eb0e78..2b7835d7fab 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -759,6 +759,18 @@ module Ci user == current_user end + def source_ref + if triggered_by_merge_request? + merge_request.source_branch + else + ref + end + end + + def source_ref_slug + Gitlab::Utils.slugify(source_ref.to_s) + end + private def ci_yaml_from_repo diff --git a/app/models/concerns/ci/contextable.rb b/app/models/concerns/ci/contextable.rb index 4986a42dbd2..e1d5ce7f7d4 100644 --- a/app/models/concerns/ci/contextable.rb +++ b/app/models/concerns/ci/contextable.rb @@ -70,8 +70,8 @@ module Ci variables.append(key: 'CI_COMMIT_SHA', value: sha) variables.append(key: 'CI_COMMIT_SHORT_SHA', value: short_sha) variables.append(key: 'CI_COMMIT_BEFORE_SHA', value: before_sha) - variables.append(key: 'CI_COMMIT_REF_NAME', value: ref) - variables.append(key: 'CI_COMMIT_REF_SLUG', value: ref_slug) + variables.append(key: 'CI_COMMIT_REF_NAME', value: source_ref) + variables.append(key: 'CI_COMMIT_REF_SLUG', value: source_ref_slug) variables.append(key: "CI_COMMIT_TAG", value: ref) if tag? variables.append(key: "CI_PIPELINE_TRIGGERED", value: 'true') if trigger_request variables.append(key: "CI_JOB_MANUAL", value: 'true') if action? @@ -85,8 +85,8 @@ module Ci Gitlab::Ci::Variables::Collection.new.tap do |variables| variables.append(key: 'CI_BUILD_REF', value: sha) variables.append(key: 'CI_BUILD_BEFORE_SHA', value: before_sha) - variables.append(key: 'CI_BUILD_REF_NAME', value: ref) - variables.append(key: 'CI_BUILD_REF_SLUG', value: ref_slug) + variables.append(key: 'CI_BUILD_REF_NAME', value: source_ref) + variables.append(key: 'CI_BUILD_REF_SLUG', value: source_ref_slug) variables.append(key: 'CI_BUILD_NAME', value: name) variables.append(key: 'CI_BUILD_STAGE', value: stage) variables.append(key: "CI_BUILD_TAG", value: ref) if tag? diff --git a/app/models/concerns/ci/pipeline_delegator.rb b/app/models/concerns/ci/pipeline_delegator.rb new file mode 100644 index 00000000000..dbc5ed1bc9a --- /dev/null +++ b/app/models/concerns/ci/pipeline_delegator.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +## +# This module is mainly used by child associations of `Ci::Pipeline` that needs to look up +# single source of truth. For example, `Ci::Build` has `git_ref` method, which behaves +# slightly different from `Ci::Pipeline`'s `git_ref`. This is very confusing as +# the system could behave differently time to time. +# We should have a single interface in `Ci::Pipeline` and access the method always. +module Ci + module PipelineDelegator + extend ActiveSupport::Concern + + included do + delegate :merge_request_event?, + :merge_request_ref?, + :source_ref, + :source_ref_slug, + :legacy_detached_merge_request_pipeline?, to: :pipeline + end + end +end diff --git a/app/models/concerns/has_ref.rb b/app/models/concerns/has_ref.rb index 413cd36dcaa..fa0cf5ddfd2 100644 --- a/app/models/concerns/has_ref.rb +++ b/app/models/concerns/has_ref.rb @@ -1,5 +1,8 @@ # frozen_string_literal: true +## +# We will disable `ref` and `sha` attributes in `Ci::Build` in the future +# and remove this module in favor of Ci::PipelineDelegator. module HasRef extend ActiveSupport::Concern diff --git a/changelogs/unreleased/fix-ci-commit-ref-name-and-slug.yml b/changelogs/unreleased/fix-ci-commit-ref-name-and-slug.yml new file mode 100644 index 00000000000..c34bc6d8b52 --- /dev/null +++ b/changelogs/unreleased/fix-ci-commit-ref-name-and-slug.yml @@ -0,0 +1,5 @@ +--- +title: Make `CI_COMMIT_REF_NAME` and `SLUG` variable idempotent +merge_request: 27663 +author: +type: fixed diff --git a/spec/models/ci/bridge_spec.rb b/spec/models/ci/bridge_spec.rb index 44b5af5e5aa..eb32198265b 100644 --- a/spec/models/ci/bridge_spec.rb +++ b/spec/models/ci/bridge_spec.rb @@ -10,6 +10,8 @@ describe Ci::Bridge do create(:ci_bridge, pipeline: pipeline) end + it { is_expected.to include_module(Ci::PipelineDelegator) } + describe '#tags' do it 'only has a bridge tag' do expect(bridge.tags).to eq [:bridge] diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 339483d4f7d..59ec7310391 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -28,6 +28,7 @@ describe Ci::Build do it { is_expected.to delegate_method(:merge_request_event?).to(:pipeline) } it { is_expected.to delegate_method(:merge_request_ref?).to(:pipeline) } it { is_expected.to delegate_method(:legacy_detached_merge_request_pipeline?).to(:pipeline) } + it { is_expected.to include_module(Ci::PipelineDelegator) } it { is_expected.to be_a(ArtifactMigratable) } @@ -2273,6 +2274,19 @@ describe Ci::Build do it { user_variables.each { |v| is_expected.to include(v) } } end + context 'when build belongs to a pipeline for merge request' do + let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline, source_branch: 'improve/awesome') } + let(:pipeline) { merge_request.all_pipelines.first } + let(:build) { create(:ci_build, ref: pipeline.ref, pipeline: pipeline) } + + it 'returns values based on source ref' do + is_expected.to include( + { key: 'CI_COMMIT_REF_NAME', value: 'improve/awesome', public: true, masked: false }, + { key: 'CI_COMMIT_REF_SLUG', value: 'improve-awesome', public: true, masked: false } + ) + end + end + context 'when build has an environment' do let(:environment_variables) do [ @@ -2664,6 +2678,8 @@ describe Ci::Build do ) end + let(:pipeline) { create(:ci_pipeline, project: project, ref: 'feature') } + it 'returns static predefined variables' do expect(build.variables.size).to be >= 28 expect(build.variables) @@ -2713,6 +2729,8 @@ describe Ci::Build do ) end + let(:pipeline) { create(:ci_pipeline, project: project, ref: 'feature') } + it 'does not persist the build' do expect(build).to be_valid expect(build).not_to be_persisted diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 3c823b78be7..9d0cd654f13 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -382,6 +382,54 @@ describe Ci::Pipeline, :mailer do end end + describe '#source_ref' do + subject { pipeline.source_ref } + + let(:pipeline) { create(:ci_pipeline, ref: 'feature') } + + it 'returns source ref' do + is_expected.to eq('feature') + end + + context 'when the pipeline is a detached merge request pipeline' do + let(:merge_request) { create(:merge_request) } + + let(:pipeline) do + create(:ci_pipeline, source: :merge_request_event, merge_request: merge_request, ref: merge_request.ref_path) + end + + it 'returns source ref' do + is_expected.to eq(merge_request.source_branch) + end + end + end + + describe '#source_ref_slug' do + subject { pipeline.source_ref_slug } + + let(:pipeline) { create(:ci_pipeline, ref: 'feature') } + + it 'slugifies with the source ref' do + expect(Gitlab::Utils).to receive(:slugify).with('feature') + + subject + end + + context 'when the pipeline is a detached merge request pipeline' do + let(:merge_request) { create(:merge_request) } + + let(:pipeline) do + create(:ci_pipeline, source: :merge_request_event, merge_request: merge_request, ref: merge_request.ref_path) + end + + it 'slugifies with the source ref of the merge request' do + expect(Gitlab::Utils).to receive(:slugify).with(merge_request.source_branch) + + subject + end + end + end + describe '.triggered_for_branch' do subject { described_class.triggered_for_branch(ref) } From 38078a356f86c41e1261bd900f90d50345e745f1 Mon Sep 17 00:00:00 2001 From: Peter Leitzen Date: Mon, 29 Apr 2019 14:16:54 +0200 Subject: [PATCH 23/73] Remove superfluous navigator operator --- app/models/clusters/platforms/kubernetes.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb index 94a6b224113..ca7d109d4f0 100644 --- a/app/models/clusters/platforms/kubernetes.rb +++ b/app/models/clusters/platforms/kubernetes.rb @@ -77,7 +77,7 @@ module Clusters end def namespace_for(project) - cluster.find_or_initialize_kubernetes_namespace_for_project(project)&.namespace + cluster.find_or_initialize_kubernetes_namespace_for_project(project).namespace end def predefined_variables(project:) From b389ef66d46219abbd671bcdbdfdd2fb142798b8 Mon Sep 17 00:00:00 2001 From: Peter Leitzen Date: Mon, 29 Apr 2019 14:34:50 +0200 Subject: [PATCH 24/73] Also define KubernetesService#namespace_for --- app/models/project_services/kubernetes_service.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb index f650dbd3726..fc8afa9bead 100644 --- a/app/models/project_services/kubernetes_service.rb +++ b/app/models/project_services/kubernetes_service.rb @@ -94,6 +94,10 @@ class KubernetesService < DeploymentService end end + def namespace_for(project) + actual_namespace + end + # Check we can connect to the Kubernetes API def test(*args) kubeclient = build_kube_client! From d80fcccfb1f6df295c0b37ae36ebd1084c454a2b Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Thu, 25 Apr 2019 12:43:28 -0700 Subject: [PATCH 25/73] Add full backtrace for RSpec output Attempt to debug https://gitlab.com/gitlab-org/gitlab-ce/issues/60953 --- spec/spec_helper.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 8ca4c172707..fbc5fcea7b9 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -53,6 +53,7 @@ RSpec.configure do |config| config.display_try_failure_messages = true config.infer_spec_type_from_file_location! + config.full_backtrace = true config.define_derived_metadata(file_path: %r{/spec/}) do |metadata| location = metadata[:location] From 565150205cdcb52ee4656d55c8ac3c53fc66d4f3 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Mon, 29 Apr 2019 15:26:23 +0100 Subject: [PATCH 26/73] Don't allow a relative_url_root of '/' This will fail in a few ways: 1. We might end up having a path (not a URL) starting with `//`, which will be interpreted by browsers as a protocol-relative URL. 2. Issue, MR, snippet, etc. reference parsing will look for URLs at `http://gitlab.example.com//project/...`, with the double slash preventing single slashes from working. In general, it doesn't seem like there's a valid case for this. --- config/initializers/1_settings.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 3c426cdb969..39b16a873aa 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -136,6 +136,8 @@ Settings.gitlab['ssh_host'] ||= Settings.gitlab.host Settings.gitlab['https'] = false if Settings.gitlab['https'].nil? Settings.gitlab['port'] ||= ENV['GITLAB_PORT'] || (Settings.gitlab.https ? 443 : 80) Settings.gitlab['relative_url_root'] ||= ENV['RAILS_RELATIVE_URL_ROOT'] || '' +# / is not a valid relative URL root +Settings.gitlab['relative_url_root'] = '' if Settings.gitlab['relative_url_root'] == '/' Settings.gitlab['protocol'] ||= Settings.gitlab.https ? "https" : "http" Settings.gitlab['email_enabled'] ||= true if Settings.gitlab['email_enabled'].nil? Settings.gitlab['email_from'] ||= ENV['GITLAB_EMAIL_FROM'] || "gitlab@#{Settings.gitlab.host}" From 9bc3dfea4f365d5dc0b93a5fef0418dfad971361 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Mon, 29 Apr 2019 14:42:16 +0100 Subject: [PATCH 27/73] Stop serialising project when removing todos `Todos::Destroy::EntityLeaveService#project_ids` was returning ActiveRecord objects with IDs, not simply IDs. That means we were serialising more than we needed to in Sidekiq. We can simply rename this method to `#projects` as this class doesn't use any of the superclass methods that would use `#project_ids`. --- app/services/todos/destroy/entity_leave_service.rb | 13 ++++++------- .../todos/destroy/entity_leave_service_spec.rb | 14 ++++++++++++++ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/app/services/todos/destroy/entity_leave_service.rb b/app/services/todos/destroy/entity_leave_service.rb index ebfb20132d0..4743e9b02ce 100644 --- a/app/services/todos/destroy/entity_leave_service.rb +++ b/app/services/todos/destroy/entity_leave_service.rb @@ -37,8 +37,8 @@ module Todos private def enqueue_private_features_worker - project_ids.each do |project_id| - TodosDestroyer::PrivateFeaturesWorker.perform_async(project_id, user.id) + projects.each do |project| + TodosDestroyer::PrivateFeaturesWorker.perform_async(project.id, user.id) end end @@ -62,9 +62,8 @@ module Todos end # rubocop: enable CodeReuse/ActiveRecord - override :project_ids # rubocop: disable CodeReuse/ActiveRecord - def project_ids + def projects condition = case entity when Project { id: entity.id } @@ -72,13 +71,13 @@ module Todos { namespace_id: non_member_groups } end - Project.where(condition).select(:id) + Project.where(condition) end # rubocop: enable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord def non_authorized_projects - project_ids.where('id NOT IN (?)', user.authorized_projects.select(:id)) + projects.where('id NOT IN (?)', user.authorized_projects.select(:id)) end # rubocop: enable CodeReuse/ActiveRecord @@ -110,7 +109,7 @@ module Todos authorized_reporter_projects = user .authorized_projects(Gitlab::Access::REPORTER).select(:id) - Issue.where(project_id: project_ids, confidential: true) + Issue.where(project_id: projects, confidential: true) .where('project_id NOT IN(?)', authorized_reporter_projects) .where('author_id != ?', user.id) .where('id NOT IN (?)', assigned_ids) diff --git a/spec/services/todos/destroy/entity_leave_service_spec.rb b/spec/services/todos/destroy/entity_leave_service_spec.rb index 1447b9d4126..2a553e18807 100644 --- a/spec/services/todos/destroy/entity_leave_service_spec.rb +++ b/spec/services/todos/destroy/entity_leave_service_spec.rb @@ -75,6 +75,13 @@ describe Todos::Destroy::EntityLeaveService do project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL) end + it 'enqueues the PrivateFeaturesWorker' do + expect(TodosDestroyer::PrivateFeaturesWorker) + .to receive(:perform_async).with(project.id, user.id) + + subject + end + context 'confidential issues' do context 'when a user is not an author of confidential issue' do it 'removes only confidential issues todos' do @@ -246,6 +253,13 @@ describe Todos::Destroy::EntityLeaveService do project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL) end + it 'enqueues the PrivateFeaturesWorker' do + expect(TodosDestroyer::PrivateFeaturesWorker) + .to receive(:perform_async).with(project.id, user.id) + + subject + end + context 'when user is not member' do it 'removes only confidential issues todos' do expect { subject }.to change { Todo.count }.from(5).to(4) From 03f136fcd9768e020a43309cce68f07e12bc7174 Mon Sep 17 00:00:00 2001 From: Peter Leitzen Date: Mon, 29 Apr 2019 15:01:28 +0000 Subject: [PATCH 28/73] Prefer usage of helper `expanded_by_default?` Stop using `Rails.env.test?` in views. --- app/views/clusters/clusters/show.html.haml | 2 +- app/views/groups/edit.html.haml | 2 +- app/views/groups/settings/ci_cd/show.html.haml | 2 +- app/views/projects/cleanup/_show.html.haml | 2 +- app/views/projects/default_branch/_show.html.haml | 2 +- app/views/projects/deploy_keys/_index.html.haml | 2 +- app/views/projects/edit.html.haml | 2 +- app/views/projects/mirrors/_mirror_repos.html.haml | 2 +- app/views/projects/protected_branches/shared/_index.html.haml | 2 +- app/views/projects/protected_tags/shared/_index.html.haml | 2 +- app/views/projects/settings/ci_cd/show.html.haml | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/app/views/clusters/clusters/show.html.haml b/app/views/clusters/clusters/show.html.haml index e38a16e7a1a..80d706ae3d3 100644 --- a/app/views/clusters/clusters/show.html.haml +++ b/app/views/clusters/clusters/show.html.haml @@ -4,7 +4,7 @@ - page_title _('Kubernetes Cluster') - manage_prometheus_path = edit_project_service_path(@cluster.project, 'prometheus') if @project -- expanded = Rails.env.test? +- expanded = expanded_by_default? - 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, diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml index 2f635757902..0c8f86c2822 100644 --- a/app/views/groups/edit.html.haml +++ b/app/views/groups/edit.html.haml @@ -1,6 +1,6 @@ - breadcrumb_title "General Settings" - @content_class = "limit-container-width" unless fluid_layout -- expanded = Rails.env.test? +- expanded = expanded_by_default? %section.settings.gs-general.no-animate#js-general-settings{ class: ('expanded') } diff --git a/app/views/groups/settings/ci_cd/show.html.haml b/app/views/groups/settings/ci_cd/show.html.haml index d0f5cd94002..d21496ee0aa 100644 --- a/app/views/groups/settings/ci_cd/show.html.haml +++ b/app/views/groups/settings/ci_cd/show.html.haml @@ -1,7 +1,7 @@ - breadcrumb_title "CI / CD Settings" - page_title "CI / CD" -- expanded = Rails.env.test? +- expanded = expanded_by_default? %section.settings#ci-variables.no-animate{ class: ('expanded' if expanded) } .settings-header diff --git a/app/views/projects/cleanup/_show.html.haml b/app/views/projects/cleanup/_show.html.haml index 888be4ee282..ed3c9890efd 100644 --- a/app/views/projects/cleanup/_show.html.haml +++ b/app/views/projects/cleanup/_show.html.haml @@ -1,4 +1,4 @@ -- expanded = Rails.env.test? +- expanded = expanded_by_default? %section.settings.no-animate#cleanup{ class: ('expanded' if expanded) } .settings-header diff --git a/app/views/projects/default_branch/_show.html.haml b/app/views/projects/default_branch/_show.html.haml index ff6a9d49a61..59efcde5825 100644 --- a/app/views/projects/default_branch/_show.html.haml +++ b/app/views/projects/default_branch/_show.html.haml @@ -1,4 +1,4 @@ -- expanded = Rails.env.test? +- expanded = expanded_by_default? %section.settings.no-animate#default-branch-settings{ class: ('expanded' if expanded) } .settings-header diff --git a/app/views/projects/deploy_keys/_index.html.haml b/app/views/projects/deploy_keys/_index.html.haml index 24d665761cc..fcf27351a21 100644 --- a/app/views/projects/deploy_keys/_index.html.haml +++ b/app/views/projects/deploy_keys/_index.html.haml @@ -1,4 +1,4 @@ -- expanded = Rails.env.test? +- expanded = expanded_by_default? %section.qa-deploy-keys-settings.settings.no-animate#js-deploy-keys-settings{ class: ('expanded' if expanded) } .settings-header %h4 diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index c04530dc62c..c15b84d0aac 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -1,7 +1,7 @@ - breadcrumb_title _("General Settings") - page_title _("General") - @content_class = "limit-container-width" unless fluid_layout -- expanded = Rails.env.test? +- expanded = expanded_by_default? %section.settings.general-settings.no-animate.expanded#js-general-settings .settings-header diff --git a/app/views/projects/mirrors/_mirror_repos.html.haml b/app/views/projects/mirrors/_mirror_repos.html.haml index a1ec2c887c2..0cd00d3e708 100644 --- a/app/views/projects/mirrors/_mirror_repos.html.haml +++ b/app/views/projects/mirrors/_mirror_repos.html.haml @@ -1,4 +1,4 @@ -- expanded = Rails.env.test? +- expanded = expanded_by_default? - protocols = Gitlab::UrlSanitizer::ALLOWED_SCHEMES.join('|') %section.settings.project-mirror-settings.js-mirror-settings.no-animate.qa-mirroring-repositories-settings#js-push-remote-settings{ class: ('expanded' if expanded) } diff --git a/app/views/projects/protected_branches/shared/_index.html.haml b/app/views/projects/protected_branches/shared/_index.html.haml index 539b184e5c2..63748d8d85f 100644 --- a/app/views/projects/protected_branches/shared/_index.html.haml +++ b/app/views/projects/protected_branches/shared/_index.html.haml @@ -1,4 +1,4 @@ -- expanded = Rails.env.test? +- expanded = expanded_by_default? %section.qa-protected-branches-settings.settings.no-animate#js-protected-branches-settings{ class: ('expanded' if expanded) } .settings-header diff --git a/app/views/projects/protected_tags/shared/_index.html.haml b/app/views/projects/protected_tags/shared/_index.html.haml index 9a50a51e4be..b0c87ac8c17 100644 --- a/app/views/projects/protected_tags/shared/_index.html.haml +++ b/app/views/projects/protected_tags/shared/_index.html.haml @@ -1,4 +1,4 @@ -- expanded = Rails.env.test? +- expanded = expanded_by_default? %section.settings.no-animate#js-protected-tags-settings{ class: ('expanded' if expanded) } .settings-header diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml index 548b7c06867..5e3e1076c2c 100644 --- a/app/views/projects/settings/ci_cd/show.html.haml +++ b/app/views/projects/settings/ci_cd/show.html.haml @@ -2,7 +2,7 @@ - page_title _("CI / CD Settings") - page_title _("CI / CD") -- expanded = Rails.env.test? +- expanded = expanded_by_default? - general_expanded = @project.errors.empty? ? expanded : true %section.settings#js-general-pipeline-settings.no-animate{ class: ('expanded' if general_expanded) } From 054323aae5ef118bb1324f94e0756d60bf3954aa Mon Sep 17 00:00:00 2001 From: Lukas 'Eipi' Eipert Date: Mon, 29 Apr 2019 15:27:26 +0000 Subject: [PATCH 29/73] Update dependency @gitlab/ui to ^3.7.0 --- package.json | 2 +- yarn.lock | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index d2ee29fe06d..7981ec850a2 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "@babel/preset-env": "^7.3.1", "@gitlab/csslab": "^1.9.0", "@gitlab/svgs": "^1.59.0", - "@gitlab/ui": "^3.5.0", + "@gitlab/ui": "^3.7.0", "apollo-cache-inmemory": "^1.5.1", "apollo-client": "^2.5.1", "apollo-upload-client": "^10.0.0", diff --git a/yarn.lock b/yarn.lock index a0446b652ac..d97659ab5fa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -663,12 +663,13 @@ resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.59.0.tgz#affcf9596d736836d37469bb4aea2226ac03e087" integrity sha512-dokGyyLRRsoBKO70KP1g+ZsDGyTK/RIHWDmvWI6Bx5AxQ3UqAzVXn2OIb3owjJAexyRG1uBmJrriiVVyHznQ4g== -"@gitlab/ui@^3.5.0": - version "3.5.0" - resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-3.5.0.tgz#31ecfc16e3f7663545f31ddf07e02bba96a6d138" - integrity sha512-eDD++hhGJuH59g2QcGshuou9/NLcLfse4Abm9KOIWIaYI3NPWW2KRGwLHPB6H0d5W0/X5pyWYQvXgF7JE2ZXbA== +"@gitlab/ui@^3.7.0": + version "3.7.0" + resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-3.7.0.tgz#8d0892ae54ddcb3c309bd970c57a433af6098edf" + integrity sha512-DEIPfem9P5j0DyzZp0M62SbLQu1D4feiNO0oAYN8bJrgiMC8H3VEJwiyplNItSwFYa985O1xOr3B81eTiZEWDQ== dependencies: "@babel/standalone" "^7.0.0" + "@gitlab/vue-toasted" "^1.2.1" bootstrap-vue "^2.0.0-rc.11" copy-to-clipboard "^3.0.8" echarts "^4.2.0-rc.2" @@ -678,7 +679,11 @@ url-search-params-polyfill "^5.0.0" vue "^2.5.21" vue-loader "^15.4.2" - vue-toasted "^1.1.26" + +"@gitlab/vue-toasted@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@gitlab/vue-toasted/-/vue-toasted-1.2.1.tgz#f407b5aa710863e5b7f021f4a1f66160331ab263" + integrity sha512-ve2PLxKqrwNpsd+4bV5zGJT5+H5N/VJBZoFS2Vp1mH5cUDBYIHTzDmbS6AbBGUDh0F3TxmFMiqfXfpO/1VjBNQ== "@mrmlnc/readdir-enhanced@^2.2.1": version "2.2.1" @@ -10983,11 +10988,6 @@ vue-template-es2015-compiler@^1.9.0: resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825" integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw== -vue-toasted@^1.1.26: - version "1.1.26" - resolved "https://registry.yarnpkg.com/vue-toasted/-/vue-toasted-1.1.26.tgz#1333d1a42157ab78389c3810023a49ba94e69c7b" - integrity sha512-Z4/gfPcqdzsRvif7UITrZOkh3C6jm0yQKJyr9kX31IGWXor5dNipE1Sc5SnlL5RLmY7vlLa+SqIjc9Gbpy7V0g== - vue-virtual-scroll-list@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/vue-virtual-scroll-list/-/vue-virtual-scroll-list-1.3.1.tgz#efcb83d3a3dcc69cd886fa4de1130a65493e8f76" From c7569a4942230b31e5d5163a1869858c0e7b1a61 Mon Sep 17 00:00:00 2001 From: Brandon Labuschagne Date: Mon, 29 Apr 2019 15:33:00 +0000 Subject: [PATCH 30/73] Internationalisation of cycle_analytics directory This is one of many MRs opened in order to improve the overall internationalisation of the GitLab codebase. i18n documentation https://docs.gitlab.com/ee/development/i18n/externalization.html --- .../javascripts/cycle_analytics/cycle_analytics_bundle.js | 3 ++- locale/gitlab.pot | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js index 4de425b48e7..3f0a9f2602c 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js @@ -12,6 +12,7 @@ import stageStagingComponent from './components/stage_staging_component.vue'; import stageTestComponent from './components/stage_test_component.vue'; import CycleAnalyticsService from './cycle_analytics_service'; import CycleAnalyticsStore from './cycle_analytics_store'; +import { __ } from '~/locale'; Vue.use(Translate); @@ -61,7 +62,7 @@ export default () => { methods: { handleError() { this.store.setErrorState(true); - return new Flash('There was an error while fetching cycle analytics data.'); + return new Flash(__('There was an error while fetching cycle analytics data.')); }, initDropdown() { const $dropdown = $('.js-ca-dropdown'); diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 0a5cd6730e6..78d50c74eac 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -9156,6 +9156,9 @@ msgstr "" msgid "There was an error when unsubscribing from this label." msgstr "" +msgid "There was an error while fetching cycle analytics data." +msgstr "" + msgid "There was an error with the reCAPTCHA. Please solve the reCAPTCHA again." msgstr "" From 42980a55b750771874cff481b4bb2bde4b140b0b Mon Sep 17 00:00:00 2001 From: GitalyBot Date: Mon, 29 Apr 2019 16:16:52 +0000 Subject: [PATCH 31/73] Upgrade Gitaly to v1.36.0 --- GITALY_SERVER_VERSION | 2 +- changelogs/unreleased/gitaly-version-v1.36.0.yml | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/gitaly-version-v1.36.0.yml diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index a2d87226ac2..39fc130ef85 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -1.35.0 \ No newline at end of file +1.36.0 diff --git a/changelogs/unreleased/gitaly-version-v1.36.0.yml b/changelogs/unreleased/gitaly-version-v1.36.0.yml new file mode 100644 index 00000000000..22fdca8da80 --- /dev/null +++ b/changelogs/unreleased/gitaly-version-v1.36.0.yml @@ -0,0 +1,5 @@ +--- +title: Upgrade to Gitaly v1.36.0 +merge_request: 27831 +author: +type: changed From 042370ad55ff86c04ba6a9e02196a1d80265b642 Mon Sep 17 00:00:00 2001 From: Peter Leitzen Date: Mon, 29 Apr 2019 17:14:46 +0000 Subject: [PATCH 32/73] Upgrade letter_opener_web to support Rails 5.1 Before this commit using `/rails/letter_opener` in `development` environment failed with: undefined method `before_filter' See https://github.com/fgrehm/letter_opener_web/issues/68 This commit upgrades `letter_opener_web` to 1.3.4 so Rails 5.1 is supported. --- Gemfile | 2 +- Gemfile.lock | 6 +++--- changelogs/unreleased/pl-upgrade-letter_opener_web.yml | 5 +++++ 3 files changed, 9 insertions(+), 4 deletions(-) create mode 100644 changelogs/unreleased/pl-upgrade-letter_opener_web.yml diff --git a/Gemfile b/Gemfile index 6654b285a72..95d65ec30c7 100644 --- a/Gemfile +++ b/Gemfile @@ -309,7 +309,7 @@ group :development do gem 'foreman', '~> 0.84.0' gem 'brakeman', '~> 4.2', require: false - gem 'letter_opener_web', '~> 1.3.0' + gem 'letter_opener_web', '~> 1.3.4' gem 'rblineprof', '~> 0.3.6', platform: :mri, require: false # Better errors handler diff --git a/Gemfile.lock b/Gemfile.lock index ceece1da8d7..c08f0c24ba6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -444,9 +444,9 @@ GEM rest-client (~> 2.0) launchy (2.4.3) addressable (~> 2.3) - letter_opener (1.4.1) + letter_opener (1.7.0) launchy (~> 2.2) - letter_opener_web (1.3.0) + letter_opener_web (1.3.4) actionmailer (>= 3.2) letter_opener (~> 1.0) railties (>= 3.2) @@ -1093,7 +1093,7 @@ DEPENDENCIES kaminari (~> 1.0) knapsack (~> 1.17) kubeclient (~> 4.2.2) - letter_opener_web (~> 1.3.0) + letter_opener_web (~> 1.3.4) license_finder (~> 5.4) licensee (~> 8.9) lograge (~> 0.5) diff --git a/changelogs/unreleased/pl-upgrade-letter_opener_web.yml b/changelogs/unreleased/pl-upgrade-letter_opener_web.yml new file mode 100644 index 00000000000..9891344215a --- /dev/null +++ b/changelogs/unreleased/pl-upgrade-letter_opener_web.yml @@ -0,0 +1,5 @@ +--- +title: Upgrade letter_opener_web to support Rails 5.1 +merge_request: 27829 +author: +type: fixed From db2eefba1d7cb4b1bcab5f6efcd7d855ddcfe118 Mon Sep 17 00:00:00 2001 From: Brandon Labuschagne Date: Mon, 29 Apr 2019 18:09:11 +0000 Subject: [PATCH 33/73] Internationalisation of vue_shared directory This is one of many MRs opened in order to improve the overall internationalisation of the GitLab codebase. This commit only targets the Vanilla JS files. i18n documentation https://docs.gitlab.com/ee/development/i18n/externalization.html --- .../vue_shared/components/content_viewer/lib/viewer_utils.js | 4 +++- locale/gitlab.pot | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js b/app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js index f01a51da0b3..ba63683f5c0 100644 --- a/app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js +++ b/app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js @@ -1,10 +1,12 @@ +import { __ } from '~/locale'; + const viewers = { image: { id: 'image', }, markdown: { id: 'markdown', - previewTitle: 'Preview Markdown', + previewTitle: __('Preview Markdown'), }, }; diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 78d50c74eac..ada9229553c 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -6688,6 +6688,9 @@ msgstr "" msgid "Preview" msgstr "" +msgid "Preview Markdown" +msgstr "" + msgid "Preview changes" msgstr "" From 2d5b7a4b7b5644e93e7a016f26882076a918cf09 Mon Sep 17 00:00:00 2001 From: Brandon Labuschagne Date: Mon, 29 Apr 2019 18:17:14 +0000 Subject: [PATCH 34/73] Internationalisation of javascript/m* directories This is one of many MRs opened in order to improve the overall internationalisation of the GitLab codebase. i18n documentation https://docs.gitlab.com/ee/development/i18n/externalization.html --- app/assets/javascripts/mirrors/ssh_mirror.js | 2 +- .../javascripts/monitoring/services/monitoring_service.js | 4 ++-- app/assets/javascripts/mr_popover/constants.js | 8 +++++--- locale/gitlab.pot | 6 ++++++ 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/mirrors/ssh_mirror.js b/app/assets/javascripts/mirrors/ssh_mirror.js index 547c078ec55..f7e80950803 100644 --- a/app/assets/javascripts/mirrors/ssh_mirror.js +++ b/app/assets/javascripts/mirrors/ssh_mirror.js @@ -290,7 +290,7 @@ export default class SSHMirror { this.setSSHPublicKey(data.import_data_attributes.ssh_public_key); }) .catch(() => { - Flash(_('Unable to regenerate public ssh key.')); + Flash(__('Unable to regenerate public ssh key.')); }); } diff --git a/app/assets/javascripts/monitoring/services/monitoring_service.js b/app/assets/javascripts/monitoring/services/monitoring_service.js index 5fcc2c8cfac..1efa5189996 100644 --- a/app/assets/javascripts/monitoring/services/monitoring_service.js +++ b/app/assets/javascripts/monitoring/services/monitoring_service.js @@ -1,7 +1,7 @@ import axios from '../../lib/utils/axios_utils'; import statusCodes from '../../lib/utils/http_status'; import { backOff } from '../../lib/utils/common_utils'; -import { s__ } from '../../locale'; +import { s__, __ } from '../../locale'; const MAX_REQUESTS = 3; @@ -15,7 +15,7 @@ function backOffRequest(makeRequestCallback) { if (requestCounter < MAX_REQUESTS) { next(); } else { - stop(new Error('Failed to connect to the prometheus server')); + stop(new Error(__('Failed to connect to the prometheus server'))); } } else { stop(resp); diff --git a/app/assets/javascripts/mr_popover/constants.js b/app/assets/javascripts/mr_popover/constants.js index 433df844c80..c13c417cc18 100644 --- a/app/assets/javascripts/mr_popover/constants.js +++ b/app/assets/javascripts/mr_popover/constants.js @@ -1,10 +1,12 @@ +import { __ } from '~/locale'; + export const mrStates = { merged: 'merged', closed: 'closed', }; export const humanMRStates = { - merged: 'Merged', - closed: 'Closed', - open: 'Open', + merged: __('Merged'), + closed: __('Closed'), + open: __('Open'), }; diff --git a/locale/gitlab.pot b/locale/gitlab.pot index ada9229553c..c8b583575c8 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -3958,6 +3958,9 @@ msgstr "" msgid "Failed to check related branches." msgstr "" +msgid "Failed to connect to the prometheus server" +msgstr "" + msgid "Failed to create repository via gitlab-shell" msgstr "" @@ -9805,6 +9808,9 @@ msgstr "" msgid "Unable to load the diff. %{button_try_again}" msgstr "" +msgid "Unable to regenerate public ssh key." +msgstr "" + msgid "Unable to schedule a pipeline to run immediately" msgstr "" From dfc9d0b6940022b82a526e642ed89a9d5422d494 Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Thu, 25 Apr 2019 17:23:30 -0700 Subject: [PATCH 35/73] Exclude reviewers with OOO in status --- danger/roulette/Dangerfile | 42 ++++++++++++++++++++++++++++++-------- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/danger/roulette/Dangerfile b/danger/roulette/Dangerfile index e6820f49ee2..27763052192 100644 --- a/danger/roulette/Dangerfile +++ b/danger/roulette/Dangerfile @@ -31,26 +31,52 @@ Please consider creating a merge request to for them. MARKDOWN -def spin(team, project, category, branch_name) +def spin_for_category(team, project, category, branch_name) rng = Random.new(Digest::MD5.hexdigest(branch_name).to_i(16)) reviewers = team.select { |member| member.reviewer?(project, category) } traintainers = team.select { |member| member.traintainer?(project, category) } maintainers = team.select { |member| member.maintainer?(project, category) } - # TODO: filter out people who are currently not in the office - # https://gitlab.com/gitlab-org/gitlab-ce/issues/57652 - # # TODO: take CODEOWNERS into account? # https://gitlab.com/gitlab-org/gitlab-ce/issues/57653 # Make traintainers have triple the chance to be picked as a reviewer - reviewer = (reviewers + traintainers + traintainers).sample(random: rng) - maintainer = maintainers.sample(random: rng) + reviewer = spin_for_person(reviewers + traintainers + traintainers, random: rng) + maintainer = spin_for_person(maintainers, random: rng) "| #{helper.label_for_category(category)} | #{reviewer&.markdown_name} | #{maintainer&.markdown_name} |" end +# Known issue: If someone is rejected due to OOO, and then becomes not OOO, the +# selection will change on next spin +def spin_for_person(people, random:) + person = nil + people = people.dup + + people.size.times do + person = people.sample(random: random) + + return person unless out_of_office?(person) + + people -= [person] + end +end + +def out_of_office?(person) + username = CGI.escape(person.username) + api_endpoint = "https://gitlab.com/api/v4/users/#{username}/status" + response = HTTParty.get(api_endpoint) # rubocop:disable Gitlab/HTTParty + + if response.code == 200 + response["message"]&.match(/OOO/i) + else + false # this is no worse than not checking for OOO + end +rescue + false +end + def build_list(items) list = items.map { |filename| "* `#{filename}`" }.join("\n") @@ -63,7 +89,7 @@ end changes = helper.changes_by_category -# Ignore any files that are known but uncategoried. Prompt for any unknown files +# Ignore any files that are known but uncategorized. Prompt for any unknown files changes.delete(:none) categories = changes.keys - [:unknown] @@ -92,7 +118,7 @@ if changes.any? && !gitlab.mr_labels.include?('single codebase') && !gitlab.mr_l project = helper.project_name unknown = changes.fetch(:unknown, []) - rows = categories.map { |category| spin(team, project, category, canonical_branch_name) } + rows = categories.map { |category| spin_for_category(team, project, category, canonical_branch_name) } markdown(MESSAGE) markdown(CATEGORY_TABLE_HEADER + rows.join("\n")) unless rows.empty? From 9cff6716a07114ed94a18cc56ad138a8cd091612 Mon Sep 17 00:00:00 2001 From: Marcia Ramos Date: Tue, 30 Apr 2019 00:50:53 +0000 Subject: [PATCH 36/73] Merge feature set tables into one Plus: add note for .com users --- doc/ci/README.md | 168 +++++++++++++++---------- doc/ci/img/add_file_template_11_10.png | Bin 0 -> 55910 bytes 2 files changed, 100 insertions(+), 68 deletions(-) create mode 100644 doc/ci/img/add_file_template_11_10.png diff --git a/doc/ci/README.md b/doc/ci/README.md index 123a5e50f14..440a79c7782 100644 --- a/doc/ci/README.md +++ b/doc/ci/README.md @@ -5,71 +5,113 @@ description: "Learn how to use GitLab CI/CD, the GitLab built-in Continuous Inte # GitLab Continuous Integration (GitLab CI/CD) -GitLab CI/CD is GitLab's built-in tool for software development using continuous methodology: +GitLab CI/CD is a tool built into GitLab for software development +through the [continuous methodologies](introduction/index.md#introduction-to-cicd-methodologies): -- Continuous integration (CI). -- Continuous delivery and deployment (CD). - -Within the [DevOps lifecycle](../README.md#the-entire-devops-lifecycle), GitLab CI/CD spans -the [Verify (CI)](../README.md#verify) and [Release (CD)](../README.md#release) stages. +- Continuous Integration (CI) +- Continuous Delivery (CD) +- Continuous Deployment (CD) ## Overview -CI/CD is a vast area, so GitLab provides documentation for all levels of expertise. Consult the following table to find the right documentation for you: +Continuous Integration works by pushing small code chunks to your +application's code base hosted in a Git repository, and, to every +push, run a pipeline of scripts to build, test, and validate the +code changes before merging them into the main branch. -| Level of expertise | Resource | -|:------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------| -| New to the concepts of CI and CD | For a high-level overview, read an [introduction to CI/CD with GitLab](introduction/index.md). | -| Familiar with GitLab CI/CD concepts | After getting familiar with GitLab CI/CD, let us walk you through a simple example in our [getting started guide](quick_start/README.md). | -| A GitLab CI/CD expert | Jump straight to our [`.gitlab.yml`](yaml/README.md) reference. | +Continuous Delivery and Deployment consist of a step further CI, +deploying your application to production at every +push to the default branch of the repository. -Familiarity with GitLab Runner is also useful because it is responsible for running the jobs in your -CI/CD pipeline. On GitLab.com, shared Runners are enabled by default so you won't need to set this up to get started. +These methodologies allow you to catch bugs and errors early in +the development cycle, ensuring that all the code deployed to +production complies with the code standards you established for +your app. -## CI/CD with Auto DevOps +For a complete overview of these methodologies and GitLab CI/CD, +read the [Introduction to CI/CD with GitLab](introduction/index.md). -[Auto DevOps](../topics/autodevops/index.md) is the default minimum-configuration method for -implementing CI/CD. Auto DevOps: +## Getting started -- Provides simplified setup and execution of CI/CD. -- Allows GitLab to automatically detect, build, test, deploy, and monitor your applications. +GitLab CI/CD is configured by a file called `.gitlab-ci.yml` placed +at the repository's root. The scripts set in this file are executed +by the [GitLab Runner](https://docs.gitlab.com/runner/). -## Manually configured CI/CD +To get started with GitLab CI/CD, we recommend you read through +the following documents: -For complete control, you can manually configure GitLab CI/CD. +- [How GitLab CI/CD works](introduction/index.md#how-gitlab-cicd-works). +- [GitLab CI/CD basic workflow](introduction/index.md#basic-cicd-workflow). +- [Step-by-step guide for writing `.gitlab-ci.yml` for the first time](../user/project/pages/getting_started_part_four.md). -### Configuration and Usage +You can also get started by using one of the +[`.gitlab-ci.yml` templates](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/lib/gitlab/ci/templates) +available through the UI. You can use them by creating a new file, +choosing a template that suits your application, and adjusting it +to your needs: -The following topics contain configuration and usage information for all features of GitLab CI/CD: +![Use a .gitlab-ci.yml template](img/add_file_template_11_10.png) -| Topic | Description | -|:--------------------------------------------------------------------------------------------------------------------------|:-------------------------------------------------------------------------------------------| -| [Creating and using CI/CD pipelines](pipelines.md) | Understand, visualize, create, and use CI/CD pipelines. | -| [CI/CD Variables](variables/README.md) | Configuring and using environment variables in pipelines. | -| [Where variables can be used](variables/where_variables_can_be_used.md) | Where and how CI/CD variables can be used. | -| [User](../user/permissions.md#gitlab-cicd-permissions) and [job](../user/permissions.md#job-permissions) permissions | User access levels for performing certain CI actions. | -| [Configuring GitLab Runners](runners/README.md) | Configuring [GitLab Runner](https://docs.gitlab.com/runner/). | -| [Environments and deployments](environments.md) | Deploy the output of jobs into environments for reviewing, staging, and production. | -| [Review Apps](review_apps/index.md) | Configure GitLab CI/CD to preview code changes. | -| [Job artifacts](../user/project/pipelines/job_artifacts.md) | Using the output of jobs. | -| [Cache dependencies in GitLab CI/CD](caching/index.md) | Speed up pipelines using caching. | -| [Using Git submodules with GitLab CI](git_submodules.md) | How to run your CI jobs when using Git submodules. | -| [Using SSH keys with GitLab CI/CD](ssh_keys/README.md) | Use SSH keys in your build environment. | -| [Triggering pipelines through the API](triggers/README.md) | Use the GitLab API to trigger a pipeline. | -| [Connecting GitLab with a Kubernetes cluster](../user/project/clusters/index.md) | Integrate one or more Kubernetes clusters to your project. | -| [ChatOps](chatops/README.md) | Trigger CI jobs from chat, with results sent back to the channel. | -| [Interactive web terminals](interactive_web_terminal/index.md) | Open an interactive web terminal to debug the running jobs. | -| [Optimizing GitLab for large repositories](large_repositories/index.md) | Useful tips on how to optimize GitLab and GitLab Runner for big repositories. | -| [Deploy Boards](https://docs.gitlab.com/ee/user/project/deploy_boards.html) **[PREMIUM]** | Check the current health and status of each CI/CD environment running on Kubernetes. | -| [GitLab CI/CD for external repositories](https://docs.gitlab.com/ee/ci/ci_cd_for_external_repos/index.html) **[PREMIUM]** | Get the benefits of GitLab CI/CD combined with repositories in GitHub and BitBucket Cloud. | +For a broader overview, see the [CI/CD getting started](quick_start/README.md) guide. -### GitLab Pages +Once you're familiar with how GitLab CI/CD works, see the +[`. gitlab-ci.yml` full reference](yaml/README.md) +for all the attributes you can set and use. -GitLab CI/CD can be used to build and host static websites. For more information, see the -documentation on [GitLab Pages](../user/project/pages/index.md), -or dive right into the [CI/CD step-by-step guide for Pages](../user/project/pages/getting_started_part_four.md). +NOTE: **Note:** +GitLab CI/CD and [shared runners](runners/README.md#shared-specific-and-group-runners) are enabled in GitLab.com and available for all users, limited only to the [user's pipelines quota](https://docs.gitlab.com/ee/user/admin_area/settings/continuous_integration.html#extra-shared-runners-pipeline-minutes-quota). -### Examples +## GitLab CI/CD configuration + +GitLab CI/CD supports numerous configuration options: + +| Configuration | Description | +|:--- |:--- | +| [Pipelines](pipelines.md) | Structure your CI/CD process through pipelines. | +| [Environment variables](variables/README.md) | Reuse values based on a variable/value key pair. | +| [Environments](environments.md) | Deploy your application to different environments (e.g., staging, production). | +| [Job artifacts](../user/project/pipelines/job_artifacts.md) | Output, use, and reuse job artifacts. | +| [Cache dependencies](caching/index.md) | Cache your dependencies for a faster execution. | +| [Schedule pipelines](../user/project/pipelines/schedules.md) | Schedule pipelines to run as often as you need. | +| [Custom path for `.gitlab-ci.yml`](../user/project/pipelines/settings.md#custom-ci-config-path) | Define a custom path for the CI/CD configuration file. | +| [Git submodules for CI/CD](git_submodules.md) | Configure jobs for using Git submodules. | +| [SSH keys for CI/CD](ssh_keys/README.md) | Using SSH keys in your CI pipelines. | +| [Pipelines triggers](triggers/README.md) | Trigger pipelines through the API. | +| [Integrate with Kubernetes clusters](../user/project/clusters/index.md) | Connect your project to Google Kubernetes Engine (GKE) or an existing Kubernetes cluster. | +| [GitLab Runner](https://docs.gitlab.com/runner/) | Configure your own GitLab Runners to execute your scripts. | +| [Optimize GitLab and Runner for large repositories](large_repositories/index.md) | Recommended strategies for handling large repos. | +| [`.gitlab-ci.yml` full reference](yaml/README.md) | All the attributes you can use with GitLab CI/CD. | + +Note that certain operations can only be performed according to the +[user](../user/permissions.md#gitlab-cicd-permissions) and [job](../user/permissions.md#job-permissions) permissions. + +## GitLab CI/CD feature set + +You can also use the vast GitLab CI/CD feature set to easily configure +it for specific purposes: + +| Feature | Description | +|:--- |:--- | +| [Auto Deploy](../topics/autodevops/index.md#auto-deploy) | Deploy your application to a production environment in a Kubernetes cluster. | +| [Auto DevOps](../topics/autodevops/index.md) | Set up your app's entire lifecycle. | +| [Building Docker images](docker/using_docker_build.md) | Maintain Docker-based projects using GitLab CI/CD. | +| [Canary Deployments](https://docs.gitlab.com/ee/user/project/canary_deployments.html) **[PREMIUM]** | Ship features to only a portion of your pods and let a percentage of your user base to visit the temporarily deployed feature. | +| [ChatOps](chatops/README.md) | Trigger CI jobs from chat, with results sent back to the channel. | +| [CI services](services/README.md)| Link Docker containers with your base image. | +| [Container Scanning](https://docs.gitlab.com/ee/ci/examples/container_scanning.html) **[ULTIMATE]**| Check your Docker containers for known vulnerabilities. | +| [Dependency Scanning](https://docs.gitlab.com/ee/ci/examples/dependency_scanning.html) **[ULTIMATE]**| Analyze your dependencies for known vulnerabilities. | +| [Deploy Boards](https://docs.gitlab.com/ee/user/project/deploy_boards.html) **[PREMIUM]** | Check the current health and status of each CI/CD environment running on Kubernetes. | +| [Feature Flags](https://docs.gitlab.com/ee/user/project/operations/feature_flags.html) **[PREMIUM]** | Deploy your features behind Feature Flags. | +| [GitLab CI/CD for external repositories](https://docs.gitlab.com/ee/ci/ci_cd_for_external_repos/) **[PREMIUM]** | Get the benefits of GitLab CI/CD combined with repositories in GitHub and BitBucket Cloud. | +| [GitLab Pages](../user/project/pages/index.md) | Deploy static websites. | +| [GitLab Releases](../user/project/releases/index.md) | Add release notes to Git tags. | +| [Interactive Web Terminals](interactive_web_terminal/index.md) **[CORE ONLY]** | Open an interactive web terminal to debug the running jobs. | +| [JUnit tests](junit_test_reports.md)| Identify script failures directly on merge requests. | +| [Review Apps](review_apps/index.md) | Configure GitLab CI/CD to preview code changes. | +| [Security Test reports](https://docs.gitlab.com/ee/user/project/merge_requests/#security-reports-ultimate) **[ULTIMATE]** | Check for app vulnerabilities. | +| [Using Docker images](docker/using_docker_images.md) | Use GitLab and GitLab Runner with Docker to build and test applications. | + +## GitLab CI/CD examples GitLab provides examples of configuring GitLab CI/CD in the form of: @@ -78,9 +120,10 @@ GitLab provides examples of configuring GitLab CI/CD in the form of: - [`multi-project-pipelines`](https://gitlab.com/gitlab-examples/multi-project-pipelines) for examples of implementing multi-project pipelines. - [`review-apps-nginx`](https://gitlab.com/gitlab-examples/review-apps-nginx/) provides an example of using Review Apps. -### Administration +## GitLab CI/CD administration **[CORE ONLY]** -As a GitLab administrator, you can change the default behavior of GitLab CI/CD for: +As a GitLab administrator, you can change the default behavior +of GitLab CI/CD for: - An [entire GitLab instance](../user/admin_area/settings/continuous_integration.md). - Specific projects, using [pipelines settings](../user/project/pipelines/settings.md). @@ -90,33 +133,22 @@ See also: - [How to enable or disable GitLab CI/CD](enable_or_disable_ci.md). - Other [CI administration settings](../administration/index.md#continuous-integration-settings). -### Using Docker +## References -Docker is commonly used with GitLab CI/CD. Learn more about how to to accomplish this with the following -documentation: +### Why GitLab CI/CD? -| Topic | Description | -|:-------------------------------------------------------------------------|:-------------------------------------------------------------------------| -| [Using Docker images](docker/using_docker_images.md) | Use GitLab and GitLab Runner with Docker to build and test applications. | -| [Building Docker images with GitLab CI/CD](docker/using_docker_build.md) | Maintain Docker-based projects using GitLab CI/CD. | - -Related topics include: - -- [Docker integration](docker/README.md). -- [CI services (linked Docker containers)](services/README.md). - -## Why GitLab CI/CD? - -The following articles explain reasons to use GitLab CI/CD for your CI/CD infrastructure: +The following articles explain reasons to use GitLab CI/CD +for your CI/CD infrastructure: - [Why we chose GitLab CI for our CI/CD solution](https://about.gitlab.com/2016/10/17/gitlab-ci-oohlala/) - [Building our web-app on GitLab CI](https://about.gitlab.com/2016/07/22/building-our-web-app-on-gitlab-ci/) See also the [Why CI/CD?](https://docs.google.com/presentation/d/1OGgk2Tcxbpl7DJaIOzCX4Vqg3dlwfELC3u2jEeCBbDk) presentation. -## Breaking changes +### Breaking changes -As GitLab CI/CD has evolved, certain breaking changes have been necessary. These are: +As GitLab CI/CD has evolved, certain breaking changes have +been necessary. These are: - [CI variables renaming for GitLab 9.0](variables/deprecated_variables.md#gitlab-90-renamed-variables). Read about the deprecated CI variables and what you should use for GitLab 9.0+. diff --git a/doc/ci/img/add_file_template_11_10.png b/doc/ci/img/add_file_template_11_10.png new file mode 100644 index 0000000000000000000000000000000000000000..ca04d72615b8ba71b3e8391d216fad368eee946e GIT binary patch literal 55910 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

@%?a_dbsJJ z?UJ=4CzJ3xTT*7_09mfJQD*#o-|@BlygfHLu+os&bC+E<%<50ZiXP5!V7kPnofyrs zIjQm&vOs_e=zttjXYK1?)=W zjMrm6sI>M9UnE9P<98R3uTSw4N3^SF&$$Q~Y=sGSqYW0`e#yHvT{n6rgW0^?>Uu4R zt+mOxHV`jjGp?6Y6k!d+0i3j7ub^ZJhRU$O&iPQ1KOTh+g$64c9)39T3&D%G(tgdq z!X0IXyNtx`V_mb7irw;DT`bh;K4fH+mz&k*ZVjn;|AmWCQ(#Fn3i5j}(?Z@f-moOD zACr6^%DzjxOI;emrk5f@Weq>(_(@;Ja@*M$R9j0M`XgkSzx@@SE$ouE$GE(4k0tSG zBH>r~&1>X`+c!u2_R+-4%6aaoVO?Vn)`5n~q2V60yRd0(bfvg-p1#6$&0Y<}z_0#W z*U=D4BG&!|vHWI4KPBUYS(H)Ha8C> zj3O;@>7pXFk+=W8;;5kkM%mGFc^>jCq=f=kYWa3_^`!rzfwUku{J;(7R#VMV_$_Xt zY~XM6zw-^qf_IcWO5ZoL|Nd*iPLKujYu99c%#*Wbo?Yv=g=%s(yh;?i42K zq~4&!2Cb=}NulP9=mtOxjC=1yf#pD*uml|)4olK?d7t_7oiXe4tp=ny@)^^-!Cd{! zVuH`_>%^%>V2y|GWl}^aCFW{>Hn=slNYuJ~Z2NO$9{X;~Wq4Tus;?bbt{8GZJFk9> zDklqRp@9)vD*+g2;bhpC4Aw%~Dubnsa6LVYP$7`9Zw!l%m5A~UavKX3jS&Df>CmZ4 zWN=#VQoz&e=hRZ^?S@_PV-}{DP9YB!CHrm-@R~v`9{i`pPG)11-b3dsCYHVh-IOX} z7L7mBT7lfBxb8H1yr~2?NP$j@R_GyEPKVtLn429F*+6?)^g0+@p*_@T5ab zfr23Q^mI$x#oW|Xoy!m}$dPj56Su52F>qpQ{`{hWF2;A8t2br3?<))}NN!VX!=%A! zVcI?yn;+J8O02$&F@96wk@_DqN}J`B)maR)$m|((B(W)8X(lLlbwP_2eRH`f9v`Axbqffsf;O6v+zsddclX z2|$PXFXA60SaI3buq9c%rd|T7-yic;dH$~Re^+tH|Cx{TXiI31?DqT_=eky};#)1M zB0;ck*+CN7tZbOCHVb`WXMwNxH&^vUsw)}%-mNG^EX)Mfss)G^Y-ljuu<$dr&H>{qSQbua^-V;hS zfog2z?+?+T7QgvV2Q(d;O#yM(=u#8}J6*;TPJ0p=jRdT@+(`1c z<1rJRA4CjRE`oT8&T?t)@&8XS@|SYJaKkz~ntYksHcO~*`%9B3NZdR3KCuUN+ah)R zhy4m~?=_8|_x`5>RHfA+o{yeUV+Hr*fs~m8yyrK0GVxKzOxPz{_pG=@iV|{p8a;J& z1fi;FbZG8HJ*X%hT0@-Itx4~T6nL6>uao#&Y8%0Gc?wIs-DHsPzTI2A1%@>aO=rxV z@Mc!E>{1}VCZSf92TDBr@5(DVUo1T}tUEPs@RM@0r@|=sZ}y!Fri#!U>-`#>ql~`l z!`{W8T5Emf<+Wzgu3T<^_*_={SX zW2|^-oH}ghyv_Ryio@xHPZc%g+;V>_uWiDNs-x}3Mf+FwFZF5a=|OI?l3<{cHv#wB zJE*ALzit3QSe8jBkE5`E;={~+$@@QnUWUM^&G_p{o^LpI5Oa4BIvF=8Fo7ccOgf(?!@Fs246FU z+)kW6W+np2_nr+oCWWt`Xl0B_5PQT@Ko+t8*DAi00v9GUw9sGp|NdnLi*G0eGCscr z7*Zza&}8tGF*>w3Bcwvta*-Aop`STO3!;~hKe0D~-3B!uK{wU5_y3t29)-B;Mh0i1 z_|L387*MawHC$X7&siILtnEwLIw21r%wfX{CSmNfUrE`)PM&1N%)5c z>`{Q&-nooK;J$dgXN`hH>jaU%MtDw(#_}n8m6K!K3r;J6ImO5wic2ThG(!hmUYQ&o zM6D5iNrRS|Yw7;I_o8z~4+%Lo31}$yFsSFa;?8a!RSa%!im!M5L)lkpOz@PVajh_W zAGUf`Hlx;TBR|`)3!Yi&81j=MW%)^HTg*q};iQl2@=YC)xOWa$Arvzm?&Qkh+?_hn z=1zT>P4F9DnG6%rniXvi4Tim8T;UfLUmTrTBu9PO=-X85#0OQt;D1^l4AIqM2 z*xvYNp^!0Lt-DA%(Z_h|To-p<1ZBLg#MRSL*?tP0gK{$rUs8#YP z7bd9lAP`>s(uGi!U<-DO-qaWrV9gnDBGo%yjXV*zFlV!v6pg3c6zx;u{F6r;`&0_d z%h|A2MZMl&*Xm+sIe9A9bR*&^H{334`(kFqLelYCdZvjRRdBfmGCJ?L!TvKWWPPQK z|L73XD9B_KOE{1xFR`XoR51umr$Md-y!Us6w|U6pBY47@Qt26sprX__EvP!~Wgt`Y z4tmP$PPkpy$R4rOq!r0@y`M_>i`71_6v-FV<7NT@LZM!49zde&Un(d13UMmLLBq?rgwJiBwWL+6^^|Zw# zAT9nXp)OS8%PVNE)v*jEd#`@vqMF$(qwR$k@`S{K2vP=8ad`?9-ymnI+%p1?8J=3X zV`lXDH{D$}eqTG08{M|<$xHee)@`L2XZd`*v2RG4*W}BR8Z8iJj4$!=O2db3=1K!b z^l$>Tj_4+7*giIn3vX>9KShJjJn6LQ=r*W~@P{kvYv9xM4B9GQzxRnd#Ep{YV<8>+ zBbq4g`~LUooGafx`%15>skWHG*xB6VUn)9Pk#)a1FjOQDjXYGj%$^C-{>z8j#RZUP zmDr;k(8`L&EW~^{LY`cmT+&9$YL>QgV}cVcqtbrKg?c8oMQ;Jy{i&aiI@B3n0PI?$ z;sGVXV2Jp}=Vm7Gd0g-{hG*Bf2Pm;p4mFF9^S2h?1sv1oqT*FyxqJ6G;`qCTP|WqU z{NWFVvgC!qYGhoZGzUB(TLDwFl`Qs<4qXUY0mI%=3{Fi!39|)NB(85rObnt#1teRl zL*V6a;t(xqj#FG~OYpsAN)2SQeBenmITpekCwpx`#2#|i(alsS-E%sf9nXunn}!NN zWf>luq5UK(5?LhHi7_)o8{v1>3>MP1R?q8Nh!XBy!GIA6KhHJ1szg&0$bCZQ@#vck z2iofQ^WfrJ+tS+s%HcLfEnh0I&03hVWcQ?K-|RLPN^H#QSPa}}xzZVC1OMV&S3v|m z32GAtr+djsEP1@5mB)y?lE*oJE(5|W5=j*y!|y_E-+jCe_h4)V@}m)sdQG|N<`EL^ z{cp&zPlA`K#mL}BkRK*L;YZ5acb(#&wIdq09W(^AFqUPofZzsTG%;9bjgDL%D0e-}&$O%uz2q z48Sh&NHhYWQ9115HoDel9 zVoE-u0)KDD%7@`E4JA|G2XgtIlAZIdN0DPU0O1xuI471D%7i4zyVWMeWdr4Oz-e9B z!a`v3`ix7gdpjvHFYfr@N!22is(-BG@%3IkYAt>Vpg;A#lpixV{Ji&P70Hy@Ygq(@ zs!_ca20s`G7k~ zMW&H=hf&S5S=1@5TfK_bUk@ZfLna4?z#b*uKReJ{7jkKLG%+(1A?Q0EXjD)`UWLSRLL0|M$nzTl zDZ#L_$bPEi)9@3aNr_Ni{llw@(*}FL3o|X-^77O`};RX=7ek$iE9nq z;)XOPmJ;qB7ma%(rVG4cUiwE2f@Ik4ZNx3zjn~xtjp#0Vyreb9Y5rc;a4sUUC-ql0 zSk;FxJ(k)I4ips8)GDe+)=+><#S|tF=1IH{l<4_E``=`HK1fQc+Am{p1KV@Z(zwS|VMp1KsYTjQ>V7Q?*!INuW zK&`UOMA@%FVoq*AZ4QQOdQEF1QC*FC`bcuF93H!u#|ZbYU6&ePPAPgL7pyP ze`Ma+^x{sDBN6uiHM~g@r*_S>SH1gnK2dU0EwNq6=I58sFq<_@FGsy}<*!r1gMP7@ zvl;&DcgFU1B5_m;nNMxl3bqIQISX$~v>!}te%Wm`jEy}oSbnK`qfvM6buF9q+LsKO zCiZ8GL5DVZR{J#R!J;9*1FNKEcenFpZnKBlUN**?cub6CtdMBrKwli&$oVX9!Y>hJ z+L~T_XTD8NDI@!1P~ux9P|SVMveHu7M!IxU8zuXM8dPkXl&|QG_{R$t0dw?9YP>!o zetEg9$MHnHg0W{AK=;Jka*Khkdj>f_w6u$WZA*!FLstqhuSu`NB#XuV@t+xmP|sie zED!QlZ}MZ#FhG8>C#-LW9L_^s&5ZqqA9?oM$RV9Ns7`zcP>QI~8HzeQ2{14Fh?*>5 zuKKm_E>&IWMgOlSeg};t;g}@rv%=SVFbrH+!fNTtWQe})OT#%FN_6gV9GordT;u+0&rnLPm3M(u2wZw@kUN+iV)a@TQj1Fg$W%8$Wb*;iVws~D$S{Mp&nayG#I4Lw z+ghJlSzfrB99;OOlaDg`am&AX2L&pJ7%nSXzpoTs>hD5g?NYX}n zGln%S&fn#B{za9*^O5~M-G}r-`xjcjBU9BHUujj5XeeBf;^O)qllk#+$wZCGwE?<< z`2kPq?D+Och3w6?a2aGaRLb=x_&W^BEPH+vB&5XpBGQOhIU^<1pnSYtZCKOY*|Qam zH(xF)Sbo;y6O&+G--qg|p3$Q3{F)J&6o@-F#qwIfub-Tm5(bj~-$T`C-+8R%XSr6s z63SKH{|p9QuQ8y>z3kJB)Wc&4N#XmfNTzpCZrbrp@F*Hr=1h%b;sm{ce}})k1)4m( z+~>?uNF8A-{{rO8a60~P9H=7Ft+12_%#YjWUdKM{V-l3H%&%i(DH0Kq>%7xjG9Zk8 zJtkzaIkxKjT^e3|;4>l!_QW&>{|x{CSbOWRD8DBT6afXKJ4GZ#LPF%FQCdonZm9(% z7M3niDd|vh0g-M61Sw-zPy}9Dq+N-X5R@e(lzaHr{XNh9`B*K9f!Jdf;yGkJ|0e9tO3#Ya~I0 zM5h5{eRx*Fosu2o1}7U^jypB}wD8`KSET|?zTdYn96kKD(mXc*bt{NXOO~K`!}SlZ zFiIwctcE@9hMJkQe;Yllu#q!xkhFqa})gwTC03mZRv47cFWcbvE4w;%iZf7AazE#q+jJe!bV z!P+SI{UwsmZd>mF)Lztmz;cHZ-Y+^eX9$(Z{v+-4ASCN4YU$#`fQ4SA@mhR7=X+Eb zCgdFh2;q`pHK2q4m+an58>JD$n!$m)X2P9dOZnCYB&;n2%ti436uSa`M)T3TcIH{% zjK<_*j-}tx1a!0405Z~1_3{NY{Ur>0lnSP;OKj6#=IC6ASb=+gN?+M7J*=_PFz>PH)wvo1?w+rvk;-;iQW0jY{9cpZ70Qv;Y*+qQ z&{h{=xGY7V8I7uWV8lQ$H+A<#%{^uA=ImZzU-{t~*!kMZa*Yk{4p&xTjw5{kGeD-H zBl0ON%jBEFpU8t}GK!D4KmTWs=W$05KyF)i-gsh($QG%Jd(QN;vR8B+FB#7;^Kv*m z@1gWC>AW`)Z~hcRa>nQ3FU)K2j?*h&&))L%^?W@)>U=&hBLq~DUUcp!vtEwyxkU+* z-F&LPVdMd7BDw!D#GzLk;!u||dp2K)XSK_iM({>;kRx6K@3C{}Np!C%SlN6WCM{Y91@6BQr+xP*-Hu-{)Rh4K@ysVt|FY&a@&FvM%= z;X?s3SG}G2^P;Lyi5mE7ogR*sPD@2Ip?O!GLpZUh8R)`9wwMtoC8Jxjz43TF~fj6)&T8tA$A25YSIM-22cyXGN!Zp0Ji!4wVCu#pkiPvQmmneO<}etx3PD2@JXW95wa*eT5kpS2N$@pZ7doFqYxe z=*wuQ7xRiCwBD%}p|rDu8Z@iOl6AJfdg;h;TS1q9_0ARizCy3z%cb%_sbaWPqoCE% zlh;CYq?_2Z&yMs`KJUegVb|!GeD!A-7Vd8J7nDiMPo-Q@7zOH1M-vh*dM&o+Fi$HHyWN0PoVceE zuQXa-ZFwb>wg}B9*$KX{`l)5^w-6$PR`|Fi$uR>}dZ%6Zba&;+-tsb+C%Vrh45j6i-^w!KV9Z>GjM6QxDvcmAd8!7p9$x6*yJ+RS;g4k#ByEum3GB}r2k|vHB8;5 z)l>Oo?ef%%q9=65`6>LA4_LGyP`HN(qXvahuf}$Ah_4(p?lLl6H{)u1gN&>OuC3uPA+*Xv=g<#Q z!Wq@9aQ@h{3+Xb(t{QWvV^MEyf9d^3+R@7iM!CikNKv^|rpt1&Xmg!5=hHrUA%#qJ z7L<54;huE*#?IS?NRTEamrQ2`l>PpvNr5EqSDW6-wC*a`E}*&O14*zUsxc@nF|__= zKIaggj0RRXTP=nF$tl1QZ-d5p1jM9ns-Q=CDM^RkAydk~znXQn^TURuKeArs#aq;6 ztbC@%yBkj_Wu9I}bHYmf*3Q<~{%-6#boed_IXCnuqu$U><=Oy_7O`iXj49SWI3sM! zD=EUQ3^wboY;`E(sZT$9zyl1Rv0!}z2jJD2*jmAqSTjF$N=jW3~7a}u4WeX;wJ z!WBxH5Bj^jp?TvA-okunu&NNP(-h$5sMQ&2#6;@vY|D`@p}~Wzjt&=Jzl@@ZEVBk*(g&OC8-5XwKiE zO*C*8ue7|Fm7L3X4dm+2RVvzxL&YKd9`Cnt!othYWY*tf?HL_B|9O1JQ|HcQ<^ZL# zkY;5N&!cWyfspF8$tC~4L2aJzL-kul^;Ea6>l zYilBJqn0;>I2(JX5c$fh(?{smE8S>B*cPJY{5sW2syg z{g@y1P&7M7YZl4vY)~jvrj-2mxC4=AJ#8ce1DJWiMTg)X0o}4x?{{NkS$ekERTC>U z?f@0BV&fU%lwgyc{?d!*4dig^IYL^-k2b6z5ms&X5Aml{XJQ4T2^y}E*IOC)$$^-x#SO>m2iqSyyjMqoEp0|R(Sfp zFn4s#Grrl3MadGb$g$*xNL)m-=7b!I(l3e3$Ds;y`<9&@bJ}EAW2FP8&*=**%F%%Z z`3EJ?fsSI=SXbuI>Ju~hoyg6-KP|E;^R|4lXj#K9`H{+Uuj*so5Ow8j0}bJ%ckof2 zG%LlIJ*-@xqDDOzA@C+NdRE%J^)PAw>1Fg|Bb49WK(Q)rv@s=~uzXLJkrt7Y__tLB zlj9xknn1Fd2fjS2m?)rG0xguYV8W;?H5dZ!LTC{33f8xmKX$dm?V!DQNo$OW zZ!fM-N-j^6p3LY-I#4wtN>z*o|AV)IVOtHMFmd^2T4EXMuV3x&AvGZq(!&KTJ4jeV zbhkm}%kc$FTYXG$cmWYd6f*Xh7)(wa06s8}VSjRs9P#VpTmd*jci(0^Hhi1yM(l>C zp%An(YF1+=)wiWEqvuVi{1Qt+m)U+w{2?WUy=`b|^azQPzQ@QEK4`QpVzbZy?r#Qr zMOBHDGxQNT;{Vf!`dfHadkq?DNO<}Va=99mAF=|wCLT7N&B{uFQVTEsE;+tV&vk^u z=~k*&s;QjpUWAj&B-js%fo%X?3bOtO@7%(*#UonWkzL;N9>|y--c!Vd8)W=Ti3=Bw zgjAByX0-Tv40Xq)*+yNg7Uy|b2!TLcUZ=#vJI-VOLoV&u@)P6x#Prcs+>vU#98Yri z<19{Y*dRz4eF`-C@UKR96(pG(1~ndAT|wgoTox5yYeOY8(b7{u-8@{l^&s&SAN9EB znro}6wb!frR!4KMHdGWXp*fn7wpCbcTadUSdbXYApyr+}cnN6jvbgr{sDF`zuB*UD#rxDkvX*DyHqTni9`jC?S zu`_HDWwf%cCU?5#)3LDv{JLez&-i^Mx_IQHg$ByCEa)z_EzAn;1Y5GjMH-8^77eJx z5*YJ+dRVzj$v0e79xXG(r;|^3us7$m5lxWRiILH{a}S5k%qSfFgA~`cO1q)b+x-L? z$ty5=D&d zq~LIF3KU{-1zsF-O(<+};a9kP^;-^n%(kMJOMP~xZi|;K2UtCRdJ|%rnx@=IMugVd zr#ac+pEA-4!Yk1#Xjac@GU57ju=lyQVEeA02vnYgtiRgI^?stns+Q(~u1g5vqBf%$ zJ8WL-+mP2Q+BdexcS+Up(Rw;n>&su(g*!5{dcpDMHiRh648!bWULjxcd!6AL zPkToXPCRaQ(b9jO1(At)k>2L&5iCM3|9}$iEkj}C5h=j3VZ^Xs|x|_k${{zz| z8Wuh)()&v9Rh2ASD{-Vy4Vqp7z4HZ12eTpBV?`f@r8nvbBV#3kA#|M9ESrObZCVDi zLa`x@;!9e|lBQL+llGs?Aqy?j@_GRJ-@h|O72>N0qJ_!vPv#5Fb>$@Jr_#obLSGnH zbEQmw3(hal_)wETxkhRUG-rnGd5IuP45yBqUAfT1cNTnc$85)o143LC(@7XYuW&1aBy4eLCR4=F@1G z0W|-ifn5b%*X<2iPk!;YX)6G}zrS8jL8hod`7VZ^)}Nk-=XHAqR_?|T+Dq$HA#(-W z&XFvW&VDX!mETBlZo~W(3d>nSUWJS4^>Q^wdN>|T0Z|H={S>`-DQ6YxGG%3puA)|`+5ghR|Ci6d?vcAe4b}dY)-ShGZn89tm)|O33_E{=qg{QFvQM!8W+w`T;PsZ z6yy(=W-niAH_(?3D*D{g<<#bF1_cca4j%7l^uprk7j}*i8ApY8t`8f0DpV-rF!vR* zH@$RtXJG_sb|a=3B$aFP8qD<}v?#fpyvo|F-5blQjPCMX&7$Olhg(_>nY4unS4@!~ zj&eRGNgW=UD0_&|m(gkFAas7GT|qxKzTgIB%RT?@(P#1eyJlZ8t`=AODhz6s-n6lT zWF=6d+K|x8ciJD2-hw-7e>M69XX85EQX{%xL3}{$-&W1%EIKlgc-f8%mKnY&z}b*M z%7s4o{7#7_q#9r?g_+T@s5V5)@0sw9 zJJV{PV!ky3{A|z$O|km}`CHmcl?nx~v{GnSwz*8Vc)M;i-#r_i;rJqU#EIW(WW`Ph zT{1WrHP&h0>s{K-@z~Ag#VIZ?AIzZ{4BnzR8x~;#S$s-_6#p`&)jBdLd1e8I(e_ zJ*@SlfJ;a<<$ zzs@$ML>b`kl588LAqz=iGnG@{cBt_ennYgYonrZ_ToL8;+~R4ia}kN#-66%+NK%pQ zm)9Ilzj;Ry#duaqoWjA4uvfRo0n1vwM0#Hx_vCG{!dmW+f(C#8@>Yke2fyDJs~MA} zLGVANJ+<9Uc*FORM3O;Q)i4ctbyu`npJ+1z%g#{U$Nr`6SmS+3nZP0K%%IRi&*<~A zDat4KwXt)nm@pI3hD5DDayI@&84gvyQT^a!WbDZ(Dag=y{k*JDsh zC`=xL+i$)fUAyK)Z_WP`dg};sPgeBr$9b;P8TlwQSyUAhGrlDFJz+;2YCc1z)IYoU z3d;8@+*FTmnH*k-L1ELH#CAr4mdX-Gi;mW=omljJ5NGhszQt=Z?=J1vPcavxv&76{ zSx~3U>fVK?LK*#%4oOMg4za{Bgep-X8s{U-!gv=|PE0Z?1?DusybDT=+Pf;VvOpqM#lJfNKis$zPuj!Dr z>rB@bY|g#H9d$&bq=&UA@OBP3Wurx^+&onr+Wn&7X2feyDSCB>Y{7SkilF zz(4QQpISxlmp>u{D5KBfDfX|VOD?m^qLcR;_{Lmrn0bUs-X?C5xzRxLISrXb#<`Pd z%Je<6z~qIn>|+8Z1gki|k))m_l=}b-mbi0hC6MuXcConY?xiY~HtBIGS1i zRwZ=c@U?y3XQ}b!L%XL%ccnSEJ(AZ{|B-anM9Yi;-$8%4eUtcg;C*S^4ac^bW7_7Z z5gysk`iXxt5jovc-|m!#Nnp3D^`7U3;&c72ahcA3ZE7j|; zAb`GI)DBX}_Rw}Kk3Wd&xrv*hIMO${B|7=wGA4xbTZ2ub;k|dqZ1@<$)o1o0Gi*QZ z19JdnS#t=^>r#U(!0_LZP}oT^%WGj)2^hH>7*BRVhRxjf3AmXL{UrPxgOrHDE02Tu zDBYyI+B~m;|rb0dN1PYOQ~VR{Mxg`7cg=eeg}bxAypuU3V+X% zF|N|Qcl#4HqV@KIfI?IMxo)Jl%bei*YG={$F8={LHaM(*PdhZiqghZD(&cI0uT>Oh zjbO4LQCh*{n=rWqtS*zRElxTamwe?y&(k`=xD@nXHim&|YL-oW({GDZGzfRFuf{*t= zr~+BeXg88ONNwhWE_5r)_PYVpzy*bVh*yq$DWxRo(xMB|`n~l9mN0{??|Zcd8GI}v zYuT@z>3k5!wTu(35~;w|K00fFZPV*-dB1cm(d)hF`#oBG3l-Jjee#gKm1i}b&TG^6 zY^Cys#x$Aqp8^y&OO)bi;Cmw4?bV9%wS(@GN^m;Q@IM>US?!^uhebR;TIP@`y~53W z48NW-w}s3q$XxvOFn1OicszBpzkn5{HuhJOaBJUb|( zLh_?#>9MD1{=n7jN1kyZe-hMHqGZp9p|F|kiHdJ~sEJ3#NFIz59SDDTImMJF=e9EQ zZO%(50Ck!cZzV1RQlrT&Uy&=!ibL(gn#(4VXxZGL55pkMaG!O3L=h6c!DqOanj|2A zPIem%O`wzy$A%bkga^2QVvOfuxh7l;qHyf-$(jTfK^wtP$LYZN#Yg+?9IagDbnr2e zTf%5t77?0R_tWsBlKa%Ei?babFBx|1dJ_a2oVyu$=^ym*8b0%Vfb8ad+cVBjRD8ZI zq6DxOk(o73<62Lk#1{?FZ0^`cqau1?nPn#-^ir3kpXlkfbG9G3+SAY4X+sJeDy&m& zlS1eL)Ld2I+!`a?2z6B-=MDeE)J6rOMXaZ~j&d0I9qh6}h%GUh|Ep6(d`klPmQ{QV z3;INGs2f$z4kn~n;YNKnoz`M7@$iD6!5?x*+3SToV4x@u7!Uc5OU9QAfR*wGQrR_9 zH^qHf|26U|=P#1^vtYBY*{pf zhTRdSx)XVvz22!&Mx0vHMR`{rdS-7P4@5D%7%b#OWbFnsF}?CmQ!%NHPfAL`gMd z#=}WUsTu8l9Po`Qx-014wgja3WlQ<_RAqx%rhC}^;xL8@B_D+^*+m4`_XZa%6)OI4 z!Ue9kUOj8_j+Z+p94oE#C?%_elmh=UJg-Gzu@HT8xuUVYnIAIkO9rI?<|HV+{KLRV`;Q zW_NvB?|-|h>UR3r$0D!aQwI|rdkWwD7%#^StnwC3Fp8rODY@T-t@}YDN!SqeD)#D( zud^O$MO)oe@YrciD+pH@mh_=O|2Q^28tWXi23F+~9;FGh8IytNEBavn~ zu>@xso!O}*i{!1Q`4oGj(7TFgteh#c&MjML^jwEox78Rwnk@Da!yqLC>=$UdwrG*I z)Kt4B@DKri=n!$VG)C@2+=C_zK>VbUCa)*t>grDCzd)u)qISpQlKJqFS!+p*!;o~UVrQYA3*$umJIa;-`Y4rX zX@4+yh}gi@VgH>jG0M|kYpvmFa=MbKwt`P}WYM`r*8V&=MPpGKDHSpKWskuUaWrdU z_xXMb_S6ion6^fP7E)bE#c?@*@mn9A7;EB3OIwgh^&=PGu)YlavK;Y`IAmgax^{@wwU{(SOuRO9qpl_xxxD&43w zsW0A6v}3pH{NBqyT{&Czvwl#>-6yZSYfW~bcXRPRh_?f%v_mnCB`(E;6tbcRZ={94 z)a#o`fXnprZ;Yy%L1Ffwud5prc)`hQHz31Dz4rN5!=jS_(?knm&H<)L(ZT&{{6J3T zaZAs>U`YI1WPOQ97?hf!)rFhkofyTiizJN&%JO7@VEZjq5bX!>S$HOq_soMiT5oH0 zNDza)KWw16GhZ8no4z~HVnpg0pO;?igq1!y<@D}+pD%#-iASCD2x3d%%xvla~;p}}+&8!Wqv-`R9o{%In` zjL~mPLK{O}vlnD^)fxfZgc}50100>)@UJ?aU?=0ZlTfe5o8xyMcHSZDGmAGlVAHL0 zQD^x|W>p5Q1GXr!i#tlTZ9VjEd&C4YRx}$_T=(Nl?`7zyepVl#y;enymzM5I&AJ*I3Ezdz`L)3u{>SLGoMNj5aN(Q0pjMZO#p#VFoB**U z(VXSG1s$7918pBPWnu_rfBpcVaJ4)5XLf=B-uxU#_$q`riwmEgtL?A5@ZZy4DPfQT-c|E%3@Q=T zRH*a$k0jt0Fs*pPbkI6>zgtu7DIJ`A2;1)Vr=YwWFgWxN;Pz7S{jn}`G0TJ0NJdx| zooQ<;d_)#le6u9=?$Fr&wxxBfTtM?lA73qS!yUT-9HUSWhR+RQg^Y5M+$$T(G3Ddj z-`hf2eAtz=jr($wC31Lr9kI$)#rSHiT%009X?>iEJV>SOYF<(H z!R!2Um#MNRC5-;gC*8OjOSp&RMtVz3C3(qBm_0|@8IR|O?KZ}}@gI7JU6VE&CTdlv6X3Kw!V z-kIB`PYmj#iCp9x5)>qnfBIibuYJA8bPW_**Sr*hy8Li6s8g=?FV6q4GOb9X#A7F* zwVFPOx&v-iV^gi4`RNnuuGN>8VhD0It|D{o$82!<+P!$a3t7|M(sLc>u62*ncb^y* z2pa|QbDY5n>&JZrcGOz{bbX%ln>PE z%Z-yv*8{{RuQ@183yq`?xN6b6(5C1|I>E1{A;N3tyZcSAzs!UR{}uV?+f3$i#6(0l zp-@iN6sS?d&1{EqsN#U-E-hY*T+SP_}gkX2Yg>jKBqBot|SDyJ${&Mt&>gWQE|gZlikLGf(@t^;J;2~_(rIx9MFJbp|f5&inV=qf7@+!`~HTH*#y>h)RE`p~{=2{$}stPt2rq zmen|tSo)_2fjut?qAz~Wlqj8dMc-a~5f3J?=aOjJXhOWS8U?3q z%eAK%+>G1Le4>%+lD6yOXYt*yW14Q!-y!Ai91u09nGW}s#{-+<@8TPtED#uVpcAqeG0BQT(uI9FjzcCtp7bf-)7k+ z^KZj$ql1eB@ix}p8zH?lNNBTim-zFot}_I6eW~N#g4c0PKPODARZR-M#-S{pTjuDe zE0kf{EY(@)<-L+;_VFs0!)iR(KyBUn~i+v z8fBOCBGPfGf27O?XTq}{B-(Rn_phh{D8&4Bnp~TEP^@>ol)Q>6gxh=!b z*-g*IS~kC3{*vNh*s!WYDpf6iGkfY>wi+hHmX$l=6`79$)#i~<^-4F_LPHga< z5A2MVr=Rf)&dRouR~!AcKD~@1818Ny3q6FuVV;u^PYDE21CPBTrVl0#kwqN@iDp_AXLKhwr{Byhgf|?F*j2u~Azh}7ry0|{(KXU~ z9l-+EyIHKxS+%6=j_#5eZP&(L9T7$E(Cg+>CDy$+E|0E^V2f)q7=B8vx_lW|I}!0> zT6Oy}&W40WgZIbq(rm>YrL&9bA(ZP6muWYoBrc?I%0J~Zb)yYQyrv3d`hgAGb^-l3 z{L`EPbu=uGru9x_33C;$dV7#w?Uv6Mc+RkLKm+(GYb#dqADcF_zOUlrA;IxB?Da9_ zery|AB;{QSy#A9*Da6mj8;yO(|4`vw94u+=JZrOAW05f&M{CG>n_taq$31iXt@eKX-I%fD%zF7B zi*rXem3a$)b~Us{S0?Agp{TVx{fNGe;}k_(INxLUIeuK@x*WAuG0*jKZSgzHGm&K1 zD9c_V=DqAUPlB&oI#|Y`thmBnIj1gF6mL_WrUP?Sza%>Oh>%b65SAk#=l1kwqmxfBlK%KFvTs%uU>C2x7x{7Ld`!vI+$+L9NIOz=+44OtWGP|f3R>ymb&KX7iZ(Tp!@ENg6N*3R3q5sakdfB+oIY6p{4~<_ zH$^8v@jR?v@a75{5y0NTOQDqEYuLr-i>jSh;^LkP!dN;!7Q5K(p`Q2QdSAU#<$mAQ z*F{lVKl501yM`wGy17|z>(&SL89{w$+p$BDjaQX_{s=3FC_Dkvs&mT8uGV8!4>LJ4 zyX9cPk+c_-$d@rlty_a>?GaDutBTb{5e7FCmSdZ=`CAzX6TiRoJr^i`a$?h9XNk?N zY`aQ{Z*ho|E|3QDmFhu)tT+Wxfa(VM|Fh3Dkx&rrc7kE>n@<9f`v6Znb}5$N`#T1M zDn@~A^NHd7g z?}FjpgJ8u%iknc0B$`qm6Jqk6jrr*(uuJCyDRRI>5V9n}4M5Oqj*6dgGv=2bdqcQ& z%ciG)lOhGA*w=m`UokciFl|0E+_b^agvKgPZwz3?$LddnR=a?;)Y!WI=OS8n6(xg3 z3L1l7=udu0!gc;W@XJ;~o1=>Fc(KE{`cdruma#5Cq5G8VR?xgbmIs2}6tB~{^Ki%N z5+#14;AZ74xO2ZWE@01X_303?Yl=6nI`>@ZMwc(PEzrSoep8kj|IlQX?B_~ICfD=Y z^zFyu7Y+vIy0)*-$M~|Ti2ahiTFl$TSn`lTkC9B|?4?T{oqmt=Z$4n4d`EiuA>E_{ z8 zF~)>cS%`8|;Da(e`mhbJ$H)=rcw{Ush#CouMzLTlr~2Px4M5r77C-MVdD}HuZhVXy z4qc4za-NMhpCM z>*V-RTD%CSKI-=hj%zNyBBME*16EmRKpCB-#vR=V-5{dGuyg;-YQ=fD$)mR-j^HK| zLg|jxSj#CvYS-VBiN)pN-g9JSD>x3;$ zDCingp-qdw;4VRfZ_pt=Pl<0dk@f^3SuBVgI`{_&v3Zk%gbkoUq1&C~ooh2w&ykTA zl&(Y*?p;~`ABH9=@V+ZS&EZ+Dwu_l5u_zz5uG{4JfU3p+dB&0g|Hs9>QkKK}#zaY3 zbqP)p;`NeOgR%w02**Ug#rl^Xfbt%Dyl6j1>QkM5*f!%BQ9yCOV%YfGS{{h#{anNW z!X$Wsw>oBX#6ud5?M;20ds?`2lmI*9y821W3~?@r3tntZ)~2fArHATvYB)L|C*zl|uY)GNVZcLhV?j+Udf zFmqZVok$nn%|Cq~e~bmX44)FZfX0EYMxDc-GTNRc@d3>T2*W;mf-cj7f`B6%aSE68dVPA* zhrx&`>Zi0mp6MY@$4-xVm~z~)tsZ-+Rrjg=Y59m|{!4-16J7mf86WuV zIE-^~ho}8$N%{%dI`A`qIt%^_(8Y~Q8U}6t_zTdbh+!`Qaj*c$2)GH3mexE0ba8^v zE0Uk&h@>;1M}Glbzk>ZgHRyp)G~t*Q@2Q-@$kR=ZpgM^Qlf_JWh>Nb1cF-u^{nY+E zaMRCw93emM@~U;I9cza=6fqC$Zv1rcDkPw=!?w{SKxW&lZ1RR40+?#{-7pF=uQ^CRHv zgvQ+xZn!6BX@ADiG-MGA~ij|WVyxTjl!wAQb zYD*m>3l%o{AGSKO%`t#+lg6(@g-_i*#|Y~?U42i+3TN&ZYlt+BCpAZi6=gdd#I3IH zC>7O!@W;X1`@M39_zU07=B!F5T?nVuXcEMdH@m zo*bA&`G39z%~K!u*KBfUH@xG@<~)P1sE)AE&2Ed zcNu)B#}B{uSJt;+_tgxh3~IdJMK{^_8J1?7RFG=NDa8<8QsZ@=Z)IPvXx`XpVUv%U z@`mim3pBoxf$E<8+8U70p?{OW+^Z_Hj#-bzq0`Qm)JH2`H+-+ z#26o{2Yy~x*9oVc(R&)*B$~%OuzXrO(Sq#BA_D%sdwIjr%sJIVQY3O6Tg$u7yceBC~2f1vnf3VS;fx_BZykj{N8QKb_XDx9IRl8aGWn zq>j)yyzz&1P^MX8aykWo^8f`SAfR&%J|PY`(F(${^}xeTgUjgDym9`a<~N)xlP+eD zU%5-?GxM}L#x&_wt%|3!uA*x%tGrI9#Y|2b)!cca2?18p)WJOS**R>(z|ISu{mm}t zd%C`|lJ|F3p0=+sgq&)-N3*M#;Ot)s*Pysv!@+7){9ldVBgfy%l@d9d#`_Z!5?5`Y zYq_I#Otq1*{oQ0-@B}ETN=y$wkcxconMx$ga2bs^l@XDyX63(0iQOOh_|csL00hZT zFsKaiNbcyz`+H%jIT7?c`d_0_8wqE6XKoIpCQ29Ih6bz6XcJIYrXVl*$&5mWQzTDZ z?tqC?{3p$z%c63F39C#f-m=d8=G1f3Ljdd*sJ3Ib=02UA9U@0}GTM02WVZ)AyCj5E z+utb?E%|Anb-uT^(?>V;yAAeo{lzH*SR4Pyp=+j0!&?+5#Bx`t1C$??al(FK4F}cI zmcDcJf3-~wWPSsBf3w=8vC%TH)2L14WLhW&`?R|tj?v=ciP_whCEZiQu(N^E&ao#T z0I8EzwnY z`4_2$L6;dlLX;S>T<~MR{Z1mC>yVQLVErlQKJw37cy{*+b@LQQ$`J zJ#nM|uu=lCrqUCgNSccxJD`s5J7jrgDmabcUW7vprf|z|E?BOtJ&ywp>~L!u8FD$C zz=gj$thnjEz?kGr6Dys2;_WX65Bq$pGl1^isvasu!sW&l@FKnKBTwRozP?NC3~RJ_ z>8eKpRtTH2dv1=m%dSC)iKlNiK3Bd}hO)c8)ly?)K2T91q)rarm^(Yjmep94v2Z|{ z4$kJx$7!N+!#WZ+U&aOMaCpyOBzTKg%373#Pt|;Q_(BS?$8LT60*iz^(L@?u?!AAy z>_&yh;*MTPz1-HUs>>M%abk|L$=~ib#kq|V@jCTM4_kps6~p(gm;KNlh}UL0WvG#O zDu?|Q`>Cx+=rwUmQHmtGm+K#ft~){JY4C`TG6)$-H2q2v37ecXv8IMM@k_p`GAjrK zQZ*=Tf09)J1vq`?aM8Yd^3O_Bv$*k{$TQs5TZK5iWd|q|gruo6abVl7K|W#uS0&MX z3?ylf52M7)m~t|J$DHVAPmKq}YT7M%s`da}r2y3l-v0ZoZ8A344myPG26ir2^4Y$Z z+ntfM1CRst3{^Ylv(J`f=-g>z33O5Qcc`&)r`O}1Xe@qUKoPh%n*34%>aPE^eL^J? zBC|EW@iiqO==e3k3kCF zI`Pvf#-c#hWbsQfABDi70O&;gPkvdb8buzQkf0z`tz;D__5Pje75R4h863$Pl6+i zn=BJNY4j7QFHOJdi7b9bM&{!5Vjcfcm{ngKs?E5n_M(jKaJ|*1?d9mYiXv~}7egW7 z(wXdueCOFx={M~7=Rw*G-A}Qy;(6XLtQB=z-hahsPnB}SSYah48N%|p(u^KwTM zo-x8he=}GICO}^97=k4^!VQzl0Zs&F%4oi?YrR*{sUYCx&fnh&qK~~(X#{ac8*1+i z!NToCNNmwZ9u&zUU&3+eIwZ zT4Bqfr6wh2nwIR!p~k3j);d3B6;g>DBB#QL3J*m&C1%OgmP5@>C8OT!(eA!~!284d z*$(CGULANAW!iFF!d)5ve-PCXJb#`-VU-eOS+9}Blh;Ylvrx1?|ENLpX6oTu0 zThwW$Aiqo9DQ_}&p?NG`YUoT7BuGZT)LDkfnilc|f$rfKyMu{Yp@z{`sluCuTfEr&Gk zHAGX6Fz)S1@uS+yZ~L4ey=OubQ*ldXwEYZ^U(%|I1#f`_JBhhC*1>~_-RQ-^)KanQ zt*gh%igJ84SHT-|Rt8Y!cPl2o(Hy;1;J@bwH%rjEw3(71$yquktvXfp5nfrfP4X6HD zRdsS=PMV*9YZTl$o%{%czz}Ay4!R5%Qur@SD|9NiKv)q~{OwgAz5fo|qIqgNuFXs< zXoTrLd0$JA()!C2gSa#u6)Ow8EcyH1X)^LE*>%knGAFTKy-8{tR} z`z&*ZSfM~vSol7% zHtV+Z`trQqMhQeS4a%&~Qc+!XN2GsV%75F3`{t4oIcj4z9oXP@TH_U2a#&UkM;?HsYl4FyP;4?3~U5kRHz4Jqyb}kic@VlW=sO>No ze5{S(6B~?M*{EV^X>e!DmMA8lwD3pn#-yhx&}rc&sBR&LHAQSlzN$YPXo;5n6k=SJ z(S5HJTB92Vi3CS?*FAr|p{3ntW-AV(05ER6IVr~4qSBxs8%TZ0EdOzH+aISw6RP*r zb(P|nltoOTwc*p>eLsw8_XG@!);-bOy~@=9bL@ob`2mR2!)U(Yx6seKhs$Lc**2^Y zxtNAWX`ZxH5tT`mSdSa>yxf8T=_xby83|m-3|zU6rsf!xn9x7i_R|af7w;`s*Px)0ZES*+}A40$R2y?kJ(GYTtYCuj&mKkpdkY zZsrdTR}_pl^=Da!30$rip#5WaJc989GzK0=kaq3c`-eBZv$-ZG&?;GlnW4Hb@JTgn zS4?4JZm2u*Wyzh^f%M2P+<%Ba;H!;BXR^kVhwNFU2Iyre`9!9rOUMOV%9aYwSiFp5JWF-E|5l7$WdA6T= z2AME%R2z-)h+|dFht6kp*&<^k!D=iQginS&R+F(NYlsPuEBmlM*)nfB zzID$af6N=cR!Gn!wOGcXd^Xi28`h1RC zIcXLuM8E-mGtw4#VhdRqq`mFT%$%dCa!hmQ2f9f3*5{06=^y>;1;;gdYmW?92P96< zbnNM=KISgkHDZXy$l^MaJ=bAX#Og~ncq$M#{BSq&d&iLKI!a`@f4hZ3i>vas{NpQ{ zmzX;@?R{gnMcFcN?F+B1YxW5mq$0ybH2jESoH}ZYgzqkS%zs{W&#ltSQc??)tQLc5 zRmQs2Q#kI)G4Xg^x00_AfIn?h`n76mqjgfa@~xbos42@~|I_X-N2=2wITN7sIm$PF z)!7e;pv1-1(#o&H@)(igvmHe_F(mu|B+KZWIb;{%27r+cAHS$XfKkZkj~>MbAVw1b z1Vn?qJsdCy2q;Yg5&-IQ{Lj;omHVV6An*}Ri*I Date: Mon, 29 Apr 2019 14:29:54 +0700 Subject: [PATCH 37/73] Fix ref_text of merge request pipelines Source branch can be removed after the merge and we have to make sure to avoid rendering links if it's the case. --- app/presenters/ci/pipeline_presenter.rb | 12 +---- app/presenters/merge_request_presenter.rb | 16 +++++++ .../fix-ref-text-of-mr-pipelines.yml | 6 +++ .../projects/pipelines/pipeline_spec.rb | 40 +++++++++++++--- .../merge_request_presenter_spec.rb | 46 +++++++++++++++++++ 5 files changed, 104 insertions(+), 16 deletions(-) create mode 100644 changelogs/unreleased/fix-ref-text-of-mr-pipelines.yml diff --git a/app/presenters/ci/pipeline_presenter.rb b/app/presenters/ci/pipeline_presenter.rb index 1c1347c5a57..944895904fe 100644 --- a/app/presenters/ci/pipeline_presenter.rb +++ b/app/presenters/ci/pipeline_presenter.rb @@ -63,19 +63,11 @@ module Ci end def link_to_merge_request_source_branch - return unless merge_request_presenter - - link_to(merge_request_presenter.source_branch, - merge_request_presenter.source_branch_commits_path, - class: 'ref-name') + merge_request_presenter&.source_branch_link end def link_to_merge_request_target_branch - return unless merge_request_presenter - - link_to(merge_request_presenter.target_branch, - merge_request_presenter.target_branch_commits_path, - class: 'ref-name') + merge_request_presenter&.target_branch_link end private diff --git a/app/presenters/merge_request_presenter.rb b/app/presenters/merge_request_presenter.rb index 3f7b5bebb74..ba0711ca867 100644 --- a/app/presenters/merge_request_presenter.rb +++ b/app/presenters/merge_request_presenter.rb @@ -216,6 +216,22 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated help_page_path('ci/merge_request_pipelines/index.md') end + def source_branch_link + if source_branch_exists? + link_to(source_branch, source_branch_commits_path, class: 'ref-name') + else + content_tag(:span, source_branch, class: 'ref-name') + end + end + + def target_branch_link + if target_branch_exists? + link_to(target_branch, target_branch_commits_path, class: 'ref-name') + else + content_tag(:span, target_branch, class: 'ref-name') + end + end + private def cached_can_be_reverted? diff --git a/changelogs/unreleased/fix-ref-text-of-mr-pipelines.yml b/changelogs/unreleased/fix-ref-text-of-mr-pipelines.yml new file mode 100644 index 00000000000..8803f9b52a4 --- /dev/null +++ b/changelogs/unreleased/fix-ref-text-of-mr-pipelines.yml @@ -0,0 +1,6 @@ +--- +title: Fix pipelines for merge requests does not show pipeline page when source branch + is removed +merge_request: 27803 +author: +type: fixed diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb index cf334e1e4da..4ec44cb05b3 100644 --- a/spec/features/projects/pipelines/pipeline_spec.rb +++ b/spec/features/projects/pipelines/pipeline_spec.rb @@ -331,11 +331,9 @@ describe 'Pipeline', :js do merge_request.all_pipelines.last end - before do - visit_pipeline - end - it 'shows the pipeline information' do + visit_pipeline + within '.pipeline-info' do expect(page).to have_content("#{pipeline.statuses.count} jobs " \ "for !#{merge_request.iid} " \ @@ -347,6 +345,21 @@ describe 'Pipeline', :js do end end + context 'when source branch does not exist' do + before do + project.repository.rm_branch(user, merge_request.source_branch) + end + + it 'does not link to the source branch commit path' do + visit_pipeline + + within '.pipeline-info' do + expect(page).not_to have_link(merge_request.source_branch) + expect(page).to have_content(merge_request.source_branch) + end + end + end + context 'when source project is a forked project' do let(:source_project) { fork_project(project, user, repository: true) } @@ -386,11 +399,11 @@ describe 'Pipeline', :js do before do pipeline.update(user: user) - - visit_pipeline end it 'shows the pipeline information' do + visit_pipeline + within '.pipeline-info' do expect(page).to have_content("#{pipeline.statuses.count} jobs " \ "for !#{merge_request.iid} " \ @@ -405,6 +418,21 @@ describe 'Pipeline', :js do end end + context 'when target branch does not exist' do + before do + project.repository.rm_branch(user, merge_request.target_branch) + end + + it 'does not link to the target branch commit path' do + visit_pipeline + + within '.pipeline-info' do + expect(page).not_to have_link(merge_request.target_branch) + expect(page).to have_content(merge_request.target_branch) + end + end + end + context 'when source project is a forked project' do let(:source_project) { fork_project(project, user, repository: true) } diff --git a/spec/presenters/merge_request_presenter_spec.rb b/spec/presenters/merge_request_presenter_spec.rb index e5f08aeb1fa..451dc88880c 100644 --- a/spec/presenters/merge_request_presenter_spec.rb +++ b/spec/presenters/merge_request_presenter_spec.rb @@ -439,6 +439,52 @@ describe MergeRequestPresenter do end end + describe '#source_branch_link' do + subject { presenter.source_branch_link } + + let(:presenter) { described_class.new(resource, current_user: user) } + + context 'when source branch exists' do + it 'returns link' do + allow(resource).to receive(:source_branch_exists?) { true } + + is_expected + .to eq("#{presenter.source_branch}") + end + end + + context 'when source branch does not exist' do + it 'returns text' do + allow(resource).to receive(:source_branch_exists?) { false } + + is_expected.to eq("#{presenter.source_branch}") + end + end + end + + describe '#target_branch_link' do + subject { presenter.target_branch_link } + + let(:presenter) { described_class.new(resource, current_user: user) } + + context 'when target branch exists' do + it 'returns link' do + allow(resource).to receive(:target_branch_exists?) { true } + + is_expected + .to eq("#{presenter.target_branch}") + end + end + + context 'when target branch does not exist' do + it 'returns text' do + allow(resource).to receive(:target_branch_exists?) { false } + + is_expected.to eq("#{presenter.target_branch}") + end + end + end + describe '#source_branch_with_namespace_link' do subject do described_class.new(resource, current_user: user).source_branch_with_namespace_link From 25818bd7ae765422c934d0a32efb4ba353d11183 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Mon, 29 Apr 2019 17:07:42 -0700 Subject: [PATCH 38/73] Disable method replacement in avatar loading We've seen a significant performance penalty when using `BatchLoader#__replace_with!`. This defines methods on the batch loader that proxy to the 'real' object using send. The alternative is `method_missing`, which is slower. However, we've noticed that `method_missing` can be faster if: 1. The objects being loaded have a large interface. 2. We don't call too many methods on the loaded object. Avatar uploads meet both criteria above, so let's use the newly-released feature in https://github.com/exAspArk/batch-loader/pull/45. Closes https://gitlab.com/gitlab-org/gitlab-ce/issues/60903 --- Gemfile | 2 +- Gemfile.lock | 4 ++-- app/models/concerns/avatarable.rb | 3 ++- .../unreleased/sh-disable-batch-load-replace-methods.yml | 5 +++++ spec/uploaders/object_storage_spec.rb | 8 ++++++++ 5 files changed, 18 insertions(+), 4 deletions(-) create mode 100644 changelogs/unreleased/sh-disable-batch-load-replace-methods.yml diff --git a/Gemfile b/Gemfile index 95d65ec30c7..e075e2478a8 100644 --- a/Gemfile +++ b/Gemfile @@ -285,7 +285,7 @@ gem 'gettext_i18n_rails', '~> 1.8.0' gem 'gettext_i18n_rails_js', '~> 1.3' gem 'gettext', '~> 3.2.2', require: false, group: :development -gem 'batch-loader', '~> 1.2.2' +gem 'batch-loader', '~> 1.4.0' # Perf bar gem 'peek', '~> 1.0.1' diff --git a/Gemfile.lock b/Gemfile.lock index c08f0c24ba6..c5ad2357434 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -76,7 +76,7 @@ GEM thread_safe (~> 0.3, >= 0.3.1) babosa (1.0.2) base32 (0.3.2) - batch-loader (1.2.2) + batch-loader (1.4.0) bcrypt (3.1.12) bcrypt_pbkdf (1.0.0) benchmark-ips (2.3.0) @@ -999,7 +999,7 @@ DEPENDENCIES awesome_print babosa (~> 1.0.2) base32 (~> 0.3.0) - batch-loader (~> 1.2.2) + batch-loader (~> 1.4.0) bcrypt_pbkdf (~> 1.0) benchmark-ips (~> 2.3.0) better_errors (~> 2.5.0) diff --git a/app/models/concerns/avatarable.rb b/app/models/concerns/avatarable.rb index 4687ec7d166..80278e07e65 100644 --- a/app/models/concerns/avatarable.rb +++ b/app/models/concerns/avatarable.rb @@ -91,7 +91,8 @@ module Avatarable private def retrieve_upload_from_batch(identifier) - BatchLoader.for(identifier: identifier, model: self).batch(key: self.class) do |upload_params, loader, args| + BatchLoader.for(identifier: identifier, model: self) + .batch(key: self.class, cache: true, replace_methods: false) do |upload_params, loader, args| model_class = args[:key] paths = upload_params.flat_map do |params| params[:model].upload_paths(params[:identifier]) diff --git a/changelogs/unreleased/sh-disable-batch-load-replace-methods.yml b/changelogs/unreleased/sh-disable-batch-load-replace-methods.yml new file mode 100644 index 00000000000..00f897ac4b1 --- /dev/null +++ b/changelogs/unreleased/sh-disable-batch-load-replace-methods.yml @@ -0,0 +1,5 @@ +--- +title: Disable method replacement in avatar loading +merge_request: 27866 +author: +type: performance diff --git a/spec/uploaders/object_storage_spec.rb b/spec/uploaders/object_storage_spec.rb index 9ce9a353913..a62830c35f1 100644 --- a/spec/uploaders/object_storage_spec.rb +++ b/spec/uploaders/object_storage_spec.rb @@ -771,6 +771,14 @@ describe ObjectStorage do expect { avatars }.not_to exceed_query_limit(1) end + it 'does not attempt to replace methods' do + models.each do |model| + expect(model.avatar.upload).to receive(:method_missing).and_call_original + + model.avatar.upload.path + end + end + it 'fetches a unique upload for each model' do expect(avatars.map(&:url).uniq).to eq(avatars.map(&:url)) expect(avatars.map(&:upload).uniq).to eq(avatars.map(&:upload)) From 43be4d54f3940633ad76e746a9a999c4a9a65870 Mon Sep 17 00:00:00 2001 From: Thong Kuah Date: Mon, 8 Apr 2019 13:26:05 +1200 Subject: [PATCH 39/73] Define state transitions for uninstalling apps Added :uninstalled state as wasn't sure if we should be destroying the cluster apps --- .../clusters/concerns/application_status.rb | 18 ++++- spec/factories/clusters/applications/helm.rb | 23 ++++-- ...ster_application_status_shared_examples.rb | 76 ++++++++++++++++--- 3 files changed, 99 insertions(+), 18 deletions(-) diff --git a/app/models/clusters/concerns/application_status.rb b/app/models/clusters/concerns/application_status.rb index 1273ed83abe..16679a21e64 100644 --- a/app/models/clusters/concerns/application_status.rb +++ b/app/models/clusters/concerns/application_status.rb @@ -25,9 +25,12 @@ module Clusters state :updating, value: 4 state :updated, value: 5 state :update_errored, value: 6 + state :uninstalling, value: 7 + state :uninstall_errored, value: 8 + state :uninstalled, value: 9 event :make_scheduled do - transition [:installable, :errored, :installed, :updated, :update_errored] => :scheduled + transition [:installable, :errored, :installed, :updated, :update_errored, :uninstall_errored] => :scheduled end event :make_installing do @@ -40,8 +43,9 @@ module Clusters end event :make_errored do - transition any - [:updating] => :errored + transition any - [:updating, :uninstalling] => :errored transition [:updating] => :update_errored + transition [:uninstalling] => :uninstall_errored end event :make_updating do @@ -52,6 +56,14 @@ module Clusters transition any => :update_errored end + event :make_uninstalling do + transition [:scheduled] => :uninstalling + end + + event :make_uninstalled do + transition [:uninstalling] => :uninstalled + end + before_transition any => [:scheduled] do |app_status, _| app_status.status_reason = nil end @@ -65,7 +77,7 @@ module Clusters app_status.status_reason = nil end - before_transition any => [:update_errored] do |app_status, transition| + before_transition any => [:update_errored, :uninstall_errored] do |app_status, transition| status_reason = transition.args.first app_status.status_reason = status_reason if status_reason end diff --git a/spec/factories/clusters/applications/helm.rb b/spec/factories/clusters/applications/helm.rb index fe56ac5b71d..ac230950fce 100644 --- a/spec/factories/clusters/applications/helm.rb +++ b/spec/factories/clusters/applications/helm.rb @@ -6,6 +6,11 @@ FactoryBot.define do status(-2) end + trait :errored do + status(-1) + status_reason 'something went wrong' + end + trait :installable do status 0 end @@ -30,16 +35,24 @@ FactoryBot.define do status 5 end - trait :errored do - status(-1) - status_reason 'something went wrong' - end - trait :update_errored do status(6) status_reason 'something went wrong' end + trait :uninstalling do + status 7 + end + + trait :uninstall_errored do + status(8) + status_reason 'something went wrong' + end + + trait :uninstalled do + status 9 + end + trait :timeouted do installing updated_at { ClusterWaitForAppInstallationWorker::TIMEOUT.ago } diff --git a/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb b/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb index b8c19cab0c4..c56b148cb8c 100644 --- a/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb +++ b/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb @@ -114,6 +114,17 @@ shared_examples 'cluster application status specs' do |application_name| expect(subject.status_reason).to eq(reason) end end + + context 'application is uninstalling' do + subject { create(application_name, :uninstalling) } + + it 'is uninstall_errored' do + subject.make_errored(reason) + + expect(subject).to be_uninstall_errored + expect(subject.status_reason).to eq(reason) + end + end end describe '#make_scheduled' do @@ -125,6 +136,16 @@ shared_examples 'cluster application status specs' do |application_name| expect(subject).to be_scheduled end + describe 'when installed' do + subject { create(application_name, :installed) } + + it 'is scheduled' do + subject.make_scheduled + + expect(subject).to be_scheduled + end + end + describe 'when was errored' do subject { create(application_name, :errored) } @@ -148,6 +169,38 @@ shared_examples 'cluster application status specs' do |application_name| expect(subject.status_reason).to be_nil end end + + describe 'when was uninstall_errored' do + subject { create(application_name, :uninstall_errored) } + + it 'clears #status_reason' do + expect(subject.status_reason).not_to be_nil + + subject.make_scheduled! + + expect(subject.status_reason).to be_nil + end + end + end + + describe '#make_uninstalling' do + subject { create(application_name, :scheduled) } + + it 'is uninstalling' do + subject.make_uninstalling! + + expect(subject).to be_uninstalling + end + end + + describe '#make_uninstalled' do + subject { create(application_name, :uninstalling) } + + it 'is uninstalled' do + subject.make_uninstalled! + + expect(subject).to be_uninstalled + end end end @@ -155,16 +208,19 @@ shared_examples 'cluster application status specs' do |application_name| using RSpec::Parameterized::TableSyntax where(:trait, :available) do - :not_installable | false - :installable | false - :scheduled | false - :installing | false - :installed | true - :updating | false - :updated | true - :errored | false - :update_errored | false - :timeouted | false + :not_installable | false + :installable | false + :scheduled | false + :installing | false + :installed | true + :updating | false + :updated | true + :errored | false + :update_errored | false + :uninstalling | false + :uninstall_errored | false + :uninstalled | false + :timeouted | false end with_them do From 33a765c17a246e4a2376056b1c301707c78806d0 Mon Sep 17 00:00:00 2001 From: Thong Kuah Date: Wed, 10 Apr 2019 10:57:23 +1200 Subject: [PATCH 40/73] Teach Helm::Api about #uninstall --- lib/gitlab/kubernetes/helm/api.rb | 7 +++++++ spec/lib/gitlab/kubernetes/helm/api_spec.rb | 22 +++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/lib/gitlab/kubernetes/helm/api.rb b/lib/gitlab/kubernetes/helm/api.rb index 7dfd9ed4f35..ff1dadf9247 100644 --- a/lib/gitlab/kubernetes/helm/api.rb +++ b/lib/gitlab/kubernetes/helm/api.rb @@ -22,6 +22,13 @@ module Gitlab alias_method :update, :install + def uninstall(command) + namespace.ensure_exists! + + delete_pod!(command.pod_name) + kubeclient.create_pod(command.pod_resource) + end + ## # Returns Pod phase # diff --git a/spec/lib/gitlab/kubernetes/helm/api_spec.rb b/spec/lib/gitlab/kubernetes/helm/api_spec.rb index 8433d40b2ea..24ce397ec3d 100644 --- a/spec/lib/gitlab/kubernetes/helm/api_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/api_spec.rb @@ -33,6 +33,28 @@ describe Gitlab::Kubernetes::Helm::Api do end end + describe '#uninstall' do + before do + allow(client).to receive(:create_pod).and_return(nil) + allow(client).to receive(:delete_pod).and_return(nil) + allow(namespace).to receive(:ensure_exists!).once + end + + it 'ensures the namespace exists before creating the POD' do + expect(namespace).to receive(:ensure_exists!).once.ordered + expect(client).to receive(:create_pod).once.ordered + + subject.uninstall(command) + end + + it 'removes an existing pod before installing' do + expect(client).to receive(:delete_pod).with('install-app-name', 'gitlab-managed-apps').once.ordered + expect(client).to receive(:create_pod).once.ordered + + subject.uninstall(command) + end + end + describe '#install' do before do allow(client).to receive(:create_pod).and_return(nil) From 938e90f47288901790a96c50a8c0dfa2b7eab137 Mon Sep 17 00:00:00 2001 From: Thong Kuah Date: Wed, 10 Apr 2019 14:50:14 +1200 Subject: [PATCH 41/73] Services to uninstall cluster application + to monitor progress of uninstallation pod --- .../check_uninstall_progress_service.rb | 62 +++++++++ .../applications/uninstall_service.rb | 29 ++++ app/workers/all_queues.yml | 1 + .../wait_for_uninstall_app_worker.rb | 20 +++ .../check_uninstall_progress_service_spec.rb | 127 ++++++++++++++++++ .../applications/uninstall_service_spec.rb | 77 +++++++++++ .../wait_for_uninstall_app_worker_spec.rb | 32 +++++ 7 files changed, 348 insertions(+) create mode 100644 app/services/clusters/applications/check_uninstall_progress_service.rb create mode 100644 app/services/clusters/applications/uninstall_service.rb create mode 100644 app/workers/clusters/applications/wait_for_uninstall_app_worker.rb create mode 100644 spec/services/clusters/applications/check_uninstall_progress_service_spec.rb create mode 100644 spec/services/clusters/applications/uninstall_service_spec.rb create mode 100644 spec/workers/clusters/applications/wait_for_uninstall_app_worker_spec.rb diff --git a/app/services/clusters/applications/check_uninstall_progress_service.rb b/app/services/clusters/applications/check_uninstall_progress_service.rb new file mode 100644 index 00000000000..2a2594a30c8 --- /dev/null +++ b/app/services/clusters/applications/check_uninstall_progress_service.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +module Clusters + module Applications + class CheckUninstallProgressService < BaseHelmService + def execute + return unless app.uninstalling? + + case installation_phase + when Gitlab::Kubernetes::Pod::SUCCEEDED + on_success + when Gitlab::Kubernetes::Pod::FAILED + on_failed + else + check_timeout + end + rescue Kubeclient::HttpError => e + log_error(e) + + app.make_errored!("Kubernetes error: #{e.error_code}") + end + + private + + def on_success + app.make_uninstalled! + ensure + remove_installation_pod + end + + def on_failed + app.make_errored!("Operation failed. Check pod logs for #{pod_name} for more details.") + end + + def check_timeout + if timeouted? + begin + app.make_errored!("Operation timed out. Check pod logs for #{pod_name} for more details.") + end + else + WaitForUninstallAppWorker.perform_in(WaitForUninstallAppWorker::INTERVAL, app.name, app.id) + end + end + + def pod_name + app.uninstall_command.pod_name + end + + def timeouted? + Time.now.utc - app.updated_at.to_time.utc > WaitForUninstallAppWorker::TIMEOUT + end + + def remove_installation_pod + helm_api.delete_pod!(pod_name) + end + + def installation_phase + helm_api.status(pod_name) + end + end + end +end diff --git a/app/services/clusters/applications/uninstall_service.rb b/app/services/clusters/applications/uninstall_service.rb new file mode 100644 index 00000000000..50c8d806c14 --- /dev/null +++ b/app/services/clusters/applications/uninstall_service.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Clusters + module Applications + class UninstallService < BaseHelmService + def execute + return unless app.scheduled? + + app.make_uninstalling! + uninstall + end + + private + + def uninstall + helm_api.uninstall(app.uninstall_command) + + Clusters::Applications::WaitForUninstallAppWorker.perform_in( + Clusters::Applications::WaitForUninstallAppWorker::INTERVAL, app.name, app.id) + rescue Kubeclient::HttpError => e + log_error(e) + app.make_errored!("Kubernetes error: #{e.error_code}") + rescue StandardError => e + log_error(e) + app.make_errored!('Failed to uninstall.') + end + end + end +end diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index d01bbbf269e..0d14d313d21 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -32,6 +32,7 @@ - gcp_cluster:cluster_wait_for_ingress_ip_address - gcp_cluster:cluster_configure - gcp_cluster:cluster_project_configure +- gcp_cluster:clusters_applications_wait_for_uninstall_app - github_import_advance_stage - github_importer:github_import_import_diff_note diff --git a/app/workers/clusters/applications/wait_for_uninstall_app_worker.rb b/app/workers/clusters/applications/wait_for_uninstall_app_worker.rb new file mode 100644 index 00000000000..163c99d3c3c --- /dev/null +++ b/app/workers/clusters/applications/wait_for_uninstall_app_worker.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Clusters + module Applications + class WaitForUninstallAppWorker + include ApplicationWorker + include ClusterQueue + include ClusterApplications + + INTERVAL = 10.seconds + TIMEOUT = 20.minutes + + def perform(app_name, app_id) + find_application(app_name, app_id) do |app| + Clusters::Applications::CheckUninstallProgressService.new(app).execute + end + end + end + end +end diff --git a/spec/services/clusters/applications/check_uninstall_progress_service_spec.rb b/spec/services/clusters/applications/check_uninstall_progress_service_spec.rb new file mode 100644 index 00000000000..ccae7fd133f --- /dev/null +++ b/spec/services/clusters/applications/check_uninstall_progress_service_spec.rb @@ -0,0 +1,127 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Clusters::Applications::CheckUninstallProgressService do + RESCHEDULE_PHASES = Gitlab::Kubernetes::Pod::PHASES - [Gitlab::Kubernetes::Pod::SUCCEEDED, Gitlab::Kubernetes::Pod::FAILED].freeze + + let(:application) { create(:clusters_applications_helm, :uninstalling) } + let(:service) { described_class.new(application) } + let(:phase) { Gitlab::Kubernetes::Pod::UNKNOWN } + let(:errors) { nil } + let(:worker_class) { Clusters::Applications::WaitForUninstallAppWorker } + + shared_examples 'a not yet terminated installation' do |a_phase| + let(:phase) { a_phase } + + before do + expect(service).to receive(:installation_phase).once.and_return(phase) + end + + context "when phase is #{a_phase}" do + context 'when not timeouted' do + it 'reschedule a new check' do + expect(worker_class).to receive(:perform_in).once + expect(service).not_to receive(:remove_installation_pod) + + expect do + service.execute + + application.reload + end.not_to change(application, :status) + + expect(application.status_reason).to be_nil + end + end + end + end + + before do + allow(service).to receive(:installation_errors).and_return(errors) + allow(service).to receive(:remove_installation_pod).and_return(nil) + end + + context 'when application is installing' do + RESCHEDULE_PHASES.each { |phase| it_behaves_like 'a not yet terminated installation', phase } + + context 'when installation POD succeeded' do + let(:phase) { Gitlab::Kubernetes::Pod::SUCCEEDED } + before do + expect(service).to receive(:installation_phase).once.and_return(phase) + end + + it 'removes the installation POD' do + expect(service).to receive(:remove_installation_pod).once + + service.execute + end + + it 'make the application installed' do + expect(worker_class).not_to receive(:perform_in) + + service.execute + + expect(application).to be_uninstalled + expect(application.status_reason).to be_nil + end + end + + context 'when installation POD failed' do + let(:phase) { Gitlab::Kubernetes::Pod::FAILED } + let(:errors) { 'test installation failed' } + + before do + expect(service).to receive(:installation_phase).once.and_return(phase) + end + + it 'make the application errored' do + service.execute + + expect(application).to be_uninstall_errored + expect(application.status_reason).to eq('Operation failed. Check pod logs for uninstall-helm for more details.') + end + end + + context 'when timed out' do + let(:application) { create(:clusters_applications_helm, :timeouted, :uninstalling) } + + before do + expect(service).to receive(:installation_phase).once.and_return(phase) + end + + it 'make the application errored' do + expect(worker_class).not_to receive(:perform_in) + + service.execute + + expect(application).to be_uninstall_errored + expect(application.status_reason).to eq('Operation timed out. Check pod logs for uninstall-helm for more details.') + end + end + + context 'when installation raises a Kubeclient::HttpError' do + let(:cluster) { create(:cluster, :provided_by_user, :project) } + let(:logger) { service.send(:logger) } + let(:error) { Kubeclient::HttpError.new(401, 'Unauthorized', nil) } + + before do + application.update!(cluster: cluster) + + expect(service).to receive(:installation_phase).and_raise(error) + end + + include_examples 'logs kubernetes errors' do + let(:error_name) { 'Kubeclient::HttpError' } + let(:error_message) { 'Unauthorized' } + let(:error_code) { 401 } + end + + it 'shows the response code from the error' do + service.execute + + expect(application).to be_uninstall_errored + expect(application.status_reason).to eq('Kubernetes error: 401') + end + end + end +end diff --git a/spec/services/clusters/applications/uninstall_service_spec.rb b/spec/services/clusters/applications/uninstall_service_spec.rb new file mode 100644 index 00000000000..d1d0e923e18 --- /dev/null +++ b/spec/services/clusters/applications/uninstall_service_spec.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Clusters::Applications::UninstallService, '#execute' do + let(:application) { create(:clusters_applications_helm, :scheduled) } + let(:service) { described_class.new(application) } + let(:helm_client) { instance_double(Gitlab::Kubernetes::Helm::Api) } + let(:worker_class) { Clusters::Applications::WaitForUninstallAppWorker } + + before do + allow(service).to receive(:helm_api).and_return(helm_client) + end + + context 'when there are no errors' do + before do + expect(helm_client).to receive(:uninstall).with(kind_of(Gitlab::Kubernetes::Helm::ResetCommand)) + allow(worker_class).to receive(:perform_in).and_return(nil) + end + + it 'make the application to be uninstalling' do + expect(application.cluster).not_to be_nil + service.execute + + expect(application).to be_uninstalling + end + + it 'schedule async installation status check' do + expect(worker_class).to receive(:perform_in).once + + service.execute + end + end + + context 'when k8s cluster communication fails' do + let(:error) { Kubeclient::HttpError.new(500, 'system failure', nil) } + + before do + expect(helm_client).to receive(:uninstall).with(kind_of(Gitlab::Kubernetes::Helm::ResetCommand)).and_raise(error) + end + + include_examples 'logs kubernetes errors' do + let(:error_name) { 'Kubeclient::HttpError' } + let(:error_message) { 'system failure' } + let(:error_code) { 500 } + end + + it 'make the application errored' do + service.execute + + expect(application).to be_uninstall_errored + expect(application.status_reason).to match('Kubernetes error: 500') + end + end + + context 'a non kubernetes error happens' do + let(:application) { create(:clusters_applications_helm, :scheduled) } + let(:error) { StandardError.new('something bad happened') } + + before do + expect(helm_client).to receive(:uninstall).with(kind_of(Gitlab::Kubernetes::Helm::ResetCommand)).and_raise(error) + end + + include_examples 'logs kubernetes errors' do + let(:error_name) { 'StandardError' } + let(:error_message) { 'something bad happened' } + let(:error_code) { nil } + end + + it 'make the application errored' do + service.execute + + expect(application).to be_uninstall_errored + expect(application.status_reason).to eq('Failed to uninstall.') + end + end +end diff --git a/spec/workers/clusters/applications/wait_for_uninstall_app_worker_spec.rb b/spec/workers/clusters/applications/wait_for_uninstall_app_worker_spec.rb new file mode 100644 index 00000000000..aaf5c9defc4 --- /dev/null +++ b/spec/workers/clusters/applications/wait_for_uninstall_app_worker_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Clusters::Applications::WaitForUninstallAppWorker, '#perform' do + let(:app) { create(:clusters_applications_helm) } + let(:app_name) { app.name } + let(:app_id) { app.id } + + subject { described_class.new.perform(app_name, app_id) } + + context 'app exists' do + let(:service) { instance_double(Clusters::Applications::CheckUninstallProgressService) } + + it 'calls the check service' do + expect(Clusters::Applications::CheckUninstallProgressService).to receive(:new).with(app).and_return(service) + expect(service).to receive(:execute).once + + subject + end + end + + context 'app does not exist' do + let(:app_id) { 0 } + + it 'does not call the check service' do + expect(Clusters::Applications::CheckUninstallProgressService).not_to receive(:new) + + expect { subject }.to raise_error(ActiveRecord::RecordNotFound) + end + end +end From 3c8df0c944f0b23f9ee8b6b08a0a355b00456dd9 Mon Sep 17 00:00:00 2001 From: Thong Kuah Date: Fri, 12 Apr 2019 17:28:06 +1200 Subject: [PATCH 42/73] Destroy app on successful uninstallation Rescue and put into :uninstall_errored if something goes wrong while destroying, which can happen. I think it is safe to expose the full error message from the destroy error. Remove the :uninstalled state as no longer used. --- .../clusters/concerns/application_status.rb | 5 ---- .../check_uninstall_progress_service.rb | 4 +++- spec/factories/clusters/applications/helm.rb | 4 ---- .../check_uninstall_progress_service_spec.rb | 23 ++++++++++++++++--- ...ster_application_status_shared_examples.rb | 11 --------- 5 files changed, 23 insertions(+), 24 deletions(-) diff --git a/app/models/clusters/concerns/application_status.rb b/app/models/clusters/concerns/application_status.rb index 16679a21e64..54a3dda6d75 100644 --- a/app/models/clusters/concerns/application_status.rb +++ b/app/models/clusters/concerns/application_status.rb @@ -27,7 +27,6 @@ module Clusters state :update_errored, value: 6 state :uninstalling, value: 7 state :uninstall_errored, value: 8 - state :uninstalled, value: 9 event :make_scheduled do transition [:installable, :errored, :installed, :updated, :update_errored, :uninstall_errored] => :scheduled @@ -60,10 +59,6 @@ module Clusters transition [:scheduled] => :uninstalling end - event :make_uninstalled do - transition [:uninstalling] => :uninstalled - end - before_transition any => [:scheduled] do |app_status, _| app_status.status_reason = nil end diff --git a/app/services/clusters/applications/check_uninstall_progress_service.rb b/app/services/clusters/applications/check_uninstall_progress_service.rb index 2a2594a30c8..efb2bb1b67d 100644 --- a/app/services/clusters/applications/check_uninstall_progress_service.rb +++ b/app/services/clusters/applications/check_uninstall_progress_service.rb @@ -23,7 +23,9 @@ module Clusters private def on_success - app.make_uninstalled! + app.destroy! + rescue StandardError => e + app.make_errored!("Application uninstalled but failed to destroy: #{e.message}") ensure remove_installation_pod end diff --git a/spec/factories/clusters/applications/helm.rb b/spec/factories/clusters/applications/helm.rb index ac230950fce..22a0888947e 100644 --- a/spec/factories/clusters/applications/helm.rb +++ b/spec/factories/clusters/applications/helm.rb @@ -49,10 +49,6 @@ FactoryBot.define do status_reason 'something went wrong' end - trait :uninstalled do - status 9 - end - trait :timeouted do installing updated_at { ClusterWaitForAppInstallationWorker::TIMEOUT.ago } diff --git a/spec/services/clusters/applications/check_uninstall_progress_service_spec.rb b/spec/services/clusters/applications/check_uninstall_progress_service_spec.rb index ccae7fd133f..084f29d9d2d 100644 --- a/spec/services/clusters/applications/check_uninstall_progress_service_spec.rb +++ b/spec/services/clusters/applications/check_uninstall_progress_service_spec.rb @@ -56,13 +56,30 @@ describe Clusters::Applications::CheckUninstallProgressService do service.execute end - it 'make the application installed' do + it 'destroys the application' do expect(worker_class).not_to receive(:perform_in) service.execute + expect(application).to be_destroyed + end - expect(application).to be_uninstalled - expect(application.status_reason).to be_nil + context 'an error occurs while destroying' do + before do + expect(application).to receive(:destroy!).once.and_raise("destroy failed") + end + + it 'still removes the installation POD' do + expect(service).to receive(:remove_installation_pod).once + + service.execute + end + + it 'makes the application uninstall_errored' do + service.execute + + expect(application).to be_uninstall_errored + expect(application.status_reason).to eq('Application uninstalled but failed to destroy: destroy failed') + end end end diff --git a/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb b/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb index c56b148cb8c..233b9db8f7b 100644 --- a/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb +++ b/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb @@ -192,16 +192,6 @@ shared_examples 'cluster application status specs' do |application_name| expect(subject).to be_uninstalling end end - - describe '#make_uninstalled' do - subject { create(application_name, :uninstalling) } - - it 'is uninstalled' do - subject.make_uninstalled! - - expect(subject).to be_uninstalled - end - end end describe '#available?' do @@ -219,7 +209,6 @@ shared_examples 'cluster application status specs' do |application_name| :update_errored | false :uninstalling | false :uninstall_errored | false - :uninstalled | false :timeouted | false end From 44eec56834b7f524a2bf99d0f5e1571b52576d72 Mon Sep 17 00:00:00 2001 From: Thong Kuah Date: Fri, 12 Apr 2019 17:42:48 +1200 Subject: [PATCH 43/73] Expose can_uninstall in cluster_status.json Only prometheus can be uninstalled atm, the rest will be dealt with later. Presumption is that new application types will have uninstallation implmemented at the same time. --- app/models/clusters/applications/cert_manager.rb | 6 ++++++ app/models/clusters/applications/helm.rb | 7 +++++++ app/models/clusters/applications/ingress.rb | 7 +++++++ app/models/clusters/applications/jupyter.rb | 6 ++++++ app/models/clusters/applications/knative.rb | 6 ++++++ app/models/clusters/applications/runner.rb | 7 +++++++ app/models/clusters/concerns/application_core.rb | 10 ++++++++++ app/serializers/cluster_application_entity.rb | 1 + spec/fixtures/api/schemas/cluster_status.json | 3 ++- spec/models/clusters/applications/cert_manager_spec.rb | 6 ++++++ spec/models/clusters/applications/helm_spec.rb | 8 ++++++++ spec/models/clusters/applications/ingress_spec.rb | 6 ++++++ spec/models/clusters/applications/jupyter_spec.rb | 9 +++++++++ spec/models/clusters/applications/knative_spec.rb | 6 ++++++ spec/models/clusters/applications/prometheus_spec.rb | 8 ++++++++ spec/models/clusters/applications/runner_spec.rb | 8 ++++++++ spec/serializers/cluster_application_entity_spec.rb | 4 ++++ .../models/cluster_application_core_shared_examples.rb | 8 ++++++++ 18 files changed, 115 insertions(+), 1 deletion(-) diff --git a/app/models/clusters/applications/cert_manager.rb b/app/models/clusters/applications/cert_manager.rb index ac0e7eb03bc..d6a7d1d2bdd 100644 --- a/app/models/clusters/applications/cert_manager.rb +++ b/app/models/clusters/applications/cert_manager.rb @@ -24,6 +24,12 @@ module Clusters 'stable/cert-manager' end + # We will implement this in future MRs. + # Need to reverse postinstall step + def allowed_to_uninstall? + false + end + def install_command Gitlab::Kubernetes::Helm::InstallCommand.new( name: 'certmanager', diff --git a/app/models/clusters/applications/helm.rb b/app/models/clusters/applications/helm.rb index 71aff00077d..a83d06c4b00 100644 --- a/app/models/clusters/applications/helm.rb +++ b/app/models/clusters/applications/helm.rb @@ -29,6 +29,13 @@ module Clusters self.status = 'installable' if cluster&.platform_kubernetes_active? end + # We will implement this in future MRs. + # Basically we need to check all other applications are not installed + # first. + def allowed_to_uninstall? + false + end + def install_command Gitlab::Kubernetes::Helm::InitCommand.new( name: name, diff --git a/app/models/clusters/applications/ingress.rb b/app/models/clusters/applications/ingress.rb index 376d54aab2c..a1023f44049 100644 --- a/app/models/clusters/applications/ingress.rb +++ b/app/models/clusters/applications/ingress.rb @@ -35,6 +35,13 @@ module Clusters 'stable/nginx-ingress' end + # We will implement this in future MRs. + # Basically we need to check all dependent applications are not installed + # first. + def allowed_to_uninstall? + false + end + def install_command Gitlab::Kubernetes::Helm::InstallCommand.new( name: name, diff --git a/app/models/clusters/applications/jupyter.rb b/app/models/clusters/applications/jupyter.rb index f86ff3551a1..987c057ad6d 100644 --- a/app/models/clusters/applications/jupyter.rb +++ b/app/models/clusters/applications/jupyter.rb @@ -38,6 +38,12 @@ module Clusters content_values.to_yaml end + # Will be addressed in future MRs + # We need to investigate and document what will be permenantly deleted. + def allowed_to_uninstall? + false + end + def install_command Gitlab::Kubernetes::Helm::InstallCommand.new( name: name, diff --git a/app/models/clusters/applications/knative.rb b/app/models/clusters/applications/knative.rb index 38cbc9ce8eb..9fbf5d8af04 100644 --- a/app/models/clusters/applications/knative.rb +++ b/app/models/clusters/applications/knative.rb @@ -51,6 +51,12 @@ module Clusters { "domain" => hostname }.to_yaml end + # Handled in a new issue: + # https://gitlab.com/gitlab-org/gitlab-ce/issues/59369 + def allowed_to_uninstall? + false + end + def install_command Gitlab::Kubernetes::Helm::InstallCommand.new( name: name, diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb index 3ebf7a5cfba..af648db3708 100644 --- a/app/models/clusters/applications/runner.rb +++ b/app/models/clusters/applications/runner.rb @@ -29,6 +29,13 @@ module Clusters content_values.to_yaml end + # Need to investigate if pipelines run by this runner will stop upon the + # executor pod stopping + # I.e.run a pipeline, and uninstall runner while pipeline is running + def allowed_to_uninstall? + false + end + def install_command Gitlab::Kubernetes::Helm::InstallCommand.new( name: name, diff --git a/app/models/clusters/concerns/application_core.rb b/app/models/clusters/concerns/application_core.rb index ee964fb7c93..4514498b84b 100644 --- a/app/models/clusters/concerns/application_core.rb +++ b/app/models/clusters/concerns/application_core.rb @@ -18,6 +18,16 @@ module Clusters self.status = 'installable' if cluster&.application_helm_available? end + def can_uninstall? + allowed_to_uninstall? + end + + # All new applications should uninstall by default + # Override if there's dependencies that needs to be uninstalled first + def allowed_to_uninstall? + true + end + def self.application_name self.to_s.demodulize.underscore end diff --git a/app/serializers/cluster_application_entity.rb b/app/serializers/cluster_application_entity.rb index a4a2c015c4e..2a916b13f52 100644 --- a/app/serializers/cluster_application_entity.rb +++ b/app/serializers/cluster_application_entity.rb @@ -10,4 +10,5 @@ class ClusterApplicationEntity < Grape::Entity expose :hostname, if: -> (e, _) { e.respond_to?(:hostname) } expose :email, if: -> (e, _) { e.respond_to?(:email) } expose :update_available?, as: :update_available, if: -> (e, _) { e.respond_to?(:update_available?) } + expose :can_uninstall?, as: :can_uninstall end diff --git a/spec/fixtures/api/schemas/cluster_status.json b/spec/fixtures/api/schemas/cluster_status.json index 9da07a0b253..695175689b9 100644 --- a/spec/fixtures/api/schemas/cluster_status.json +++ b/spec/fixtures/api/schemas/cluster_status.json @@ -36,7 +36,8 @@ "external_hostname": { "type": ["string", "null"] }, "hostname": { "type": ["string", "null"] }, "email": { "type": ["string", "null"] }, - "update_available": { "type": ["boolean", "null"] } + "update_available": { "type": ["boolean", "null"] }, + "can_uninstall": { "type": "boolean" } }, "required" : [ "name", "status" ] } diff --git a/spec/models/clusters/applications/cert_manager_spec.rb b/spec/models/clusters/applications/cert_manager_spec.rb index 5cd80edb3a1..8d853a04e33 100644 --- a/spec/models/clusters/applications/cert_manager_spec.rb +++ b/spec/models/clusters/applications/cert_manager_spec.rb @@ -10,6 +10,12 @@ describe Clusters::Applications::CertManager do include_examples 'cluster application version specs', :clusters_applications_cert_managers include_examples 'cluster application initial status specs' + describe '#can_uninstall?' do + subject { cert_manager.can_uninstall? } + + it { is_expected.to be_falsey } + end + describe '#install_command' do let(:cert_email) { 'admin@example.com' } diff --git a/spec/models/clusters/applications/helm_spec.rb b/spec/models/clusters/applications/helm_spec.rb index f177d493a2e..6ea6c110d62 100644 --- a/spec/models/clusters/applications/helm_spec.rb +++ b/spec/models/clusters/applications/helm_spec.rb @@ -18,6 +18,14 @@ describe Clusters::Applications::Helm do it { is_expected.to contain_exactly(installed_cluster, updated_cluster) } end + describe '#can_uninstall?' do + let(:helm) { create(:clusters_applications_helm) } + + subject { helm.can_uninstall? } + + it { is_expected.to be_falsey } + end + describe '#issue_client_cert' do let(:application) { create(:clusters_applications_helm) } subject { application.issue_client_cert } diff --git a/spec/models/clusters/applications/ingress_spec.rb b/spec/models/clusters/applications/ingress_spec.rb index 113d29b5551..292ddabd2d8 100644 --- a/spec/models/clusters/applications/ingress_spec.rb +++ b/spec/models/clusters/applications/ingress_spec.rb @@ -18,6 +18,12 @@ describe Clusters::Applications::Ingress do allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_async) end + describe '#can_uninstall?' do + subject { ingress.can_uninstall? } + + it { is_expected.to be_falsey } + end + describe '#make_installed!' do before do application.make_installed! diff --git a/spec/models/clusters/applications/jupyter_spec.rb b/spec/models/clusters/applications/jupyter_spec.rb index 1a7363b64f9..fc9ebed863e 100644 --- a/spec/models/clusters/applications/jupyter_spec.rb +++ b/spec/models/clusters/applications/jupyter_spec.rb @@ -10,6 +10,15 @@ describe Clusters::Applications::Jupyter do it { is_expected.to belong_to(:oauth_application) } + describe '#can_uninstall?' do + let(:ingress) { create(:clusters_applications_ingress, :installed, external_hostname: 'localhost.localdomain') } + let(:jupyter) { create(:clusters_applications_jupyter, cluster: ingress.cluster) } + + subject { jupyter.can_uninstall? } + + it { is_expected.to be_falsey } + end + describe '#set_initial_status' do before do jupyter.set_initial_status diff --git a/spec/models/clusters/applications/knative_spec.rb b/spec/models/clusters/applications/knative_spec.rb index 405b5ad691c..d5974f47190 100644 --- a/spec/models/clusters/applications/knative_spec.rb +++ b/spec/models/clusters/applications/knative_spec.rb @@ -39,6 +39,12 @@ describe Clusters::Applications::Knative do end end + describe '#can_uninstall?' do + subject { knative.can_uninstall? } + + it { is_expected.to be_falsey } + end + describe '#schedule_status_update with external_ip' do let(:application) { create(:clusters_applications_knative, :installed) } diff --git a/spec/models/clusters/applications/prometheus_spec.rb b/spec/models/clusters/applications/prometheus_spec.rb index e8ba9737c23..f390afe9b1f 100644 --- a/spec/models/clusters/applications/prometheus_spec.rb +++ b/spec/models/clusters/applications/prometheus_spec.rb @@ -29,6 +29,14 @@ describe Clusters::Applications::Prometheus do end end + describe '#can_uninstall?' do + let(:prometheus) { create(:clusters_applications_prometheus) } + + subject { prometheus.can_uninstall? } + + it { is_expected.to be_truthy } + end + describe '#prometheus_client' do context 'cluster is nil' do it 'returns nil' do diff --git a/spec/models/clusters/applications/runner_spec.rb b/spec/models/clusters/applications/runner_spec.rb index b66acf13135..bdc0cb8ed86 100644 --- a/spec/models/clusters/applications/runner_spec.rb +++ b/spec/models/clusters/applications/runner_spec.rb @@ -13,6 +13,14 @@ describe Clusters::Applications::Runner do it { is_expected.to belong_to(:runner) } + describe '#can_uninstall?' do + let(:gitlab_runner) { create(:clusters_applications_runner, runner: ci_runner) } + + subject { gitlab_runner.can_uninstall? } + + it { is_expected.to be_falsey } + end + describe '#install_command' do let(:kubeclient) { double('kubernetes client') } let(:gitlab_runner) { create(:clusters_applications_runner, runner: ci_runner) } diff --git a/spec/serializers/cluster_application_entity_spec.rb b/spec/serializers/cluster_application_entity_spec.rb index 7e151c3744e..f38a18fcf59 100644 --- a/spec/serializers/cluster_application_entity_spec.rb +++ b/spec/serializers/cluster_application_entity_spec.rb @@ -21,6 +21,10 @@ describe ClusterApplicationEntity do expect(subject[:status_reason]).to be_nil end + it 'has can_uninstall' do + expect(subject[:can_uninstall]).to be_falsey + end + context 'non-helm application' do let(:application) { build(:clusters_applications_runner, version: '0.0.0') } diff --git a/spec/support/shared_examples/models/cluster_application_core_shared_examples.rb b/spec/support/shared_examples/models/cluster_application_core_shared_examples.rb index 1f76b981292..d6490a808ce 100644 --- a/spec/support/shared_examples/models/cluster_application_core_shared_examples.rb +++ b/spec/support/shared_examples/models/cluster_application_core_shared_examples.rb @@ -2,6 +2,14 @@ shared_examples 'cluster application core specs' do |application_name| it { is_expected.to belong_to(:cluster) } it { is_expected.to validate_presence_of(:cluster) } + describe '#can_uninstall?' do + it 'calls allowed_to_uninstall?' do + expect(subject).to receive(:allowed_to_uninstall?).and_return(true) + + expect(subject.can_uninstall?).to be_truthy + end + end + describe '#name' do it 'is .application_name' do expect(subject.name).to eq(described_class.application_name) From 43c284b711ddd4db55908de0590f946de5227db6 Mon Sep 17 00:00:00 2001 From: Thong Kuah Date: Fri, 12 Apr 2019 23:15:50 +1200 Subject: [PATCH 44/73] Teach Prometheus about #uninstall_command Add specs --- .../clusters/applications/prometheus.rb | 8 ++++++ .../clusters/applications/prometheus_spec.rb | 28 +++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/app/models/clusters/applications/prometheus.rb b/app/models/clusters/applications/prometheus.rb index 954c29da196..1a8543f378e 100644 --- a/app/models/clusters/applications/prometheus.rb +++ b/app/models/clusters/applications/prometheus.rb @@ -47,6 +47,14 @@ module Clusters ) end + def uninstall_command + Gitlab::Kubernetes::Helm::DeleteCommand.new( + name: name, + rbac: cluster.platform_kubernetes_rbac?, + files: files + ) + end + def upgrade_command(values) ::Gitlab::Kubernetes::Helm::InstallCommand.new( name: name, diff --git a/spec/models/clusters/applications/prometheus_spec.rb b/spec/models/clusters/applications/prometheus_spec.rb index f390afe9b1f..4022e01195d 100644 --- a/spec/models/clusters/applications/prometheus_spec.rb +++ b/spec/models/clusters/applications/prometheus_spec.rb @@ -142,6 +142,34 @@ describe Clusters::Applications::Prometheus do end end + describe '#uninstall_command' do + let(:prometheus) { create(:clusters_applications_prometheus) } + + subject { prometheus.uninstall_command } + + it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::DeleteCommand) } + + it 'has the application name' do + expect(subject.name).to eq('prometheus') + end + + it 'has files' do + expect(subject.files).to eq(prometheus.files) + end + + it 'is rbac' do + expect(subject).to be_rbac + end + + context 'on a non rbac enabled cluster' do + before do + prometheus.cluster.platform_kubernetes.abac! + end + + it { is_expected.not_to be_rbac } + end + end + describe '#upgrade_command' do let(:prometheus) { build(:clusters_applications_prometheus) } let(:values) { prometheus.values } From abb530a61958518f6e0c739406f34c558c504206 Mon Sep 17 00:00:00 2001 From: Thong Kuah Date: Sat, 13 Apr 2019 00:19:28 +1200 Subject: [PATCH 45/73] DELETE clusters/:id/:application endpoint Add endpoint to delete/uninstall a cluster application --- .../clusters/applications_controller.rb | 13 ++++ .../clusters/applications/destroy_service.rb | 23 +++++++ app/workers/all_queues.yml | 1 + .../clusters/applications/uninstall_worker.rb | 17 +++++ config/routes.rb | 1 + .../clusters/applications_controller_spec.rb | 60 ++++++++++++++++++ .../clusters/applications_controller_spec.rb | 62 ++++++++++++++++++ .../applications/destroy_service_spec.rb | 63 +++++++++++++++++++ 8 files changed, 240 insertions(+) create mode 100644 app/services/clusters/applications/destroy_service.rb create mode 100644 app/workers/clusters/applications/uninstall_worker.rb create mode 100644 spec/services/clusters/applications/destroy_service_spec.rb diff --git a/app/controllers/clusters/applications_controller.rb b/app/controllers/clusters/applications_controller.rb index 73c744efeba..16c2365f85d 100644 --- a/app/controllers/clusters/applications_controller.rb +++ b/app/controllers/clusters/applications_controller.rb @@ -4,6 +4,7 @@ class Clusters::ApplicationsController < Clusters::BaseController before_action :cluster before_action :authorize_create_cluster!, only: [:create] before_action :authorize_update_cluster!, only: [:update] + before_action :authorize_admin_cluster!, only: [:destroy] def create request_handler do @@ -21,6 +22,14 @@ class Clusters::ApplicationsController < Clusters::BaseController end end + def destroy + request_handler do + Clusters::Applications::DestroyService + .new(@cluster, current_user, cluster_application_destroy_params) + .execute(request) + end + end + private def request_handler @@ -40,4 +49,8 @@ class Clusters::ApplicationsController < Clusters::BaseController def cluster_application_params params.permit(:application, :hostname, :email) end + + def cluster_application_destroy_params + params.permit(:application) + end end diff --git a/app/services/clusters/applications/destroy_service.rb b/app/services/clusters/applications/destroy_service.rb new file mode 100644 index 00000000000..fc74e1300a9 --- /dev/null +++ b/app/services/clusters/applications/destroy_service.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Clusters + module Applications + class DestroyService < ::Clusters::Applications::BaseService + def execute(_request) + instantiate_application.tap do |application| + break unless application.can_uninstall? + + application.make_scheduled! + + Clusters::Applications::UninstallWorker.perform_async(application.name, application.id) + end + end + + private + + def builder + cluster.method("application_#{application_name}").call + end + end + end +end diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index 0d14d313d21..7cf2e7100d5 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -33,6 +33,7 @@ - gcp_cluster:cluster_configure - gcp_cluster:cluster_project_configure - gcp_cluster:clusters_applications_wait_for_uninstall_app +- gcp_cluster:clusters_applications_uninstall - github_import_advance_stage - github_importer:github_import_import_diff_note diff --git a/app/workers/clusters/applications/uninstall_worker.rb b/app/workers/clusters/applications/uninstall_worker.rb new file mode 100644 index 00000000000..85e8ecc4ad5 --- /dev/null +++ b/app/workers/clusters/applications/uninstall_worker.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Clusters + module Applications + class UninstallWorker + include ApplicationWorker + include ClusterQueue + include ClusterApplications + + def perform(app_name, app_id) + find_application(app_name, app_id) do |app| + Clusters::Applications::UninstallService.new(app).execute + end + end + end + end +end diff --git a/config/routes.rb b/config/routes.rb index bbf00208545..f5957f43655 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -103,6 +103,7 @@ Rails.application.routes.draw do scope :applications do post '/:application', to: 'clusters/applications#create', as: :install_applications patch '/:application', to: 'clusters/applications#update', as: :update_applications + delete '/:application', to: 'clusters/applications#destroy', as: :uninstall_applications end get :cluster_status, format: :json diff --git a/spec/controllers/groups/clusters/applications_controller_spec.rb b/spec/controllers/groups/clusters/applications_controller_spec.rb index 16a63536ea6..acb9405d1a6 100644 --- a/spec/controllers/groups/clusters/applications_controller_spec.rb +++ b/spec/controllers/groups/clusters/applications_controller_spec.rb @@ -144,4 +144,64 @@ describe Groups::Clusters::ApplicationsController do it_behaves_like 'a secure endpoint' end end + + describe 'DELETE destroy' do + subject do + delete :destroy, params: params.merge(group_id: group) + end + + let!(:application) { create(:clusters_applications_cert_managers, :installed, cluster: cluster) } + let(:application_name) { application.name } + let(:params) { { application: application_name, id: cluster.id } } + let(:worker_class) { Clusters::Applications::UninstallWorker } + + describe 'functionality' do + let(:user) { create(:user) } + + before do + group.add_maintainer(user) + sign_in(user) + end + + context "when cluster and app exists" do + xit "schedules an application update" do + expect(worker_class).to receive(:perform_async).with(application.name, application.id).once + + is_expected.to have_http_status(:no_content) + + expect(cluster.application_cert_manager).to be_scheduled + end + end + + context 'when cluster do not exists' do + before do + cluster.destroy! + end + + it { is_expected.to have_http_status(:not_found) } + end + + context 'when application is unknown' do + let(:application_name) { 'unkwnown-app' } + + it { is_expected.to have_http_status(:not_found) } + end + + context 'when application is already scheduled' do + before do + application.make_scheduled! + end + + xit { is_expected.to have_http_status(:bad_request) } + end + end + + describe 'security' do + before do + allow(worker_class).to receive(:perform_async) + end + + it_behaves_like 'a secure endpoint' + end + end end diff --git a/spec/controllers/projects/clusters/applications_controller_spec.rb b/spec/controllers/projects/clusters/applications_controller_spec.rb index cd1a01f8acc..70b34f071c8 100644 --- a/spec/controllers/projects/clusters/applications_controller_spec.rb +++ b/spec/controllers/projects/clusters/applications_controller_spec.rb @@ -145,4 +145,66 @@ describe Projects::Clusters::ApplicationsController do it_behaves_like 'a secure endpoint' end end + + describe 'DELETE destroy' do + subject do + delete :destroy, params: params.merge(namespace_id: project.namespace, project_id: project) + end + + let(:cluster) { create(:cluster, :project, :provided_by_gcp) } + let(:project) { cluster.project } + let!(:application) { create(:clusters_applications_prometheus, :installed, cluster: cluster) } + let(:application_name) { application.name } + let(:params) { { application: application_name, id: cluster.id } } + let(:worker_class) { Clusters::Applications::UninstallWorker } + + describe 'functionality' do + let(:user) { create(:user) } + + before do + project.add_maintainer(user) + sign_in(user) + end + + context "when cluster and app exists" do + it "schedules an application update" do + expect(worker_class).to receive(:perform_async).with(application.name, application.id).once + + is_expected.to have_http_status(:no_content) + + expect(cluster.application_prometheus).to be_scheduled + end + end + + context 'when cluster do not exists' do + before do + cluster.destroy! + end + + it { is_expected.to have_http_status(:not_found) } + end + + context 'when application is unknown' do + let(:application_name) { 'unkwnown-app' } + + it { is_expected.to have_http_status(:not_found) } + end + + context 'when application is already scheduled' do + before do + application.make_scheduled! + end + + it { is_expected.to have_http_status(:bad_request) } + end + end + + describe 'security' do + before do + allow(worker_class).to receive(:perform_async) + end + + it_behaves_like 'a secure endpoint' + end + end end diff --git a/spec/services/clusters/applications/destroy_service_spec.rb b/spec/services/clusters/applications/destroy_service_spec.rb new file mode 100644 index 00000000000..8d9dc6a0f11 --- /dev/null +++ b/spec/services/clusters/applications/destroy_service_spec.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Clusters::Applications::DestroyService, '#execute' do + let(:cluster) { create(:cluster, :project, :provided_by_gcp) } + let(:user) { create(:user) } + let(:params) { { application: 'prometheus' } } + let(:service) { described_class.new(cluster, user, params) } + let(:test_request) { double } + let(:worker_class) { Clusters::Applications::UninstallWorker } + + subject { service.execute(test_request) } + + before do + allow(worker_class).to receive(:perform_async) + end + + context 'application is not installed' do + it 'raises Clusters::Applications::BaseService::InvalidApplicationError' do + expect(worker_class).not_to receive(:perform_async) + + expect { subject } + .to raise_exception { Clusters::Applications::BaseService::InvalidApplicationError } + .and not_change { Clusters::Applications::Prometheus.count } + .and not_change { Clusters::Applications::Prometheus.with_status(:scheduled).count } + end + end + + context 'application is installed' do + context 'application is schedulable' do + let!(:application) do + create(:clusters_applications_prometheus, :installed, cluster: cluster) + end + + it 'makes application scheduled!' do + subject + + expect(application.reload).to be_scheduled + end + + it 'schedules UninstallWorker' do + expect(worker_class).to receive(:perform_async).with(application.name, application.id) + + subject + end + end + + context 'application is not schedulable' do + let!(:application) do + create(:clusters_applications_prometheus, :updating, cluster: cluster) + end + + it 'raises StateMachines::InvalidTransition' do + expect(worker_class).not_to receive(:perform_async) + + expect { subject } + .to raise_exception { StateMachines::InvalidTransition } + .and not_change { Clusters::Applications::Prometheus.with_status(:scheduled).count } + end + end + end +end From eae0fc2bcd6f7e2e183a922321ace3380c329adc Mon Sep 17 00:00:00 2001 From: Dylan Griffith Date: Mon, 15 Apr 2019 10:10:59 +1000 Subject: [PATCH 46/73] Remove xit test for uninstall group cluster app --- .../clusters/applications_controller_spec.rb | 60 ------------------- 1 file changed, 60 deletions(-) diff --git a/spec/controllers/groups/clusters/applications_controller_spec.rb b/spec/controllers/groups/clusters/applications_controller_spec.rb index acb9405d1a6..16a63536ea6 100644 --- a/spec/controllers/groups/clusters/applications_controller_spec.rb +++ b/spec/controllers/groups/clusters/applications_controller_spec.rb @@ -144,64 +144,4 @@ describe Groups::Clusters::ApplicationsController do it_behaves_like 'a secure endpoint' end end - - describe 'DELETE destroy' do - subject do - delete :destroy, params: params.merge(group_id: group) - end - - let!(:application) { create(:clusters_applications_cert_managers, :installed, cluster: cluster) } - let(:application_name) { application.name } - let(:params) { { application: application_name, id: cluster.id } } - let(:worker_class) { Clusters::Applications::UninstallWorker } - - describe 'functionality' do - let(:user) { create(:user) } - - before do - group.add_maintainer(user) - sign_in(user) - end - - context "when cluster and app exists" do - xit "schedules an application update" do - expect(worker_class).to receive(:perform_async).with(application.name, application.id).once - - is_expected.to have_http_status(:no_content) - - expect(cluster.application_cert_manager).to be_scheduled - end - end - - context 'when cluster do not exists' do - before do - cluster.destroy! - end - - it { is_expected.to have_http_status(:not_found) } - end - - context 'when application is unknown' do - let(:application_name) { 'unkwnown-app' } - - it { is_expected.to have_http_status(:not_found) } - end - - context 'when application is already scheduled' do - before do - application.make_scheduled! - end - - xit { is_expected.to have_http_status(:bad_request) } - end - end - - describe 'security' do - before do - allow(worker_class).to receive(:perform_async) - end - - it_behaves_like 'a secure endpoint' - end - end end From 024ddcab17517befafc0c5163d66cdcaae1b69e6 Mon Sep 17 00:00:00 2001 From: Thong Kuah Date: Mon, 15 Apr 2019 16:02:52 +1200 Subject: [PATCH 47/73] Deactivate any prometheus_service upon destroy Basically does the reverse of after_transition to :installed. --- app/models/clusters/applications/prometheus.rb | 8 ++++++++ .../clusters/applications/prometheus_spec.rb | 17 +++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/app/models/clusters/applications/prometheus.rb b/app/models/clusters/applications/prometheus.rb index 1a8543f378e..1a715830953 100644 --- a/app/models/clusters/applications/prometheus.rb +++ b/app/models/clusters/applications/prometheus.rb @@ -16,6 +16,8 @@ module Clusters default_value_for :version, VERSION + after_destroy :disable_prometheus_integration + state_machine :status do after_transition any => [:installed] do |application| application.cluster.projects.each do |project| @@ -90,6 +92,12 @@ module Clusters private + def disable_prometheus_integration + cluster.projects.each do |project| + project.prometheus_service&.update(active: false) + end + end + def kube_client cluster&.kubeclient&.core_client end diff --git a/spec/models/clusters/applications/prometheus_spec.rb b/spec/models/clusters/applications/prometheus_spec.rb index 4022e01195d..76d2c4a9d1c 100644 --- a/spec/models/clusters/applications/prometheus_spec.rb +++ b/spec/models/clusters/applications/prometheus_spec.rb @@ -11,6 +11,23 @@ describe Clusters::Applications::Prometheus do include_examples 'cluster application helm specs', :clusters_applications_prometheus include_examples 'cluster application initial status specs' + describe 'after_destroy' do + let(:project) { create(:project) } + let(:cluster) { create(:cluster, :with_installed_helm, projects: [project]) } + let!(:application) { create(:clusters_applications_prometheus, :installed, cluster: cluster) } + let!(:prometheus_service) { project.create_prometheus_service(active: true) } + + it 'deactivates prometheus_service after destroy' do + expect do + application.destroy + + prometheus_service.reload + end.to change(prometheus_service, :active) + + expect(prometheus_service).not_to be_active + end + end + describe 'transition to installed' do let(:project) { create(:project) } let(:cluster) { create(:cluster, :with_installed_helm, projects: [project]) } From 3cac5b05b2fd8377883d42be5ee5e0fb2370db04 Mon Sep 17 00:00:00 2001 From: Dylan Griffith Date: Mon, 15 Apr 2019 16:42:43 +1000 Subject: [PATCH 48/73] Fix uninstall specs: helm not uninstallable --- .../check_uninstall_progress_service_spec.rb | 8 ++++---- .../clusters/applications/uninstall_service_spec.rb | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/spec/services/clusters/applications/check_uninstall_progress_service_spec.rb b/spec/services/clusters/applications/check_uninstall_progress_service_spec.rb index 084f29d9d2d..fedca51a1b4 100644 --- a/spec/services/clusters/applications/check_uninstall_progress_service_spec.rb +++ b/spec/services/clusters/applications/check_uninstall_progress_service_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' describe Clusters::Applications::CheckUninstallProgressService do RESCHEDULE_PHASES = Gitlab::Kubernetes::Pod::PHASES - [Gitlab::Kubernetes::Pod::SUCCEEDED, Gitlab::Kubernetes::Pod::FAILED].freeze - let(:application) { create(:clusters_applications_helm, :uninstalling) } + let(:application) { create(:clusters_applications_prometheus, :uninstalling) } let(:service) { described_class.new(application) } let(:phase) { Gitlab::Kubernetes::Pod::UNKNOWN } let(:errors) { nil } @@ -95,12 +95,12 @@ describe Clusters::Applications::CheckUninstallProgressService do service.execute expect(application).to be_uninstall_errored - expect(application.status_reason).to eq('Operation failed. Check pod logs for uninstall-helm for more details.') + expect(application.status_reason).to eq('Operation failed. Check pod logs for uninstall-prometheus for more details.') end end context 'when timed out' do - let(:application) { create(:clusters_applications_helm, :timeouted, :uninstalling) } + let(:application) { create(:clusters_applications_prometheus, :timeouted, :uninstalling) } before do expect(service).to receive(:installation_phase).once.and_return(phase) @@ -112,7 +112,7 @@ describe Clusters::Applications::CheckUninstallProgressService do service.execute expect(application).to be_uninstall_errored - expect(application.status_reason).to eq('Operation timed out. Check pod logs for uninstall-helm for more details.') + expect(application.status_reason).to eq('Operation timed out. Check pod logs for uninstall-prometheus for more details.') end end diff --git a/spec/services/clusters/applications/uninstall_service_spec.rb b/spec/services/clusters/applications/uninstall_service_spec.rb index d1d0e923e18..16497d752b2 100644 --- a/spec/services/clusters/applications/uninstall_service_spec.rb +++ b/spec/services/clusters/applications/uninstall_service_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe Clusters::Applications::UninstallService, '#execute' do - let(:application) { create(:clusters_applications_helm, :scheduled) } + let(:application) { create(:clusters_applications_prometheus, :scheduled) } let(:service) { described_class.new(application) } let(:helm_client) { instance_double(Gitlab::Kubernetes::Helm::Api) } let(:worker_class) { Clusters::Applications::WaitForUninstallAppWorker } @@ -14,7 +14,7 @@ describe Clusters::Applications::UninstallService, '#execute' do context 'when there are no errors' do before do - expect(helm_client).to receive(:uninstall).with(kind_of(Gitlab::Kubernetes::Helm::ResetCommand)) + expect(helm_client).to receive(:uninstall).with(kind_of(Gitlab::Kubernetes::Helm::DeleteCommand)) allow(worker_class).to receive(:perform_in).and_return(nil) end @@ -36,7 +36,7 @@ describe Clusters::Applications::UninstallService, '#execute' do let(:error) { Kubeclient::HttpError.new(500, 'system failure', nil) } before do - expect(helm_client).to receive(:uninstall).with(kind_of(Gitlab::Kubernetes::Helm::ResetCommand)).and_raise(error) + expect(helm_client).to receive(:uninstall).with(kind_of(Gitlab::Kubernetes::Helm::DeleteCommand)).and_raise(error) end include_examples 'logs kubernetes errors' do @@ -54,11 +54,11 @@ describe Clusters::Applications::UninstallService, '#execute' do end context 'a non kubernetes error happens' do - let(:application) { create(:clusters_applications_helm, :scheduled) } + let(:application) { create(:clusters_applications_prometheus, :scheduled) } let(:error) { StandardError.new('something bad happened') } before do - expect(helm_client).to receive(:uninstall).with(kind_of(Gitlab::Kubernetes::Helm::ResetCommand)).and_raise(error) + expect(helm_client).to receive(:uninstall).with(kind_of(Gitlab::Kubernetes::Helm::DeleteCommand)).and_raise(error) end include_examples 'logs kubernetes errors' do From 50fa7847bb89254ab0dbe163b5e71bb43b6d6b14 Mon Sep 17 00:00:00 2001 From: Dylan Griffith Date: Fri, 26 Apr 2019 13:28:12 +1000 Subject: [PATCH 49/73] CheckUninstallProgressService remove unnecessary begin --- .../clusters/applications/check_uninstall_progress_service.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/services/clusters/applications/check_uninstall_progress_service.rb b/app/services/clusters/applications/check_uninstall_progress_service.rb index efb2bb1b67d..663e4b1daef 100644 --- a/app/services/clusters/applications/check_uninstall_progress_service.rb +++ b/app/services/clusters/applications/check_uninstall_progress_service.rb @@ -36,9 +36,7 @@ module Clusters def check_timeout if timeouted? - begin - app.make_errored!("Operation timed out. Check pod logs for #{pod_name} for more details.") - end + app.make_errored!("Operation timed out. Check pod logs for #{pod_name} for more details.") else WaitForUninstallAppWorker.perform_in(WaitForUninstallAppWorker::INTERVAL, app.name, app.id) end From 0a4817dd77eb371f171b634e4df180eafa115721 Mon Sep 17 00:00:00 2001 From: Dylan Griffith Date: Fri, 26 Apr 2019 13:28:49 +1000 Subject: [PATCH 50/73] In Prometheus use update! instead of update In order to not miss any errors since we are not checking the return value of update --- app/models/clusters/applications/prometheus.rb | 4 ++-- spec/models/clusters/applications/prometheus_spec.rb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/models/clusters/applications/prometheus.rb b/app/models/clusters/applications/prometheus.rb index 1a715830953..a6b7617b830 100644 --- a/app/models/clusters/applications/prometheus.rb +++ b/app/models/clusters/applications/prometheus.rb @@ -21,7 +21,7 @@ module Clusters state_machine :status do after_transition any => [:installed] do |application| application.cluster.projects.each do |project| - project.find_or_initialize_service('prometheus').update(active: true) + project.find_or_initialize_service('prometheus').update!(active: true) end end end @@ -94,7 +94,7 @@ module Clusters def disable_prometheus_integration cluster.projects.each do |project| - project.prometheus_service&.update(active: false) + project.prometheus_service&.update!(active: false) end end diff --git a/spec/models/clusters/applications/prometheus_spec.rb b/spec/models/clusters/applications/prometheus_spec.rb index 76d2c4a9d1c..8649d3e2710 100644 --- a/spec/models/clusters/applications/prometheus_spec.rb +++ b/spec/models/clusters/applications/prometheus_spec.rb @@ -40,7 +40,7 @@ describe Clusters::Applications::Prometheus do end it 'ensures Prometheus service is activated' do - expect(prometheus_service).to receive(:update).with(active: true) + expect(prometheus_service).to receive(:update!).with(active: true) subject.make_installed end From 416f3971e66762246f0af3cda97c4b55e101f61b Mon Sep 17 00:00:00 2001 From: Dylan Griffith Date: Fri, 26 Apr 2019 13:30:08 +1000 Subject: [PATCH 51/73] Minor refactoring in check_uninstall_progress_service_spec --- .../check_uninstall_progress_service_spec.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/spec/services/clusters/applications/check_uninstall_progress_service_spec.rb b/spec/services/clusters/applications/check_uninstall_progress_service_spec.rb index fedca51a1b4..8ff92eb2d09 100644 --- a/spec/services/clusters/applications/check_uninstall_progress_service_spec.rb +++ b/spec/services/clusters/applications/check_uninstall_progress_service_spec.rb @@ -11,6 +11,11 @@ describe Clusters::Applications::CheckUninstallProgressService do let(:errors) { nil } let(:worker_class) { Clusters::Applications::WaitForUninstallAppWorker } + before do + allow(service).to receive(:installation_errors).and_return(errors) + allow(service).to receive(:remove_installation_pod) + end + shared_examples 'a not yet terminated installation' do |a_phase| let(:phase) { a_phase } @@ -36,11 +41,6 @@ describe Clusters::Applications::CheckUninstallProgressService do end end - before do - allow(service).to receive(:installation_errors).and_return(errors) - allow(service).to receive(:remove_installation_pod).and_return(nil) - end - context 'when application is installing' do RESCHEDULE_PHASES.each { |phase| it_behaves_like 'a not yet terminated installation', phase } From dbb284de5a18361eae937926052abd1ea3c261b5 Mon Sep 17 00:00:00 2001 From: Dylan Griffith Date: Fri, 26 Apr 2019 14:25:51 +1000 Subject: [PATCH 52/73] Minor refactor of prometheus_spec --- spec/models/clusters/applications/prometheus_spec.rb | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/spec/models/clusters/applications/prometheus_spec.rb b/spec/models/clusters/applications/prometheus_spec.rb index 8649d3e2710..26267c64112 100644 --- a/spec/models/clusters/applications/prometheus_spec.rb +++ b/spec/models/clusters/applications/prometheus_spec.rb @@ -19,12 +19,10 @@ describe Clusters::Applications::Prometheus do it 'deactivates prometheus_service after destroy' do expect do - application.destroy + application.destroy! prometheus_service.reload - end.to change(prometheus_service, :active) - - expect(prometheus_service).not_to be_active + end.to change(prometheus_service, :active).from(true).to(false) end end From a1c216e5e4f566b762e185ad36bf566f14268cba Mon Sep 17 00:00:00 2001 From: Dylan Griffith Date: Fri, 26 Apr 2019 13:27:06 +1000 Subject: [PATCH 53/73] Use #public_send instead #method.call These builder methods are using user provided input inside a public_send but this is safe to do in this instance because before they are called we check before calling them that they match an expected application name. --- app/services/clusters/applications/create_service.rb | 4 ++-- app/services/clusters/applications/destroy_service.rb | 2 +- app/services/clusters/applications/update_service.rb | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/services/clusters/applications/create_service.rb b/app/services/clusters/applications/create_service.rb index ae36da7b3dd..f723c42c049 100644 --- a/app/services/clusters/applications/create_service.rb +++ b/app/services/clusters/applications/create_service.rb @@ -10,8 +10,8 @@ module Clusters end def builder - cluster.method("application_#{application_name}").call || - cluster.method("build_application_#{application_name}").call + cluster.public_send(:"application_#{application_name}") || # rubocop:disable GitlabSecurity/PublicSend + cluster.public_send(:"build_application_#{application_name}") # rubocop:disable GitlabSecurity/PublicSend end end end diff --git a/app/services/clusters/applications/destroy_service.rb b/app/services/clusters/applications/destroy_service.rb index fc74e1300a9..f3a4c4f754a 100644 --- a/app/services/clusters/applications/destroy_service.rb +++ b/app/services/clusters/applications/destroy_service.rb @@ -16,7 +16,7 @@ module Clusters private def builder - cluster.method("application_#{application_name}").call + cluster.public_send(:"application_#{application_name}") # rubocop:disable GitlabSecurity/PublicSend end end end diff --git a/app/services/clusters/applications/update_service.rb b/app/services/clusters/applications/update_service.rb index 5071c31839c..0fa937da865 100644 --- a/app/services/clusters/applications/update_service.rb +++ b/app/services/clusters/applications/update_service.rb @@ -10,7 +10,7 @@ module Clusters end def builder - cluster.method("application_#{application_name}").call + cluster.public_send(:"application_#{application_name}") # rubocop:disable GitlabSecurity/PublicSend end end end From f84fae1386eec1b22c1405a8b87beb30d8f10519 Mon Sep 17 00:00:00 2001 From: Dylan Griffith Date: Mon, 29 Apr 2019 15:57:12 +1000 Subject: [PATCH 54/73] Rename #timeouted -> #timed_out --- .../applications/check_installation_progress_service.rb | 4 ++-- .../applications/check_uninstall_progress_service.rb | 4 ++-- spec/factories/clusters/applications/helm.rb | 2 +- .../check_installation_progress_service_spec.rb | 6 +++--- .../applications/check_uninstall_progress_service_spec.rb | 4 ++-- .../models/cluster_application_status_shared_examples.rb | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/app/services/clusters/applications/check_installation_progress_service.rb b/app/services/clusters/applications/check_installation_progress_service.rb index c592d608b89..ae4b86a0614 100644 --- a/app/services/clusters/applications/check_installation_progress_service.rb +++ b/app/services/clusters/applications/check_installation_progress_service.rb @@ -37,7 +37,7 @@ module Clusters end def check_timeout - if timeouted? + if timed_out? begin app.make_errored!("Operation timed out. Check pod logs for #{pod_name} for more details.") end @@ -51,7 +51,7 @@ module Clusters install_command.pod_name end - def timeouted? + def timed_out? Time.now.utc - app.updated_at.to_time.utc > ClusterWaitForAppInstallationWorker::TIMEOUT end diff --git a/app/services/clusters/applications/check_uninstall_progress_service.rb b/app/services/clusters/applications/check_uninstall_progress_service.rb index 663e4b1daef..022cc754af9 100644 --- a/app/services/clusters/applications/check_uninstall_progress_service.rb +++ b/app/services/clusters/applications/check_uninstall_progress_service.rb @@ -35,7 +35,7 @@ module Clusters end def check_timeout - if timeouted? + if timed_out? app.make_errored!("Operation timed out. Check pod logs for #{pod_name} for more details.") else WaitForUninstallAppWorker.perform_in(WaitForUninstallAppWorker::INTERVAL, app.name, app.id) @@ -46,7 +46,7 @@ module Clusters app.uninstall_command.pod_name end - def timeouted? + def timed_out? Time.now.utc - app.updated_at.to_time.utc > WaitForUninstallAppWorker::TIMEOUT end diff --git a/spec/factories/clusters/applications/helm.rb b/spec/factories/clusters/applications/helm.rb index 22a0888947e..d78f01828d7 100644 --- a/spec/factories/clusters/applications/helm.rb +++ b/spec/factories/clusters/applications/helm.rb @@ -49,7 +49,7 @@ FactoryBot.define do status_reason 'something went wrong' end - trait :timeouted do + trait :timed_out do installing updated_at { ClusterWaitForAppInstallationWorker::TIMEOUT.ago } end diff --git a/spec/services/clusters/applications/check_installation_progress_service_spec.rb b/spec/services/clusters/applications/check_installation_progress_service_spec.rb index 8ad90aaf720..a54bd85a11a 100644 --- a/spec/services/clusters/applications/check_installation_progress_service_spec.rb +++ b/spec/services/clusters/applications/check_installation_progress_service_spec.rb @@ -18,7 +18,7 @@ describe Clusters::Applications::CheckInstallationProgressService, '#execute' do end context "when phase is #{a_phase}" do - context 'when not timeouted' do + context 'when not timed_out' do it 'reschedule a new check' do expect(ClusterWaitForAppInstallationWorker).to receive(:perform_in).once expect(service).not_to receive(:remove_installation_pod) @@ -113,7 +113,7 @@ describe Clusters::Applications::CheckInstallationProgressService, '#execute' do end context 'when timed out' do - let(:application) { create(:clusters_applications_helm, :timeouted, :updating) } + let(:application) { create(:clusters_applications_helm, :timed_out, :updating) } before do expect(service).to receive(:installation_phase).once.and_return(phase) @@ -174,7 +174,7 @@ describe Clusters::Applications::CheckInstallationProgressService, '#execute' do end context 'when timed out' do - let(:application) { create(:clusters_applications_helm, :timeouted) } + let(:application) { create(:clusters_applications_helm, :timed_out) } before do expect(service).to receive(:installation_phase).once.and_return(phase) diff --git a/spec/services/clusters/applications/check_uninstall_progress_service_spec.rb b/spec/services/clusters/applications/check_uninstall_progress_service_spec.rb index 8ff92eb2d09..d0730399268 100644 --- a/spec/services/clusters/applications/check_uninstall_progress_service_spec.rb +++ b/spec/services/clusters/applications/check_uninstall_progress_service_spec.rb @@ -24,7 +24,7 @@ describe Clusters::Applications::CheckUninstallProgressService do end context "when phase is #{a_phase}" do - context 'when not timeouted' do + context 'when not timed_out' do it 'reschedule a new check' do expect(worker_class).to receive(:perform_in).once expect(service).not_to receive(:remove_installation_pod) @@ -100,7 +100,7 @@ describe Clusters::Applications::CheckUninstallProgressService do end context 'when timed out' do - let(:application) { create(:clusters_applications_prometheus, :timeouted, :uninstalling) } + let(:application) { create(:clusters_applications_prometheus, :timed_out, :uninstalling) } before do expect(service).to receive(:installation_phase).once.and_return(phase) diff --git a/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb b/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb index 233b9db8f7b..4525c03837f 100644 --- a/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb +++ b/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb @@ -209,7 +209,7 @@ shared_examples 'cluster application status specs' do |application_name| :update_errored | false :uninstalling | false :uninstall_errored | false - :timeouted | false + :timed_out | false end with_them do From d252b99a0eb739e277b7453c99ac78c592ed2e03 Mon Sep 17 00:00:00 2001 From: Dylan Griffith Date: Tue, 30 Apr 2019 14:49:26 +1000 Subject: [PATCH 55/73] Internationalize errors CheckUninstallProgressService --- .../applications/check_uninstall_progress_service.rb | 8 ++++---- locale/gitlab.pot | 9 +++++++++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/app/services/clusters/applications/check_uninstall_progress_service.rb b/app/services/clusters/applications/check_uninstall_progress_service.rb index 022cc754af9..12245ec90c3 100644 --- a/app/services/clusters/applications/check_uninstall_progress_service.rb +++ b/app/services/clusters/applications/check_uninstall_progress_service.rb @@ -17,7 +17,7 @@ module Clusters rescue Kubeclient::HttpError => e log_error(e) - app.make_errored!("Kubernetes error: #{e.error_code}") + app.make_errored!(_('Kubernetes error: %{error_code}') % { error_code: e.error_code }) end private @@ -25,18 +25,18 @@ module Clusters def on_success app.destroy! rescue StandardError => e - app.make_errored!("Application uninstalled but failed to destroy: #{e.message}") + app.make_errored!(_('Application uninstalled but failed to destroy: %{error_message}') % { error_message: e.message }) ensure remove_installation_pod end def on_failed - app.make_errored!("Operation failed. Check pod logs for #{pod_name} for more details.") + app.make_errored!(_('Operation failed. Check pod logs for %{pod_name} for more details.') % { pod_name: pod_name }) end def check_timeout if timed_out? - app.make_errored!("Operation timed out. Check pod logs for #{pod_name} for more details.") + app.make_errored!(_('Operation timed out. Check pod logs for %{pod_name} for more details.') % { pod_name: pod_name }) else WaitForUninstallAppWorker.perform_in(WaitForUninstallAppWorker::INTERVAL, app.name, app.id) end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index c8b583575c8..3d56efa9834 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -942,6 +942,9 @@ msgstr "" msgid "Application settings saved successfully" msgstr "" +msgid "Application uninstalled but failed to destroy: %{error_message}" +msgstr "" + msgid "Application was successfully destroyed." msgstr "" @@ -6274,6 +6277,12 @@ msgstr "" msgid "Opens in a new window" msgstr "" +msgid "Operation failed. Check pod logs for %{pod_name} for more details." +msgstr "" + +msgid "Operation timed out. Check pod logs for %{pod_name} for more details." +msgstr "" + msgid "Operations" msgstr "" From 3d94ab3222323b2ba93b736b45a47519365dfd9e Mon Sep 17 00:00:00 2001 From: Dylan Griffith Date: Tue, 30 Apr 2019 14:59:48 +1000 Subject: [PATCH 56/73] Remove unncessary `to_time` in cluster services --- .../applications/check_installation_progress_service.rb | 2 +- .../clusters/applications/check_uninstall_progress_service.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/services/clusters/applications/check_installation_progress_service.rb b/app/services/clusters/applications/check_installation_progress_service.rb index ae4b86a0614..3c6803d24e6 100644 --- a/app/services/clusters/applications/check_installation_progress_service.rb +++ b/app/services/clusters/applications/check_installation_progress_service.rb @@ -52,7 +52,7 @@ module Clusters end def timed_out? - Time.now.utc - app.updated_at.to_time.utc > ClusterWaitForAppInstallationWorker::TIMEOUT + Time.now.utc - app.updated_at.utc > ClusterWaitForAppInstallationWorker::TIMEOUT end def remove_installation_pod diff --git a/app/services/clusters/applications/check_uninstall_progress_service.rb b/app/services/clusters/applications/check_uninstall_progress_service.rb index 12245ec90c3..8786d295d6a 100644 --- a/app/services/clusters/applications/check_uninstall_progress_service.rb +++ b/app/services/clusters/applications/check_uninstall_progress_service.rb @@ -47,7 +47,7 @@ module Clusters end def timed_out? - Time.now.utc - app.updated_at.to_time.utc > WaitForUninstallAppWorker::TIMEOUT + Time.now.utc - app.updated_at.utc > WaitForUninstallAppWorker::TIMEOUT end def remove_installation_pod From 3c52ff8c78b4e9174214d5efe5a8664d8cc608ca Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Mon, 29 Apr 2019 22:54:17 -0700 Subject: [PATCH 57/73] Add a newline in spec for readability --- .../applications/check_uninstall_progress_service_spec.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/services/clusters/applications/check_uninstall_progress_service_spec.rb b/spec/services/clusters/applications/check_uninstall_progress_service_spec.rb index d0730399268..9ab83d913f5 100644 --- a/spec/services/clusters/applications/check_uninstall_progress_service_spec.rb +++ b/spec/services/clusters/applications/check_uninstall_progress_service_spec.rb @@ -60,6 +60,7 @@ describe Clusters::Applications::CheckUninstallProgressService do expect(worker_class).not_to receive(:perform_in) service.execute + expect(application).to be_destroyed end From 84103e2d2f2668053c09e7fc225610005dfaa3f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Tue, 30 Apr 2019 09:49:10 +0200 Subject: [PATCH 58/73] Disable HTTP for the nginx-ingress Review App service MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- scripts/review_apps/review-apps.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/review_apps/review-apps.sh b/scripts/review_apps/review-apps.sh index 8be22dc0278..9455e462617 100755 --- a/scripts/review_apps/review-apps.sh +++ b/scripts/review_apps/review-apps.sh @@ -216,6 +216,7 @@ HELM_CMD=$(cat << EOF --set global.ingress.configureCertmanager=false \ --set global.ingress.tls.secretName=tls-cert \ --set global.ingress.annotations."external-dns\.alpha\.kubernetes\.io/ttl"="10" + --set nginx-ingress.controller.service.enableHttp=false \ --set nginx-ingress.defaultBackend.resources.requests.memory=7Mi \ --set nginx-ingress.controller.resources.requests.memory=440M \ --set nginx-ingress.controller.replicaCount=2 \ From 6f0d8ebc45ef1d0e5a03a0d2965169483bf7224d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Javier=20L=C3=B3pez?= Date: Tue, 30 Apr 2019 08:21:21 +0000 Subject: [PATCH 59/73] Refactored LfsImportService and ImportService In order to make `LfsImportService` more reusable, we need to extract the logic inside `ImportService` and encapsulate it into the service. --- app/services/projects/import_service.rb | 13 +- .../lfs_download_link_list_service.rb | 4 +- .../lfs_pointers/lfs_import_service.rb | 92 ++-------- .../projects/lfs_pointers/lfs_link_service.rb | 4 +- .../lfs_object_download_list_service.rb | 96 +++++++++++ spec/services/projects/import_service_spec.rb | 45 +++-- .../lfs_download_link_list_service_spec.rb | 3 +- .../lfs_pointers/lfs_download_service_spec.rb | 1 - .../lfs_pointers/lfs_import_service_spec.rb | 157 ++++-------------- .../lfs_pointers/lfs_link_service_spec.rb | 1 - .../lfs_object_download_list_service_spec.rb | 148 +++++++++++++++++ 11 files changed, 328 insertions(+), 236 deletions(-) create mode 100644 app/services/projects/lfs_pointers/lfs_object_download_list_service.rb create mode 100644 spec/services/projects/lfs_pointers/lfs_object_download_list_service_spec.rb diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb index 642465551c9..073c14040ce 100644 --- a/app/services/projects/import_service.rb +++ b/app/services/projects/import_service.rb @@ -94,16 +94,13 @@ module Projects return unless project.lfs_enabled? - lfs_objects_to_download = Projects::LfsPointers::LfsImportService.new(project).execute + result = Projects::LfsPointers::LfsImportService.new(project).execute - lfs_objects_to_download.each do |lfs_download_object| - Projects::LfsPointers::LfsDownloadService.new(project, lfs_download_object) - .execute + if result[:status] == :error + # To avoid aborting the importing process, we silently fail + # if any exception raises. + Gitlab::AppLogger.error("The Lfs import process failed. #{result[:message]}") end - rescue => e - # Right now, to avoid aborting the importing process, we silently fail - # if any exception raises. - Rails.logger.error("The Lfs import process failed. #{e.message}") end def import_data diff --git a/app/services/projects/lfs_pointers/lfs_download_link_list_service.rb b/app/services/projects/lfs_pointers/lfs_download_link_list_service.rb index a9570176e81..05974948505 100644 --- a/app/services/projects/lfs_pointers/lfs_download_link_list_service.rb +++ b/app/services/projects/lfs_pointers/lfs_download_link_list_service.rb @@ -21,9 +21,9 @@ module Projects # This method accepts two parameters: # - oids: hash of oids to query. The structure is { lfs_file_oid => lfs_file_size } # - # Returns a hash with the structure { lfs_file_oids => download_link } + # Returns an array of LfsDownloadObject def execute(oids) - return {} unless project&.lfs_enabled? && remote_uri && oids.present? + return [] unless project&.lfs_enabled? && remote_uri && oids.present? get_download_links(oids) end diff --git a/app/services/projects/lfs_pointers/lfs_import_service.rb b/app/services/projects/lfs_pointers/lfs_import_service.rb index 9215fa0a7bf..2afcce7099b 100644 --- a/app/services/projects/lfs_pointers/lfs_import_service.rb +++ b/app/services/projects/lfs_pointers/lfs_import_service.rb @@ -1,95 +1,23 @@ # frozen_string_literal: true -# This service manages the whole worflow of discovering the Lfs files in a -# repository, linking them to the project and downloading (and linking) the non -# existent ones. +# This service is responsible of managing the retrieval of the lfs objects, +# and call the service LfsDownloadService, which performs the download +# for each of the retrieved lfs objects module Projects module LfsPointers class LfsImportService < BaseService - include Gitlab::Utils::StrongMemoize - - HEAD_REV = 'HEAD'.freeze - LFS_ENDPOINT_PATTERN = /^\t?url\s*=\s*(.+)$/.freeze - LFS_BATCH_API_ENDPOINT = '/info/lfs/objects/batch'.freeze - - LfsImportError = Class.new(StandardError) - def execute - return {} unless project&.lfs_enabled? + return success unless project&.lfs_enabled? - if external_lfs_endpoint? - # If the endpoint host is different from the import_url it means - # that the repo is using a third party service for storing the LFS files. - # In this case, we have to disable lfs in the project - disable_lfs! + lfs_objects_to_download = LfsObjectDownloadListService.new(project).execute - return {} + lfs_objects_to_download.each do |lfs_download_object| + LfsDownloadService.new(project, lfs_download_object).execute end - get_download_links - rescue LfsDownloadLinkListService::DownloadLinksError => e - raise LfsImportError, "The LFS objects download list couldn't be imported. Error: #{e.message}" - end - - private - - def external_lfs_endpoint? - lfsconfig_endpoint_uri && lfsconfig_endpoint_uri.host != import_uri.host - end - - def disable_lfs! - project.update(lfs_enabled: false) - end - - # rubocop: disable CodeReuse/ActiveRecord - def get_download_links - existent_lfs = LfsListService.new(project).execute - linked_oids = LfsLinkService.new(project).execute(existent_lfs.keys) - - # Retrieving those oids not linked and which we need to download - not_linked_lfs = existent_lfs.except(*linked_oids) - - LfsDownloadLinkListService.new(project, remote_uri: current_endpoint_uri).execute(not_linked_lfs) - end - # rubocop: enable CodeReuse/ActiveRecord - - def lfsconfig_endpoint_uri - strong_memoize(:lfsconfig_endpoint_uri) do - # Retrieveing the blob data from the .lfsconfig file - data = project.repository.lfsconfig_for(HEAD_REV) - # Parsing the data to retrieve the url - parsed_data = data&.match(LFS_ENDPOINT_PATTERN) - - if parsed_data - URI.parse(parsed_data[1]).tap do |endpoint| - endpoint.user ||= import_uri.user - endpoint.password ||= import_uri.password - end - end - end - rescue URI::InvalidURIError - raise LfsImportError, 'Invalid URL in .lfsconfig file' - end - - def import_uri - @import_uri ||= URI.parse(project.import_url) - rescue URI::InvalidURIError - raise LfsImportError, 'Invalid project import URL' - end - - def current_endpoint_uri - (lfsconfig_endpoint_uri || default_endpoint_uri) - end - - # The import url must end with '.git' here we ensure it is - def default_endpoint_uri - @default_endpoint_uri ||= begin - import_uri.dup.tap do |uri| - path = uri.path.gsub(%r(/$), '') - path += '.git' unless path.ends_with?('.git') - uri.path = path + LFS_BATCH_API_ENDPOINT - end - end + success + rescue => e + error(e.message) end end end diff --git a/app/services/projects/lfs_pointers/lfs_link_service.rb b/app/services/projects/lfs_pointers/lfs_link_service.rb index 8401f3d1d89..e3c956250f0 100644 --- a/app/services/projects/lfs_pointers/lfs_link_service.rb +++ b/app/services/projects/lfs_pointers/lfs_link_service.rb @@ -6,9 +6,9 @@ module Projects class LfsLinkService < BaseService # Accept an array of oids to link # - # Returns a hash with the same structure with oids linked + # Returns an array with the oid of the existent lfs objects def execute(oids) - return {} unless project&.lfs_enabled? + return [] unless project&.lfs_enabled? # Search and link existing LFS Object link_existing_lfs_objects(oids) diff --git a/app/services/projects/lfs_pointers/lfs_object_download_list_service.rb b/app/services/projects/lfs_pointers/lfs_object_download_list_service.rb new file mode 100644 index 00000000000..5ba0f50f2ff --- /dev/null +++ b/app/services/projects/lfs_pointers/lfs_object_download_list_service.rb @@ -0,0 +1,96 @@ +# frozen_string_literal: true + +# This service manages the whole worflow of discovering the Lfs files in a +# repository, linking them to the project and downloading (and linking) the non +# existent ones. +module Projects + module LfsPointers + class LfsObjectDownloadListService < BaseService + include Gitlab::Utils::StrongMemoize + + HEAD_REV = 'HEAD'.freeze + LFS_ENDPOINT_PATTERN = /^\t?url\s*=\s*(.+)$/.freeze + LFS_BATCH_API_ENDPOINT = '/info/lfs/objects/batch'.freeze + + LfsObjectDownloadListError = Class.new(StandardError) + + def execute + return [] unless project&.lfs_enabled? + + if external_lfs_endpoint? + # If the endpoint host is different from the import_url it means + # that the repo is using a third party service for storing the LFS files. + # In this case, we have to disable lfs in the project + disable_lfs! + + return [] + end + + # Getting all Lfs pointers already in the database and linking them to the project + linked_oids = LfsLinkService.new(project).execute(lfs_pointers_in_repository.keys) + # Retrieving those oids not present in the database which we need to download + missing_oids = lfs_pointers_in_repository.except(*linked_oids) # rubocop: disable CodeReuse/ActiveRecord + # Downloading the required information and gathering it inside a LfsDownloadObject for each oid + LfsDownloadLinkListService.new(project, remote_uri: current_endpoint_uri).execute(missing_oids) + rescue LfsDownloadLinkListService::DownloadLinksError => e + raise LfsObjectDownloadListError, "The LFS objects download list couldn't be imported. Error: #{e.message}" + end + + private + + def external_lfs_endpoint? + lfsconfig_endpoint_uri && lfsconfig_endpoint_uri.host != import_uri.host + end + + def disable_lfs! + unless project.update(lfs_enabled: false) + raise LfsDownloadLinkListService::DownloadLinksError, "Invalid project state" + end + end + + # Retrieves all lfs pointers in the repository + def lfs_pointers_in_repository + @lfs_pointers_in_repository ||= LfsListService.new(project).execute + end + + def lfsconfig_endpoint_uri + strong_memoize(:lfsconfig_endpoint_uri) do + # Retrieveing the blob data from the .lfsconfig file + data = project.repository.lfsconfig_for(HEAD_REV) + # Parsing the data to retrieve the url + parsed_data = data&.match(LFS_ENDPOINT_PATTERN) + + if parsed_data + URI.parse(parsed_data[1]).tap do |endpoint| + endpoint.user ||= import_uri.user + endpoint.password ||= import_uri.password + end + end + end + rescue URI::InvalidURIError + raise LfsObjectDownloadListError, 'Invalid URL in .lfsconfig file' + end + + def import_uri + @import_uri ||= URI.parse(project.import_url) + rescue URI::InvalidURIError + raise LfsObjectDownloadListError, 'Invalid project import URL' + end + + def current_endpoint_uri + (lfsconfig_endpoint_uri || default_endpoint_uri) + end + + # The import url must end with '.git' here we ensure it is + def default_endpoint_uri + @default_endpoint_uri ||= begin + import_uri.dup.tap do |uri| + path = uri.path.gsub(%r(/$), '') + path += '.git' unless path.ends_with?('.git') + uri.path = path + LFS_BATCH_API_ENDPOINT + end + end + end + end + end +end diff --git a/spec/services/projects/import_service_spec.rb b/spec/services/projects/import_service_spec.rb index 7f233a52f50..d9f9ede8ecd 100644 --- a/spec/services/projects/import_service_spec.rb +++ b/spec/services/projects/import_service_spec.rb @@ -5,15 +5,11 @@ require 'spec_helper' describe Projects::ImportService do let!(:project) { create(:project) } let(:user) { project.creator } - let(:import_url) { 'http://www.gitlab.com/demo/repo.git' } - let(:oid_download_links) { { 'oid1' => "#{import_url}/gitlab-lfs/objects/oid1", 'oid2' => "#{import_url}/gitlab-lfs/objects/oid2" } } subject { described_class.new(project, user) } before do allow(project).to receive(:lfs_enabled?).and_return(true) - allow_any_instance_of(Projects::LfsPointers::LfsDownloadService).to receive(:execute) - allow_any_instance_of(Projects::LfsPointers::LfsImportService).to receive(:execute).and_return(oid_download_links) end describe '#async?' do @@ -77,7 +73,6 @@ describe Projects::ImportService do context 'when repository creation succeeds' do it 'does not download lfs files' do expect_any_instance_of(Projects::LfsPointers::LfsImportService).not_to receive(:execute) - expect_any_instance_of(Projects::LfsPointers::LfsDownloadService).not_to receive(:execute) subject.execute end @@ -114,7 +109,6 @@ describe Projects::ImportService do context 'when repository import scheduled' do it 'does not download lfs objects' do expect_any_instance_of(Projects::LfsPointers::LfsImportService).not_to receive(:execute) - expect_any_instance_of(Projects::LfsPointers::LfsDownloadService).not_to receive(:execute) subject.execute end @@ -130,7 +124,7 @@ describe Projects::ImportService do it 'succeeds if repository import is successful' do expect_any_instance_of(Gitlab::Shell).to receive(:import_repository).and_return(true) expect_any_instance_of(Gitlab::BitbucketImport::Importer).to receive(:execute).and_return(true) - expect_any_instance_of(Projects::LfsPointers::LfsImportService).to receive(:execute).and_return({}) + expect_any_instance_of(Projects::LfsPointers::LfsImportService).to receive(:execute).and_return(status: :success) result = subject.execute @@ -146,6 +140,19 @@ describe Projects::ImportService do expect(result[:message]).to eq "Error importing repository #{project.safe_import_url} into #{project.full_path} - Failed to import the repository [FILTERED]" end + context 'when lfs import fails' do + it 'logs the error' do + error_message = 'error message' + + expect_any_instance_of(Gitlab::Shell).to receive(:import_repository).and_return(true) + expect_any_instance_of(Gitlab::BitbucketImport::Importer).to receive(:execute).and_return(true) + expect_any_instance_of(Projects::LfsPointers::LfsImportService).to receive(:execute).and_return(status: :error, message: error_message) + expect(Gitlab::AppLogger).to receive(:error).with("The Lfs import process failed. #{error_message}") + + subject.execute + end + end + context 'when repository import scheduled' do before do allow_any_instance_of(Gitlab::Shell).to receive(:import_repository).and_return(true) @@ -155,10 +162,7 @@ describe Projects::ImportService do it 'downloads lfs objects if lfs_enabled is enabled for project' do allow(project).to receive(:lfs_enabled?).and_return(true) - service = double - expect_any_instance_of(Projects::LfsPointers::LfsImportService).to receive(:execute).and_return(oid_download_links) - expect(Projects::LfsPointers::LfsDownloadService).to receive(:new).and_return(service).twice - expect(service).to receive(:execute).twice + expect_any_instance_of(Projects::LfsPointers::LfsImportService).to receive(:execute) subject.execute end @@ -166,7 +170,6 @@ describe Projects::ImportService do it 'does not download lfs objects if lfs_enabled is not enabled for project' do allow(project).to receive(:lfs_enabled?).and_return(false) expect_any_instance_of(Projects::LfsPointers::LfsImportService).not_to receive(:execute) - expect_any_instance_of(Projects::LfsPointers::LfsDownloadService).not_to receive(:execute) subject.execute end @@ -208,7 +211,6 @@ describe Projects::ImportService do allow(Gitlab::GithubImport::ParallelImporter).to receive(:imports_repository?).and_return(true) expect_any_instance_of(Projects::LfsPointers::LfsImportService).not_to receive(:execute) - expect_any_instance_of(Projects::LfsPointers::LfsDownloadService).not_to receive(:execute) subject.execute end @@ -216,13 +218,22 @@ describe Projects::ImportService do it 'does not have a custom repository importer downloads lfs objects' do allow(Gitlab::GithubImport::ParallelImporter).to receive(:imports_repository?).and_return(false) - service = double - expect_any_instance_of(Projects::LfsPointers::LfsImportService).to receive(:execute).and_return(oid_download_links) - expect(Projects::LfsPointers::LfsDownloadService).to receive(:new).and_return(service).twice - expect(service).to receive(:execute).twice + expect_any_instance_of(Projects::LfsPointers::LfsImportService).to receive(:execute) subject.execute end + + context 'when lfs import fails' do + it 'logs the error' do + error_message = 'error message' + + allow(Gitlab::GithubImport::ParallelImporter).to receive(:imports_repository?).and_return(false) + expect_any_instance_of(Projects::LfsPointers::LfsImportService).to receive(:execute).and_return(status: :error, message: error_message) + expect(Gitlab::AppLogger).to receive(:error).with("The Lfs import process failed. #{error_message}") + + subject.execute + end + end end end diff --git a/spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb b/spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb index f1c0f5b9576..d8427d0bf78 100644 --- a/spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb +++ b/spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb @@ -1,5 +1,4 @@ # frozen_string_literal: true - require 'spec_helper' describe Projects::LfsPointers::LfsDownloadLinkListService do @@ -85,7 +84,7 @@ describe Projects::LfsPointers::LfsDownloadLinkListService do end describe '#get_download_links' do - it 'raise errorif request fails' do + it 'raise error if request fails' do allow(Gitlab::HTTP).to receive(:post).and_return(Struct.new(:success?, :message).new(false, 'Failed request')) expect { subject.send(:get_download_links, new_oids) }.to raise_error(described_class::DownloadLinksError) diff --git a/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb b/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb index cde3f2d6155..f4470b50753 100644 --- a/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb +++ b/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb @@ -1,5 +1,4 @@ # frozen_string_literal: true - require 'spec_helper' describe Projects::LfsPointers::LfsDownloadService do diff --git a/spec/services/projects/lfs_pointers/lfs_import_service_spec.rb b/spec/services/projects/lfs_pointers/lfs_import_service_spec.rb index 5c9ca99df7c..7ca20a6d751 100644 --- a/spec/services/projects/lfs_pointers/lfs_import_service_spec.rb +++ b/spec/services/projects/lfs_pointers/lfs_import_service_spec.rb @@ -1,148 +1,63 @@ # frozen_string_literal: true - require 'spec_helper' describe Projects::LfsPointers::LfsImportService do + let(:project) { create(:project) } + let(:user) { project.creator } let(:import_url) { 'http://www.gitlab.com/demo/repo.git' } - let(:default_endpoint) { "#{import_url}/info/lfs/objects/batch"} - let(:group) { create(:group, lfs_enabled: true)} - let!(:project) { create(:project, namespace: group, import_url: import_url, lfs_enabled: true) } - let!(:lfs_objects_project) { create_list(:lfs_objects_project, 2, project: project) } - let!(:existing_lfs_objects) { LfsObject.pluck(:oid, :size).to_h } - let(:oids) { { 'oid1' => 123, 'oid2' => 125 } } let(:oid_download_links) { { 'oid1' => "#{import_url}/gitlab-lfs/objects/oid1", 'oid2' => "#{import_url}/gitlab-lfs/objects/oid2" } } - let(:all_oids) { existing_lfs_objects.merge(oids) } - let(:remote_uri) { URI.parse(lfs_endpoint) } - subject { described_class.new(project) } + subject { described_class.new(project, user) } - before do - allow(project.repository).to receive(:lfsconfig_for).and_return(nil) - allow(Gitlab.config.lfs).to receive(:enabled).and_return(true) - allow_any_instance_of(Projects::LfsPointers::LfsListService).to receive(:execute).and_return(all_oids) - end + context 'when lfs is enabled for the project' do + before do + allow(project).to receive(:lfs_enabled?).and_return(true) + end - describe '#execute' do - context 'when no lfs pointer is linked' do - before do - allow_any_instance_of(Projects::LfsPointers::LfsLinkService).to receive(:execute).and_return([]) - allow_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).and_return(oid_download_links) - expect(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:new).with(project, remote_uri: URI.parse(default_endpoint)).and_call_original - end + it 'downloads lfs objects' do + service = double + expect_any_instance_of(Projects::LfsPointers::LfsObjectDownloadListService).to receive(:execute).and_return(oid_download_links) + expect(Projects::LfsPointers::LfsDownloadService).to receive(:new).and_return(service).twice + expect(service).to receive(:execute).twice - it 'retrieves all lfs pointers in the project repository' do - expect_any_instance_of(Projects::LfsPointers::LfsListService).to receive(:execute) + result = subject.execute - subject.execute - end + expect(result[:status]).to eq :success + end - it 'links existent lfs objects to the project' do - expect_any_instance_of(Projects::LfsPointers::LfsLinkService).to receive(:execute) + context 'when no downloadable lfs object links' do + it 'does not call LfsDownloadService' do + expect_any_instance_of(Projects::LfsPointers::LfsObjectDownloadListService).to receive(:execute).and_return({}) + expect(Projects::LfsPointers::LfsDownloadService).not_to receive(:new) - subject.execute - end + result = subject.execute - it 'retrieves the download links of non existent objects' do - expect_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).with(all_oids) - - subject.execute + expect(result[:status]).to eq :success end end - context 'when some lfs objects are linked' do - before do - allow_any_instance_of(Projects::LfsPointers::LfsLinkService).to receive(:execute).and_return(existing_lfs_objects.keys) - allow_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).and_return(oid_download_links) - end + context 'when an exception is raised' do + it 'returns error' do + error_message = "error message" + expect_any_instance_of(Projects::LfsPointers::LfsObjectDownloadListService).to receive(:execute).and_raise(StandardError, error_message) - it 'retrieves the download links of non existent objects' do - expect_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).with(oids) + result = subject.execute - subject.execute - end - end - - context 'when all lfs objects are linked' do - before do - allow_any_instance_of(Projects::LfsPointers::LfsLinkService).to receive(:execute).and_return(all_oids.keys) - allow_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute) - end - - it 'retrieves no download links' do - expect_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).with({}).and_call_original - - expect(subject.execute).to be_empty - end - end - - context 'when lfsconfig file exists' do - before do - allow(project.repository).to receive(:lfsconfig_for).and_return("[lfs]\n\turl = #{lfs_endpoint}\n") - end - - context 'when url points to the same import url host' do - let(:lfs_endpoint) { "#{import_url}/different_endpoint" } - let(:service) { double } - - before do - allow(service).to receive(:execute) - end - it 'downloads lfs object using the new endpoint' do - expect(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:new).with(project, remote_uri: remote_uri).and_return(service) - - subject.execute - end - - context 'when import url has credentials' do - let(:import_url) { 'http://user:password@www.gitlab.com/demo/repo.git'} - - it 'adds the credentials to the new endpoint' do - expect(Projects::LfsPointers::LfsDownloadLinkListService) - .to receive(:new).with(project, remote_uri: URI.parse("http://user:password@www.gitlab.com/demo/repo.git/different_endpoint")) - .and_return(service) - - subject.execute - end - - context 'when url has its own credentials' do - let(:lfs_endpoint) { "http://user1:password1@www.gitlab.com/demo/repo.git/different_endpoint" } - - it 'does not add the import url credentials' do - expect(Projects::LfsPointers::LfsDownloadLinkListService) - .to receive(:new).with(project, remote_uri: remote_uri) - .and_return(service) - - subject.execute - end - end - end - end - - context 'when url points to a third party service' do - let(:lfs_endpoint) { 'http://third_party_service.com/info/lfs/objects/' } - - it 'disables lfs from the project' do - expect(project.lfs_enabled?).to be_truthy - - subject.execute - - expect(project.lfs_enabled?).to be_falsey - end - - it 'does not download anything' do - expect_any_instance_of(Projects::LfsPointers::LfsListService).not_to receive(:execute) - - subject.execute - end + expect(result[:status]).to eq :error + expect(result[:message]).to eq error_message end end end - describe '#default_endpoint_uri' do - let(:import_url) { 'http://www.gitlab.com/demo/repo' } + context 'when lfs is not enabled for the project' do + it 'does not download lfs objects' do + allow(project).to receive(:lfs_enabled?).and_return(false) + expect(Projects::LfsPointers::LfsObjectDownloadListService).not_to receive(:new) + expect(Projects::LfsPointers::LfsDownloadService).not_to receive(:new) - it 'adds suffix .git if the url does not have it' do - expect(subject.send(:default_endpoint_uri).path).to match(/repo.git/) + result = subject.execute + + expect(result[:status]).to eq :success end end end diff --git a/spec/services/projects/lfs_pointers/lfs_link_service_spec.rb b/spec/services/projects/lfs_pointers/lfs_link_service_spec.rb index 5caa9de732e..849601c4a63 100644 --- a/spec/services/projects/lfs_pointers/lfs_link_service_spec.rb +++ b/spec/services/projects/lfs_pointers/lfs_link_service_spec.rb @@ -1,5 +1,4 @@ # frozen_string_literal: true - require 'spec_helper' describe Projects::LfsPointers::LfsLinkService do diff --git a/spec/services/projects/lfs_pointers/lfs_object_download_list_service_spec.rb b/spec/services/projects/lfs_pointers/lfs_object_download_list_service_spec.rb new file mode 100644 index 00000000000..9dac29765a2 --- /dev/null +++ b/spec/services/projects/lfs_pointers/lfs_object_download_list_service_spec.rb @@ -0,0 +1,148 @@ +# frozen_string_literal: true +require 'spec_helper' + +describe Projects::LfsPointers::LfsObjectDownloadListService do + let(:import_url) { 'http://www.gitlab.com/demo/repo.git' } + let(:default_endpoint) { "#{import_url}/info/lfs/objects/batch"} + let(:group) { create(:group, lfs_enabled: true)} + let!(:project) { create(:project, namespace: group, import_url: import_url, lfs_enabled: true) } + let!(:lfs_objects_project) { create_list(:lfs_objects_project, 2, project: project) } + let!(:existing_lfs_objects) { LfsObject.pluck(:oid, :size).to_h } + let(:oids) { { 'oid1' => 123, 'oid2' => 125 } } + let(:oid_download_links) { { 'oid1' => "#{import_url}/gitlab-lfs/objects/oid1", 'oid2' => "#{import_url}/gitlab-lfs/objects/oid2" } } + let(:all_oids) { existing_lfs_objects.merge(oids) } + let(:remote_uri) { URI.parse(lfs_endpoint) } + + subject { described_class.new(project) } + + before do + allow(project.repository).to receive(:lfsconfig_for).and_return(nil) + allow(Gitlab.config.lfs).to receive(:enabled).and_return(true) + allow_any_instance_of(Projects::LfsPointers::LfsListService).to receive(:execute).and_return(all_oids) + end + + describe '#execute' do + context 'when no lfs pointer is linked' do + before do + allow_any_instance_of(Projects::LfsPointers::LfsLinkService).to receive(:execute).and_return([]) + allow_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).and_return(oid_download_links) + expect(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:new).with(project, remote_uri: URI.parse(default_endpoint)).and_call_original + end + + it 'retrieves all lfs pointers in the project repository' do + expect_any_instance_of(Projects::LfsPointers::LfsListService).to receive(:execute) + + subject.execute + end + + it 'links existent lfs objects to the project' do + expect_any_instance_of(Projects::LfsPointers::LfsLinkService).to receive(:execute) + + subject.execute + end + + it 'retrieves the download links of non existent objects' do + expect_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).with(all_oids) + + subject.execute + end + end + + context 'when some lfs objects are linked' do + before do + allow_any_instance_of(Projects::LfsPointers::LfsLinkService).to receive(:execute).and_return(existing_lfs_objects.keys) + allow_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).and_return(oid_download_links) + end + + it 'retrieves the download links of non existent objects' do + expect_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).with(oids) + + subject.execute + end + end + + context 'when all lfs objects are linked' do + before do + allow_any_instance_of(Projects::LfsPointers::LfsLinkService).to receive(:execute).and_return(all_oids.keys) + allow_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute) + end + + it 'retrieves no download links' do + expect_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).with({}).and_call_original + + expect(subject.execute).to be_empty + end + end + + context 'when lfsconfig file exists' do + before do + allow(project.repository).to receive(:lfsconfig_for).and_return("[lfs]\n\turl = #{lfs_endpoint}\n") + end + + context 'when url points to the same import url host' do + let(:lfs_endpoint) { "#{import_url}/different_endpoint" } + let(:service) { double } + + before do + allow(service).to receive(:execute) + end + + it 'downloads lfs object using the new endpoint' do + expect(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:new).with(project, remote_uri: remote_uri).and_return(service) + + subject.execute + end + + context 'when import url has credentials' do + let(:import_url) { 'http://user:password@www.gitlab.com/demo/repo.git'} + + it 'adds the credentials to the new endpoint' do + expect(Projects::LfsPointers::LfsDownloadLinkListService) + .to receive(:new).with(project, remote_uri: URI.parse("http://user:password@www.gitlab.com/demo/repo.git/different_endpoint")) + .and_return(service) + + subject.execute + end + + context 'when url has its own credentials' do + let(:lfs_endpoint) { "http://user1:password1@www.gitlab.com/demo/repo.git/different_endpoint" } + + it 'does not add the import url credentials' do + expect(Projects::LfsPointers::LfsDownloadLinkListService) + .to receive(:new).with(project, remote_uri: remote_uri) + .and_return(service) + + subject.execute + end + end + end + end + + context 'when url points to a third party service' do + let(:lfs_endpoint) { 'http://third_party_service.com/info/lfs/objects/' } + + it 'disables lfs from the project' do + expect(project.lfs_enabled?).to be_truthy + + subject.execute + + expect(project.lfs_enabled?).to be_falsey + end + + it 'does not download anything' do + expect_any_instance_of(Projects::LfsPointers::LfsListService).not_to receive(:execute) + + subject.execute + end + end + end + end + + describe '#default_endpoint_uri' do + let(:import_url) { 'http://www.gitlab.com/demo/repo' } + + it 'adds suffix .git if the url does not have it' do + expect(subject.send(:default_endpoint_uri).path).to match(/repo.git/) + end + end +end From c009e23a1693ac365cdbe5024cfd7debcadc0751 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Tue, 30 Apr 2019 10:03:53 +0100 Subject: [PATCH 60/73] Give reviewer roulette its own header in the docs --- doc/development/code_review.md | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/doc/development/code_review.md b/doc/development/code_review.md index 4e2213f7742..c4e5995714d 100644 --- a/doc/development/code_review.md +++ b/doc/development/code_review.md @@ -23,11 +23,6 @@ one of the [Merge request coaches][team]. If you need assistance with security scans or comments, feel free to include the Security Team (`@gitlab-com/gl-security`) in the review. -The `danger-review` CI job will randomly pick a reviewer and a maintainer for -each area of the codebase that your merge request seems to touch. It only makes -recommendations - feel free to override it if you think someone else is a better -fit! - Depending on the areas your merge request touches, it must be **approved** by one or more [maintainers](https://about.gitlab.com/handbook/engineering/workflow/code-review/#maintainer): @@ -37,6 +32,26 @@ widget. Reviewers can add their approval by [approving additionally](https://doc Getting your merge request **merged** also requires a maintainer. If it requires more than one approval, the last maintainer to review and approve it will also merge it. +### Reviewer roulette + +The `danger-review` CI job will randomly pick a reviewer and a maintainer for +each area of the codebase that your merge request seems to touch. It only makes +recommendations - feel free to override it if you think someone else is a better +fit! + +It picks reviewers and maintainers from the list at the +[engineering projects](https://about.gitlab.com/handbook/engineering/projects/) +page, with these behaviours: + +1. It will not pick people whose [GitLab status](../user/profile/#current-status) + contains the string 'OOO'. +2. [Trainee maintainers](https://about.gitlab.com/handbook/engineering/workflow/code-review/#trainee-maintainer) + are three times as likely to be picked as other reviewers. +3. It always picks the same reviewers and maintainers for the same + branch name (unless their OOO status changes, as in point 1). It + removes leading `ce-` and `ee-`, and trailing `-ce` and `-ee`, so + that it can be stable for backport branches. + ### Approval guidelines As described in the section on the responsibility of the maintainer below, you From c5305a02e3db04f4e903d77fc3de4528b75b7131 Mon Sep 17 00:00:00 2001 From: Evan Read Date: Tue, 30 Apr 2019 10:38:46 +0000 Subject: [PATCH 61/73] Docs aren't reviewed using roulette --- lib/gitlab/danger/helper.rb | 8 ++++---- spec/lib/gitlab/danger/helper_spec.rb | 21 ++++++++++----------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/lib/gitlab/danger/helper.rb b/lib/gitlab/danger/helper.rb index d347f3c13a4..68890aa8e30 100644 --- a/lib/gitlab/danger/helper.rb +++ b/lib/gitlab/danger/helper.rb @@ -99,15 +99,15 @@ module Gitlab end CATEGORY_LABELS = { - docs: "~Documentation", + docs: "~Documentation", # Docs are reviewed along DevOps stages, so don't need roulette for now. none: "", qa: "~QA" }.freeze # rubocop:disable Style/RegexpLiteral CATEGORIES = { - %r{\Adoc/} => :docs, - %r{\A(CONTRIBUTING|LICENSE|MAINTENANCE|PHILOSOPHY|PROCESS|README)(\.md)?\z} => :docs, + %r{\Adoc/} => :none, # To reinstate roulette for documentation, set to `:docs`. + %r{\A(CONTRIBUTING|LICENSE|MAINTENANCE|PHILOSOPHY|PROCESS|README)(\.md)?\z} => :none, # To reinstate roulette for documentation, set to `:docs`. %r{\A(ee/)?app/(assets|views)/} => :frontend, %r{\A(ee/)?public/} => :frontend, @@ -148,7 +148,7 @@ module Gitlab # Fallbacks in case the above patterns miss anything %r{\.rb\z} => :backend, - %r{\.(md|txt)\z} => :docs, + %r{\.(md|txt)\z} => :none, # To reinstate roulette for documentation, set to `:docs`. %r{\.js\z} => :frontend }.freeze # rubocop:enable Style/RegexpLiteral diff --git a/spec/lib/gitlab/danger/helper_spec.rb b/spec/lib/gitlab/danger/helper_spec.rb index 66cd8171c12..32b90041c64 100644 --- a/spec/lib/gitlab/danger/helper_spec.rb +++ b/spec/lib/gitlab/danger/helper_spec.rb @@ -191,9 +191,8 @@ describe Gitlab::Danger::Helper do expect(helper.changes_by_category).to eq( backend: %w[foo.rb], database: %w[db/foo], - docs: %w[foo.md], frontend: %w[foo.js], - none: %w[ee/changelogs/foo.yml], + none: %w[ee/changelogs/foo.yml foo.md], qa: %w[qa/foo], unknown: %w[foo] ) @@ -202,13 +201,13 @@ describe Gitlab::Danger::Helper do describe '#category_for_file' do where(:path, :expected_category) do - 'doc/foo' | :docs - 'CONTRIBUTING.md' | :docs - 'LICENSE' | :docs - 'MAINTENANCE.md' | :docs - 'PHILOSOPHY.md' | :docs - 'PROCESS.md' | :docs - 'README.md' | :docs + 'doc/foo' | :none + 'CONTRIBUTING.md' | :none + 'LICENSE' | :none + 'MAINTENANCE.md' | :none + 'PHILOSOPHY.md' | :none + 'PROCESS.md' | :none + 'README.md' | :none 'ee/doc/foo' | :unknown 'ee/README' | :unknown @@ -272,8 +271,8 @@ describe Gitlab::Danger::Helper do 'foo/bar.rb' | :backend 'foo/bar.js' | :frontend - 'foo/bar.txt' | :docs - 'foo/bar.md' | :docs + 'foo/bar.txt' | :none + 'foo/bar.md' | :none end with_them do From ac744fd4fceb996dcafe7958dd35b081331f46fe Mon Sep 17 00:00:00 2001 From: Vladimir Shushlin Date: Tue, 30 Apr 2019 12:05:54 +0000 Subject: [PATCH 62/73] Remove disabled pages domains Domain will be removed by verification worker after 1 week of being disabled --- app/models/pages_domain.rb | 2 + app/workers/all_queues.yml | 1 + .../pages_domain_removal_cron_worker.rb | 16 +++++++ .../remove-disabled-pages-domains-part-2.yml | 5 +++ config/initializers/1_settings.rb | 4 ++ spec/factories/pages_domains.rb | 4 ++ spec/models/pages_domain_spec.rb | 28 +++++++++++++ .../verify_pages_domain_service_spec.rb | 12 +++--- .../pages_domain_removal_cron_worker_spec.rb | 42 +++++++++++++++++++ ...es_domain_verification_cron_worker_spec.rb | 8 ++-- 10 files changed, 112 insertions(+), 10 deletions(-) create mode 100644 app/workers/pages_domain_removal_cron_worker.rb create mode 100644 changelogs/unreleased/remove-disabled-pages-domains-part-2.yml create mode 100644 spec/workers/pages_domain_removal_cron_worker_spec.rb diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb index d73b2889f30..9e806b2e232 100644 --- a/app/models/pages_domain.rb +++ b/app/models/pages_domain.rb @@ -38,6 +38,8 @@ class PagesDomain < ApplicationRecord where(verified_at.eq(nil).or(enabled_until.eq(nil).or(enabled_until.lt(threshold)))) end + scope :for_removal, -> { where("remove_at < ?", Time.now) } + def verified? !!verified_at end diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index 7cf2e7100d5..e4e85de93da 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -6,6 +6,7 @@ - cronjob:gitlab_usage_ping - cronjob:import_export_project_cleanup - cronjob:pages_domain_verification_cron +- cronjob:pages_domain_removal_cron - cronjob:pipeline_schedule - cronjob:prune_old_events - cronjob:remove_expired_group_links diff --git a/app/workers/pages_domain_removal_cron_worker.rb b/app/workers/pages_domain_removal_cron_worker.rb new file mode 100644 index 00000000000..3aca123e5ac --- /dev/null +++ b/app/workers/pages_domain_removal_cron_worker.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class PagesDomainRemovalCronWorker + include ApplicationWorker + include CronjobQueue + + def perform + return unless Feature.enabled?(:remove_disabled_domains) + + PagesDomain.for_removal.find_each do |domain| + domain.destroy! + rescue => e + Raven.capture_exception(e) + end + end +end diff --git a/changelogs/unreleased/remove-disabled-pages-domains-part-2.yml b/changelogs/unreleased/remove-disabled-pages-domains-part-2.yml new file mode 100644 index 00000000000..9b208cbaa0e --- /dev/null +++ b/changelogs/unreleased/remove-disabled-pages-domains-part-2.yml @@ -0,0 +1,5 @@ +--- +title: Remove pages domains if they weren't verified for 1 week +merge_request: 26227 +author: +type: added diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 3c426cdb969..e9b36873d75 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -338,6 +338,10 @@ Settings.cron_jobs['pages_domain_verification_cron_worker'] ||= Settingslogic.ne Settings.cron_jobs['pages_domain_verification_cron_worker']['cron'] ||= '*/15 * * * *' Settings.cron_jobs['pages_domain_verification_cron_worker']['job_class'] = 'PagesDomainVerificationCronWorker' +Settings.cron_jobs['pages_domain_removal_cron_worker'] ||= Settingslogic.new({}) +Settings.cron_jobs['pages_domain_removal_cron_worker']['cron'] ||= '47 0 * * *' +Settings.cron_jobs['pages_domain_removal_cron_worker']['job_class'] = 'PagesDomainRemovalCronWorker' + Settings.cron_jobs['issue_due_scheduler_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['issue_due_scheduler_worker']['cron'] ||= '50 00 * * *' Settings.cron_jobs['issue_due_scheduler_worker']['job_class'] = 'IssueDueSchedulerWorker' diff --git a/spec/factories/pages_domains.rb b/spec/factories/pages_domains.rb index b74f72f2bd3..db8384877b0 100644 --- a/spec/factories/pages_domains.rb +++ b/spec/factories/pages_domains.rb @@ -45,6 +45,10 @@ nNp/xedE1YxutQ== remove_at { 1.day.from_now } end + trait :should_be_removed do + remove_at { 1.day.ago } + end + trait :unverified do verified_at nil end diff --git a/spec/models/pages_domain_spec.rb b/spec/models/pages_domain_spec.rb index 142ddebbbf8..ec4d4517f82 100644 --- a/spec/models/pages_domain_spec.rb +++ b/spec/models/pages_domain_spec.rb @@ -344,4 +344,32 @@ describe PagesDomain do end end end + + describe '.for_removal' do + subject { described_class.for_removal } + + context 'when domain is not schedule for removal' do + let!(:domain) { create :pages_domain } + + it 'does not return domain' do + is_expected.to be_empty + end + end + + context 'when domain is scheduled for removal yesterday' do + let!(:domain) { create :pages_domain, remove_at: 1.day.ago } + + it 'returns domain' do + is_expected.to eq([domain]) + end + end + + context 'when domain is scheduled for removal tomorrow' do + let!(:domain) { create :pages_domain, remove_at: 1.day.from_now } + + it 'does not return domain' do + is_expected.to be_empty + end + end + end end diff --git a/spec/services/verify_pages_domain_service_spec.rb b/spec/services/verify_pages_domain_service_spec.rb index e5c7b5bb9a7..f2b3b44d223 100644 --- a/spec/services/verify_pages_domain_service_spec.rb +++ b/spec/services/verify_pages_domain_service_spec.rb @@ -57,12 +57,12 @@ describe VerifyPagesDomainService do expect(domain).not_to be_verified end - it 'disables domain and shedules it for removal' do - Timecop.freeze do - service.execute - expect(domain).not_to be_enabled - expect(domain.remove_at).to be_within(1.second).of(1.week.from_now) - end + it 'disables domain and shedules it for removal in 1 week' do + service.execute + + expect(domain).not_to be_enabled + + expect(domain.remove_at).to be_like_time(7.days.from_now) end end diff --git a/spec/workers/pages_domain_removal_cron_worker_spec.rb b/spec/workers/pages_domain_removal_cron_worker_spec.rb new file mode 100644 index 00000000000..0e1171e8491 --- /dev/null +++ b/spec/workers/pages_domain_removal_cron_worker_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe PagesDomainRemovalCronWorker do + subject(:worker) { described_class.new } + + describe '#perform' do + context 'when there is domain which should be removed' do + let!(:domain_for_removal) { create(:pages_domain, :should_be_removed) } + + before do + stub_feature_flags(remove_disabled_domains: true) + end + + it 'removes domain' do + expect { worker.perform }.to change { PagesDomain.count }.by(-1) + expect(PagesDomain.exists?).to eq(false) + end + + context 'when domain removal is disabled' do + before do + stub_feature_flags(remove_disabled_domains: false) + end + + it 'does not remove pages domain' do + expect { worker.perform }.not_to change { PagesDomain.count } + expect(PagesDomain.find_by(domain: domain_for_removal.domain)).to be_present + end + end + end + + context 'where there is a domain which scheduled for removal in the future' do + let!(:domain_for_removal) { create(:pages_domain, :scheduled_for_removal) } + + it 'does not remove pages domain' do + expect { worker.perform }.not_to change { PagesDomain.count } + expect(PagesDomain.find_by(domain: domain_for_removal.domain)).to be_present + end + end + end +end diff --git a/spec/workers/pages_domain_verification_cron_worker_spec.rb b/spec/workers/pages_domain_verification_cron_worker_spec.rb index 9b479da1cb6..186824a444f 100644 --- a/spec/workers/pages_domain_verification_cron_worker_spec.rb +++ b/spec/workers/pages_domain_verification_cron_worker_spec.rb @@ -6,11 +6,11 @@ describe PagesDomainVerificationCronWorker do subject(:worker) { described_class.new } describe '#perform' do - it 'enqueues a PagesDomainVerificationWorker for domains needing verification' do - verified = create(:pages_domain) - reverify = create(:pages_domain, :reverify) - disabled = create(:pages_domain, :disabled) + let!(:verified) { create(:pages_domain) } + let!(:reverify) { create(:pages_domain, :reverify) } + let!(:disabled) { create(:pages_domain, :disabled) } + it 'enqueues a PagesDomainVerificationWorker for domains needing verification' do [reverify, disabled].each do |domain| expect(PagesDomainVerificationWorker).to receive(:perform_async).with(domain.id) end From 60525148e7532934913b56586ccdd4c37b74a637 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Tue, 30 Apr 2019 13:31:52 +0100 Subject: [PATCH 63/73] Fix reviewer roulette when no-one is in a category This would return `people.size` before, when it should return `nil`. --- danger/roulette/Dangerfile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/danger/roulette/Dangerfile b/danger/roulette/Dangerfile index 27763052192..62e5526c02b 100644 --- a/danger/roulette/Dangerfile +++ b/danger/roulette/Dangerfile @@ -57,10 +57,12 @@ def spin_for_person(people, random:) people.size.times do person = people.sample(random: random) - return person unless out_of_office?(person) + break person unless out_of_office?(person) people -= [person] end + + person end def out_of_office?(person) From 80f7b412c9b1da3a8a2d759729c30ba66845825e Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Tue, 9 Apr 2019 09:57:08 +0000 Subject: [PATCH 64/73] Clarify that `project_url` is used --- doc/user/project/integrations/custom_issue_tracker.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/user/project/integrations/custom_issue_tracker.md b/doc/user/project/integrations/custom_issue_tracker.md index 6fc083170b6..23f1ce7a15a 100644 --- a/doc/user/project/integrations/custom_issue_tracker.md +++ b/doc/user/project/integrations/custom_issue_tracker.md @@ -7,9 +7,9 @@ in the table below. | Field | Description | | ----- | ----------- | -| `title` | A title for the issue tracker (to differentiate between instances, for example) | +| `title` | A title for the issue tracker (to differentiate between instances, for example). | | `description` | A name for the issue tracker (to differentiate between instances, for example) | -| `project_url` | Currently unused. Will be changed in a future release. | +| `project_url` | The URL to the project in the custom issue tracker. | | `issues_url` | The URL to the issue in the issue tracker project that is linked to this GitLab project. Note that the `issues_url` requires `:id` in the URL. This ID is used by GitLab as a placeholder to replace the issue number. For example, `https://customissuetracker.com/project-name/:id`. | | `new_issue_url` | Currently unused. Will be changed in a future release. | From daa8f784d016091fd2a56fd195cbd6da2f199350 Mon Sep 17 00:00:00 2001 From: Shinya Maeda Date: Wed, 24 Apr 2019 18:37:29 +0700 Subject: [PATCH 65/73] Fix environment automatic on_stop trigger Due to the nature of pipelines for merge requests, deployments.ref can be a merge request ref instead of a branch name. We support the environment auto-stop hook for this case --- app/models/merge_request.rb | 10 +++ app/services/ci/stop_environments_service.rb | 16 ++-- app/services/merge_requests/base_service.rb | 5 ++ app/services/merge_requests/close_service.rb | 1 + .../merge_requests/post_merge_service.rb | 1 + .../fix-environment-on-stop-not-work.yml | 5 ++ spec/models/merge_request_spec.rb | 44 +++++++++++ .../ci/stop_environments_service_spec.rb | 76 +++++++++++++++++++ .../merge_requests/close_service_spec.rb | 8 ++ .../merge_requests/post_merge_service_spec.rb | 8 ++ 10 files changed, 169 insertions(+), 5 deletions(-) create mode 100644 changelogs/unreleased/fix-environment-on-stop-not-work.yml diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index a5b62659b24..c2a1487fc6e 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -1054,6 +1054,16 @@ class MergeRequest < ApplicationRecord @environments[current_user] end + ## + # This method is for looking for active environments which created via pipelines for merge requests. + # Since deployments run on a merge request ref (e.g. `refs/merge-requests/:iid/head`), + # we cannot look up environments with source branch name. + def environments + return Environment.none unless actual_head_pipeline&.triggered_by_merge_request? + + actual_head_pipeline.environments + end + def state_human_name if merged? "Merged" diff --git a/app/services/ci/stop_environments_service.rb b/app/services/ci/stop_environments_service.rb index 973ae5ce5aa..d9a800791f2 100644 --- a/app/services/ci/stop_environments_service.rb +++ b/app/services/ci/stop_environments_service.rb @@ -9,12 +9,11 @@ module Ci return unless @ref.present? - environments.each do |environment| - next unless environment.stop_action_available? - next unless can?(current_user, :stop_environment, environment) + environments.each { |environment| stop(environment) } + end - environment.stop_with_action!(current_user) - end + def execute_for_merge_request(merge_request) + merge_request.environments.each { |environment| stop(environment) } end private @@ -24,5 +23,12 @@ module Ci .new(project, current_user, ref: @ref, recently_updated: true) .execute end + + def stop(environment) + return unless environment.stop_action_available? + return unless can?(current_user, :stop_environment, environment) + + environment.stop_with_action!(current_user) + end end end diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb index b8334a87f6d..a9dd26c02ad 100644 --- a/app/services/merge_requests/base_service.rb +++ b/app/services/merge_requests/base_service.rb @@ -24,6 +24,11 @@ module MergeRequests end end + def cleanup_environments(merge_request) + Ci::StopEnvironmentsService.new(merge_request.source_project, current_user) + .execute_for_merge_request(merge_request) + end + private def handle_wip_event(merge_request) diff --git a/app/services/merge_requests/close_service.rb b/app/services/merge_requests/close_service.rb index 04527bb9713..e77051bb1c9 100644 --- a/app/services/merge_requests/close_service.rb +++ b/app/services/merge_requests/close_service.rb @@ -17,6 +17,7 @@ module MergeRequests execute_hooks(merge_request, 'close') invalidate_cache_counts(merge_request, users: merge_request.assignees) merge_request.update_project_counter_caches + cleanup_environments(merge_request) end merge_request diff --git a/app/services/merge_requests/post_merge_service.rb b/app/services/merge_requests/post_merge_service.rb index f26e3bee06f..c13f7dd5088 100644 --- a/app/services/merge_requests/post_merge_service.rb +++ b/app/services/merge_requests/post_merge_service.rb @@ -18,6 +18,7 @@ module MergeRequests invalidate_cache_counts(merge_request, users: merge_request.assignees) merge_request.update_project_counter_caches delete_non_latest_diffs(merge_request) + cleanup_environments(merge_request) end private diff --git a/changelogs/unreleased/fix-environment-on-stop-not-work.yml b/changelogs/unreleased/fix-environment-on-stop-not-work.yml new file mode 100644 index 00000000000..72e58b26c4d --- /dev/null +++ b/changelogs/unreleased/fix-environment-on-stop-not-work.yml @@ -0,0 +1,5 @@ +--- +title: "`on_stop` is not automatically triggered with pipelines for merge requests" +merge_request: 27618 +author: +type: fixed diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index f61857ea5ff..fb7f43b25cf 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -2262,6 +2262,50 @@ describe MergeRequest do end end + describe "#environments" do + subject { merge_request.environments } + + let(:merge_request) { create(:merge_request, source_branch: 'feature', target_branch: 'master') } + let(:project) { merge_request.project } + + let(:pipeline) do + create(:ci_pipeline, + source: :merge_request_event, + merge_request: merge_request, project: project, + sha: merge_request.diff_head_sha, + merge_requests_as_head_pipeline: [merge_request]) + end + + let!(:job) { create(:ci_build, :start_review_app, pipeline: pipeline, project: project) } + + it 'returns environments' do + is_expected.to eq(pipeline.environments) + expect(subject.count).to be(1) + end + + context 'when pipeline is not associated with environments' do + let!(:job) { create(:ci_build, pipeline: pipeline, project: project) } + + it 'returns empty array' do + is_expected.to be_empty + end + end + + context 'when pipeline is not a pipeline for merge request' do + let(:pipeline) do + create(:ci_pipeline, + project: project, + ref: 'feature', + sha: merge_request.diff_head_sha, + merge_requests_as_head_pipeline: [merge_request]) + end + + it 'returns empty relation' do + is_expected.to be_empty + end + end + end + describe "#reload_diff" do it 'calls MergeRequests::ReloadDiffsService#execute with correct params' do user = create(:user) diff --git a/spec/services/ci/stop_environments_service_spec.rb b/spec/services/ci/stop_environments_service_spec.rb index 31b8d540356..890fa5bc009 100644 --- a/spec/services/ci/stop_environments_service_spec.rb +++ b/spec/services/ci/stop_environments_service_spec.rb @@ -105,6 +105,82 @@ describe Ci::StopEnvironmentsService do end end + describe '#execute_for_merge_request' do + subject { service.execute_for_merge_request(merge_request) } + + let(:merge_request) { create(:merge_request, source_branch: 'feature', target_branch: 'master') } + let(:project) { merge_request.project } + let(:user) { create(:user) } + + let(:pipeline) do + create(:ci_pipeline, + source: :merge_request_event, + merge_request: merge_request, + project: project, + sha: merge_request.diff_head_sha, + merge_requests_as_head_pipeline: [merge_request]) + end + + let!(:review_job) { create(:ci_build, :start_review_app, pipeline: pipeline, project: project) } + let!(:stop_review_job) { create(:ci_build, :stop_review_app, :manual, pipeline: pipeline, project: project) } + + before do + review_job.deployment.success! + end + + it 'has active environment at first' do + expect(pipeline.environments.first).to be_available + end + + context 'when user is a developer' do + before do + project.add_developer(user) + end + + it 'stops the active environment' do + subject + + expect(pipeline.environments.first).to be_stopped + end + end + + context 'when user is a reporter' do + before do + project.add_reporter(user) + end + + it 'does not stop the active environment' do + subject + + expect(pipeline.environments.first).to be_available + end + end + + context 'when pipeline is not associated with environments' do + let!(:job) { create(:ci_build, pipeline: pipeline, project: project) } + + it 'does not raise exception' do + expect { subject }.not_to raise_exception + end + end + + context 'when pipeline is not a pipeline for merge request' do + let(:pipeline) do + create(:ci_pipeline, + project: project, + ref: 'feature', + sha: merge_request.diff_head_sha, + merge_requests_as_head_pipeline: [merge_request]) + end + + it 'does not stop the active environment' do + subject + + expect(pipeline.environments.first).to be_available + end + end + end + def expect_environment_stopped_on(branch) expect_any_instance_of(Environment) .to receive(:stop!) diff --git a/spec/services/merge_requests/close_service_spec.rb b/spec/services/merge_requests/close_service_spec.rb index aa7dfda4950..ffa612cf315 100644 --- a/spec/services/merge_requests/close_service_spec.rb +++ b/spec/services/merge_requests/close_service_spec.rb @@ -74,6 +74,14 @@ describe MergeRequests::CloseService do .to change { project.open_merge_requests_count }.from(1).to(0) end + it 'clean up environments for the merge request' do + expect_next_instance_of(Ci::StopEnvironmentsService) do |service| + expect(service).to receive(:execute_for_merge_request).with(merge_request) + end + + described_class.new(project, user).execute(merge_request) + end + context 'current user is not authorized to close merge request' do before do perform_enqueued_jobs do diff --git a/spec/services/merge_requests/post_merge_service_spec.rb b/spec/services/merge_requests/post_merge_service_spec.rb index 7b87913ab8b..ffc86f68469 100644 --- a/spec/services/merge_requests/post_merge_service_spec.rb +++ b/spec/services/merge_requests/post_merge_service_spec.rb @@ -62,5 +62,13 @@ describe MergeRequests::PostMergeService do expect(merge_request.reload).to be_merged end + + it 'clean up environments for the merge request' do + expect_next_instance_of(Ci::StopEnvironmentsService) do |service| + expect(service).to receive(:execute_for_merge_request).with(merge_request) + end + + described_class.new(project, user).execute(merge_request) + end end end From 5ee7876534891df9c2f5ab6c5112cc47f98b1df5 Mon Sep 17 00:00:00 2001 From: John Cai Date: Wed, 24 Apr 2019 18:50:45 -0700 Subject: [PATCH 66/73] Add client methods for FetchIntoObjectPool RPC Gitaly's FetchIntoObjectPool RPC will idempotently fetch objects into an object pool. If the pool doesn't exist, it will create an empty pool before attempting the fetch. This change adds client code as well as specs to cover this behavior. --- Gemfile | 2 +- Gemfile.lock | 4 +- lib/gitlab/git/object_pool.rb | 4 + .../gitaly_client/object_pool_service.rb | 9 +++ spec/lib/gitlab/git/object_pool_spec.rb | 41 ++++++++++ spec/lib/gitlab/git/repository_spec.rb | 78 +------------------ .../gitaly_client/object_pool_service_spec.rb | 20 +++++ spec/support/helpers/repo_helpers.rb | 78 +++++++++++++++++++ 8 files changed, 156 insertions(+), 80 deletions(-) diff --git a/Gemfile b/Gemfile index 6654b285a72..bbc777fec9e 100644 --- a/Gemfile +++ b/Gemfile @@ -417,7 +417,7 @@ group :ed25519 do end # Gitaly GRPC client -gem 'gitaly-proto', '~> 1.22.0', require: 'gitaly' +gem 'gitaly-proto', '~> 1.26.0', require: 'gitaly' gem 'grpc', '~> 1.19.0' diff --git a/Gemfile.lock b/Gemfile.lock index ceece1da8d7..0ee59793469 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -283,7 +283,7 @@ GEM gettext_i18n_rails (>= 0.7.1) po_to_json (>= 1.0.0) rails (>= 3.2.0) - gitaly-proto (1.22.0) + gitaly-proto (1.26.0) grpc (~> 1.0) github-markup (1.7.0) gitlab-default_value_for (3.1.1) @@ -1056,7 +1056,7 @@ DEPENDENCIES gettext (~> 3.2.2) gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails_js (~> 1.3) - gitaly-proto (~> 1.22.0) + gitaly-proto (~> 1.26.0) github-markup (~> 1.7.0) gitlab-default_value_for (~> 3.1.1) gitlab-labkit (~> 0.1.2) diff --git a/lib/gitlab/git/object_pool.rb b/lib/gitlab/git/object_pool.rb index 8eb3c28ab70..d0577d7a4ff 100644 --- a/lib/gitlab/git/object_pool.rb +++ b/lib/gitlab/git/object_pool.rb @@ -40,6 +40,10 @@ module Gitlab @repository ||= Gitlab::Git::Repository.new(storage, relative_path, GL_REPOSITORY, gl_project_path) end + def fetch + object_pool_service.fetch(source_repository) + end + private def object_pool_service diff --git a/lib/gitlab/gitaly_client/object_pool_service.rb b/lib/gitlab/gitaly_client/object_pool_service.rb index ce1fb4d68ae..d7fac26bc13 100644 --- a/lib/gitlab/gitaly_client/object_pool_service.rb +++ b/lib/gitlab/gitaly_client/object_pool_service.rb @@ -33,6 +33,15 @@ module Gitlab GitalyClient.call(storage, :object_pool_service, :link_repository_to_object_pool, request, timeout: GitalyClient.fast_timeout) end + + def fetch(repository) + request = Gitaly::FetchIntoObjectPoolRequest.new( + object_pool: object_pool, + origin: repository.gitaly_repository + ) + + GitalyClient.call(storage, :object_pool_service, :fetch_into_object_pool, request) + end end end end diff --git a/spec/lib/gitlab/git/object_pool_spec.rb b/spec/lib/gitlab/git/object_pool_spec.rb index 0d5069568e1..6511c2b61bf 100644 --- a/spec/lib/gitlab/git/object_pool_spec.rb +++ b/spec/lib/gitlab/git/object_pool_spec.rb @@ -3,8 +3,12 @@ require 'spec_helper' describe Gitlab::Git::ObjectPool do + include RepoHelpers + let(:pool_repository) { create(:pool_repository) } let(:source_repository) { pool_repository.source_project.repository } + let(:source_repository_path) { File.join(TestEnv.repos_path, source_repository.relative_path) } + let(:source_repository_rugged) { Rugged::Repository.new(source_repository_path) } subject { pool_repository.object_pool } @@ -76,4 +80,41 @@ describe Gitlab::Git::ObjectPool do end end end + + describe '#fetch' do + let(:commit_count) { source_repository.commit_count } + + context "when the object's pool repository exists" do + it 'does not raise an error' do + expect { subject.fetch }.not_to raise_error + end + end + + context "when the object's pool repository does not exist" do + before do + subject.delete + end + + it "re-creates the object pool's repository" do + subject.fetch + + expect(subject.repository.exists?).to be(true) + end + + it 'does not raise an error' do + expect { subject.fetch }.not_to raise_error + end + + it 'fetches objects from the source repository' do + new_commit_id = new_commit_edit_old_file(source_repository_rugged).oid + + expect(subject.repository.exists?).to be false + + subject.fetch + + expect(subject.repository.commit_count('refs/remotes/origin/master')).to eq(commit_count) + expect(subject.repository.commit(new_commit_id).id).to eq(new_commit_id) + end + end + end end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 5f8a2848944..0f6aac9b6de 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -3,6 +3,7 @@ require "spec_helper" describe Gitlab::Git::Repository, :seed_helper do include Gitlab::EncodingHelper + include RepoHelpers using RSpec::Parameterized::TableSyntax shared_examples 'wrapping gRPC errors' do |gitaly_client_class, gitaly_client_method| @@ -2209,83 +2210,6 @@ describe Gitlab::Git::Repository, :seed_helper do repository_rugged.references.create("refs/remotes/#{remote_name}/#{branch_name}", source_branch.dereferenced_target.sha) end - # Build the options hash that's passed to Rugged::Commit#create - def commit_options(repo, index, target, ref, message) - options = {} - options[:tree] = index.write_tree(repo) - options[:author] = { - email: "test@example.com", - name: "Test Author", - time: Time.gm(2014, "mar", 3, 20, 15, 1) - } - options[:committer] = { - email: "test@example.com", - name: "Test Author", - time: Time.gm(2014, "mar", 3, 20, 15, 1) - } - options[:message] ||= message - options[:parents] = repo.empty? ? [] : [target].compact - options[:update_ref] = ref - - options - end - - # Writes a new commit to the repo and returns a Rugged::Commit. Replaces the - # contents of CHANGELOG with a single new line of text. - def new_commit_edit_old_file(repo) - oid = repo.write("I replaced the changelog with this text", :blob) - index = repo.index - index.read_tree(repo.head.target.tree) - index.add(path: "CHANGELOG", oid: oid, mode: 0100644) - - options = commit_options( - repo, - index, - repo.head.target, - "HEAD", - "Edit CHANGELOG in its original location" - ) - - sha = Rugged::Commit.create(repo, options) - repo.lookup(sha) - end - - # Writes a new commit to the repo and returns a Rugged::Commit. Replaces the - # contents of the specified file_path with new text. - def new_commit_edit_new_file(repo, file_path, commit_message, text, branch = repo.head) - oid = repo.write(text, :blob) - index = repo.index - index.read_tree(branch.target.tree) - index.add(path: file_path, oid: oid, mode: 0100644) - options = commit_options(repo, index, branch.target, branch.canonical_name, commit_message) - sha = Rugged::Commit.create(repo, options) - repo.lookup(sha) - end - - # Writes a new commit to the repo and returns a Rugged::Commit. Replaces the - # contents of encoding/CHANGELOG with new text. - def new_commit_edit_new_file_on_branch(repo, file_path, branch_name, commit_message, text) - branch = repo.branches[branch_name] - new_commit_edit_new_file(repo, file_path, commit_message, text, branch) - end - - # Writes a new commit to the repo and returns a Rugged::Commit. Moves the - # CHANGELOG file to the encoding/ directory. - def new_commit_move_file(repo) - blob_oid = repo.head.target.tree.detect { |i| i[:name] == "CHANGELOG" }[:oid] - file_content = repo.lookup(blob_oid).content - oid = repo.write(file_content, :blob) - index = repo.index - index.read_tree(repo.head.target.tree) - index.add(path: "encoding/CHANGELOG", oid: oid, mode: 0100644) - index.remove("CHANGELOG") - - options = commit_options(repo, index, repo.head.target, "HEAD", "Move CHANGELOG to encoding/") - - sha = Rugged::Commit.create(repo, options) - repo.lookup(sha) - end - def refs(dir) IO.popen(%W[git -C #{dir} for-each-ref], &:read).split("\n").map do |line| line.split("\t").last diff --git a/spec/lib/gitlab/gitaly_client/object_pool_service_spec.rb b/spec/lib/gitlab/gitaly_client/object_pool_service_spec.rb index 149b7ec5bb0..0e0c3d329b5 100644 --- a/spec/lib/gitlab/gitaly_client/object_pool_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/object_pool_service_spec.rb @@ -43,4 +43,24 @@ describe Gitlab::GitalyClient::ObjectPoolService do end end end + + describe '#fetch' do + before do + subject.delete + end + + it 'restores the pool repository objects' do + subject.fetch(project.repository) + + expect(object_pool.repository.exists?).to be(true) + end + + context 'when called twice' do + it "doesn't raise an error" do + subject.delete + + expect { subject.fetch(project.repository) }.not_to raise_error + end + end + end end diff --git a/spec/support/helpers/repo_helpers.rb b/spec/support/helpers/repo_helpers.rb index 4af90f4af79..44d95a029af 100644 --- a/spec/support/helpers/repo_helpers.rb +++ b/spec/support/helpers/repo_helpers.rb @@ -11,6 +11,8 @@ module RepoHelpers # blob.path # => 'files/js/commit.js.coffee' # blob.data # => 'class Commit...' # + # Build the options hash that's passed to Rugged::Commit#create + def sample_blob OpenStruct.new( oid: '5f53439ca4b009096571d3c8bc3d09d30e7431b3', @@ -129,4 +131,80 @@ eos file_content: content ).execute end + + def commit_options(repo, index, target, ref, message) + options = {} + options[:tree] = index.write_tree(repo) + options[:author] = { + email: "test@example.com", + name: "Test Author", + time: Time.gm(2014, "mar", 3, 20, 15, 1) + } + options[:committer] = { + email: "test@example.com", + name: "Test Author", + time: Time.gm(2014, "mar", 3, 20, 15, 1) + } + options[:message] ||= message + options[:parents] = repo.empty? ? [] : [target].compact + options[:update_ref] = ref + + options + end + + # Writes a new commit to the repo and returns a Rugged::Commit. Replaces the + # contents of CHANGELOG with a single new line of text. + def new_commit_edit_old_file(repo) + oid = repo.write("I replaced the changelog with this text", :blob) + index = repo.index + index.read_tree(repo.head.target.tree) + index.add(path: "CHANGELOG", oid: oid, mode: 0100644) + + options = commit_options( + repo, + index, + repo.head.target, + "HEAD", + "Edit CHANGELOG in its original location" + ) + + sha = Rugged::Commit.create(repo, options) + repo.lookup(sha) + end + + # Writes a new commit to the repo and returns a Rugged::Commit. Replaces the + # contents of the specified file_path with new text. + def new_commit_edit_new_file(repo, file_path, commit_message, text, branch = repo.head) + oid = repo.write(text, :blob) + index = repo.index + index.read_tree(branch.target.tree) + index.add(path: file_path, oid: oid, mode: 0100644) + options = commit_options(repo, index, branch.target, branch.canonical_name, commit_message) + sha = Rugged::Commit.create(repo, options) + repo.lookup(sha) + end + + # Writes a new commit to the repo and returns a Rugged::Commit. Replaces the + # contents of encoding/CHANGELOG with new text. + def new_commit_edit_new_file_on_branch(repo, file_path, branch_name, commit_message, text) + branch = repo.branches[branch_name] + new_commit_edit_new_file(repo, file_path, commit_message, text, branch) + end + + # Writes a new commit to the repo and returns a Rugged::Commit. Moves the + # CHANGELOG file to the encoding/ directory. + def new_commit_move_file(repo) + blob_oid = repo.head.target.tree.detect { |i| i[:name] == "CHANGELOG" }[:oid] + file_content = repo.lookup(blob_oid).content + oid = repo.write(file_content, :blob) + index = repo.index + index.read_tree(repo.head.target.tree) + index.add(path: "encoding/CHANGELOG", oid: oid, mode: 0100644) + index.remove("CHANGELOG") + + options = commit_options(repo, index, repo.head.target, "HEAD", "Move CHANGELOG to encoding/") + + sha = Rugged::Commit.create(repo, options) + repo.lookup(sha) + end end From aebb2f70257882dd530b820f3cfdd67621d2a3fd Mon Sep 17 00:00:00 2001 From: Roger Meier Date: Sun, 7 Apr 2019 21:21:52 +0200 Subject: [PATCH 67/73] feat: allow Sentry configuration to be passed on gitlab.yml --- app/assets/javascripts/raven/index.js | 7 +++- app/assets/javascripts/raven/raven_config.js | 2 +- .../application_setting_implementation.rb | 16 ++++++++ .../application_settings/_logging.html.haml | 7 ++++ .../unreleased/feat-sentry-environment.yml | 5 +++ config/gitlab.yml.example | 7 ++++ config/initializers/1_settings.rb | 8 ++++ config/initializers/sentry.rb | 1 + lib/gitlab/gon_helper.rb | 7 +++- spec/javascripts/raven/index_spec.js | 10 ++--- spec/javascripts/raven/raven_config_spec.js | 10 ++--- .../application_setting_examples.rb | 37 +++++++++++++++++++ 12 files changed, 103 insertions(+), 14 deletions(-) create mode 100644 changelogs/unreleased/feat-sentry-environment.yml diff --git a/app/assets/javascripts/raven/index.js b/app/assets/javascripts/raven/index.js index edc2293915f..4dd0175e528 100644 --- a/app/assets/javascripts/raven/index.js +++ b/app/assets/javascripts/raven/index.js @@ -4,8 +4,11 @@ const index = function index() { RavenConfig.init({ sentryDsn: gon.sentry_dsn, currentUserId: gon.current_user_id, - whitelistUrls: [gon.gitlab_url], - isProduction: process.env.NODE_ENV, + whitelistUrls: + process.env.NODE_ENV === 'production' + ? [gon.gitlab_url] + : [gon.gitlab_url, 'webpack-internal://'], + environment: gon.sentry_environment, release: gon.revision, tags: { revision: gon.revision, diff --git a/app/assets/javascripts/raven/raven_config.js b/app/assets/javascripts/raven/raven_config.js index 338006ce2b9..b4a8c263954 100644 --- a/app/assets/javascripts/raven/raven_config.js +++ b/app/assets/javascripts/raven/raven_config.js @@ -61,7 +61,7 @@ const RavenConfig = { release: this.options.release, tags: this.options.tags, whitelistUrls: this.options.whitelistUrls, - environment: this.options.isProduction ? 'production' : 'development', + environment: this.options.environment, ignoreErrors: this.IGNORE_ERRORS, ignoreUrls: this.IGNORE_URLS, shouldSendCallback: this.shouldSendSample.bind(this), diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb index b413ffddb9d..557215ff4dc 100644 --- a/app/models/application_setting_implementation.rb +++ b/app/models/application_setting_implementation.rb @@ -183,6 +183,22 @@ module ApplicationSettingImplementation clientside_sentry_dsn.strip! if clientside_sentry_dsn.present? end + def sentry_enabled + Gitlab.config.sentry.enabled || read_attribute(:sentry_enabled) + end + + def sentry_dsn + Gitlab.config.sentry.dsn || read_attribute(:sentry_dsn) + end + + def clientside_sentry_enabled + Gitlab.config.sentry.enabled || read_attribute(:clientside_sentry_enabled) + end + + def clientside_sentry_dsn + Gitlab.config.sentry.dsn || read_attribute(:clientside_sentry_dsn) + end + def performance_bar_allowed_group Group.find_by_id(performance_bar_allowed_group_id) end diff --git a/app/views/admin/application_settings/_logging.html.haml b/app/views/admin/application_settings/_logging.html.haml index 41b787515b5..4145ef94de8 100644 --- a/app/views/admin/application_settings/_logging.html.haml +++ b/app/views/admin/application_settings/_logging.html.haml @@ -1,6 +1,13 @@ = form_for @application_setting, url: admin_application_settings_path(anchor: 'js-logging-settings'), html: { class: 'fieldset-form' } do |f| = form_errors(@application_setting) + %p + %strong + NOTE: + These settings will be removed from the UI in a GitLab 12.0 release and made available within gitlab.yml. + The specific client side DSN setting is already handled as a component from a Sentry perspective anb will be removed. + In addition, you will be able to define a Sentry Environment to differentiate between multiple deployments. For example, development, staging, and production. + %fieldset .form-group .form-check diff --git a/changelogs/unreleased/feat-sentry-environment.yml b/changelogs/unreleased/feat-sentry-environment.yml new file mode 100644 index 00000000000..44ea19375f8 --- /dev/null +++ b/changelogs/unreleased/feat-sentry-environment.yml @@ -0,0 +1,5 @@ +--- +title: Allow Sentry configuration to be passed on gitlab.yml +merge_request: 27091 +author: Roger Meier +type: added diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index bdac5b2a6a1..06530194907 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -315,6 +315,13 @@ production: &base # path: shared/registry # issuer: gitlab-issuer + + ## Error Reporting and Logging with Sentry + sentry: + # enabled: false + # dsn: https://@sentry.io/ + # environment: 'production' # e.g. development, staging, production + # # 2. GitLab CI settings # ========================== diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index e9b36873d75..cddf5bf33f5 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -215,6 +215,14 @@ Settings.registry['issuer'] ||= nil Settings.registry['host_port'] ||= [Settings.registry['host'], Settings.registry['port']].compact.join(':') Settings.registry['path'] = Settings.absolute(Settings.registry['path'] || File.join(Settings.shared['path'], 'registry')) +# +# Error Reporting and Logging with Sentry +# +Settings['sentry'] ||= Settingslogic.new({}) +Settings.sentry['enabled'] ||= false +Settings.sentry['dsn'] ||= nil +Settings.sentry['environment'] ||= nil + # # Pages # diff --git a/config/initializers/sentry.rb b/config/initializers/sentry.rb index 680cfa6f0ed..e5589ce0ad1 100644 --- a/config/initializers/sentry.rb +++ b/config/initializers/sentry.rb @@ -14,6 +14,7 @@ def configure_sentry Raven.configure do |config| config.dsn = Gitlab::CurrentSettings.current_application_settings.sentry_dsn config.release = Gitlab.revision + config.current_environment = Gitlab.config.sentry.environment.presence # Sanitize fields based on those sanitized from Rails. config.sanitize_fields = Rails.application.config.filter_parameters.map(&:to_s) diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb index e00309e7946..582c3065189 100644 --- a/lib/gitlab/gon_helper.rb +++ b/lib/gitlab/gon_helper.rb @@ -15,7 +15,12 @@ module Gitlab gon.relative_url_root = Gitlab.config.gitlab.relative_url_root gon.shortcuts_path = Gitlab::Routing.url_helpers.help_page_path('shortcuts') gon.user_color_scheme = Gitlab::ColorSchemes.for_user(current_user).css_class - gon.sentry_dsn = Gitlab::CurrentSettings.clientside_sentry_dsn if Gitlab::CurrentSettings.clientside_sentry_enabled + + if Gitlab::CurrentSettings.clientside_sentry_enabled + gon.sentry_dsn = Gitlab::CurrentSettings.clientside_sentry_dsn + gon.sentry_environment = Gitlab.config.sentry.environment + end + gon.gitlab_url = Gitlab.config.gitlab.url gon.revision = Gitlab.revision gon.gitlab_logo = ActionController::Base.helpers.asset_path('gitlab_logo.png') diff --git a/spec/javascripts/raven/index_spec.js b/spec/javascripts/raven/index_spec.js index a503a54029f..6b9fe923624 100644 --- a/spec/javascripts/raven/index_spec.js +++ b/spec/javascripts/raven/index_spec.js @@ -5,19 +5,19 @@ describe('RavenConfig options', () => { const sentryDsn = 'sentryDsn'; const currentUserId = 'currentUserId'; const gitlabUrl = 'gitlabUrl'; - const isProduction = 'isProduction'; + const environment = 'test'; const revision = 'revision'; let indexReturnValue; beforeEach(() => { window.gon = { sentry_dsn: sentryDsn, + sentry_environment: environment, current_user_id: currentUserId, gitlab_url: gitlabUrl, revision, }; - process.env.NODE_ENV = isProduction; process.env.HEAD_COMMIT_SHA = revision; spyOn(RavenConfig, 'init'); @@ -25,12 +25,12 @@ describe('RavenConfig options', () => { indexReturnValue = index(); }); - it('should init with .sentryDsn, .currentUserId, .whitelistUrls and .isProduction', () => { + it('should init with .sentryDsn, .currentUserId, .whitelistUrls and environment', () => { expect(RavenConfig.init).toHaveBeenCalledWith({ sentryDsn, currentUserId, - whitelistUrls: [gitlabUrl], - isProduction, + whitelistUrls: [gitlabUrl, 'webpack-internal://'], + environment, release: revision, tags: { revision, diff --git a/spec/javascripts/raven/raven_config_spec.js b/spec/javascripts/raven/raven_config_spec.js index 5cc59cc28d3..af634a0c196 100644 --- a/spec/javascripts/raven/raven_config_spec.js +++ b/spec/javascripts/raven/raven_config_spec.js @@ -69,8 +69,8 @@ describe('RavenConfig', () => { let ravenConfig; const options = { sentryDsn: '//sentryDsn', - whitelistUrls: ['//gitlabUrl'], - isProduction: true, + whitelistUrls: ['//gitlabUrl', 'webpack-internal://'], + environment: 'test', release: 'revision', tags: { revision: 'revision', @@ -95,7 +95,7 @@ describe('RavenConfig', () => { release: options.release, tags: options.tags, whitelistUrls: options.whitelistUrls, - environment: 'production', + environment: 'test', ignoreErrors: ravenConfig.IGNORE_ERRORS, ignoreUrls: ravenConfig.IGNORE_URLS, shouldSendCallback: jasmine.any(Function), @@ -106,8 +106,8 @@ describe('RavenConfig', () => { expect(raven.install).toHaveBeenCalled(); }); - it('should set .environment to development if isProduction is false', () => { - ravenConfig.options.isProduction = false; + it('should set environment from options', () => { + ravenConfig.options.environment = 'development'; RavenConfig.configure.call(ravenConfig); diff --git a/spec/support/shared_examples/application_setting_examples.rb b/spec/support/shared_examples/application_setting_examples.rb index e7ec24c5b7e..d8f7ba1185e 100644 --- a/spec/support/shared_examples/application_setting_examples.rb +++ b/spec/support/shared_examples/application_setting_examples.rb @@ -249,4 +249,41 @@ RSpec.shared_examples 'application settings examples' do expect(setting.password_authentication_enabled_for_web?).to be_falsey end + + describe 'sentry settings' do + context 'when the sentry settings are not set in gitlab.yml' do + it 'fallbacks to the settings in the database' do + setting.sentry_enabled = true + setting.sentry_dsn = 'https://b44a0828b72421a6d8e99efd68d44fa8@example.com/40' + setting.clientside_sentry_enabled = true + setting.clientside_sentry_dsn = 'https://b44a0828b72421a6d8e99efd68d44fa8@example.com/41' + + allow(Gitlab.config.sentry).to receive(:enabled).and_return(false) + allow(Gitlab.config.sentry).to receive(:dsn).and_return(nil) + + expect(setting.sentry_enabled).to eq true + expect(setting.sentry_dsn).to eq 'https://b44a0828b72421a6d8e99efd68d44fa8@example.com/40' + expect(setting.clientside_sentry_enabled).to eq true + expect(setting.clientside_sentry_dsn). to eq 'https://b44a0828b72421a6d8e99efd68d44fa8@example.com/41' + end + end + + context 'when the sentry settings are set in gitlab.yml' do + it 'does not fallback to the settings in the database' do + setting.sentry_enabled = false + setting.sentry_dsn = 'https://b44a0828b72421a6d8e99efd68d44fa8@example.com/40' + setting.clientside_sentry_enabled = false + setting.clientside_sentry_dsn = 'https://b44a0828b72421a6d8e99efd68d44fa8@example.com/41' + + allow(Gitlab.config.sentry).to receive(:enabled).and_return(true) + allow(Gitlab.config.sentry).to receive(:dsn).and_return('https://b44a0828b72421a6d8e99efd68d44fa8@example.com/42') + + expect(setting).not_to receive(:read_attribute) + expect(setting.sentry_enabled).to eq true + expect(setting.sentry_dsn).to eq 'https://b44a0828b72421a6d8e99efd68d44fa8@example.com/42' + expect(setting.clientside_sentry_enabled).to eq true + expect(setting.clientside_sentry_dsn). to eq 'https://b44a0828b72421a6d8e99efd68d44fa8@example.com/42' + end + end + end end From 6a9b6c22ef25bf867e1cddeb7981e19aade8ecec Mon Sep 17 00:00:00 2001 From: Brandon Labuschagne Date: Tue, 30 Apr 2019 17:21:39 +0000 Subject: [PATCH 68/73] Internationalisation of blob directory This is one of many MRs opened in order to improve the overall internationalisation of the GitLab codebase. i18n documentation https://docs.gitlab.com/ee/development/i18n/externalization.html --- .../javascripts/blob/balsamiq_viewer.js | 3 +- .../javascripts/blob/blob_file_dropzone.js | 5 ++-- app/assets/javascripts/blob/sketch/index.js | 5 ++-- .../template_selectors/dockerfile_selector.js | 3 +- app/assets/javascripts/blob/viewer/index.js | 14 +++++---- locale/gitlab.pot | 30 +++++++++++++++++++ 6 files changed, 49 insertions(+), 11 deletions(-) diff --git a/app/assets/javascripts/blob/balsamiq_viewer.js b/app/assets/javascripts/blob/balsamiq_viewer.js index b88e69a07bf..2e537d8c000 100644 --- a/app/assets/javascripts/blob/balsamiq_viewer.js +++ b/app/assets/javascripts/blob/balsamiq_viewer.js @@ -1,8 +1,9 @@ import Flash from '../flash'; import BalsamiqViewer from './balsamiq/balsamiq_viewer'; +import { __ } from '~/locale'; function onError() { - const flash = new Flash('Balsamiq file could not be loaded.'); + const flash = new Flash(__('Balsamiq file could not be loaded.')); return flash; } diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js index cd3251ad1ca..9010cd0c3c1 100644 --- a/app/assets/javascripts/blob/blob_file_dropzone.js +++ b/app/assets/javascripts/blob/blob_file_dropzone.js @@ -5,6 +5,7 @@ import Dropzone from 'dropzone'; import { visitUrl } from '../lib/utils/url_utility'; import { HIDDEN_CLASS } from '../lib/utils/constants'; import csrf from '../lib/utils/csrf'; +import { sprintf, __ } from '~/locale'; Dropzone.autoDiscover = false; @@ -73,7 +74,7 @@ export default class BlobFileDropzone { .html(errorMessage) .text(); $('.dropzone-alerts') - .html(`Error uploading file: "${stripped}"`) + .html(sprintf(__('Error uploading file: %{stripped}'), { stripped })) .show(); this.removeFile(file); }, @@ -84,7 +85,7 @@ export default class BlobFileDropzone { e.stopPropagation(); if (dropzone[0].dropzone.getQueuedFiles().length === 0) { // eslint-disable-next-line no-alert - alert('Please select a file'); + alert(__('Please select a file')); return false; } toggleLoading(submitButton, submitButtonLoadingIcon, true); diff --git a/app/assets/javascripts/blob/sketch/index.js b/app/assets/javascripts/blob/sketch/index.js index 57c1baa9886..dbff03dc734 100644 --- a/app/assets/javascripts/blob/sketch/index.js +++ b/app/assets/javascripts/blob/sketch/index.js @@ -1,5 +1,6 @@ import JSZip from 'jszip'; import JSZipUtils from 'jszip-utils'; +import { __ } from '~/locale'; export default class SketchLoader { constructor(container) { @@ -56,10 +57,10 @@ export default class SketchLoader { const errorMsg = document.createElement('p'); errorMsg.className = 'prepend-top-default append-bottom-default text-center'; - errorMsg.textContent = ` + errorMsg.textContent = __(` Cannot show preview. For previews on sketch files, they must have the file format introduced by Sketch version 43 and above. - `; + `); this.container.appendChild(errorMsg); this.removeLoadingIcon(); diff --git a/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js b/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js index 4718b642617..659d57e6a6f 100644 --- a/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js +++ b/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js @@ -1,11 +1,12 @@ import FileTemplateSelector from '../file_template_selector'; +import { __ } from '~/locale'; export default class DockerfileSelector extends FileTemplateSelector { constructor({ mediator }) { super(mediator); this.config = { key: 'dockerfile', - name: 'Dockerfile', + name: __('Dockerfile'), pattern: /(Dockerfile)/, type: 'dockerfiles', dropdown: '.js-dockerfile-selector', diff --git a/app/assets/javascripts/blob/viewer/index.js b/app/assets/javascripts/blob/viewer/index.js index d0359fc5fe9..d246a1f6064 100644 --- a/app/assets/javascripts/blob/viewer/index.js +++ b/app/assets/javascripts/blob/viewer/index.js @@ -2,6 +2,7 @@ import $ from 'jquery'; import Flash from '../../flash'; import { handleLocationHash } from '../../lib/utils/common_utils'; import axios from '../../lib/utils/axios_utils'; +import { __ } from '~/locale'; export default class BlobViewer { constructor() { @@ -26,7 +27,7 @@ export default class BlobViewer { promise .then(module => module.default(viewer)) .catch(error => { - Flash('Error loading file viewer.'); + Flash(__('Error loading file viewer.')); throw error; }); @@ -106,16 +107,19 @@ export default class BlobViewer { if (!this.copySourceBtn) return; if (this.simpleViewer.getAttribute('data-loaded')) { - this.copySourceBtn.setAttribute('title', 'Copy source to clipboard'); + this.copySourceBtn.setAttribute('title', __('Copy source to clipboard')); this.copySourceBtn.classList.remove('disabled'); } else if (this.activeViewer === this.simpleViewer) { this.copySourceBtn.setAttribute( 'title', - 'Wait for the source to load to copy it to the clipboard', + __('Wait for the source to load to copy it to the clipboard'), ); this.copySourceBtn.classList.add('disabled'); } else { - this.copySourceBtn.setAttribute('title', 'Switch to the source to copy it to the clipboard'); + this.copySourceBtn.setAttribute( + 'title', + __('Switch to the source to copy it to the clipboard'), + ); this.copySourceBtn.classList.add('disabled'); } @@ -158,7 +162,7 @@ export default class BlobViewer { this.toggleCopyButtonState(); }) - .catch(() => new Flash('Error loading viewer')); + .catch(() => new Flash(__('Error loading viewer'))); } static loadViewer(viewerParam) { diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 3d56efa9834..7ce605af062 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -1296,6 +1296,9 @@ msgstr "" msgid "Badges|e.g. %{exampleUrl}" msgstr "" +msgid "Balsamiq file could not be loaded." +msgstr "" + msgid "BambooService|A continuous integration and build server" msgstr "" @@ -1614,6 +1617,9 @@ msgstr "" msgid "Cannot render the image. Maximum character count (%{charLimit}) has been exceeded." msgstr "" +msgid "Cannot show preview. For previews on sketch files, they must have the file format introduced by Sketch version 43 and above." +msgstr "" + msgid "Cannot skip two factor authentication setup" msgstr "" @@ -2714,6 +2720,9 @@ msgstr "" msgid "Copy secret to clipboard" msgstr "" +msgid "Copy source to clipboard" +msgstr "" + msgid "Copy to clipboard" msgstr "" @@ -3292,6 +3301,9 @@ msgstr "" msgid "Do you want to customize how Google Code email addresses and usernames are imported into GitLab?" msgstr "" +msgid "Dockerfile" +msgstr "" + msgid "Domain" msgstr "" @@ -3709,6 +3721,9 @@ msgstr "" msgid "Error loading branches." msgstr "" +msgid "Error loading file viewer." +msgstr "" + msgid "Error loading last commit." msgstr "" @@ -3727,6 +3742,9 @@ msgstr "" msgid "Error loading template." msgstr "" +msgid "Error loading viewer" +msgstr "" + msgid "Error occurred when toggling the notification subscription" msgstr "" @@ -3760,6 +3778,9 @@ msgstr "" msgid "Error uploading file" msgstr "" +msgid "Error uploading file: %{stripped}" +msgstr "" + msgid "Error while loading the merge request. Please try again." msgstr "" @@ -6658,6 +6679,9 @@ msgstr "" msgid "Please note that this application is not provided by GitLab and you should verify its authenticity before allowing access." msgstr "" +msgid "Please select a file" +msgstr "" + msgid "Please select a group." msgstr "" @@ -8725,6 +8749,9 @@ msgstr "" msgid "Switch to GitLab Next" msgstr "" +msgid "Switch to the source to copy it to the clipboard" +msgstr "" + msgid "System Hooks" msgstr "" @@ -10240,6 +10267,9 @@ msgstr "" msgid "VisibilityLevel|Unknown" msgstr "" +msgid "Wait for the source to load to copy it to the clipboard" +msgstr "" + msgid "Want to see the data? Please ask an administrator for access." msgstr "" From 63454eb8c0feecd913959267ed56f15455a0d25d Mon Sep 17 00:00:00 2001 From: Brandon Labuschagne Date: Tue, 30 Apr 2019 17:26:58 +0000 Subject: [PATCH 69/73] Internationalisation of projects directory This is one of many MRs opened in order to improve the overall internationalisation of the GitLab codebase. i18n documentation https://docs.gitlab.com/ee/development/i18n/externalization.html --- .../javascripts/projects/project_new.js | 35 ++++++------- locale/gitlab.pot | 51 +++++++++++++++++++ 2 files changed, 69 insertions(+), 17 deletions(-) diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js index 2164e386fdb..ea82ff4e340 100644 --- a/app/assets/javascripts/projects/project_new.js +++ b/app/assets/javascripts/projects/project_new.js @@ -1,6 +1,7 @@ import $ from 'jquery'; import { addSelectOnFocusBehaviour } from '../lib/utils/common_utils'; import { slugifyWithHyphens } from '../lib/utils/text_utility'; +import { s__ } from '~/locale'; let hasUserDefinedProjectPath = false; @@ -114,71 +115,71 @@ const bindEvents = () => { const value = $(this).val(); const templates = { rails: { - text: 'Ruby on Rails', + text: s__('ProjectTemplates|Ruby on Rails'), icon: '.template-option .icon-rails', }, express: { - text: 'NodeJS Express', + text: s__('ProjectTemplates|NodeJS Express'), icon: '.template-option .icon-express', }, spring: { - text: 'Spring', + text: s__('ProjectTemplates|Spring'), icon: '.template-option .icon-spring', }, iosswift: { - text: 'iOS (Swift)', + text: s__('ProjectTemplates|iOS (Swift)'), icon: '.template-option svg.icon-gitlab', }, dotnetcore: { - text: '.NET Core', + text: s__('ProjectTemplates|.NET Core'), icon: '.template-option .icon-dotnet', }, android: { - text: 'Android', + text: s__('ProjectTemplates|Android'), icon: '.template-option svg.icon-android', }, gomicro: { - text: 'Go Micro', + text: s__('ProjectTemplates|Go Micro'), icon: '.template-option .icon-gomicro', }, hugo: { - text: 'Pages/Hugo', + text: s__('ProjectTemplates|Pages/Hugo'), icon: '.template-option .icon-hugo', }, jekyll: { - text: 'Pages/Jekyll', + text: s__('ProjectTemplates|Pages/Jekyll'), icon: '.template-option .icon-jekyll', }, plainhtml: { - text: 'Pages/Plain HTML', + text: s__('ProjectTemplates|Pages/Plain HTML'), icon: '.template-option .icon-plainhtml', }, gitbook: { - text: 'Pages/GitBook', + text: s__('ProjectTemplates|Pages/GitBook'), icon: '.template-option .icon-gitbook', }, hexo: { - text: 'Pages/Hexo', + text: s__('ProjectTemplates|Pages/Hexo'), icon: '.template-option .icon-hexo', }, nfhugo: { - text: 'Netlify/Hugo', + text: s__('ProjectTemplates|Netlify/Hugo'), icon: '.template-option .icon-netlify', }, nfjekyll: { - text: 'Netlify/Jekyll', + text: s__('ProjectTemplates|Netlify/Jekyll'), icon: '.template-option .icon-netlify', }, nfplainhtml: { - text: 'Netlify/Plain HTML', + text: s__('ProjectTemplates|Netlify/Plain HTML'), icon: '.template-option .icon-netlify', }, nfgitbook: { - text: 'Netlify/GitBook', + text: s__('ProjectTemplates|Netlify/GitBook'), icon: '.template-option .icon-netlify', }, nfhexo: { - text: 'Netlify/Hexo', + text: s__('ProjectTemplates|Netlify/Hexo'), icon: '.template-option .icon-netlify', }, }; diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 355baa9e262..57f89acf52f 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -7228,6 +7228,57 @@ msgstr "" msgid "ProjectSettings|When conflicts arise the user is given the option to rebase" msgstr "" +msgid "ProjectTemplates|.NET Core" +msgstr "" + +msgid "ProjectTemplates|Android" +msgstr "" + +msgid "ProjectTemplates|Go Micro" +msgstr "" + +msgid "ProjectTemplates|Netlify/GitBook" +msgstr "" + +msgid "ProjectTemplates|Netlify/Hexo" +msgstr "" + +msgid "ProjectTemplates|Netlify/Hugo" +msgstr "" + +msgid "ProjectTemplates|Netlify/Jekyll" +msgstr "" + +msgid "ProjectTemplates|Netlify/Plain HTML" +msgstr "" + +msgid "ProjectTemplates|NodeJS Express" +msgstr "" + +msgid "ProjectTemplates|Pages/GitBook" +msgstr "" + +msgid "ProjectTemplates|Pages/Hexo" +msgstr "" + +msgid "ProjectTemplates|Pages/Hugo" +msgstr "" + +msgid "ProjectTemplates|Pages/Jekyll" +msgstr "" + +msgid "ProjectTemplates|Pages/Plain HTML" +msgstr "" + +msgid "ProjectTemplates|Ruby on Rails" +msgstr "" + +msgid "ProjectTemplates|Spring" +msgstr "" + +msgid "ProjectTemplates|iOS (Swift)" +msgstr "" + msgid "Projects" msgstr "" From 656d51d8300db2ea315f61146c52245e53a5068d Mon Sep 17 00:00:00 2001 From: Russell Dickenson Date: Tue, 30 Apr 2019 17:52:06 +0000 Subject: [PATCH 70/73] Added content on the Admin Area's `Projects` page Part of issue 57874 --- doc/user/admin_area/index.md | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/doc/user/admin_area/index.md b/doc/user/admin_area/index.md index 5e67cb0ef16..f924ff8dfde 100644 --- a/doc/user/admin_area/index.md +++ b/doc/user/admin_area/index.md @@ -16,7 +16,7 @@ The Admin Area is made up of the following sections: | Section | Description | |:------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------| -| Overview | View your GitLab [Dashboard](#admin-dashboard), and maintain projects, users, groups, jobs, runners, and Gitaly servers. | +| Overview | View your GitLab [Dashboard](#admin-dashboard), and administer [projects](#administer-projects), users, groups, jobs, runners, and Gitaly servers. | | Monitoring | View GitLab system information, and information on background jobs, logs, [health checks](monitoring/health_check.md), request profiles, and audit logs. | | Messages | Send and manage [broadcast messages](broadcast_messages.md) for your users. | | System Hooks | Configure [system hooks](../../system_hooks/system_hooks.md) for many events. | @@ -46,4 +46,28 @@ The Dashboard is the default view of the Admin Area, and is made up of the follo | Groups | The total number of groups, up to 10 of the latest groups, and the option of creating a new group. | | Statistics | Totals of all elements of the GitLab instance. | | Features | All features available on the GitLab instance. Enabled features are marked with a green circle icon, and disabled features are marked with a power icon. | -| Components | The major components of GitLab and the version number of each. A link to the Gitaly Servers is also included. | \ No newline at end of file +| Components | The major components of GitLab and the version number of each. A link to the Gitaly Servers is also included. | + +## Administer Projects + +You can administer all projects in the GitLab instance from the Admin Area's Projects page. + +To access the Projects page, go to **Admin Area > Overview > Projects**. + +Click the **All**, **Private**, **Internal**, or **Public** tab to list only projects of that +criteria. + +By default, all projects are listed, in reverse order of when they were last updated. For each +project, the name, namespace, description, and size is listed, also options to **Edit** or +**Delete** it. + +Sort projects by **Name**, **Last created**, **Oldest created**, **Last updated**, **Oldest +updated**, **Owner**, and choose to hide or show archived projects. + +In the **Filter by name** field, type the project name you want to find, and GitLab will filter +them as you type. + +Select from the **Namespace** dropdown to filter only projects in that namespace. + +You can combine the filter options. For example, click the **Public** tab, and enter `score` in +the **Filter by name...** input box to list only public projects with `score` in their name. \ No newline at end of file From 4a413f250ecaf44c800c65c88955cc55d1d1679d Mon Sep 17 00:00:00 2001 From: Evan Read Date: Tue, 30 Apr 2019 18:04:19 +0000 Subject: [PATCH 71/73] MVC improvements to basics landing page --- doc/README.md | 16 +++++++------- doc/gitlab-basics/README.md | 42 +++++++++++++++++++++++++------------ 2 files changed, 37 insertions(+), 21 deletions(-) diff --git a/doc/README.md b/doc/README.md index 14a1eeffda0..dd4909ce303 100644 --- a/doc/README.md +++ b/doc/README.md @@ -218,12 +218,12 @@ scales to run your tests faster. The following documentation relates to the DevOps **Verify** stage: -| Verify Topics | Description | -|:---------------------------------------------------|:-----------------------------------------------------------------------------| -| [GitLab CI/CD](ci/README.md) | Explore the features and capabilities of Continuous Integration with GitLab. | -| [JUnit test reports](ci/junit_test_reports.md) | Display JUnit test reports on merge requests. | -| [Pipeline Graphs](ci/pipelines.md#visualizing-pipelines) | Visualize builds. | -| [Review Apps](ci/review_apps/index.md) | Preview changes to your application right from a merge request. | +| Verify Topics | Description | +|:---------------------------------------------------------|:-----------------------------------------------------------------------------| +| [GitLab CI/CD](ci/README.md) | Explore the features and capabilities of Continuous Integration with GitLab. | +| [JUnit test reports](ci/junit_test_reports.md) | Display JUnit test reports on merge requests. | +| [Pipeline Graphs](ci/pipelines.md#visualizing-pipelines) | Visualize builds. | +| [Review Apps](ci/review_apps/index.md) | Preview changes to your application right from a merge request. |

@@ -288,7 +288,7 @@ The following documentation relates to the DevOps **Configure** stage: | [GitLab ChatOps](ci/chatops/README.md) | Interact with CI/CD jobs through chat services. | | [Installing Applications](user/project/clusters/index.md#installing-applications) | Deploy Helm, Ingress, and Prometheus on Kubernetes. | | [Mattermost slash commands](user/project/integrations/mattermost_slash_commands.md) | Enable and use slash commands from within Mattermost. | -| [Protected variables](ci/variables/README.md#protected-environment-variables) | Restrict variables to protected branches and tags. | +| [Protected variables](ci/variables/README.md#protected-environment-variables) | Restrict variables to protected branches and tags. | | [Serverless](user/project/clusters/serverless/index.md) | Run serverless workloads on Kubernetes. | | [Slack slash commands](user/project/integrations/slack_slash_commands.md) | Enable and use slash commands from within Slack. | @@ -418,7 +418,7 @@ We have the following documentation to rapidly uplift your GitLab knowledge: | Topic | Description | |:-----------------------------------------------------------------------------------------------------------------------|:---------------------------------------------------------------| -| [GitLab Basics](gitlab-basics/README.md) | Start working on the command line and with GitLab. | +| [GitLab basics guides](gitlab-basics/README.md) | Start working on the command line and with GitLab. | | [GitLab Workflow](workflow/README.md) and [overview](https://about.gitlab.com/2016/10/25/gitlab-workflow-an-overview/) | Enhance your workflow with the best of GitLab Workflow. | | [Get started with GitLab CI/CD](ci/quick_start/README.md) | Quickly implement GitLab CI/CD. | | [Auto DevOps](topics/autodevops/index.md) | Learn more about GitLab's Auto DevOps. | diff --git a/doc/gitlab-basics/README.md b/doc/gitlab-basics/README.md index 4e15f7cfd49..aa008d6f768 100644 --- a/doc/gitlab-basics/README.md +++ b/doc/gitlab-basics/README.md @@ -2,18 +2,34 @@ comments: false --- -# GitLab basics +# GitLab basics guides -Step-by-step guides on the basics of working with Git and GitLab. +This section provides resources to help you start with GitLab by focusing on basic functionality. -- [Command line basics](command-line-commands.md) -- [Start using Git on the command line](start-using-git.md) -- [Create and add your SSH Keys](create-your-ssh-keys.md) -- [Create a project](create-project.md) -- [Create a group](../user/group/index.md#create-a-new-group) -- [Create a branch](create-branch.md) -- [Fork a project](fork-project.md) -- [Add a file](add-file.md) -- [Add an image](add-image.md) -- [Create an issue](../user/project/issues/create_new_issue.md) -- [Create a merge request](add-merge-request.md) +This documentation is split into the following groups: + +- [GitLab-specific functionality](#gitlab-basics), for basic GitLab features. +- [General Git functionality](#git-basics), for working with Git in conjunction with GitLab. + +## GitLab basics + +The following are guides to basic GitLab functionality: + +- [Create and add your SSH Keys](create-your-ssh-keys.md), for enabling Git over SSH. +- [Create a project](create-project.md), to start using GitLab. +- [Create a group](../user/group/index.md#create-a-new-group), to combine and administer projects together. +- [Create a branch](create-branch.md), to make changes to files stored in a project's repository. +- [Fork a project](fork-project.md), to duplicate projects so they can be worked on in parallel. +- [Add a file](add-file.md), to add new files to a project's repository. +- [Add an image](add-image.md), to add new images to a project's repository. +- [Create an issue](../user/project/issues/create_new_issue.md), to start collaborating within a project. +- [Create a merge request](add-merge-request.md), to request changes made in a branch be merged into a project's repository. + +## Git basics + +If you're unfamiliar with the command line, these resources will help: + +- [Command line basics](command-line-commands.md), for those unfamiliar with the command line interface. +- [Start using Git on the command line](start-using-git.md), for some simple Git commands. + +More Git resources are available at GitLab's [Git documentation](../topics/git/index.md). From 099d3e111fe4c7eb4bc6353edd888d68e95099bc Mon Sep 17 00:00:00 2001 From: Brandon Labuschagne Date: Tue, 30 Apr 2019 19:12:25 +0000 Subject: [PATCH 72/73] Internationalisation of u2f directory This is one of many MRs opened in order to improve the overall internationalisation of the GitLab codebase. i18n documentation https://docs.gitlab.com/ee/development/i18n/externalization.html --- app/assets/javascripts/u2f/error.js | 12 ++++++++---- locale/gitlab.pot | 12 ++++++++++++ 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/u2f/error.js b/app/assets/javascripts/u2f/error.js index 1a98564ff55..ca0fc0700ad 100644 --- a/app/assets/javascripts/u2f/error.js +++ b/app/assets/javascripts/u2f/error.js @@ -1,3 +1,5 @@ +import { __ } from '~/locale'; + export default class U2FError { constructor(errorCode, u2fFlowType) { this.errorCode = errorCode; @@ -8,15 +10,17 @@ export default class U2FError { message() { if (this.errorCode === window.u2f.ErrorCodes.BAD_REQUEST && this.httpsDisabled) { - return 'U2F only works with HTTPS-enabled websites. Contact your administrator for more details.'; + return __( + 'U2F only works with HTTPS-enabled websites. Contact your administrator for more details.', + ); } else if (this.errorCode === window.u2f.ErrorCodes.DEVICE_INELIGIBLE) { if (this.u2fFlowType === 'authenticate') { - return 'This device has not been registered with us.'; + return __('This device has not been registered with us.'); } if (this.u2fFlowType === 'register') { - return 'This device has already been registered with us.'; + return __('This device has already been registered with us.'); } } - return 'There was a problem communicating with your device.'; + return __('There was a problem communicating with your device.'); } } diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 57f89acf52f..7b5770a6075 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -9231,6 +9231,9 @@ msgstr "" msgid "There is already a repository with that name on disk" msgstr "" +msgid "There was a problem communicating with your device." +msgstr "" + msgid "There was an error loading users activity calendar." msgstr "" @@ -9309,6 +9312,12 @@ msgstr "" msgid "This container registry has been scheduled for deletion." msgstr "" +msgid "This device has already been registered with us." +msgstr "" + +msgid "This device has not been registered with us." +msgstr "" + msgid "This diff is collapsed." msgstr "" @@ -9895,6 +9904,9 @@ msgstr "" msgid "Type" msgstr "" +msgid "U2F only works with HTTPS-enabled websites. Contact your administrator for more details." +msgstr "" + msgid "Unable to connect to server: %{error}" msgstr "" From fb4c375b031c5a2e628710d9b19891fa2d1b3827 Mon Sep 17 00:00:00 2001 From: Roger Meier Date: Tue, 30 Apr 2019 20:15:17 +0000 Subject: [PATCH 73/73] style: fix typo --- app/views/admin/application_settings/_logging.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/admin/application_settings/_logging.html.haml b/app/views/admin/application_settings/_logging.html.haml index 4145ef94de8..006ebf09576 100644 --- a/app/views/admin/application_settings/_logging.html.haml +++ b/app/views/admin/application_settings/_logging.html.haml @@ -5,7 +5,7 @@ %strong NOTE: These settings will be removed from the UI in a GitLab 12.0 release and made available within gitlab.yml. - The specific client side DSN setting is already handled as a component from a Sentry perspective anb will be removed. + The specific client side DSN setting is already handled as a component from a Sentry perspective and will be removed. In addition, you will be able to define a Sentry Environment to differentiate between multiple deployments. For example, development, staging, and production. %fieldset