class Seira::App

Constants

SUMMARY
VALID_ACTIONS

Attributes

action[R]
app[R]
args[R]
context[R]

Public Class Methods

new(app:, action:, args:, context:) click to toggle source
# File lib/seira/app.rb, line 16
def initialize(app:, action:, args:, context:)
  @app = app
  @action = action
  @args = args
  @context = context
end

Public Instance Methods

ask_cluster_for_current_revision() click to toggle source
# File lib/seira/app.rb, line 56
def ask_cluster_for_current_revision
  tier = context[:settings].config_for_app(app)['golden_tier'] || 'web'
  current_image = kubectl("get deployment -l app=#{app},tier=#{tier} -o=jsonpath='{$.items[:1].spec.template.spec.containers[:1].image}'", context: context, return_output: true).strip.chomp
  current_revision = current_image.split(':').last
  current_revision
end
run() click to toggle source
# File lib/seira/app.rb, line 23
def run
  case action
  when 'help'
    run_help
  when 'bootstrap'
    run_bootstrap
  when 'apply'
    run_apply
  when 'restart'
    run_restart
  when 'scale'
    run_scale
  when 'revision'
    run_revision
  else
    fail "Unknown command encountered"
  end
end
run_help() click to toggle source
# File lib/seira/app.rb, line 42
def run_help
  puts SUMMARY
  puts "\n\n"
  puts "Possible actions:\n\n"
  puts "bootstrap: Create new app with main secret, cloudsql secret, and gcr secret in the new namespace."
  puts "apply: Apply the configuration in kubernetes/<cluster-name>/<app-name> using first argument or REVISION environment variable to find/replace REVISION in the YAML."
  puts "restart: Forces a rolling deploy for any deployment making use of RESTARTED_AT_VALUE in the deployment."
  puts "scale: Scales the given tier deployment to the specified number of instances."
end
run_restart() click to toggle source
# File lib/seira/app.rb, line 52
def run_restart
  run_apply(restart: true)
end

Private Instance Methods

bootstrap_main_secret() click to toggle source
# File lib/seira/app.rb, line 175
def bootstrap_main_secret
  puts "Creating main secret and namespace..."
  main_secret_name = Seira::Secrets.new(app: app, action: action, args: args, context: context).main_secret_name

  # 'internal' is a unique cluster/project "cluster". It always means production in terms of rails app.
  rails_env = Helpers.rails_env(context: context)

  kubectl("create secret generic #{main_secret_name}", context: context)
end
find_and_replace_revision(source:, destination:, replacement_hash:) click to toggle source
# File lib/seira/app.rb, line 185
def find_and_replace_revision(source:, destination:, replacement_hash:)
  puts "Copying source yaml from #{source} to temp folder"
  FileUtils.mkdir_p destination # Create the nested directory
  FileUtils.rm_rf("#{destination}/.", secure: true) # Clean out old files from the tmp folder

  # Iterate through each yaml file and find/replace and save
  puts "Iterating temp folder files find/replace revision information"
  Dir.foreach(source) do |item|
    next if item == '.' || item == '..'

    # If we have run into a directory item, skip it
    next if File.directory?("#{source}/#{item}")

    # Skip any manifest file that has "seira-skip.yaml" in the file name (ERB or not). Common use case is for Job definitions
    # to be used in "seira staging <app> jobs run"
    next if item.include?("seira-skip.yaml")

    text = File.read("#{source}/#{item}")

    # First run it through ERB if it should be
    if item.end_with?('.erb')
      locals = {}.merge(replacement_hash)
      renderer = Seira::Util::ResourceRenderer.new(template: text, context: context, locals: locals)
      text = renderer.render
    end

    # Then run through old basic find/replace tempalating.
    # TODO: Do deprecation process for old home-made templating
    new_contents = text
    replacement_hash.each do |key, value|
      new_contents.gsub!(key, value)
    end

    target_name = item.gsub('.erb', '')

    # To write changes to the file, use:
    File.open("#{destination}/#{target_name}", 'w') { |file| file.write(new_contents) }
  end
end
key_value_map() click to toggle source

TODO(josh): factor out and share this method with similar commands (e.g. `secrets set`)

# File lib/seira/app.rb, line 226
def key_value_map
  args.map do |arg|
    equals_index = arg.index('=')
    [arg[0..equals_index - 1], arg[equals_index + 1..-1]]
  end.to_h
