activejob-temporal 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +20 -0
- data/bin/temporal-worker +3 -3
- data/lib/activejob/temporal/client.rb +34 -0
- data/lib/activejob/temporal/configuration.rb +32 -47
- data/lib/activejob/temporal/credential_refresher.rb +23 -12
- data/lib/activejob/temporal/locales/en.yml +4 -6
- data/lib/activejob/temporal/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ebc4386d6eac6522fba437fbdbd136334a3b5c333f6f83ad90fbfbaab5664144
|
|
4
|
+
data.tar.gz: 53cc07919979a66c013f6dd9597064cc73929ee8930a05c351774883760a1b59
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 652237c25d9342062bebd8269328f684c2cfc1d7a630cdbe0c5be9ae0a2f7d6bce0a96f48cd47de8dbad8da7a9fe5dbbf0974cfa7b92a3a646789c4e25c78873
|
|
7
|
+
data.tar.gz: 6503e062254a40daae4d00a97fa66a312e2d57d127c2cecd13218468ec73b9eecc168f8c3f2dcdc7734fb089f4f9b0ea7b53f4369f8e11ee0934ab4e0d05a143
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.5.0] - 2026-08-07
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Configuration now fails to validate when `api_key` or `api_key_file` is set while `tls` is left unset. The Temporal SDK enables TLS on its own whenever an API key is present, so leaving `tls` at `nil` silently coupled transport security to an unrelated setting and failed the handshake against a plaintext frontend.
|
|
14
|
+
- The client logs `credential_source_shadowed` when two sources for the same credential are configured (`api_key` alongside `api_key_file`, or `tls` alongside the `tls_*_path` settings). Precedence was already defined but silent.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- **Breaking:** `tls_cert_watch` and `api_key_watch` are replaced by a single `credential_watch`, default `true`. A file path is only configured because the credential behind it rotates, so watching every configured credential file is now the default instead of two opt-in flags that could each be forgotten.
|
|
18
|
+
- **Breaking:** `tls_reload_signal` is renamed `reload_signal` (`ACTIVEJOB_TEMPORAL_TLS_RELOAD_SIGNAL` becomes `ACTIVEJOB_TEMPORAL_RELOAD_SIGNAL`). It rebuilds the whole client and re-resolves the API key, not just TLS material.
|
|
19
|
+
- **Breaking:** `CredentialRefresher.from_config` no longer accepts `on_api_key_change`. Applying a rotated token mutates the live connection, so it is the same call in every process and had no caller that overrode it.
|
|
20
|
+
|
|
21
|
+
### Documentation
|
|
22
|
+
- `CredentialRefresher.from_config` and the worker setup guide now state that a process running its own `Temporalio::Worker` must pass `on_tls_change`. The default `reload_client!` closes the client it replaces, which is the one such a worker is still polling on. `bin/temporal-worker` already passes it.
|
|
23
|
+
|
|
24
|
+
### Migration from 0.4.x
|
|
25
|
+
- Rename `tls_cert_watch` / `api_key_watch` to `credential_watch`, or drop them: watching is on by default and applies to whatever credential files are configured.
|
|
26
|
+
- Rename `tls_reload_signal` to `reload_signal`, and `ACTIVEJOB_TEMPORAL_TLS_RELOAD_SIGNAL` to `ACTIVEJOB_TEMPORAL_RELOAD_SIGNAL`.
|
|
27
|
+
- Set `tls` explicitly wherever `api_key` or `api_key_file` is configured: `config.tls = false` for a plaintext in-cluster frontend, `config.tls = true` for Temporal Cloud. Boot now fails until you do.
|
|
28
|
+
- Drop `on_api_key_change:` from any `CredentialRefresher.from_config` call.
|
|
29
|
+
|
|
10
30
|
## [0.4.0] - 2026-08-07
|
|
11
31
|
|
|
12
32
|
### Added
|
data/bin/temporal-worker
CHANGED
|
@@ -107,11 +107,11 @@ if pool_size < 1
|
|
|
107
107
|
exit(1)
|
|
108
108
|
end
|
|
109
109
|
|
|
110
|
-
reload_signal = config.
|
|
110
|
+
reload_signal = config.reload_signal.to_s.sub(/\ASIG/i, "").upcase
|
|
111
111
|
unless Signal.list.key?(reload_signal) && !ActiveJob::Temporal::UNTRAPPABLE_SIGNALS.include?(reload_signal)
|
|
112
112
|
warn(
|
|
113
|
-
"Error:
|
|
114
|
-
"got: #{config.
|
|
113
|
+
"Error: ACTIVEJOB_TEMPORAL_RELOAD_SIGNAL must be a signal name safe to trap, " \
|
|
114
|
+
"got: #{config.reload_signal.inspect}"
|
|
115
115
|
)
|
|
116
116
|
exit(1)
|
|
117
117
|
end
|
|
@@ -8,6 +8,7 @@ rescue LoadError
|
|
|
8
8
|
end
|
|
9
9
|
|
|
10
10
|
require_relative "tls_file"
|
|
11
|
+
require_relative "logger"
|
|
11
12
|
|
|
12
13
|
module ActiveJob
|
|
13
14
|
module Temporal
|
|
@@ -121,6 +122,8 @@ module ActiveJob
|
|
|
121
122
|
def connection_kwargs(configuration)
|
|
122
123
|
kwargs = {}
|
|
123
124
|
|
|
125
|
+
warn_on_shadowed_credentials(configuration)
|
|
126
|
+
|
|
124
127
|
tls = tls_options(configuration)
|
|
125
128
|
kwargs[:tls] = tls unless tls.nil?
|
|
126
129
|
|
|
@@ -131,6 +134,37 @@ module ActiveJob
|
|
|
131
134
|
end
|
|
132
135
|
private_class_method :connection_kwargs
|
|
133
136
|
|
|
137
|
+
# Every credential has more than one possible source and the precedence between them is
|
|
138
|
+
# silent, so a config that sets two looks like it works while one of them is ignored.
|
|
139
|
+
# @api private
|
|
140
|
+
def warn_on_shadowed_credentials(configuration)
|
|
141
|
+
if credential_present?(configuration, :api_key) && credential_present?(configuration, :api_key_file)
|
|
142
|
+
Logger.warn("credential_source_shadowed", credential: "api_key", ignored: "api_key_file")
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
return unless configuration.respond_to?(:tls) && !configuration.tls.nil?
|
|
146
|
+
return if tls_path_attributes(configuration).empty?
|
|
147
|
+
|
|
148
|
+
Logger.warn(
|
|
149
|
+
"credential_source_shadowed",
|
|
150
|
+
credential: "tls",
|
|
151
|
+
ignored: tls_path_attributes(configuration).join(", ")
|
|
152
|
+
)
|
|
153
|
+
end
|
|
154
|
+
private_class_method :warn_on_shadowed_credentials
|
|
155
|
+
|
|
156
|
+
def credential_present?(configuration, attribute)
|
|
157
|
+
configuration.respond_to?(attribute) && configuration.public_send(attribute).to_s.strip != ""
|
|
158
|
+
end
|
|
159
|
+
private_class_method :credential_present?
|
|
160
|
+
|
|
161
|
+
def tls_path_attributes(configuration)
|
|
162
|
+
%i[tls_cert_path tls_key_path tls_server_root_ca_cert_path].select do |attribute|
|
|
163
|
+
credential_present?(configuration, attribute)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
private_class_method :tls_path_attributes
|
|
167
|
+
|
|
134
168
|
# Resolves the API key: an explicit api_key wins, otherwise api_key_file is read - at
|
|
135
169
|
# connection build time, and again by {ActiveJob::Temporal.refresh_api_key!} when the
|
|
136
170
|
# token file rotates.
|
|
@@ -122,18 +122,12 @@ module ActiveJob
|
|
|
122
122
|
description: "Optional TLS SNI domain override"
|
|
123
123
|
},
|
|
124
124
|
|
|
125
|
-
|
|
126
|
-
default: false,
|
|
127
|
-
env_var: "ACTIVEJOB_TEMPORAL_TLS_CERT_WATCH",
|
|
128
|
-
type: :boolean,
|
|
129
|
-
description: "Watch TLS certificate files and reload worker clients when they change"
|
|
130
|
-
},
|
|
131
|
-
|
|
132
|
-
tls_reload_signal: {
|
|
125
|
+
reload_signal: {
|
|
133
126
|
default: "HUP",
|
|
134
|
-
env_var: "
|
|
127
|
+
env_var: "ACTIVEJOB_TEMPORAL_RELOAD_SIGNAL",
|
|
135
128
|
type: :string,
|
|
136
|
-
description: "Signal used by workers to
|
|
129
|
+
description: "Signal used by workers to rebuild the Temporal client manually, picking up " \
|
|
130
|
+
"rotated TLS material and a rotated API key"
|
|
137
131
|
},
|
|
138
132
|
|
|
139
133
|
api_key: {
|
|
@@ -151,19 +145,20 @@ module ActiveJob
|
|
|
151
145
|
"projected Kubernetes ServiceAccount token. An explicit api_key takes precedence"
|
|
152
146
|
},
|
|
153
147
|
|
|
154
|
-
|
|
155
|
-
default:
|
|
156
|
-
env_var: "
|
|
148
|
+
credential_watch: {
|
|
149
|
+
default: true,
|
|
150
|
+
env_var: "ACTIVEJOB_TEMPORAL_CREDENTIAL_WATCH",
|
|
157
151
|
type: :boolean,
|
|
158
|
-
description: "
|
|
159
|
-
"
|
|
152
|
+
description: "Keep every configured credential file fresh: api_key_file and the TLS " \
|
|
153
|
+
"certificate paths. A file path exists because the credential rotates, so " \
|
|
154
|
+
"this is on by default and watches whatever paths are configured"
|
|
160
155
|
},
|
|
161
156
|
|
|
162
157
|
credential_poll_interval: {
|
|
163
158
|
default: 30,
|
|
164
159
|
env_var: "ACTIVEJOB_TEMPORAL_CREDENTIAL_POLL_INTERVAL",
|
|
165
160
|
type: :integer,
|
|
166
|
-
description: "Seconds between credential file checks when
|
|
161
|
+
description: "Seconds between credential file checks when credential_watch is enabled"
|
|
167
162
|
},
|
|
168
163
|
|
|
169
164
|
credential_file_events: {
|
|
@@ -1054,8 +1049,18 @@ module ActiveJob
|
|
|
1054
1049
|
validate_tls_file_path(:tls_key_path)
|
|
1055
1050
|
validate_tls_file_path(:tls_server_root_ca_cert_path)
|
|
1056
1051
|
validate_tls_domain
|
|
1057
|
-
|
|
1058
|
-
|
|
1052
|
+
validate_tls_decision
|
|
1053
|
+
validate_reload_signal
|
|
1054
|
+
end
|
|
1055
|
+
|
|
1056
|
+
# The Temporal SDK turns TLS on by itself whenever an api_key is present, so leaving `tls`
|
|
1057
|
+
# unset silently couples transport security to an unrelated setting - and fails at connect
|
|
1058
|
+
# against a plaintext in-cluster frontend. Make the caller state the intent instead.
|
|
1059
|
+
def validate_tls_decision
|
|
1060
|
+
return unless tls.nil?
|
|
1061
|
+
return if api_key.to_s.strip.empty? && api_key_file.to_s.strip.empty?
|
|
1062
|
+
|
|
1063
|
+
errors.add(:tls, :requires_explicit_choice)
|
|
1059
1064
|
end
|
|
1060
1065
|
|
|
1061
1066
|
def validate_tls_cert_key_pair
|
|
@@ -1084,31 +1089,15 @@ module ActiveJob
|
|
|
1084
1089
|
errors.add(:tls_domain, :blank)
|
|
1085
1090
|
end
|
|
1086
1091
|
|
|
1087
|
-
def validate_tls_cert_watch
|
|
1088
|
-
unless [true, false].include?(tls_cert_watch)
|
|
1089
|
-
errors.add(:tls_cert_watch, :not_boolean, value: tls_cert_watch.inspect)
|
|
1090
|
-
return
|
|
1091
|
-
end
|
|
1092
|
-
|
|
1093
|
-
return unless tls_cert_watch && tls_watch_paths.empty?
|
|
1094
|
-
|
|
1095
|
-
errors.add(:tls_cert_watch, :requires_paths)
|
|
1096
|
-
end
|
|
1097
|
-
|
|
1098
1092
|
def validate_api_key_settings
|
|
1099
1093
|
validate_tls_file_path(:api_key_file)
|
|
1100
|
-
|
|
1101
|
-
unless [true, false].include?(api_key_watch)
|
|
1102
|
-
errors.add(:api_key_watch, :not_boolean, value: api_key_watch.inspect)
|
|
1103
|
-
return
|
|
1104
|
-
end
|
|
1105
|
-
|
|
1106
|
-
return unless api_key_watch && api_key_file.to_s.strip.empty?
|
|
1107
|
-
|
|
1108
|
-
errors.add(:api_key_watch, :requires_path)
|
|
1109
1094
|
end
|
|
1110
1095
|
|
|
1111
1096
|
def validate_credential_refresh_settings
|
|
1097
|
+
unless [true, false].include?(credential_watch)
|
|
1098
|
+
errors.add(:credential_watch, :not_boolean, value: credential_watch.inspect)
|
|
1099
|
+
end
|
|
1100
|
+
|
|
1112
1101
|
unless [true, false].include?(credential_file_events)
|
|
1113
1102
|
errors.add(:credential_file_events, :not_boolean, value: credential_file_events.inspect)
|
|
1114
1103
|
end
|
|
@@ -1129,20 +1118,16 @@ module ActiveJob
|
|
|
1129
1118
|
errors.add(:worker_activejob_workloads, :requires_activities)
|
|
1130
1119
|
end
|
|
1131
1120
|
|
|
1132
|
-
def
|
|
1133
|
-
unless
|
|
1134
|
-
errors.add(:
|
|
1121
|
+
def validate_reload_signal
|
|
1122
|
+
unless reload_signal.is_a?(String) && reload_signal.strip.present?
|
|
1123
|
+
errors.add(:reload_signal, :blank)
|
|
1135
1124
|
return
|
|
1136
1125
|
end
|
|
1137
1126
|
|
|
1138
|
-
normalized_signal =
|
|
1127
|
+
normalized_signal = reload_signal.sub(/\ASIG/i, "").upcase
|
|
1139
1128
|
return if Signal.list.key?(normalized_signal) && !UNTRAPPABLE_SIGNALS.include?(normalized_signal)
|
|
1140
1129
|
|
|
1141
|
-
errors.add(:
|
|
1142
|
-
end
|
|
1143
|
-
|
|
1144
|
-
def tls_watch_paths
|
|
1145
|
-
[tls_cert_path, tls_key_path, tls_server_root_ca_cert_path].compact.reject { |path| path.to_s.strip.empty? }
|
|
1130
|
+
errors.add(:reload_signal, :invalid, value: reload_signal.inspect)
|
|
1146
1131
|
end
|
|
1147
1132
|
|
|
1148
1133
|
def callable_accepts_positional_job?(callable)
|
|
@@ -40,29 +40,40 @@ module ActiveJob
|
|
|
40
40
|
Source = Struct.new(:name, :paths, :on_change, keyword_init: true)
|
|
41
41
|
|
|
42
42
|
class << self
|
|
43
|
-
# Builds a refresher
|
|
44
|
-
#
|
|
45
|
-
#
|
|
43
|
+
# Builds a refresher for every credential file the configuration names. A path is only
|
|
44
|
+
# configured because the credential behind it rotates, so `credential_watch` gates the
|
|
45
|
+
# whole mechanism rather than each file individually.
|
|
46
|
+
#
|
|
47
|
+
# A token refresh mutates the live connection, so it is the same call in every process.
|
|
48
|
+
# A certificate refresh is not: the default rebuilds the memoized client, which is right
|
|
49
|
+
# for an enqueue-side process but wrong inside a worker.
|
|
50
|
+
#
|
|
51
|
+
# @note A process running a `Temporalio::Worker` MUST pass `on_tls_change`. The worker holds
|
|
52
|
+
# its own client reference, and the default `reload_client!` closes the client it replaces
|
|
53
|
+
# - which is the one the worker is still polling on. Pass a callback that assigns the fresh
|
|
54
|
+
# client to the worker, as {WorkerClientReloader} does.
|
|
46
55
|
#
|
|
47
56
|
# @param configuration [Configuration] the gem configuration
|
|
48
|
-
# @param on_tls_change [#call] invoked when any TLS file changes
|
|
49
|
-
# @param on_api_key_change [#call] invoked when the API key file changes
|
|
57
|
+
# @param on_tls_change [#call] invoked when any TLS file changes; see the note above
|
|
50
58
|
# @param logger [#log_event, #warn, #error] structured logger
|
|
51
59
|
# @param listener_factory [#to, nil] injection point for tests, defaults to `Listen`
|
|
52
|
-
# @return [CredentialRefresher] a refresher with no sources when
|
|
60
|
+
# @return [CredentialRefresher] a refresher with no sources when no credential file is
|
|
61
|
+
# configured, or when `credential_watch` is off
|
|
53
62
|
def from_config(configuration,
|
|
54
63
|
on_tls_change: -> { ActiveJob::Temporal.reload_client! },
|
|
55
|
-
on_api_key_change: -> { ActiveJob::Temporal.refresh_api_key! },
|
|
56
64
|
logger: ActiveJob::Temporal::Logger,
|
|
57
65
|
listener_factory: nil)
|
|
58
66
|
sources = []
|
|
59
67
|
|
|
60
|
-
if configuration.
|
|
61
|
-
|
|
62
|
-
|
|
68
|
+
if configuration.credential_watch
|
|
69
|
+
tls = tls_paths(configuration)
|
|
70
|
+
sources << Source.new(name: "tls", paths: tls, on_change: on_tls_change) if tls.any?
|
|
63
71
|
|
|
64
|
-
|
|
65
|
-
|
|
72
|
+
token = configuration.api_key_file.to_s.strip
|
|
73
|
+
if token.present?
|
|
74
|
+
sources << Source.new(name: "api_key", paths: [token],
|
|
75
|
+
on_change: -> { ActiveJob::Temporal.refresh_api_key! })
|
|
76
|
+
end
|
|
66
77
|
end
|
|
67
78
|
|
|
68
79
|
new(
|
|
@@ -129,11 +129,10 @@ en:
|
|
|
129
129
|
tls_domain:
|
|
130
130
|
blank: "must be present when configured"
|
|
131
131
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
requires_paths: "requires at least one TLS certificate path to watch"
|
|
132
|
+
tls:
|
|
133
|
+
requires_explicit_choice: "must be set explicitly to true, false, or TLS options when api_key or api_key_file is configured - the Temporal SDK enables TLS on its own whenever an API key is present, which fails the handshake against a plaintext frontend"
|
|
135
134
|
|
|
136
|
-
|
|
135
|
+
reload_signal:
|
|
137
136
|
blank: "must be present when configured"
|
|
138
137
|
invalid: "must be a signal name like HUP or USR1, got: %{value}"
|
|
139
138
|
|
|
@@ -141,9 +140,8 @@ en:
|
|
|
141
140
|
invalid_path: "must be a non-empty file path, got: %{value}"
|
|
142
141
|
unreadable_path: "must resolve to a readable regular file, got: %{value}"
|
|
143
142
|
|
|
144
|
-
|
|
143
|
+
credential_watch:
|
|
145
144
|
not_boolean: "must be true or false, got: %{value}"
|
|
146
|
-
requires_path: "requires api_key_file to watch"
|
|
147
145
|
|
|
148
146
|
credential_poll_interval:
|
|
149
147
|
invalid: "must be a positive number of seconds, got: %{value}"
|