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