Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-05-14 00:11:05 +00:00
parent 52b32eecb7
commit 980faa8f34
93 changed files with 2945 additions and 487 deletions

View file

@ -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" />

View 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>

View file

@ -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

View file

@ -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

View file

@ -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)

View 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

View file

@ -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?

View file

@ -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

View file

@ -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|

View file

@ -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

View file

@ -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

View file

@ -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] }

View file

@ -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) }

View 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

View file

@ -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

View file

@ -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'

View file

@ -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('&nbsp;' * 3 + '|' + '&nbsp;' * 4).html_safe
= @message.footer_links.join('&nbsp;' * 3 + '|' + '&nbsp;' * 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

View file

@ -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 %>

View file

@ -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')

View file

@ -0,0 +1,5 @@
---
title: Add body to finding evidence requests
merge_request: 61408
author:
type: changed

View file

@ -0,0 +1,5 @@
---
title: Allows masking ~ character.
merge_request: 61517
author: Thomas Dallmair
type: changed

View file

@ -0,0 +1,5 @@
---
title: Add Terraform Module Registry
merge_request: 55018
author:
type: added

View 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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -0,0 +1 @@
949038f9f66788e3289afbf210617f7947762e4bbab4c7389164cbd775302642

View file

@ -0,0 +1 @@
e59505ee2a3ef04c1af8a426f7ebdb83874c926cf7d7f98b56e0af8cd38988f5

View file

@ -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))
);

View file

@ -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.

View file

@ -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`

View file

@ -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).

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View 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

View 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

View file

@ -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?

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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 ""

View file

@ -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

View file

@ -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

View file

@ -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|

View file

@ -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 cant edit files directly in this project.")
end
after do

View file

@ -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}" }

View file

@ -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' }

View file

@ -0,0 +1,12 @@
{
"type": "object",
"required" : ["versions"],
"optional" : ["source"],
"properties" : {
"source": { "type": "string" },
"versions": {
"minItems": 0,
"items": { "$ref": "./version.json" }
}
}
}

View file

@ -0,0 +1,4 @@
{
"type": "array",
"items": { "$ref": "./module.json" }
}

View 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"
}
}
}
}
}
}
}
}

View file

@ -0,0 +1,9 @@
{
"type": "object",
"required" : ["modules"],
"properties" : {
"modules": {
"items": { "$ref": "./module.json" }
}
}
}

Binary file not shown.

View file

@ -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', () => {

View 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();
});
});
});

View file

@ -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 () => {

View file

@ -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

View file

@ -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 }

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View 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

View file

@ -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 }

View file

@ -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|

View file

@ -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

View file

@ -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

View file

@ -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') }

View 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

View file

@ -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) }

View 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

View file

@ -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,

View 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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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