Deployment and lifecycle management for Nix
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

activator: switch to systemd socket activation

+105 -28
+81 -13
cmd/sower-activator/server.go
··· 3 3 import ( 4 4 "context" 5 5 "errors" 6 + "fmt" 6 7 "log/slog" 7 8 "net" 8 9 "os" 9 10 "os/signal" 11 + "strconv" 10 12 "sync" 11 13 "syscall" 12 14 ) 15 + 16 + var errSystemdSocketUnavailable = errors.New("systemd socket activation unavailable") 17 + 18 + const systemdListenFDStart = 3 13 19 14 20 // Server handles Unix socket connections for activation requests. 15 21 type Server struct { ··· 31 37 32 38 // Run starts the server and blocks until shutdown. 33 39 func (s *Server) Run(ctx context.Context) error { 34 - // Remove existing socket file if present 35 - if err := os.Remove(s.socketPath); err != nil && !os.IsNotExist(err) { 40 + listener, err := listenerFromSystemd() 41 + if err != nil && !errors.Is(err, errSystemdSocketUnavailable) { 36 42 return err 37 43 } 38 44 39 - // Create listener 40 - var err error 41 - s.listener, err = net.Listen("unix", s.socketPath) 42 - if err != nil { 43 - return err 44 - } 45 - defer s.listener.Close() 45 + if errors.Is(err, errSystemdSocketUnavailable) { 46 + // Remove existing socket file if present 47 + if err := os.Remove(s.socketPath); err != nil && !os.IsNotExist(err) { 48 + return err 49 + } 50 + 51 + // Create listener when not socket-activated 52 + listener, err = net.Listen("unix", s.socketPath) 53 + if err != nil { 54 + return err 55 + } 56 + 57 + // Set socket permissions (owner rw, group rw) 58 + if err := os.Chmod(s.socketPath, 0660); err != nil { 59 + return err 60 + } 46 61 47 - // Set socket permissions (owner rw, group rw) 48 - if err := os.Chmod(s.socketPath, 0660); err != nil { 49 - return err 62 + slog.Info("Server listening", "socket", s.socketPath, "mode", "standalone") 63 + } else { 64 + slog.Info("Server listening", "socket", s.socketPath, "mode", "systemd-socket-activation") 50 65 } 51 66 52 - slog.Info("Server listening", "socket", s.socketPath) 67 + s.listener = listener 68 + defer s.listener.Close() 53 69 54 70 // Handle shutdown 55 71 go func() { ··· 80 96 slog.Info("Server stopped") 81 97 82 98 return nil 99 + } 100 + 101 + func listenerFromSystemd() (net.Listener, error) { 102 + pidStr := os.Getenv("LISTEN_PID") 103 + fdsStr := os.Getenv("LISTEN_FDS") 104 + if pidStr == "" && fdsStr == "" { 105 + return nil, errSystemdSocketUnavailable 106 + } 107 + if pidStr == "" || fdsStr == "" { 108 + return nil, fmt.Errorf("incomplete systemd socket activation environment") 109 + } 110 + 111 + listenPID, err := strconv.Atoi(pidStr) 112 + if err != nil { 113 + return nil, fmt.Errorf("invalid LISTEN_PID: %w", err) 114 + } 115 + if listenPID != os.Getpid() { 116 + return nil, errSystemdSocketUnavailable 117 + } 118 + 119 + listenFDs, err := strconv.Atoi(fdsStr) 120 + if err != nil { 121 + return nil, fmt.Errorf("invalid LISTEN_FDS: %w", err) 122 + } 123 + if listenFDs <= 0 { 124 + return nil, errSystemdSocketUnavailable 125 + } 126 + if listenFDs != 1 { 127 + return nil, fmt.Errorf("expected exactly 1 socket from systemd, got %d", listenFDs) 128 + } 129 + 130 + _ = os.Unsetenv("LISTEN_PID") 131 + _ = os.Unsetenv("LISTEN_FDS") 132 + _ = os.Unsetenv("LISTEN_FDNAMES") 133 + 134 + file := os.NewFile(uintptr(systemdListenFDStart), "systemd-activator-socket") 135 + if file == nil { 136 + return nil, fmt.Errorf("failed to access systemd socket FD") 137 + } 138 + defer file.Close() 139 + 140 + listener, err := net.FileListener(file) 141 + if err != nil { 142 + return nil, fmt.Errorf("creating listener from systemd socket: %w", err) 143 + } 144 + 145 + if _, ok := listener.(*net.UnixListener); !ok { 146 + listener.Close() 147 + return nil, fmt.Errorf("systemd socket is not a unix listener") 148 + } 149 + 150 + return listener, nil 83 151 } 84 152 85 153 // RunServer is the entry point for server mode.
+21 -12
nix/nixos/activator.nix
··· 42 42 }; 43 43 44 44 config = lib.mkIf cfg.enable { 45 - systemd.services.sower-activator = { 46 - description = "Sower Activator Service"; 47 - wantedBy = [ "multi-user.target" ]; 48 - after = [ "network.target" ]; 45 + systemd.sockets.sower-activator = { 46 + description = "Sower Activator Socket"; 47 + wantedBy = [ "sockets.target" ]; 49 48 50 49 # Start before agent so socket is ready 51 50 before = [ "sower-agent.service" ]; 52 51 52 + socketConfig = { 53 + ListenStream = cfg.socketPath; 54 + SocketMode = "0660"; 55 + SocketUser = "root"; 56 + SocketGroup = cfg.socketGroup; 57 + DirectoryMode = "0755"; 58 + RemoveOnStop = true; 59 + }; 60 + }; 61 + 62 + systemd.services.sower-activator = { 63 + description = "Sower Activator Service"; 64 + requires = [ "sower-activator.socket" ]; 65 + after = [ 66 + "network.target" 67 + "sower-activator.socket" 68 + ]; 69 + 53 70 path = [ 54 71 config.nix.package 55 72 pkgs.getent ··· 62 79 Type = "simple"; 63 80 Restart = "on-failure"; 64 81 RestartSec = "5s"; 65 - 66 - # Run as root (needed for NixOS activation) but with socketGroup 67 - # so the socket is created with root:socketGroup ownership 68 - Group = cfg.socketGroup; 69 - 70 - # RuntimeDirectory creates /run/sower-activator with proper ownership 71 - RuntimeDirectory = "sower-activator"; 72 - RuntimeDirectoryMode = "0750"; 73 82 74 83 # Build allowed GIDs list at runtime (group GIDs may not be known at eval time) 75 84 ExecStart =
+2 -2
nix/nixos/agent.nix
··· 97 97 "network-online.target" 98 98 ] 99 99 ++ lib.optionals config.services.sower.server.enable [ "sower.service" ] 100 - ++ lib.optionals activatorCfg.enable [ "sower-activator.service" ]; 100 + ++ lib.optionals activatorCfg.enable [ "sower-activator.socket" ]; 101 101 requires = [ 102 102 "network-online.target" 103 103 ]; 104 104 wants = 105 - lib.optionals activatorCfg.enable [ "sower-activator.service" ] 105 + lib.optionals activatorCfg.enable [ "sower-activator.socket" ] 106 106 ++ lib.optionals config.services.sower.server.enable [ "sower.service" ]; 107 107 108 108 path = [
+1 -1
nix/tests/e2e.nix
··· 139 139 start_all() 140 140 server.wait_for_unit("postgresql.service") 141 141 server.wait_for_unit("sower.service") 142 - server.wait_for_unit("sower-activator.service") 142 + server.wait_for_unit("sower-activator.socket") 143 143 server.wait_for_unit("sower-agent.service") 144 144 server.wait_for_open_port(4000) 145 145