# SelfHostBlocks Service Implementation Guide {#service-implementation-guide} This guide documents the complete process for implementing a new service in SelfHostBlocks, based on lessons learned from the nzbget implementation and analysis of existing service patterns. **Note**: SelfHostBlocks aims to be "the smallest amount of code above what is available in nixpkgs" (see `docs/contributing.md`). Services should leverage existing nixpkgs options when possible and focus on providing contract integrations rather than reimplementing configuration. ## What Makes a "Complete" SHB Service {#complete-shb-service} According to the project maintainer's criteria, a service is considered fully supported if it includes: 1. **SSL block integration** - HTTPS/TLS certificate management 2. **Backup block integration** - Automated backup of service data 3. **Monitoring integration** - Prometheus metrics and health checks 4. **LDAP (LLDAP) integration** - Directory-based authentication 5. **SSO (Authelia) integration** - Single sign-on authentication 6. **Comprehensive tests** - All integration variants tested ## Pre-Implementation Research {#pre-implementation-research} ### 1. Analyze Existing Services {#analyze-existing-services} Before starting, study existing services to understand patterns: ```bash # Study service patterns ls modules/services/ # List all services cat modules/services/deluge.nix # Best practice example cat modules/services/vaultwarden.nix # Another good example ``` **Key patterns to identify:** - Configuration structure and options - How contracts are used (SSL, backup, monitoring, secrets) - Authentication integration approaches - Service-specific settings and defaults ### 2. Understand the Target Service {#understand-target-service} Research the service you're implementing: - **Configuration format** (YAML, INI, JSON, etc.) - **Authentication methods** (built-in users, LDAP, OIDC/OAuth) - **API endpoints** (for monitoring/health checks) - **Data directories** (what needs backing up) - **Network requirements** (ports, protocols) - **Dependencies** (databases, external tools) ### 3. Check NixOS Integration {#check-nixos-integration} Verify nixpkgs support: ```bash # Check if NixOS service exists nix eval --impure --expr '(import { configuration = {...}: {}; }).options.services' --apply 'builtins.attrNames' --json | jq -r '.[]' | grep -i servicename # or search online: https://search.nixos.org/options?query=services.servicename ``` If no nixpkgs integration exists, you may need to: - Package the service first - Use containerized approach - Request upstream nixpkgs integration ## Implementation Steps {#implementation-steps} ### 1. Create the Service Module {#create-service-module} Location: `modules/services/servicename.nix` **Basic structure:** ```nix { config, pkgs, lib, ... }: let cfg = config.shb.servicename; contracts = pkgs.callPackage ../contracts {}; fqdn = "${cfg.subdomain}.${cfg.domain}"; # Choose appropriate format based on service config settingsFormat = pkgs.formats.yaml {}; # or .ini, .json, etc. in { options.shb.servicename = { # Core options (always required) enable = lib.mkEnableOption "selfhostblocks.servicename"; subdomain = lib.mkOption { ... }; domain = lib.mkOption { ... }; # SSL integration (always include) ssl = lib.mkOption { description = "Path to SSL files"; type = lib.types.nullOr contracts.ssl.certs; default = null; }; # Service-specific options port = lib.mkOption { ... }; dataDir = lib.mkOption { ... }; settings = lib.mkOption { type = lib.types.submodule { freeformType = settingsFormat.type; options = { # Define key options with descriptions }; }; }; # Authentication options authEndpoint = lib.mkOption { type = lib.types.nullOr lib.types.str; description = "OIDC endpoint for SSO"; default = null; }; ldap = lib.mkOption { ... }; # LDAP integration users = lib.mkOption { ... }; # Local user management # Integration options backup = lib.mkOption { type = lib.types.submodule { options = contracts.backup.mkRequester { user = "servicename"; sourceDirectories = [ cfg.dataDir ]; }; }; }; monitoring = lib.mkOption { type = lib.types.nullOr (lib.types.submodule { options = { # Service-specific monitoring options }; }); default = null; }; # System options extraServiceConfig = lib.mkOption { ... }; logLevel = lib.mkOption { ... }; }; config = lib.mkIf cfg.enable (lib.mkMerge [ { # Base service configuration services.servicename = { enable = true; # Map SHB options to nixpkgs service options }; # Nginx reverse proxy shb.nginx.vhosts = [{ inherit (cfg) subdomain domain ssl; upstream = "http://127.0.0.1:${toString cfg.port}"; # SSO integration autheliaRules = lib.mkIf (cfg.authEndpoint != null) [ { domain = fqdn; policy = "bypass"; resources = [ "^/api" ]; # API endpoints } { domain = fqdn; policy = "two_factor"; resources = [ "^.*" ]; # Everything else } ]; }]; # User/group setup users.users.servicename = { extraGroups = [ "media" ]; # If needed for file access }; # Directory permissions systemd.tmpfiles.rules = [ "d ${cfg.dataDir} 0755 servicename servicename - -" ]; } # Monitoring configuration (conditional) (lib.mkIf (cfg.monitoring != null) { services.prometheus.scrapeConfigs = [{ job_name = "servicename"; static_configs = [{ targets = [ "127.0.0.1:${toString cfg.port}" ]; labels = { hostname = config.networking.hostName; domain = cfg.domain; }; }]; metrics_path = "/metrics"; # or appropriate endpoint scrape_interval = "30s"; }]; }) ]); } ``` ### 2. Key Implementation Considerations {#implementation-considerations} #### Configuration Management {#configuration-management} - **Use freeform settings** when possible: `freeformType = settingsFormat.type` - **Provide sensible defaults** for common options - **Use lib.mkDefault** for user-overridable settings - **Use lib.mkForce** for security-critical settings #### Authentication Integration {#authentication-integration} - **SSO (Authelia)**: Use `autheliaRules` with appropriate bypass policies - **LDAP**: Follow the patterns from existing services - **Local users**: Use SHB secret contracts for password management #### Security Best Practices {#security-best-practices} - **Bind to localhost**: Services should listen on `127.0.0.1` only - **Use nginx for TLS**: Don't configure TLS in the service itself - **Proper file permissions**: Use systemd.tmpfiles.rules - **Secret management**: Always use SHB secret contracts ### 3. Monitoring Implementation {#monitoring-implementation} Choose the appropriate monitoring approach: #### Option A: Native Prometheus Metrics {#native-prometheus-metrics} If the service supports Prometheus natively: ```nix services.prometheus.scrapeConfigs = [{ job_name = "servicename"; static_configs = [{ targets = [ "127.0.0.1:${toString cfg.port}" ]; }]; metrics_path = "/metrics"; }]; ``` #### Option B: API Health Check {#api-health-check} If no native metrics, monitor API endpoints: ```nix services.prometheus.scrapeConfigs = [{ job_name = "servicename"; static_configs = [{ targets = [ "127.0.0.1:${toString cfg.port}" ]; }]; metrics_path = "/api/status"; # or appropriate endpoint }]; ``` #### Option C: External Exporter {#external-exporter} For services requiring dedicated exporters (like Deluge): ```nix services.prometheus.exporters.servicename = { enable = true; # exporter-specific configuration }; ``` ### 4. Create Comprehensive Tests {#create-comprehensive-tests} Location: `test/services/servicename.nix` **Test structure:** ```nix { pkgs, ... }: let testLib = pkgs.callPackage ../common.nix {}; # Common test scripts commonTestScript = testLib.mkScripts { hasSSL = { node, ... }: !(isNull node.config.shb.servicename.ssl); waitForServices = { ... }: [ "nginx.service" "servicename.service" ]; waitForPorts = { node, ... }: [ node.config.services.servicename.port ]; # Service-specific connectivity test extraScript = { node, proto_fqdn, ... }: '' with subtest("service connectivity"): response = curl(client, "", "${proto_fqdn}/api/health") # Add service-specific checks ''; }; # Monitoring test script prometheusTestScript = { nodes, ... }: '' server.wait_for_open_port(${toString nodes.server.config.services.servicename.port}) with subtest("prometheus monitoring"): # Test the actual monitoring endpoint response = server.succeed("curl -sSf http://localhost:${port}/metrics") # Validate response format ''; # Base configuration basic = { config, ... }: { imports = [ testLib.baseModule ../../modules/services/servicename.nix ]; shb.servicename = { enable = true; inherit (config.test) domain subdomain; # Basic configuration }; }; in { # Test variants (all 6 required) basic = lib.shb.test.runNixOSTest { ... }; backup = lib.shb.test.runNixOSTest { ... }; https = lib.shb.test.runNixOSTest { ... }; ldap = lib.shb.test.runNixOSTest { ... }; monitoring = lib.shb.test.runNixOSTest { ... }; sso = lib.shb.test.runNixOSTest { ... }; } ``` #### Required Test Variants {#required-test-variants} 1. **basic**: Core functionality without authentication 2. **backup**: Tests backup integration 3. **https**: Tests SSL/TLS integration 4. **ldap**: Tests LDAP authentication 5. **monitoring**: Tests Prometheus integration 6. **sso**: Tests Authelia SSO integration ### 5. Update Flake Configuration {#update-flake-configuration} Add to `flake.nix`: ```nix allModules = [ # ... existing modules modules/services/servicename.nix ]; ``` ```nix checks = { # ... existing checks // (vm_test "servicename" ./test/services/servicename.nix) }; ``` ### 6. Create Service Documentation {#create-service-documentation} Create comprehensive documentation for the new service: **Location**: `modules/services/servicename/docs/default.md` ```markdown # ServiceName Service {\#services-servicename} Brief description of what the service does. ## Features {\#services-servicename-features} - Feature 1 - Feature 2 ## Usage {\#services-servicename-usage} ### Basic Configuration {\#services-servicename-basic} shb.servicename = { enable = true; domain = "example.com"; subdomain = "servicename"; }; ### SSL Configuration {\#services-servicename-ssl} shb.servicename.ssl.paths = { cert = /path/to/cert; key = /path/to/key; }; ## Options Reference {\#services-servicename-options} {=include=} options id-prefix: services-servicename-options- list-id: selfhostblocks-servicename-options source: @OPTIONS_JSON@ ``` **Important**: Use consistent heading ID patterns: - Service overview: `{\#services-servicename}` - Features: `{\#services-servicename-features}` - Usage sections: `{\#services-servicename-basic}`, `{\#services-servicename-ssl}`, etc. - Options: `{\#services-servicename-options}` Note: Replace `servicename` with your actual service name (e.g., `nzbget`, `jellyfin`). For the `@OPTIONS_JSON@` to work, a line must be added in the `flake.nix` file: ```nix packages.manualHtml = pkgs.callPackage ./docs { modules = { "blocks/authelia" = ./modules/blocks/authelia.nix; // Add line and keep in alphabetical order. }; }; ``` ### 7. Update Redirects Automatically {#update-redirects-automatically} After creating documentation, generate the required redirects: ```bash # Scan documentation and add missing redirects nix run .#update-redirects # Review the changes git diff docs/redirects.json # The tool will show what redirects were added ``` The automation will: - Find all heading IDs in your documentation - Generate appropriate redirect entries - Add them to `docs/redirects.json` - Follow established naming patterns ### 8. Handle Unfree Dependencies {#handle-unfree-dependencies} If the service requires unfree packages: ```nix # In flake.nix config = { allowUnfree = true; permittedInsecurePackages = [ # List any required insecure packages ]; }; ``` Update CI workflow if needed: ```yaml # In .github/workflows/build.yaml - name: Setup Nix uses: cachix/install-nix-action@v31 with: extra_nix_config: | allow-unfree = true ``` ## Testing and Validation {#testing-and-validation} ### Local Testing {#local-testing} ```bash # Test redirect automation nix run .#update-redirects # Test all service variants (replace ${system} with your system, e.g., x86_64-linux) nix build .#checks.${system}.vm_servicename_basic nix build .#checks.${system}.vm_servicename_backup nix build .#checks.${system}.vm_servicename_https nix build .#checks.${system}.vm_servicename_ldap nix build .#checks.${system}.vm_servicename_monitoring nix build .#checks.${system}.vm_servicename_sso # Or run all tests (as recommended in docs/contributing.md) nix flake check # For interactive testing and debugging, see docs/contributing.md: # nix run .#checks.${system}.vm_servicename_basic.driverInteractive # Test documentation build (includes redirect validation) nix build .#manualHtml ``` ### Iterative Development Approach {#iterative-development-approach} 1. **Start with basic functionality** - get core service working 2. **Add SSL integration** - enable HTTPS 3. **Add backup integration** - ensure data protection 4. **Add monitoring** - implement health checks 5. **Add authentication** - LDAP and SSO integration 6. **Create documentation** - write service documentation with heading IDs 7. **Update redirects** - run `nix run .#update-redirects` to generate redirects 8. **Comprehensive testing** - all 6 test variants 9. **Final validation** - ensure documentation builds correctly ## Common Pitfalls and Solutions {#common-pitfalls-and-solutions} ### Configuration Issues {#configuration-issues} - **Problem**: Service doesn't start due to config validation - **Solution**: Use `lib.mkDefault` for user settings, `lib.mkForce` for security settings ### Authentication Integration {#authentication-integration-pitfalls} - **Problem**: SSO redirect loops or access denied - **Solution**: Check `autheliaRules` bypass patterns for API endpoints ### Monitoring Failures {#monitoring-failures} - **Problem**: Prometheus scraping fails with 404 - **Solution**: Verify the actual API endpoints the service provides ### Test Failures {#test-failures} - **Problem**: VM tests timeout or fail connectivity - **Solution**: Check `waitForServices` and `waitForPorts` configurations ### Nixpkgs Integration {#nixpkgs-integration} - **Problem**: Service options don't match SHB needs - **Solution**: Map SHB options to nixpkgs options, use `extraConfig` for overrides ## Best Practices Summary {#best-practices-summary} 1. **Follow existing patterns** - study deluge.nix and vaultwarden.nix 2. **Use freeform configuration** - maximum flexibility with typed key options 3. **Implement all contracts** - SSL, backup, monitoring, secrets 4. **Test comprehensively** - all 6 integration variants 5. **Security first** - localhost binding, proper permissions, secret management 6. **Document thoroughly** - clear descriptions for all options 7. **Iterative development** - build complexity gradually 8. **CI/CD validation** - ensure all tests pass before submission ## Redirect Management {#redirect-management} SelfHostBlocks uses `nixos-render-docs` for documentation generation, which includes built-in redirect validation. The `docs/redirects.json` file maps documentation identifiers to their target URLs. ### Automated Redirect Generation {#automated-redirect-generation} SelfHostBlocks includes an automated redirect management tool that leverages the official `nixos-render-docs` ecosystem: ```bash # Generate fresh redirects from HTML documentation nix run .#update-redirects ``` This tool: - **Generates HTML documentation** using `nixos-render-docs` with redirect collection enabled - **Scans actual HTML files** for anchor IDs to ensure perfect accuracy - **Creates fresh redirects** from scratch by mapping anchors to their real file locations - **Filters system-generated anchors** (excludes `opt-*` and `selfhostblock*` entries) - **Provides interactive confirmation** before updating `docs/redirects.json` ### How Redirects Work {#how-redirects-work} 1. **nixos-render-docs validation**: During documentation builds, `nixos-render-docs` automatically validates that all heading IDs have corresponding redirect entries 2. **Automated maintenance**: The `update-redirects` tool automatically maintains `redirects.json` by: - Building HTML documentation with patched `nixos-render-docs` - Scanning generated HTML files for actual anchor IDs and their file locations - Creating accurate redirect mappings without guesswork or pattern matching 3. **Manual override**: You can still manually edit `docs/redirects.json` for special cases ### Redirect Patterns {#redirect-patterns} The automation follows these patterns when mapping headings to redirect targets: | Heading ID | Source File | Redirect Target | |------------|-------------|-----------------| | `services-nzbget-basic` | `modules/services/nzbget/docs/default.md` | `["services-nzbget.html#services-nzbget-basic"]` | | `blocks-monitoring` | `modules/blocks/monitoring/docs/default.md` | `["blocks-monitoring.html#blocks-monitoring"]` | | `demo-nextcloud` | `demo/nextcloud/README.md` | `["demo-nextcloud.html#demo-nextcloud"]` | | `contracts` | `docs/contracts.md` | `["contracts.html#contracts"]` | Note: Redirects always include the anchor link (`#heading-id`) to jump to the specific heading within the target page. ### Adding New Service Documentation {#adding-new-service-documentation} When implementing a new service, the redirect workflow is now automated: 1. **Write documentation** with heading IDs: ```markdown # NewService {\#services-newservice} ## Basic Configuration {\#services-newservice-basic} ``` 2. **Update redirects automatically**: ```bash nix run .#update-redirects ``` 3. **Review and commit** the changes: ```bash git add docs/redirects.json modules/services/newservice/docs/default.md git commit -m "Add newservice documentation" ``` ### Build-time Validation {#build-time-validation} The documentation build process will fail if: - Any documentation heading ID lacks a corresponding redirect entry - Redirect targets point to non-existent content - There are formatting errors in the redirects file This ensures documentation links remain functional when content is moved or reorganized. ## Resources {#resources} - **Contributing guide**: `docs/contributing.md` for authoritative development workflows and testing procedures - **Existing services**: `modules/services/` for patterns and implementation examples - **Contracts documentation**: `modules/contracts/` for understanding integration interfaces - **Test framework**: `test/common.nix` for testing utilities and patterns - **NixOS options**: https://search.nixos.org/options for upstream service options - **SHB documentation**: Generated docs showing existing service patterns - **Redirect automation**: `nix run .#update-redirects` for automated redirect management - **nixos-render-docs**: Built-in redirect validation and documentation generation ## Quick Reference {#quick-reference} ### Complete Workflow {#complete-workflow} ```bash # 1. Implement service module vim modules/services/SERVICENAME.nix # 2. Create tests vim test/services/SERVICENAME.nix # 3. Update flake vim flake.nix # Add to allModules and checks # 4. Write documentation vim modules/services/SERVICENAME/docs/default.md # 5. Generate redirects nix run .#update-redirects # 6. Test everything nix flake check # Run all tests (recommended) # Or test specific variants: # nix build .#checks.${system}.vm_SERVICENAME_basic nix build .#manualHtml # 7. Commit changes git add . git commit -m "Add SERVICENAME with full integration" ``` This guide provides a complete roadmap for implementing production-ready SelfHostBlocks services that meet the project's quality standards.