end
load_configs() click to toggle source
# File lib/seira/app.rb, line 233
def load_configs
  directory = "kubernetes/#{context[:cluster]}/#{app}/"
  Dir.new(directory).flat_map do |filename|
    next if File.directory?(File.join(directory, filename))
    YAML.load_stream(File.read(File.join(directory, filename)))
  end.compact
end
run_apply(restart: false) click to toggle source

Kube vanilla based upgrade

# File lib/seira/app.rb, line 76
def run_apply(restart: false)
  async = false
  revision = nil
  deployment = :all

  warn_env = context[:settings].expected_environment_variable_during_deploys
  if warn_env && ENV[warn_env].nil?
    exit(1) unless HighLine.agree("WARNING: Expected '#{warn_env}' to be present, but is not. This might mean your are running a deploy in an environment that is not safe. Are you sure you want to continue? This command will apply your *local* kubernetes configs. If you continue, make sure you have the latest master changes and valid local YAML.")
  end

  args.each do |arg|
    if arg == '--async'
      async = true
    elsif arg.start_with? '--deployment='
      deployment = arg.split('=')[1]
    elsif revision.nil?
      revision = arg
    else
      puts "Warning: unrecognized argument #{arg}"
    end
  end

  Dir.mktmpdir do |dir|
    destination = "#{dir}/#{context[:cluster]}/#{app}"
    revision ||= ENV['REVISION']

    if revision.nil?
      current_revision = ask_cluster_for_current_revision
      exit(1) unless HighLine.agree("No REVISION specified. Use current deployment revision '#{current_revision}'?")
      revision = current_revision
    end

    replacement_hash = {
      'REVISION' => revision,
      'RESTARTED_AT_VALUE' => "Initial Deploy for #{revision}"
    }

    if restart
      replacement_hash['RESTARTED_AT_VALUE'] = Time.now.to_s
    end

    replacement_hash.each do |k, v|
      next unless v.nil? || v == ''
      puts "Found nil or blank value for replacement hash key #{k}. Aborting!"
      exit(1)
    end

    find_and_replace_revision(
      source: "kubernetes/#{context[:cluster]}/#{app}",
      destination: destination,
      replacement_hash: replacement_hash
    )

    to_apply = destination
    to_apply += "/#{deployment}.yaml" unless deployment == :all
    kubectl("apply -f #{to_apply}", context: context)

    unless async
      puts "Monitoring rollout status..."
      # Wait for rollout of all deployments to complete (running `kubectl rollout status` in parallel via xargs)
      rollout_wait_command =
        if deployment == :all
          "kubectl get deployments -n #{app} -o name | xargs -n1 -P10 kubectl rollout status -n #{app}"
        else
          "kubectl rollout status -n #{app} deployments/#{app}-#{deployment}"
        end
      exit 1 unless system(rollout_wait_command)
    end
  end
end
run_bootstrap() click to toggle source
# File lib/seira/app.rb, line 65
def run_bootstrap
  # TODO: Verify that 00-namespace exists
  # TODO: Do conformance test on the yaml files before running anything, including that 00-namespace.yaml exists and has right name
  # Create namespace before anything else
  kubectl("apply -f kubernetes/#{context[:cluster]}/#{app}/00-namespace.yaml", context: context)
  bootstrap_main_secret

  puts "Successfully installed"
end
run_revision() click to toggle source
# File lib/seira/app.rb, line 171
def run_revision
  puts ask_cluster_for_current_revision
end
run_scale() click to toggle source
# File lib/seira/app.rb, line 147
def run_scale
  scales = key_value_map.dup
  configs = load_configs

  if scales.key? 'all'
    configs.each do |config|
      next unless config['kind'] == 'Deployment'
      scales[config['metadata']['labels']['tier']] ||= scales['all']
    end
    scales.delete 'all'
  end

  scales.each do |tier, replicas|
    config = configs.find { |c| c['kind'] == 'Deployment' && c['metadata']['labels']['tier'] == tier }
    if config.nil?
      puts "Warning: could not find config for tier #{tier}"
      next
    end
    replicas = config['spec']['replicas'] if replicas == 'default'
    puts "scaling #{tier} to #{replicas}"
    kubectl("scale --replicas=#{replicas} deployments/#{config['metadata']['name']}", context: context)
  end
end