Commit b5cc8c42 authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Merge branch '5491-license-management-at-pipeline-ee' into 'master'

Show License Management at pipeline level

Closes #5491

See merge request gitlab-org/gitlab-ee!6688
parents 5678460d 011fd86e
......@@ -7,10 +7,6 @@ import pipelineGraph from './components/graph/graph_component.vue';
import pipelineHeader from './components/header_component.vue';
import eventHub from './event_hub';
import SecurityReportApp from 'ee/vue_shared/security_reports/split_security_reports_app.vue'; // eslint-disable-line import/first
import SastSummaryWidget from 'ee/pipelines/components/security_reports/report_summary_widget.vue'; // eslint-disable-line import/first
import createStore from 'ee/vue_shared/security_reports/store'; // eslint-disable-line import/first
Vue.use(Translate);
export default () => {
......@@ -87,103 +83,4 @@ export default () => {
});
},
});
/**
* EE only
*/
const securityTab = document.getElementById('js-security-report-app');
const sastSummary = document.querySelector('.js-sast-summary');
const updateBadgeCount = count => {
const badge = document.querySelector('.js-sast-counter');
if (badge.textContent !== '') {
badge.textContent = parseInt(badge.textContent, 10) + count;
} else {
badge.textContent = count;
}
badge.classList.remove('hidden');
};
// They are being rendered under the same condition
if (securityTab && sastSummary) {
const datasetOptions = securityTab.dataset;
const {
endpoint,
blobPath,
sastHelpPath,
dependencyScanningEndpoint,
dependencyScanningHelpPath,
vulnerabilityFeedbackPath,
vulnerabilityFeedbackHelpPath,
dastEndpoint,
sastContainerEndpoint,
dastHelpPath,
sastContainerHelpPath,
} = datasetOptions;
const pipelineId = parseInt(datasetOptions.pipelineId, 10);
const { canCreateIssue, canCreateFeedback } = datasetOptions;
const store = createStore();
// Widget summary
// eslint-disable-next-line no-new
new Vue({
el: sastSummary,
store,
components: {
SastSummaryWidget,
},
methods: {
updateBadge(count) {
updateBadgeCount(count);
},
},
render(createElement) {
return createElement('sast-summary-widget', {
on: {
updateBadgeCount: this.updateBadge,
},
});
},
});
// Tab content
// eslint-disable-next-line no-new
new Vue({
el: securityTab,
store,
components: {
SecurityReportApp,
},
methods: {
updateBadge(count) {
updateBadgeCount(count);
},
},
render(createElement) {
return createElement('security-report-app', {
props: {
headBlobPath: blobPath,
sastHeadPath: endpoint,
sastHelpPath,
dependencyScanningHeadPath: dependencyScanningEndpoint,
dependencyScanningHelpPath,
vulnerabilityFeedbackPath,
vulnerabilityFeedbackHelpPath,
pipelineId,
dastHeadPath: dastEndpoint,
sastContainerHeadPath: sastContainerEndpoint,
dastHelpPath,
sastContainerHelpPath,
canCreateFeedback,
canCreateIssue,
},
on: {
updateBadgeCount: this.updateBadge,
},
});
},
});
}
};
......@@ -232,6 +232,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
get :failures
get :status
get :security
get :licenses
end
end
......
// /builds is an alias for show
import '../show/index';
// /failures is an alias for show
import '../show/index';
// /licenses is an alias for show
import '../show/index';
import initPipelineDetails from '~/pipelines/pipeline_details_bundle';
import initPipelines from '~/pages/projects/pipelines/init_pipelines';
document.addEventListener('DOMContentLoaded', () => {
initPipelines();
initPipelineDetails();
});
// /security is an alias for show
import '../show/index';
import initPipelineDetails from '~/pipelines/pipeline_details_bundle';
import initPipelines from '~/pages/projects/pipelines/init_pipelines';
import initSecurityReport from './security_report';
import initLicenseReport from './license_report';
document.addEventListener('DOMContentLoaded', () => {
initPipelines();
initPipelineDetails();
initSecurityReport();
initLicenseReport();
});
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
import LicenseReportApp from 'ee/vue_shared/license_management/mr_widget_license_report.vue';
import { convertPermissionToBoolean } from '~/lib/utils/common_utils';
import { updateBadgeCount } from './utils';
Vue.use(Translate);
export default () => {
const licensesTab = document.getElementById('js-licenses-app');
if (licensesTab) {
const { licenseHeadPath, canManageLicenses, apiUrl } = licensesTab.dataset;
// eslint-disable-next-line no-new
new Vue({
el: licensesTab,
components: {
LicenseReportApp,
},
render(createElement) {
return createElement('license-report-app', {
props: {
apiUrl,
headPath: licenseHeadPath,
canManageLicenses: convertPermissionToBoolean(canManageLicenses),
alwaysOpen: true,
reportSectionClass: 'split-report-section',
},
on: {
updateBadgeCount: (count) => {
updateBadgeCount('.js-licenses-counter', count);
},
},
});
},
});
}
};
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
import SecurityReportApp from 'ee/vue_shared/security_reports/split_security_reports_app.vue';
import SastSummaryWidget from 'ee/pipelines/components/security_reports/report_summary_widget.vue';
import createStore from 'ee/vue_shared/security_reports/store';
import { convertPermissionToBoolean } from '~/lib/utils/common_utils';
import { updateBadgeCount } from './utils';
Vue.use(Translate);
export default () => {
const securityTab = document.getElementById('js-security-report-app');
const sastSummary = document.querySelector('.js-sast-summary');
// They are being rendered under the same condition
if (securityTab) {
const datasetOptions = securityTab.dataset;
const {
headBlobPath,
sastHeadPath,
sastHelpPath,
dependencyScanningHeadPath,
dependencyScanningHelpPath,
vulnerabilityFeedbackPath,
vulnerabilityFeedbackHelpPath,
dastHeadPath,
sastContainerHeadPath,
dastHelpPath,
sastContainerHelpPath,
canCreateIssue,
canCreateFeedback,
} = datasetOptions;
const pipelineId = parseInt(datasetOptions.pipelineId, 10);
const store = createStore();
// Widget summary
if (sastSummary) {
// eslint-disable-next-line no-new
new Vue({
el: sastSummary,
store,
components: {
SastSummaryWidget,
},
render(createElement) {
return createElement('sast-summary-widget');
},
});
}
// Tab content
// eslint-disable-next-line no-new
new Vue({
el: securityTab,
store,
components: {
SecurityReportApp,
},
render(createElement) {
return createElement('security-report-app', {
props: {
headBlobPath,
sastHeadPath,
sastHelpPath,
dependencyScanningHeadPath,
dependencyScanningHelpPath,
vulnerabilityFeedbackPath,
vulnerabilityFeedbackHelpPath,
pipelineId,
dastHeadPath,
sastContainerHeadPath,
dastHelpPath,
sastContainerHelpPath,
canCreateFeedback: convertPermissionToBoolean(canCreateFeedback),
canCreateIssue: convertPermissionToBoolean(canCreateIssue),
},
on: {
updateBadgeCount: (count) => {
updateBadgeCount('.js-licenses-counter', count);
},
},
});
},
});
}
};
/* eslint-disable import/prefer-default-export */
/**
*
* Sets the text content of a DOM element to a given value.
* If the given value is an integer,
* the text content will set to the value and will be shown.
* If the given value is not an integer,
* the text content will be emptied and the element will be hidden.
*
* The visibility of the element is based on the helper class `.hidden`
*
* @param selector {String} selector of the DOM element
* @param count {Number=} value for the DOM element
*/
export const updateBadgeCount = (selector, count) => {
const badge = document.querySelector(selector);
if (Number.isInteger(count)) {
badge.textContent = `${count}`;
badge.classList.remove('hidden');
} else {
badge.classList.add('hidden');
badge.textContent = '';
}
};
......@@ -36,6 +36,16 @@ export default {
type: Boolean,
required: true,
},
reportSectionClass: {
type: String,
required: false,
default: '',
},
alwaysOpen: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
...mapState(['loadLicenseReportError']),
......@@ -48,6 +58,14 @@ export default {
return this.checkReportStatus(this.isLoading, this.loadLicenseReportError);
},
},
watch: {
licenseReport() {
this.$emit(
'updateBadgeCount',
this.licenseReport.length,
);
},
},
mounted() {
const { headPath, basePath, apiUrl, canManageLicenses } = this;
......@@ -77,7 +95,9 @@ export default {
:neutral-issues="licenseReport"
:has-issues="hasLicenseReportIssues"
:component="$options.componentNames.LicenseIssueBody"
class="license-report-widget mr-widget-border-top"
:class="reportSectionClass"
:always-open="alwaysOpen"
class="license-report-widget"
/>
</div>
</template>
......@@ -110,6 +110,20 @@ export default {
dastText() {
return this.summaryTextBuilder('DAST', this.dast.newIssues.length);
},
issuesCount() {
return (
this.dast.newIssues.length +
this.dependencyScanning.newIssues.length +
this.sastContainer.newIssues.length +
this.sast.newIssues.length
);
},
},
watch: {
issuesCount() {
this.$emit('updateBadgeCount', this.issuesCount);
},
},
created() {
// update the store with the received props
......@@ -123,33 +137,23 @@ export default {
if (this.sastHeadPath) {
this.setSastHeadPath(this.sastHeadPath);
this.fetchSastReports()
.then(() => {
this.$emit('updateBadgeCount', this.sast.newIssues.length);
})
.catch(() => createFlash(s__('ciReport|There was an error loading SAST report')));
this.fetchSastReports().catch(() =>
createFlash(s__('ciReport|There was an error loading SAST report')),
);
}
if (this.dependencyScanningHeadPath) {
this.setDependencyScanningHeadPath(this.dependencyScanningHeadPath);
this.fetchDependencyScanningReports()
.then(() => {
this.$emit('updateBadgeCount', this.dependencyScanning.newIssues.length);
})
.catch(() =>
createFlash(s__('ciReport|There was an error loading dependency scanning report')),
);
this.fetchDependencyScanningReports().catch(() =>
createFlash(s__('ciReport|There was an error loading dependency scanning report')),
);
}
if (this.sastContainerHeadPath) {
this.setSastContainerHeadPath(this.sastContainerHeadPath);
this.fetchSastContainerReports()
.then(() => {
this.$emit('updateBadgeCount', this.sastContainer.newIssues.length);
})
.catch(() =>
this.fetchSastContainerReports().catch(() =>
createFlash(s__('ciReport|There was an error loading container scanning report')),
);
}
......@@ -157,11 +161,7 @@ export default {
if (this.dastHeadPath) {
this.setDastHeadPath(this.dastHeadPath);
this.fetchDastReports()
.then(() => {
this.$emit('updateBadgeCount', this.dast.newIssues.length);
})
.catch(() =>
this.fetchDastReports().catch(() =>
createFlash(s__('ciReport|There was an error loading DAST report')),
);
}
......@@ -184,7 +184,6 @@ export default {
'setCanCreateIssuePermission',
'setCanCreateFeedbackPermission',
]),
summaryTextBuilder(type, issuesCount = 0) {
if (issuesCount === 0) {
return sprintf(s__('ciReport|%{type} detected no vulnerabilities'), {
......
......@@ -4,7 +4,15 @@ module EE
extend ActiveSupport::Concern
def security
if pipeline.expose_sast_data?
if pipeline.expose_security_dashboard?
render_show
else
redirect_to pipeline_path(pipeline)
end
end
def licenses
if pipeline.expose_license_management_data?
render_show
else
redirect_to pipeline_path(pipeline)
......
......@@ -2,6 +2,7 @@ module EE
module GitlabRoutingHelper
include ::ProjectsHelper
include ::ApplicationSettingsHelper
include ::API::Helpers::RelatedResourcesHelpers
def geo_primary_web_url(project_or_wiki)
File.join(::Gitlab::Geo.primary_node.url, project_or_wiki.full_path)
......@@ -64,5 +65,9 @@ module EE
pipeline.license_management_artifact,
path: Ci::Build::LICENSE_MANAGEMENT_FILE)
end
def license_management_api_url(project)
api_v4_projects_managed_licenses_path(id: project.id)
end
end
end
- pipeline = local_assigns.fetch(:pipeline)
- project = local_assigns.fetch(:project)
- return unless pipeline.expose_security_dashboard?
- sast_endpoint = pipeline.expose_sast_data? ? sast_artifact_url(pipeline) : nil
- dependency_scanning_endpoint = pipeline.expose_dependency_scanning_data? ? dependency_scanning_artifact_url(pipeline) : nil
- dast_endpoint = pipeline.expose_dast_data? ? dast_artifact_url(pipeline) : nil
- sast_container_endpoint = pipeline.expose_sast_container_data? ? sast_container_artifact_url(pipeline) : pipeline.expose_container_scanning_data? ? container_scanning_artifact_url(pipeline) : nil
- blob_path = project_blob_path(project, pipeline.sha)
#js-tab-security.build-security.tab-pane
#js-security-report-app{ data: { endpoint: sast_endpoint,
blob_path: blob_path,
dependency_scanning_endpoint: dependency_scanning_endpoint,
dast_endpoint: dast_endpoint,
sast_container_endpoint: sast_container_endpoint,
pipeline_id: pipeline.id,
vulnerability_feedback_path: project_vulnerability_feedback_index_path(project),
vulnerability_feedback_help_path: help_page_path("user/project/merge_requests/index", anchor: "interacting-with-security-reports-ultimate"),
sast_help_path: help_page_path('user/project/merge_requests/sast'),
dependency_scanning_help_path: help_page_path('user/project/merge_requests/dependency_scanning'),
dast_help_path: help_page_path('user/project/merge_requests/dast'),
sast_container_help_path: help_page_path('user/project/merge_requests/sast_container'),
can_create_feedback: can?(current_user, :admin_vulnerability_feedback, project),
can_create_issue: show_new_issue_link?(project)} }
- if pipeline.expose_security_dashboard?
#js-tab-security.build-security.tab-pane
#js-security-report-app{ data: { head_blob_path: blob_path,
sast_head_path: sast_endpoint,
dependency_scanning_head_path: dependency_scanning_endpoint,
dast_head_path: dast_endpoint,
sast_container_head_path: sast_container_endpoint,
pipeline_id: pipeline.id,
vulnerability_feedback_path: project_vulnerability_feedback_index_path(project),
vulnerability_feedback_help_path: help_page_path("user/project/merge_requests/index", anchor: "interacting-with-security-reports-ultimate"),
sast_help_path: help_page_path('user/project/merge_requests/sast'),
dependency_scanning_help_path: help_page_path('user/project/merge_requests/dependency_scanning'),
dast_help_path: help_page_path('user/project/merge_requests/dast'),
sast_container_help_path: help_page_path('user/project/merge_requests/sast_container'),
can_create_feedback: can?(current_user, :admin_vulnerability_feedback, project).to_s,
can_create_issue: show_new_issue_link?(project).to_s } }
- if pipeline.expose_license_management_data?
#js-tab-licenses.tab-pane
#js-licenses-app{ data: { license_head_path: pipeline.expose_license_management_data? ? license_management_artifact_url(pipeline) : nil,
api_url: license_management_api_url(project),
can_manage_licenses: can?(current_user, :admin_software_license_policy, project).to_s } }
- pipeline = local_assigns.fetch(:pipeline)
- project = local_assigns.fetch(:project)
- return unless pipeline.expose_security_dashboard?
- if pipeline.expose_security_dashboard?
%li.js-security-tab-link
= link_to security_project_pipeline_path(project, pipeline), data: { target: '#js-tab-security', action: 'security', toggle: 'tab' }, class: 'security-tab' do
= _("Security")
%span.badge.badge-pill.js-sast-counter.hidden
%li.js-security-tab-link
= link_to security_project_pipeline_path(project, pipeline), data: { target: '#js-tab-security', action: 'security', toggle: 'tab' }, class: 'security-tab' do
= _("Security report")
%span.badge.badge-pill.js-sast-counter.hidden
- if pipeline.expose_license_management_data?
%li.js-licenses-tab-link
= link_to licenses_project_pipeline_path(project, pipeline), data: { target: '#js-tab-licenses', action: 'licenses', toggle: 'tab' }, class: 'licenses-tab' do
= _("Licenses")
%span.badge.badge-pill.js-licenses-counter.hidden
---
title: Show License Management at pipeline level
merge_request: 6688
author:
type: added
......@@ -11,7 +11,7 @@ describe Projects::PipelinesController do
end
describe 'GET security' do
let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id) }
set(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id) }
context 'with a sast artifact' do
before do
......@@ -31,7 +31,7 @@ describe Projects::PipelinesController do
context 'with feature enabled' do
before do
allow(License).to receive(:feature_available?).and_return(true)
stub_licensed_features(sast: true)
get :security, namespace_id: project.namespace, project_id: project, id: pipeline
end
......@@ -48,7 +48,6 @@ describe Projects::PipelinesController do
end
it do
expect(response).to have_gitlab_http_status(:redirect)
expect(response).to redirect_to(pipeline_path(pipeline))
end
end
......@@ -57,13 +56,12 @@ describe Projects::PipelinesController do
context 'without sast artifact' do
context 'with feature enabled' do
before do
allow(License).to receive(:feature_available?).and_return(true)
stub_licensed_features(sast: true)
get :security, namespace_id: project.namespace, project_id: project, id: pipeline
end
it do
expect(response).to have_gitlab_http_status(:redirect)
expect(response).to redirect_to(pipeline_path(pipeline))
end
end
......@@ -74,7 +72,74 @@ describe Projects::PipelinesController do
end
it do
expect(response).to have_gitlab_http_status(:redirect)
expect(response).to redirect_to(pipeline_path(pipeline))
end
end
end
end
describe 'GET licenses' do
set(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id) }
context 'with a license management artifact' do
before do
create(
:ci_build,
:success,
:artifacts,
name: 'license_management',
pipeline: pipeline,
options: {
artifacts: {
paths: [Ci::Build::LICENSE_MANAGEMENT_FILE]
}
}
)
end
context 'with feature enabled' do
before do
stub_licensed_features(license_management: true)
get :licenses, namespace_id: project.namespace, project_id: project, id: pipeline
end
it do
expect(response).to have_gitlab_http_status(200)
expect(response).to render_template :show
end
end
context 'with feature disabled' do
before do
get :licenses, namespace_id: project.namespace, project_id: project, id: pipeline
end
it do
expect(response).to redirect_to(pipeline_path(pipeline))
end
end
end
context 'without license management artifact' do
context 'with feature enabled' do
before do
stub_licensed_features(license_management: true)
get :licenses, namespace_id: project.namespace, project_id: project, id: pipeline
end
it do
expect(response).to redirect_to(pipeline_path(pipeline))
end
end
context 'with feature disabled' do
before do
get :licenses, namespace_id: project.namespace, project_id: project, id: pipeline
end
it do
expect(response).to redirect_to(pipeline_path(pipeline))
end
end
......
......@@ -7,13 +7,15 @@ describe 'Pipeline', :js do
before do
sign_in(user)
project.add_developer(user)
allow(License).to receive(:feature_available?).and_return(true)
end
describe 'GET /:project/pipelines/:id/security' do
let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id) }
before do
stub_licensed_features(sast: true)
end
context 'with a sast artifact' do
before do
create(
......@@ -33,7 +35,7 @@ describe 'Pipeline', :js do
end
it 'shows jobs tab pane as active' do
expect(page).to have_content('Security report')
expect(page).to have_content('Security')
expect(page).to have_css('#js-tab-security')
end
......@@ -49,7 +51,56 @@ describe 'Pipeline', :js do
it 'displays the pipeline graph' do
expect(current_path).to eq(pipeline_path(pipeline))
expect(page).not_to have_content('Security report')
expect(page).not_to have_content('Security')
expect(page).to have_selector('.pipeline-visualization')
end
end
end
describe 'GET /:project/pipelines/:id/licenses' do
let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id) }
before do
stub_licensed_features(license_management: true)
end
context 'with a license management artifact' do
before do
create(
:ci_build,
:success,
:artifacts,
name: 'license_management',
pipeline: pipeline,
options: {
artifacts: {
paths: [Ci::Build::LICENSE_MANAGEMENT_FILE]
}
}
)
visit licenses_project_pipeline_path(project, pipeline)
end
it 'shows jobs tab pane as active' do
expect(page).to have_content('Licenses')
expect(page).to have_css('#js-tab-licenses')
expect(find('.js-licenses-counter')).to have_content('0')
end
it 'shows security report section' do
expect(page).to have_content('Loading license management report')
end
end
context 'without license management artifact' do
before do
visit licenses_project_pipeline_path(project, pipeline)
end
it 'displays the pipeline graph' do
expect(current_path).to eq(pipeline_path(pipeline))
expect(page).not_to have_content('Licenses')
expect(page).to have_selector('.pipeline-visualization')
end
end
......
......@@ -3934,6 +3934,9 @@ msgstr ""
msgid "LicenseManagement|You are about to remove the license, %{name}, from this project."
msgstr ""
msgid "Licenses"
msgstr ""
msgid "LinkedIn"
msgstr ""
......@@ -5645,10 +5648,10 @@ msgstr ""
msgid "Secret:"
msgstr ""
msgid "Security Dashboard"
msgid "Security"
msgstr ""
msgid "Security report"
msgid "Security Dashboard"
msgstr ""
msgid "SecurityDashboard|Monitor vulnerabilities in your code"
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment