require "option_parser" require "yaml" require "colorize" require "./config.cr" # TODO: # - Be more declarative about the definition of commands. require "./service/*" parser = uninitialized OptionParser args = [] of String force = false alias Command = Proc(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 |args| providers = Hash(String, String).new domain = nil ports = Hash(String, Int32).new if Service.get_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] 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 " next end _, key, value = match if 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(service, environment).tap do |service| if domain service.domain = domain end service.consumes.each do |token| provider = providers[token.token]? if provider.nil? provider = service.get_default_provider token.token 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}=` 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 && ! Service.is_port_used default_port, ports.map { |k, v| v } number = default_port ports[name] = number end elsif Service.is_port_used number raise Service::Exception.new "Port #{number} is already reserved by another service." next end if number.nil? number = Service.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 end.write RC_DIRECTORY end commands.push "del", "Removes a service from an environment." do |args| if args.size < 1 STDERR.puts "usage: service del [id [...]]" exit 1 end rvalue = 0 args.each do |id| service = Service.get_by_id(id) if service.nil? STDERR.puts "#{id}: no such service" rvalue = 1 next end revdeps = service.reverse_dependencies if revdeps.size > 1 && ! force STDERR.puts "#{id}: has reverse dependencies, use -f to force" rvalue = 2 next end revdeps.reverse.each do |service| next if ! service.running? PID_DIRECTORY puts "stopping #{service.to_s}" service.stop PID_DIRECTORY end revdeps.reverse.each do |service| service.remove RC_DIRECTORY end end exit rvalue end commands.push "start", "Starts a service." do services = args.map do |arg| service = Service.get_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| next if service.running? PID_DIRECTORY puts "starting #{service.to_s}" service.start PID_DIRECTORY, LOG_DIRECTORY end end end commands.push "stop", "Stops a running service." do |args| services = args.map do |arg| service = Service.get_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 puts "stopping #{service.to_s}" service.stop PID_DIRECTORY end end end commands.push "status", "Prints the status of services." do |args| 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 |args| if args.size < 1 STDERR << "usage: service show [id [...]]\n" next end args.each do |arg| environment_name, service_name = Service.parse_id arg service = Service.all.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 |arg| if arg.size != 1 STDERR.puts "usage: service add-environment " exit 1 end Environment.new(arg[0]).write ENVIRONMENTS_DIRECTORY end commands.push "list-environments", "Lists all currently defined environments.s", do |arg| Environment.all.map do |env| puts env.to_s end end commands.push "del-environment", "Removes an empty environment." do |arg| rvalue = 0 arg.each do |arg| environment = Environment.all.find &.name.==(arg) if environment.nil? STDERR.puts "#{arg}: no such environment" rvalue = 2 next end if Service.all.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 [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.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 ServiceDefinition.load SERVICES_DIRECTORY Environment.load ENVIRONMENTS_DIRECTORY Service.load RC_DIRECTORY begin args.shift command.[2].call(args) rescue e : Service::Exception STDERR << e.message << "\n" exit 2 end