Compare commits

..

7 commits
v0.1.0 ... main

Author SHA1 Message Date
b6309a9e12 Update hm module to use the user's netrc
Some checks are pending
buildbot/nix-eval Build done.
buildbot/nix-build Build started.
2026-02-27 09:53:35 +01:00
b0959fcf38 Fix netrc writing bug 2026-02-27 09:53:24 +01:00
29fe7f76c1 expand the path to support working in systemd service
All checks were successful
buildbot/nix-eval Build done.
buildbot/nix-build Build done.
buildbot/nix-effects Build done.
2026-02-27 09:28:29 +01:00
ba34ac0d67 add nixos and darwin-nix modules 2026-02-27 09:27:34 +01:00
eeb8a69740 add package to PATH
All checks were successful
buildbot/nix-eval Build done.
buildbot/nix-build Build done.
buildbot/nix-effects Build done.
2026-02-27 08:39:18 +01:00
7b49665ee5 lower min go version
All checks were successful
buildbot/nix-eval Build done.
buildbot/nix-build Build done.
buildbot/nix-effects Build done.
2026-02-27 08:36:10 +01:00
07bd576628 add initial nixos modules
All checks were successful
buildbot/nix-eval Build done.
buildbot/nix-build gitea:ops/nix-cache-login#checks.x86_64-linux.devShell Build done.
buildbot/nix-build gitea:ops/nix-cache-login#checks.x86_64-linux.tests Build done.
buildbot/nix-build Build done.
buildbot/nix-effects Build done.
2026-02-26 19:11:53 +01:00
11 changed files with 296 additions and 23 deletions

View file

@ -4,6 +4,10 @@
Changes yet to be released are documented here. Changes yet to be released are documented here.
- Fix path expansion when running in systemd
- Fix bug in netrc writing
- Add nixos, home-manager, and darwin-nix modules
## v0.1.0 ## v0.1.0
Initial release. Initial release.

View file

@ -6,6 +6,7 @@
let let
systems = [ systems = [
"x86_64-linux" "x86_64-linux"
"aarch64-linux"
"aarch64-darwin" "aarch64-darwin"
]; ];
forAllSystems = fn: nixpkgs.lib.genAttrs systems (system: fn nixpkgs.legacyPackages.${system}); forAllSystems = fn: nixpkgs.lib.genAttrs systems (system: fn nixpkgs.legacyPackages.${system});
@ -23,7 +24,9 @@
}; };
}); });
checks = forAllSystems (pkgs: { checks = forAllSystems (
pkgs:
{
tests = self.packages.${pkgs.stdenv.hostPlatform.system}.default.overrideAttrs (_: { tests = self.packages.${pkgs.stdenv.hostPlatform.system}.default.overrideAttrs (_: {
pname = "nix-cache-login-tests"; pname = "nix-cache-login-tests";
checkPhase = '' checkPhase = ''
@ -34,7 +37,11 @@
doCheck = true; doCheck = true;
}); });
devShell = self.devShells.${pkgs.stdenv.hostPlatform.system}.default; devShell = self.devShells.${pkgs.stdenv.hostPlatform.system}.default;
}); }
// pkgs.lib.optionalAttrs pkgs.stdenv.isLinux {
nixos-module = pkgs.testers.runNixOSTest (import ./nixos-test.nix self);
}
);
devShells = forAllSystems (pkgs: { devShells = forAllSystems (pkgs: {
default = pkgs.mkShell { default = pkgs.mkShell {
@ -44,5 +51,54 @@
]; ];
}; };
}); });
homeModules = {
# Workstation (Linux + macOS): home-manager module running `nix-cache-login refresh`
default =
{
config,
lib,
pkgs,
...
}:
{
imports = [ ./home-module.nix ];
services.nix-cache-login.package =
lib.mkDefault
self.packages.${pkgs.stdenv.hostPlatform.system}.default;
};
};
nixosModules = {
# Workstation: systemd user timer+service running `nix-cache-login refresh`
default =
{
config,
lib,
pkgs,
...
}:
{
imports = [ ./nixos-module.nix ];
services.nix-cache-login.package =
lib.mkDefault
self.packages.${pkgs.stdenv.hostPlatform.system}.default;
};
# Server: system-level timer+service running `nix-cache-login service-account`
server =
{
config,
lib,
pkgs,
...
}:
{
imports = [ ./nixos-module-server.nix ];
services.nix-cache-login-server.package =
lib.mkDefault
self.packages.${pkgs.stdenv.hostPlatform.system}.default;
};
};
}; };
} }

