Deployment and lifecycle management for Nix
0
fork

Configure Feed

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

activator: run as oneshot with systemd socket

+28 -214
+22 -9
cmd/sower-activator/main.go
··· 3 3 import ( 4 4 "flag" 5 5 "log/slog" 6 + "net" 6 7 "os" 7 8 "strconv" 8 9 "strings" ··· 12 13 func main() { 13 14 // Define command-line flags 14 15 var ( 15 - // Server mode flags 16 - serverMode = flag.Bool("server", false, "Run as server daemon") 17 - socketPath = flag.String("socket", "/run/sower-activator/activator.sock", "Unix socket path") 16 + socketMode = flag.Bool("socket-mode", false, "Process one request from connected socket on stdin") 18 17 allowedUIDs = flag.String("allowed-uids", "", "Comma-separated list of allowed UIDs") 19 18 allowedGIDs = flag.String("allowed-gids", "", "Comma-separated list of allowed GIDs") 20 19 ··· 32 31 // Setup logger 33 32 initLogger(*debug) 34 33 35 - if *serverMode { 36 - runServerMode(*socketPath, *allowedUIDs, *allowedGIDs) 34 + if *socketMode || (*seedType == "" && stdinIsSocket()) { 35 + runSocketMode(*allowedUIDs, *allowedGIDs) 37 36 } else { 38 37 runCLIMode(*seedType, *path, *mode) 39 38 } 40 39 } 41 40 42 - func runServerMode(socketPath, allowedUIDsStr, allowedGIDsStr string) { 41 + func runSocketMode(allowedUIDsStr, allowedGIDsStr string) { 43 42 // Check if running as root 44 43 if os.Getuid() != 0 { 45 - slog.Error("Server mode must be run as root") 44 + slog.Error("Socket mode must be run as root") 46 45 os.Exit(1) 47 46 } 48 47 ··· 58 57 os.Exit(1) 59 58 } 60 59 61 - if err := RunServer(socketPath, uids, gids); err != nil { 62 - slog.Error("Server error", "error", err) 60 + conn, err := net.FileConn(os.Stdin) 61 + if err != nil { 62 + slog.Error("Failed to get socket connection from stdin", "error", err) 63 63 os.Exit(1) 64 64 } 65 + defer conn.Close() 66 + 67 + handler := NewConnectionHandler(conn, uids, gids) 68 + handler.Handle() 69 + } 70 + 71 + func stdinIsSocket() bool { 72 + info, err := os.Stdin.Stat() 73 + if err != nil { 74 + return false 75 + } 76 + 77 + return info.Mode()&os.ModeSocket != 0 65 78 } 66 79 67 80 func runCLIMode(seedType, path, mode string) {
-169
cmd/sower-activator/server.go
··· 1 - package main 2 - 3 - import ( 4 - "context" 5 - "errors" 6 - "fmt" 7 - "log/slog" 8 - "net" 9 - "os" 10 - "os/signal" 11 - "strconv" 12 - "sync" 13 - "syscall" 14 - ) 15 - 16 - var errSystemdSocketUnavailable = errors.New("systemd socket activation unavailable") 17 - 18 - const systemdListenFDStart = 3 19 - 20 - // Server handles Unix socket connections for activation requests. 21 - type Server struct { 22 - socketPath string 23 - allowedUIDs []uint32 24 - allowedGIDs []uint32 25 - listener net.Listener 26 - wg sync.WaitGroup 27 - } 28 - 29 - // NewServer creates a new activation server. 30 - func NewServer(socketPath string, allowedUIDs, allowedGIDs []uint32) *Server { 31 - return &Server{ 32 - socketPath: socketPath, 33 - allowedUIDs: allowedUIDs, 34 - allowedGIDs: allowedGIDs, 35 - } 36 - } 37 - 38 - // Run starts the server and blocks until shutdown. 39 - func (s *Server) Run(ctx context.Context) error { 40 - listener, err := listenerFromSystemd() 41 - if err != nil && !errors.Is(err, errSystemdSocketUnavailable) { 42 - return err 43 - } 44 - 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 - } 61 - 62 - slog.Info("Server listening", "socket", s.socketPath, "mode", "standalone") 63 - } else { 64 - slog.Info("Server listening", "socket", s.socketPath, "mode", "systemd-socket-activation") 65 - } 66 - 67 - s.listener = listener 68 - defer s.listener.Close() 69 - 70 - // Handle shutdown 71 - go func() { 72 - <-ctx.Done() 73 - slog.Info("Shutting down server") 74 - s.listener.Close() 75 - }() 76 - 77 - // Accept connections 78 - for { 79 - conn, err := s.listener.Accept() 80 - if err != nil { 81 - if errors.Is(err, net.ErrClosed) { 82 - break 83 - } 84 - slog.Error("Accept error", "error", err) 85 - continue 86 - } 87 - 88 - s.wg.Go(func() { 89 - handler := NewConnectionHandler(conn, s.allowedUIDs, s.allowedGIDs) 90 - handler.Handle() 91 - }) 92 - } 93 - 94 - // Wait for active connections to finish 95 - s.wg.Wait() 96 - slog.Info("Server stopped") 97 - 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 151 - } 152 - 153 - // RunServer is the entry point for server mode. 154 - func RunServer(socketPath string, allowedUIDs, allowedGIDs []uint32) error { 155 - ctx, cancel := context.WithCancel(context.Background()) 156 - defer cancel() 157 - 158 - // Handle signals 159 - sigCh := make(chan os.Signal, 1) 160 - signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) 161 - go func() { 162 - sig := <-sigCh 163 - slog.Info("Received signal", "signal", sig) 164 - cancel() 165 - }() 166 - 167 - server := NewServer(socketPath, allowedUIDs, allowedGIDs) 168 - return server.Run(ctx) 169 - }
+6 -11
nix/nixos/activator.nix
··· 51 51 52 52 socketConfig = { 53 53 ListenStream = cfg.socketPath; 54 + Accept = true; 54 55 SocketMode = "0660"; 55 56 SocketUser = "root"; 56 57 SocketGroup = cfg.socketGroup; ··· 59 60 }; 60 61 }; 61 62 62 - systemd.services.sower-activator = { 63 + systemd.services."sower-activator@" = { 63 64 description = "Sower Activator Service"; 64 - requires = [ "sower-activator.socket" ]; 65 - after = [ 66 - "network.target" 67 - "sower-activator.socket" 68 - ]; 69 65 70 66 path = [ 71 67 config.nix.package 72 68 pkgs.getent 73 69 ]; 74 70 75 - # avoid restarting mid-switch 71 + # avoid stopping mid-switch 76 72 restartIfChanged = false; 77 73 78 74 serviceConfig = { 79 75 Type = "simple"; 80 - Restart = "on-failure"; 81 - RestartSec = "5s"; 76 + StandardInput = "socket"; 77 + StandardOutput = "socket"; 82 78 83 79 # Build allowed GIDs list at runtime (group GIDs may not be known at eval time) 84 80 ExecStart = ··· 98 94 ALLOWED_GIDS="$SOCKET_GID${lib.optionalString (additionalGroupsArg != "") ",$EXTRA_GIDS"}" 99 95 100 96 exec ${lib.getExe config.services.sower.activator.package} \ 101 - --server \ 102 - --socket ${cfg.socketPath} \ 97 + --socket-mode \ 103 98 --allowed-gids "$ALLOWED_GIDS" \ 104 99 ${debugFlag} 105 100 '';
-25
nix/tests/e2e.nix
··· 145 145 server.wait_for_open_port(4000) 146 146 147 147 with subtest("activator socket activation"): 148 - server.succeed("test \"$(systemctl show -p ActiveState --value sower-activator.service)\" = inactive") 149 148 server.succeed("test -S /run/sower-activator/activator.sock") 150 149 server.succeed("test \"$(stat -c '%a' /run/sower-activator/activator.sock)\" = 660") 151 150 server.succeed("test \"$(stat -c '%G' /run/sower-activator/activator.sock)\" = sower-activator") 152 - 153 - server.succeed( 154 - """ 155 - python3 - <<'PY' 156 - import json 157 - import socket 158 - 159 - sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 160 - sock.settimeout(5) 161 - sock.connect("/run/sower-activator/activator.sock") 162 - req = { 163 - "id": "socket-activation-probe", 164 - "type": "nixos", 165 - "path": "/nix/store/not-a-real-profile", 166 - "mode": "switch", 167 - } 168 - sock.sendall((json.dumps(req) + "\\n").encode()) 169 - sock.recv(4096) 170 - sock.close() 171 - PY 172 - """ 173 - ) 174 - 175 - server.wait_until_succeeds("test \"$(systemctl show -p ActiveState --value sower-activator.service)\" = active") 176 151 177 152 # with subtest("basic submission"): 178 153 # server_profile = server.succeed("readlink -f /run/booted-system").strip()