lib/cell_manager.rb (174 lines of code) (raw):
# frozen_string_literal: true
class CellManager
include GDK::CoreHelper::DeepHash
PORT_BASE_OFFSET = 12_000
PORT_OFFSET = 150
LEGACY_CELL_ID = 1
LEGACY_CELL_SEQUENCE_MAXVAL = (2**63) - 1
DEFAULT_SEQUENCE_RANGE = 100_000_000_000
AUTO_GENERATED_HEADER = <<~TEXT
# This configuration was generated by `gdk cells up` on %<date>s.
#
# Do not change this file!
# Change `%<main_gdk>s` instead and rerun the command.
TEXT
Result = Struct.new('Result', :success?)
def up
return true unless enabled?
cells.all? do |cell|
break unless gdk_build(cell)
sh = run_in_cell(cell[:id], %w[reconfigure], skip_directory_check: true)
if sh.success?
GDK::Output.success("Cell #{cell[:id]} is ready.")
else
GDK::Output.error("Cell #{cell[:id]} failed to reconfigure.")
end
sh.success?
end
end
def update
return true unless enabled?
cells.all? do |cell|
GDK::Output.info("Updating cell #{cell[:id]}")
run_in_cell(cell[:id], %w[update]).success?
end
end
def start
return true unless enabled?
cells.all? do |cell|
run_in_cell(cell[:id], %w[start]).success?
end
end
def stop
return true unless enabled?
cells.all? do |cell|
run_in_cell(cell[:id], %w[stop]).success?
end
end
def restart
return true unless enabled?
cells.all? do |cell|
run_in_cell(cell[:id], %w[restart]).success?
end
end
def status
return true unless enabled?
cells.all? do |cell|
GDK::Output.info("cell-#{cell[:id]}")
run_in_cell(cell[:id], %w[status]).success?
end
end
def run_in_cell(cell_id, args, quiet: false, skip_directory_check: false)
return Result.new(false) unless enabled?
cell, _index = find_cell(cell_id)
unless cell
GDK::Output.error("Cell #{cell_id} not found. #{help_cell_list}")
return Result.new(false)
end
dir = directory_for_cell(cell)
unless skip_directory_check || Dir.exist?(dir)
GDK::Output.error("Cell #{cell[:id]} doesn’t exist yet, run `gdk cells up` first.")
return Result.new(false)
end
opts = { chdir: dir }
sh = GDK::Shellout.new("gdk #{args.join ' '};", **opts)
sh.execute(display_output: !quiet)
sh
end
def enabled?
GDK.config.cells.enabled
end
def cell_exist?(id)
!!find_cell(id)
end
def get_config_for(id)
cell = find_cell(id)
raise "No config for cell `#{id}` found" unless cell
cell_gdk_root = Pathname.new(directory_for_cell(cell))
cell_gdk_config_path = cell_gdk_root.join('gdk.yml')
merged_cell_config(cell_gdk_config_path, cell)
end
private
def find_cell(id)
cells.find { |cell| cell[:id] == id }
end
def port_offset_for(index)
PORT_BASE_OFFSET + (index * PORT_OFFSET)
end
def gdk_build(cell)
cell_gdk_root = directory_for_cell(cell)
if Dir.exist?(cell_gdk_root)
write_cell_config(cell_gdk_root, cell)
else
GDK::Output.info("Cloning into GDK for #{cell[:id]}")
sh = GDK::Shellout.new(*%W[git clone #{GDK.root} #{cell_gdk_root}])
sh.execute
return false unless sh.success?
sh = GDK::Shellout.new(*%w[git remote get-url origin], chdir: GDK.root)
sh.execute
original_origin = sh.success? ? sh.read_stdout.chomp : 'https://gitlab.com/gitlab-org/gitlab-development-kit.git'
sh = GDK::Shellout.new(*%W[git remote set-url origin #{original_origin}], chdir: cell_gdk_root)
sh.execute
return false unless sh.success?
write_cell_config(cell_gdk_root, cell)
return false unless gdk_install(cell)
end
true
end
def write_cell_config(cell_gdk_root, cell)
cell_gdk_config_path = "#{cell_gdk_root}/gdk.yml"
# Always start with an empty file
FileUtils.rm_f(cell_gdk_config_path)
config = merged_cell_config(cell_gdk_config_path, cell)
config.save_yaml!
header = format(AUTO_GENERATED_HEADER, date: Time.now, main_gdk: GDK::Config::FILE)
File.write(cell_gdk_config_path, header + File.read(cell_gdk_config_path))
end
def merged_cell_config(cell_gdk_config_path, cell)
yaml = deep_merge(main_gdk_config_yaml, cell_instance_config(cell))
config = cell_config_for(cell_gdk_config_path, yaml)
config.validate!
config
end
def main_gdk_config_yaml
yaml = GDK.config.yaml.except('cells')
# deep dup to avoid side-effects
JSON.parse(JSON.dump(yaml))
end
# Returns a config value:
# - Cell-specific value if it's defined in cell's gdk.yml
# - Main's value otherwise if +fallback_value+ is a GDK::Config
# - Passed +fallback_value+ otherwise
def value_for(slug, config, fallback_value)
if config.user_defined?(slug)
config.dig(slug) # rubocop:disable Style/SingleArgumentDig -- That's a different dig
else
fallback_value.dig(slug) # rubocop:disable Style/SingleArgumentDig -- That's a different dig
end
end
def cell_instance_config(cell)
yaml = cell.config || {}
# Ensure that old/non-synced cells GDKs still work as expected
# See https://gitlab.com/gitlab-org/gitlab-development-kit/-/issues/2309
yaml['port_offset'] = port_offset_for(cell.__index)
yaml['cells'] ||= {}
yaml['cells']['port_offset'] = yaml['port_offset']
config = cell.root.class.new(yaml: yaml).tap(&:validate!)
config.dump!(user_only: true)
end
def gdk_install(cell)
pathname = directory_for_cell(cell)
return if Dir.exist?("#{pathname}/gitlab")
gitlab_repo = "#{GDK.root}/gitlab"
# Force shell invocation by adding ';'
# This is necessary to make `gdk` command work in the right directory.
# TODO use YAML value, or default
cmd = "gdk install gitlab_repo=#{gitlab_repo};"
sh = GDK::Shellout.new(cmd, chdir: pathname)
sh.execute(display_output: true)
raise 'Failed to gdk install' unless sh.success?
true
end
def help_cell_list
return 'Check doc/howto/cells.md on how to add local cell instances.' if cells.empty?
"Found: #{cells.map(&:id).join(', ')}."
end
def cells
GDK.config.cells.instances.elems
end
def directory_for_cell(cell)
"#{GDK.root}/gitlab-cells/cell-#{cell[:id]}"
end
def cell_config_for(gdk_config_path, yaml)
# Currently, there's no good way of passing GDK root and config path to config
# so we override constants and assign YAML just for Cells.
# See https://gitlab.com/gitlab-org/gitlab-development-kit/-/merge_requests/4027#note_2148267092
klass = Class.new(GDK::Config)
klass.const_set(:FILE, gdk_config_path)
# Avoid messing up the superclass (i.e. `GDK::Config`)
klass.attributes = GDK::Config.attributes.dup
config = klass.load_from_file
config.instance_variable_set(:@yaml, yaml)
config
end
end