From 759e1041bd47d26b6e5f5d1a321ed076d15b7f33 Mon Sep 17 00:00:00 2001 From: houseme Date: Sun, 30 Aug 2026 00:14:44 +0800 Subject: [PATCH] feat(nix): add NixOS service module and client package (#6856) Co-authored-by: heihutu --- .github/workflows/nix.yml | 2 + README.md | 20 +++++ README_ZH.md | 6 ++ flake.nix | 74 ++++++++++++++++- nix/rustfs-module.nix | 171 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 269 insertions(+), 4 deletions(-) create mode 100644 nix/rustfs-module.nix diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index cfa77f3a5..31344c7b6 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -27,6 +27,7 @@ on: paths: - 'flake.nix' - 'flake.lock' + - 'nix/**' - 'Cargo.toml' - 'Cargo.lock' - '.github/workflows/nix.yml' @@ -36,6 +37,7 @@ on: paths: - 'flake.nix' - 'flake.lock' + - 'nix/**' - 'Cargo.toml' - 'Cargo.lock' - '.github/workflows/nix.yml' diff --git a/README.md b/README.md index 28eefe23e..ffa8423c8 100644 --- a/README.md +++ b/README.md @@ -245,6 +245,26 @@ nix build nix run ``` +The flake also exports a NixOS module and the RustFS `rc` client. Add the +module to your system and provide credentials through runtime files (for +example, sops-nix or agenix) so secrets are never stored in the Nix store: + +```nix +imports = [ inputs.rustfs.nixosModules.rustfs ]; + +services.rustfs = { + enable = true; + accessKeyFile = "/run/secrets/rustfs-access-key"; + secretKeyFile = "/run/secrets/rustfs-secret-key"; + volumes = [ "/var/lib/rustfs" ]; +}; +``` + +Install the S3-compatible client with +`nix profile install github:rustfs/rustfs#rustfs-client` (the executable is named +`rc`), or use `inputs.rustfs.packages.${pkgs.system}.rustfs-client` in a system +configuration. + ### 6\. X-CMD (Option 6) If you are an [x-cmd](https://www.x-cmd.com/install/rustfs) user: diff --git a/README_ZH.md b/README_ZH.md index aac677df2..c97c13391 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -191,6 +191,12 @@ nix build nix run ``` +该 Flake 同时提供 NixOS 模块和 RustFS `rc` 客户端。将 +`inputs.rustfs.nixosModules.rustfs` 加入 `imports`,并通过运行时密钥文件 +(例如 sops-nix 或 agenix)配置 `accessKeyFile` 与 `secretKeyFile`,避免密钥 +进入 Nix store。客户端包为 +`inputs.rustfs.packages.${pkgs.system}.rustfs-client`,安装后的命令名为 `rc`。 + ### 6\. X-CMD (Option 6) 如果你是 [x-cmd](https://www.x-cmd.com/install/rustfs) 用户: diff --git a/flake.nix b/flake.nix index 4b1526448..6fbe3240a 100644 --- a/flake.nix +++ b/flake.nix @@ -35,6 +35,23 @@ forAllSystems = nixpkgs.lib.genAttrs systems; in { + nixosModules.rustfs = import ./nix/rustfs-module.nix { + defaultPackage = system: self.packages.${system}.rustfs; + }; + nixosModules.default = self.nixosModules.rustfs; + + overlays.default = final: prev: + let + packages = self.packages.${prev.stdenv.hostPlatform.system}; + in + { + rustfs = packages.rustfs; + } + // prev.lib.optionalAttrs (builtins.hasAttr "rustfs-client" packages) { + rustfs-client = packages.rustfs-client; + rc = packages.rustfs-client; + }; + packages = forAllSystems ( system: let @@ -55,9 +72,10 @@ cargo = rustToolchain; rustc = rustToolchain; }; - in - { - default = rustPlatform.buildRustPackage { + + clientVersion = "0.1.32"; + + rustfs = rustPlatform.buildRustPackage { pname = "rustfs"; version = "1.0.0-rc.4"; @@ -82,7 +100,6 @@ "rustfs" ]; - # Set environment variables for build PROTOC = "${pkgs.protobuf}/bin/protoc"; doCheck = false; @@ -94,6 +111,55 @@ mainProgram = "rustfs"; }; }; + + clientAssets = { + "x86_64-linux" = { + name = "rustfs-cli-linux-amd64-v${clientVersion}.tar.gz"; + hash = "sha256-qwDZNwedy28ce0HTS7+q0OsL1PchhnLLy3wzZS0cRt8="; + }; + "aarch64-linux" = { + name = "rustfs-cli-linux-arm64-v${clientVersion}.tar.gz"; + hash = "sha256-1T1M9Q3lcy9IJo/n5eQezmbTaEgHVJbx1QFCaTX3BYY="; + }; + }; + + clientSupported = builtins.hasAttr system clientAssets; + + clientPackage = + if clientSupported then + let + asset = clientAssets.${system}; + in + pkgs.stdenvNoCC.mkDerivation { + pname = "rustfs-cli"; + version = clientVersion; + src = pkgs.fetchurl { + url = "https://github.com/rustfs/cli/releases/download/v${clientVersion}/${asset.name}"; + inherit (asset) hash; + }; + sourceRoot = "."; + installPhase = '' + runHook preInstall + install -Dm755 rc "$out/bin/rc" + runHook postInstall + ''; + meta = { + description = "RustFS S3-compatible command-line client"; + homepage = "https://github.com/rustfs/cli"; + license = pkgs.lib.licenses.asl20; + mainProgram = "rc"; + }; + } + else + null; + in + { + inherit rustfs; + default = rustfs; + } + // pkgs.lib.optionalAttrs clientSupported { + rustfs-client = clientPackage; + rc = clientPackage; } ); diff --git a/nix/rustfs-module.nix b/nix/rustfs-module.nix new file mode 100644 index 000000000..f86e23827 --- /dev/null +++ b/nix/rustfs-module.nix @@ -0,0 +1,171 @@ +# Copyright 2024 RustFS Team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +{ defaultPackage }: + +{ config, lib, pkgs, ... }: + +let + cfg = config.services.rustfs; + volumeArguments = lib.concatStringsSep " " cfg.volumes; +in +{ + options.services.rustfs = { + enable = lib.mkEnableOption "the RustFS object storage server"; + + package = lib.mkOption { + type = lib.types.package; + default = defaultPackage pkgs.stdenv.hostPlatform.system; + defaultText = lib.literalExpression "inputs.rustfs.packages.\${pkgs.system}.rustfs"; + description = "RustFS server package to run."; + }; + + user = lib.mkOption { + type = lib.types.str; + default = "rustfs"; + description = "User account under which RustFS runs."; + }; + + group = lib.mkOption { + type = lib.types.str; + default = "rustfs"; + description = "Group under which RustFS runs."; + }; + + volumes = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ "/var/lib/rustfs" ]; + description = "Data volumes passed to RustFS via RUSTFS_VOLUMES."; + }; + + address = lib.mkOption { + type = lib.types.str; + default = ":9000"; + description = "Address on which the S3 API listens."; + }; + + consoleEnable = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Whether to enable the management console."; + }; + + consoleAddress = lib.mkOption { + type = lib.types.str; + default = "127.0.0.1:9001"; + description = "Address on which the management console listens."; + }; + + accessKeyFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + example = "/run/secrets/rustfs-access-key"; + description = "Runtime file containing the root access key."; + }; + + secretKeyFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + example = "/run/secrets/rustfs-secret-key"; + description = "Runtime file containing the root secret key."; + }; + + logLevel = lib.mkOption { + type = lib.types.str; + default = "info"; + description = "Rust log filter passed through RUST_LOG."; + }; + + extraEnvironmentVariables = lib.mkOption { + type = lib.types.attrsOf lib.types.str; + default = { }; + description = "Additional environment variables for the service."; + }; + }; + + config = lib.mkIf cfg.enable { + assertions = [ + { + assertion = cfg.volumes != [ ]; + message = "services.rustfs.volumes must contain at least one path."; + } + { + assertion = builtins.all (volume: lib.hasPrefix "/" volume) cfg.volumes; + message = "services.rustfs.volumes entries must be absolute local paths."; + } + { + assertion = cfg.accessKeyFile != null; + message = "services.rustfs.accessKeyFile must be set; default credentials are not enabled by this module."; + } + { + assertion = cfg.secretKeyFile != null; + message = "services.rustfs.secretKeyFile must be set; default credentials are not enabled by this module."; + } + { + assertion = !(builtins.hasAttr "RUSTFS_ACCESS_KEY" cfg.extraEnvironmentVariables); + message = "services.rustfs.extraEnvironmentVariables must not set RUSTFS_ACCESS_KEY; use accessKeyFile."; + } + { + assertion = !(builtins.hasAttr "RUSTFS_SECRET_KEY" cfg.extraEnvironmentVariables); + message = "services.rustfs.extraEnvironmentVariables must not set RUSTFS_SECRET_KEY; use secretKeyFile."; + } + ]; + + users.groups.${cfg.group} = { }; + users.users.${cfg.user} = { + group = cfg.group; + isSystemUser = true; + description = "RustFS service user"; + }; + + systemd.tmpfiles.rules = lib.map (volume: "d ${volume} 0750 ${cfg.user} ${cfg.group} -") cfg.volumes; + + systemd.services.rustfs = { + description = "RustFS Object Storage Server"; + documentation = [ "https://docs.rustfs.com/" ]; + wantedBy = [ "multi-user.target" ]; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + + environment = cfg.extraEnvironmentVariables // { + RUSTFS_VOLUMES = volumeArguments; + RUSTFS_ADDRESS = cfg.address; + RUSTFS_CONSOLE_ENABLE = lib.boolToString cfg.consoleEnable; + RUSTFS_CONSOLE_ADDRESS = cfg.consoleAddress; + RUSTFS_ACCESS_KEY_FILE = "%d/access-key"; + RUSTFS_SECRET_KEY_FILE = "%d/secret-key"; + RUST_LOG = cfg.logLevel; + }; + + serviceConfig = { + ExecStart = "${cfg.package}/bin/rustfs"; + User = cfg.user; + Group = cfg.group; + LoadCredential = lib.optionals (cfg.accessKeyFile != null && cfg.secretKeyFile != null) [ + "access-key:${toString cfg.accessKeyFile}" + "secret-key:${toString cfg.secretKeyFile}" + ]; + Restart = "on-failure"; + RestartSec = "5s"; + LimitNOFILE = 1048576; + NoNewPrivileges = true; + PrivateTmp = true; + ProtectHome = true; + ProtectSystem = "strict"; + ReadWritePaths = cfg.volumes; + UMask = "0077"; + }; + }; + }; +}