From d3fe31766568896c4204b18dee070107f937c0d0 Mon Sep 17 00:00:00 2001 From: Luka Vandervelden Date: Sat, 9 Nov 2019 21:52:43 +0100 Subject: [PATCH] Grooming, `service reload`, `Service#reload`. `reload-command:` can be defined in service definition `.spec` files. --- services/nginx.spec | 3 +- src/gen-config.cr | 11 +-- src/get-port.cr | 7 +- src/service.cr | 60 +++++++----- src/service/context.cr | 150 ++++++++++++++++++++++++++++++ src/service/environment.cr | 52 ++--------- src/service/service.cr | 147 ++++++++++++++--------------- src/service/service_definition.cr | 15 +-- src/status.cr | 10 +- 9 files changed, 279 insertions(+), 176 deletions(-) create mode 100644 src/service/context.cr diff --git a/services/nginx.spec b/services/nginx.spec index 5d26301..c23b994 100644 --- a/services/nginx.spec +++ b/services/nginx.spec @@ -1,6 +1,7 @@ -command: nginx -c nginx.conf +command: nginx -c ${SERVICE_ROOT}/nginx.conf consumes: http? provides: www, http ports: http=80, https=443 +reload-command: kill -HUP ${SERVICE_PID} %configuration nginx.conf diff --git a/src/gen-config.cr b/src/gen-config.cr index 02bfdcd..4185b7f 100644 --- a/src/gen-config.cr +++ b/src/gen-config.cr @@ -67,9 +67,8 @@ class GenConfig::Context end def generate(template, target : String, options : Hash(String, Variables)) - Environment.load ENVIRONMENTS_DIRECTORY - ServiceDefinition.load SERVICES_DIRECTORY - Service.load RC_DIRECTORY + context = Service::Context.new + context.load target_file = File.open target, "w" @@ -95,13 +94,13 @@ class GenConfig::Context end if service_id = ENV["SERVICE_ID"]? - if service = Service.get_by_id service_id + if service = context.get_service_by_id service_id environment = service.environment options["service"] = service.to_genconfig options["providers"] = service.providers.compact_map do |token, provider| - provider = Service.get_by_id provider + provider = context.get_service_by_id provider next unless provider @@ -127,7 +126,7 @@ class GenConfig::Context id = (arguments.varargs[0]? || options["id"]).to_s password_id = arguments.varargs[1]? || "main" - _service = Service.get_by_id(id).not_nil! + _service = context.get_service_by_id(id).not_nil! # FIXME: hardcoded path password_file = "#{_service.root}/password_#{password_id}" diff --git a/src/get-port.cr b/src/get-port.cr index 84b3488..1e3b01f 100644 --- a/src/get-port.cr +++ b/src/get-port.cr @@ -4,9 +4,8 @@ require "./service/environment.cr" require "./service/service_definition.cr" require "./service/service.cr" -Environment.load ENVIRONMENTS_DIRECTORY -ServiceDefinition.load SERVICES_DIRECTORY -Service.load RC_DIRECTORY +context = Service::Context.new +context.load if ARGV.size != 2 STDERR.puts "usage: get-port " @@ -15,5 +14,5 @@ end service_id, port_id = ARGV -puts Service.get_by_id(service_id).not_nil!.ports[port_id] +puts context.get_service_by_id(service_id).not_nil!.ports[port_id] diff --git a/src/service.cr b/src/service.cr index dd48161..8340afc 100644 --- a/src/service.cr +++ b/src/service.cr @@ -2,8 +2,6 @@ require "option_parser" require "yaml" require "colorize" -require "weird-crystal-base" - require "./config.cr" require "./service/*" @@ -15,12 +13,6 @@ args = [] of String force = false verbose = false -class Service::Context < Weird::Base - property pid_directory = PID_DIRECTORY - property log_directory = LOG_DIRECTORY - property services_directory = RC_DIRECTORY -end - alias Command = Proc(Service::Context, Array(String), Nil) alias CommandTuple = Tuple(String, String, Command) class CommandsList @@ -53,7 +45,7 @@ commands.push "add", "Adds a service to an environment." do |context, args| domain = nil ports = Hash(String, Int32).new - if Service.get_by_id args[0] + 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 @@ -85,7 +77,7 @@ commands.push "add", "Adds a service to an environment." do |context, args| end # FIXME: Some of that code is ugly and should not even be here. - Service.new(service, environment).tap do |service| + Service.new(context, service, environment).tap do |service| if domain service.domain = domain end @@ -119,17 +111,17 @@ commands.push "add", "Adds a service to an environment." do |context, args| if number.nil? default_port = definition.default_value - if default_port && ! Service.is_port_used default_port, ports.map { |k, v| v } + if default_port && ! context.is_port_used default_port, ports.map { |k, v| v } number = default_port ports[name] = number end - elsif Service.is_port_used number + elsif context.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 } + number = context.get_free_port ports.map { |k, v| v } ports[name] = number end @@ -168,7 +160,7 @@ commands.push "del", "Removes a service from an environment." do |context, args| rvalue = 0 args.each do |id| - service = Service.get_by_id(id) + service = context.get_service_by_id(id) if service.nil? context.error "#{id}: no such service" @@ -200,7 +192,7 @@ end commands.push "start", "Starts a service." do |context, args| services = args.map do |arg| - service = Service.get_by_id(arg) + service = context.get_service_by_id(arg) unless service raise Service::Exception.new "Service '#{arg}' does not exist." @@ -218,7 +210,7 @@ end commands.push "stop", "Stops a running service." do |context, args| services = args.map do |arg| - service = Service.get_by_id(arg) + service = context.get_service_by_id(arg) unless service raise Service::Exception.new "Service '#{arg}' does not exist." @@ -238,6 +230,27 @@ commands.push "stop", "Stops a running service." do |context, args| 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 @@ -263,7 +276,7 @@ commands.push "show", "Shows a service's configuration and state." do |context, args.each do |arg| environment_name, service_name = Service.parse_id arg - service = Service.all.find do |service| + service = context.services.find do |service| service.name == service_name && service.environment.name == environment_name end @@ -283,11 +296,11 @@ commands.push "add-environment", "Creates a new (empty) environment." do |contex exit 1 end - Environment.new(args[0]).write ENVIRONMENTS_DIRECTORY + Environment.new(context, args[0]).write ENVIRONMENTS_DIRECTORY end commands.push "list-environments", "Lists all currently defined environments.s", do |context, args| - Environment.all.map do |env| + context.environments.map do |env| puts env.to_s end end @@ -296,7 +309,7 @@ commands.push "del-environment", "Removes an empty environment." do |context, ar rvalue = 0 args.each do |arg| - environment = Environment.all.find &.name.==(arg) + environment = context.environments.find &.name.==(arg) if environment.nil? STDERR.puts "#{arg}: no such environment" @@ -304,7 +317,7 @@ commands.push "del-environment", "Removes an empty environment." do |context, ar next end - if Service.all.select(&.environment.name.==(environment.name)).size > 0 + if context.services.select(&.environment.name.==(environment.name)).size > 0 STDERR.puts "#{arg}: is not empty" rvalue = 1 next @@ -352,11 +365,8 @@ if command.nil? exit 1 end -ServiceDefinition.load SERVICES_DIRECTORY -Environment.load ENVIRONMENTS_DIRECTORY -Service.load RC_DIRECTORY - context = Service::Context.new +context.load begin args.shift diff --git a/src/service/context.cr b/src/service/context.cr new file mode 100644 index 0000000..46ce3fb --- /dev/null +++ b/src/service/context.cr @@ -0,0 +1,150 @@ +require "weird-crystal-base" + +class Service +end + +class Service::Context < Weird::Base + property pid_directory = PID_DIRECTORY + property log_directory = LOG_DIRECTORY + property services_directory = RC_DIRECTORY + property service_definitions_directory = SERVICES_DIRECTORY + property environments_directory = ENVIRONMENTS_DIRECTORY + + # FIXME: Namespaces. 😬 + getter services = [] of Service + getter service_definitions = [] of ServiceDefinition + getter environments = [] of Environment + + def initialize + @environments = [Environment.new self] + end + def root_environment + @environments[0] + end + + def load_services + path = services_directory + + 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 + + @services << Service.new self, specs + end + end + + def load_environments + path = environments_directory + + return unless Dir.exists? path + + Dir.each_child path do |child| + unless child.match /\.spec$/ + next + end + + file_path = "#{path}/#{child}" + + begin + name = File.basename(child, ".spec") + specs = SpecParser.parse File.read(file_path) + + environment = Environment.new self, name, specs + rescue e + STDERR << "error loading #{file_path}: " << e << "\n" + # FIXME: Print stacktrace? Debug mode? + next + end + + @environments << environment + end + end + + def load_service_definitions + path = service_definitions_directory + + Dir.each_child path do |child| + if child.match /\.spec$/ + name = File.basename(child, ".spec") + specs = SpecParser.parse File.read "#{path}/#{child}" + + @service_definitions << ServiceDefinition.new name, specs + else + next + end + end + end + + def load + load_service_definitions + load_environments + load_services + end + + def get_service_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 + + @services.find do |service| + service.name == service_name && service.environment.name == environment_name + end + end + + def get_environment_by_name(name) + _def = @environments.find &.name.==(name) + + if _def.nil? + raise ::Service::Exception.new "Environment '#{name}' does not exist." + end + + _def + end + + def get_used_ports(other_reservations = Array(Int32).new) + services.map(&.ports.to_a) + .flatten + .map { |k, v| v } + .+(other_reservations) + end + + def get_free_port(other_reservations = Array(Int32).new) + port = 49152 + + used_ports = get_used_ports other_reservations + + while used_ports.any? &.==(port) + port = port + 1 + end + + port + end + + def get_service_definition_by_name(name) + _def = @service_definitions.find &.name.==(name) + + if _def.nil? + raise Exception.new "Service '#{name}' does not exist." + end + + _def + end + + def is_port_used(port, other_reservations = Array(Int32).new) + used_ports = get_used_ports other_reservations + + used_ports.any? &.==(port) + end +end + diff --git a/src/service/environment.cr b/src/service/environment.cr index adc402c..cba8e62 100644 --- a/src/service/environment.cr +++ b/src/service/environment.cr @@ -1,5 +1,7 @@ require "specparser" +require "./context.cr" + class Environment enum Type Prefix @@ -13,17 +15,19 @@ class Environment # The place we’ll put services’ data and configuration. @root : String? - def initialize() - initialize "root" + @context : Service::Context + + def initialize(context) + initialize context, "root" end - def initialize(@name, type = "prefix") + def initialize(@context, @name, type = "prefix") @type = Type.parse type @files = Array(ServiceDefinition::FileDefinition).new end - def initialize(@name, specs : SpecParser) + def initialize(@context, @name, specs : SpecParser) assignments = specs.assignments assignments["type"].try &.as_s.tap do |type| @@ -39,34 +43,6 @@ class Environment @root || "#{SERVED_DATA_DIRECTORY}/#{@name}" end - class_getter root = Environment.new - class_getter all = [@@root] of Environment - - def self.load(path) - return unless Dir.exists? path - - Dir.each_child path do |child| - unless child.match /\.spec$/ - next - end - - file_path = "#{path}/#{child}" - - begin - name = File.basename(child, ".spec") - specs = SpecParser.parse File.read(file_path) - - environment = Environment.new name, specs - rescue e - STDERR << "error loading #{file_path}: " << e << "\n" - # FIXME: Print stacktrace? Debug mode? - next - end - - @@all << environment - end - end - def write(dir : String) File.write "#{dir}/#{@name}.spec", to_spec end @@ -81,18 +57,8 @@ class Environment ].join("\n") + "\n" end - def self.get(name) - _def = @@all.find &.name.==(name) - - if _def.nil? - raise ::Service::Exception.new "Environment '#{name}' does not exist." - end - - _def - end - def get_provider(token) - Service.all.find do |service| + @context.services.find do |service| service.environment == self && service.provides? token end.try &.id end diff --git a/src/service/service.cr b/src/service/service.cr index 14b22c2..0a27c14 100644 --- a/src/service/service.cr +++ b/src/service/service.cr @@ -2,6 +2,7 @@ require "yaml" require "colorize" require "file_utils" +require "./context.cr" require "./service_definition.cr" require "./environment.cr" require "./libc.cr" @@ -30,7 +31,7 @@ end class Service getter environment : Environment - getter providers = ProvidersList.new + getter providers : ProvidersList setter name : String? property domain : String? @@ -39,12 +40,19 @@ class Service # The place we’ll store configuration and data. @root : String? + @context : Context + class Exception < ::Exception end class ProvidersList < Hash(String, String) + @context : Service::Context + def initialize(@context) + initialize + end + def []?(name) - super(name).try { |x| Service.get_by_id x} + super(name).try { |x| @context.get_service_by_id x} end end @@ -55,16 +63,18 @@ class Service end end - def initialize(name, environment_name : String?) + def initialize(@context : Context, name, environment_name : String?) + @providers = ProvidersList.new @context @reference = ServiceDefinition.get name @environment = if environment_name.nil? || environment_name == "" - Environment.root + @context.root_environment else - Environment.get environment_name + @context.get_environment_by_name environment_name end end - def initialize(specs : SpecParser) + def initialize(@context : Context, specs : SpecParser) + @providers = ProvidersList.new @context assignments = specs.assignments # Complicated because of compat with when services had no dedicated @@ -77,9 +87,9 @@ class Service env = assignments["environment"]?.try &.as_s @environment = if env.nil? || env == "" - Environment.root + @context.root_environment else - Environment.get env + @context.get_environment_by_name env end @root = assignments["root"]?.try &.as_s @@ -164,6 +174,9 @@ class Service def stop_command @reference.stop_command end + def reload_command + @reference.reload_command + end def provides @reference.provides end @@ -197,11 +210,16 @@ class Service env["SERVICE_NAME"] = name env["SERVICE_ROOT"] = root env["SERVICE_ID"] = full_id + if _pid = pid @context.pid_directory + env["SERVICE_PID"] = _pid.to_s + end + env["ENVIRONMENT"] = @environment.name env["ENVIRONMENT_TYPE"] = @environment.type.to_s + @providers.each do |token, provider| - service_provider = Service.get_by_id provider + service_provider = @context.get_service_by_id provider # FIXME: Warning? next if service_provider.nil? @@ -248,17 +266,7 @@ class Service @environment.files + @reference.files end - def start(context : Context) - return if running? context.pid_directory - - if non_runnable - context.title "Setting up #{to_s}" - else - context.title "Starting #{to_s}" - end - - FileUtils.mkdir_p root - + def build_files!(context : Context) files.each do |file| run_hook = false @@ -289,6 +297,20 @@ class Service break end end + end + + def start(context : Context) + return if running? context.pid_directory + + if non_runnable + context.title "Setting up #{to_s}" + else + context.title "Starting #{to_s}" + end + + FileUtils.mkdir_p root + + build_files! context return if non_runnable @@ -347,6 +369,28 @@ class Service end end + def reloadable? + !@reload_command.nil? + end + def reload(context : Context) + build_files! context + + command = reload_command + + unless command + raise Exception.new "This service cannot be reloaded!" + end + + Process.fork do + Dir.cd root + + Process.exec "sh", ["-c", command], + output: Process::Redirect::Inherit, + error: Process::Redirect::Inherit, + env: commands_environment + end.wait + end + def get_pid_file(pid_dir) "#{pid_dir}/#{name}.#{environment.name}.pid" end @@ -445,25 +489,6 @@ class Service ) 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 @@ -502,17 +527,6 @@ class Service File.delete "#{context.services_directory}/#{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 @@ -531,31 +545,6 @@ class Service {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. @@ -564,7 +553,7 @@ class Service tree = [self] of ServiceTree @providers.each do |token, provider_id| - service = Service.get_by_id provider_id + service = @context.get_service_by_id provider_id unless service # FIXME: Does it make the dep tree invalid? @@ -585,7 +574,7 @@ class Service while i < rdeps.size item = rdeps[i] - @@all.each do |service| + @context.services.each do |service| service.providers.any? do |token, id| if item.is_id?(id) && ! rdeps.any? service rdeps << service @@ -600,7 +589,7 @@ class Service end def get_default_provider(token) : String? - @environment.get_provider(token) || Environment.root.get_provider(token) + @environment.get_provider(token) || @context.root_environment.get_provider(token) end def consumes?(token, origin) @@ -610,7 +599,7 @@ class Service end def get_consumers(token) - Service.all.select(&.consumes?(token, self)) + @context.services.select(&.consumes?(token, self)) end end diff --git a/src/service/service_definition.cr b/src/service/service_definition.cr index 954ad57..c1dad12 100644 --- a/src/service/service_definition.cr +++ b/src/service/service_definition.cr @@ -73,6 +73,7 @@ class ServiceDefinition getter name : String getter command : String getter stop_command : String? + getter reload_command : String? getter directory : String? getter user : String? getter provides : String? @@ -91,6 +92,7 @@ class ServiceDefinition @command = specs["command"].as_s @non_runnable = (@command == "none") @stop_command = specs["stop-command"]?.try &.as_s + @reload_command = specs["reload-command"]?.try &.as_s @directory = specs["directory"]?.try &.as_s @user = specs["user"]?.try &.as_s @provides = specs["provides"]?.try &.as_a_or_s.map { |x| Provides.new x } || Array(Provides).new @@ -148,19 +150,6 @@ class ServiceDefinition end end - def self.load(path) - Dir.each_child path do |child| - if child.match /\.spec$/ - name = File.basename(child, ".spec") - specs = SpecParser.parse File.read "#{path}/#{child}" - - @@all << ServiceDefinition.new name, specs - else - next - end - end - end - def self.get(name) : ServiceDefinition _def = @@all.find &.name.==(name) diff --git a/src/status.cr b/src/status.cr index 89df686..8ed9d6b 100644 --- a/src/status.cr +++ b/src/status.cr @@ -3,9 +3,9 @@ require "./service/service.cr" require "./config.cr" -ServiceDefinition.load SERVICES_DIRECTORY -Environment.load ENVIRONMENTS_DIRECTORY -Service.load RC_DIRECTORY +context = Service::Context.new + +context.load LibC.setuid 0 LibC.setgid 0 @@ -16,10 +16,10 @@ list_status = false services = ARGV if services.size == 0 list_status = true - services = Service.all + services = context.services else services = services.map do |id| - Service.get_by_id id + context.get_service_by_id id end end