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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0e1e7c38c2a169353b2d9f7edadca0614e32cb6c8c33ed87fe1d29b72a7dd5cb
4
- data.tar.gz: 3255fbe711eac898e2b91a65fa2bc5f858eccda59e605774fa027a6a2ba72291
3
+ metadata.gz: ebc4386d6eac6522fba437fbdbd136334a3b5c333f6f83ad90fbfbaab5664144
4
+ data.tar.gz: 53cc07919979a66c013f6dd9597064cc73929ee8930a05c351774883760a1b59
5
5
  SHA512:
6
- metadata.gz: b722896482be5075f145926ad914fedd43b860d438c0badf855f030aaa501d6839233315e0427b841e24d108e89fe775375f77e8e07f8ac983f43169eac383cd
7
- data.tar.gz: 6f40349c1961652d0f4c1e242a383895264cf585a2a6bcf127e3ed6dc66f9025413a664289d3ac05325ed9f10dab0d3f427c240af600555dfab8fb50d37f46fc
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.tls_reload_signal.to_s.sub(/\ASIG/i, "").upcase
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: ACTIVEJOB_TEMPORAL_TLS_RELOAD_SIGNAL must be a signal name safe to trap, " \
114
- "got: #{config.tls_reload_signal.inspect}"
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
- tls_cert_watch: {
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: "ACTIVEJOB_TEMPORAL_TLS_RELOAD_SIGNAL",
127
+ env_var: "ACTIVEJOB_TEMPORAL_RELOAD_SIGNAL",
135
128
  type: :string,
136
- description: "Signal used by workers to reload TLS certificates manually"
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
- api_key_watch: {
155
- default: false,
156
- env_var: "ACTIVEJOB_TEMPORAL_API_KEY_WATCH",
148
+ credential_watch: {
149
+ default: true,
150
+ env_var: "ACTIVEJOB_TEMPORAL_CREDENTIAL_WATCH",
157
151
  type: :boolean,
158
- description: "Watch api_key_file and refresh the token when it changes (kubelet rotates " \
159
- "projected tokens atomically, long before they expire)"
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 tls_cert_watch or api_key_watch is enabled"
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
- validate_tls_cert_watch
1058
- validate_tls_reload_signal
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 validate_tls_reload_signal
1133
- unless tls_reload_signal.is_a?(String) && tls_reload_signal.strip.present?
1134
- errors.add(:tls_reload_signal, :blank)
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 = tls_reload_signal.sub(/\ASIG/i, "").upcase
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(:tls_reload_signal, :invalid, value: tls_reload_signal.inspect)
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 from the `tls_cert_watch` / `api_key_watch` configuration flags.
44
- # The callbacks default to the process-wide reload paths, which is what an enqueue-side
45
- # process wants; workers override the TLS one so the running worker gets the new client.
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 both flags are off
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.tls_cert_watch
61
- sources << Source.new(name: "tls", paths: tls_paths(configuration), on_change: on_tls_change)
62
- end
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
- if configuration.api_key_watch
65
- sources << Source.new(name: "api_key", paths: [configuration.api_key_file], on_change: on_api_key_change)
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
- tls_cert_watch:
133
- not_boolean: "must be true or false, got: %{value}"
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
- tls_reload_signal:
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
- api_key_watch:
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}"
@@ -2,6 +2,6 @@
2
2
 
3
3
  module ActiveJob
4
4
  module Temporal
5
- VERSION = "0.4.0"
5
+ VERSION = "0.5.0"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activejob-temporal
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Temporal Technologies