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 " 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}=` 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 [...]]" 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 [...]]\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 " 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 [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