service/src/service.cr

387 lines
8.3 KiB
Crystal

require "option_parser"
require "yaml"
require "colorize"
require "./config.cr"
require "./service/*"
parser = uninitialized OptionParser
args = [] of String
force = false
verbose = false
alias Command = Proc(Service::Context, Array(String), Nil)
alias CommandTuple = Tuple(String, String, Command)
class CommandsList
def initialize
@commands = Array(CommandTuple).new
end
def push(name : String, description : String, &proc : Command)
@commands << Tuple.new(name, description, proc)
end
def find(&block : Proc(CommandTuple, Bool))
@commands.find do |tuple|
if block.call tuple
next true
end
end
end
def to_s
"commands:\n\n"+ @commands.map do |tuple|
" %-32s %s" % [tuple[0], tuple[1]]
end.join "\n"
end
end
commands = CommandsList.new
commands.push "add", "Adds a service to an environment." do |context, args|
providers = Hash(String, String).new
domain = nil
ports = Hash(String, Int32).new
if context.get_service_by_id args[0]
raise ::Service::Exception.new "'#{args[0]}' already exists. You have to remove it before re-adding it."
next
end
environment, service = Service.parse_id args[0]
name = service
args.each_with_index do |arg, i|
next if i == 0
match = arg.match /(.*)=(.*)/
if match.nil? || match.size < 2
raise ::Service::Exception.new "usage: service add <service> <token=provider>"
next
end
_, key, value = match
if key == "type"
service = value
elsif key == "domain"
domain = value
elsif match = key.match /^port:(.*)/
ports[match[1]] = value.to_i
else
providers[key] = value
end
end
# FIXME: Some of that code is ugly and should not even be here.
Service.new(context, service, environment).tap do |service|
if domain
service.domain = domain
end
if name != service
service.name = name
end
service.consumes.each do |token|
provider = providers[token.token]?
if provider.nil?
provider = service.get_default_provider token.token
elsif provider == "none"
provider = nil
end
next if provider.nil? && token.optional
if provider.nil?
STDERR.puts "This service consumes a “#{token.token}” token, but you have not specified what other service is supposed to provide it."
STDERR.puts "Use the `service add #{args[1]} #{token.token}=<provider>` syntax to specify it."
exit 1
end
service.providers[token.token] = provider
end
service.port_definitions.each do |definition|
name = definition.name
number = ports[name]?
if number.nil?
default_port = definition.default_value
if default_port && ! context.is_port_used default_port, ports.map { |k, v| v }
number = default_port
ports[name] = number
end
elsif context.is_port_used number
raise Service::Exception.new "Port #{number} is already reserved by another service."
next
end
if number.nil?
number = context.get_free_port ports.map { |k, v| v }
ports[name] = number
end
# FIXME: Check the port is not duplicated here.
service.ports[definition.name] = number
end
if service.requires_domain && !service.domain
raise Service::Exception.new "'#{service.name}' requires a domain= parameter to be provided!"
end
context.title "Adding #{service.to_s}"
service.providers.each do |token, provider|
context.info "'#{token}' provider will be #{provider.to_s}"
end
service.port_definitions.each do |definition|
name = definition.name
context.info "Port for '#{name}' is #{service.ports[name]}"
end
if service.domain
context.info "Domain is '#{service.domain}'"
end
end.write RC_DIRECTORY
end
commands.push "del", "Removes a service from an environment." do |context, args|
if args.size < 1
STDERR.puts "usage: service del <id> [id [...]]"
exit 1
end
rvalue = 0
args.each do |id|
service = context.get_service_by_id(id)
if service.nil?
context.error "#{id}: no such service"
rvalue = 1
next
end
revdeps = service.reverse_dependencies
if revdeps.size > 1 && ! force
context.error "#{id}: has reverse dependencies, use -f to force"
rvalue = 2
next
end
revdeps.reverse.each do |service|
next if ! service.running? PID_DIRECTORY
service.stop context
end
revdeps.reverse.each do |service|
service.remove context
end
end
exit rvalue
end
commands.push "start", "Starts a service." do |context, args|
services = args.map do |arg|
service = context.get_service_by_id(arg)
unless service
raise Service::Exception.new "Service '#{arg}' does not exist."
end
service
end
services.each do |service|
service.dependency_tree.flatten.reverse.each do |service|
service.start context
end
end
end
commands.push "stop", "Stops a running service." do |context, args|
services = args.map do |arg|
service = context.get_service_by_id(arg)
unless service
raise Service::Exception.new "Service '#{arg}' does not exist."
end
service
end
services.each do |service|
next if ! service.running? PID_DIRECTORY
service.reverse_dependencies.reverse.each do |service|
next if ! service.running? PID_DIRECTORY
service.stop context
end
end
end
commands.push "reload", "Updates configuration and reloads services." do |context, args|
services = args.map do |arg|
service = context.get_service_by_id(arg)
unless service
raise Service::Exception.new "Service '#{arg}' does not exist."
end
service
end
services.each do |service|
if ! service.running? PID_DIRECTORY
context.warning "#{service.to_s} is not currently running."
next
end
service.reload context
end
end
commands.push "status", "Prints the status of services." do |context, args|
ENV["SERVICE_VERBOSE"] = verbose.to_s
child = Process.run "#{OWN_LIBEXEC_DIR}/status", args,
output: Process::Redirect::Inherit,
error: Process::Redirect::Inherit
return_value = (child.exit_status / 256).to_i
# Errors not registered here should probably be verbose in `status`.
if return_value == 1
STDERR << "No such service.\n"
end
exit return_value
end
commands.push "show", "Shows a service's configuration and state." do |context, args|
if args.size < 1
STDERR << "usage: service show <id> [id [...]]\n"
next
end
args.each do |arg|
environment_name, service_name = Service.parse_id arg
service = context.services.find do |service|
service.name == service_name &&
service.environment.name == environment_name
end
if service
puts service.summary
else
STDERR << "No such service is registered.\n"
exit 2
end
end
end
commands.push "add-environment", "Creates a new (empty) environment." do |context, args|
if args.size != 1
STDERR.puts "usage: service add-environment <name>"
exit 1
end
Environment.new(context, args[0]).write ENVIRONMENTS_DIRECTORY
end
commands.push "list-environments", "Lists all currently defined environments.s", do |context, args|
context.environments.map do |env|
puts env.to_s
end
end
commands.push "del-environment", "Removes an empty environment." do |context, args|
rvalue = 0
args.each do |arg|
environment = context.environments.find &.name.==(arg)
if environment.nil?
STDERR.puts "#{arg}: no such environment"
rvalue = 2
next
end
if context.services.select(&.environment.name.==(environment.name)).size > 0
STDERR.puts "#{arg}: is not empty"
rvalue = 1
next
end
environment.remove ENVIRONMENTS_DIRECTORY
end
exit rvalue
end
commands.push "help", "Prints this help message." do
puts parser
puts
puts commands.to_s
end
parser = OptionParser.parse do |cli|
cli.banner = "usage: service <command> [options]\n" +
"options:\n"
cli.on "-h", "--help", "Prints this help message." do
puts cli
puts
puts commands.to_s
exit 0
end
cli.on "-f", "--force", "Ignores warnings and executes dangerous operations." do
force = true
end
cli.on "-v", "--verbose", "Prints more data when doing things." do
verbose = true
end
cli.unknown_args do |x|
args = x
end
end
command = commands.find &.[0].==(args[0]?)
if command.nil?
STDERR << parser << "\n"
STDERR << "\n"
STDERR << commands.to_s << "\n"
exit 1
end
context = Service::Context.new
begin
context.load
args.shift
command.[2].call(context, args)
rescue e : Service::Exception
context.error e.message
exit 2
rescue e
STDERR.puts "unhandled exception: #{e.message}"
e.backtrace.map do |line|
STDERR << " - " << line << '\n'
end
end