diff --git a/modules/cardano.nix b/modules/cardano.nix index 6545dde..1314748 100644 --- a/modules/cardano.nix +++ b/modules/cardano.nix @@ -10,6 +10,7 @@ in # assert cfg.networkNumbers ? cfg.network; { options.cardano = { + enable = lib.mkEnableOption "all Cardano services and HTTP proxy."; network = lib.mkOption { description = "Cardano network to operate on."; type = types.enum (lib.attrNames cfg.networkNumbers); diff --git a/modules/default.nix b/modules/default.nix index c4deb42..d3f2aaf 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -37,6 +37,12 @@ config.flake.overlays.ogmios ]; }; + http = { + imports = [ + ./services/http-proxy.nix + ./http.nix + ]; + }; # the default module imports all modules default = { imports = with builtins; attrValues (removeAttrs config.flake.nixosModules ["default"]); diff --git a/modules/http.nix b/modules/http.nix new file mode 100644 index 0000000..35f90e3 --- /dev/null +++ b/modules/http.nix @@ -0,0 +1,32 @@ +{ + config, + lib, + ... +}: let + cfg = config.cardano.http; + inherit (lib) mkIf mkEnableOption optional; +in { + options.cardano.http = { + enable = + mkEnableOption "HTTP SSL proxy and load balancer for cardano services" + // {default = config.cardano.enable or false;}; + }; + config = mkIf cfg.enable { + networking.firewall.allowedTCPPorts = [80] ++ optional config.services.http-proxy.https.enable 443; + + services.http-proxy = { + enable = true; + servers = mkIf config.cardano.enable ["127.0.0.1"]; + services = { + cardano-node = { + inherit (config.services.cardano-node) port; + inherit (config.services.cardano-node.package.passthru.identifier) version; + }; + ogmios = { + inherit (config.services.ogmios) port; + inherit (config.services.ogmios.package) version; + }; + }; + }; + }; +} diff --git a/modules/node.nix b/modules/node.nix index 1da38fd..ebd7d2d 100644 --- a/modules/node.nix +++ b/modules/node.nix @@ -7,11 +7,9 @@ cfg = config.cardano.node; in { options.cardano.node = { - enable = lib.mkOption { - description = "Whether to enable the cardano-node service."; - type = lib.types.bool; - default = config.cardano.enable or false; - }; + enable = + lib.mkEnableOption "cardano-node service" + // {default = config.cardano.enable or false;}; socketPath = lib.mkOption { description = "Path to cardano-node socket."; diff --git a/modules/ogmios.nix b/modules/ogmios.nix index c1abdbd..4f9c0bf 100644 --- a/modules/ogmios.nix +++ b/modules/ogmios.nix @@ -6,11 +6,9 @@ cfg = config.cardano.ogmios; in { options.cardano.ogmios = { - enable = lib.mkOption { - description = "Whether to enable the Ogmios bridge interface for cardano-node."; - type = lib.types.bool; - default = config.cardano.enable or false; - }; + enable = + lib.mkEnableOption "Ogmios bridge interface for cardano-node" + // {default = config.cardano.enable or false;}; }; config = lib.mkIf cfg.enable { diff --git a/modules/services/http-proxy.nix b/modules/services/http-proxy.nix new file mode 100644 index 0000000..9389cc2 --- /dev/null +++ b/modules/services/http-proxy.nix @@ -0,0 +1,112 @@ +{ + config, + lib, + ... +}: let + cfg = config.services.http-proxy; + inherit (lib) types listToAttrs mkOption mkEnableOption mapAttrs mkIf optionalString; +in { + options.services.http-proxy = { + enable = mkEnableOption "HTTP proxy, TLS endpoint and load balancer"; + domainName = mkOption { + description = "Domain name. For each service a virtualHost is configured as a subdomain."; + type = types.str; + default = ""; + }; + https.enable = mkOption { + description = "Enable TLS and redirect all connections to HTTPS. Requires certificates. Supports ACME."; + type = types.bool; + default = false; + }; + https.acme.enable = mkOption { + description = "Enable Let's Encrypt ACME TLS certificates. Requires public DNS records pointing to server and `security.acme` configured."; + type = types.bool; + default = cfg.https.enable; + }; + servers = mkOption { + description = "List of upstream server host names used for all services."; + type = types.listOf types.str; + default = []; + }; + services = mkOption { + description = "Configuraiton for each upstream service."; + type = types.attrsOf (types.submodule ({name, ...}: { + options = { + name = mkOption { + description = "Name of the service."; + type = types.str; + default = name; + }; + servers = mkOption { + description = "List of upstream server host names."; + type = types.listOf types.str; + default = cfg.servers; + }; + port = mkOption { + description = "Upstream server port."; + type = types.port; + }; + version = mkOption { + description = "This string will be served at path '/version'."; + type = types.nullOr types.str; + default = null; + }; + }; + })); + }; + _mkUpstream = mkOption { + type = types.functionTo types.attrs; + internal = true; + default = service: { + servers = listToAttrs (map + (server: { + name = "${server}:${toString service.port}"; + value = {}; + }) + service.servers); + }; + }; + _mkVirtualHost = mkOption { + type = types.functionTo types.attrs; + internal = true; + default = service: { + serverName = "${service.name}${optionalString (cfg.domainName != "") ".${cfg.domainName}"}"; + forceSSL = cfg.https.enable; + enableACME = cfg.https.acme.enable; + locations = { + "=/version" = mkIf (service.version != null) { + return = "200 ${service.version}"; + extraConfig = "add_header Content-Type text/plain;"; + }; + "/" = { + proxyWebsockets = true; + proxyPass = "http://${service.name}"; + }; + }; + extraConfig = " + proxy_hide_header Access-Control-Allow-Origin; + add_header Access-Control-Allow-Origin * always; + proxy_hide_header Access-Control-Allow-Headers; + add_header Access-Control-Allow-Headers * always; + proxy_hide_header Access-Control-Allow-Methods; + add_header Access-Control-Allow-Methods * always; + "; + }; + }; + }; + config = mkIf cfg.enable { + services.nginx = { + enable = true; + recommendedGzipSettings = true; + recommendedOptimisation = true; + recommendedProxySettings = true; + recommendedTlsSettings = true; + serverNamesHashBucketSize = 128; + + statusPage = true; + + upstreams = mapAttrs (_: cfg._mkUpstream) cfg.services; + virtualHosts = mapAttrs (_: cfg._mkVirtualHost) cfg.services; + }; + }; +} diff --git a/modules/services/ogmios.nix b/modules/services/ogmios.nix index 0da0040..679ad3d 100644 --- a/modules/services/ogmios.nix +++ b/modules/services/ogmios.nix @@ -43,7 +43,7 @@ in { host = mkOption { description = "Host address or name to listen on."; type = str; - default = "localhost"; + default = "127.0.0.1"; }; port = mkOption { diff --git a/tests/default.nix b/tests/default.nix index 57f6822..0e044f0 100644 --- a/tests/default.nix +++ b/tests/default.nix @@ -3,5 +3,6 @@ ./cardano-cli.nix ./cardano-node.nix ./ogmios.nix + ./http.nix ]; } diff --git a/tests/http.nix b/tests/http.nix new file mode 100644 index 0000000..50e9c85 --- /dev/null +++ b/tests/http.nix @@ -0,0 +1,36 @@ +{ + perSystem.vmTests.tests.http = { + impure = true; + module = { + nodes.node = {config, ...}: { + cardano = { + network = "preview"; + node.enable = true; + ogmios.enable = true; + }; + services.ogmios.host = "0.0.0.0"; + networking.firewall.allowedTCPPorts = [config.services.ogmios.port]; + }; + + nodes.proxy = { + cardano = { + http.enable = true; + }; + services.http-proxy.servers = ["node"]; + }; + + nodes.client = {pkgs, ...}: { + environment.systemPackages = [pkgs.curl]; + }; + + testScript = {nodes, ...}: '' + start_all() + node.wait_for_unit("ogmios") + node.wait_until_succeeds('curl --fail http://127.0.0.1:1337/health') + proxy.wait_for_unit("nginx") + client.wait_until_succeeds('curl --fail -H "Host: ogmios" http://proxy/health') + client.succeed('[ "${nodes.node.services.ogmios.package.version}" == "$(curl --silent --fail -H "Host: ogmios" http://proxy/version)" ]') + ''; + }; + }; +}