From 739d4882f4c55c165d688fda8027f3755ec307a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Boros?= Date: Wed, 3 Apr 2024 17:18:52 +0000 Subject: [PATCH 1/3] add http proxy module --- modules/cardano.nix | 1 + modules/default.nix | 6 ++ modules/http.nix | 34 ++++++++++ modules/services/http-proxy.nix | 112 ++++++++++++++++++++++++++++++++ modules/services/ogmios.nix | 2 +- tests/default.nix | 1 + tests/http.nix | 35 ++++++++++ 7 files changed, 190 insertions(+), 1 deletion(-) create mode 100644 modules/http.nix create mode 100644 modules/services/http-proxy.nix create mode 100644 tests/http.nix 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..24e1e65 --- /dev/null +++ b/modules/http.nix @@ -0,0 +1,34 @@ +{ + config, + lib, + ... +}: let + cfg = config.cardano.http; + inherit (lib) types last mkIf mkOption optional splitString; +in { + options.cardano.http = { + enable = mkOption { + description = "Whether to enable HTTP SSL proxy and load balancer for cardano services."; + type = types.bool; + 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; + version = last (splitString " " config.services.cardano-node.package); + }; + ogmios = { + inherit (config.services.ogmios) port; + inherit (config.services.ogmios.package) version; + }; + }; + }; + }; +} diff --git a/modules/services/http-proxy.nix b/modules/services/http-proxy.nix new file mode 100644 index 0000000..8251f63 --- /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..c041489 --- /dev/null +++ b/tests/http.nix @@ -0,0 +1,35 @@ +{ + 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 = '' + 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') + ''; + }; + }; +} From 810ac558c078049fa8b02413d5737dbf7d76f3b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Boros?= Date: Tue, 9 Apr 2024 17:13:50 +0000 Subject: [PATCH 2/3] requested changes --- modules/http.nix | 12 +++++------- modules/node.nix | 8 +++----- modules/ogmios.nix | 8 +++----- modules/services/http-proxy.nix | 2 +- 4 files changed, 12 insertions(+), 18 deletions(-) diff --git a/modules/http.nix b/modules/http.nix index 24e1e65..35f90e3 100644 --- a/modules/http.nix +++ b/modules/http.nix @@ -4,14 +4,12 @@ ... }: let cfg = config.cardano.http; - inherit (lib) types last mkIf mkOption optional splitString; + inherit (lib) mkIf mkEnableOption optional; in { options.cardano.http = { - enable = mkOption { - description = "Whether to enable HTTP SSL proxy and load balancer for cardano services."; - type = types.bool; - default = config.cardano.enable or false; - }; + 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; @@ -22,7 +20,7 @@ in { services = { cardano-node = { inherit (config.services.cardano-node) port; - version = last (splitString " " config.services.cardano-node.package); + inherit (config.services.cardano-node.package.passthru.identifier) version; }; ogmios = { inherit (config.services.ogmios) port; 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 index 8251f63..9389cc2 100644 --- a/modules/services/http-proxy.nix +++ b/modules/services/http-proxy.nix @@ -7,7 +7,7 @@ inherit (lib) types listToAttrs mkOption mkEnableOption mapAttrs mkIf optionalString; in { options.services.http-proxy = { - enable = mkEnableOption "HTTP proxy, TLS endpoint and load balancer."; + 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; From 42e60125a47a6281a34f276025a5b07d73ea3364 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Boros?= Date: Tue, 9 Apr 2024 18:22:50 +0000 Subject: [PATCH 3/3] test version endpoint --- tests/http.nix | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/http.nix b/tests/http.nix index c041489..50e9c85 100644 --- a/tests/http.nix +++ b/tests/http.nix @@ -23,12 +23,13 @@ environment.systemPackages = [pkgs.curl]; }; - testScript = '' + 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)" ]') ''; }; };