nixpkgs mirror (for testing) github.com/NixOS/nixpkgs
nix

nixos/temporal: init module (#436466)

authored by misuzu.tngl.sh and committed by

GitHub efd138d0 97dc7178

+474 -3
+4
nixos/doc/manual/release-notes/rl-2511.section.md
··· 98 98 99 99 - [KMinion](https://github.com/redpanda-data/kminion), feature-rich Prometheus exporter for Apache Kafka. Available as [services.prometheus.exporters.kafka](options.html#opt-services.prometheus.exporters.kafka). 100 100 101 + - [Temporal](https://temporal.io/), a durable execution platform that enables 102 + developers to build scalable applications without sacrificing productivity or 103 + reliability. Available as [services.temporal](#opt-services.temporal.enable). 104 + 101 105 ## Backward Incompatibilities {#sec-release-25.11-incompatibilities} 102 106 103 107 <!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->
+1
nixos/modules/module-list.nix
··· 484 484 ./services/cluster/patroni/default.nix 485 485 ./services/cluster/rke2/default.nix 486 486 ./services/cluster/spark/default.nix 487 + ./services/cluster/temporal/default.nix 487 488 ./services/computing/boinc/client.nix 488 489 ./services/computing/foldingathome/client.nix 489 490 ./services/computing/slurm/slurm.nix
+146
nixos/modules/services/cluster/temporal/default.nix
··· 1 + { 2 + config, 3 + lib, 4 + pkgs, 5 + ... 6 + }: 7 + let 8 + cfg = config.services.temporal; 9 + 10 + settingsFormat = pkgs.formats.yaml { }; 11 + 12 + usingDefaultDataDir = cfg.dataDir == "/var/lib/temporal"; 13 + usingDefaultUserAndGroup = cfg.user == "temporal" && cfg.group == "temporal"; 14 + in 15 + { 16 + meta.maintainers = [ lib.maintainers.jpds ]; 17 + 18 + options.services.temporal = { 19 + enable = lib.mkEnableOption "Temporal"; 20 + 21 + package = lib.mkPackageOption pkgs "Temporal" { 22 + default = [ "temporal" ]; 23 + }; 24 + 25 + settings = lib.mkOption { 26 + type = lib.types.submodule { 27 + freeformType = settingsFormat.type; 28 + }; 29 + 30 + description = '' 31 + Temporal configuration. 32 + 33 + See <https://docs.temporal.io/references/configuration> for more 34 + information about Temporal configuration options 35 + ''; 36 + }; 37 + 38 + dataDir = lib.mkOption { 39 + type = lib.types.path; 40 + default = "/var/lib/temporal"; 41 + apply = lib.converge (lib.removeSuffix "/"); 42 + description = '' 43 + Data directory for Temporal. If you change this, you need to 44 + manually create the directory. You also need to create the 45 + `temporal` user and group, or change 46 + [](#opt-services.temporal.user) and 47 + [](#opt-services.temporal.group) to existing ones with 48 + access to the directory. 49 + ''; 50 + }; 51 + 52 + user = lib.mkOption { 53 + type = lib.types.str; 54 + default = "temporal"; 55 + description = '' 56 + The user Temporal runs as. Should be left at default unless 57 + you have very specific needs. 58 + ''; 59 + }; 60 + 61 + group = lib.mkOption { 62 + type = lib.types.str; 63 + default = "temporal"; 64 + description = '' 65 + The group temporal runs as. Should be left at default unless 66 + you have very specific needs. 67 + ''; 68 + }; 69 + 70 + restartIfChanged = lib.mkOption { 71 + type = lib.types.bool; 72 + description = '' 73 + Automatically restart the service on config change. 74 + This can be set to false to defer restarts on a server or cluster. 75 + Please consider the security implications of inadvertently running an older version, 76 + and the possibility of unexpected behavior caused by inconsistent versions across a cluster when disabling this option. 77 + ''; 78 + default = true; 79 + }; 80 + }; 81 + 82 + config = lib.mkIf cfg.enable { 83 + environment.etc."temporal/temporal-server.yaml".source = 84 + settingsFormat.generate "temporal-server.yaml" cfg.settings; 85 + 86 + systemd.services.temporal = { 87 + description = "Temporal server"; 88 + wantedBy = [ "multi-user.target" ]; 89 + after = [ "network.target" ]; 90 + inherit (cfg) restartIfChanged; 91 + restartTriggers = [ config.environment.etc."temporal/temporal-server.yaml".source ]; 92 + environment = { 93 + HOME = cfg.dataDir; 94 + }; 95 + serviceConfig = { 96 + ExecStart = '' 97 + ${cfg.package}/bin/temporal-server --root / --config /etc/temporal/ -e temporal-server start 98 + ''; 99 + User = cfg.user; 100 + Group = cfg.group; 101 + Restart = "on-failure"; 102 + DynamicUser = usingDefaultUserAndGroup && usingDefaultDataDir; 103 + CapabilityBoundingSet = [ "" ]; 104 + DevicePolicy = "closed"; 105 + LockPersonality = true; 106 + MemoryDenyWriteExecute = true; 107 + NoNewPrivileges = true; 108 + PrivateDevices = true; 109 + ProcSubset = "pid"; 110 + ProtectClock = true; 111 + ProtectHome = true; 112 + ProtectHostname = true; 113 + ProtectControlGroups = true; 114 + ProtectKernelLogs = true; 115 + ProtectKernelModules = true; 116 + ProtectKernelTunables = true; 117 + ProtectProc = "invisible"; 118 + ProtectSystem = "strict"; 119 + ReadWritePaths = [ 120 + cfg.dataDir 121 + ]; 122 + RestrictAddressFamilies = [ 123 + "AF_NETLINK" 124 + "AF_INET" 125 + "AF_INET6" 126 + ]; 127 + RestrictNamespaces = true; 128 + RestrictRealtime = true; 129 + RestrictSUIDSGID = true; 130 + SystemCallArchitectures = "native"; 131 + SystemCallFilter = [ 132 + # 1. allow a reasonable set of syscalls 133 + "@system-service @resources" 134 + # 2. and deny unreasonable ones 135 + "~@privileged" 136 + # 3. then allow the required subset within denied groups 137 + "@chown" 138 + ]; 139 + } 140 + // (lib.optionalAttrs (usingDefaultDataDir) { 141 + StateDirectory = "temporal"; 142 + StateDirectoryMode = "0700"; 143 + }); 144 + }; 145 + }; 146 + }
+1
nixos/tests/all-tests.nix
··· 1488 1488 teleport = handleTest ./teleport.nix { }; 1489 1489 teleports = runTest ./teleports.nix; 1490 1490 thelounge = handleTest ./thelounge.nix { }; 1491 + temporal = runTest ./temporal.nix; 1491 1492 terminal-emulators = handleTest ./terminal-emulators.nix { }; 1492 1493 thanos = runTest ./thanos.nix; 1493 1494 tiddlywiki = runTest ./tiddlywiki.nix;
+311
nixos/tests/temporal.nix
··· 1 + ( 2 + { lib, pkgs, ... }: 3 + 4 + { 5 + name = "temporal"; 6 + meta.maintainers = [ pkgs.lib.maintainers.jpds ]; 7 + 8 + nodes = { 9 + temporal = 10 + { config, pkgs, ... }: 11 + { 12 + networking.firewall.allowedTCPPorts = [ 7233 ]; 13 + 14 + environment.systemPackages = [ 15 + (pkgs.writers.writePython3Bin "temporal-hello-workflow.py" 16 + { 17 + libraries = [ pkgs.python3Packages.temporalio ]; 18 + } 19 + # Graciously taken from https://github.com/temporalio/samples-python/blob/main/hello/hello_activity.py 20 + '' 21 + import asyncio 22 + from concurrent.futures import ThreadPoolExecutor 23 + from dataclasses import dataclass 24 + from datetime import timedelta 25 + 26 + from temporalio import activity, workflow 27 + from temporalio.client import Client 28 + from temporalio.worker import Worker 29 + 30 + 31 + # While we could use multiple parameters in the activity, Temporal strongly 32 + # encourages using a single dataclass instead which can have fields added to it 33 + # in a backwards-compatible way. 34 + @dataclass 35 + class ComposeGreetingInput: 36 + greeting: str 37 + name: str 38 + 39 + 40 + # Basic activity that logs and does string concatenation 41 + @activity.defn 42 + def compose_greeting(input: ComposeGreetingInput) -> str: 43 + activity.logger.info("Running activity with parameter %s" % input) 44 + return f"{input.greeting}, {input.name}!" 45 + 46 + 47 + # Basic workflow that logs and invokes an activity 48 + @workflow.defn 49 + class GreetingWorkflow: 50 + @workflow.run 51 + async def run(self, name: str) -> str: 52 + workflow.logger.info("Running workflow with parameter %s" % name) 53 + return await workflow.execute_activity( 54 + compose_greeting, 55 + ComposeGreetingInput("Hello", name), 56 + start_to_close_timeout=timedelta(seconds=10), 57 + ) 58 + 59 + 60 + async def main(): 61 + # Uncomment the lines below to see logging output 62 + # import logging 63 + # logging.basicConfig(level=logging.INFO) 64 + 65 + # Start client 66 + client = await Client.connect("localhost:7233") 67 + 68 + # Run a worker for the workflow 69 + async with Worker( 70 + client, 71 + task_queue="hello-activity-task-queue", 72 + workflows=[GreetingWorkflow], 73 + activities=[compose_greeting], 74 + # Non-async activities require an executor; 75 + # a thread pool executor is recommended. 76 + # This same thread pool could be passed to multiple workers if desired. 77 + activity_executor=ThreadPoolExecutor(5), 78 + ): 79 + 80 + # While the worker is running, use the client to run the workflow and 81 + # print out its result. Note, in many production setups, the client 82 + # would be in a completely separate process from the worker. 83 + result = await client.execute_workflow( 84 + GreetingWorkflow.run, 85 + "World", 86 + id="hello-activity-workflow-id", 87 + task_queue="hello-activity-task-queue", 88 + ) 89 + print(f"Result: {result}") 90 + 91 + 92 + if __name__ == "__main__": 93 + asyncio.run(main()) 94 + '' 95 + ) 96 + pkgs.temporal-cli 97 + ]; 98 + 99 + services.temporal = { 100 + enable = true; 101 + settings = { 102 + # Based on https://github.com/temporalio/temporal/blob/main/config/development-sqlite.yaml 103 + log = { 104 + stdout = true; 105 + level = "info"; 106 + }; 107 + services = { 108 + frontend = { 109 + rpc = { 110 + grpcPort = 7233; 111 + membershipPort = 6933; 112 + bindOnLocalHost = true; 113 + httpPort = 7243; 114 + }; 115 + }; 116 + matching = { 117 + rpc = { 118 + grpcPort = 7235; 119 + membershipPort = 6935; 120 + bindOnLocalHost = true; 121 + }; 122 + }; 123 + history = { 124 + rpc = { 125 + grpcPort = 7234; 126 + membershipPort = 6934; 127 + bindOnLocalHost = true; 128 + }; 129 + }; 130 + worker = { 131 + rpc = { 132 + grpcPort = 7239; 133 + membershipPort = 6939; 134 + bindOnLocalHost = true; 135 + }; 136 + }; 137 + }; 138 + 139 + persistence = { 140 + defaultStore = "sqlite-default"; 141 + visibilityStore = "sqlite-visibility"; 142 + numHistoryShards = 1; 143 + datastores = { 144 + sqlite-default = { 145 + sql = { 146 + user = ""; 147 + password = ""; 148 + pluginName = "sqlite"; 149 + databaseName = "default"; 150 + connectAddr = "localhost"; 151 + connectProtocol = "tcp"; 152 + connectAttributes = { 153 + mode = "memory"; 154 + cache = "private"; 155 + }; 156 + maxConns = 1; 157 + maxIdleConns = 1; 158 + maxConnLifetime = "1h"; 159 + tls = { 160 + enabled = false; 161 + caFile = ""; 162 + certFile = ""; 163 + keyFile = ""; 164 + enableHostVerification = false; 165 + serverName = ""; 166 + }; 167 + }; 168 + }; 169 + sqlite-visibility = { 170 + sql = { 171 + user = ""; 172 + password = ""; 173 + pluginName = "sqlite"; 174 + databaseName = "default"; 175 + connectAddr = "localhost"; 176 + connectProtocol = "tcp"; 177 + connectAttributes = { 178 + mode = "memory"; 179 + cache = "private"; 180 + }; 181 + maxConns = 1; 182 + maxIdleConns = 1; 183 + maxConnLifetime = "1h"; 184 + tls = { 185 + enabled = false; 186 + caFile = ""; 187 + certFile = ""; 188 + keyFile = ""; 189 + enableHostVerification = false; 190 + serverName = ""; 191 + }; 192 + }; 193 + }; 194 + }; 195 + }; 196 + clusterMetadata = { 197 + enableGlobalNamespace = false; 198 + failoverVersionIncrement = 10; 199 + masterClusterName = "active"; 200 + currentClusterName = "active"; 201 + clusterInformation = { 202 + active = { 203 + enabled = true; 204 + initialFailoverVersion = 1; 205 + rpcName = "frontend"; 206 + rpcAddress = "localhost:7233"; 207 + httpAddress = "localhost:7243"; 208 + }; 209 + }; 210 + }; 211 + 212 + dcRedirectionPolicy = { 213 + policy = "noop"; 214 + }; 215 + 216 + archival = { 217 + history = { 218 + state = "enabled"; 219 + enableRead = true; 220 + provider = { 221 + filestore = { 222 + fileMode = "0666"; 223 + dirMode = "0766"; 224 + }; 225 + gstorage = { 226 + credentialsPath = "/tmp/gcloud/keyfile.json"; 227 + }; 228 + }; 229 + }; 230 + visibility = { 231 + state = "enabled"; 232 + enableRead = true; 233 + provider = { 234 + filestore = { 235 + fileMode = "0666"; 236 + dirMode = "0766"; 237 + }; 238 + }; 239 + }; 240 + }; 241 + 242 + namespaceDefaults = { 243 + archival = { 244 + history = { 245 + state = "disabled"; 246 + URI = "file:///tmp/temporal_archival/development"; 247 + }; 248 + visibility = { 249 + state = "disabled"; 250 + URI = "file:///tmp/temporal_vis_archival/development"; 251 + }; 252 + }; 253 + }; 254 + }; 255 + }; 256 + }; 257 + }; 258 + 259 + testScript = '' 260 + temporal.wait_for_unit("temporal") 261 + temporal.wait_for_open_port(6933) 262 + temporal.wait_for_open_port(6934) 263 + temporal.wait_for_open_port(6935) 264 + temporal.wait_for_open_port(7233) 265 + temporal.wait_for_open_port(7234) 266 + temporal.wait_for_open_port(7235) 267 + 268 + temporal.wait_until_succeeds( 269 + "journalctl -o cat -u temporal.service | grep 'server-version' | grep '${pkgs.temporal.version}'" 270 + ) 271 + 272 + temporal.wait_until_succeeds( 273 + "journalctl -o cat -u temporal.service | grep 'Frontend is now healthy'" 274 + ) 275 + 276 + import json 277 + cluster_list_json = json.loads(temporal.wait_until_succeeds("temporal operator cluster list --output json")) 278 + assert cluster_list_json[0]['clusterName'] == "active" 279 + 280 + cluster_describe_json = json.loads(temporal.wait_until_succeeds("temporal operator cluster describe --output json")) 281 + assert cluster_describe_json['serverVersion'] in "${pkgs.temporal.version}" 282 + 283 + temporal.log(temporal.wait_until_succeeds("temporal operator namespace create --namespace default")) 284 + 285 + temporal.wait_until_succeeds( 286 + "journalctl -o cat -u temporal.service | grep 'Register namespace succeeded'" 287 + ) 288 + 289 + namespace_list_json = json.loads(temporal.wait_until_succeeds("temporal operator namespace list --output json")) 290 + assert len(namespace_list_json) == 2 291 + 292 + namespace_describe_json = json.loads(temporal.wait_until_succeeds("temporal operator namespace describe --output json --namespace default")) 293 + assert namespace_describe_json['namespaceInfo']['name'] == "default" 294 + assert namespace_describe_json['namespaceInfo']['state'] == "NAMESPACE_STATE_REGISTERED" 295 + 296 + workflow_json = json.loads(temporal.wait_until_succeeds("temporal workflow list --output json")) 297 + assert len(workflow_json) == 0 298 + 299 + out = temporal.wait_until_succeeds("temporal-hello-workflow.py") 300 + assert "Result: Hello, World!" in out 301 + 302 + workflow_json = json.loads(temporal.wait_until_succeeds("temporal workflow list --output json")) 303 + assert workflow_json[0]['execution']['workflowId'] == "hello-activity-workflow-id" 304 + assert workflow_json[0]['status'] == "WORKFLOW_EXECUTION_STATUS_COMPLETED" 305 + 306 + temporal.log(temporal.succeed( 307 + "systemd-analyze security temporal.service | grep -v '✓'" 308 + )) 309 + ''; 310 + } 311 + )
+6 -2
pkgs/by-name/te/temporal/package.nix
··· 2 2 lib, 3 3 fetchFromGitHub, 4 4 buildGoModule, 5 + nixosTests, 5 6 testers, 6 7 temporal, 7 8 }: ··· 47 46 runHook postInstall 48 47 ''; 49 48 50 - passthru.tests.version = testers.testVersion { 51 - package = temporal; 49 + passthru.tests = { 50 + inherit (nixosTests) temporal; 51 + version = testers.testVersion { 52 + package = temporal; 53 + }; 52 54 }; 53 55 54 56 meta = {
+5 -1
pkgs/development/python-modules/temporalio/default.nix
··· 7 7 maturin, 8 8 nexusrpc, 9 9 nix-update-script, 10 + nixosTests, 10 11 pythonOlder, 11 12 poetry-core, 12 13 protobuf5, ··· 80 79 "temporalio.worker" 81 80 ]; 82 81 83 - passthru.updateScript = nix-update-script { }; 82 + passthru = { 83 + tests = { inherit (nixosTests) temporal; }; 84 + updateScript = nix-update-script { }; 85 + }; 84 86 85 87 meta = { 86 88 description = "Temporal Python SDK";