service/src/service/service.cr

516 lines
10 KiB
Crystal
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

require "yaml"
require "colorize"
require "file_utils"
require "./service_definition.cr"
require "./environment.cr"
require "./libc.cr"
def split_command(string)
args = string.split /\ (?=([^\"]*\"[^\"]*\")*[^\"]*$)/
command = args[0]
args.delete_at 0
return command, args
end
class Service
getter environment : Environment
getter providers = ProvidersList.new
property domain : String?
getter ports = Hash(String, Int32).new
# The place well store configuration and data.
@root : String?
class Exception < ::Exception
end
class ProvidersList < Hash(String, String)
def []?(name)
super(name).try { |x| Service.get_by_id x}
end
end
struct Consumer
getter token : String
getter from : String
def initialize(@token, @from)
end
end
def initialize(name, environment_name : String?)
@reference = ServiceDefinition.get name
@environment = if environment_name.nil? || environment_name == ""
Environment.root
else
Environment.get environment_name
end
end
def initialize(specs : SpecParser)
assignments = specs.assignments
@reference = ServiceDefinition.get assignments["name"].as_s
@domain = assignments["domain"]?.try &.as_s
env = assignments["environment"]?.try &.as_s
@environment = if env.nil? || env == ""
Environment.root
else
Environment.get env
end
@root = assignments["root"]?.try &.as_s
assignments["ports"]?.try &.as_a_or_s.each do |port_string|
match = port_string.match /(.*)=(.*)/
if match.nil?
STDERR.puts "warning: '#{id}' has invalid port strings"
next
end
_, name, number = match
number = number.to_i
@ports[name] = number
end
specs.sections.select(&.name.==("consumes")).each do |section|
env, provider = Service.parse_id section.content["from"].as_s
@providers[section.options[0]] = "#{env}/#{provider}"
end
end
def to_spec
file = [
"name: #{@reference.name}",
"environment: #{@environment.name}"
]
if @ports.size > 0
ports_list = @ports.map do |name, number|
"#{name}=#{number}"
end.join ", "
file << "ports: #{ports_list}"
end
if @root
file << "root: #{@root}"
end
if @domain
file << "domain: #{@domain}"
end
@providers.each do |token, provider|
file << "%consumes #{token}"
file << " from: #{provider}"
end
file.join("\n") + "\n"
end
def full_id
"#{@environment.name}/#{name}"
end
def id
if @environment.name == "root"
name
else
full_id
end
end
# FIXME: At this point, macros would be both more readable and shorter.
def name
@reference.name
end
def type
@reference.type
end
def command
@reference.command
end
def stop_command
@reference.stop_command
end
def provides
@reference.provides
end
def consumes
@reference.consumes
end
def requires_domain
@reference.requires_domain
end
def port_definitions
@reference.port_definitions
end
def root
@root || "#{@environment.root}/#{name}"
end
def provides?(token)
provides.any? do |provider|
provider.token == token
end
end
private def build_environment
env = {} of String => String
env["SERVICE_ROOT"] = root
env["SERVICE_ID"] = full_id
env["ENVIRONMENT"] = @environment.name
env["ENVIRONMENT_TYPE"] = @environment.type.to_s
@providers.each do |token, provider|
service_provider = Service.get_by_id provider
# FIXME: Warning?
next if service_provider.nil?
env["#{token.upcase}_PROVIDER"] = provider
env["#{token.upcase}_ENVIRONMENT"] = service_provider.environment.name
env["#{token.upcase}_ROOT"] = service_provider.root
end
env["SERVICE_TOKENS"] = @providers.to_a.map(&.[0]).join ':'
# FIXME: Parsing should probably be done… when parsing the file.
# FIXME: Parsing is probably a bit primitive. Maybe this isnt the right way of defining this.
@reference.environment_variables.each do |string|
# FIXME: Should probably deserve a warning.
variable = string.match(/^[^=]*=/).not_nil![0]
value = string[variable.size..string.size]
variable = variable[0..variable.size-2]
env[variable] = value
end
env
end
# FIXME: Is working on ${} really a good idea?
private def evaluate(string)
string.gsub /\${[a-zA-Z_]+}/ do |match|
match = match[2..match.size-2]
if match.downcase == "environment"
@environment.name
elsif match.downcase == "service_root"
root
else
""
end
end
end
def pre_start_hooks
@environment.pre_start_hooks + @reference.pre_start_hooks
end
def start(pid_dir : String, log_dir : String)
pre_start_hooks.each do |hook|
run_hook = false
hook.unless_file.try do |file|
file = evaluate file
run_hook = true if ! File.exists? file
end
hook.unless_directory.try do |directory|
directory = evaluate directory
run_hook = true if ! Dir.exists? directory
end
unless run_hook
next
end
puts " - #{hook.name}"
child = Process.fork do
Process.exec "sh", ["-c", hook.command],
output: Process::Redirect::Inherit,
error: Process::Redirect::Inherit,
env: build_environment
end.wait
if child.exit_status != 0
raise Service::Exception.new "Child process exited with status “#{child.exit_status}”."
break
end
end
# FIXME: Should evaluate be used in split_command? What namespace should split_command use?
command, args = split_command command
args.map! do |arg| evaluate arg end
process = Process.fork do
base_log_name = "#{log_dir}/#{name}.#{@environment.name}"
stdout_file = File.open "#{base_log_name}.out", "w"
stderr_file = File.open "#{base_log_name}.err", "w"
LibC.dup2 stdout_file.fd, 1
LibC.dup2 stderr_file.fd, 2
@reference.user.try do |user|
unless System.become_user user
STDERR << "service: child could not setuid() to user '#{user}'.\n"
exit 1
end
end
Process.exec command, args,
chdir: (@reference.directory.try { |x| evaluate x } || root),
env: build_environment
end
self.save_pid pid_dir, process.pid
end
# TODO:
# - Custom shutdown commands.
# - Should we wait for the process to die?
# - Shouldnt we remove the pid file?
def stop(pid_dir : String)
_pid = pid pid_dir
if _pid
command = stop_command
if command
command, args = split_command command
Process.run(command, args)
else
Process.kill Signal::TERM, _pid
end
else
# Already stopped or dead, nothing to be done here.
end
end
def get_pid_file(pid_dir)
"#{pid_dir}/#{name}.#{environment.name}.pid"
end
def pid(pid_dir)
File.read(get_pid_file pid_dir).to_i
rescue e # pid file missing, corrupted or otherwise not readable
nil
end
def save_pid(pid_dir, new_pid)
FileUtils.mkdir_p pid_dir
File.write get_pid_file(pid_dir), new_pid
end
enum Status
Running
Dead
Stopped
end
def status(pid_dir)
_pid = pid pid_dir
if _pid
if Process.exists? _pid
Status::Running
else
Status::Dead
end
else
Status::Stopped
end
end
def running?(pid_dir)
status(pid_dir) == Service::Status::Running
end
def to_s
"#{name} (in #{@environment.name})"
end
def summary
"Name: #{name}\n" +
"Environment: #{environment.name} (#{environment.type.to_s.downcase})\n" +
(
if provides.size > 0
"Provides:\n" +
(provides.map { |x| " - " + x.token + "\n" }).join
else
""
end
) +
(
if @reference.consumes.size > 0
"Consumes:\n" +
(@reference.consumes.map { |x|
provider = @providers[x.token]?
if provider
" - #{x.token} (from #{provider.id})\n"
else
" - #{x.token} " +
"(NOT CURRENTLY PROVIDED)".colorize(:red).to_s +
"\n"
end
}).join
else
""
end
)
end
class_getter all = [] of Service
def self.load(path)
return unless Dir.exists? path
Dir.each_child path do |child|
unless child.match /\.spec$/
next
end
begin
specs = SpecParser.parse(File.read "#{path}/#{child}").not_nil!
rescue
next
end
@@all << Service.new specs
end
end
def write(path)
FileUtils.mkdir_p "#{path}"
File.write "#{path}/#{name}.#{@environment.name}.spec", to_spec
end
def remove(path)
File.delete "#{path}/#{name}.#{@environment.name}.spec"
end
def self.get_by_id(id)
matches = id.match /[^\/]*/
unless matches # Should not happen, above regex would always match.
raise Exception.new "FIXME"
end
environment_name, service_name = Service.parse_id id
@@all.find do |service|
service.name == service_name && service.environment.name == environment_name
end
end
def is_id?(id)
id == self.id || (@environment.name == "root" && id == "root/#{name}")
end
def self.parse_id(id) : Tuple(String, String)
s = id.split '/'
environment = s[0]
service = s[1]?
if service.nil?
service = environment
environment = "root"
end
{environment, service}
end
def self.get_used_ports(other_reservations = Array(Int32).new)
Service.all.map(&.ports.to_a)
.flatten
.map { |k, v| v }
.+(other_reservations)
end
def self.get_free_port(other_reservations = Array(Int32).new)
port = 49152
used_ports = Service.get_used_ports other_reservations
while used_ports.any? &.==(port)
port = port + 1
end
port
end
def self.is_port_used(port, other_reservations = Array(Int32).new)
used_ports = Service.get_used_ports other_reservations
used_ports.any? &.==(port)
end
alias ServiceTree = Array(ServiceTree) | Service
# Returns a dependency tree.
# Youll probably want to flatten and reverse it afterwards.
def dependency_tree
tree = [self] of ServiceTree
@providers.each do |token, provider_id|
service = Service.get_by_id provider_id
unless service
# FIXME: Does it make the dep tree invalid?
# FIXME: Only optional deps should be fine.
next
end
tree << service.dependency_tree
end
tree
end
def reverse_dependencies
rdeps = [self]
i = 0
while i < rdeps.size
item = rdeps[i]
@@all.each do |service|
service.providers.any? do |token, id|
if item.is_id?(id) && ! rdeps.any? service
rdeps << service
end
end
end
i += 1
end
rdeps
end
def get_default_provider(token) : String?
@environment.get_provider(token) || Environment.root.get_provider(token)
end
def consumes?(token, origin)
providers.select do |_token, provider|
token == _token && origin.is_id?(provider)
end.size > 0
end
def get_consumers(token)
Service.all.select(&.consumes?(token, self))
end
end