2
go.mod
View file

@ -1,6 +1,6 @@
module guardianproject.dev/ops/nix-cache-login module guardianproject.dev/ops/nix-cache-login
go 1.25.7 go 1.25.0
require ( require (
github.com/adrg/xdg v0.5.3 github.com/adrg/xdg v0.5.3

63
home-module.nix Normal file
View file

@ -0,0 +1,63 @@
{
config,
lib,
...
}:
let
cfg = config.services.nix-cache-login;
in
{
options.services.nix-cache-login = {
enable = lib.mkEnableOption "nix-cache-login automatic token refresh";
package = lib.mkOption {
type = lib.types.package;
description = "The nix-cache-login package to use.";
};
refreshInterval = lib.mkOption {
type = lib.types.ints.positive;
default = 900;
description = ''
How often to attempt token refresh, in seconds.
If no valid session exists, the service logs an error and retries on
the next interval. Run {command}`nix-cache-login` to log in.
'';
example = 1800;
};
};
config = lib.mkIf cfg.enable {
nix.settings.netrc-file = "${config.xdg.configHome}/nix/netrc";
home.packages = [ cfg.package ];
systemd.user.services.nix-cache-login = {
Unit.Description = "Nix cache login - refresh access token";
Service = {
Type = "oneshot";
ExecStart = "${cfg.package}/bin/nix-cache-login refresh";
};
};
systemd.user.timers.nix-cache-login = {
Unit.Description = "Nix cache login - periodic token refresh";
Timer = {
OnBootSec = "2min";
OnUnitActiveSec = "${toString cfg.refreshInterval}s";
};
Install.WantedBy = [ "timers.target" ];
};
launchd.agents.nix-cache-login = {
enable = true;
config = {
ProgramArguments = [
"${cfg.package}/bin/nix-cache-login"
"refresh"
];
StartInterval = cfg.refreshInterval;
RunAtLoad = true;
ProcessType = "Background";
StandardOutPath = "${config.home.homeDirectory}/Library/Logs/nix-cache-login.log";
StandardErrorPath = "${config.home.homeDirectory}/Library/Logs/nix-cache-login.log";
};
};
};
}

View file

@ -37,8 +37,8 @@ func Load(path string) (*Config, error) {
return nil, fmt.Errorf("parsing config file: %w", err) return nil, fmt.Errorf("parsing config file: %w", err)
} }
cfg.NetrcPath = os.ExpandEnv(cfg.NetrcPath) cfg.NetrcPath = expandPath(cfg.NetrcPath)
cfg.ClientSecretFile = os.ExpandEnv(cfg.ClientSecretFile) cfg.ClientSecretFile = expandPath(cfg.ClientSecretFile)
if cfg.ClientSecretFile != "" { if cfg.ClientSecretFile != "" {
secret, err := os.ReadFile(cfg.ClientSecretFile) secret, err := os.ReadFile(cfg.ClientSecretFile)
@ -71,6 +71,29 @@ func (c *Config) validate() error {
return nil return nil
} }
// expandPath expands environment variables in a path. XDG base directory
// variables are resolved using the xdg library, which applies the XDG spec
// fallbacks (e.g. $HOME/.config when $XDG_CONFIG_HOME is unset). This ensures
// correct behaviour in systemd user services, which do not set XDG variables.
func expandPath(s string) string {
return os.Expand(s, func(key string) string {
switch key {
case "XDG_CONFIG_HOME":
return xdg.ConfigHome
case "XDG_DATA_HOME":
return xdg.DataHome
case "XDG_CACHE_HOME":
return xdg.CacheHome
case "XDG_STATE_HOME":
return xdg.StateHome
case "XDG_RUNTIME_DIR":
return xdg.RuntimeDir
default:
return os.Getenv(key)
}
})
}
// RefreshTokenPath returns the path to the stored refresh token. // RefreshTokenPath returns the path to the stored refresh token.
func RefreshTokenPath() string { func RefreshTokenPath() string {
return filepath.Join(xdg.ConfigHome, "nix-cache-login", "refresh-token") return filepath.Join(xdg.ConfigHome, "nix-cache-login", "refresh-token")

View file

@ -25,7 +25,7 @@ func Upsert(path, machine, password string) error {
} }
} }
if !found { if !found {
entries = append(entries, entry{machine: machine, password: password}) entries = append(entries, entry{machine: machine, login: "dummy", password: password})
} }
return write(path, entries) return write(path, entries)
@ -71,6 +71,7 @@ func GetPassword(path, machine string) (string, error) {
type entry struct { type entry struct {
machine string machine string
login string
password string password string
} }
@ -102,6 +103,10 @@ func parse(path string) ([]entry, error) {
entries = append(entries, *current) entries = append(entries, *current)
} }
current = &entry{machine: fields[1]} current = &entry{machine: fields[1]}
case "login":
if current != nil {
current.login = fields[1]
}
case "password": case "password":
if current != nil { if current != nil {
current.password = fields[1] current.password = fields[1]
@ -129,7 +134,7 @@ func write(path string, entries []entry) error {
if i > 0 { if i > 0 {
b.WriteString("\n") b.WriteString("\n")
} }
fmt.Fprintf(&b, "machine %s\npassword %s\n", e.machine, e.password) fmt.Fprintf(&b, "machine %s\nlogin %s\npassword %s\n", e.machine, e.login, e.password)
} }
if err := os.WriteFile(path, []byte(b.String()), 0600); err != nil { if err := os.WriteFile(path, []byte(b.String()), 0600); err != nil {

View file

@ -27,7 +27,7 @@ func TestUpsertUpdateExisting(t *testing.T) {
dir := t.TempDir() dir := t.TempDir()
path := filepath.Join(dir, "netrc") path := filepath.Join(dir, "netrc")
initial := "machine other.host\npassword otherpass\n\nmachine cache.example.com\npassword oldtoken\n" initial := "machine other.host\nlogin dummy\npassword otherpass\n\nmachine cache.example.com\nlogin dummy\npassword oldtoken\n"
if err := os.WriteFile(path, []byte(initial), 0600); err != nil { if err := os.WriteFile(path, []byte(initial), 0600); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -59,7 +59,7 @@ func TestUpsertAppend(t *testing.T) {
dir := t.TempDir() dir := t.TempDir()
path := filepath.Join(dir, "netrc") path := filepath.Join(dir, "netrc")
initial := "machine existing.host\npassword existingpass\n" initial := "machine existing.host\nlogin dummy\npassword existingpass\n"
if err := os.WriteFile(path, []byte(initial), 0600); err != nil { if err := os.WriteFile(path, []byte(initial), 0600); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -89,7 +89,7 @@ func TestRemove(t *testing.T) {
dir := t.TempDir() dir := t.TempDir()
path := filepath.Join(dir, "netrc") path := filepath.Join(dir, "netrc")
initial := "machine keep.host\npassword keeppass\n\nmachine remove.host\npassword removepass\n" initial := "machine keep.host\nlogin dummy\npassword keeppass\n\nmachine remove.host\nlogin dummy\npassword removepass\n"
if err := os.WriteFile(path, []byte(initial), 0600); err != nil { if err := os.WriteFile(path, []byte(initial), 0600); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -141,7 +141,7 @@ func TestGetPasswordNotFound(t *testing.T) {
dir := t.TempDir() dir := t.TempDir()
path := filepath.Join(dir, "netrc") path := filepath.Join(dir, "netrc")
content := "machine other.host\npassword otherpass\n" content := "machine other.host\nlogin dummy\npassword otherpass\n"
if err := os.WriteFile(path, []byte(content), 0600); err != nil { if err := os.WriteFile(path, []byte(content), 0600); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -179,7 +179,7 @@ func TestFilePermissionsCorrected(t *testing.T) {
path := filepath.Join(dir, "netrc") path := filepath.Join(dir, "netrc")
// Create file with overly permissive mode // Create file with overly permissive mode
if err := os.WriteFile(path, []byte("machine old.host\npassword oldpass\n"), 0644); err != nil { if err := os.WriteFile(path, []byte("machine old.host\nlogin dummy\npassword oldpass\n"), 0644); err != nil {
t.Fatal(err) t.Fatal(err)
} }

50
nixos-module-server.nix Normal file
View file

@ -0,0 +1,50 @@
{ config, lib, ... }:
let
cfg = config.services.nix-cache-login-server;
in
{
options.services.nix-cache-login-server = {
enable = lib.mkEnableOption "nix-cache-login service-account token refresh";
package = lib.mkOption {
type = lib.types.package;
description = "The nix-cache-login package to use.";
};
configFile = lib.mkOption {
type = lib.types.path;
description = ''
Path to the nix-cache-login config.toml file. Must include
client_secret_file pointing to a readable credentials file.
'';
example = "/etc/nix-cache-login/config.toml";
};
refreshInterval = lib.mkOption {
type = lib.types.str;
default = "15min";
description = ''
Interval between token refresh attempts, as a systemd time span.
On failure the service logs an error and the timer retries on schedule.
'';
example = "1h";
};
};
config = lib.mkIf cfg.enable {
environment.systemPackages = [ cfg.package ];
systemd.services.nix-cache-login = {
description = "Nix cache login - service account token refresh";
serviceConfig = {
Type = "oneshot";
ExecStart = "${cfg.package}/bin/nix-cache-login --config ${cfg.configFile} service-account";
};
};
systemd.timers.nix-cache-login = {
description = "Nix cache login - periodic service account token refresh";
timerConfig = {
OnBootSec = "2min";
OnUnitActiveSec = cfg.refreshInterval;
};
wantedBy = [ "timers.target" ];
};
};
}

43
nixos-module.nix Normal file
View file

@ -0,0 +1,43 @@
{ config, lib, ... }:
let
cfg = config.services.nix-cache-login;
in
{
options.services.nix-cache-login = {
enable = lib.mkEnableOption "nix-cache-login automatic token refresh";
package = lib.mkOption {
type = lib.types.package;
description = "The nix-cache-login package to use.";
};
refreshInterval = lib.mkOption {
type = lib.types.str;
default = "15min";
description = ''
Interval between token refresh attempts, as a systemd time span.
If no valid session exists, the service logs an error and the timer
retries on the next interval. Run nix-cache-login to log in.
'';
example = "1h";
};
};
config = lib.mkIf cfg.enable {
environment.systemPackages = [ cfg.package ];
systemd.user.services.nix-cache-login = {
description = "Nix cache login - refresh access token";
serviceConfig = {
Type = "oneshot";
ExecStart = "${cfg.package}/bin/nix-cache-login refresh";
};
};
systemd.user.timers.nix-cache-login = {
description = "Nix cache login - periodic token refresh";
timerConfig = {
OnBootSec = "2min";
OnUnitActiveSec = cfg.refreshInterval;
};
wantedBy = [ "timers.target" ];
};
};
}

29
nixos-test.nix Normal file
View file

@ -0,0 +1,29 @@
self: {
name = "nix-cache-login-nixos-module";
nodes.machine =
{ ... }:
{
imports = [ self.nixosModules.default ];
services.nix-cache-login.enable = true;
};
testScript = ''
machine.wait_for_unit("multi-user.target")
# The module should install timer and service unit files for all users
machine.succeed("test -f /etc/systemd/user/nix-cache-login.timer")
machine.succeed("test -f /etc/systemd/user/nix-cache-login.service")
# wantedBy = ["timers.target"] should create this symlink
machine.succeed(
"test -L /etc/systemd/user/timers.target.wants/nix-cache-login.timer"
)
# Service unit should reference the correct subcommand
unit = machine.succeed("cat /etc/systemd/user/nix-cache-login.service")
assert "nix-cache-login refresh" in unit, (
f"ExecStart not found in service unit:\n{unit}"
)
'';
}

View file

@ -6,7 +6,7 @@
buildGoModule { buildGoModule {
pname = "nix-cache-login"; pname = "nix-cache-login";
version = "0.1.0"; version = "0.1.2";
src = ./.; src = ./.;
# src = fetchgit { # src = fetchgit {
# url = "https://guardianproject.dev/ops/nix-cache-login.git"; # url = "https://guardianproject.dev/ops/nix-cache-login.git";