lib/release_tools/public_release/helm_gitlab_release.rb (274 lines of code) (raw):
# frozen_string_literal: true
module ReleaseTools
module PublicRelease
# A release of Helm using the API.
class HelmGitlabRelease
include Release
attr_reader :version, :client, :release_metadata, :gitlab_version
# The Markdown file that maps Helm versions to GitLab versions.
VERSION_MAPPING_FILE = 'doc/installation/version_mappings.md'
# The name of the file containing the Chart information.
CHART_FILE = 'Chart.yaml'
# The name of the file containing the Chart's default values.
VALUES_FILE = 'values.yaml'
# The default branch of the charts project.
DEFAULT_BRANCH = Project::HelmGitlab.default_branch
# The file the chart uses to validate required stops are respected
RUN_CHECK_TPL = 'templates/_runcheck.tpl'
# The charts that we manage, and the source of their appVersion fields.
#
# The keys are the names of the directories the chart files reside in. The
# directory "." is used for the top-level chart file.
#
# The following values are available:
#
# * `:gitlab_version`: appVersion will be populated with the GitLab Rails
# version.
# * `:unmanaged`: appVersion is left as-is, and only the charts version is
# updated. This is useful for components where appVersion is managed
# separate from Release Tools.
# * a `String`: the path/name of the VERSION file to read in the GitLab EE
# repository. The contents of this file will be used to populate the
# appVersion field.
CHART_APP_VERSION_SOURCES = {
'.' => :gitlab_version,
'geo-logcursor' => :gitlab_version,
'gitlab-grafana' => :gitlab_version,
'migrations' => :gitlab_version,
'operator' => :gitlab_version,
'sidekiq' => :gitlab_version,
'spamcheck' => :unmanaged,
'toolbox' => :gitlab_version,
'webservice' => :gitlab_version,
'mailroom' => :gitlab_version,
'gitlab-exporter' => :unmanaged,
'gitaly' => Project::Gitaly.version_file,
'gitlab-shell' => Project::GitlabShell.version_file,
'kas' => Project::Kas.version_file,
'praefect' => Project::Gitaly.version_file,
'gitlab-pages' => Project::GitlabPages.version_file,
# TODO: Remove when 14.5 is the earliest supported version (i.e., 14.8)
# See https://gitlab.com/groups/gitlab-org/charts/-/epics/25
'task-runner' => :gitlab_version
}.freeze
def initialize(
version,
gitlab_version,
client: GitlabClient,
release_metadata: ReleaseMetadata.new,
commit: nil
)
@version = version
@gitlab_version = gitlab_version.to_ee
@client = client
@release_metadata = release_metadata
@commit = commit
end
def execute
if gitlab_version.rc?
logger.info('Not releasing Helm for an RC', version: version, gitlab_version: gitlab_version)
return
end
logger.info('Starting release of Helm', version: version, gitlab_version: gitlab_version)
create_target_branch
return if SharedStatus.dry_run?
compile_changelog
update_versions
update_version_mapping
update_last_upgrade_stop
tag = create_tag
add_release_metadata(tag)
notify_slack(project, version)
end
def compile_changelog
return if version.rc?
logger.info('Compiling changelog', project: project_path)
ChangelogCompiler
.new(project_path, client: client)
.compile(version, branch: target_branch)
end
def update_versions
stable_charts = all_chart_files
default_charts = all_chart_files(DEFAULT_BRANCH)
# We validate first so we either update all charts, or none at all. This
# way we notice unrecognised charts as early as possible.
verify_chart_files(stable_charts)
verify_chart_files(default_charts)
update_target_branch_versions(stable_charts)
update_default_branch_versions(default_charts)
end
def update_target_branch_versions(charts)
# Update Chart files and root values.yaml on target branch.
update = update_chart_files(charts)
update.merge!(update_global_version_value(target_branch))
commit_version_files(target_branch, update, message: version_files_commit_message)
end
def update_default_branch_versions(charts)
# On the default branch we need to only update the "version" field of
# the Chart file.
update = update_default_chart_files(charts)
commit_version_files(DEFAULT_BRANCH, update, message: version_files_commit_message)
end
def verify_chart_files(files)
files.each do |file|
directory = File.dirname(file)
name = File.basename(directory)
next if CHART_APP_VERSION_SOURCES.key?(name)
logger.error('Aborting Helm release due to unrecognised chart directory', directory: directory)
raise <<~ERROR
The chart located at #{directory} is not recognised by Release Tools.
You will have to add its directory name (#{name}) to the
CHART_APP_VERSION_SOURCES constant in this file, with the correct
source for its appVersion field. For example, if the chart uses
the same appVersion as the GitLab Rails version, add the following
entry:
'#{name}' => :gitlab_version
ERROR
end
end
def update_chart_files(files)
to_update = {}
files.each do |file|
name = File.basename(File.dirname(file))
yaml = read_file(file)
hash = YAML.safe_load(yaml)
source = CHART_APP_VERSION_SOURCES[name]
app_version =
case source
when :gitlab_version
# parseAppVersion won't add a `v` if `prepend` is true or false:
# https://gitlab.com/gitlab-org/charts/gitlab/-/blob/9dc2e51bc7d6bbc775ebd2901aea22ca11f416e6/charts/gitlab/templates/_helpers.tpl#L27-32
"v#{gitlab_version.to_normalized_version}"
when :unmanaged
# If a chart's appVersion is unmanaged, we still want to update
# the version field. So instead of using something like `next`, we
# return `nil` and handle this below.
nil
else
read_ee_file(source)
end
hash['appVersion'] = app_version if app_version
hash['version'] = version.to_s
to_update[file] = YAML.dump(hash)
end
to_update
end
def update_default_chart_files(files)
to_update = {}
files.each do |file|
yaml = read_file(file, branch: DEFAULT_BRANCH)
hash = YAML.safe_load(yaml)
# We don't want to overwrite the version with an older one. For
# example:
#
# * On the default branch the `version` field 2.0.0
# * We are running a patch release for 1.5.0
#
# In a case like this, the `version` field is left as-is, instead of
# being (incorrectly) set to 1.5.0.
next unless version > Version.new(hash['version'])
hash['version'] = version.to_s
to_update[file] = YAML.dump(hash)
end
to_update
end
def update_global_version_value(branch = target_branch)
gitlab_version_regex = /(((?=#)# *|)gitlabVersion:[^\n]*)/
global_values_file = VALUES_FILE
values = read_file(global_values_file, branch: branch)
# Replace global.gitlabVersion preserving comments.
values.sub!(gitlab_version_regex, "gitlabVersion: \"#{gitlab_version.to_normalized_version}\"")
{ global_values_file => values }
end
def update_version_mapping
update_version_mapping_on_branch(target_branch)
# For the default branch we can't reuse the Markdown of the target
# branch, as both branches may have different content in these files.
# For example, when running a patch release for an older version the
# target branch won't have entries for newer versions.
update_version_mapping_on_branch(DEFAULT_BRANCH)
end
def update_version_mapping_on_branch(branch)
mapping = updated_version_mapping(branch)
commit_version_files(
branch,
{ VERSION_MAPPING_FILE => mapping.to_s },
message: "Update version mapping for #{version}"
)
end
def updated_version_mapping(branch)
@updated_version_mapping ||= {}
@updated_version_mapping[branch] ||= begin
markdown = read_file(VERSION_MAPPING_FILE, branch: branch)
mapping = Helm::VersionMapping.parse(markdown)
mapping.add(version, gitlab_version)
mapping
end
end
def create_tag
logger.info('Creating tag', tag: tag_name, project: project_path)
client.find_or_create_tag(
project_path,
tag_name,
target_branch,
message: "Version #{tag_name} - contains GitLab EE #{gitlab_version.to_normalized_version}"
)
end
def add_release_metadata(tag)
meta_version = version.to_normalized_version
logger.info('Recording release data', project: project_path, version: meta_version, tag: tag.name)
release_metadata.add_release(
name: 'helm-gitlab',
version: meta_version,
sha: tag.commit.id,
ref: tag.name,
tag: true
)
end
def update_last_upgrade_stop
return if ReleaseTools::Feature.disabled?(:maintain_upgrade_stops)
begin
stop = UpgradeStop.new.last_required_stop
chart_stop = required_stop_chart_version(stop)
logger.info('Last required stop for version detected', last_required_stop: stop)
logger.info('Last required stop for chart version calculated', last_required_stop: chart_stop)
action = change_upgrade_stop_action!(stop, chart_stop)
if action.nil?
logger.info('Nothing to be done to apply upgrade stop to GitLab Helm chart',
version: version.to_minor, last_required_stop: stop)
return
end
Retriable.with_context(:api) do
client.create_commit(
project_path,
project.default_branch,
"Update upgrade stop to #{stop}\n\n[ci skip]",
[action]
)
end
rescue StandardError => ex
error_msg = <<~ERROR
Something went wrong with the GitLab Helm chart upgrade stop.
Disable the 'maintain_upgrade_stops' feature flag, and retry the job.
Please notify Distribution about this error.
ERROR
logger.fatal(error_msg, error: ex)
raise
end
end
def change_upgrade_stop_action!(stop, chart_stop)
gitlab_version_regex = /(?<=[\s]|^)MIN_VERSION=[^\n]*/
chart_version_regex = /(?<=[\s]|^)CHART_MIN_VERSION=[^\n]*/
content = read_file(RUN_CHECK_TPL, branch: project.default_branch)
raise_regex_failure(gitlab_version_regex) unless gitlab_version_regex.match?(content)
raise_regex_failure(chart_version_regex) unless chart_version_regex.match?(content)
new_content = content
.sub(gitlab_version_regex, "MIN_VERSION=#{stop}")
.sub(chart_version_regex, "CHART_MIN_VERSION=#{chart_stop}")
return nil if content == new_content
{
action: 'update',
file_path: RUN_CHECK_TPL,
content: new_content
}
end
def raise_regex_failure(regexp)
raise "The regexp '#{regexp}' does not match in #{RUN_CHECK_TPL}."
end
def raise_required_stop_is_not_mapped(version)
raise "We can't make #{version} a required stop because it was not detected in the Helm Chart version mapping."
end
def required_stop_chart_version(gitlab_version_stop)
mapping = updated_version_mapping(DEFAULT_BRANCH)
# TODO: Currently the Helm chart does not support required stops based on patch versions
# Since this method receives a Version#to_minor, but our version mapping
# has only full versions, we temporarily recreate the full_version with patch `.0`,
# just so we're able to detect the version in the version mapping.
gitlab_version_stop_full_version = ReleaseTools::Version.new(gitlab_version_stop).to_patch
chart_version = mapping.rows.find { |r| r.gitlab_version == gitlab_version_stop_full_version }&.helm_version
raise raise_required_stop_is_not_mapped(gitlab_version_stop_full_version) unless chart_version
chart_version.to_minor
end
def read_file(file, project: project_path, branch: target_branch)
Retriable.with_context(:api) do
client.file_contents(project, file, branch).strip
end
end
def read_ee_file(file)
project = Project::GitlabEe.canonical_or_security_path
branch = gitlab_version.stable_branch(ee: true)
read_file(file, project: project, branch: branch)
end
def all_chart_files(branch = target_branch)
# We use a Set here so that if we retry the operation below, we don't
# end up producing duplicate paths.
paths = Set.new([CHART_FILE])
Retriable.with_context(:api) do
client
.tree(project_path, ref: branch, path: 'charts/gitlab/charts', per_page: 100)
.auto_paginate do |entry|
paths << File.join(entry.path, CHART_FILE) if entry.type == 'tree'
end
end
paths.to_a
end
def project
Project::HelmGitlab
end
def source_for_target_branch
logger.info('Using specific commit', project: project_path, commit: @commit) if @commit
@commit || DEFAULT_BRANCH
end
def version_files_commit_message
"Update versions for #{version}\n\n[ci skip]"
end
end
end
end