class Deployer

Constants

CONFIG_DEFAULTS
CONFIG_LOCATIONS

Attributes

branch[R]
commit[R]
default_branch[R]
default_context[R]
image_tag[R]
images[R]
production_context[R]
registry[R]
repository[R]
root[R]
spec_dir[RW]
ssh_host[R]

Public Class Methods

docker() click to toggle source
# File lib/deployer.rb, line 23
def self.docker
  @docker_path ||= File.which('docker') || File.which('podman')
end
new(root = nil) click to toggle source
# File lib/deployer.rb, line 31
def initialize(root = nil)
  @images = []
  @specs = {}

  @root = root || Dir.pwd
  load_config
  self.branch = default_branch
end
podman?() click to toggle source
# File lib/deployer.rb, line 27
def self.podman?
  docker.include?('podman')
end

Public Instance Methods

branch=(branch) click to toggle source
# File lib/deployer.rb, line 40
def branch=(branch)
  return  unless branch
  @branch = branch.dup
  @branch.delete_prefix!('g')  if @branch.match(/^g\h{8}$/)
  @commit = git.object(@branch).sha

  @image_tag = @commit[0, 8]
  images.each do |image|
    image.commit = commit
    image.tag = image_tag
  end
end
deploy!(context) click to toggle source
# File lib/deployer.rb, line 91
def deploy!(context)
  context = (context || default_context).to_s
  specs = deploy_specs(context).presence  or raise "No kubernetes specs to deploy"
  stdout, stderr, _success = kubectl(context, 'apply -f -', YAML.dump_stream(*specs))
  puts stdout  if stdout.present?
  puts stderr  if stderr.present?
end
deploy_specs(context = nil) click to toggle source
# File lib/deployer.rb, line 99
def deploy_specs(context = nil)
  dspecs = []
  specs(context).deep_dup.each do |spec|
    containers =
      Array(spec.dig('spec', 'template', 'spec', 'containers')) +                       # deployments/statefulsets
      Array(spec.dig('spec', 'jobTemplate', 'spec', 'template', 'spec', 'containers'))  # cronjobs

    containers.each do |container|
      image = images.detect { |image|  image.remote_repo == container['image'] }
      if image
        container['image'] = image.remote_image
        dspecs << spec  unless dspecs.include?(spec)
      elsif !container['image'].include?(':')
        raise "Unknown image #{container['image']}"
      end
    end
  end
  dspecs
end
kubectl(context, cmd, data = nil) click to toggle source
# File lib/deployer.rb, line 129
def kubectl(context, cmd, data = nil)
  cmd = "kubectl --context #{context} #{cmd}"

  if ssh_host.blank?
    stdout, stderr, cmd_status = Open3.capture3(cmd, stdin_data: data)
    [ stdout, stderr, cmd_status.success? ]
  else
    require 'net/ssh'
    exit_code = -1
    stdout = String.new
    stderr = String.new

    ssh = Net::SSH.start(ssh_host)
    ssh.open_channel do |channel|
      channel.exec(cmd) do |_ch, success|
        success or raise "FAILED: couldn't execute command on #{ssh_host}: #{cmd.inspect}"
        channel.on_data { |_ch, in_data|  stdout << in_data }
        channel.on_extended_data { |_ch, _type, in_data|  stderr << in_data }
        channel.on_request('exit-status') { |_ch, in_data|  exit_code = in_data.read_long }
        channel.send_data(data)  if data
        channel.eof!
      end
    end
    ssh.loop
    [ stdout, stderr, exit_code.zero? ]
  end
end
specs(context = nil) click to toggle source
# File lib/deployer.rb, line 119
def specs(context = nil)
  spec_dir = self.spec_dir.presence || (context || default_context).to_s
  @specs[spec_dir] ||= begin
    spec_dir = "platform/#{spec_dir}/"
    paths = git.ls_tree(commit, spec_dir)['blob'].keys
    raise "No specs found in #{spec_dir}"  unless paths.present?
    paths.map { |path| YAML.load_stream( git.show(commit, path) ) }.flatten.compact
  end
end
specs_running(context = nil) click to toggle source
# File lib/deployer.rb, line 53
def specs_running(context = nil)
  context = (context || default_context).to_s
  specs = deploy_specs(context)

  cmd = String.new "--output=json"
  if (namespace = specs.first.dig('metadata', 'namespace'))
    cmd += " --namespace #{namespace}"
  end
  cmd += " get"
  specs.each do |spec|
    cmd += " #{spec['kind'].downcase}/#{spec.dig('metadata', 'name')}"
  end

  statuses, stderr, success = kubectl(context, cmd)
  unless (success || stderr.match(/not found/)) && statuses.present?
    puts stderr  if stderr.present?
    return nil
  end

  spec_status = specs.map { |spec|  [ spec, nil ] }.to_h
  statuses = JSON.parse(statuses)
  statuses = statuses['items']  if statuses.key?('items')
  Array.wrap(statuses).each do |item|
    containers = Array(item.dig('spec', 'template', 'spec', 'containers')) +
                 Array(item.dig('spec', 'jobTemplate', 'spec', 'template', 'spec', 'containers'))
    version = containers.first['image'].split(':').last               # FIXME: support multiple containers
    status = item.delete('status').with_indifferent_access

    spec = specs.detect do |s|
      (item['kind'] == s['kind']) &&
      (item.dig('metadata', 'name') == s.dig('metadata', 'name')) &&
      (item.dig('metadata', 'namespace') == (s.dig('metadata', 'namespace') || 'default'))
    end
    spec_status[spec] = { spec: item, version: version, status: status }
  end
  spec_status
end

Private Instance Methods

find_config(dir_or_path) click to toggle source
# File lib/deployer.rb, line 180
def find_config(dir_or_path)
  return dir_or_path  unless File.directory?(dir_or_path)
  CONFIG_LOCATIONS.map { |location|  File.join(dir_or_path, location) }.detect { |path|  File.exist?(path) }
end
git() click to toggle source
# File lib/deployer.rb, line 159
def git
  @git ||= Git.open(repository, log: nil)
end
load_config() click to toggle source
# File lib/deployer.rb, line 163
def load_config
  conf_path = find_config(root)
  conf = conf_path ? YAML.load_file(conf_path) : {}
  conf = conf.reverse_merge(CONFIG_DEFAULTS)

  @repository, @registry, @default_branch, @default_context, @production_context, @ssh_host, images =
    conf.values_at('repository', 'registry', 'default_branch', 'default_context', 'production_context', 'ssh', 'images').map(&:presence)
  @repository ||= root

  images ||= [{ 'name' => File.basename(File.absolute_path(repository)) }]
  @images = images.map do |image|
    name = image['name']
    dockerfile = image['dockerfile'].presence || 'Dockerfile'
    Image.new(name: name, repository: repository, dockerfile: dockerfile, registry: registry, commit: nil, tag: nil)
  end
end