Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
52b32eecb7
commit
980faa8f34
93 changed files with 2945 additions and 487 deletions
|
@ -112,6 +112,7 @@ export default {
|
|||
:empty-text="s__('AdminUsers|No users found')"
|
||||
show-empty
|
||||
stacked="md"
|
||||
data-qa-selector="user_row_content"
|
||||
>
|
||||
<template #cell(name)="{ item: user }">
|
||||
<user-avatar :user="user" :admin-user-path="paths.adminUser" />
|
||||
|
|
124
app/assets/javascripts/members/components/members_tabs.vue
Normal file
124
app/assets/javascripts/members/components/members_tabs.vue
Normal file
|
@ -0,0 +1,124 @@
|
|||
<script>
|
||||
import { GlTabs, GlTab, GlBadge } from '@gitlab/ui';
|
||||
import { mapState } from 'vuex';
|
||||
import { urlParamsToObject } from '~/lib/utils/common_utils';
|
||||
import { __ } from '~/locale';
|
||||
import { MEMBER_TYPES } from '../constants';
|
||||
import MembersApp from './app.vue';
|
||||
|
||||
const countComputed = (state, namespace) => state[namespace]?.pagination?.totalItems || 0;
|
||||
|
||||
export default {
|
||||
name: 'MembersTabs',
|
||||
tabs: [
|
||||
{
|
||||
namespace: MEMBER_TYPES.user,
|
||||
title: __('Members'),
|
||||
},
|
||||
{
|
||||
namespace: MEMBER_TYPES.group,
|
||||
title: __('Groups'),
|
||||
attrs: { 'data-qa-selector': 'groups_list_tab' },
|
||||
},
|
||||
{
|
||||
namespace: MEMBER_TYPES.invite,
|
||||
title: __('Invited'),
|
||||
canManageMembersPermissionsRequired: true,
|
||||
},
|
||||
{
|
||||
namespace: MEMBER_TYPES.accessRequest,
|
||||
title: __('Access requests'),
|
||||
canManageMembersPermissionsRequired: true,
|
||||
},
|
||||
],
|
||||
urlParams: [],
|
||||
components: { MembersApp, GlTabs, GlTab, GlBadge },
|
||||
inject: ['canManageMembers'],
|
||||
data() {
|
||||
return {
|
||||
selectedTabIndex: 0,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
userCount(state) {
|
||||
return countComputed(state, MEMBER_TYPES.user);
|
||||
},
|
||||
groupCount(state) {
|
||||
return countComputed(state, MEMBER_TYPES.group);
|
||||
},
|
||||
inviteCount(state) {
|
||||
return countComputed(state, MEMBER_TYPES.invite);
|
||||
},
|
||||
accessRequestCount(state) {
|
||||
return countComputed(state, MEMBER_TYPES.accessRequest);
|
||||
},
|
||||
}),
|
||||
urlParams() {
|
||||
return Object.keys(urlParamsToObject(window.location.search));
|
||||
},
|
||||
activeTabIndexCalculatedFromUrlParams() {
|
||||
return this.$options.tabs.findIndex(({ namespace }) => {
|
||||
return this.getTabUrlParams(namespace).some((urlParam) =>
|
||||
this.urlParams.includes(urlParam),
|
||||
);
|
||||
});
|
||||
},
|
||||
},
|
||||
created() {
|
||||
if (this.activeTabIndexCalculatedFromUrlParams === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.selectedTabIndex = this.activeTabIndexCalculatedFromUrlParams;
|
||||
},
|
||||
methods: {
|
||||
getTabUrlParams(namespace) {
|
||||
const state = this.$store.state[namespace];
|
||||
const urlParams = [];
|
||||
|
||||
if (state?.pagination?.paramName) {
|
||||
urlParams.push(state.pagination.paramName);
|
||||
}
|
||||
|
||||
if (state?.filteredSearchBar?.searchParam) {
|
||||
urlParams.push(state.filteredSearchBar.searchParam);
|
||||
}
|
||||
|
||||
return urlParams;
|
||||
},
|
||||
getTabCount({ namespace }) {
|
||||
return this[`${namespace}Count`];
|
||||
},
|
||||
showTab(tab, index) {
|
||||
if (tab.namespace === MEMBER_TYPES.user) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const { canManageMembersPermissionsRequired = false } = tab;
|
||||
const tabCanBeShown =
|
||||
this.getTabCount(tab) > 0 || this.activeTabIndexCalculatedFromUrlParams === index;
|
||||
|
||||
if (canManageMembersPermissionsRequired) {
|
||||
return this.canManageMembers && tabCanBeShown;
|
||||
}
|
||||
|
||||
return tabCanBeShown;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-tabs v-model="selectedTabIndex">
|
||||
<template v-for="(tab, index) in $options.tabs">
|
||||
<gl-tab v-if="showTab(tab, index)" :key="tab.namespace" :title-link-attributes="tab.attrs">
|
||||
<template slot="title">
|
||||
<span>{{ tab.title }}</span>
|
||||
<gl-badge size="sm" class="gl-tab-counter-badge">{{ getTabCount(tab) }}</gl-badge>
|
||||
</template>
|
||||
<members-app :namespace="tab.namespace" />
|
||||
</gl-tab>
|
||||
</template>
|
||||
</gl-tabs>
|
||||
</template>
|
|
@ -28,7 +28,7 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
isExpanded: true,
|
||||
isExpanded: false,
|
||||
topPosition: 0,
|
||||
};
|
||||
},
|
||||
|
@ -49,8 +49,18 @@ export default {
|
|||
},
|
||||
mounted() {
|
||||
this.setTopPosition();
|
||||
this.setInitialExpandState();
|
||||
},
|
||||
methods: {
|
||||
setInitialExpandState() {
|
||||
// We check in the local storage and if no value is defined, we want the default
|
||||
// to be true. We want to explicitly set it to true here so that the drawer
|
||||
// animates to open on load.
|
||||
const localValue = localStorage.getItem(this.$options.localDrawerKey);
|
||||
if (localValue === null) {
|
||||
this.isExpanded = true;
|
||||
}
|
||||
},
|
||||
setTopPosition() {
|
||||
const navbarEl = document.querySelector('.js-navbar');
|
||||
|
||||
|
@ -68,7 +78,7 @@ export default {
|
|||
<local-storage-sync v-model="isExpanded" :storage-key="$options.localDrawerKey" as-json>
|
||||
<aside
|
||||
aria-live="polite"
|
||||
class="gl-fixed gl-right-0 gl-bg-gray-10 gl-shadow-drawer gl-transition-medium gl-border-l-solid gl-border-1 gl-border-gray-100 gl-h-full gl-z-index-3 gl-overflow-y-auto"
|
||||
class="gl-fixed gl-right-0 gl-bg-gray-10 gl-shadow-drawer gl-transition-property-width gl-transition-duration-medium gl-border-l-solid gl-border-1 gl-border-gray-100 gl-h-full gl-z-index-3 gl-overflow-y-auto"
|
||||
:style="rootStyle"
|
||||
>
|
||||
<gl-button
|
||||
|
|
|
@ -35,6 +35,7 @@ class Admin::PlanLimitsController < Admin::ApplicationController
|
|||
npm_max_file_size
|
||||
nuget_max_file_size
|
||||
pypi_max_file_size
|
||||
terraform_module_max_file_size
|
||||
generic_packages_max_file_size
|
||||
])
|
||||
end
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Groups::EmailCampaignsController < Groups::ApplicationController
|
||||
include InProductMarketingHelper
|
||||
|
||||
EMAIL_CAMPAIGNS_SCHEMA_URL = 'iglu:com.gitlab/email_campaigns/jsonschema/1-0-0'
|
||||
|
||||
feature_category :navigation
|
||||
|
@ -18,11 +16,13 @@ class Groups::EmailCampaignsController < Groups::ApplicationController
|
|||
|
||||
def track_click
|
||||
if Gitlab.com?
|
||||
message = Gitlab::Email::Message::InProductMarketing.for(@track).new(group: group, series: @series)
|
||||
|
||||
data = {
|
||||
namespace_id: group.id,
|
||||
track: @track.to_s,
|
||||
series: @series,
|
||||
subject_line: subject_line(@track, @series)
|
||||
subject_line: message.subject_line
|
||||
}
|
||||
context = SnowplowTracker::SelfDescribingJson.new(EMAIL_CAMPAIGNS_SCHEMA_URL, data)
|
||||
|
||||
|
|
11
app/controllers/terraform/services_controller.rb
Normal file
11
app/controllers/terraform/services_controller.rb
Normal file
|
@ -0,0 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Terraform::ServicesController < ApplicationController
|
||||
skip_before_action :authenticate_user!
|
||||
|
||||
feature_category :infrastructure_as_code
|
||||
|
||||
def index
|
||||
render json: { 'modules.v1' => "/api/#{::API::API.version}/packages/terraform/modules/v1/" }
|
||||
end
|
||||
end
|
|
@ -42,7 +42,7 @@ module Packages
|
|||
end
|
||||
|
||||
def filter_by_package_type(packages)
|
||||
return packages unless package_type
|
||||
return packages.without_package_type(:terraform_module) unless package_type
|
||||
raise InvalidPackageTypeError unless ::Packages::Package.package_types.key?(package_type)
|
||||
|
||||
packages.with_package_type(package_type)
|
||||
|
@ -54,6 +54,12 @@ module Packages
|
|||
packages.search_by_name(params[:package_name])
|
||||
end
|
||||
|
||||
def filter_by_package_version(packages)
|
||||
return packages unless params[:package_version].present?
|
||||
|
||||
packages.with_version(params[:package_version])
|
||||
end
|
||||
|
||||
def filter_with_version(packages)
|
||||
return packages if params[:include_versionless].present?
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@ module Packages
|
|||
packages = filter_with_version(packages)
|
||||
packages = filter_by_package_type(packages)
|
||||
packages = filter_by_package_name(packages)
|
||||
packages = filter_by_package_version(packages)
|
||||
installable_only ? packages.installable : filter_by_status(packages)
|
||||
end
|
||||
|
||||
|
|
|
@ -5,7 +5,8 @@ module Types
|
|||
class PackageTypeEnum < BaseEnum
|
||||
PACKAGE_TYPE_NAMES = {
|
||||
pypi: 'PyPI',
|
||||
npm: 'npm'
|
||||
npm: 'npm',
|
||||
terraform_module: 'Terraform Module'
|
||||
}.freeze
|
||||
|
||||
::Packages::Package.package_types.keys.each do |package_type|
|
||||
|
|
|
@ -1,381 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module InProductMarketingHelper
|
||||
def subject_line(track, series)
|
||||
{
|
||||
create: [
|
||||
s_('InProductMarketing|Create a project in GitLab in 5 minutes'),
|
||||
s_('InProductMarketing|Import your project and code from GitHub, Bitbucket and others'),
|
||||
s_('InProductMarketing|Understand repository mirroring')
|
||||
],
|
||||
verify: [
|
||||
s_('InProductMarketing|Feel the need for speed?'),
|
||||
s_('InProductMarketing|3 ways to dive into GitLab CI/CD'),
|
||||
s_('InProductMarketing|Explore the power of GitLab CI/CD')
|
||||
],
|
||||
trial: [
|
||||
s_('InProductMarketing|Go farther with GitLab'),
|
||||
s_('InProductMarketing|Automated security scans directly within GitLab'),
|
||||
s_('InProductMarketing|Take your source code management to the next level')
|
||||
],
|
||||
team: [
|
||||
s_('InProductMarketing|Working in GitLab = more efficient'),
|
||||
s_("InProductMarketing|Multiple owners, confusing workstreams? We've got you covered"),
|
||||
s_('InProductMarketing|Your teams can be more efficient')
|
||||
]
|
||||
}[track][series]
|
||||
end
|
||||
|
||||
def in_product_marketing_logo(track, series)
|
||||
inline_image_link('mailers/in_product_marketing', "#{track}-#{series}.png", { width: '150', style: 'width: 150px;' })
|
||||
end
|
||||
|
||||
def about_link(folder, image, width)
|
||||
link_to inline_image_link(folder, image, { width: width, style: "width: #{width}px;", alt: s_('InProductMarketing|go to about.gitlab.com') }), 'https://about.gitlab.com/'
|
||||
end
|
||||
|
||||
def in_product_marketing_tagline(track, series)
|
||||
{
|
||||
create: [
|
||||
s_('InProductMarketing|Get started today'),
|
||||
s_('InProductMarketing|Get our import guides'),
|
||||
s_('InProductMarketing|Need an alternative to importing?')
|
||||
],
|
||||
verify: [
|
||||
s_('InProductMarketing|Use GitLab CI/CD'),
|
||||
s_('InProductMarketing|Test, create, deploy'),
|
||||
s_('InProductMarketing|Are your runners ready?')
|
||||
],
|
||||
trial: [
|
||||
s_('InProductMarketing|Start a free trial of GitLab Ultimate – no CC required'),
|
||||
s_('InProductMarketing|Improve app security with a 30-day trial'),
|
||||
s_('InProductMarketing|Start with a GitLab Ultimate free trial')
|
||||
],
|
||||
team: [
|
||||
s_('InProductMarketing|Invite your colleagues to join in less than one minute'),
|
||||
s_('InProductMarketing|Get your team set up on GitLab'),
|
||||
nil
|
||||
]
|
||||
}[track][series]
|
||||
end
|
||||
|
||||
def in_product_marketing_title(track, series)
|
||||
{
|
||||
create: [
|
||||
s_('InProductMarketing|Take your first steps with GitLab'),
|
||||
s_('InProductMarketing|Start by importing your projects'),
|
||||
s_('InProductMarketing|How (and why) mirroring makes sense')
|
||||
],
|
||||
verify: [
|
||||
s_('InProductMarketing|Rapid development, simplified'),
|
||||
s_('InProductMarketing|Get started with GitLab CI/CD'),
|
||||
s_('InProductMarketing|Launch GitLab CI/CD in 20 minutes or less')
|
||||
],
|
||||
trial: [
|
||||
s_('InProductMarketing|Give us one minute...'),
|
||||
s_("InProductMarketing|Security that's integrated into your development lifecycle"),
|
||||
s_('InProductMarketing|Improve code quality and streamline reviews')
|
||||
],
|
||||
team: [
|
||||
s_('InProductMarketing|Team work makes the dream work'),
|
||||
s_('InProductMarketing|*GitLab*, noun: a synonym for efficient teams'),
|
||||
s_('InProductMarketing|Find out how your teams are really doing')
|
||||
]
|
||||
}[track][series]
|
||||
end
|
||||
|
||||
def in_product_marketing_subtitle(track, series)
|
||||
{
|
||||
create: [
|
||||
s_('InProductMarketing|Dig in and create a project and a repo'),
|
||||
s_("InProductMarketing|Here's what you need to know"),
|
||||
s_('InProductMarketing|Try it out')
|
||||
],
|
||||
verify: [
|
||||
s_('InProductMarketing|How to build and test faster'),
|
||||
s_('InProductMarketing|Explore the options'),
|
||||
s_('InProductMarketing|Follow our steps')
|
||||
],
|
||||
trial: [
|
||||
s_('InProductMarketing|...and you can get a free trial of GitLab Ultimate'),
|
||||
s_('InProductMarketing|Try GitLab Ultimate for free'),
|
||||
s_('InProductMarketing|Better code in less time')
|
||||
],
|
||||
team: [
|
||||
s_('InProductMarketing|Actually, GitLab makes the team work (better)'),
|
||||
s_('InProductMarketing|Our tool brings all the things together'),
|
||||
s_("InProductMarketing|It's all in the stats")
|
||||
]
|
||||
}[track][series]
|
||||
end
|
||||
|
||||
def in_product_marketing_body_line1(track, series, format: nil)
|
||||
{
|
||||
create: [
|
||||
s_("InProductMarketing|To understand and get the most out of GitLab, start at the beginning and %{project_link}. In GitLab, repositories are part of a project, so after you've created your project you can go ahead and %{repo_link}.") % { project_link: project_link(format), repo_link: repo_link(format) },
|
||||
s_("InProductMarketing|Making the switch? It's easier than you think to import your projects into GitLab. Move %{github_link}, or import something %{bitbucket_link}.") % { github_link: github_link(format), bitbucket_link: bitbucket_link(format) },
|
||||
s_("InProductMarketing|Sometimes you're not ready to make a full transition to a new tool. If you're not ready to fully commit, %{mirroring_link} gives you a safe way to try out GitLab in parallel with your current tool.") % { mirroring_link: mirroring_link(format) }
|
||||
],
|
||||
verify: [
|
||||
s_("InProductMarketing|Tired of wrestling with disparate tool chains, information silos and inefficient processes? GitLab's CI/CD is built on a DevOps platform with source code management, planning, monitoring and more ready to go. Find out %{ci_link}.") % { ci_link: ci_link(format) },
|
||||
s_("InProductMarketing|GitLab's CI/CD makes software development easier. Don't believe us? Here are three ways you can take it for a fast (and satisfying) test drive:"),
|
||||
s_("InProductMarketing|Get going with CI/CD quickly using our %{quick_start_link}. Start with an available runner and then create a CI .yml file – it's really that easy.") % { quick_start_link: quick_start_link(format) }
|
||||
],
|
||||
trial: [
|
||||
[
|
||||
s_("InProductMarketing|GitLab's premium tiers are designed to make you, your team and your application more efficient and more secure with features including but not limited to:"),
|
||||
list([
|
||||
s_('InProductMarketing|%{strong_start}Company wide portfolio management%{strong_end} — including multi-level epics, scoped labels').html_safe % strong_options(format),
|
||||
s_('InProductMarketing|%{strong_start}Multiple approval roles%{strong_end} — including code owners and required merge approvals').html_safe % strong_options(format),
|
||||
s_('InProductMarketing|%{strong_start}Advanced application security%{strong_end} — including SAST, DAST scanning, FUZZ testing, dependency scanning, license compliance, secrete detection').html_safe % strong_options(format),
|
||||
s_('InProductMarketing|%{strong_start}Executive level insights%{strong_end} — including reporting on productivity, tasks by type, days to completion, value stream').html_safe % strong_options(format)
|
||||
], format)
|
||||
].join("\n"),
|
||||
s_('InProductMarketing|GitLab provides static application security testing (SAST), dynamic application security testing (DAST), container scanning, and dependency scanning to help you deliver secure applications along with license compliance.'),
|
||||
s_('InProductMarketing|By enabling code owners and required merge approvals the right person will review the right MR. This is a win-win: cleaner code and a more efficient review process.')
|
||||
],
|
||||
team: [
|
||||
[
|
||||
s_('InProductMarketing|Did you know teams that use GitLab are far more efficient?'),
|
||||
list([
|
||||
s_('InProductMarketing|Goldman Sachs went from 1 build every two weeks to thousands of builds a day'),
|
||||
s_('InProductMarketing|Ticketmaster decreased their CI build time by 15X')
|
||||
], format)
|
||||
].join("\n"),
|
||||
s_("InProductMarketing|We know a thing or two about efficiency and we don't want to keep that to ourselves. Sign up for a free trial of GitLab Ultimate and your teams will be on it from day one."),
|
||||
[
|
||||
s_('InProductMarketing|Stop wondering and use GitLab to answer questions like:'),
|
||||
list([
|
||||
s_('InProductMarketing|How long does it take us to close issues/MRs by types like feature requests, bugs, tech debt, security?'),
|
||||
s_('InProductMarketing|How many days does it take our team to complete various tasks?'),
|
||||
s_('InProductMarketing|What does our value stream timeline look like from product to development to review and production?')
|
||||
], format)
|
||||
].join("\n")
|
||||
]
|
||||
}[track][series]
|
||||
end
|
||||
|
||||
def in_product_marketing_body_line2(track, series, format: nil)
|
||||
{
|
||||
create: [
|
||||
s_("InProductMarketing|That's all it takes to get going with GitLab, but if you're new to working with Git, check out our %{basics_link} for helpful tips and tricks for getting started.") % { basics_link: basics_link(format) },
|
||||
s_("InProductMarketing|Have a different instance you'd like to import? Here's our %{import_link}.") % { import_link: import_link(format) },
|
||||
s_("InProductMarketing|It's also possible to simply %{external_repo_link} in order to take advantage of GitLab's CI/CD.") % { external_repo_link: external_repo_link(format) }
|
||||
],
|
||||
verify: [
|
||||
nil,
|
||||
list([
|
||||
s_('InProductMarketing|Start by %{performance_link}').html_safe % { performance_link: performance_link(format) },
|
||||
s_('InProductMarketing|Move on to easily creating a Pages website %{ci_template_link}').html_safe % { ci_template_link: ci_template_link(format) },
|
||||
s_('InProductMarketing|And finally %{deploy_link} a Python application.').html_safe % { deploy_link: deploy_link(format) }
|
||||
], format),
|
||||
nil
|
||||
],
|
||||
trial: [
|
||||
s_('InProductMarketing|Start a GitLab Ultimate trial today in less than one minute, no credit card required.'),
|
||||
s_('InProductMarketing|Get started today with a 30-day GitLab Ultimate trial, no credit card required.'),
|
||||
s_('InProductMarketing|Code owners and required merge approvals are part of the paid tiers of GitLab. You can start a free 30-day trial of GitLab Ultimate and enable these features in less than 5 minutes with no credit card required.')
|
||||
],
|
||||
team: [
|
||||
s_('InProductMarketing|Invite your colleagues and start shipping code faster.'),
|
||||
s_("InProductMarketing|Streamline code review, know at a glance who's unavailable, communicate in comments or in email and integrate with Slack so everyone's on the same page."),
|
||||
s_('InProductMarketing|When your team is on GitLab these answers are a click away.')
|
||||
]
|
||||
}[track][series]
|
||||
end
|
||||
|
||||
def cta_link(track, series, group, format: nil)
|
||||
case format
|
||||
when :html
|
||||
link_to in_product_marketing_cta_text(track, series), group_email_campaigns_url(group, track: track, series: series), target: '_blank', rel: 'noopener noreferrer'
|
||||
else
|
||||
[in_product_marketing_cta_text(track, series), group_email_campaigns_url(group, track: track, series: series)].join(' >> ')
|
||||
end
|
||||
end
|
||||
|
||||
def in_product_marketing_progress(track, series, format: nil)
|
||||
if Gitlab.com?
|
||||
s_('InProductMarketing|This is email %{series} of 3 in the %{track} series.') % { series: series + 1, track: track.to_s.humanize }
|
||||
else
|
||||
s_('InProductMarketing|This is email %{series} of 3 in the %{track} series. To disable notification emails sent by your local GitLab instance, either contact your administrator or %{unsubscribe_link}.') % { series: series + 1, track: track.to_s.humanize, unsubscribe_link: unsubscribe_link(format) }
|
||||
end
|
||||
end
|
||||
|
||||
def footer_links(format: nil)
|
||||
links = [
|
||||
[s_('InProductMarketing|Blog'), 'https://about.gitlab.com/blog'],
|
||||
[s_('InProductMarketing|Twitter'), 'https://twitter.com/gitlab'],
|
||||
[s_('InProductMarketing|Facebook'), 'https://www.facebook.com/gitlab'],
|
||||
[s_('InProductMarketing|YouTube'), 'https://www.youtube.com/channel/UCnMGQ8QHMAnVIsI3xJrihhg']
|
||||
]
|
||||
case format
|
||||
when :html
|
||||
links.map do |text, link|
|
||||
link_to(text, link)
|
||||
end
|
||||
else
|
||||
'| ' + links.map do |text, link|
|
||||
[text, link].join(' ')
|
||||
end.join("\n| ")
|
||||
end
|
||||
end
|
||||
|
||||
def address(format: nil)
|
||||
s_('InProductMarketing|%{strong_start}GitLab Inc.%{strong_end} 268 Bush Street, #350, San Francisco, CA 94104, USA').html_safe % strong_options(format)
|
||||
end
|
||||
|
||||
def unsubscribe(track, series, format: nil)
|
||||
parts = Gitlab.com? ? unsubscribe_com(format) : unsubscribe_self_managed(track, series, format)
|
||||
|
||||
case format
|
||||
when :html
|
||||
parts.join(' ')
|
||||
else
|
||||
parts.join("\n" + ' ' * 16)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def unsubscribe_com(format)
|
||||
[
|
||||
s_('InProductMarketing|If you no longer wish to receive marketing emails from us,'),
|
||||
s_('InProductMarketing|you may %{unsubscribe_link} at any time.') % { unsubscribe_link: unsubscribe_link(format) }
|
||||
]
|
||||
end
|
||||
|
||||
def unsubscribe_self_managed(track, series, format)
|
||||
[
|
||||
s_('InProductMarketing|To opt out of these onboarding emails, %{unsubscribe_link}.') % { unsubscribe_link: unsubscribe_link(format) },
|
||||
s_("InProductMarketing|If you don't want to receive marketing emails directly from GitLab, %{marketing_preference_link}.") % { marketing_preference_link: marketing_preference_link(track, series, format) }
|
||||
]
|
||||
end
|
||||
|
||||
def in_product_marketing_cta_text(track, series)
|
||||
{
|
||||
create: [
|
||||
s_('InProductMarketing|Create your first project!'),
|
||||
s_('InProductMarketing|Master the art of importing!'),
|
||||
s_('InProductMarketing|Understand your project options')
|
||||
],
|
||||
verify: [
|
||||
s_('InProductMarketing|Get to know GitLab CI/CD'),
|
||||
s_('InProductMarketing|Try it yourself'),
|
||||
s_('InProductMarketing|Explore GitLab CI/CD')
|
||||
],
|
||||
trial: [
|
||||
s_('InProductMarketing|Start a trial'),
|
||||
s_('InProductMarketing|Beef up your security'),
|
||||
s_('InProductMarketing|Start your trial now!')
|
||||
],
|
||||
team: [
|
||||
s_('InProductMarketing|Invite your colleagues today'),
|
||||
s_('InProductMarketing|Invite your team in less than 60 seconds'),
|
||||
s_('InProductMarketing|Invite your team now')
|
||||
]
|
||||
}[track][series]
|
||||
end
|
||||
|
||||
def project_link(format)
|
||||
link(s_('InProductMarketing|create a project'), help_page_url('gitlab-basics/create-project'), format)
|
||||
end
|
||||
|
||||
def repo_link(format)
|
||||
link(s_('InProductMarketing|set up a repo'), help_page_url('user/project/repository/index', anchor: 'create-a-repository'), format)
|
||||
end
|
||||
|
||||
def github_link(format)
|
||||
link(s_('InProductMarketing|GitHub Enterprise projects to GitLab'), help_page_url('integration/github'), format)
|
||||
end
|
||||
|
||||
def bitbucket_link(format)
|
||||
link(s_('InProductMarketing|from Bitbucket'), help_page_url('user/project/import/bitbucket_server'), format)
|
||||
end
|
||||
|
||||
def mirroring_link(format)
|
||||
link(s_('InProductMarketing|repository mirroring'), help_page_url('user/project/repository/repository_mirroring'), format)
|
||||
end
|
||||
|
||||
def ci_link(format)
|
||||
link(s_('InProductMarketing|how easy it is to get started'), help_page_url('ci/README'), format)
|
||||
end
|
||||
|
||||
def performance_link(format)
|
||||
link(s_('InProductMarketing|testing browser performance'), help_page_url('user/project/merge_requests/browser_performance_testing'), format)
|
||||
end
|
||||
|
||||
def ci_template_link(format)
|
||||
link(s_('InProductMarketing|using a CI/CD template'), help_page_url('user/project/pages/getting_started/pages_ci_cd_template'), format)
|
||||
end
|
||||
|
||||
def deploy_link(format)
|
||||
link(s_('InProductMarketing|test and deploy'), help_page_url('ci/examples/test-and-deploy-python-application-to-heroku'), format)
|
||||
end
|
||||
|
||||
def quick_start_link(format)
|
||||
link(s_('InProductMarketing|quick start guide'), help_page_url('ci/quick_start/README'), format)
|
||||
end
|
||||
|
||||
def basics_link(format)
|
||||
link(s_('InProductMarketing|Git basics'), help_page_url('gitlab-basics/README'), format)
|
||||
end
|
||||
|
||||
def import_link(format)
|
||||
link(s_('InProductMarketing|comprehensive guide'), help_page_url('user/project/import/index'), format)
|
||||
end
|
||||
|
||||
def external_repo_link(format)
|
||||
link(s_('InProductMarketing|connect an external repository'), new_project_url(anchor: 'cicd_for_external_repo'), format)
|
||||
end
|
||||
|
||||
def unsubscribe_link(format)
|
||||
unsubscribe_url = Gitlab.com? ? '%tag_unsubscribe_url%' : profile_notifications_url
|
||||
|
||||
link(s_('InProductMarketing|unsubscribe'), unsubscribe_url, format)
|
||||
end
|
||||
|
||||
def marketing_preference_link(track, series, format)
|
||||
params = {
|
||||
utm_source: 'SM',
|
||||
utm_medium: 'email',
|
||||
utm_campaign: 'onboarding',
|
||||
utm_term: "#{track}_#{series}"
|
||||
}
|
||||
|
||||
preference_link = "https://about.gitlab.com/company/preference-center/?#{params.to_query}"
|
||||
|
||||
link(s_('InProductMarketing|update your preferences'), preference_link, format)
|
||||
end
|
||||
|
||||
def link(text, link, format)
|
||||
case format
|
||||
when :html
|
||||
link_to text, link
|
||||
else
|
||||
"#{text} (#{link})"
|
||||
end
|
||||
end
|
||||
|
||||
def list(array, format)
|
||||
case format
|
||||
when :html
|
||||
tag.ul { array.map { |item| concat tag.li item} }
|
||||
else
|
||||
'- ' + array.join("\n- ")
|
||||
end
|
||||
end
|
||||
|
||||
def strong_options(format)
|
||||
case format
|
||||
when :html
|
||||
{ strong_start: '<b>'.html_safe, strong_end: '</b>'.html_safe }
|
||||
else
|
||||
{ strong_start: '', strong_end: '' }
|
||||
end
|
||||
end
|
||||
|
||||
def inline_image_link(folder, image, options)
|
||||
attachments.inline[image] = File.read(Rails.root.join("app/assets/images", folder, image))
|
||||
def inline_image_link(image, options)
|
||||
attachments.inline[image] = File.read(Rails.root.join("app/assets/images", image))
|
||||
image_tag attachments[image].url, **options
|
||||
end
|
||||
|
||||
def about_link(image, width)
|
||||
link_to inline_image_link(image, { width: width, style: "width: #{width}px;", alt: s_('InProductMarketing|go to about.gitlab.com') }), 'https://about.gitlab.com/'
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,8 +2,6 @@
|
|||
|
||||
module Emails
|
||||
module InProductMarketing
|
||||
include InProductMarketingHelper
|
||||
|
||||
FROM_ADDRESS = 'GitLab <team@gitlab.com>'
|
||||
CUSTOM_HEADERS = {
|
||||
from: FROM_ADDRESS,
|
||||
|
@ -15,13 +13,11 @@ module Emails
|
|||
}.freeze
|
||||
|
||||
def in_product_marketing_email(recipient_id, group_id, track, series)
|
||||
@track = track
|
||||
@series = series
|
||||
@group = Group.find(group_id)
|
||||
group = Group.find(group_id)
|
||||
email = User.find(recipient_id).notification_email_for(group)
|
||||
@message = Gitlab::Email::Message::InProductMarketing.for(track).new(group: group, series: series)
|
||||
|
||||
email = User.find(recipient_id).notification_email_for(@group)
|
||||
subject = subject_line(track, series)
|
||||
mail_to(to: email, subject: subject)
|
||||
mail_to(to: email, subject: @message.subject_line)
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -29,8 +25,17 @@ module Emails
|
|||
def mail_to(to:, subject:)
|
||||
custom_headers = Gitlab.com? ? CUSTOM_HEADERS : {}
|
||||
mail(to: to, subject: subject, **custom_headers) do |format|
|
||||
format.html { render layout: nil }
|
||||
format.text { render layout: nil }
|
||||
format.html do
|
||||
@message.format = :html
|
||||
|
||||
render layout: nil
|
||||
end
|
||||
|
||||
format.text do
|
||||
@message.format = :text
|
||||
|
||||
render layout: nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -9,9 +9,9 @@ module Ci
|
|||
# * No variables
|
||||
# * No spaces
|
||||
# * Minimal length of 8 characters
|
||||
# * Characters must be from the Base64 alphabet (RFC4648) with the addition of '@', ':' and '.'
|
||||
# * Characters must be from the Base64 alphabet (RFC4648) with the addition of '@', ':', '.', and '~'
|
||||
# * Absolutely no fun is allowed
|
||||
REGEX = /\A[a-zA-Z0-9_+=\/@:.-]{8,}\z/.freeze
|
||||
REGEX = /\A[a-zA-Z0-9_+=\/@:.~-]{8,}\z/.freeze
|
||||
|
||||
included do
|
||||
validates :masked, inclusion: { in: [true, false] }
|
||||
|
|
|
@ -51,6 +51,7 @@ class Packages::Package < ApplicationRecord
|
|||
validates :name, format: { with: Gitlab::Regex.helm_package_regex }, if: :helm?
|
||||
validates :name, format: { with: Gitlab::Regex.npm_package_name_regex }, if: :npm?
|
||||
validates :name, format: { with: Gitlab::Regex.nuget_package_name_regex }, if: :nuget?
|
||||
validates :name, format: { with: Gitlab::Regex.terraform_module_package_name_regex }, if: :terraform_module?
|
||||
validates :name, format: { with: Gitlab::Regex.debian_package_name_regex }, if: :debian_package?
|
||||
validates :name, inclusion: { in: %w[incoming] }, if: :debian_incoming?
|
||||
validates :version, format: { with: Gitlab::Regex.nuget_version_regex }, if: :nuget?
|
||||
|
@ -59,7 +60,7 @@ class Packages::Package < ApplicationRecord
|
|||
validates :version, format: { with: Gitlab::Regex.pypi_version_regex }, if: :pypi?
|
||||
validates :version, format: { with: Gitlab::Regex.prefixed_semver_regex }, if: :golang?
|
||||
validates :version, format: { with: Gitlab::Regex.prefixed_semver_regex }, if: :helm?
|
||||
validates :version, format: { with: Gitlab::Regex.semver_regex }, if: -> { composer_tag_version? || npm? }
|
||||
validates :version, format: { with: Gitlab::Regex.semver_regex }, if: -> { composer_tag_version? || npm? || terraform_module? }
|
||||
|
||||
validates :version,
|
||||
presence: true,
|
||||
|
@ -73,7 +74,7 @@ class Packages::Package < ApplicationRecord
|
|||
|
||||
enum package_type: { maven: 1, npm: 2, conan: 3, nuget: 4, pypi: 5,
|
||||
composer: 6, generic: 7, golang: 8, debian: 9,
|
||||
rubygems: 10, helm: 11 }
|
||||
rubygems: 10, helm: 11, terraform_module: 12 }
|
||||
|
||||
enum status: { default: 0, hidden: 1, processing: 2, error: 3 }
|
||||
|
||||
|
@ -85,6 +86,7 @@ class Packages::Package < ApplicationRecord
|
|||
scope :with_version, ->(version) { where(version: version) }
|
||||
scope :without_version_like, -> (version) { where.not(arel_table[:version].matches(version)) }
|
||||
scope :with_package_type, ->(package_type) { where(package_type: package_type) }
|
||||
scope :without_package_type, ->(package_type) { where.not(package_type: package_type) }
|
||||
scope :with_status, ->(status) { where(status: status) }
|
||||
scope :displayable, -> { with_status(DISPLAYABLE_STATUSES) }
|
||||
scope :installable, -> { with_status(INSTALLABLE_STATUSES) }
|
||||
|
|
40
app/presenters/terraform/modules_presenter.rb
Normal file
40
app/presenters/terraform/modules_presenter.rb
Normal file
|
@ -0,0 +1,40 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Terraform
|
||||
class ModulesPresenter < Gitlab::View::Presenter::Simple
|
||||
attr_accessor :packages, :system
|
||||
|
||||
presents :modules
|
||||
|
||||
def initialize(packages, system)
|
||||
@packages = packages
|
||||
@system = system
|
||||
end
|
||||
|
||||
def modules
|
||||
project_url = @packages.first&.project&.web_url
|
||||
versions = @packages.map do |package|
|
||||
{
|
||||
'version' => package.version,
|
||||
'submodules' => [],
|
||||
'root' => {
|
||||
'dependencies' => [],
|
||||
'providers' => [
|
||||
{
|
||||
'name' => @system,
|
||||
'version' => ''
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
[
|
||||
{
|
||||
'versions' => versions,
|
||||
'source' => project_url
|
||||
}.compact
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,70 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Packages
|
||||
module TerraformModule
|
||||
class CreatePackageService < ::Packages::CreatePackageService
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
def execute
|
||||
return error('Version is empty.', 400) if params[:module_version].blank?
|
||||
return error('Package already exists.', 403) if current_package_exists_elsewhere?
|
||||
return error('Package version already exists.', 403) if current_package_version_exists?
|
||||
return error('File is too large.', 400) if file_size_exceeded?
|
||||
|
||||
ActiveRecord::Base.transaction { create_terraform_module_package! }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_terraform_module_package!
|
||||
package = create_package!(:terraform_module, name: name, version: params[:module_version])
|
||||
|
||||
::Packages::CreatePackageFileService.new(package, file_params).execute
|
||||
|
||||
package
|
||||
end
|
||||
|
||||
def current_package_exists_elsewhere?
|
||||
::Packages::Package
|
||||
.for_projects(project.root_namespace.all_projects.id_not_in(project.id))
|
||||
.with_package_type(:terraform_module)
|
||||
.with_name(name)
|
||||
.exists?
|
||||
end
|
||||
|
||||
def current_package_version_exists?
|
||||
project.packages
|
||||
.with_package_type(:terraform_module)
|
||||
.with_name(name)
|
||||
.with_version(params[:module_version])
|
||||
.exists?
|
||||
end
|
||||
|
||||
def name
|
||||
strong_memoize(:name) do
|
||||
"#{params[:module_name]}/#{params[:module_system]}"
|
||||
end
|
||||
end
|
||||
|
||||
def file_name
|
||||
strong_memoize(:file_name) do
|
||||
"#{params[:module_name]}-#{params[:module_system]}-#{params[:module_version]}.tgz"
|
||||
end
|
||||
end
|
||||
|
||||
def file_params
|
||||
{
|
||||
file: params[:file],
|
||||
size: params[:file].size,
|
||||
file_sha256: params[:file].sha256,
|
||||
file_name: file_name,
|
||||
build: params[:build]
|
||||
}
|
||||
end
|
||||
|
||||
def file_size_exceeded?
|
||||
project.actual_limits.exceeded?(:generic_packages_max_file_size, params[:file].size)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -44,6 +44,9 @@
|
|||
.form-group
|
||||
= f.label :pypi_max_file_size, _('Maximum PyPI package file size in bytes'), class: 'label-bold'
|
||||
= f.number_field :pypi_max_file_size, class: 'form-control gl-form-input'
|
||||
.form-group
|
||||
= f.label :terraform_module_max_file_size, _('Maximum Terraform Module package file size in bytes'), class: 'label-bold'
|
||||
= f.number_field :terraform_module_max_file_size, class: 'form-control gl-form-input'
|
||||
.form-group
|
||||
= f.label :generic_packages_max_file_size, _('Generic package file size in bytes'), class: 'label-bold'
|
||||
= f.number_field :generic_packages_max_file_size, class: 'form-control gl-form-input'
|
||||
|
|
|
@ -163,43 +163,43 @@
|
|||
%table{ border: "0", cellpadding: "0", cellspacing: "0", role: "presentation", width: "100%" }
|
||||
%tr
|
||||
%td{ align: "left", style: "padding: 0 20px;" }
|
||||
= about_link('mailers/in_product_marketing', 'gitlab-logo-gray-rgb.png', 200)
|
||||
= about_link('mailers/in_product_marketing/gitlab-logo-gray-rgb.png', 200)
|
||||
%tr
|
||||
%td{ "aria-hidden" => "true", height: "30", style: "font-size: 0; line-height: 0;" }
|
||||
%tr{ style: "background-color: #ffffff;" }
|
||||
%td{ style: "color: #424242; padding: 10px 30px; text-align: center; font-family: 'Source Sans Pro', helvetica, arial, sans-serif;font-size: 16px; line-height: 22px; border: 1px solid #dddddd" }
|
||||
%p
|
||||
= in_product_marketing_progress(@track, @series, format: :html).html_safe
|
||||
= @message.progress.html_safe
|
||||
%tr
|
||||
%td{ bgcolor: "#ffffff", height: "auto", style: "max-width: 600px; width: 100%; text-align: center; height: 200px; padding: 25px 15px; mso-line-height-rule: exactly; min-height: 40px; font-family: 'Source Sans Pro', helvetica, arial, sans-serif;", valign: "middle", width: "100%" }
|
||||
= in_product_marketing_logo(@track, @series)
|
||||
= inline_image_link(@message.logo_path, { width: '150', style: 'width: 150px;' })
|
||||
%h1{ style: "font-size: 40px; line-height: 46x; color: #000000; padding: 20px 0 0 0; font-weight: normal;" }
|
||||
= in_product_marketing_title(@track, @series)
|
||||
= @message.title
|
||||
%h2{ style: "font-size: 28px; line-height: 34px; color: #000000; padding: 0; font-weight: 400;" }
|
||||
= in_product_marketing_subtitle(@track, @series)
|
||||
= @message.subtitle
|
||||
%tr
|
||||
%td{ style: "padding: 10px 20px 30px 20px; font-family: 'Source Sans Pro', helvetica, arial, sans-serif; color:#000000; font-size: 18px; line-height: 24px;" }
|
||||
%p{ style: "margin: 0 0 20px 0;" }
|
||||
= in_product_marketing_body_line1(@track, @series, format: :html).html_safe
|
||||
- in_product_marketing_body_line2(@track, @series, format: :html)&.tap do |line|
|
||||
= @message.body_line1.html_safe
|
||||
- @message.body_line2&.tap do |line|
|
||||
%p{ style: "margin: 0 0 20px 0;" }
|
||||
= line.html_safe
|
||||
%tr
|
||||
%td{ align: "center", style: "padding: 10px 20px 80px 20px; font-family: 'Source Sans Pro', helvetica, arial, sans-serif;" }
|
||||
.cta_link= cta_link(@track, @series, @group, format: :html)
|
||||
.cta_link= @message.cta_link
|
||||
%tr{ style: "background-color: #ffffff;" }
|
||||
%td{ align: "center", style: "padding:75px 20px 25px;" }
|
||||
= about_link('', 'gitlab_logo.png', 80)
|
||||
= about_link('gitlab_logo.png', 80)
|
||||
%tr{ style: "background-color: #ffffff;" }
|
||||
%td{ align: "center", style: "padding:0px ;" }
|
||||
%tr{ style: "background-color: #ffffff;" }
|
||||
%td{ align: "center", style: "padding:0px 10px; font-family: 'Source Sans Pro', helvetica, arial, sans-serif; " }
|
||||
%span.footernav{ style: "color: #6e49cb; font-size: 16px; line-height: 26px; font-family: 'Source Sans Pro', helvetica, arial, sans-serif;" }
|
||||
= footer_links(format: :html).join(' ' * 3 + '|' + ' ' * 4).html_safe
|
||||
= @message.footer_links.join(' ' * 3 + '|' + ' ' * 4).html_safe
|
||||
%tr{ style: "background-color:#ffffff;" }
|
||||
%td{ align: "center", style: "padding: 40px 30px 20px 30px; font-family: 'Source Sans Pro', helvetica, arial, sans-serif;" }
|
||||
.address= address(format: :html)
|
||||
.address= @message.address
|
||||
%tr{ style: "background-color: #ffffff;" }
|
||||
%td{ align: "left", style: "padding:20px 30px 20px 30px;" }
|
||||
%span.footernav{ style: "color: #6e49cb; font-size: 14px; line-height: 20px; font-family: 'Source Sans Pro', helvetica, arial, sans-serif; color:#424242;" }
|
||||
= unsubscribe(@track, @series, format: :html).html_safe
|
||||
= @message.unsubscribe.html_safe
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
<%= in_product_marketing_tagline(@track, @series) %>
|
||||
<%= @message.tagline %>
|
||||
|
||||
<%= in_product_marketing_title(@track, @series) %>
|
||||
<%= in_product_marketing_subtitle(@track, @series) %>
|
||||
<%= @message.title %>
|
||||
<%= @message.subtitle %>
|
||||
|
||||
|
||||
<%= in_product_marketing_body_line1(@track, @series) %>
|
||||
<%= @message.body_line1 %>
|
||||
|
||||
<%= in_product_marketing_body_line2(@track, @series) %>
|
||||
<%= @message.body_line2 %>
|
||||
|
||||
<%= cta_link(@track, @series, @group) %>
|
||||
<%= @message.cta_link %>
|
||||
|
||||
|
||||
|
||||
|
@ -16,8 +16,8 @@
|
|||
|
||||
|
||||
|
||||
<%= footer_links %>
|
||||
<%= @message.footer_links %>
|
||||
|
||||
<%= address %>
|
||||
<%= @message.address %>
|
||||
|
||||
<%= unsubscribe(@track, @series) %>
|
||||
<%= @message.unsubscribe %>
|
||||
|
|
|
@ -2,48 +2,50 @@
|
|||
- can_update_merge_request = can?(current_user, :update_merge_request, @merge_request)
|
||||
- can_reopen_merge_request = can?(current_user, :reopen_merge_request, @merge_request)
|
||||
- are_close_and_open_buttons_hidden = merge_request_button_hidden?(@merge_request, true) && merge_request_button_hidden?(@merge_request, false)
|
||||
- cache_key = [@project, @merge_request, can_update_merge_request, can_reopen_merge_request, are_close_and_open_buttons_hidden]
|
||||
|
||||
- if @merge_request.closed_or_merged_without_fork?
|
||||
.gl-alert.gl-alert-danger.gl-mb-5
|
||||
= sprite_icon('error', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
|
||||
.gl-alert-body
|
||||
The source project of this merge request has been removed.
|
||||
= cache_if(Feature.enabled?(:cached_mr_title, @project, default_enabled: :yaml), cache_key, expires_in: 1.day) do
|
||||
- if @merge_request.closed_or_merged_without_fork?
|
||||
.gl-alert.gl-alert-danger.gl-mb-5
|
||||
= sprite_icon('error', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
|
||||
.gl-alert-body
|
||||
The source project of this merge request has been removed.
|
||||
|
||||
.detail-page-header.border-bottom-0.pt-0.pb-0
|
||||
.detail-page-header-body
|
||||
= render "shared/issuable/status_box", issuable: @merge_request
|
||||
.detail-page-header.border-bottom-0.pt-0.pb-0
|
||||
.detail-page-header-body
|
||||
= render "shared/issuable/status_box", issuable: @merge_request
|
||||
|
||||
.issuable-meta
|
||||
#js-issuable-header-warnings
|
||||
= issuable_meta(@merge_request, @project)
|
||||
.issuable-meta
|
||||
#js-issuable-header-warnings
|
||||
= issuable_meta(@merge_request, @project)
|
||||
|
||||
%a.gl-button.btn.btn-default.btn-icon.float-right.d-block.d-sm-none.gutter-toggle.issuable-gutter-toggle.js-sidebar-toggle{ href: "#" }
|
||||
= sprite_icon('chevron-double-lg-left')
|
||||
%a.gl-button.btn.btn-default.btn-icon.float-right.d-block.d-sm-none.gutter-toggle.issuable-gutter-toggle.js-sidebar-toggle{ href: "#" }
|
||||
= sprite_icon('chevron-double-lg-left')
|
||||
|
||||
.detail-page-header-actions.js-issuable-actions
|
||||
.clearfix.dropdown
|
||||
%button.gl-button.btn.btn-default.float-left.gl-md-display-none.gl-w-full{ type: "button", data: { toggle: "dropdown" } }
|
||||
Options
|
||||
= sprite_icon('chevron-down', css_class: 'gl-text-gray-500')
|
||||
.dropdown-menu.dropdown-menu-right
|
||||
%ul
|
||||
- if can_update_merge_request
|
||||
%li= link_to 'Edit', edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)
|
||||
- if @merge_request.opened?
|
||||
%li
|
||||
= link_to @merge_request.work_in_progress? ? _('Mark as ready') : _('Mark as draft'), toggle_draft_merge_request_path(@merge_request), method: :put, class: "js-draft-toggle-button"
|
||||
%li{ class: [merge_request_button_visibility(@merge_request, true), 'js-close-item'] }
|
||||
= link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, title: 'Close merge request'
|
||||
- if can_reopen_merge_request
|
||||
%li{ class: merge_request_button_visibility(@merge_request, false) }
|
||||
= link_to 'Reopen', merge_request_path(@merge_request, merge_request: { state_event: :reopen }), method: :put, title: 'Reopen merge request'
|
||||
- unless @merge_request.merged? || current_user == @merge_request.author
|
||||
%li= link_to 'Report abuse', new_abuse_report_path(user_id: @merge_request.author.id, ref_url: merge_request_url(@merge_request))
|
||||
.detail-page-header-actions.js-issuable-actions
|
||||
.clearfix.dropdown
|
||||
%button.gl-button.btn.btn-default.float-left.gl-md-display-none.gl-w-full{ type: "button", data: { toggle: "dropdown" } }
|
||||
Options
|
||||
= sprite_icon('chevron-down', css_class: 'gl-text-gray-500')
|
||||
.dropdown-menu.dropdown-menu-right
|
||||
%ul
|
||||
- if can_update_merge_request
|
||||
%li= link_to 'Edit', edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)
|
||||
- if @merge_request.opened?
|
||||
%li
|
||||
= link_to @merge_request.work_in_progress? ? _('Mark as ready') : _('Mark as draft'), toggle_draft_merge_request_path(@merge_request), method: :put, class: "js-draft-toggle-button"
|
||||
%li{ class: [merge_request_button_visibility(@merge_request, true), 'js-close-item'] }
|
||||
= link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, title: 'Close merge request'
|
||||
- if can_reopen_merge_request
|
||||
%li{ class: merge_request_button_visibility(@merge_request, false) }
|
||||
= link_to 'Reopen', merge_request_path(@merge_request, merge_request: { state_event: :reopen }), method: :put, title: 'Reopen merge request'
|
||||
- unless @merge_request.merged? || current_user == @merge_request.author
|
||||
%li= link_to 'Report abuse', new_abuse_report_path(user_id: @merge_request.author.id, ref_url: merge_request_url(@merge_request))
|
||||
|
||||
- if can_update_merge_request
|
||||
= link_to 'Edit', edit_project_merge_request_path(@project, @merge_request), class: "d-none d-md-block btn gl-button btn-default btn-grouped js-issuable-edit", data: { qa_selector: "edit_button" }
|
||||
- if can_update_merge_request
|
||||
= link_to 'Edit', edit_project_merge_request_path(@project, @merge_request), class: "d-none d-md-block btn gl-button btn-default btn-grouped js-issuable-edit", data: { qa_selector: "edit_button" }
|
||||
|
||||
- if can_update_merge_request && !are_close_and_open_buttons_hidden
|
||||
= render 'projects/merge_requests/close_reopen_draft_report_toggle'
|
||||
- elsif !@merge_request.merged?
|
||||
= link_to _('Report abuse'), new_abuse_report_path(user_id: @merge_request.author.id, ref_url: merge_request_url(@merge_request)), class: 'gl-display-none gl-md-display-block gl-button btn btn-default float-right gl-ml-3', title: _('Report abuse')
|
||||
- if can_update_merge_request && !are_close_and_open_buttons_hidden
|
||||
= render 'projects/merge_requests/close_reopen_draft_report_toggle'
|
||||
- elsif !@merge_request.merged?
|
||||
= link_to _('Report abuse'), new_abuse_report_path(user_id: @merge_request.author.id, ref_url: merge_request_url(@merge_request)), class: 'gl-display-none gl-md-display-block gl-button btn btn-default float-right gl-ml-3', title: _('Report abuse')
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add body to finding evidence requests
|
||||
merge_request: 61408
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Allows masking ~ character.
|
||||
merge_request: 61517
|
||||
author: Thomas Dallmair
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add Terraform Module Registry
|
||||
merge_request: 55018
|
||||
author:
|
||||
type: added
|
8
config/feature_flags/development/cached_mr_title.yml
Normal file
8
config/feature_flags/development/cached_mr_title.yml
Normal file
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: cached_mr_title
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/61605
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/330907
|
||||
milestone: '13.12'
|
||||
type: development
|
||||
group: group::source code
|
||||
default_enabled: false
|
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
key_path: redis_hll_counters.deploy_token_packages.i_package_terraform_module_deploy_token_monthly
|
||||
description: Number of distinct users authorized via deploy token creating Terraform Module packages in recent 28 days
|
||||
product_section: ops
|
||||
product_stage: configure
|
||||
product_group: group::configure
|
||||
product_category: infrastructure_as_code
|
||||
value_type: number
|
||||
status: implemented
|
||||
milestone: '13.11'
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55018
|
||||
time_frame: 28d
|
||||
data_source: redis_hll
|
||||
distribution:
|
||||
- ce
|
||||
- ee
|
||||
tier:
|
||||
- free
|
||||
- premium
|
||||
- ultimate
|
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
key_path: redis_hll_counters.user_packages.i_package_terraform_module_user_monthly
|
||||
description: Number of distinct users creating Terraform Module packages in recent 28 days
|
||||
product_section: ops
|
||||
product_stage: configure
|
||||
product_group: group::configure
|
||||
product_category: infrastructure_as_code
|
||||
value_type: number
|
||||
status: implemented
|
||||
milestone: '13.11'
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55018
|
||||
time_frame: 28d
|
||||
data_source: redis_hll
|
||||
distribution:
|
||||
- ce
|
||||
- ee
|
||||
tier:
|
||||
- free
|
||||
- premium
|
||||
- ultimate
|
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
key_path: redis_hll_counters.deploy_token_packages.i_package_terraform_module_deploy_token_weekly
|
||||
description: Number of distinct users authorized via deploy token creating Terraform Module packages in recent 7 days
|
||||
product_section: ops
|
||||
product_stage: configure
|
||||
product_group: group::configure
|
||||
product_category: infrastructure_as_code
|
||||
value_type: number
|
||||
status: implemented
|
||||
milestone: '13.11'
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55018
|
||||
time_frame: 7d
|
||||
data_source: redis_hll
|
||||
distribution:
|
||||
- ce
|
||||
- ee
|
||||
tier:
|
||||
- free
|
||||
- premium
|
||||
- ultimate
|
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
key_path: redis_hll_counters.user_packages.i_package_terraform_module_user_weekly
|
||||
description: Number of distinct users creating Terraform Module packages in recent 7 days
|
||||
product_section: ops
|
||||
product_stage: configure
|
||||
product_group: group::configure
|
||||
product_category: infrastructure_as_code
|
||||
value_type: number
|
||||
status: implemented
|
||||
milestone: '13.11'
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55018
|
||||
time_frame: 7d
|
||||
data_source: redis_hll
|
||||
distribution:
|
||||
- ce
|
||||
- ee
|
||||
tier:
|
||||
- free
|
||||
- premium
|
||||
- ultimate
|
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
key_path: counts.package_events_i_package_terraform_module_delete_package
|
||||
description: Total count of Terraform Module packages delete events
|
||||
product_section: ops
|
||||
product_stage: configure
|
||||
product_group: group::configure
|
||||
product_category: infrastructure_as_code
|
||||
value_type: number
|
||||
status: implemented
|
||||
milestone: '13.11'
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55018
|
||||
time_frame: all
|
||||
data_source: redis
|
||||
distribution:
|
||||
- ce
|
||||
- ee
|
||||
tier:
|
||||
- free
|
||||
- premium
|
||||
- ultimate
|
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
key_path: counts.package_events_i_package_terraform_module_pull_package
|
||||
description: Total count of pull Terraform Module packages events
|
||||
product_section: ops
|
||||
product_stage: configure
|
||||
product_group: group::configure
|
||||
product_category: infrastructure_as_code
|
||||
value_type: number
|
||||
status: implemented
|
||||
milestone: '13.11'
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55018
|
||||
time_frame: all
|
||||
data_source: redis
|
||||
distribution:
|
||||
- ce
|
||||
- ee
|
||||
tier:
|
||||
- free
|
||||
- premium
|
||||
- ultimate
|
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
key_path: counts.package_events_i_package_terraform_module_push_package
|
||||
description: Total count of push Terraform Module packages events
|
||||
product_section: ops
|
||||
product_stage: configure
|
||||
product_group: group::configure
|
||||
product_category: infrastructure_as_code
|
||||
value_type: number
|
||||
status: implemented
|
||||
milestone: '13.11'
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55018
|
||||
time_frame: all
|
||||
data_source: redis
|
||||
distribution:
|
||||
- ce
|
||||
- ee
|
||||
tier:
|
||||
- free
|
||||
- premium
|
||||
- ultimate
|
|
@ -77,6 +77,9 @@ Rails.application.routes.draw do
|
|||
# Health check
|
||||
get 'health_check(/:checks)' => 'health_check#index', as: :health_check
|
||||
|
||||
# Terraform service discovery
|
||||
get '.well-known/terraform.json' => 'terraform/services#index', as: :terraform_services
|
||||
|
||||
# Begin of the /-/ scope.
|
||||
# Use this scope for all new global routes.
|
||||
scope path: '-' do
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddBodyToFindingsEvidencesRequest < ActiveRecord::Migration[6.0]
|
||||
# rubocop:disable Migration/AddLimitToTextColumns
|
||||
# limit is added in 20210510191552_add_limit_to_findings_evidences_request_body.rb
|
||||
def change
|
||||
add_column :vulnerability_finding_evidence_requests, :body, :text
|
||||
end
|
||||
# rubocop:enable Migration/AddLimitToTextColumns
|
||||
end
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddLimitToFindingsEvidencesRequestBody < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_text_limit :vulnerability_finding_evidence_requests, :body, 2048
|
||||
end
|
||||
|
||||
def down
|
||||
remove_text_limit :vulnerability_finding_evidence_requests, :body
|
||||
end
|
||||
end
|
1
db/schema_migrations/20210510191551
Normal file
1
db/schema_migrations/20210510191551
Normal file
|
@ -0,0 +1 @@
|
|||
949038f9f66788e3289afbf210617f7947762e4bbab4c7389164cbd775302642
|
1
db/schema_migrations/20210510191552
Normal file
1
db/schema_migrations/20210510191552
Normal file
|
@ -0,0 +1 @@
|
|||
e59505ee2a3ef04c1af8a426f7ebdb83874c926cf7d7f98b56e0af8cd38988f5
|
|
@ -18809,6 +18809,8 @@ CREATE TABLE vulnerability_finding_evidence_requests (
|
|||
vulnerability_finding_evidence_id bigint NOT NULL,
|
||||
method text,
|
||||
url text,
|
||||
body text,
|
||||
CONSTRAINT check_7e37f2d01a CHECK ((char_length(body) <= 2048)),
|
||||
CONSTRAINT check_8152fbb236 CHECK ((char_length(url) <= 2048)),
|
||||
CONSTRAINT check_d9d11300f4 CHECK ((char_length(method) <= 32))
|
||||
);
|
||||
|
|
|
@ -1401,6 +1401,13 @@ praefect['reconciliation_scheduling_interval'] = '0' # disable the feature
|
|||
|
||||
### Manual reconciliation
|
||||
|
||||
WARNING:
|
||||
The `reconcile` sub-command is deprecated and scheduled for removal in GitLab 14.0. Use
|
||||
[automatic reconciliation](#automatic-reconciliation) instead. Manual reconciliation may
|
||||
produce excess replication jobs and is limited in functionality. Manual reconciliation does
|
||||
not work when [repository-specific primary nodes](#repository-specific-primary-nodes) are
|
||||
enabled.
|
||||
|
||||
The Praefect `reconcile` sub-command allows for the manual reconciliation between two Gitaly nodes. The
|
||||
command replicates every repository on a later version on the reference storage to the target storage.
|
||||
|
||||
|
|
|
@ -14232,6 +14232,7 @@ Values for sorting package.
|
|||
| <a id="packagetypeenumnuget"></a>`NUGET` | Packages from the Nuget package manager. |
|
||||
| <a id="packagetypeenumpypi"></a>`PYPI` | Packages from the PyPI package manager. |
|
||||
| <a id="packagetypeenumrubygems"></a>`RUBYGEMS` | Packages from the Rubygems package manager. |
|
||||
| <a id="packagetypeenumterraform_module"></a>`TERRAFORM_MODULE` | Packages from the Terraform Module package manager. |
|
||||
|
||||
### `PipelineConfigSourceEnum`
|
||||
|
||||
|
|
|
@ -349,8 +349,8 @@ report format XML files contain an `attachment` tag, GitLab parses the attachmen
|
|||
```
|
||||
|
||||
- You should set the job that uploads the screenshot to
|
||||
[`artifacts:when: on_failure`](yaml/README.md#artifactswhen) so that it uploads a screenshot when
|
||||
a test fails.
|
||||
[`artifacts:when: always`](yaml/README.md#artifactswhen) so that it still uploads a screenshot
|
||||
when a test fails.
|
||||
|
||||
A link to the test case attachment appears in the test case details in
|
||||
[the pipeline test report](#viewing-unit-test-reports-on-gitlab).
|
||||
|
|
|
@ -298,6 +298,7 @@ The value of the variable must:
|
|||
- Characters from the Base64 alphabet (RFC4648).
|
||||
- The `@` and `:` characters ([In GitLab 12.2](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/63043) and later).
|
||||
- The `.` character ([In GitLab 12.10](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/29022) and later).
|
||||
- The `~` character ([In GitLab 13.12](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/61517) and later).
|
||||
- Not match the name of an existing predefined or custom CI/CD variable.
|
||||
|
||||
### Protect a CI/CD variable
|
||||
|
|
|
@ -3754,6 +3754,42 @@ Status: `data_available`
|
|||
|
||||
Tiers: `free`
|
||||
|
||||
### `counts.package_events_i_package_terraform_module_delete_package`
|
||||
|
||||
Total count of Terraform Module packages delete events
|
||||
|
||||
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210410012200_package_events_i_package_terraform_module_delete_package.yml)
|
||||
|
||||
Group: `group::configure`
|
||||
|
||||
Status: `implemented`
|
||||
|
||||
Tiers: `free`, `premium`, `ultimate`
|
||||
|
||||
### `counts.package_events_i_package_terraform_module_pull_package`
|
||||
|
||||
Total count of pull Terraform Module packages events
|
||||
|
||||
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210410012202_package_events_i_package_terraform_module_pull_package.yml)
|
||||
|
||||
Group: `group::configure`
|
||||
|
||||
Status: `implemented`
|
||||
|
||||
Tiers: `free`, `premium`, `ultimate`
|
||||
|
||||
### `counts.package_events_i_package_terraform_module_push_package`
|
||||
|
||||
Total count of push Terraform Module packages events
|
||||
|
||||
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210410012204_package_events_i_package_terraform_module_push_package.yml)
|
||||
|
||||
Group: `group::configure`
|
||||
|
||||
Status: `implemented`
|
||||
|
||||
Tiers: `free`, `premium`, `ultimate`
|
||||
|
||||
### `counts.packages`
|
||||
|
||||
Number of packages
|
||||
|
@ -9898,6 +9934,30 @@ Status: `data_available`
|
|||
|
||||
Tiers:
|
||||
|
||||
### `redis_hll_counters.deploy_token_packages.i_package_terraform_module_deploy_token_monthly`
|
||||
|
||||
Number of distinct users authorized via deploy token creating Terraform Module packages in recent 28 days
|
||||
|
||||
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_28d/20210410012206_i_package_terraform_module_deploy_token_monthly.yml)
|
||||
|
||||
Group: `group::configure`
|
||||
|
||||
Status: `implemented`
|
||||
|
||||
Tiers: `free`, `premium`, `ultimate`
|
||||
|
||||
### `redis_hll_counters.deploy_token_packages.i_package_terraform_module_deploy_token_weekly`
|
||||
|
||||
Number of distinct users authorized via deploy token creating Terraform Module packages in recent 7 days
|
||||
|
||||
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_7d/20210410012207_i_package_terraform_module_deploy_token_weekly.yml)
|
||||
|
||||
Group: `group::configure`
|
||||
|
||||
Status: `implemented`
|
||||
|
||||
Tiers: `free`, `premium`, `ultimate`
|
||||
|
||||
### `redis_hll_counters.ecosystem.ecosystem_total_unique_counts_monthly`
|
||||
|
||||
Missing description
|
||||
|
@ -14866,6 +14926,30 @@ Status: `data_available`
|
|||
|
||||
Tiers:
|
||||
|
||||
### `redis_hll_counters.user_packages.i_package_terraform_module_user_monthly`
|
||||
|
||||
Number of distinct users creating Terraform Module packages in recent 28 days
|
||||
|
||||
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_28d/20210410012208_i_package_terraform_module_user_monthly.yml)
|
||||
|
||||
Group: `group::configure`
|
||||
|
||||
Status: `implemented`
|
||||
|
||||
Tiers: `free`, `premium`, `ultimate`
|
||||
|
||||
### `redis_hll_counters.user_packages.i_package_terraform_module_user_weekly`
|
||||
|
||||
Number of distinct users creating Terraform Module packages in recent 7 days
|
||||
|
||||
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_7d/20210410012209_i_package_terraform_module_user_weekly.yml)
|
||||
|
||||
Group: `group::configure`
|
||||
|
||||
Status: `implemented`
|
||||
|
||||
Tiers: `free`, `premium`, `ultimate`
|
||||
|
||||
### `redis_hll_counters.user_packages.user_packages_total_unique_counts_monthly`
|
||||
|
||||
Missing description
|
||||
|
|
|
@ -126,6 +126,7 @@ For each user, the following are listed:
|
|||
1. Username
|
||||
1. Email address
|
||||
1. Project membership count
|
||||
1. Group membership count ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/276215) in GitLab 13.12)
|
||||
1. Date of account creation
|
||||
1. Date of last activity
|
||||
|
||||
|
|
|
@ -241,6 +241,7 @@ module API
|
|||
mount ::API::ProjectTemplates
|
||||
mount ::API::Terraform::State
|
||||
mount ::API::Terraform::StateVersion
|
||||
mount ::API::Terraform::Modules::V1::Packages
|
||||
mount ::API::PersonalAccessTokens
|
||||
mount ::API::ProtectedBranches
|
||||
mount ::API::ProtectedTags
|
||||
|
|
11
lib/api/entities/terraform/module_versions.rb
Normal file
11
lib/api/entities/terraform/module_versions.rb
Normal file
|
@ -0,0 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
module Entities
|
||||
module Terraform
|
||||
class ModuleVersions < Grape::Entity
|
||||
expose :modules
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
200
lib/api/terraform/modules/v1/packages.rb
Normal file
200
lib/api/terraform/modules/v1/packages.rb
Normal file
|
@ -0,0 +1,200 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
module Terraform
|
||||
module Modules
|
||||
module V1
|
||||
class Packages < ::API::Base
|
||||
include ::API::Helpers::Authentication
|
||||
helpers ::API::Helpers::PackagesHelpers
|
||||
helpers ::API::Helpers::Packages::BasicAuthHelpers
|
||||
|
||||
SEMVER_REGEX = Gitlab::Regex.semver_regex
|
||||
|
||||
TERRAFORM_MODULE_REQUIREMENTS = {
|
||||
module_namespace: API::NO_SLASH_URL_PART_REGEX,
|
||||
module_name: API::NO_SLASH_URL_PART_REGEX,
|
||||
module_system: API::NO_SLASH_URL_PART_REGEX
|
||||
}.freeze
|
||||
|
||||
TERRAFORM_MODULE_VERSION_REQUIREMENTS = {
|
||||
module_version: SEMVER_REGEX
|
||||
}.freeze
|
||||
|
||||
feature_category :package_registry
|
||||
|
||||
after_validation do
|
||||
require_packages_enabled!
|
||||
end
|
||||
|
||||
helpers do
|
||||
params :module_name do
|
||||
requires :module_name, type: String, desc: "", regexp: API::NO_SLASH_URL_PART_REGEX
|
||||
requires :module_system, type: String, regexp: API::NO_SLASH_URL_PART_REGEX
|
||||
end
|
||||
|
||||
params :module_version do
|
||||
requires :module_version, type: String, desc: 'Module version', regexp: SEMVER_REGEX
|
||||
end
|
||||
|
||||
def module_namespace
|
||||
strong_memoize(:module_namespace) do
|
||||
find_namespace(params[:module_namespace])
|
||||
end
|
||||
end
|
||||
|
||||
def finder_params
|
||||
{
|
||||
package_type: :terraform_module,
|
||||
package_name: "#{params[:module_name]}/#{params[:module_system]}"
|
||||
}.tap do |finder_params|
|
||||
finder_params[:package_version] = params[:module_version] if params.has_key?(:module_version)
|
||||
end
|
||||
end
|
||||
|
||||
def packages
|
||||
strong_memoize(:packages) do
|
||||
::Packages::GroupPackagesFinder.new(
|
||||
current_user,
|
||||
module_namespace,
|
||||
finder_params
|
||||
).execute
|
||||
end
|
||||
end
|
||||
|
||||
def package
|
||||
strong_memoize(:package) do
|
||||
packages.first
|
||||
end
|
||||
end
|
||||
|
||||
def package_file
|
||||
strong_memoize(:package_file) do
|
||||
package.package_files.first
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
params do
|
||||
requires :module_namespace, type: String, desc: "Group's ID or slug", regexp: API::NO_SLASH_URL_PART_REGEX
|
||||
includes :module_name
|
||||
end
|
||||
|
||||
namespace 'packages/terraform/modules/v1/:module_namespace/:module_name/:module_system', requirements: TERRAFORM_MODULE_REQUIREMENTS do
|
||||
authenticate_with do |accept|
|
||||
accept.token_types(:personal_access_token, :deploy_token, :job_token)
|
||||
.sent_through(:http_bearer_token)
|
||||
end
|
||||
|
||||
after_validation do
|
||||
authorize_read_package!(package || module_namespace)
|
||||
end
|
||||
|
||||
get 'versions' do
|
||||
presenter = ::Terraform::ModulesPresenter.new(packages, params[:module_system])
|
||||
present presenter, with: ::API::Entities::Terraform::ModuleVersions
|
||||
end
|
||||
|
||||
params do
|
||||
includes :module_version
|
||||
end
|
||||
|
||||
namespace '*module_version', requirements: TERRAFORM_MODULE_VERSION_REQUIREMENTS do
|
||||
after_validation do
|
||||
not_found! unless package && package_file
|
||||
end
|
||||
|
||||
get 'download' do
|
||||
module_file_path = api_v4_packages_terraform_modules_v1_module_version_file_path(
|
||||
module_namespace: params[:module_namespace],
|
||||
module_name: params[:module_name],
|
||||
module_system: params[:module_system],
|
||||
module_version: params[:module_version]
|
||||
)
|
||||
|
||||
jwt_token = Gitlab::TerraformRegistryToken.from_token(token_from_namespace_inheritable).encoded
|
||||
|
||||
header 'X-Terraform-Get', module_file_path.sub(%r{module_version/file$}, "#{params[:module_version]}/file?token=#{jwt_token}&archive=tgz")
|
||||
status :no_content
|
||||
end
|
||||
|
||||
namespace 'file' do
|
||||
authenticate_with do |accept|
|
||||
accept.token_types(:deploy_token_from_jwt, :job_token_from_jwt, :personal_access_token_from_jwt).sent_through(:token_param)
|
||||
end
|
||||
|
||||
get do
|
||||
track_package_event('pull_package', :terraform_module)
|
||||
|
||||
present_carrierwave_file!(package_file.file)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
params do
|
||||
requires :id, type: String, desc: 'The ID or full path of a project'
|
||||
includes :module_name
|
||||
includes :module_version
|
||||
end
|
||||
|
||||
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
|
||||
namespace ':id/packages/terraform/modules/:module_name/:module_system/*module_version/file' do
|
||||
authenticate_with do |accept|
|
||||
accept.token_types(:deploy_token).sent_through(:http_deploy_token_header)
|
||||
accept.token_types(:job_token).sent_through(:http_job_token_header)
|
||||
accept.token_types(:personal_access_token).sent_through(:http_private_token_header)
|
||||
end
|
||||
|
||||
desc 'Workhorse authorize Terraform Module package file' do
|
||||
detail 'This feature was introduced in GitLab 13.11'
|
||||
end
|
||||
|
||||
put 'authorize' do
|
||||
authorize_workhorse!(
|
||||
subject: authorized_user_project,
|
||||
maximum_size: authorized_user_project.actual_limits.terraform_module_max_file_size
|
||||
)
|
||||
end
|
||||
|
||||
desc 'Upload Terraform Module package file' do
|
||||
detail 'This feature was introduced in GitLab 13.11'
|
||||
end
|
||||
|
||||
params do
|
||||
requires :file, type: ::API::Validations::Types::WorkhorseFile, desc: 'The package file to be published (generated by Multipart middleware)'
|
||||
end
|
||||
|
||||
put do
|
||||
authorize_upload!(authorized_user_project)
|
||||
bad_request!('File is too large') if authorized_user_project.actual_limits.exceeded?(:terraform_module_max_file_size, params[:file].size)
|
||||
|
||||
create_package_file_params = {
|
||||
module_name: params['module_name'],
|
||||
module_system: params['module_system'],
|
||||
module_version: params['module_version'],
|
||||
file: params['file'],
|
||||
build: current_authenticated_job
|
||||
}
|
||||
|
||||
result = ::Packages::TerraformModule::CreatePackageService
|
||||
.new(authorized_user_project, current_user, create_package_file_params)
|
||||
.execute
|
||||
|
||||
render_api_error!(result[:message], result[:http_status]) if result[:status] == :error
|
||||
|
||||
track_package_event('push_package', :terraform_module)
|
||||
|
||||
created!
|
||||
rescue ObjectStorage::RemoteStoreError => e
|
||||
Gitlab::ErrorTracking.track_exception(e, extra: { file_name: params[:file_name], project_id: authorized_user_project.id })
|
||||
|
||||
forbidden!
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -10,7 +10,17 @@ module Gitlab
|
|||
|
||||
attr_reader :location
|
||||
|
||||
validates :location, inclusion: { in: %i[http_basic_auth http_token token_param] }
|
||||
validates :location, inclusion: {
|
||||
in: %i[
|
||||
http_basic_auth
|
||||
http_token
|
||||
http_bearer_token
|
||||
http_deploy_token_header
|
||||
http_job_token_header
|
||||
http_private_token_header
|
||||
token_param
|
||||
]
|
||||
}
|
||||
|
||||
def initialize(location)
|
||||
@location = location
|
||||
|
@ -23,6 +33,14 @@ module Gitlab
|
|||
extract_from_http_basic_auth request
|
||||
when :http_token
|
||||
extract_from_http_token request
|
||||
when :http_bearer_token
|
||||
extract_from_http_bearer_token request
|
||||
when :http_deploy_token_header
|
||||
extract_from_http_deploy_token_header request
|
||||
when :http_job_token_header
|
||||
extract_from_http_job_token_header request
|
||||
when :http_private_token_header
|
||||
extract_from_http_private_token_header request
|
||||
when :token_param
|
||||
extract_from_token_param request
|
||||
end
|
||||
|
@ -44,6 +62,34 @@ module Gitlab
|
|||
UsernameAndPassword.new(nil, password)
|
||||
end
|
||||
|
||||
def extract_from_http_bearer_token(request)
|
||||
password = request.headers['Authorization']
|
||||
return unless password.present?
|
||||
|
||||
UsernameAndPassword.new(nil, password.split(' ').last)
|
||||
end
|
||||
|
||||
def extract_from_http_deploy_token_header(request)
|
||||
password = request.headers['Deploy-Token']
|
||||
return unless password.present?
|
||||
|
||||
UsernameAndPassword.new(nil, password)
|
||||
end
|
||||
|
||||
def extract_from_http_job_token_header(request)
|
||||
password = request.headers['Job-Token']
|
||||
return unless password.present?
|
||||
|
||||
UsernameAndPassword.new(nil, password)
|
||||
end
|
||||
|
||||
def extract_from_http_private_token_header(request)
|
||||
password = request.headers['Private-Token']
|
||||
return unless password.present?
|
||||
|
||||
UsernameAndPassword.new(nil, password)
|
||||
end
|
||||
|
||||
def extract_from_token_param(request)
|
||||
password = request.query_parameters['token']
|
||||
return unless password.present?
|
||||
|
|
19
lib/gitlab/email/message/in_product_marketing.rb
Normal file
19
lib/gitlab/email/message/in_product_marketing.rb
Normal file
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Email
|
||||
module Message
|
||||
module InProductMarketing
|
||||
UnknownTrackError = Class.new(StandardError)
|
||||
|
||||
TRACKS = [:create, :verify, :team, :trial].freeze
|
||||
|
||||
def self.for(track)
|
||||
raise UnknownTrackError unless TRACKS.include?(track)
|
||||
|
||||
"Gitlab::Email::Message::InProductMarketing::#{track.to_s.classify}".constantize
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
154
lib/gitlab/email/message/in_product_marketing/base.rb
Normal file
154
lib/gitlab/email/message/in_product_marketing/base.rb
Normal file
|
@ -0,0 +1,154 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Email
|
||||
module Message
|
||||
module InProductMarketing
|
||||
class Base
|
||||
include Gitlab::Email::Message::InProductMarketing::Helper
|
||||
include Gitlab::Routing
|
||||
|
||||
attr_accessor :format
|
||||
|
||||
def initialize(group:, series:, format: :html)
|
||||
raise ArgumentError, "Only #{total_series} series available for this track." unless series.between?(0, total_series - 1)
|
||||
|
||||
@group = group
|
||||
@series = series
|
||||
@format = format
|
||||
end
|
||||
|
||||
def subject_line
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def tagline
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def title
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def subtitle
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def body_line1
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def body_line2
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def cta_text
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def cta_link
|
||||
case format
|
||||
when :html
|
||||
link_to cta_text, group_email_campaigns_url(group, track: track, series: series), target: '_blank', rel: 'noopener noreferrer'
|
||||
else
|
||||
[cta_text, group_email_campaigns_url(group, track: track, series: series)].join(' >> ')
|
||||
end
|
||||
end
|
||||
|
||||
def unsubscribe
|
||||
parts = Gitlab.com? ? unsubscribe_com : unsubscribe_self_managed(track, series)
|
||||
|
||||
case format
|
||||
when :html
|
||||
parts.join(' ')
|
||||
else
|
||||
parts.join("\n" + ' ' * 16)
|
||||
end
|
||||
end
|
||||
|
||||
def progress
|
||||
if Gitlab.com?
|
||||
s_('InProductMarketing|This is email %{current_series} of %{total_series} in the %{track} series.') % { current_series: series + 1, total_series: total_series, track: track.to_s.humanize }
|
||||
else
|
||||
s_('InProductMarketing|This is email %{current_series} of %{total_series} in the %{track} series. To disable notification emails sent by your local GitLab instance, either contact your administrator or %{unsubscribe_link}.') % { current_series: series + 1, total_series: total_series, track: track.to_s.humanize, unsubscribe_link: unsubscribe_link }
|
||||
end
|
||||
end
|
||||
|
||||
def address
|
||||
s_('InProductMarketing|%{strong_start}GitLab Inc.%{strong_end} 268 Bush Street, #350, San Francisco, CA 94104, USA').html_safe % strong_options
|
||||
end
|
||||
|
||||
def footer_links
|
||||
links = [
|
||||
[s_('InProductMarketing|Blog'), 'https://about.gitlab.com/blog'],
|
||||
[s_('InProductMarketing|Twitter'), 'https://twitter.com/gitlab'],
|
||||
[s_('InProductMarketing|Facebook'), 'https://www.facebook.com/gitlab'],
|
||||
[s_('InProductMarketing|YouTube'), 'https://www.youtube.com/channel/UCnMGQ8QHMAnVIsI3xJrihhg']
|
||||
]
|
||||
case format
|
||||
when :html
|
||||
links.map do |text, link|
|
||||
link_to(text, link)
|
||||
end
|
||||
else
|
||||
'| ' + links.map do |text, link|
|
||||
[text, link].join(' ')
|
||||
end.join("\n| ")
|
||||
end
|
||||
end
|
||||
|
||||
def logo_path
|
||||
["mailers/in_product_marketing", "#{track}-#{series}.png"].join('/')
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
attr_reader :group, :series
|
||||
|
||||
def total_series
|
||||
3
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def track
|
||||
self.class.name.demodulize.downcase.to_sym
|
||||
end
|
||||
|
||||
def unsubscribe_com
|
||||
[
|
||||
s_('InProductMarketing|If you no longer wish to receive marketing emails from us,'),
|
||||
s_('InProductMarketing|you may %{unsubscribe_link} at any time.') % { unsubscribe_link: unsubscribe_link }
|
||||
]
|
||||
end
|
||||
|
||||
def unsubscribe_self_managed(track, series)
|
||||
[
|
||||
s_('InProductMarketing|To opt out of these onboarding emails, %{unsubscribe_link}.') % { unsubscribe_link: unsubscribe_link },
|
||||
s_("InProductMarketing|If you don't want to receive marketing emails directly from GitLab, %{marketing_preference_link}.") % { marketing_preference_link: marketing_preference_link(track, series) }
|
||||
]
|
||||
end
|
||||
|
||||
def unsubscribe_link
|
||||
unsubscribe_url = Gitlab.com? ? '%tag_unsubscribe_url%' : profile_notifications_url
|
||||
|
||||
link(s_('InProductMarketing|unsubscribe'), unsubscribe_url)
|
||||
end
|
||||
|
||||
def marketing_preference_link(track, series)
|
||||
params = {
|
||||
utm_source: 'SM',
|
||||
utm_medium: 'email',
|
||||
utm_campaign: 'onboarding',
|
||||
utm_term: "#{track}_#{series}"
|
||||
}
|
||||
|
||||
preference_link = "https://about.gitlab.com/company/preference-center/?#{params.to_query}"
|
||||
|
||||
link(s_('InProductMarketing|update your preferences'), preference_link)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
101
lib/gitlab/email/message/in_product_marketing/create.rb
Normal file
101
lib/gitlab/email/message/in_product_marketing/create.rb
Normal file
|
@ -0,0 +1,101 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Email
|
||||
module Message
|
||||
module InProductMarketing
|
||||
class Create < Base
|
||||
def subject_line
|
||||
[
|
||||
s_('InProductMarketing|Create a project in GitLab in 5 minutes'),
|
||||
s_('InProductMarketing|Import your project and code from GitHub, Bitbucket and others'),
|
||||
s_('InProductMarketing|Understand repository mirroring')
|
||||
][series]
|
||||
end
|
||||
|
||||
def tagline
|
||||
[
|
||||
s_('InProductMarketing|Get started today'),
|
||||
s_('InProductMarketing|Get our import guides'),
|
||||
s_('InProductMarketing|Need an alternative to importing?')
|
||||
][series]
|
||||
end
|
||||
|
||||
def title
|
||||
[
|
||||
s_('InProductMarketing|Take your first steps with GitLab'),
|
||||
s_('InProductMarketing|Start by importing your projects'),
|
||||
s_('InProductMarketing|How (and why) mirroring makes sense')
|
||||
][series]
|
||||
end
|
||||
|
||||
def subtitle
|
||||
[
|
||||
s_('InProductMarketing|Dig in and create a project and a repo'),
|
||||
s_("InProductMarketing|Here's what you need to know"),
|
||||
s_('InProductMarketing|Try it out')
|
||||
][series]
|
||||
end
|
||||
|
||||
def body_line1
|
||||
[
|
||||
s_("InProductMarketing|To understand and get the most out of GitLab, start at the beginning and %{project_link}. In GitLab, repositories are part of a project, so after you've created your project you can go ahead and %{repo_link}.") % { project_link: project_link, repo_link: repo_link },
|
||||
s_("InProductMarketing|Making the switch? It's easier than you think to import your projects into GitLab. Move %{github_link}, or import something %{bitbucket_link}.") % { github_link: github_link, bitbucket_link: bitbucket_link },
|
||||
s_("InProductMarketing|Sometimes you're not ready to make a full transition to a new tool. If you're not ready to fully commit, %{mirroring_link} gives you a safe way to try out GitLab in parallel with your current tool.") % { mirroring_link: mirroring_link }
|
||||
][series]
|
||||
end
|
||||
|
||||
def body_line2
|
||||
[
|
||||
s_("InProductMarketing|That's all it takes to get going with GitLab, but if you're new to working with Git, check out our %{basics_link} for helpful tips and tricks for getting started.") % { basics_link: basics_link },
|
||||
s_("InProductMarketing|Have a different instance you'd like to import? Here's our %{import_link}.") % { import_link: import_link },
|
||||
s_("InProductMarketing|It's also possible to simply %{external_repo_link} in order to take advantage of GitLab's CI/CD.") % { external_repo_link: external_repo_link }
|
||||
][series]
|
||||
end
|
||||
|
||||
def cta_text
|
||||
[
|
||||
s_('InProductMarketing|Create your first project!'),
|
||||
s_('InProductMarketing|Master the art of importing!'),
|
||||
s_('InProductMarketing|Understand your project options')
|
||||
][series]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def project_link
|
||||
link(s_('InProductMarketing|create a project'), help_page_url('gitlab-basics/create-project'))
|
||||
end
|
||||
|
||||
def repo_link
|
||||
link(s_('InProductMarketing|set up a repo'), help_page_url('user/project/repository/index', anchor: 'create-a-repository'))
|
||||
end
|
||||
|
||||
def github_link
|
||||
link(s_('InProductMarketing|GitHub Enterprise projects to GitLab'), help_page_url('integration/github'))
|
||||
end
|
||||
|
||||
def bitbucket_link
|
||||
link(s_('InProductMarketing|from Bitbucket'), help_page_url('user/project/import/bitbucket_server'))
|
||||
end
|
||||
|
||||
def mirroring_link
|
||||
link(s_('InProductMarketing|repository mirroring'), help_page_url('user/project/repository/repository_mirroring'))
|
||||
end
|
||||
|
||||
def basics_link
|
||||
link(s_('InProductMarketing|Git basics'), help_page_url('gitlab-basics/README'))
|
||||
end
|
||||
|
||||
def import_link
|
||||
link(s_('InProductMarketing|comprehensive guide'), help_page_url('user/project/import/index'))
|
||||
end
|
||||
|
||||
def external_repo_link
|
||||
link(s_('InProductMarketing|connect an external repository'), new_project_url(anchor: 'cicd_for_external_repo'))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
44
lib/gitlab/email/message/in_product_marketing/helper.rb
Normal file
44
lib/gitlab/email/message/in_product_marketing/helper.rb
Normal file
|
@ -0,0 +1,44 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Email
|
||||
module Message
|
||||
module InProductMarketing
|
||||
module Helper
|
||||
include ActionView::Context
|
||||
include ActionView::Helpers::TagHelper
|
||||
include ActionView::Helpers::UrlHelper
|
||||
|
||||
private
|
||||
|
||||
def list(array)
|
||||
case format
|
||||
when :html
|
||||
tag.ul { array.map { |item| tag.li item} }
|
||||
else
|
||||
'- ' + array.join("\n- ")
|
||||
end
|
||||
end
|
||||
|
||||
def strong_options
|
||||
case format
|
||||
when :html
|
||||
{ strong_start: '<b>'.html_safe, strong_end: '</b>'.html_safe }
|
||||
else
|
||||
{ strong_start: '', strong_end: '' }
|
||||
end
|
||||
end
|
||||
|
||||
def link(text, link)
|
||||
case format
|
||||
when :html
|
||||
link_to text, link
|
||||
else
|
||||
"#{text} (#{link})"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
80
lib/gitlab/email/message/in_product_marketing/team.rb
Normal file
80
lib/gitlab/email/message/in_product_marketing/team.rb
Normal file
|
@ -0,0 +1,80 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Email
|
||||
module Message
|
||||
module InProductMarketing
|
||||
class Team < Base
|
||||
def subject_line
|
||||
[
|
||||
s_('InProductMarketing|Working in GitLab = more efficient'),
|
||||
s_("InProductMarketing|Multiple owners, confusing workstreams? We've got you covered"),
|
||||
s_('InProductMarketing|Your teams can be more efficient')
|
||||
][series]
|
||||
end
|
||||
|
||||
def tagline
|
||||
[
|
||||
s_('InProductMarketing|Invite your colleagues to join in less than one minute'),
|
||||
s_('InProductMarketing|Get your team set up on GitLab'),
|
||||
nil
|
||||
][series]
|
||||
end
|
||||
|
||||
def title
|
||||
[
|
||||
s_('InProductMarketing|Team work makes the dream work'),
|
||||
s_('InProductMarketing|*GitLab*, noun: a synonym for efficient teams'),
|
||||
s_('InProductMarketing|Find out how your teams are really doing')
|
||||
][series]
|
||||
end
|
||||
|
||||
def subtitle
|
||||
[
|
||||
s_('InProductMarketing|Actually, GitLab makes the team work (better)'),
|
||||
s_('InProductMarketing|Our tool brings all the things together'),
|
||||
s_("InProductMarketing|It's all in the stats")
|
||||
][series]
|
||||
end
|
||||
|
||||
def body_line1
|
||||
[
|
||||
[
|
||||
s_('InProductMarketing|Did you know teams that use GitLab are far more efficient?'),
|
||||
list([
|
||||
s_('InProductMarketing|Goldman Sachs went from 1 build every two weeks to thousands of builds a day'),
|
||||
s_('InProductMarketing|Ticketmaster decreased their CI build time by 15X')
|
||||
])
|
||||
].join("\n"),
|
||||
s_("InProductMarketing|We know a thing or two about efficiency and we don't want to keep that to ourselves. Sign up for a free trial of GitLab Ultimate and your teams will be on it from day one."),
|
||||
[
|
||||
s_('InProductMarketing|Stop wondering and use GitLab to answer questions like:'),
|
||||
list([
|
||||
s_('InProductMarketing|How long does it take us to close issues/MRs by types like feature requests, bugs, tech debt, security?'),
|
||||
s_('InProductMarketing|How many days does it take our team to complete various tasks?'),
|
||||
s_('InProductMarketing|What does our value stream timeline look like from product to development to review and production?')
|
||||
])
|
||||
].join("\n")
|
||||
][series]
|
||||
end
|
||||
|
||||
def body_line2
|
||||
[
|
||||
s_('InProductMarketing|Invite your colleagues and start shipping code faster.'),
|
||||
s_("InProductMarketing|Streamline code review, know at a glance who's unavailable, communicate in comments or in email and integrate with Slack so everyone's on the same page."),
|
||||
s_('InProductMarketing|When your team is on GitLab these answers are a click away.')
|
||||
][series]
|
||||
end
|
||||
|
||||
def cta_text
|
||||
[
|
||||
s_('InProductMarketing|Invite your colleagues today'),
|
||||
s_('InProductMarketing|Invite your team in less than 60 seconds'),
|
||||
s_('InProductMarketing|Invite your team now')
|
||||
][series]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
75
lib/gitlab/email/message/in_product_marketing/trial.rb
Normal file
75
lib/gitlab/email/message/in_product_marketing/trial.rb
Normal file
|
@ -0,0 +1,75 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Email
|
||||
module Message
|
||||
module InProductMarketing
|
||||
class Trial < Base
|
||||
def subject_line
|
||||
[
|
||||
s_('InProductMarketing|Go farther with GitLab'),
|
||||
s_('InProductMarketing|Automated security scans directly within GitLab'),
|
||||
s_('InProductMarketing|Take your source code management to the next level')
|
||||
][series]
|
||||
end
|
||||
|
||||
def tagline
|
||||
[
|
||||
s_('InProductMarketing|Start a free trial of GitLab Ultimate – no CC required'),
|
||||
s_('InProductMarketing|Improve app security with a 30-day trial'),
|
||||
s_('InProductMarketing|Start with a GitLab Ultimate free trial')
|
||||
][series]
|
||||
end
|
||||
|
||||
def title
|
||||
[
|
||||
s_('InProductMarketing|Give us one minute...'),
|
||||
s_("InProductMarketing|Security that's integrated into your development lifecycle"),
|
||||
s_('InProductMarketing|Improve code quality and streamline reviews')
|
||||
][series]
|
||||
end
|
||||
|
||||
def subtitle
|
||||
[
|
||||
s_('InProductMarketing|...and you can get a free trial of GitLab Ultimate'),
|
||||
s_('InProductMarketing|Try GitLab Ultimate for free'),
|
||||
s_('InProductMarketing|Better code in less time')
|
||||
][series]
|
||||
end
|
||||
|
||||
def body_line1
|
||||
[
|
||||
[
|
||||
s_("InProductMarketing|GitLab's premium tiers are designed to make you, your team and your application more efficient and more secure with features including but not limited to:"),
|
||||
list([
|
||||
s_('InProductMarketing|%{strong_start}Company wide portfolio management%{strong_end} — including multi-level epics, scoped labels').html_safe % strong_options,
|
||||
s_('InProductMarketing|%{strong_start}Multiple approval roles%{strong_end} — including code owners and required merge approvals').html_safe % strong_options,
|
||||
s_('InProductMarketing|%{strong_start}Advanced application security%{strong_end} — including SAST, DAST scanning, FUZZ testing, dependency scanning, license compliance, secrete detection').html_safe % strong_options,
|
||||
s_('InProductMarketing|%{strong_start}Executive level insights%{strong_end} — including reporting on productivity, tasks by type, days to completion, value stream').html_safe % strong_options
|
||||
])
|
||||
].join("\n"),
|
||||
s_('InProductMarketing|GitLab provides static application security testing (SAST), dynamic application security testing (DAST), container scanning, and dependency scanning to help you deliver secure applications along with license compliance.'),
|
||||
s_('InProductMarketing|By enabling code owners and required merge approvals the right person will review the right MR. This is a win-win: cleaner code and a more efficient review process.')
|
||||
][series]
|
||||
end
|
||||
|
||||
def body_line2
|
||||
[
|
||||
s_('InProductMarketing|Start a GitLab Ultimate trial today in less than one minute, no credit card required.'),
|
||||
s_('InProductMarketing|Get started today with a 30-day GitLab Ultimate trial, no credit card required.'),
|
||||
s_('InProductMarketing|Code owners and required merge approvals are part of the paid tiers of GitLab. You can start a free 30-day trial of GitLab Ultimate and enable these features in less than 5 minutes with no credit card required.')
|
||||
][series]
|
||||
end
|
||||
|
||||
def cta_text
|
||||
[
|
||||
s_('InProductMarketing|Start a trial'),
|
||||
s_('InProductMarketing|Beef up your security'),
|
||||
s_('InProductMarketing|Start your trial now!')
|
||||
][series]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
93
lib/gitlab/email/message/in_product_marketing/verify.rb
Normal file
93
lib/gitlab/email/message/in_product_marketing/verify.rb
Normal file
|
@ -0,0 +1,93 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Email
|
||||
module Message
|
||||
module InProductMarketing
|
||||
class Verify < Base
|
||||
def subject_line
|
||||
[
|
||||
s_('InProductMarketing|Feel the need for speed?'),
|
||||
s_('InProductMarketing|3 ways to dive into GitLab CI/CD'),
|
||||
s_('InProductMarketing|Explore the power of GitLab CI/CD')
|
||||
][series]
|
||||
end
|
||||
|
||||
def tagline
|
||||
[
|
||||
s_('InProductMarketing|Use GitLab CI/CD'),
|
||||
s_('InProductMarketing|Test, create, deploy'),
|
||||
s_('InProductMarketing|Are your runners ready?')
|
||||
][series]
|
||||
end
|
||||
|
||||
def title
|
||||
[
|
||||
s_('InProductMarketing|Rapid development, simplified'),
|
||||
s_('InProductMarketing|Get started with GitLab CI/CD'),
|
||||
s_('InProductMarketing|Launch GitLab CI/CD in 20 minutes or less')
|
||||
][series]
|
||||
end
|
||||
|
||||
def subtitle
|
||||
[
|
||||
s_('InProductMarketing|How to build and test faster'),
|
||||
s_('InProductMarketing|Explore the options'),
|
||||
s_('InProductMarketing|Follow our steps')
|
||||
][series]
|
||||
end
|
||||
|
||||
def body_line1
|
||||
[
|
||||
s_("InProductMarketing|Tired of wrestling with disparate tool chains, information silos and inefficient processes? GitLab's CI/CD is built on a DevOps platform with source code management, planning, monitoring and more ready to go. Find out %{ci_link}.") % { ci_link: ci_link },
|
||||
s_("InProductMarketing|GitLab's CI/CD makes software development easier. Don't believe us? Here are three ways you can take it for a fast (and satisfying) test drive:"),
|
||||
s_("InProductMarketing|Get going with CI/CD quickly using our %{quick_start_link}. Start with an available runner and then create a CI .yml file – it's really that easy.") % { quick_start_link: quick_start_link }
|
||||
][series]
|
||||
end
|
||||
|
||||
def body_line2
|
||||
[
|
||||
nil,
|
||||
list([
|
||||
s_('InProductMarketing|Start by %{performance_link}').html_safe % { performance_link: performance_link },
|
||||
s_('InProductMarketing|Move on to easily creating a Pages website %{ci_template_link}').html_safe % { ci_template_link: ci_template_link },
|
||||
s_('InProductMarketing|And finally %{deploy_link} a Python application.').html_safe % { deploy_link: deploy_link }
|
||||
]),
|
||||
nil
|
||||
][series]
|
||||
end
|
||||
|
||||
def cta_text
|
||||
[
|
||||
s_('InProductMarketing|Get to know GitLab CI/CD'),
|
||||
s_('InProductMarketing|Try it yourself'),
|
||||
s_('InProductMarketing|Explore GitLab CI/CD')
|
||||
][series]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def ci_link
|
||||
link(s_('InProductMarketing|how easy it is to get started'), help_page_url('ci/README'))
|
||||
end
|
||||
|
||||
def quick_start_link
|
||||
link(s_('InProductMarketing|quick start guide'), help_page_url('ci/quick_start/README'))
|
||||
end
|
||||
|
||||
def performance_link
|
||||
link(s_('InProductMarketing|testing browser performance'), help_page_url('user/project/merge_requests/browser_performance_testing'))
|
||||
end
|
||||
|
||||
def ci_template_link
|
||||
link(s_('InProductMarketing|using a CI/CD template'), help_page_url('user/project/pages/getting_started/pages_ci_cd_template'))
|
||||
end
|
||||
|
||||
def deploy_link
|
||||
link(s_('InProductMarketing|test and deploy'), help_page_url('ci/examples/test-and-deploy-python-application-to-heroku'))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -77,6 +77,10 @@ module Gitlab
|
|||
/x.freeze
|
||||
end
|
||||
|
||||
def terraform_module_package_name_regex
|
||||
@terraform_module_package_name_regex ||= %r{\A[-a-z0-9]+\/[-a-z0-9]+\z}.freeze
|
||||
end
|
||||
|
||||
def pypi_version_regex
|
||||
# See the official regex: https://github.com/pypa/packaging/blob/16.7/packaging/version.py#L159
|
||||
|
||||
|
@ -149,7 +153,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
def semver_regex
|
||||
@semver_regex ||= Regexp.new("\\A#{::Gitlab::Regex.unbounded_semver_regex.source}\\z", ::Gitlab::Regex.unbounded_semver_regex.options)
|
||||
@semver_regex ||= Regexp.new("\\A#{::Gitlab::Regex.unbounded_semver_regex.source}\\z", ::Gitlab::Regex.unbounded_semver_regex.options).freeze
|
||||
end
|
||||
|
||||
# These partial semver regexes are intended for use in composing other
|
||||
|
|
|
@ -47,3 +47,6 @@
|
|||
- i_package_tag_delete_package
|
||||
- i_package_tag_pull_package
|
||||
- i_package_tag_push_package
|
||||
- i_package_terraform_module_delete_package
|
||||
- i_package_terraform_module_pull_package
|
||||
- i_package_terraform_module_push_package
|
||||
|
|
|
@ -95,3 +95,11 @@
|
|||
category: user_packages
|
||||
aggregation: weekly
|
||||
redis_slot: package
|
||||
- name: i_package_terraform_module_deploy_token
|
||||
category: deploy_token_packages
|
||||
aggregation: weekly
|
||||
redis_slot: package
|
||||
- name: i_package_terraform_module_user
|
||||
category: user_packages
|
||||
aggregation: weekly
|
||||
redis_slot: package
|
||||
|
|
|
@ -17103,10 +17103,10 @@ msgstr ""
|
|||
msgid "InProductMarketing|That's all it takes to get going with GitLab, but if you're new to working with Git, check out our %{basics_link} for helpful tips and tricks for getting started."
|
||||
msgstr ""
|
||||
|
||||
msgid "InProductMarketing|This is email %{series} of 3 in the %{track} series."
|
||||
msgid "InProductMarketing|This is email %{current_series} of %{total_series} in the %{track} series."
|
||||
msgstr ""
|
||||
|
||||
msgid "InProductMarketing|This is email %{series} of 3 in the %{track} series. To disable notification emails sent by your local GitLab instance, either contact your administrator or %{unsubscribe_link}."
|
||||
msgid "InProductMarketing|This is email %{current_series} of %{total_series} in the %{track} series. To disable notification emails sent by your local GitLab instance, either contact your administrator or %{unsubscribe_link}."
|
||||
msgstr ""
|
||||
|
||||
msgid "InProductMarketing|Ticketmaster decreased their CI build time by 15X"
|
||||
|
@ -20184,6 +20184,9 @@ msgstr ""
|
|||
msgid "Maximum PyPI package file size in bytes"
|
||||
msgstr ""
|
||||
|
||||
msgid "Maximum Terraform Module package file size in bytes"
|
||||
msgstr ""
|
||||
|
||||
msgid "Maximum Users"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ module QA
|
|||
|
||||
def click_user(username)
|
||||
within_element(:user_row_content, text: username) do
|
||||
click_element(:username_link)
|
||||
click_link(username)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module QA
|
||||
RSpec.describe "Manage", :requires_admin do
|
||||
describe "Group bulk import" do
|
||||
RSpec.describe 'Manage', :requires_admin do
|
||||
describe 'Group bulk import' do
|
||||
let!(:api_client) { Runtime::API::Client.as_admin }
|
||||
let!(:user) do
|
||||
Resource::User.fabricate_via_api! do |usr|
|
||||
|
@ -52,8 +52,13 @@ module QA
|
|||
)
|
||||
end
|
||||
|
||||
def staging?
|
||||
Runtime::Scenario.gitlab_address.include?('staging.gitlab.com')
|
||||
end
|
||||
|
||||
before(:all) do
|
||||
Runtime::Feature.enable(:bulk_import)
|
||||
Runtime::Feature.enable(:top_level_group_creation_enabled) if staging?
|
||||
end
|
||||
|
||||
before do
|
||||
|
@ -66,10 +71,10 @@ module QA
|
|||
end
|
||||
|
||||
it(
|
||||
"performs bulk group import from another gitlab instance",
|
||||
testcase: "https://gitlab.com/gitlab-org/quality/testcases/-/issues/1785",
|
||||
'performs bulk group import from another gitlab instance',
|
||||
testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1785',
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/330344
|
||||
exclude: { job: ["ce:relative_url", "ee:relative_url"] }
|
||||
exclude: { job: ['ce:relative_url', 'ee:relative_url'] }
|
||||
) do
|
||||
Page::Group::BulkImport.perform do |import_page|
|
||||
import_page.import_group(source_group.path, sandbox.path)
|
||||
|
@ -88,6 +93,7 @@ module QA
|
|||
|
||||
after(:all) do
|
||||
Runtime::Feature.disable(:bulk_import)
|
||||
Runtime::Feature.disable(:top_level_group_creation_enabled) if staging?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -138,7 +138,7 @@ module QA
|
|||
Page::Admin::Overview::Users::Index.perform do |index|
|
||||
index.click_pending_approval_tab
|
||||
index.search_user(user.username)
|
||||
index.click_user(user.username)
|
||||
index.click_user(user.name)
|
||||
end
|
||||
|
||||
Page::Admin::Overview::Users::Show.perform do |show|
|
||||
|
|
|
@ -49,7 +49,7 @@ module QA
|
|||
|
||||
Page::File::Show.perform(&:click_edit)
|
||||
|
||||
expect(page).to have_text("You're not allowed to edit files in this project directly.")
|
||||
expect(page).to have_text("You can’t edit files directly in this project.")
|
||||
end
|
||||
|
||||
after do
|
||||
|
|
|
@ -129,6 +129,25 @@ FactoryBot.define do
|
|||
end
|
||||
end
|
||||
|
||||
factory :terraform_module_package do
|
||||
sequence(:name) { |n| "module-#{n}/system" }
|
||||
version { '1.0.0' }
|
||||
package_type { :terraform_module }
|
||||
|
||||
after :create do |package|
|
||||
create :package_file, :terraform_module, package: package
|
||||
end
|
||||
|
||||
trait :with_build do
|
||||
after :create do |package|
|
||||
user = package.project.creator
|
||||
pipeline = create(:ci_pipeline, user: user)
|
||||
create(:ci_build, user: user, pipeline: pipeline)
|
||||
create :package_build_info, package: package, pipeline: pipeline
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
factory :nuget_package do
|
||||
sequence(:name) { |n| "NugetPackage#{n}"}
|
||||
sequence(:version) { |n| "1.0.#{n}" }
|
||||
|
|
|
@ -254,6 +254,13 @@ FactoryBot.define do
|
|||
size { 400.kilobytes }
|
||||
end
|
||||
|
||||
trait(:terraform_module) do
|
||||
file_fixture { 'spec/fixtures/packages/terraform_module/module-system-v1.0.0.tgz' }
|
||||
file_name { 'module-system-v1.0.0.tgz' }
|
||||
file_sha1 { 'abf850accb1947c0c0e3ef4b441b771bb5c9ae3c' }
|
||||
size { 806.bytes }
|
||||
end
|
||||
|
||||
trait(:nuget) do
|
||||
package
|
||||
file_fixture { 'spec/fixtures/packages/nuget/package.nupkg' }
|
||||
|
|
12
spec/fixtures/api/schemas/public_api/v4/packages/terraform/modules/v1/module.json
vendored
Normal file
12
spec/fixtures/api/schemas/public_api/v4/packages/terraform/modules/v1/module.json
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"type": "object",
|
||||
"required" : ["versions"],
|
||||
"optional" : ["source"],
|
||||
"properties" : {
|
||||
"source": { "type": "string" },
|
||||
"versions": {
|
||||
"minItems": 0,
|
||||
"items": { "$ref": "./version.json" }
|
||||
}
|
||||
}
|
||||
}
|
4
spec/fixtures/api/schemas/public_api/v4/packages/terraform/modules/v1/modules.json
vendored
Normal file
4
spec/fixtures/api/schemas/public_api/v4/packages/terraform/modules/v1/modules.json
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"type": "array",
|
||||
"items": { "$ref": "./module.json" }
|
||||
}
|
38
spec/fixtures/api/schemas/public_api/v4/packages/terraform/modules/v1/version.json
vendored
Normal file
38
spec/fixtures/api/schemas/public_api/v4/packages/terraform/modules/v1/version.json
vendored
Normal file
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"type": "object",
|
||||
"required": ["version", "submodules", "root"],
|
||||
"properties": {
|
||||
"version": {
|
||||
"type": "string"
|
||||
},
|
||||
"submodules": {
|
||||
"type": "array",
|
||||
"maxItems": 0
|
||||
},
|
||||
"root": {
|
||||
"type": "object",
|
||||
"required": ["dependencies", "providers"],
|
||||
"properties": {
|
||||
"dependencies": {
|
||||
"type": "array",
|
||||
"maxItems": 0
|
||||
},
|
||||
"providers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["name", "version"],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
9
spec/fixtures/api/schemas/public_api/v4/packages/terraform/modules/v1/versions.json
vendored
Normal file
9
spec/fixtures/api/schemas/public_api/v4/packages/terraform/modules/v1/versions.json
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"type": "object",
|
||||
"required" : ["modules"],
|
||||
"properties" : {
|
||||
"modules": {
|
||||
"items": { "$ref": "./module.json" }
|
||||
}
|
||||
}
|
||||
}
|
BIN
spec/fixtures/packages/terraform_module/module-system-v1.0.0.tgz
vendored
Normal file
BIN
spec/fixtures/packages/terraform_module/module-system-v1.0.0.tgz
vendored
Normal file
Binary file not shown.
|
@ -226,7 +226,7 @@ describe('Ci variable modal', () => {
|
|||
};
|
||||
createComponent(mount);
|
||||
store.state.variable = validMaskandKeyVariable;
|
||||
store.state.maskableRegex = /^[a-zA-Z0-9_+=/@:-]{8,}$/;
|
||||
store.state.maskableRegex = /^[a-zA-Z0-9_+=/@:.~-]{8,}$/;
|
||||
});
|
||||
|
||||
it('does not disable the submit button', () => {
|
||||
|
|
194
spec/frontend/members/components/members_tabs_spec.js
Normal file
194
spec/frontend/members/components/members_tabs_spec.js
Normal file
|
@ -0,0 +1,194 @@
|
|||
import Vue, { nextTick } from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import MembersApp from '~/members/components/app.vue';
|
||||
import MembersTabs from '~/members/components/members_tabs.vue';
|
||||
import { MEMBER_TYPES } from '~/members/constants';
|
||||
import { pagination } from '../mock_data';
|
||||
|
||||
describe('MembersApp', () => {
|
||||
Vue.use(Vuex);
|
||||
|
||||
let wrapper;
|
||||
|
||||
const createComponent = ({ totalItems = 10, options = {} } = {}) => {
|
||||
const store = new Vuex.Store({
|
||||
modules: {
|
||||
[MEMBER_TYPES.user]: {
|
||||
namespaced: true,
|
||||
state: {
|
||||
pagination: {
|
||||
...pagination,
|
||||
totalItems,
|
||||
},
|
||||
filteredSearchBar: {
|
||||
searchParam: 'search',
|
||||
},
|
||||
},
|
||||
},
|
||||
[MEMBER_TYPES.group]: {
|
||||
namespaced: true,
|
||||
state: {
|
||||
pagination: {
|
||||
...pagination,
|
||||
totalItems,
|
||||
paramName: 'groups_page',
|
||||
},
|
||||
filteredSearchBar: {
|
||||
searchParam: 'search_groups',
|
||||
},
|
||||
},
|
||||
},
|
||||
[MEMBER_TYPES.invite]: {
|
||||
namespaced: true,
|
||||
state: {
|
||||
pagination: {
|
||||
...pagination,
|
||||
totalItems,
|
||||
paramName: 'invited_page',
|
||||
},
|
||||
filteredSearchBar: {
|
||||
searchParam: 'search_invited',
|
||||
},
|
||||
},
|
||||
},
|
||||
[MEMBER_TYPES.accessRequest]: {
|
||||
namespaced: true,
|
||||
state: {
|
||||
pagination: {
|
||||
...pagination,
|
||||
totalItems,
|
||||
paramName: 'access_requests_page',
|
||||
},
|
||||
filteredSearchBar: {
|
||||
searchParam: 'search_access_requests',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
wrapper = mountExtended(MembersTabs, {
|
||||
store,
|
||||
stubs: ['members-app'],
|
||||
provide: {
|
||||
canManageMembers: true,
|
||||
},
|
||||
...options,
|
||||
});
|
||||
|
||||
return nextTick();
|
||||
};
|
||||
|
||||
const findTabs = () => wrapper.findAllByRole('tab').wrappers;
|
||||
const findTabByText = (text) => findTabs().find((tab) => tab.text().includes(text));
|
||||
const findActiveTab = () => wrapper.findByRole('tab', { selected: true });
|
||||
|
||||
beforeEach(() => {
|
||||
delete window.location;
|
||||
window.location = new URL('https://localhost');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('when tabs have a count', () => {
|
||||
it('renders tabs with count', async () => {
|
||||
await createComponent();
|
||||
|
||||
const tabs = findTabs();
|
||||
|
||||
expect(tabs[0].text()).toBe('Members 10');
|
||||
expect(tabs[1].text()).toBe('Groups 10');
|
||||
expect(tabs[2].text()).toBe('Invited 10');
|
||||
expect(tabs[3].text()).toBe('Access requests 10');
|
||||
expect(findActiveTab().text()).toContain('Members');
|
||||
});
|
||||
|
||||
it('renders `MembersApp` and passes `namespace` prop', async () => {
|
||||
await createComponent();
|
||||
|
||||
const membersApps = wrapper.findAllComponents(MembersApp).wrappers;
|
||||
|
||||
expect(membersApps[0].attributes('namespace')).toBe(MEMBER_TYPES.user);
|
||||
expect(membersApps[1].attributes('namespace')).toBe(MEMBER_TYPES.group);
|
||||
expect(membersApps[2].attributes('namespace')).toBe(MEMBER_TYPES.invite);
|
||||
expect(membersApps[3].attributes('namespace')).toBe(MEMBER_TYPES.accessRequest);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when tabs do not have a count', () => {
|
||||
it('only renders `Members` tab', async () => {
|
||||
await createComponent({ totalItems: 0 });
|
||||
|
||||
expect(findTabByText('Members')).not.toBeUndefined();
|
||||
expect(findTabByText('Groups')).toBeUndefined();
|
||||
expect(findTabByText('Invited')).toBeUndefined();
|
||||
expect(findTabByText('Access requests')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when url param matches `filteredSearchBar.searchParam`', () => {
|
||||
beforeEach(() => {
|
||||
window.location.search = '?search_groups=foo+bar';
|
||||
});
|
||||
|
||||
const expectGroupsTabActive = () => {
|
||||
expect(findActiveTab().text()).toContain('Groups');
|
||||
};
|
||||
|
||||
describe('when tab has a count', () => {
|
||||
it('sets tab that corresponds to search param as active tab', async () => {
|
||||
await createComponent();
|
||||
|
||||
expectGroupsTabActive();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when tab does not have a count', () => {
|
||||
it('sets tab that corresponds to search param as active tab', async () => {
|
||||
await createComponent({ totalItems: 0 });
|
||||
|
||||
expectGroupsTabActive();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when url param matches `pagination.paramName`', () => {
|
||||
beforeEach(() => {
|
||||
window.location.search = '?invited_page=2';
|
||||
});
|
||||
|
||||
const expectInvitedTabActive = () => {
|
||||
expect(findActiveTab().text()).toContain('Invited');
|
||||
};
|
||||
|
||||
describe('when tab has a count', () => {
|
||||
it('sets tab that corresponds to pagination param as active tab', async () => {
|
||||
await createComponent();
|
||||
|
||||
expectInvitedTabActive();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when tab does not have a count', () => {
|
||||
it('sets tab that corresponds to pagination param as active tab', async () => {
|
||||
await createComponent({ totalItems: 0 });
|
||||
|
||||
expectInvitedTabActive();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when `canManageMembers` is `false`', () => {
|
||||
it('shows all tabs except `Invited` and `Access requests`', async () => {
|
||||
await createComponent({ options: { provide: { canManageMembers: false } } });
|
||||
|
||||
expect(findTabByText('Members')).not.toBeUndefined();
|
||||
expect(findTabByText('Groups')).not.toBeUndefined();
|
||||
expect(findTabByText('Invited')).toBeUndefined();
|
||||
expect(findTabByText('Access requests')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -38,6 +38,16 @@ describe('Pipeline editor drawer', () => {
|
|||
localStorage.clear();
|
||||
});
|
||||
|
||||
it('it sets the drawer to be opened by default', async () => {
|
||||
createComponent();
|
||||
|
||||
expect(findDrawerContent().exists()).toBe(false);
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(findDrawerContent().exists()).toBe(true);
|
||||
});
|
||||
|
||||
describe('when the drawer is collapsed', () => {
|
||||
beforeEach(async () => {
|
||||
createComponent();
|
||||
|
@ -100,10 +110,15 @@ describe('Pipeline editor drawer', () => {
|
|||
|
||||
describe('local storage', () => {
|
||||
it('saves the drawer expanded value to local storage', async () => {
|
||||
localStorage.setItem(DRAWER_EXPANDED_KEY, 'false');
|
||||
|
||||
createComponent();
|
||||
await clickToggleBtn();
|
||||
|
||||
expect(localStorage.setItem.mock.calls).toEqual([[DRAWER_EXPANDED_KEY, 'false']]);
|
||||
expect(localStorage.setItem.mock.calls).toEqual([
|
||||
[DRAWER_EXPANDED_KEY, 'false'],
|
||||
[DRAWER_EXPANDED_KEY, 'true'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('loads the drawer collapsed when local storage is set to `false`, ', async () => {
|
||||
|
|
|
@ -4,6 +4,6 @@ require 'spec_helper'
|
|||
|
||||
RSpec.describe GitlabSchema.types['PackageTypeEnum'] do
|
||||
it 'exposes all package types' do
|
||||
expect(described_class.values.keys).to contain_exactly(*%w[MAVEN NPM CONAN NUGET PYPI COMPOSER GENERIC GOLANG DEBIAN RUBYGEMS HELM])
|
||||
expect(described_class.values.keys).to contain_exactly(*%w[MAVEN NPM CONAN NUGET PYPI COMPOSER GENERIC GOLANG DEBIAN RUBYGEMS HELM TERRAFORM_MODULE])
|
||||
end
|
||||
end
|
||||
|
|
|
@ -73,6 +73,90 @@ RSpec.describe Gitlab::APIAuthentication::TokenLocator do
|
|||
end
|
||||
end
|
||||
|
||||
context 'with :http_bearer_token' do
|
||||
let(:type) { :http_bearer_token }
|
||||
|
||||
context 'without credentials' do
|
||||
let(:request) { double(headers: {}) }
|
||||
|
||||
it 'returns nil' do
|
||||
expect(subject).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'with credentials' do
|
||||
let(:password) { 'bar' }
|
||||
let(:request) { double(headers: { "Authorization" => "Bearer #{password}" }) }
|
||||
|
||||
it 'returns the credentials' do
|
||||
expect(subject.password).to eq(password)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with :http_deploy_token_header' do
|
||||
let(:type) { :http_deploy_token_header }
|
||||
|
||||
context 'without credentials' do
|
||||
let(:request) { double(headers: {}) }
|
||||
|
||||
it 'returns nil' do
|
||||
expect(subject).to be(nil)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with credentials' do
|
||||
let(:password) { 'bar' }
|
||||
let(:request) { double(headers: { 'Deploy-Token' => password }) }
|
||||
|
||||
it 'returns the credentials' do
|
||||
expect(subject.password).to eq(password)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with :http_job_token_header' do
|
||||
let(:type) { :http_job_token_header }
|
||||
|
||||
context 'without credentials' do
|
||||
let(:request) { double(headers: {}) }
|
||||
|
||||
it 'returns nil' do
|
||||
expect(subject).to be(nil)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with credentials' do
|
||||
let(:password) { 'bar' }
|
||||
let(:request) { double(headers: { 'Job-Token' => password }) }
|
||||
|
||||
it 'returns the credentials' do
|
||||
expect(subject.password).to eq(password)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with :http_private_token_header' do
|
||||
let(:type) { :http_private_token_header }
|
||||
|
||||
context 'without credentials' do
|
||||
let(:request) { double(headers: {}) }
|
||||
|
||||
it 'returns nil' do
|
||||
expect(subject).to be(nil)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with credentials' do
|
||||
let(:password) { 'bar' }
|
||||
let(:request) { double(headers: { 'Private-Token' => password }) }
|
||||
|
||||
it 'returns the credentials' do
|
||||
expect(subject.password).to eq(password)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with :token_param' do
|
||||
let(:type) { :token_param }
|
||||
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Email::Message::InProductMarketing::Base do
|
||||
let_it_be(:group) { build(:group) }
|
||||
|
||||
let(:series) { 0 }
|
||||
let(:test_class) { Gitlab::Email::Message::InProductMarketing::Create }
|
||||
|
||||
describe 'initialize' do
|
||||
subject { test_class.new(group: group, series: series) }
|
||||
|
||||
context 'when series does not exist' do
|
||||
let(:series) { 3 }
|
||||
|
||||
it 'raises error' do
|
||||
expect { subject }.to raise_error(ArgumentError)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when series exists' do
|
||||
let(:series) { 0 }
|
||||
|
||||
it 'does not raise error' do
|
||||
expect { subject }.not_to raise_error(ArgumentError)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#logo_path' do
|
||||
subject { test_class.new(group: group, series: series).logo_path }
|
||||
|
||||
it { is_expected.to eq('mailers/in_product_marketing/create-0.png') }
|
||||
end
|
||||
|
||||
describe '#unsubscribe' do
|
||||
subject { test_class.new(group: group, series: series).unsubscribe }
|
||||
|
||||
before do
|
||||
allow(Gitlab).to receive(:com?).and_return(is_gitlab_com)
|
||||
end
|
||||
|
||||
context 'on gitlab.com' do
|
||||
let(:is_gitlab_com) { true }
|
||||
|
||||
it { is_expected.to include('%tag_unsubscribe_url%') }
|
||||
end
|
||||
|
||||
context 'not on gitlab.com' do
|
||||
let(:is_gitlab_com) { false }
|
||||
|
||||
it { is_expected.to include(Gitlab::Routing.url_helpers.profile_notifications_url) }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#cta_link' do
|
||||
subject(:cta_link) { test_class.new(group: group, series: series).cta_link }
|
||||
|
||||
it 'renders link' do
|
||||
expect(CGI.unescapeHTML(cta_link)).to include(Gitlab::Routing.url_helpers.group_email_campaigns_url(group, track: :create, series: series))
|
||||
end
|
||||
end
|
||||
|
||||
describe '#progress' do
|
||||
subject { test_class.new(group: group, series: series).progress }
|
||||
|
||||
before do
|
||||
allow(Gitlab).to receive(:com?).and_return(is_gitlab_com)
|
||||
end
|
||||
|
||||
context 'on gitlab.com' do
|
||||
let(:is_gitlab_com) { true }
|
||||
|
||||
it { is_expected.to include('This is email 1 of 3 in the Create series') }
|
||||
end
|
||||
|
||||
context 'not on gitlab.com' do
|
||||
let(:is_gitlab_com) { false }
|
||||
|
||||
it { is_expected.to include('This is email 1 of 3 in the Create series', Gitlab::Routing.url_helpers.profile_notifications_url) }
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,27 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Email::Message::InProductMarketing::Create do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
let_it_be(:group) { build(:group) }
|
||||
|
||||
subject(:message) { described_class.new(group: group, series: series)}
|
||||
|
||||
describe "public methods" do
|
||||
where(series: [0, 1, 2])
|
||||
|
||||
with_them do
|
||||
it 'returns value for series', :aggregate_failures do
|
||||
expect(message.subject_line).to be_present
|
||||
expect(message.tagline).to be_present
|
||||
expect(message.title).to be_present
|
||||
expect(message.subtitle).to be_present
|
||||
expect(message.body_line1).to be_present
|
||||
expect(message.body_line2).to be_present
|
||||
expect(message.cta_text).to be_present
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,41 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Email::Message::InProductMarketing::Team do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
let_it_be(:group) { build(:group) }
|
||||
|
||||
subject(:message) { described_class.new(group: group, series: series)}
|
||||
|
||||
describe "public methods" do
|
||||
where(series: [0, 1])
|
||||
|
||||
with_them do
|
||||
it 'returns value for series', :aggregate_failures do
|
||||
expect(message.subject_line).to be_present
|
||||
expect(message.tagline).to be_present
|
||||
expect(message.title).to be_present
|
||||
expect(message.subtitle).to be_present
|
||||
expect(message.body_line1).to be_present
|
||||
expect(message.body_line2).to be_present
|
||||
expect(message.cta_text).to be_present
|
||||
end
|
||||
end
|
||||
|
||||
context 'with series 2' do
|
||||
let(:series) { 2 }
|
||||
|
||||
it 'returns value for series', :aggregate_failures do
|
||||
expect(message.subject_line).to be_present
|
||||
expect(message.tagline).to be_nil
|
||||
expect(message.title).to be_present
|
||||
expect(message.subtitle).to be_present
|
||||
expect(message.body_line1).to be_present
|
||||
expect(message.body_line2).to be_present
|
||||
expect(message.cta_text).to be_present
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,27 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Email::Message::InProductMarketing::Trial do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
let_it_be(:group) { build(:group) }
|
||||
|
||||
subject(:message) { described_class.new(group: group, series: series)}
|
||||
|
||||
describe "public methods" do
|
||||
where(series: [0, 1, 2])
|
||||
|
||||
with_them do
|
||||
it 'returns value for series', :aggregate_failures do
|
||||
expect(message.subject_line).to be_present
|
||||
expect(message.tagline).to be_present
|
||||
expect(message.title).to be_present
|
||||
expect(message.subtitle).to be_present
|
||||
expect(message.body_line1).to be_present
|
||||
expect(message.body_line2).to be_present
|
||||
expect(message.cta_text).to be_present
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,53 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Email::Message::InProductMarketing::Verify do
|
||||
let_it_be(:group) { build(:group) }
|
||||
|
||||
subject(:message) { described_class.new(group: group, series: series)}
|
||||
|
||||
describe "public methods" do
|
||||
context 'with series 0' do
|
||||
let(:series) { 0 }
|
||||
|
||||
it 'returns value for series', :aggregate_failures do
|
||||
expect(message.subject_line).to be_present
|
||||
expect(message.tagline).to be_present
|
||||
expect(message.title).to be_present
|
||||
expect(message.subtitle).to be_present
|
||||
expect(message.body_line1).to be_present
|
||||
expect(message.body_line2).to be_nil
|
||||
expect(message.cta_text).to be_present
|
||||
end
|
||||
end
|
||||
|
||||
context 'with series 1' do
|
||||
let(:series) { 1 }
|
||||
|
||||
it 'returns value for series', :aggregate_failures do
|
||||
expect(message.subject_line).to be_present
|
||||
expect(message.tagline).to be_present
|
||||
expect(message.title).to be_present
|
||||
expect(message.subtitle).to be_present
|
||||
expect(message.body_line1).to be_present
|
||||
expect(message.body_line2).to be_present
|
||||
expect(message.cta_text).to be_present
|
||||
end
|
||||
end
|
||||
|
||||
context 'with series 2' do
|
||||
let(:series) { 2 }
|
||||
|
||||
it 'returns value for series', :aggregate_failures do
|
||||
expect(message.subject_line).to be_present
|
||||
expect(message.tagline).to be_present
|
||||
expect(message.title).to be_present
|
||||
expect(message.subtitle).to be_present
|
||||
expect(message.body_line1).to be_present
|
||||
expect(message.body_line2).to be_nil
|
||||
expect(message.cta_text).to be_present
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
32
spec/lib/gitlab/email/message/in_product_marketing_spec.rb
Normal file
32
spec/lib/gitlab/email/message/in_product_marketing_spec.rb
Normal file
|
@ -0,0 +1,32 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Email::Message::InProductMarketing do
|
||||
describe '.for' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
subject { described_class.for(track) }
|
||||
|
||||
context 'when track exists' do
|
||||
where(:track, :expected_class) do
|
||||
:create | described_class::Create
|
||||
:verify | described_class::Verify
|
||||
:trial | described_class::Trial
|
||||
:team | described_class::Team
|
||||
end
|
||||
|
||||
with_them do
|
||||
it { is_expected.to eq(expected_class) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when track does not exist' do
|
||||
let(:track) { :non_existent }
|
||||
|
||||
it 'raises error' do
|
||||
expect { subject }.to raise_error(described_class::UnknownTrackError)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -427,6 +427,19 @@ RSpec.describe Gitlab::Regex do
|
|||
it { is_expected.not_to match('%2e%2e%2fmy_package') }
|
||||
end
|
||||
|
||||
describe '.terraform_module_package_name_regex' do
|
||||
subject { described_class.terraform_module_package_name_regex }
|
||||
|
||||
it { is_expected.to match('my-module/my-system') }
|
||||
it { is_expected.to match('my/module') }
|
||||
it { is_expected.not_to match('my-module') }
|
||||
it { is_expected.not_to match('My-Module') }
|
||||
it { is_expected.not_to match('my_module') }
|
||||
it { is_expected.not_to match('my.module') }
|
||||
it { is_expected.not_to match('../../../my-module') }
|
||||
it { is_expected.not_to match('%2e%2e%2fmy-module') }
|
||||
end
|
||||
|
||||
describe '.pypi_version_regex' do
|
||||
subject { described_class.pypi_version_regex }
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ RSpec.describe Gitlab::UsageDataCounters::PackageEventCounter, :clean_gitlab_red
|
|||
end
|
||||
|
||||
it 'includes the right events' do
|
||||
expect(described_class::KNOWN_EVENTS.size).to eq 48
|
||||
expect(described_class::KNOWN_EVENTS.size).to eq 51
|
||||
end
|
||||
|
||||
described_class::KNOWN_EVENTS.each do |event|
|
||||
|
|
|
@ -5,7 +5,6 @@ require 'email_spec'
|
|||
|
||||
RSpec.describe Emails::InProductMarketing do
|
||||
include EmailSpec::Matchers
|
||||
include InProductMarketingHelper
|
||||
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:group) { create(:group) }
|
||||
|
@ -62,11 +61,13 @@ RSpec.describe Emails::InProductMarketing do
|
|||
|
||||
with_them do
|
||||
it 'has the correct subject and content' do
|
||||
message = Gitlab::Email::Message::InProductMarketing.for(track).new(group: group, series: series)
|
||||
|
||||
aggregate_failures do
|
||||
is_expected.to have_subject(subject_line(track, series))
|
||||
is_expected.to have_body_text(in_product_marketing_title(track, series))
|
||||
is_expected.to have_body_text(in_product_marketing_subtitle(track, series))
|
||||
is_expected.to have_body_text(in_product_marketing_cta_text(track, series))
|
||||
is_expected.to have_subject(message.subject_line)
|
||||
is_expected.to have_body_text(message.title)
|
||||
is_expected.to have_body_text(message.subtitle)
|
||||
is_expected.to have_body_text(CGI.unescapeHTML(message.cta_link))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -66,7 +66,7 @@ RSpec.describe Ci::Maskable do
|
|||
end
|
||||
|
||||
it 'matches valid strings' do
|
||||
expect(subject.match?('Hello+World_123/@:-.')).to eq(true)
|
||||
expect(subject.match?('Hello+World_123/@:-~.')).to eq(true)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -208,6 +208,19 @@ RSpec.describe Packages::Package, type: :model do
|
|||
it { is_expected.not_to allow_value("@scope%2e%2e%fpackage").for(:name) }
|
||||
it { is_expected.not_to allow_value("@scope/sub/package").for(:name) }
|
||||
end
|
||||
|
||||
context 'terraform module package' do
|
||||
subject { build_stubbed(:terraform_module_package) }
|
||||
|
||||
it { is_expected.to allow_value('my-module/my-system').for(:name) }
|
||||
it { is_expected.to allow_value('my/module').for(:name) }
|
||||
it { is_expected.not_to allow_value('my-module').for(:name) }
|
||||
it { is_expected.not_to allow_value('My-Module').for(:name) }
|
||||
it { is_expected.not_to allow_value('my_module').for(:name) }
|
||||
it { is_expected.not_to allow_value('my.module').for(:name) }
|
||||
it { is_expected.not_to allow_value('../../../my-module').for(:name) }
|
||||
it { is_expected.not_to allow_value('%2e%2e%2fmy-module').for(:name) }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#version' do
|
||||
|
@ -395,6 +408,7 @@ RSpec.describe Packages::Package, type: :model do
|
|||
end
|
||||
|
||||
it_behaves_like 'validating version to be SemVer compliant for', :npm_package
|
||||
it_behaves_like 'validating version to be SemVer compliant for', :terraform_module_package
|
||||
|
||||
context 'nuget package' do
|
||||
it_behaves_like 'validating version to be SemVer compliant for', :nuget_package
|
||||
|
@ -492,6 +506,26 @@ RSpec.describe Packages::Package, type: :model do
|
|||
end
|
||||
end
|
||||
|
||||
describe '.with_package_type' do
|
||||
let!(:package1) { create(:terraform_module_package) }
|
||||
let!(:package2) { create(:npm_package) }
|
||||
let(:package_type) { :terraform_module }
|
||||
|
||||
subject { described_class.with_package_type(package_type) }
|
||||
|
||||
it { is_expected.to eq([package1]) }
|
||||
end
|
||||
|
||||
describe '.without_package_type' do
|
||||
let!(:package1) { create(:npm_package) }
|
||||
let!(:package2) { create(:terraform_module_package) }
|
||||
let(:package_type) { :terraform_module }
|
||||
|
||||
subject { described_class.without_package_type(package_type) }
|
||||
|
||||
it { is_expected.to eq([package1]) }
|
||||
end
|
||||
|
||||
context 'version scopes' do
|
||||
let!(:package1) { create(:npm_package, version: '1.0.0') }
|
||||
let!(:package2) { create(:npm_package, version: '1.0.1') }
|
||||
|
|
22
spec/presenters/terraform/modules_presenter_spec.rb
Normal file
22
spec/presenters/terraform/modules_presenter_spec.rb
Normal file
|
@ -0,0 +1,22 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe ::Terraform::ModulesPresenter do
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:module_system) { 'my-system' }
|
||||
let_it_be(:package_name) { "my-module/#{module_system}" }
|
||||
let_it_be(:package1) { create(:terraform_module_package, version: '1.0.1', project: project, name: package_name) }
|
||||
let_it_be(:package2) { create(:terraform_module_package, version: '1.0.10', project: project, name: package_name) }
|
||||
|
||||
let(:packages) { project.packages.terraform_module.with_name(package_name) }
|
||||
let(:presenter) { described_class.new(packages, module_system) }
|
||||
|
||||
describe '#modules' do
|
||||
subject { presenter.modules }
|
||||
|
||||
it { is_expected.to be_an(Array) }
|
||||
it { expect(subject.first).to be_a(Hash) }
|
||||
it { expect(subject).to match_schema('public_api/v4/packages/terraform/modules/v1/modules') }
|
||||
end
|
||||
end
|
|
@ -37,6 +37,16 @@ RSpec.describe API::ProjectPackages do
|
|||
end
|
||||
end
|
||||
|
||||
context 'with terraform module package' do
|
||||
let_it_be(:terraform_module_package) { create(:terraform_module_package, project: project) }
|
||||
|
||||
it 'filters out terraform module packages when no package_type filter is set' do
|
||||
subject
|
||||
|
||||
expect(json_response).not_to include(a_hash_including('package_type' => 'terraform_module'))
|
||||
end
|
||||
end
|
||||
|
||||
context 'project is private' do
|
||||
let(:project) { create(:project, :private) }
|
||||
|
||||
|
|
360
spec/requests/api/terraform/modules/v1/packages_spec.rb
Normal file
360
spec/requests/api/terraform/modules/v1/packages_spec.rb
Normal file
|
@ -0,0 +1,360 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe API::Terraform::Modules::V1::Packages do
|
||||
include PackagesManagerApiSpecHelpers
|
||||
include WorkhorseHelpers
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
let_it_be_with_reload(:group) { create(:group) }
|
||||
let_it_be_with_reload(:project) { create(:project, namespace: group) }
|
||||
let_it_be(:package) { create(:terraform_module_package, project: project) }
|
||||
let_it_be(:personal_access_token) { create(:personal_access_token) }
|
||||
let_it_be(:user) { personal_access_token.user }
|
||||
let_it_be(:job) { create(:ci_build, :running, user: user) }
|
||||
let_it_be(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) }
|
||||
let_it_be(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) }
|
||||
|
||||
let(:headers) { {} }
|
||||
|
||||
let(:tokens) do
|
||||
{
|
||||
personal_access_token: personal_access_token.token,
|
||||
deploy_token: deploy_token.token,
|
||||
job_token: job.token
|
||||
}
|
||||
end
|
||||
|
||||
describe 'GET /api/v4/packages/terraform/modules/v1/:module_namespace/:module_name/:module_system/versions' do
|
||||
let(:url) { api("/packages/terraform/modules/v1/#{group.path}/#{package.name}/versions") }
|
||||
let(:headers) { {} }
|
||||
|
||||
subject { get(url, headers: headers) }
|
||||
|
||||
context 'with valid namespace' do
|
||||
where(:visibility, :user_role, :member, :token_type, :valid_token, :shared_examples_name, :expected_status) do
|
||||
:public | :developer | true | :personal_access_token | true | 'returns terraform module packages' | :success
|
||||
:public | :guest | true | :personal_access_token | true | 'returns terraform module packages' | :success
|
||||
:public | :developer | true | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:public | :guest | true | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:public | :developer | false | :personal_access_token | true | 'returns no terraform module packages' | :success
|
||||
:public | :guest | false | :personal_access_token | true | 'returns no terraform module packages' | :success
|
||||
:public | :developer | false | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:public | :guest | false | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:public | :anonymous | false | :personal_access_token | true | 'returns no terraform module packages' | :success
|
||||
:private | :developer | true | :personal_access_token | true | 'returns terraform module packages' | :success
|
||||
:private | :guest | true | :personal_access_token | true | 'rejects terraform module packages access' | :forbidden
|
||||
:private | :developer | true | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:private | :guest | true | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:private | :developer | false | :personal_access_token | true | 'rejects terraform module packages access' | :forbidden
|
||||
:private | :guest | false | :personal_access_token | true | 'rejects terraform module packages access' | :forbidden
|
||||
:private | :developer | false | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:private | :guest | false | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:private | :anonymous | false | :personal_access_token | true | 'rejects terraform module packages access' | :unauthorized
|
||||
:public | :developer | true | :job_token | true | 'returns terraform module packages' | :success
|
||||
:public | :guest | true | :job_token | true | 'returns no terraform module packages' | :success
|
||||
:public | :guest | true | :job_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:public | :developer | false | :job_token | true | 'returns no terraform module packages' | :success
|
||||
:public | :guest | false | :job_token | true | 'returns no terraform module packages' | :success
|
||||
:public | :developer | false | :job_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:public | :guest | false | :job_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:private | :developer | true | :job_token | true | 'returns terraform module packages' | :success
|
||||
:private | :guest | true | :job_token | true | 'rejects terraform module packages access' | :forbidden
|
||||
:private | :developer | true | :job_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:private | :guest | true | :job_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:private | :developer | false | :job_token | true | 'rejects terraform module packages access' | :forbidden
|
||||
:private | :guest | false | :job_token | true | 'rejects terraform module packages access' | :forbidden
|
||||
:private | :developer | false | :job_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:private | :guest | false | :job_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
end
|
||||
|
||||
with_them do
|
||||
let(:token) { valid_token ? tokens[token_type] : 'invalid-token123' }
|
||||
let(:headers) { user_role == :anonymous ? {} : { 'Authorization' => "Bearer #{token}" } }
|
||||
|
||||
before do
|
||||
group.update!(visibility: visibility.to_s)
|
||||
end
|
||||
|
||||
it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /api/v4/packages/terraform/modules/v1/:module_namespace/:module_name/:module_system/:module_version/download' do
|
||||
let(:url) { api("/packages/terraform/modules/v1/#{group.path}/#{package.name}/#{package.version}/download") }
|
||||
let(:headers) { {} }
|
||||
|
||||
subject { get(url, headers: headers) }
|
||||
|
||||
context 'with valid namespace' do
|
||||
where(:visibility, :user_role, :member, :token_type, :valid_token, :shared_examples_name, :expected_status) do
|
||||
:public | :developer | true | :personal_access_token | true | 'grants terraform module download' | :success
|
||||
:public | :guest | true | :personal_access_token | true | 'rejects terraform module packages access' | :not_found
|
||||
:public | :developer | true | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:public | :guest | true | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:public | :developer | false | :personal_access_token | true | 'rejects terraform module packages access' | :not_found
|
||||
:public | :guest | false | :personal_access_token | true | 'rejects terraform module packages access' | :not_found
|
||||
:public | :developer | false | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:public | :guest | false | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:public | :anonymous | false | :personal_access_token | true | 'rejects terraform module packages access' | :not_found
|
||||
:private | :developer | true | :personal_access_token | true | 'grants terraform module download' | :success
|
||||
:private | :guest | true | :personal_access_token | true | 'rejects terraform module packages access' | :forbidden
|
||||
:private | :developer | true | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:private | :guest | true | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:private | :developer | false | :personal_access_token | true | 'rejects terraform module packages access' | :forbidden
|
||||
:private | :guest | false | :personal_access_token | true | 'rejects terraform module packages access' | :forbidden
|
||||
:private | :developer | false | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:private | :guest | false | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:private | :anonymous | false | :personal_access_token | true | 'rejects terraform module packages access' | :unauthorized
|
||||
:public | :developer | true | :job_token | true | 'grants terraform module download' | :success
|
||||
:public | :guest | true | :job_token | true | 'rejects terraform module packages access' | :not_found
|
||||
:public | :guest | true | :job_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:public | :developer | false | :job_token | true | 'rejects terraform module packages access' | :not_found
|
||||
:public | :guest | false | :job_token | true | 'rejects terraform module packages access' | :not_found
|
||||
:public | :developer | false | :job_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:public | :guest | false | :job_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:private | :developer | true | :job_token | true | 'grants terraform module download' | :success
|
||||
:private | :guest | true | :job_token | true | 'rejects terraform module packages access' | :forbidden
|
||||
:private | :developer | true | :job_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:private | :guest | true | :job_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:private | :developer | false | :job_token | true | 'rejects terraform module packages access' | :forbidden
|
||||
:private | :guest | false | :job_token | true | 'rejects terraform module packages access' | :forbidden
|
||||
:private | :developer | false | :job_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:private | :guest | false | :job_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
end
|
||||
|
||||
with_them do
|
||||
let(:token) { valid_token ? tokens[token_type] : 'invalid-token123' }
|
||||
let(:headers) { user_role == :anonymous ? {} : { 'Authorization' => "Bearer #{token}" } }
|
||||
|
||||
before do
|
||||
group.update!(visibility: visibility.to_s)
|
||||
end
|
||||
|
||||
it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /api/v4/packages/terraform/modules/v1/:module_namespace/:module_name/:module_system/:module_version/file' do
|
||||
let(:tokens) do
|
||||
{
|
||||
personal_access_token: ::Gitlab::JWTToken.new.tap { |jwt| jwt['token'] = personal_access_token.id }.encoded,
|
||||
job_token: ::Gitlab::JWTToken.new.tap { |jwt| jwt['token'] = job.token }.encoded
|
||||
}
|
||||
end
|
||||
|
||||
subject { get(url, headers: headers) }
|
||||
|
||||
context 'with valid namespace' do
|
||||
where(:visibility, :user_role, :member, :token_type, :valid_token, :shared_examples_name, :expected_status) do
|
||||
:public | :developer | true | :personal_access_token | true | 'grants terraform module package file access' | :success
|
||||
:public | :guest | true | :personal_access_token | true | 'rejects terraform module packages access' | :not_found
|
||||
:public | :developer | true | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:public | :guest | true | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:public | :developer | false | :personal_access_token | true | 'rejects terraform module packages access' | :not_found
|
||||
:public | :guest | false | :personal_access_token | true | 'rejects terraform module packages access' | :not_found
|
||||
:public | :developer | false | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:public | :guest | false | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:public | :anonymous | false | :personal_access_token | true | 'rejects terraform module packages access' | :not_found
|
||||
:private | :developer | true | :personal_access_token | true | 'grants terraform module package file access' | :success
|
||||
:private | :guest | true | :personal_access_token | true | 'rejects terraform module packages access' | :forbidden
|
||||
:private | :developer | true | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:private | :guest | true | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:private | :developer | false | :personal_access_token | true | 'rejects terraform module packages access' | :forbidden
|
||||
:private | :guest | false | :personal_access_token | true | 'rejects terraform module packages access' | :forbidden
|
||||
:private | :developer | false | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:private | :guest | false | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:private | :anonymous | false | :personal_access_token | true | 'rejects terraform module packages access' | :forbidden
|
||||
:public | :developer | true | :job_token | true | 'grants terraform module package file access' | :success
|
||||
:public | :guest | true | :job_token | true | 'rejects terraform module packages access' | :not_found
|
||||
:public | :guest | true | :job_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:public | :developer | false | :job_token | true | 'rejects terraform module packages access' | :not_found
|
||||
:public | :guest | false | :job_token | true | 'rejects terraform module packages access' | :not_found
|
||||
:public | :developer | false | :job_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:public | :guest | false | :job_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:private | :developer | true | :job_token | true | 'grants terraform module package file access' | :success
|
||||
:private | :guest | true | :job_token | true | 'rejects terraform module packages access' | :forbidden
|
||||
:private | :developer | true | :job_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:private | :guest | true | :job_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:private | :developer | false | :job_token | true | 'rejects terraform module packages access' | :forbidden
|
||||
:private | :guest | false | :job_token | true | 'rejects terraform module packages access' | :forbidden
|
||||
:private | :developer | false | :job_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:private | :guest | false | :job_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
end
|
||||
|
||||
with_them do
|
||||
let(:token) { valid_token ? tokens[token_type] : 'invalid-token123' }
|
||||
let(:url) { api("/packages/terraform/modules/v1/#{group.path}/#{package.name}/#{package.version}/file?token=#{token}") }
|
||||
|
||||
before do
|
||||
group.update!(visibility: visibility.to_s)
|
||||
end
|
||||
|
||||
it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'PUT /api/v4/projects/:project_id/packages/terraform/modules/:module_name/:module_system/:module_version/file/authorize' do
|
||||
include_context 'workhorse headers'
|
||||
|
||||
let(:url) { api("/projects/#{project.id}/packages/terraform/modules/mymodule/mysystem/1.0.0/file/authorize") }
|
||||
let(:headers) { {} }
|
||||
|
||||
subject { put(url, headers: headers) }
|
||||
|
||||
context 'with valid project' do
|
||||
where(:visibility, :user_role, :member, :token_header, :token_type, :valid_token, :shared_examples_name, :expected_status) do
|
||||
:public | :developer | true | 'PRIVATE-TOKEN' | :personal_access_token | true | 'process terraform module workhorse authorization' | :success
|
||||
:public | :guest | true | 'PRIVATE-TOKEN' | :personal_access_token | true | 'rejects terraform module packages access' | :forbidden
|
||||
:public | :developer | true | 'PRIVATE-TOKEN' | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:public | :guest | true | 'PRIVATE-TOKEN' | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:public | :developer | false | 'PRIVATE-TOKEN' | :personal_access_token | true | 'rejects terraform module packages access' | :forbidden
|
||||
:public | :guest | false | 'PRIVATE-TOKEN' | :personal_access_token | true | 'rejects terraform module packages access' | :forbidden
|
||||
:public | :developer | false | 'PRIVATE-TOKEN' | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:public | :guest | false | 'PRIVATE-TOKEN' | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:public | :anonymous | false | 'PRIVATE-TOKEN' | :personal_access_token | true | 'rejects terraform module packages access' | :unauthorized
|
||||
:private | :developer | true | 'PRIVATE-TOKEN' | :personal_access_token | true | 'process terraform module workhorse authorization' | :success
|
||||
:private | :guest | true | 'PRIVATE-TOKEN' | :personal_access_token | true | 'rejects terraform module packages access' | :forbidden
|
||||
:private | :developer | true | 'PRIVATE-TOKEN' | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:private | :guest | true | 'PRIVATE-TOKEN' | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:private | :developer | false | 'PRIVATE-TOKEN' | :personal_access_token | true | 'rejects terraform module packages access' | :not_found
|
||||
:private | :guest | false | 'PRIVATE-TOKEN' | :personal_access_token | true | 'rejects terraform module packages access' | :not_found
|
||||
:private | :developer | false | 'PRIVATE-TOKEN' | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:private | :guest | false | 'PRIVATE-TOKEN' | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:private | :anonymous | false | 'PRIVATE-TOKEN' | :personal_access_token | true | 'rejects terraform module packages access' | :unauthorized
|
||||
:public | :developer | true | 'JOB-TOKEN' | :job_token | true | 'process terraform module workhorse authorization' | :success
|
||||
:public | :guest | true | 'JOB-TOKEN' | :job_token | true | 'rejects terraform module packages access' | :forbidden
|
||||
:public | :developer | true | 'JOB-TOKEN' | :job_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:public | :guest | true | 'JOB-TOKEN' | :job_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:public | :developer | false | 'JOB-TOKEN' | :job_token | true | 'rejects terraform module packages access' | :forbidden
|
||||
:public | :guest | false | 'JOB-TOKEN' | :job_token | true | 'rejects terraform module packages access' | :forbidden
|
||||
:public | :developer | false | 'JOB-TOKEN' | :job_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:public | :guest | false | 'JOB-TOKEN' | :job_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:private | :developer | true | 'JOB-TOKEN' | :job_token | true | 'process terraform module workhorse authorization' | :success
|
||||
:private | :guest | true | 'JOB-TOKEN' | :job_token | true | 'rejects terraform module packages access' | :forbidden
|
||||
:private | :developer | true | 'JOB-TOKEN' | :job_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:private | :guest | true | 'JOB-TOKEN' | :job_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:private | :developer | false | 'JOB-TOKEN' | :job_token | true | 'rejects terraform module packages access' | :not_found
|
||||
:private | :guest | false | 'JOB-TOKEN' | :job_token | true | 'rejects terraform module packages access' | :not_found
|
||||
:private | :developer | false | 'JOB-TOKEN' | :job_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:private | :guest | false | 'JOB-TOKEN' | :job_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:public | :developer | true | 'DEPLOY-TOKEN' | :deploy_token | true | 'process terraform module workhorse authorization' | :success
|
||||
:public | :developer | true | 'DEPLOY-TOKEN' | :deploy_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:private | :developer | true | 'DEPLOY-TOKEN' | :deploy_token | true | 'process terraform module workhorse authorization' | :success
|
||||
:private | :developer | true | 'DEPLOY-TOKEN' | :deploy_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
end
|
||||
|
||||
with_them do
|
||||
let(:token) { valid_token ? tokens[token_type] : 'invalid-token123' }
|
||||
let(:headers) { user_headers.merge(workhorse_headers) }
|
||||
let(:user_headers) { user_role == :anonymous ? {} : { token_header => token } }
|
||||
|
||||
before do
|
||||
project.update!(visibility: visibility.to_s)
|
||||
end
|
||||
|
||||
it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'PUT /api/v4/projects/:project_id/packages/terraform/modules/:module_name/:module_system/:module_version/file' do
|
||||
include_context 'workhorse headers'
|
||||
|
||||
let_it_be(:file_name) { 'module-system-v1.0.0.tgz' }
|
||||
|
||||
let(:url) { "/projects/#{project.id}/packages/terraform/modules/mymodule/mysystem/1.0.0/file" }
|
||||
let(:headers) { {} }
|
||||
let(:params) { { file: temp_file(file_name) } }
|
||||
let(:file_key) { :file }
|
||||
let(:send_rewritten_field) { true }
|
||||
|
||||
subject do
|
||||
workhorse_finalize(
|
||||
api(url),
|
||||
method: :put,
|
||||
file_key: file_key,
|
||||
params: params,
|
||||
headers: headers,
|
||||
send_rewritten_field: send_rewritten_field
|
||||
)
|
||||
end
|
||||
|
||||
context 'with valid project' do
|
||||
where(:visibility, :user_role, :member, :token_header, :token_type, :valid_token, :shared_examples_name, :expected_status) do
|
||||
:public | :developer | true | 'PRIVATE-TOKEN' | :personal_access_token | true | 'process terraform module upload' | :created
|
||||
:public | :guest | true | 'PRIVATE-TOKEN' | :personal_access_token | true | 'rejects terraform module packages access' | :forbidden
|
||||
:public | :developer | true | 'PRIVATE-TOKEN' | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:public | :guest | true | 'PRIVATE-TOKEN' | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:public | :developer | false | 'PRIVATE-TOKEN' | :personal_access_token | true | 'rejects terraform module packages access' | :forbidden
|
||||
:public | :guest | false | 'PRIVATE-TOKEN' | :personal_access_token | true | 'rejects terraform module packages access' | :forbidden
|
||||
:public | :developer | false | 'PRIVATE-TOKEN' | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:public | :guest | false | 'PRIVATE-TOKEN' | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:public | :anonymous | false | 'PRIVATE-TOKEN' | :personal_access_token | true | 'rejects terraform module packages access' | :unauthorized
|
||||
:private | :developer | true | 'PRIVATE-TOKEN' | :personal_access_token | true | 'process terraform module upload' | :created
|
||||
:private | :guest | true | 'PRIVATE-TOKEN' | :personal_access_token | true | 'rejects terraform module packages access' | :forbidden
|
||||
:private | :developer | true | 'PRIVATE-TOKEN' | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:private | :guest | true | 'PRIVATE-TOKEN' | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:private | :developer | false | 'PRIVATE-TOKEN' | :personal_access_token | true | 'rejects terraform module packages access' | :not_found
|
||||
:private | :guest | false | 'PRIVATE-TOKEN' | :personal_access_token | true | 'rejects terraform module packages access' | :not_found
|
||||
:private | :developer | false | 'PRIVATE-TOKEN' | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:private | :guest | false | 'PRIVATE-TOKEN' | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:private | :anonymous | false | 'PRIVATE-TOKEN' | :personal_access_token | true | 'rejects terraform module packages access' | :unauthorized
|
||||
:public | :developer | true | 'JOB-TOKEN' | :job_token | true | 'process terraform module upload' | :created
|
||||
:public | :guest | true | 'JOB-TOKEN' | :job_token | true | 'rejects terraform module packages access' | :forbidden
|
||||
:public | :developer | true | 'JOB-TOKEN' | :job_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:public | :guest | true | 'JOB-TOKEN' | :job_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:public | :developer | false | 'JOB-TOKEN' | :job_token | true | 'rejects terraform module packages access' | :forbidden
|
||||
:public | :guest | false | 'JOB-TOKEN' | :job_token | true | 'rejects terraform module packages access' | :forbidden
|
||||
:public | :developer | false | 'JOB-TOKEN' | :job_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:public | :guest | false | 'JOB-TOKEN' | :job_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:private | :developer | true | 'JOB-TOKEN' | :job_token | true | 'process terraform module upload' | :created
|
||||
:private | :guest | true | 'JOB-TOKEN' | :job_token | true | 'rejects terraform module packages access' | :forbidden
|
||||
:private | :developer | true | 'JOB-TOKEN' | :job_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:private | :guest | true | 'JOB-TOKEN' | :job_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:private | :developer | false | 'JOB-TOKEN' | :job_token | true | 'rejects terraform module packages access' | :not_found
|
||||
:private | :guest | false | 'JOB-TOKEN' | :job_token | true | 'rejects terraform module packages access' | :not_found
|
||||
:private | :developer | false | 'JOB-TOKEN' | :job_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:private | :guest | false | 'JOB-TOKEN' | :job_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:public | :developer | true | 'DEPLOY-TOKEN' | :deploy_token | true | 'process terraform module upload' | :created
|
||||
:public | :developer | true | 'DEPLOY-TOKEN' | :deploy_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
:private | :developer | true | 'DEPLOY-TOKEN' | :deploy_token | true | 'process terraform module upload' | :created
|
||||
:private | :developer | true | 'DEPLOY-TOKEN' | :deploy_token | false | 'rejects terraform module packages access' | :unauthorized
|
||||
end
|
||||
|
||||
with_them do
|
||||
let(:token) { valid_token ? tokens[token_type] : 'invalid-token123' }
|
||||
let(:user_headers) { user_role == :anonymous ? {} : { token_header => token } }
|
||||
let(:headers) { user_headers.merge(workhorse_headers) }
|
||||
|
||||
before do
|
||||
project.update!(visibility: visibility.to_s)
|
||||
end
|
||||
|
||||
it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
|
||||
end
|
||||
|
||||
context 'failed package file save' do
|
||||
let(:user_headers) { { 'PRIVATE-TOKEN' => personal_access_token.token } }
|
||||
let(:headers) { user_headers.merge(workhorse_headers) }
|
||||
|
||||
before do
|
||||
project.add_developer(user)
|
||||
end
|
||||
|
||||
it 'does not create package record', :aggregate_failures do
|
||||
allow(Packages::CreatePackageFileService).to receive(:new).and_raise(StandardError)
|
||||
|
||||
expect { subject }
|
||||
.to change { project.packages.count }.by(0)
|
||||
.and change { Packages::PackageFile.count }.by(0)
|
||||
expect(response).to have_gitlab_http_status(:error)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -3,7 +3,6 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Groups::EmailCampaignsController do
|
||||
include InProductMarketingHelper
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
describe 'GET #index', :snowplow do
|
||||
|
@ -13,7 +12,7 @@ RSpec.describe Groups::EmailCampaignsController do
|
|||
let(:track) { 'create' }
|
||||
let(:series) { '0' }
|
||||
let(:schema) { described_class::EMAIL_CAMPAIGNS_SCHEMA_URL }
|
||||
let(:subject_line_text) { subject_line(track.to_sym, series.to_i) }
|
||||
let(:subject_line_text) { Gitlab::Email::Message::InProductMarketing.for(track.to_sym).new(group: group, series: series.to_i).subject_line }
|
||||
let(:data) do
|
||||
{
|
||||
namespace_id: group.id,
|
||||
|
|
15
spec/requests/terraform/services_controller_spec.rb
Normal file
15
spec/requests/terraform/services_controller_spec.rb
Normal file
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Terraform::ServicesController do
|
||||
describe 'GET /.well-known/terraform.json' do
|
||||
subject { get '/.well-known/terraform.json' }
|
||||
|
||||
it 'responds with terraform service discovery' do
|
||||
subject
|
||||
|
||||
expect(json_response['modules.v1']).to eq("/api/#{::API::API.version}/packages/terraform/modules/v1/")
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,57 @@
|
|||
# frozen_string_literal: true
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Packages::TerraformModule::CreatePackageService do
|
||||
let_it_be(:namespace) { create(:namespace) }
|
||||
let_it_be(:project) { create(:project, namespace: namespace) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:sha256) { '440e5e148a25331bbd7991575f7d54933c0ebf6cc735a18ee5066ac1381bb590' }
|
||||
let_it_be(:temp_file) { Tempfile.new('test') }
|
||||
let_it_be(:file) { UploadedFile.new(temp_file.path, sha256: sha256) }
|
||||
|
||||
let(:overrides) { {} }
|
||||
|
||||
let(:params) do
|
||||
{
|
||||
module_name: 'foo',
|
||||
module_system: 'bar',
|
||||
module_version: '1.0.1',
|
||||
file: file,
|
||||
file_name: 'foo-bar-1.0.1.tgz'
|
||||
}.merge(overrides)
|
||||
end
|
||||
|
||||
subject { described_class.new(project, user, params).execute }
|
||||
|
||||
describe '#execute' do
|
||||
context 'valid package' do
|
||||
it 'creates a package' do
|
||||
expect { subject }
|
||||
.to change { ::Packages::Package.count }.by(1)
|
||||
.and change { ::Packages::Package.terraform_module.count }.by(1)
|
||||
end
|
||||
end
|
||||
|
||||
context 'package already exists elsewhere' do
|
||||
let(:project2) { create(:project, namespace: namespace) }
|
||||
let!(:existing_package) { create(:terraform_module_package, project: project2, name: 'foo/bar', version: '1.0.0') }
|
||||
|
||||
it { expect(subject[:http_status]).to eq 403 }
|
||||
it { expect(subject[:message]).to be 'Package already exists.' }
|
||||
end
|
||||
|
||||
context 'version already exists' do
|
||||
let!(:existing_version) { create(:terraform_module_package, project: project, name: 'foo/bar', version: '1.0.1') }
|
||||
|
||||
it { expect(subject[:http_status]).to eq 403 }
|
||||
it { expect(subject[:message]).to be 'Package version already exists.' }
|
||||
end
|
||||
|
||||
context 'with empty version' do
|
||||
let(:overrides) { { module_version: '' } }
|
||||
|
||||
it { expect(subject[:http_status]).to eq 400 }
|
||||
it { expect(subject[:message]).to eq 'Version is empty.' }
|
||||
end
|
||||
end
|
||||
end
|
|
@ -170,16 +170,18 @@ RSpec.configure do |config|
|
|||
Capybara.raise_server_errors = false
|
||||
|
||||
example.run
|
||||
|
||||
if example.metadata[:screenshot]
|
||||
screenshot = example.metadata[:screenshot][:image] || example.metadata[:screenshot][:html]
|
||||
example.metadata[:stdout] = %{[[ATTACHMENT|#{screenshot}]]}
|
||||
end
|
||||
|
||||
ensure
|
||||
Capybara.raise_server_errors = true
|
||||
end
|
||||
|
||||
config.append_after do |example|
|
||||
if example.metadata[:screenshot]
|
||||
screenshot = example.metadata[:screenshot][:image] || example.metadata[:screenshot][:html]
|
||||
screenshot&.delete_prefix!(ENV.fetch('CI_PROJECT_DIR', ''))
|
||||
example.metadata[:stdout] = %{[[ATTACHMENT|#{screenshot}]]}
|
||||
end
|
||||
end
|
||||
|
||||
config.after(:example, :js) do |example|
|
||||
# when a test fails, display any messages in the browser's console
|
||||
# but fail don't add the message if the failure is a pending test that got
|
||||
|
|
|
@ -0,0 +1,251 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.shared_examples 'when package feature is disabled' do
|
||||
before do
|
||||
stub_config(packages: { enabled: false })
|
||||
end
|
||||
|
||||
it_behaves_like 'returning response status', :not_found
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'without authentication' do
|
||||
it_behaves_like 'returning response status', :unauthorized
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'with authentication' do
|
||||
where(:user_role, :token_header, :token_type, :valid_token, :status) do
|
||||
:guest | 'PRIVATE-TOKEN' | :personal_access_token | true | :not_found
|
||||
:guest | 'PRIVATE-TOKEN' | :personal_access_token | false | :unauthorized
|
||||
:guest | 'DEPLOY-TOKEN' | :deploy_token | true | :not_found
|
||||
:guest | 'DEPLOY-TOKEN' | :deploy_token | false | :unauthorized
|
||||
:guest | 'JOB-TOKEN' | :job_token | true | :not_found
|
||||
:guest | 'JOB-TOKEN' | :job_token | false | :unauthorized
|
||||
:reporter | 'PRIVATE-TOKEN' | :personal_access_token | true | :not_found
|
||||
:reporter | 'PRIVATE-TOKEN' | :personal_access_token | false | :unauthorized
|
||||
:reporter | 'DEPLOY-TOKEN' | :deploy_token | true | :not_found
|
||||
:reporter | 'DEPLOY-TOKEN' | :deploy_token | false | :unauthorized
|
||||
:reporter | 'JOB-TOKEN' | :job_token | true | :not_found
|
||||
:reporter | 'JOB-TOKEN' | :job_token | false | :unauthorized
|
||||
:developer | 'PRIVATE-TOKEN' | :personal_access_token | true | :not_found
|
||||
:developer | 'PRIVATE-TOKEN' | :personal_access_token | false | :unauthorized
|
||||
:developer | 'DEPLOY-TOKEN' | :deploy_token | true | :not_found
|
||||
:developer | 'DEPLOY-TOKEN' | :deploy_token | false | :unauthorized
|
||||
:developer | 'JOB-TOKEN' | :job_token | true | :not_found
|
||||
:developer | 'JOB-TOKEN' | :job_token | false | :unauthorized
|
||||
end
|
||||
|
||||
with_them do
|
||||
before do
|
||||
project.send("add_#{user_role}", user) unless user_role == :anonymous
|
||||
end
|
||||
|
||||
let(:token) { valid_token ? tokens[token_type] : 'invalid-token123' }
|
||||
let(:headers) { { token_header => token } }
|
||||
|
||||
it_behaves_like 'returning response status', params[:status]
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'an unimplemented route' do
|
||||
it_behaves_like 'without authentication'
|
||||
it_behaves_like 'with authentication'
|
||||
it_behaves_like 'when package feature is disabled'
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'grants terraform module download' do |user_type, status, add_member = true|
|
||||
context "for user type #{user_type}" do
|
||||
before do
|
||||
group.send("add_#{user_type}", user) if add_member && user_type != :anonymous
|
||||
end
|
||||
|
||||
it_behaves_like 'returning response status', status
|
||||
|
||||
it 'returns a valid response' do
|
||||
subject
|
||||
|
||||
expect(response.headers).to include 'X-Terraform-Get'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'returns terraform module packages' do |user_type, status, add_member = true|
|
||||
context "for user type #{user_type}" do
|
||||
before do
|
||||
group.send("add_#{user_type}", user) if add_member && user_type != :anonymous
|
||||
end
|
||||
|
||||
it_behaves_like 'returning response status', status
|
||||
|
||||
it 'returning a valid response' do
|
||||
subject
|
||||
|
||||
expect(json_response).to match_schema('public_api/v4/packages/terraform/modules/v1/versions')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'returns no terraform module packages' do |user_type, status, add_member = true|
|
||||
context "for user type #{user_type}" do
|
||||
before do
|
||||
group.send("add_#{user_type}", user) if add_member && user_type != :anonymous
|
||||
end
|
||||
|
||||
it_behaves_like 'returning response status', status
|
||||
|
||||
it 'returns a response with no versions' do
|
||||
subject
|
||||
|
||||
expect(json_response['modules'][0]['versions'].size).to eq(0)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'grants terraform module packages access' do |user_type, status, add_member = true|
|
||||
context "for user type #{user_type}" do
|
||||
before do
|
||||
project.send("add_#{user_type}", user) if add_member && user_type != :anonymous
|
||||
end
|
||||
|
||||
it_behaves_like 'returning response status', status
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'grants terraform module package file access' do |user_type, status, add_member = true|
|
||||
context "for user type #{user_type}" do
|
||||
before do
|
||||
project.send("add_#{user_type}", user) if add_member && user_type != :anonymous
|
||||
end
|
||||
|
||||
it_behaves_like 'returning response status', status
|
||||
it_behaves_like 'a package tracking event', described_class.name, 'pull_package'
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'rejects terraform module packages access' do |user_type, status, add_member = true|
|
||||
context "for user type #{user_type}" do
|
||||
before do
|
||||
project.send("add_#{user_type}", user) if add_member && user_type != :anonymous
|
||||
end
|
||||
|
||||
it_behaves_like 'returning response status', status
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'process terraform module workhorse authorization' do |user_type, status, add_member = true|
|
||||
context "for user type #{user_type}" do
|
||||
before do
|
||||
project.send("add_#{user_type}", user) if add_member && user_type != :anonymous
|
||||
end
|
||||
|
||||
it_behaves_like 'returning response status', status
|
||||
|
||||
it 'has the proper content type' do
|
||||
subject
|
||||
|
||||
expect(response.media_type).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
|
||||
end
|
||||
|
||||
context 'with a request that bypassed gitlab-workhorse' do
|
||||
let(:headers) do
|
||||
{ 'HTTP_PRIVATE_TOKEN' => personal_access_token.token }
|
||||
.merge(workhorse_headers)
|
||||
.tap { |h| h.delete(Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER) }
|
||||
end
|
||||
|
||||
before do
|
||||
project.add_maintainer(user)
|
||||
end
|
||||
|
||||
it_behaves_like 'returning response status', :forbidden
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'process terraform module upload' do |user_type, status, add_member = true|
|
||||
RSpec.shared_examples 'creates terraform module package files' do
|
||||
it 'creates package files', :aggregate_failures do
|
||||
expect { subject }
|
||||
.to change { project.packages.count }.by(1)
|
||||
.and change { Packages::PackageFile.count }.by(1)
|
||||
expect(response).to have_gitlab_http_status(status)
|
||||
|
||||
package_file = project.packages.last.package_files.reload.last
|
||||
expect(package_file.file_name).to eq('mymodule-mysystem-1.0.0.tgz')
|
||||
end
|
||||
end
|
||||
|
||||
context "for user type #{user_type}" do
|
||||
before do
|
||||
project.send("add_#{user_type}", user) if add_member && user_type != :anonymous
|
||||
end
|
||||
|
||||
context 'with object storage disabled' do
|
||||
before do
|
||||
stub_package_file_object_storage(enabled: false)
|
||||
end
|
||||
|
||||
context 'without a file from workhorse' do
|
||||
let(:send_rewritten_field) { false }
|
||||
|
||||
it_behaves_like 'returning response status', :bad_request
|
||||
end
|
||||
|
||||
context 'with correct params' do
|
||||
it_behaves_like 'package workhorse uploads'
|
||||
it_behaves_like 'creates terraform module package files'
|
||||
it_behaves_like 'a package tracking event', described_class.name, 'push_package'
|
||||
end
|
||||
end
|
||||
|
||||
context 'with object storage enabled' do
|
||||
let(:tmp_object) do
|
||||
fog_connection.directories.new(key: 'packages').files.create( # rubocop:disable Rails/SaveBang
|
||||
key: "tmp/uploads/#{file_name}",
|
||||
body: 'content'
|
||||
)
|
||||
end
|
||||
|
||||
let(:fog_file) { fog_to_uploaded_file(tmp_object) }
|
||||
let(:params) { { file: fog_file, 'file.remote_id' => file_name } }
|
||||
|
||||
context 'and direct upload enabled' do
|
||||
let(:fog_connection) do
|
||||
stub_package_file_object_storage(direct_upload: true)
|
||||
end
|
||||
|
||||
it_behaves_like 'creates terraform module package files'
|
||||
|
||||
['123123', '../../123123'].each do |remote_id|
|
||||
context "with invalid remote_id: #{remote_id}" do
|
||||
let(:params) do
|
||||
{
|
||||
file: fog_file,
|
||||
'file.remote_id' => remote_id
|
||||
}
|
||||
end
|
||||
|
||||
it_behaves_like 'returning response status', :forbidden
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'and direct upload disabled' do
|
||||
context 'and background upload disabled' do
|
||||
let(:fog_connection) do
|
||||
stub_package_file_object_storage(direct_upload: false, background_upload: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'creates terraform module package files'
|
||||
end
|
||||
|
||||
context 'and background upload enabled' do
|
||||
let(:fog_connection) do
|
||||
stub_package_file_object_storage(direct_upload: false, background_upload: true)
|
||||
end
|
||||
|
||||
it_behaves_like 'creates terraform module package files'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -205,6 +205,7 @@ RSpec.shared_examples 'filters on each package_type' do |is_project: false|
|
|||
let_it_be(:package9) { create(:debian_package, project: project) }
|
||||
let_it_be(:package10) { create(:rubygems_package, project: project) }
|
||||
let_it_be(:package11) { create(:helm_package, project: project) }
|
||||
let_it_be(:package12) { create(:terraform_module_package, project: project) }
|
||||
|
||||
Packages::Package.package_types.keys.each do |package_type|
|
||||
context "for package type #{package_type}" do
|
||||
|
|
Loading…
Reference in a new issue