kessel-sdk 1.10.0 → 1.12.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: 136206dbccfc87a36252fa0b635f195d00ebc861f9d26c3a35095403257e971c
4
- data.tar.gz: 6e1366c5ed2fc78316fd7804f4ff31ac59819817e919fda84e689c7c8023f980
3
+ metadata.gz: 997a5d14ffc861c340630e331dfd5c34f367ed7f68b9f760363a817180c10711
4
+ data.tar.gz: 6144712b5dec3a5355bc3f7e6a35953a0b4a2c2beeee56b017eebc659c03f3bc
5
5
  SHA512:
6
- metadata.gz: 2c015adbcbf6838b582f598b31d7860d31541c4adeb622c60877399b67ae797a516f603185640890f6c711311e0ff9f09ef0968153219b9dddcc070bb75acfeb
7
- data.tar.gz: 4967035e1789c864e4197e5e6ffcbf7bfe6e2d47dc24b2c1a4823b83e198171f043f0ceb0f9bee4cbbc5ae5f495882c2654b6d6e08989a4cc15fae5de3513a96
6
+ metadata.gz: 7397b29d146033e8d2060af9e25195debd851f052b4c3fad903277bd13b25ed8a1588f5a712bc8952e18fc75315cb79bd32c3a8732c07604f0cf51d2689ba843
7
+ data.tar.gz: 0c3b20634ed5d7529d66ba4b3e06c15e677eddfaf676618eaa4864205d7157a3b57d9cbab91b77de8e3adbbdbb707e255ef3055e986de6064a87b9c284b0e7dc
data/README.md CHANGED
@@ -58,7 +58,6 @@ lib/
58
58
  sig/ # RBS type signatures for hand-written code
59
59
  spec/ # RSpec test suite
60
60
  examples/ # Working examples with dotenv configuration
61
- docs/ # Domain-specific guidelines (see Documentation below)
62
61
  ```
63
62
 
64
63
  Files under `lib/kessel/inventory/v*/`, `lib/google/`, and `lib/buf/` are **generated** by `buf generate` and must never be hand-edited. They are automatically regenerated every 6 hours via CI.
@@ -98,7 +97,8 @@ discovery = fetch_oidc_discovery('https://sso.example.com/auth/realms/my-realm')
98
97
  oauth = OAuth2ClientCredentials.new(
99
98
  client_id: 'my-app',
100
99
  client_secret: 'my-secret',
101
- token_endpoint: discovery.token_endpoint
100
+ token_endpoint: discovery.token_endpoint,
101
+ retry: { max_retries: 3, base_delay: 0.5, max_delay: 2.0, jitter: :full }
102
102
  )
103
103
 
104
104
  # Build the client -- tokens are cached and refreshed automatically
@@ -107,6 +107,13 @@ client = KesselInventoryService::ClientBuilder.new('kessel.example.com:443')
107
107
  .build
108
108
  ```
109
109
 
110
+ Token fetch retries apply only to the token endpoint, not OIDC discovery. By default, network/timeout errors and HTTP
111
+ 429/5xx responses receive four total attempts (initial plus three retries), with full-jitter delay caps of 0.5, 1,
112
+ and 2 seconds and total added sleep below 3.5 seconds. `retry` accepts non-negative integer `max_retries` (0
113
+ disables retries), positive finite-second `base_delay` and `max_delay`, and `jitter: :full` or `:none`; defaults are
114
+ `3`, `0.5`, `2.0`, and `:full`. Invalid retry configuration raises `ArgumentError`. Permanent OAuth/configuration
115
+ failures are not retried. Refresh is synchronous, so a transient SSO failure may delay an RPC.
116
+
110
117
  #### Custom or No Credentials
111
118
 
112
119
  ```ruby
@@ -355,16 +362,11 @@ ruby check.rb
355
362
 
356
363
  ## Documentation
357
364
 
358
- Detailed domain-specific guidelines are maintained in the `docs/` directory:
359
-
360
- - **[API Contracts](docs/api-contracts-guidelines.md)** -- Protobuf code generation, module/namespace mapping, ClientBuilder API, request/response patterns, and RBS type signatures
361
- - **[Integration](docs/integration-guidelines.md)** -- gRPC client construction, authentication flows, RBAC helpers, streaming/pagination, and environment configuration
362
- - **[Security](docs/security-guidelines.md)** -- Token caching thread safety, gRPC channel security, credential validation, and secrets management
363
- - **[Performance](docs/performance-guidelines.md)** -- Token caching, gRPC client reuse, bulk vs. individual operations, consistency controls, and streaming pagination
364
- - **[Error Handling](docs/error-handling-guidelines.md)** -- Custom exception hierarchy, error wrapping conventions, and gRPC error passthrough policy
365
- - **[Testing](docs/testing-guidelines.md)** -- RSpec configuration, mocking conventions, coverage setup, and CI expectations
365
+ For AI-assisted development context, see [AGENTS.md](AGENTS.md). Directory-local `GUIDELINES.md` files provide detailed conventions for specific areas of the codebase:
366
366
 
367
- For AI-assisted development context, see [AGENTS.md](AGENTS.md).
367
+ - **[lib/kessel/inventory/GUIDELINES.md](lib/kessel/inventory/GUIDELINES.md)** -- Inventory module: ClientBuilder, service wiring, V1beta2 API patterns, auth integration
368
+ - **[lib/kessel/rbac/GUIDELINES.md](lib/kessel/rbac/GUIDELINES.md)** -- RBAC V2 module conventions
369
+ - **[examples/GUIDELINES.md](examples/GUIDELINES.md)** -- Example script conventions
368
370
 
369
371
  ## Release Instructions
370
372
 
@@ -496,7 +498,7 @@ rake release
496
498
  4. Push to the branch (`git push origin feature/amazing-feature`)
497
499
  5. Open a Pull Request
498
500
 
499
- Please review the [domain-specific guidelines](docs/) before contributing. All specs must pass on Ruby 3.3 and 3.4. Fix RuboCop violations before merging, and update RBS type signatures in `sig/kessel/` when modifying hand-written code.
501
+ Please review [AGENTS.md](AGENTS.md) and the directory-local `GUIDELINES.md` files before contributing. All specs must pass on Ruby 3.3 and 3.4. Fix RuboCop violations before merging, and update RBS type signatures in `sig/kessel/` when modifying hand-written code.
500
502
 
501
503
  ## License
502
504
 
@@ -7,7 +7,7 @@ require 'google/protobuf'
7
7
  require 'google/protobuf/any_pb'
8
8
 
9
9
 
10
- descriptor_data = "\n\x17google/rpc/status.proto\x12\ngoogle.rpc\x1a\x19google/protobuf/any.proto\"f\n\x06Status\x12\x12\n\x04\x63ode\x18\x01 \x01(\x05R\x04\x63ode\x12\x18\n\x07message\x18\x02 \x01(\tR\x07message\x12.\n\x07\x64\x65tails\x18\x03 \x03(\x0b\x32\x14.google.protobuf.AnyR\x07\x64\x65tailsBa\n\x0e\x63om.google.rpcB\x0bStatusProtoP\x01Z7google.golang.org/genproto/googleapis/rpc/status;status\xf8\x01\x01\xa2\x02\x03RPCb\x06proto3"
10
+ descriptor_data = "\n\x17google/rpc/status.proto\x12\ngoogle.rpc\x1a\x19google/protobuf/any.proto\"f\n\x06Status\x12\x12\n\x04\x63ode\x18\x01 \x01(\x05R\x04\x63ode\x12\x18\n\x07message\x18\x02 \x01(\tR\x07message\x12.\n\x07\x64\x65tails\x18\x03 \x03(\x0b\x32\x14.google.protobuf.AnyR\x07\x64\x65tailsB^\n\x0e\x63om.google.rpcB\x0bStatusProtoP\x01Z7google.golang.org/genproto/googleapis/rpc/status;status\xa2\x02\x03RPCb\x06proto3"
11
11
 
12
12
  pool = ::Google::Protobuf::DescriptorPool.generated_pool
13
13
  pool.add_serialized_file(descriptor_data)
data/lib/kessel/auth.rb CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  require 'grpc'
4
4
  require 'kessel/version'
5
+ require 'socket'
6
+ require 'timeout'
5
7
 
6
8
  module Kessel
7
9
  # OpenID Connect authentication module for Kessel services.
@@ -105,6 +107,7 @@ module Kessel
105
107
  #
106
108
  # # Get current access token (automatically cached and refreshed)
107
109
  # token = oauth.get_token
110
+ # rubocop:disable Metrics/ClassLength
108
111
  class OAuth2ClientCredentials
109
112
  include Kessel::Auth
110
113
 
@@ -113,9 +116,14 @@ module Kessel
113
116
  # @param client_id [String] OIDC client identifier
114
117
  # @param client_secret [String] OIDC client secret
115
118
  # @param token_endpoint [String] OIDC token endpoint URL
119
+ # @param retry [Hash] Optional token-endpoint retry settings. Keys are
120
+ # `max_retries` (non-negative Integer; 0 disables retries), `base_delay`
121
+ # and `max_delay` (positive finite seconds), and `jitter` (`:full` or
122
+ # `:none`). Defaults are 3, 0.5, 2.0, and `:full`, respectively.
116
123
  #
117
124
  # @raise [OAuthDependencyError] if the openid_connect gem is not available
118
125
  # @raise [OAuthAuthenticationError] if authentication fails
126
+ # @raise [ArgumentError] if retry settings are invalid
119
127
  #
120
128
  # @example
121
129
  # oauth = OAuth2ClientCredentials.new(
@@ -123,13 +131,20 @@ module Kessel
123
131
  # client_secret: 'secret',
124
132
  # token_endpoint: 'https://my-domain/auth/realms/my-realm/protocol/openid-connect/token'
125
133
  # )
126
- def initialize(client_id:, client_secret:, token_endpoint:)
134
+ def initialize(client_id:, client_secret:, token_endpoint:, **options)
135
+ validate_retry_options!(options)
136
+ retry_config = normalize_retry_config(options.fetch(:retry, {}))
127
137
  check_dependencies!
128
138
 
129
139
  @client_id = client_id
130
140
  @client_secret = client_secret
131
141
  @token_endpoint = token_endpoint
142
+ @retry_config = retry_config
132
143
  @token_mutex = Mutex.new
144
+ @generation_state_mutex = Mutex.new
145
+ @generation_users = Hash.new(0)
146
+ @generation_failures = {}
147
+ @cached_token_generation = nil
133
148
  @generation = 0
134
149
  end
135
150
 
@@ -147,23 +162,80 @@ module Kessel
147
162
  def get_token(force_refresh: false)
148
163
  return @cached_token if !force_refresh && token_valid?
149
164
 
150
- generation = @generation
165
+ generation = register_generation
151
166
 
152
- @token_mutex.synchronize do
153
- # Another thread already refreshed while we waited on the lock
154
- return @cached_token if @generation != generation && token_valid?
167
+ begin
168
+ @token_mutex.synchronize do
169
+ failure = generation_failure(generation)
155
170
 
156
- @cached_token = refresh
157
- @generation += 1
171
+ # A later successful generation may have recovered after this caller's failure.
172
+ return @cached_token if newer_cached_token?(generation)
173
+ raise failure if failure
174
+
175
+ begin
176
+ token = refresh
177
+ rescue StandardError => e
178
+ record_failure_and_advance(generation, e)
179
+ raise
180
+ end
181
+ cache_token_and_advance(token)
158
182
 
159
- return @cached_token
183
+ @cached_token
184
+ end
160
185
  rescue StandardError => e
161
- raise OAuthAuthenticationError, "Failed to obtain client credentials token: #{e.message}"
186
+ raise OAuthAuthenticationError, "Failed to obtain client credentials token: #{e.message}", cause: e
187
+ ensure
188
+ unregister_generation(generation)
162
189
  end
163
190
  end
164
191
 
165
192
  private
166
193
 
194
+ def register_generation
195
+ @generation_state_mutex.synchronize do
196
+ generation = @generation
197
+ @generation_users[generation] += 1
198
+ generation
199
+ end
200
+ end
201
+
202
+ def generation_failure(generation)
203
+ @generation_state_mutex.synchronize { @generation_failures[generation] }
204
+ end
205
+
206
+ def record_failure_and_advance(generation, error)
207
+ @generation_state_mutex.synchronize do
208
+ @generation_failures[generation] = error
209
+ @generation += 1
210
+ end
211
+ end
212
+
213
+ def advance_generation
214
+ @generation_state_mutex.synchronize { @generation += 1 }
215
+ end
216
+
217
+ def cache_token_and_advance(token)
218
+ @generation_state_mutex.synchronize do
219
+ @cached_token = token
220
+ @generation += 1
221
+ @cached_token_generation = @generation
222
+ end
223
+ end
224
+
225
+ def newer_cached_token?(generation)
226
+ !!(@cached_token_generation && @cached_token_generation > generation && token_valid?)
227
+ end
228
+
229
+ def unregister_generation(generation)
230
+ @generation_state_mutex.synchronize do
231
+ @generation_users[generation] -= 1
232
+ next unless @generation_users[generation].zero?
233
+
234
+ @generation_users.delete(generation)
235
+ @generation_failures.delete(generation)
236
+ end
237
+ end
238
+
167
239
  def refresh
168
240
  client = create_oidc_client
169
241
 
@@ -173,13 +245,117 @@ module Kessel
173
245
  client_secret: @client_secret
174
246
  }
175
247
 
176
- token_data = client.access_token!(request_params)
248
+ token_data = access_token_with_retries(client, request_params)
177
249
  RefreshTokenResponse.new(
178
250
  access_token: token_data.access_token,
179
251
  expires_at: Time.now + (token_data.expires_in || DEFAULT_EXPIRES_IN)
180
252
  ).freeze
181
253
  end
182
254
 
255
+ def access_token_with_retries(client, request_params)
256
+ retry_index = 0
257
+
258
+ begin
259
+ client.access_token!(request_params)
260
+ rescue StandardError => e
261
+ raise unless retryable_token_error?(e) && retry_index < @retry_config[:max_retries]
262
+
263
+ sleep(retry_delay(retry_index))
264
+ retry_index += 1
265
+ retry
266
+ end
267
+ end
268
+
269
+ def validate_retry_options!(options)
270
+ unknown_keys = options.keys - [:retry]
271
+ return if unknown_keys.empty?
272
+
273
+ raise ArgumentError, "unknown keyword: #{unknown_keys.first.inspect}"
274
+ end
275
+
276
+ def normalize_retry_config(retry_options)
277
+ unless retry_options.is_a?(Hash) && retry_options.keys.all?(Symbol)
278
+ raise ArgumentError, 'retry must be a symbol-keyed Hash'
279
+ end
280
+
281
+ unknown_keys = retry_options.keys - %i[max_retries base_delay max_delay jitter]
282
+ raise ArgumentError, "unknown retry option: #{unknown_keys.first.inspect}" unless unknown_keys.empty?
283
+
284
+ config = {
285
+ max_retries: 3,
286
+ base_delay: 0.5,
287
+ max_delay: 2.0,
288
+ jitter: :full
289
+ }.merge(retry_options)
290
+ validate_retry_config_values!(config)
291
+ config.freeze
292
+ end
293
+
294
+ def validate_retry_config_values!(config)
295
+ unless config[:max_retries].is_a?(Integer) && config[:max_retries] >= 0
296
+ raise ArgumentError, 'retry max_retries must be a non-negative Integer'
297
+ end
298
+
299
+ %i[base_delay max_delay].each do |key|
300
+ next if valid_delay?(config[key])
301
+
302
+ raise ArgumentError, "retry #{key} must be a positive finite Integer or Float"
303
+ end
304
+
305
+ return if %i[full none].include?(config[:jitter])
306
+
307
+ raise ArgumentError, 'retry jitter must be :full or :none'
308
+ end
309
+
310
+ def valid_delay?(value)
311
+ (value.is_a?(Integer) || value.is_a?(Float)) && value.positive? &&
312
+ (!value.is_a?(Float) || value.finite?)
313
+ end
314
+
315
+ def retry_delay(retry_index)
316
+ cap = [@retry_config[:max_delay], @retry_config[:base_delay] * (2**retry_index)].min
317
+ return cap if @retry_config[:jitter] == :none
318
+
319
+ rand(cap.to_f)
320
+ end
321
+
322
+ def retryable_token_error?(error)
323
+ retryable_rack_oauth_error?(error) || retryable_transient_error?(error)
324
+ end
325
+
326
+ def retryable_rack_oauth_error?(error)
327
+ return false unless defined?(::Rack::OAuth2::Client::Error)
328
+ return false unless error.is_a?(::Rack::OAuth2::Client::Error)
329
+
330
+ status = error.status
331
+ status == 429 || (status.is_a?(Integer) && status.between?(500, 599))
332
+ end
333
+
334
+ def retryable_transient_error?(error)
335
+ transient_error_classes.any? { |error_class| error.is_a?(error_class) }
336
+ end
337
+
338
+ def transient_error_classes
339
+ %w[
340
+ Faraday::ConnectionFailed
341
+ Faraday::TimeoutError
342
+ Timeout::Error
343
+ SocketError
344
+ EOFError
345
+ Errno::ECONNREFUSED
346
+ Errno::ECONNRESET
347
+ Errno::ETIMEDOUT
348
+ Errno::EHOSTUNREACH
349
+ Errno::ENETUNREACH
350
+ ].map { |name| optional_error_class(name) }.compact
351
+ end
352
+
353
+ def optional_error_class(name)
354
+ Object.const_get(name, false)
355
+ rescue NameError
356
+ nil
357
+ end
358
+
183
359
  # Checks if we have a valid cached token.
184
360
  #
185
361
  # @return [Boolean] true if token exists and not expired
@@ -206,5 +382,6 @@ module Kessel
206
382
  )
207
383
  end
208
384
  end
385
+ # rubocop:enable Metrics/ClassLength
209
386
  end
210
387
  end
@@ -0,0 +1,161 @@
1
+ # Inventory Module Guidelines
2
+
3
+ This directory contains the Kessel Inventory client module -- the `ClientBuilder` fluent API, version-specific service wiring, and generated protobuf/gRPC stubs. The `ClientBuilder` base class is defined in `../inventory.rb` (the `Kessel::Inventory` module), and each version file in this directory creates service-specific builders from it.
4
+
5
+ For repo-wide conventions (testing framework, RBS signatures, file headers, error patterns), see AGENTS.md at the repository root.
6
+
7
+ ## File Organization
8
+
9
+ ### Hand-written files (editable)
10
+
11
+ - `../inventory.rb` -- `Kessel::Inventory` module with `client_builder_for_stub` factory and `ClientBuilder` base class
12
+ - `v1.rb` -- Service wiring for `KesselInventoryHealthService::ClientBuilder` (health checks)
13
+ - `v1beta1.rb` -- Service wiring for V1beta1 typed resource/relationship services (deprecated)
14
+ - `v1beta2.rb` -- Service wiring for `KesselInventoryService::ClientBuilder` (current API)
15
+
16
+ ### Generated files (never edit)
17
+
18
+ Everything under `v1/`, `v1beta1/`, and `v1beta2/` subdirectories (`*_pb.rb`, `*_services_pb.rb`) is generated by `buf generate` and overwritten automatically every 6 hours via CI. Changes to these files must go upstream in the `.proto` definitions at `buf.build/project-kessel/inventory-api`.
19
+
20
+ ## ClientBuilder Pattern
21
+
22
+ ### Factory Function
23
+
24
+ `client_builder_for_stub(stub_class)` dynamically creates a new `Class` inheriting from `ClientBuilder` with `@stub_class` set. Each service module creates its builder as a constant:
25
+
26
+ ```ruby
27
+ ClientBuilder = ::Kessel::Inventory.client_builder_for_stub(Stub)
28
+ ```
29
+
30
+ Do not call `client_builder_for_stub` repeatedly at runtime -- the result is cached as a constant.
31
+
32
+ ### Fluent API
33
+
34
+ `ClientBuilder` uses method chaining -- all authentication methods return `self`:
35
+
36
+ ```ruby
37
+ # Insecure (local dev only)
38
+ client = KesselInventoryService::ClientBuilder.new(target).insecure.build
39
+
40
+ # OAuth2 authenticated
41
+ client = KesselInventoryService::ClientBuilder.new(target)
42
+ .oauth2_client_authenticated(oauth2_client_credentials: creds)
43
+ .build
44
+
45
+ # Custom credentials
46
+ client = KesselInventoryService::ClientBuilder.new(target)
47
+ .authenticated(call_credentials: call_creds, channel_credentials: chan_creds)
48
+ .build
49
+ ```
50
+
51
+ ### Credential Defaults and Validation
52
+
53
+ - When no channel credentials are set, `build` defaults to `GRPC::Core::ChannelCredentials.new` (TLS). This secure-by-default behavior must be preserved.
54
+ - `insecure` sets a sentinel (`:this_channel_is_insecure`) that prevents composing with call credentials.
55
+ - `validate_credentials` runs immediately on each auth method call, not deferred to `build`. This ensures invalid configurations fail at configuration time. Do not move validation to `build`.
56
+ - `build` calls `credentials.compose(@call_credentials)` when both channel and call credentials are present.
57
+
58
+ ### Build Output
59
+
60
+ `build` returns a single gRPC stub instance (not a tuple). The underlying gRPC channel manages its own HTTP/2 connection pool. Build once at application startup and reuse -- do not create a new stub per request.
61
+
62
+ ## Service Wiring Pattern
63
+
64
+ Every gRPC service module must follow this exact pattern:
65
+
66
+ ```ruby
67
+ # frozen_string_literal: true
68
+
69
+ require 'kessel/inventory'
70
+ require 'kessel/inventory/v1betaN/some_service_services_pb'
71
+
72
+ include Kessel::Inventory
73
+
74
+ module Kessel
75
+ module Inventory
76
+ module V1betaN
77
+ module KesselSomeService
78
+ ClientBuilder = ::Kessel::Inventory.client_builder_for_stub(Stub)
79
+ end
80
+ end
81
+ end
82
+ end
83
+ ```
84
+
85
+ The `include Kessel::Inventory` at the top level is required for `client_builder_for_stub` to resolve. The `Stub` constant comes from the generated `*_services_pb.rb` file.
86
+
87
+ When adding a new service, also add the `ClientBuilder` constant declaration in `sig/kessel/inventory.rbs`.
88
+
89
+ ## Version Conventions
90
+
91
+ | Directory | Status | Service |
92
+ |---|---|---|
93
+ | `v1/` | Active | `KesselInventoryHealthService` (health checks only) |
94
+ | `v1beta2/` | Active (current) | `KesselInventoryService` (unified inventory API) |
95
+ | `v1beta1/` | Deprecated | Typed K8s resources/relationships. Do not extend. |
96
+
97
+ All new features target `v1beta2`. The unified `KesselInventoryService` uses `ReportResource`/`DeleteResource` instead of resource-specific services.
98
+
99
+ Protobuf package `kessel.inventory.v1beta2` maps to Ruby module `Kessel::Inventory::V1beta2` -- note lowercase `beta` (not `V1Beta2`). This follows the protobuf-to-Ruby naming convention from `buf generate`.
100
+
101
+ ## V1beta2 API Patterns
102
+
103
+ ### Check Operations
104
+
105
+ Check operations use a triplet: `object` (ResourceReference), `relation` (string), `subject` (SubjectReference). Use `Check` for read-path authorization and `CheckForUpdate` for strongly consistent pre-mutation checks.
106
+
107
+ `CheckSelf` omits the subject (inferred from auth context).
108
+
109
+ ### Bulk Operations
110
+
111
+ `CheckBulk` and `CheckForUpdateBulk` accept 1--1000 items per RPC. Responses use a `pairs` field where each pair contains either `item` (success) or `error` (`Google::Rpc::Status`). Always check per-item errors -- the RPC can succeed while individual items fail.
112
+
113
+ ### Resource Reporting
114
+
115
+ `ReportResourceRequest` requires `type`, `reporter_type`, `reporter_instance_id`, `representations`, and `write_visibility`. Convert Ruby hashes to protobuf `Struct` via `Google::Protobuf::Struct.decode_json(hash.to_json)`.
116
+
117
+ ### Consistency Controls
118
+
119
+ The `Consistency` message supports `minimize_latency` (default, may be stale), `at_least_as_fresh` (token from prior write), and `at_least_as_acknowledged` (waits for all acknowledged writes). `WriteVisibility` on report requests defaults to `WRITE_VISIBILITY_UNSPECIFIED` -- set `IMMEDIATE` only when the caller will immediately `Check` the newly reported resource.
120
+
121
+ ### Streaming RPCs
122
+
123
+ `StreamedListObjects` and `StreamedListSubjects` use server-side streaming with `RequestPagination` (limit + continuation_token). Iterate with `.each` for constant memory.
124
+
125
+ ### Allowed Enum
126
+
127
+ V1beta2 uses a shared top-level `Allowed` enum (`ALLOWED_TRUE`, `ALLOWED_FALSE`). V1beta1 uses per-response nested enums (`CheckResponse::Allowed`).
128
+
129
+ ### HTTP API Routes
130
+
131
+ - V1: `/api/kessel/v1/{method}`
132
+ - V1beta1: `/api/inventory/v1beta1/{domain}/{method}`
133
+ - V1beta2: `/api/kessel/v1beta2/{method}`
134
+ - RBAC V2 REST: `/api/rbac/v2/workspaces/`
135
+
136
+ ## Authentication Integration
137
+
138
+ Authentication feeds into the `ClientBuilder` through two paths:
139
+
140
+ - **gRPC path**: `Kessel::GRPC#oauth2_call_credentials(auth)` wraps an `OAuth2ClientCredentials` instance as `GRPC::Core::CallCredentials`. The proc is invoked on every RPC call, hitting the token cache fast path when the token is valid. Do not add blocking operations inside this proc.
141
+ - **HTTP path**: `Kessel::Auth#oauth2_auth_request(oauth)` returns an `OAuth2AuthRequest` implementing the `AuthRequest` interface. The `configure_request(request)` method sets the `authorization` header on `Net::HTTPRequest` objects.
142
+
143
+ The `AuthRequest` module defines an interface contract -- any class including it must implement `configure_request` or callers get `NotImplementedError`. New auth mechanisms must implement this interface and have matching RBS signatures in `sig/kessel/auth.rbs`.
144
+
145
+ ### Token Caching
146
+
147
+ `OAuth2ClientCredentials` uses double-checked locking with a `@generation` counter to coalesce concurrent refresh requests into a single SSO call. Cached tokens are frozen with `.freeze` for concurrent read safety. Use `force_refresh: true` only after receiving an explicit 401/UNAUTHENTICATED error, never preemptively.
148
+
149
+ ## Console Helper
150
+
151
+ `lib/kessel/console.rb` provides `principal_from_rh_identity` and `principal_from_rh_identity_header` for converting Red Hat identity headers into `SubjectReference` objects. It depends on `Kessel::RBAC::V2` factory helpers (specifically `principal_subject`).
152
+
153
+ The `IDENTITY_TYPE_FIELDS` constant maps identity types to their JSON field names (`'User' => 'user'`, `'ServiceAccount' => 'service_account'`). When adding a new identity type, add it to this hash.
154
+
155
+ ## Adding a New Service Version
156
+
157
+ 1. Run `buf generate` to produce stubs in `lib/kessel/inventory/<version>/`.
158
+ 2. Create `lib/kessel/inventory/<version>.rb` following the service wiring pattern above.
159
+ 3. Add the `ClientBuilder` constant declaration to `sig/kessel/inventory.rbs`.
160
+ 4. Add a corresponding example in `examples/`.
161
+ 5. Run `bundle exec rspec` and `steep check`.
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
3
+ # source: kessel/inventory/v1beta2/acquire_lock_request.proto
4
+
5
+ require 'google/protobuf'
6
+
7
+ require 'buf/validate/validate_pb'
8
+
9
+
10
+ descriptor_data = "\n3kessel/inventory/v1beta2/acquire_lock_request.proto\x12\x18kessel.inventory.v1beta2\x1a\x1b\x62uf/validate/validate.proto\"6\n\x12\x41\x63quireLockRequest\x12 \n\x07lock_id\x18\x01 \x01(\tB\x07\xbaH\x04r\x02\x10\x01R\x06lockIdBr\n(org.project_kessel.api.inventory.v1beta2P\x01ZDgithub.com/project-kessel/inventory-api/api/kessel/inventory/v1beta2b\x06proto3"
11
+
12
+ pool = ::Google::Protobuf::DescriptorPool.generated_pool
13
+ pool.add_serialized_file(descriptor_data)
14
+
15
+ module Kessel
16
+ module Inventory
17
+ module V1beta2
18
+ AcquireLockRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("kessel.inventory.v1beta2.AcquireLockRequest").msgclass
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
3
+ # source: kessel/inventory/v1beta2/acquire_lock_response.proto
4
+
5
+ require 'google/protobuf'
6
+
7
+
8
+ descriptor_data = "\n4kessel/inventory/v1beta2/acquire_lock_response.proto\x12\x18kessel.inventory.v1beta2\"4\n\x13\x41\x63quireLockResponse\x12\x1d\n\nlock_token\x18\x01 \x01(\tR\tlockTokenBr\n(org.project_kessel.api.inventory.v1beta2P\x01ZDgithub.com/project-kessel/inventory-api/api/kessel/inventory/v1beta2b\x06proto3"
9
+
10
+ pool = ::Google::Protobuf::DescriptorPool.generated_pool
11
+ pool.add_serialized_file(descriptor_data)
12
+
13
+ module Kessel
14
+ module Inventory
15
+ module V1beta2
16
+ AcquireLockResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("kessel.inventory.v1beta2.AcquireLockResponse").msgclass
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
3
+ # source: kessel/inventory/v1beta2/create_tuples_request.proto
4
+
5
+ require 'google/protobuf'
6
+
7
+ require 'kessel/inventory/v1beta2/relationship_pb'
8
+ require 'kessel/inventory/v1beta2/relation_fencing_check_pb'
9
+
10
+
11
+ descriptor_data = "\n4kessel/inventory/v1beta2/create_tuples_request.proto\x12\x18kessel.inventory.v1beta2\x1a+kessel/inventory/v1beta2/relationship.proto\x1a\x35kessel/inventory/v1beta2/relation_fencing_check.proto\"\xd9\x01\n\x13\x43reateTuplesRequest\x12\x16\n\x06upsert\x18\x01 \x01(\x08R\x06upsert\x12>\n\x06tuples\x18\x02 \x03(\x0b\x32&.kessel.inventory.v1beta2.RelationshipR\x06tuples\x12X\n\rfencing_check\x18\x03 \x01(\x0b\x32..kessel.inventory.v1beta2.RelationFencingCheckH\x00R\x0c\x66\x65ncingCheck\x88\x01\x01\x42\x10\n\x0e_fencing_checkBr\n(org.project_kessel.api.inventory.v1beta2P\x01ZDgithub.com/project-kessel/inventory-api/api/kessel/inventory/v1beta2b\x06proto3"
12
+
13
+ pool = ::Google::Protobuf::DescriptorPool.generated_pool
14
+ pool.add_serialized_file(descriptor_data)
15
+
16
+ module Kessel
17
+ module Inventory
18
+ module V1beta2
19
+ CreateTuplesRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("kessel.inventory.v1beta2.CreateTuplesRequest").msgclass
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
3
+ # source: kessel/inventory/v1beta2/create_tuples_response.proto
4
+
5
+ require 'google/protobuf'
6
+
7
+ require 'kessel/inventory/v1beta2/consistency_token_pb'
8
+
9
+
10
+ descriptor_data = "\n5kessel/inventory/v1beta2/create_tuples_response.proto\x12\x18kessel.inventory.v1beta2\x1a\x30kessel/inventory/v1beta2/consistency_token.proto\"o\n\x14\x43reateTuplesResponse\x12W\n\x11\x63onsistency_token\x18\x01 \x01(\x0b\x32*.kessel.inventory.v1beta2.ConsistencyTokenR\x10\x63onsistencyTokenBr\n(org.project_kessel.api.inventory.v1beta2P\x01ZDgithub.com/project-kessel/inventory-api/api/kessel/inventory/v1beta2b\x06proto3"
11
+
12
+ pool = ::Google::Protobuf::DescriptorPool.generated_pool
13
+ pool.add_serialized_file(descriptor_data)
14
+
15
+ module Kessel
16
+ module Inventory
17
+ module V1beta2
18
+ CreateTuplesResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("kessel.inventory.v1beta2.CreateTuplesResponse").msgclass
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
3
+ # source: kessel/inventory/v1beta2/delete_tuples_request.proto
4
+
5
+ require 'google/protobuf'
6
+
7
+ require 'buf/validate/validate_pb'
8
+ require 'kessel/inventory/v1beta2/relation_tuple_filter_pb'
9
+ require 'kessel/inventory/v1beta2/relation_fencing_check_pb'
10
+
11
+
12
+ descriptor_data = "\n4kessel/inventory/v1beta2/delete_tuples_request.proto\x12\x18kessel.inventory.v1beta2\x1a\x1b\x62uf/validate/validate.proto\x1a\x34kessel/inventory/v1beta2/relation_tuple_filter.proto\x1a\x35kessel/inventory/v1beta2/relation_fencing_check.proto\"\xd0\x01\n\x13\x44\x65leteTuplesRequest\x12M\n\x06\x66ilter\x18\x01 \x01(\x0b\x32-.kessel.inventory.v1beta2.RelationTupleFilterB\x06\xbaH\x03\xc8\x01\x01R\x06\x66ilter\x12X\n\rfencing_check\x18\x02 \x01(\x0b\x32..kessel.inventory.v1beta2.RelationFencingCheckH\x00R\x0c\x66\x65ncingCheck\x88\x01\x01\x42\x10\n\x0e_fencing_checkBr\n(org.project_kessel.api.inventory.v1beta2P\x01ZDgithub.com/project-kessel/inventory-api/api/kessel/inventory/v1beta2b\x06proto3"
13
+
14
+ pool = ::Google::Protobuf::DescriptorPool.generated_pool
15
+ pool.add_serialized_file(descriptor_data)
16
+
17
+ module Kessel
18
+ module Inventory
19
+ module V1beta2
20
+ DeleteTuplesRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("kessel.inventory.v1beta2.DeleteTuplesRequest").msgclass
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
3
+ # source: kessel/inventory/v1beta2/delete_tuples_response.proto
4
+
5
+ require 'google/protobuf'
6
+
7
+ require 'kessel/inventory/v1beta2/consistency_token_pb'
8
+
9
+
10
+ descriptor_data = "\n5kessel/inventory/v1beta2/delete_tuples_response.proto\x12\x18kessel.inventory.v1beta2\x1a\x30kessel/inventory/v1beta2/consistency_token.proto\"o\n\x14\x44\x65leteTuplesResponse\x12W\n\x11\x63onsistency_token\x18\x01 \x01(\x0b\x32*.kessel.inventory.v1beta2.ConsistencyTokenR\x10\x63onsistencyTokenBr\n(org.project_kessel.api.inventory.v1beta2P\x01ZDgithub.com/project-kessel/inventory-api/api/kessel/inventory/v1beta2b\x06proto3"
11
+
12
+ pool = ::Google::Protobuf::DescriptorPool.generated_pool
13
+ pool.add_serialized_file(descriptor_data)
14
+
15
+ module Kessel
16
+ module Inventory
17
+ module V1beta2
18
+ DeleteTuplesResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("kessel.inventory.v1beta2.DeleteTuplesResponse").msgclass
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
3
+ # source: kessel/inventory/v1beta2/read_tuples_request.proto
4
+
5
+ require 'google/protobuf'
6
+
7
+ require 'buf/validate/validate_pb'
8
+ require 'kessel/inventory/v1beta2/relation_tuple_filter_pb'
9
+ require 'kessel/inventory/v1beta2/request_pagination_pb'
10
+ require 'kessel/inventory/v1beta2/consistency_pb'
11
+
12
+
13
+ descriptor_data = "\n2kessel/inventory/v1beta2/read_tuples_request.proto\x12\x18kessel.inventory.v1beta2\x1a\x1b\x62uf/validate/validate.proto\x1a\x34kessel/inventory/v1beta2/relation_tuple_filter.proto\x1a\x31kessel/inventory/v1beta2/request_pagination.proto\x1a*kessel/inventory/v1beta2/consistency.proto\"\xa1\x02\n\x11ReadTuplesRequest\x12M\n\x06\x66ilter\x18\x01 \x01(\x0b\x32-.kessel.inventory.v1beta2.RelationTupleFilterB\x06\xbaH\x03\xc8\x01\x01R\x06\x66ilter\x12P\n\npagination\x18\x02 \x01(\x0b\x32+.kessel.inventory.v1beta2.RequestPaginationH\x00R\npagination\x88\x01\x01\x12L\n\x0b\x63onsistency\x18\x03 \x01(\x0b\x32%.kessel.inventory.v1beta2.ConsistencyH\x01R\x0b\x63onsistency\x88\x01\x01\x42\r\n\x0b_paginationB\x0e\n\x0c_consistencyBr\n(org.project_kessel.api.inventory.v1beta2P\x01ZDgithub.com/project-kessel/inventory-api/api/kessel/inventory/v1beta2b\x06proto3"
14
+
15
+ pool = ::Google::Protobuf::DescriptorPool.generated_pool
16
+ pool.add_serialized_file(descriptor_data)
17
+
18
+ module Kessel
19
+ module Inventory
20
+ module V1beta2
21
+ ReadTuplesRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("kessel.inventory.v1beta2.ReadTuplesRequest").msgclass
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
3
+ # source: kessel/inventory/v1beta2/read_tuples_response.proto
4
+
5
+ require 'google/protobuf'
6
+
7
+ require 'kessel/inventory/v1beta2/relationship_pb'
8
+ require 'kessel/inventory/v1beta2/response_pagination_pb'
9
+ require 'kessel/inventory/v1beta2/consistency_token_pb'
10
+
11
+
12
+ descriptor_data = "\n3kessel/inventory/v1beta2/read_tuples_response.proto\x12\x18kessel.inventory.v1beta2\x1a+kessel/inventory/v1beta2/relationship.proto\x1a\x32kessel/inventory/v1beta2/response_pagination.proto\x1a\x30kessel/inventory/v1beta2/consistency_token.proto\"\xf9\x01\n\x12ReadTuplesResponse\x12<\n\x05tuple\x18\x01 \x01(\x0b\x32&.kessel.inventory.v1beta2.RelationshipR\x05tuple\x12L\n\npagination\x18\x02 \x01(\x0b\x32,.kessel.inventory.v1beta2.ResponsePaginationR\npagination\x12W\n\x11\x63onsistency_token\x18\x03 \x01(\x0b\x32*.kessel.inventory.v1beta2.ConsistencyTokenR\x10\x63onsistencyTokenBr\n(org.project_kessel.api.inventory.v1beta2P\x01ZDgithub.com/project-kessel/inventory-api/api/kessel/inventory/v1beta2b\x06proto3"
13
+
14
+ pool = ::Google::Protobuf::DescriptorPool.generated_pool
15
+ pool.add_serialized_file(descriptor_data)
16
+
17
+ module Kessel
18
+ module Inventory
19
+ module V1beta2
20
+ ReadTuplesResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("kessel.inventory.v1beta2.ReadTuplesResponse").msgclass
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
3
+ # source: kessel/inventory/v1beta2/relation_fencing_check.proto
4
+
5
+ require 'google/protobuf'
6
+
7
+ require 'buf/validate/validate_pb'
8
+
9
+
10
+ descriptor_data = "\n5kessel/inventory/v1beta2/relation_fencing_check.proto\x12\x18kessel.inventory.v1beta2\x1a\x1b\x62uf/validate/validate.proto\"`\n\x14RelationFencingCheck\x12 \n\x07lock_id\x18\x01 \x01(\tB\x07\xbaH\x04r\x02\x10\x01R\x06lockId\x12&\n\nlock_token\x18\x02 \x01(\tB\x07\xbaH\x04r\x02\x10\x01R\tlockTokenBr\n(org.project_kessel.api.inventory.v1beta2P\x01ZDgithub.com/project-kessel/inventory-api/api/kessel/inventory/v1beta2b\x06proto3"
11
+
12
+ pool = ::Google::Protobuf::DescriptorPool.generated_pool
13
+ pool.add_serialized_file(descriptor_data)
14
+
15
+ module Kessel
16
+ module Inventory
17
+ module V1beta2
18
+ RelationFencingCheck = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("kessel.inventory.v1beta2.RelationFencingCheck").msgclass
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
3
+ # source: kessel/inventory/v1beta2/relation_object_reference.proto
4
+
5
+ require 'google/protobuf'
6
+
7
+ require 'buf/validate/validate_pb'
8
+ require 'kessel/inventory/v1beta2/relation_object_type_pb'
9
+
10
+
11
+ descriptor_data = "\n8kessel/inventory/v1beta2/relation_object_reference.proto\x12\x18kessel.inventory.v1beta2\x1a\x1b\x62uf/validate/validate.proto\x1a\x33kessel/inventory/v1beta2/relation_object_type.proto\"|\n\x17RelationObjectReference\x12H\n\x04type\x18\x01 \x01(\x0b\x32,.kessel.inventory.v1beta2.RelationObjectTypeB\x06\xbaH\x03\xc8\x01\x01R\x04type\x12\x17\n\x02id\x18\x02 \x01(\tB\x07\xbaH\x04r\x02\x10\x01R\x02idBr\n(org.project_kessel.api.inventory.v1beta2P\x01ZDgithub.com/project-kessel/inventory-api/api/kessel/inventory/v1beta2b\x06proto3"
12
+
13
+ pool = ::Google::Protobuf::DescriptorPool.generated_pool
14
+ pool.add_serialized_file(descriptor_data)
15
+
16
+ module Kessel
17
+ module Inventory
18
+ module V1beta2
19
+ RelationObjectReference = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("kessel.inventory.v1beta2.RelationObjectReference").msgclass
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
3
+ # source: kessel/inventory/v1beta2/relation_object_type.proto
4
+
5
+ require 'google/protobuf'
6
+
7
+ require 'buf/validate/validate_pb'
8
+
9
+
10
+ descriptor_data = "\n3kessel/inventory/v1beta2/relation_object_type.proto\x12\x18kessel.inventory.v1beta2\x1a\x1b\x62uf/validate/validate.proto\"X\n\x12RelationObjectType\x12%\n\tnamespace\x18\x01 \x01(\tB\x07\xbaH\x04r\x02\x10\x01R\tnamespace\x12\x1b\n\x04name\x18\x02 \x01(\tB\x07\xbaH\x04r\x02\x10\x01R\x04nameBr\n(org.project_kessel.api.inventory.v1beta2P\x01ZDgithub.com/project-kessel/inventory-api/api/kessel/inventory/v1beta2b\x06proto3"
11
+
12
+ pool = ::Google::Protobuf::DescriptorPool.generated_pool
13
+ pool.add_serialized_file(descriptor_data)
14
+
15
+ module Kessel
16
+ module Inventory
17
+ module V1beta2
18
+ RelationObjectType = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("kessel.inventory.v1beta2.RelationObjectType").msgclass
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
3
+ # source: kessel/inventory/v1beta2/relation_subject_filter.proto
4
+
5
+ require 'google/protobuf'
6
+
7
+
8
+ descriptor_data = "\n6kessel/inventory/v1beta2/relation_subject_filter.proto\x12\x18kessel.inventory.v1beta2\"\xf9\x01\n\x15RelationSubjectFilter\x12\x30\n\x11subject_namespace\x18\x01 \x01(\tH\x00R\x10subjectNamespace\x88\x01\x01\x12&\n\x0csubject_type\x18\x02 \x01(\tH\x01R\x0bsubjectType\x88\x01\x01\x12\"\n\nsubject_id\x18\x03 \x01(\tH\x02R\tsubjectId\x88\x01\x01\x12\x1f\n\x08relation\x18\x04 \x01(\tH\x03R\x08relation\x88\x01\x01\x42\x14\n\x12_subject_namespaceB\x0f\n\r_subject_typeB\r\n\x0b_subject_idB\x0b\n\t_relationBr\n(org.project_kessel.api.inventory.v1beta2P\x01ZDgithub.com/project-kessel/inventory-api/api/kessel/inventory/v1beta2b\x06proto3"
9
+
10
+ pool = ::Google::Protobuf::DescriptorPool.generated_pool
11
+ pool.add_serialized_file(descriptor_data)
12
+
13
+ module Kessel
14
+ module Inventory
15
+ module V1beta2
16
+ RelationSubjectFilter = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("kessel.inventory.v1beta2.RelationSubjectFilter").msgclass
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
3
+ # source: kessel/inventory/v1beta2/relation_subject_reference.proto
4
+
5
+ require 'google/protobuf'
6
+
7
+ require 'buf/validate/validate_pb'
8
+ require 'kessel/inventory/v1beta2/relation_object_reference_pb'
9
+
10
+
11
+ descriptor_data = "\n9kessel/inventory/v1beta2/relation_subject_reference.proto\x12\x18kessel.inventory.v1beta2\x1a\x1b\x62uf/validate/validate.proto\x1a\x38kessel/inventory/v1beta2/relation_object_reference.proto\"\x9d\x01\n\x18RelationSubjectReference\x12\x1f\n\x08relation\x18\x01 \x01(\tH\x00R\x08relation\x88\x01\x01\x12S\n\x07subject\x18\x02 \x01(\x0b\x32\x31.kessel.inventory.v1beta2.RelationObjectReferenceB\x06\xbaH\x03\xc8\x01\x01R\x07subjectB\x0b\n\t_relationBr\n(org.project_kessel.api.inventory.v1beta2P\x01ZDgithub.com/project-kessel/inventory-api/api/kessel/inventory/v1beta2b\x06proto3"
12
+
13
+ pool = ::Google::Protobuf::DescriptorPool.generated_pool
14
+ pool.add_serialized_file(descriptor_data)
15
+
16
+ module Kessel
17
+ module Inventory
18
+ module V1beta2
19
+ RelationSubjectReference = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("kessel.inventory.v1beta2.RelationSubjectReference").msgclass
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
3
+ # source: kessel/inventory/v1beta2/relation_tuple_filter.proto
4
+
5
+ require 'google/protobuf'
6
+
7
+ require 'kessel/inventory/v1beta2/relation_subject_filter_pb'
8
+
9
+
10
+ descriptor_data = "\n4kessel/inventory/v1beta2/relation_tuple_filter.proto\x12\x18kessel.inventory.v1beta2\x1a\x36kessel/inventory/v1beta2/relation_subject_filter.proto\"\xf0\x02\n\x13RelationTupleFilter\x12\x32\n\x12resource_namespace\x18\x01 \x01(\tH\x00R\x11resourceNamespace\x88\x01\x01\x12(\n\rresource_type\x18\x02 \x01(\tH\x01R\x0cresourceType\x88\x01\x01\x12$\n\x0bresource_id\x18\x03 \x01(\tH\x02R\nresourceId\x88\x01\x01\x12\x1f\n\x08relation\x18\x04 \x01(\tH\x03R\x08relation\x88\x01\x01\x12[\n\x0esubject_filter\x18\x05 \x01(\x0b\x32/.kessel.inventory.v1beta2.RelationSubjectFilterH\x04R\rsubjectFilter\x88\x01\x01\x42\x15\n\x13_resource_namespaceB\x10\n\x0e_resource_typeB\x0e\n\x0c_resource_idB\x0b\n\t_relationB\x11\n\x0f_subject_filterBr\n(org.project_kessel.api.inventory.v1beta2P\x01ZDgithub.com/project-kessel/inventory-api/api/kessel/inventory/v1beta2b\x06proto3"
11
+
12
+ pool = ::Google::Protobuf::DescriptorPool.generated_pool
13
+ pool.add_serialized_file(descriptor_data)
14
+
15
+ module Kessel
16
+ module Inventory
17
+ module V1beta2
18
+ RelationTupleFilter = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("kessel.inventory.v1beta2.RelationTupleFilter").msgclass
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
3
+ # source: kessel/inventory/v1beta2/relationship.proto
4
+
5
+ require 'google/protobuf'
6
+
7
+ require 'buf/validate/validate_pb'
8
+ require 'kessel/inventory/v1beta2/relation_object_reference_pb'
9
+ require 'kessel/inventory/v1beta2/relation_subject_reference_pb'
10
+
11
+
12
+ descriptor_data = "\n+kessel/inventory/v1beta2/relationship.proto\x12\x18kessel.inventory.v1beta2\x1a\x1b\x62uf/validate/validate.proto\x1a\x38kessel/inventory/v1beta2/relation_object_reference.proto\x1a\x39kessel/inventory/v1beta2/relation_subject_reference.proto\"\xe0\x01\n\x0cRelationship\x12U\n\x08resource\x18\x01 \x01(\x0b\x32\x31.kessel.inventory.v1beta2.RelationObjectReferenceB\x06\xbaH\x03\xc8\x01\x01R\x08resource\x12#\n\x08relation\x18\x02 \x01(\tB\x07\xbaH\x04r\x02\x10\x01R\x08relation\x12T\n\x07subject\x18\x03 \x01(\x0b\x32\x32.kessel.inventory.v1beta2.RelationSubjectReferenceB\x06\xbaH\x03\xc8\x01\x01R\x07subjectBr\n(org.project_kessel.api.inventory.v1beta2P\x01ZDgithub.com/project-kessel/inventory-api/api/kessel/inventory/v1beta2b\x06proto3"
13
+
14
+ pool = ::Google::Protobuf::DescriptorPool.generated_pool
15
+ pool.add_serialized_file(descriptor_data)
16
+
17
+ module Kessel
18
+ module Inventory
19
+ module V1beta2
20
+ Relationship = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("kessel.inventory.v1beta2.Relationship").msgclass
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
3
+ # source: kessel/inventory/v1beta2/tuple_service.proto
4
+
5
+ require 'google/protobuf'
6
+
7
+ require 'kessel/inventory/v1beta2/create_tuples_request_pb'
8
+ require 'kessel/inventory/v1beta2/create_tuples_response_pb'
9
+ require 'kessel/inventory/v1beta2/read_tuples_request_pb'
10
+ require 'kessel/inventory/v1beta2/read_tuples_response_pb'
11
+ require 'kessel/inventory/v1beta2/delete_tuples_request_pb'
12
+ require 'kessel/inventory/v1beta2/delete_tuples_response_pb'
13
+ require 'kessel/inventory/v1beta2/acquire_lock_request_pb'
14
+ require 'kessel/inventory/v1beta2/acquire_lock_response_pb'
15
+
16
+
17
+ descriptor_data = "\n,kessel/inventory/v1beta2/tuple_service.proto\x12\x18kessel.inventory.v1beta2\x1a\x34kessel/inventory/v1beta2/create_tuples_request.proto\x1a\x35kessel/inventory/v1beta2/create_tuples_response.proto\x1a\x32kessel/inventory/v1beta2/read_tuples_request.proto\x1a\x33kessel/inventory/v1beta2/read_tuples_response.proto\x1a\x34kessel/inventory/v1beta2/delete_tuples_request.proto\x1a\x35kessel/inventory/v1beta2/delete_tuples_response.proto\x1a\x33kessel/inventory/v1beta2/acquire_lock_request.proto\x1a\x34kessel/inventory/v1beta2/acquire_lock_response.proto2\xdd\x03\n\x12KesselTupleService\x12r\n\x0c\x43reateTuples\x12-.kessel.inventory.v1beta2.CreateTuplesRequest\x1a..kessel.inventory.v1beta2.CreateTuplesResponse\"\x03\x88\x02\x01\x12r\n\x0c\x44\x65leteTuples\x12-.kessel.inventory.v1beta2.DeleteTuplesRequest\x1a..kessel.inventory.v1beta2.DeleteTuplesResponse\"\x03\x88\x02\x01\x12n\n\nReadTuples\x12+.kessel.inventory.v1beta2.ReadTuplesRequest\x1a,.kessel.inventory.v1beta2.ReadTuplesResponse\"\x03\x88\x02\x01\x30\x01\x12o\n\x0b\x41\x63quireLock\x12,.kessel.inventory.v1beta2.AcquireLockRequest\x1a-.kessel.inventory.v1beta2.AcquireLockResponse\"\x03\x88\x02\x01\x42r\n(org.project_kessel.api.inventory.v1beta2P\x01ZDgithub.com/project-kessel/inventory-api/api/kessel/inventory/v1beta2b\x06proto3"
18
+
19
+ pool = ::Google::Protobuf::DescriptorPool.generated_pool
20
+ pool.add_serialized_file(descriptor_data)
21
+
22
+ module Kessel
23
+ module Inventory
24
+ module V1beta2
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,44 @@
1
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
2
+ # Source: kessel/inventory/v1beta2/tuple_service.proto for package 'kessel.inventory.v1beta2'
3
+
4
+ require 'grpc'
5
+ require 'kessel/inventory/v1beta2/tuple_service_pb'
6
+
7
+ module Kessel
8
+ module Inventory
9
+ module V1beta2
10
+ module KesselTupleService
11
+ # DEPRECATED: KesselTupleService provides legacy tuple-layer operations.
12
+ # This service exists only for RBAC backward compatibility and will be removed.
13
+ # All endpoints are marked deprecated. Do not add new consumers.
14
+ # Use KesselInventoryService's ReportResource for new integrations.
15
+ class Service
16
+
17
+ include ::GRPC::GenericService
18
+
19
+ self.marshal_class_method = :encode
20
+ self.unmarshal_class_method = :decode
21
+ self.service_name = 'kessel.inventory.v1beta2.KesselTupleService'
22
+
23
+ # DEPRECATED: CreateTuples creates relationship tuples.
24
+ # This endpoint exists only for RBAC backward compatibility.
25
+ # Use ReportResource instead for new code.
26
+ rpc :CreateTuples, ::Kessel::Inventory::V1beta2::CreateTuplesRequest, ::Kessel::Inventory::V1beta2::CreateTuplesResponse
27
+ # DEPRECATED: DeleteTuples deletes relationship tuples.
28
+ # This endpoint exists only for RBAC backward compatibility.
29
+ # Use DeleteResource instead for new code.
30
+ rpc :DeleteTuples, ::Kessel::Inventory::V1beta2::DeleteTuplesRequest, ::Kessel::Inventory::V1beta2::DeleteTuplesResponse
31
+ # DEPRECATED: ReadTuples reads relationship tuples.
32
+ # This endpoint exists only for RBAC backward compatibility.
33
+ # Use StreamedListObjects/StreamedListSubjects instead for new code.
34
+ rpc :ReadTuples, ::Kessel::Inventory::V1beta2::ReadTuplesRequest, stream(::Kessel::Inventory::V1beta2::ReadTuplesResponse)
35
+ # DEPRECATED: AcquireLock acquires a distributed lock.
36
+ # This endpoint exists only for RBAC backward compatibility.
37
+ rpc :AcquireLock, ::Kessel::Inventory::V1beta2::AcquireLockRequest, ::Kessel::Inventory::V1beta2::AcquireLockResponse
38
+ end
39
+
40
+ Stub = Service.rpc_stub_class
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,104 @@
1
+ # RBAC V2 Module Guidelines
2
+
3
+ ## Directory Purpose
4
+
5
+ This directory contains the hand-written RBAC convenience layer (`Kessel::RBAC::V2`) built on top of V1beta2 protobuf types. It provides workspace operations over both HTTP and gRPC, plus factory helpers for constructing protobuf resource/subject references.
6
+
7
+ ## File Organization
8
+
9
+ The module is split across three files reopening the same `Kessel::RBAC::V2` module:
10
+
11
+ - `v2.rb` -- Public API: `fetch_default_workspace`, `fetch_root_workspace`, `list_workspaces`, plus private response processing. Defines the `Workspace` struct and constants.
12
+ - `v2_helpers.rb` -- Factory methods for protobuf references: `workspace_type`, `role_type`, `principal_resource`, `role_resource`, `workspace_resource`, `principal_subject`, `subject`.
13
+ - `v2_http.rb` -- Private HTTP helpers: `run_request` (sets headers, calls auth), `check_http_client` (validates host/port match).
14
+
15
+ All three files must be required together -- `lib/kessel-sdk.rb` handles this. Do not require individual files in isolation.
16
+
17
+ ## Module Design
18
+
19
+ - `V2` is a Ruby module meant to be `include`d, not instantiated. Tests use `include Kessel::RBAC::V2` directly in the RSpec context.
20
+ - `V2` includes `Kessel::Inventory::V1beta2` to access protobuf types (`SubjectReference`, `ResourceReference`, `RepresentationType`, etc.) without full qualification.
21
+ - The `include V1beta2` appears in both `v2.rb` and `v2_helpers.rb` -- this is intentional for load-order independence.
22
+
23
+ ## Constants and Structs
24
+
25
+ - `WORKSPACE_ENDPOINT = '/api/rbac/v2/workspaces/'` -- the REST path appended to `rbac_base_endpoint`.
26
+ - `DEFAULT_PAGE_LIMIT = 1000` -- page size for `list_workspaces` auto-pagination.
27
+ - `Workspace = Struct.new(:id, :name, :type, :description)` -- keyword-argument struct returned by HTTP workspace fetches.
28
+
29
+ ## Factory Helper Conventions
30
+
31
+ All factory methods in `v2_helpers.rb` follow these rules:
32
+
33
+ - Every RBAC resource uses `reporter_type: 'rbac'` (hardcoded, not configurable).
34
+ - `principal_resource(id, domain)` formats `resource_id` as `"#{domain}/#{id}"` -- the domain comes first.
35
+ - `principal_subject(id, domain)` wraps `principal_resource` in a `SubjectReference`.
36
+ - `subject(resource_ref, relation = nil)` is a generic factory -- pass `nil` for relation to omit it.
37
+ - `workspace_type` and `role_type` return `RepresentationType` structs, not `ResourceReference`.
38
+ - Factory methods return frozen protobuf objects (protobuf default). Do not `.dup` or mutate them.
39
+
40
+ ## HTTP Workspace Operations
41
+
42
+ ### Request Pattern
43
+
44
+ - `fetch_workspace` (private) strips trailing slashes from `rbac_base_endpoint`, appends `WORKSPACE_ENDPOINT`, sets a `type` query parameter, and issues a `GET` request.
45
+ - The `x-rh-rbac-org-id` header is required on every request -- set from the `org_id` parameter in `run_request`.
46
+ - Authentication is optional: when `auth` is non-nil, `auth.configure_request(request)` is called. The `auth` object must implement the `AuthRequest` interface (i.e., `Kessel::Auth#oauth2_auth_request(oauth)` return value).
47
+
48
+ ### HTTP Client Handling
49
+
50
+ - When `http_client` is `nil`, a new `Net::HTTP` is created per call (one TCP connection per fetch).
51
+ - When `http_client` is provided, `check_http_client` validates that its `address` and `port` match the endpoint URI. Mismatches raise `RuntimeError`.
52
+ - For multiple workspace fetches, pass a pre-started `Net::HTTP` to avoid repeated TCP handshakes.
53
+
54
+ ### Response Processing
55
+
56
+ - `process_response` raises `RuntimeError` for non-success HTTP status codes.
57
+ - It expects exactly one workspace in `data[]` -- any other count raises `RuntimeError`.
58
+ - `extract_workspace` maps JSON keys to the `Workspace` struct.
59
+
60
+ ## gRPC Workspace Listing
61
+
62
+ `list_workspaces(inventory, subject, relation, continuation_token = nil, consistency: nil)` returns a lazy `Enumerator`:
63
+
64
+ - Internally calls `inventory.streamed_list_objects` with a `StreamedListObjectsRequest`.
65
+ - Auto-paginates: extracts `continuation_token` from each response's `pagination` field.
66
+ - Stops when the server returns no responses or `continuation_token` is nil/falsy.
67
+ - The `consistency` parameter is forwarded to every paginated request -- it is not modified between pages.
68
+ - Consume with `.each` for constant memory, or `.to_a` to materialize all results (use with caution on large datasets).
69
+
70
+ ## Error Handling
71
+
72
+ - Local validation and HTTP failures use bare `RuntimeError` (via `raise "message"`) -- not custom exception classes. These cover workspace fetch HTTP errors and unexpected response shapes.
73
+ - gRPC errors from `streamed_list_objects` propagate directly as `GRPC::BadStatus` subclasses -- do not wrap them.
74
+
75
+ ## RBS Type Signatures
76
+
77
+ - Signatures live in `sig/kessel/rbac.rbs`. Update this file when adding or changing public method signatures.
78
+ - The `Workspace` struct, all public methods, and `list_workspaces` return type (`Enumerator`) are declared.
79
+ - Run `steep check` after changes to verify type correctness.
80
+
81
+ ## Testing
82
+
83
+ - Tests are in `spec/kessel/rbac/v2_spec.rb`.
84
+ - The spec includes `Kessel::RBAC::V2` in the test context and calls module methods directly.
85
+ - HTTP calls are fully mocked -- `Net::HTTP`, `Net::HTTP::Get`, `URI`, and response objects.
86
+ - gRPC streaming is mocked by stubbing `inventory.streamed_list_objects` with arrays or dynamic blocks.
87
+ - Test pagination by returning different continuation tokens across multiple mock invocations.
88
+ - Assert both the exception class and a message regex for error paths.
89
+
90
+ ## RuboCop
91
+
92
+ This directory is subject to RuboCop rules (unlike generated code). Limits: method length 25, class length 150, cyclomatic complexity 10, line length 120.
93
+
94
+ ## Relationship to Kessel::Console
95
+
96
+ `lib/kessel/console.rb` depends on `v2_helpers.rb` (via `require_relative 'rbac/v2_helpers'`) and uses `principal_subject` to convert Red Hat identity headers into `SubjectReference` objects. Changes to factory helper signatures affect `Console`.
97
+
98
+ ## Adding New RBAC Operations
99
+
100
+ 1. Determine whether the operation is HTTP-based (REST) or gRPC-based (protobuf service call).
101
+ 2. Add public methods to `v2.rb`. Add factory helpers to `v2_helpers.rb`. Add HTTP plumbing to `v2_http.rb`.
102
+ 3. Update `sig/kessel/rbac.rbs` with the new method signature.
103
+ 4. Add tests in `spec/kessel/rbac/v2_spec.rb` following the existing mock patterns.
104
+ 5. Run `bundle exec rspec` and `steep check`.
@@ -14,12 +14,14 @@ module Kessel
14
14
  DEFAULT_PAGE_LIMIT = 1000
15
15
  Workspace = Struct.new(:id, :name, :type, :description)
16
16
 
17
- def fetch_default_workspace(rbac_base_endpoint, org_id, auth: nil, http_client: nil)
18
- fetch_workspace(rbac_base_endpoint, org_id, 'default', auth: auth, http_client: http_client)
17
+ def fetch_default_workspace(rbac_base_endpoint, org_id, auth: nil, http_client: nil, with_ancestry: true)
18
+ fetch_workspace(rbac_base_endpoint, org_id, 'default', auth: auth, http_client: http_client,
19
+ with_ancestry: with_ancestry)
19
20
  end
20
21
 
21
- def fetch_root_workspace(rbac_base_endpoint, org_id, auth: nil, http_client: nil)
22
- fetch_workspace(rbac_base_endpoint, org_id, 'root', auth: auth, http_client: http_client)
22
+ def fetch_root_workspace(rbac_base_endpoint, org_id, auth: nil, http_client: nil, with_ancestry: true)
23
+ fetch_workspace(rbac_base_endpoint, org_id, 'root', auth: auth, http_client: http_client,
24
+ with_ancestry: with_ancestry)
23
25
  end
24
26
 
25
27
  # Lists all workspaces that a subject has a specific relation to.
@@ -100,12 +102,12 @@ module Kessel
100
102
  )
101
103
  end
102
104
 
103
- def fetch_workspace(rbac_base_endpoint, org_id, workspace_type, auth: nil, http_client: nil)
105
+ def fetch_workspace(rbac_base_endpoint, org_id, workspace_type, auth: nil, http_client: nil,
106
+ with_ancestry: true)
104
107
  rbac_base_endpoint = rbac_base_endpoint.delete_suffix('/')
105
108
  uri = URI(rbac_base_endpoint + WORKSPACE_ENDPOINT)
106
- query = {
107
- type: workspace_type
108
- }
109
+ query = { type: workspace_type }
110
+ query[:with_ancestry] = 'true' if with_ancestry
109
111
  uri.query = URI.encode_www_form(query)
110
112
  if http_client.nil?
111
113
  http_client = Net::HTTP.new(uri.host, uri.port)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Kessel
4
4
  module Inventory
5
- VERSION = '1.10.0'
5
+ VERSION = '1.12.0'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kessel-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.10.0
4
+ version: 1.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Project Kessel
@@ -210,6 +210,7 @@ files:
210
210
  - lib/kessel/console.rb
211
211
  - lib/kessel/grpc.rb
212
212
  - lib/kessel/inventory.rb
213
+ - lib/kessel/inventory/GUIDELINES.md
213
214
  - lib/kessel/inventory/v1.rb
214
215
  - lib/kessel/inventory/v1/health_pb.rb
215
216
  - lib/kessel/inventory/v1/health_services_pb.rb
@@ -236,6 +237,8 @@ files:
236
237
  - lib/kessel/inventory/v1beta1/resources/reporter_data_pb.rb
237
238
  - lib/kessel/inventory/v1beta1/resources/resource_label_pb.rb
238
239
  - lib/kessel/inventory/v1beta2.rb
240
+ - lib/kessel/inventory/v1beta2/acquire_lock_request_pb.rb
241
+ - lib/kessel/inventory/v1beta2/acquire_lock_response_pb.rb
239
242
  - lib/kessel/inventory/v1beta2/allowed_pb.rb
240
243
  - lib/kessel/inventory/v1beta2/check_bulk_request_pb.rb
241
244
  - lib/kessel/inventory/v1beta2/check_bulk_response_pb.rb
@@ -251,10 +254,23 @@ files:
251
254
  - lib/kessel/inventory/v1beta2/check_self_response_pb.rb
252
255
  - lib/kessel/inventory/v1beta2/consistency_pb.rb
253
256
  - lib/kessel/inventory/v1beta2/consistency_token_pb.rb
257
+ - lib/kessel/inventory/v1beta2/create_tuples_request_pb.rb
258
+ - lib/kessel/inventory/v1beta2/create_tuples_response_pb.rb
254
259
  - lib/kessel/inventory/v1beta2/delete_resource_request_pb.rb
255
260
  - lib/kessel/inventory/v1beta2/delete_resource_response_pb.rb
261
+ - lib/kessel/inventory/v1beta2/delete_tuples_request_pb.rb
262
+ - lib/kessel/inventory/v1beta2/delete_tuples_response_pb.rb
256
263
  - lib/kessel/inventory/v1beta2/inventory_service_pb.rb
257
264
  - lib/kessel/inventory/v1beta2/inventory_service_services_pb.rb
265
+ - lib/kessel/inventory/v1beta2/read_tuples_request_pb.rb
266
+ - lib/kessel/inventory/v1beta2/read_tuples_response_pb.rb
267
+ - lib/kessel/inventory/v1beta2/relation_fencing_check_pb.rb
268
+ - lib/kessel/inventory/v1beta2/relation_object_reference_pb.rb
269
+ - lib/kessel/inventory/v1beta2/relation_object_type_pb.rb
270
+ - lib/kessel/inventory/v1beta2/relation_subject_filter_pb.rb
271
+ - lib/kessel/inventory/v1beta2/relation_subject_reference_pb.rb
272
+ - lib/kessel/inventory/v1beta2/relation_tuple_filter_pb.rb
273
+ - lib/kessel/inventory/v1beta2/relationship_pb.rb
258
274
  - lib/kessel/inventory/v1beta2/report_resource_request_pb.rb
259
275
  - lib/kessel/inventory/v1beta2/report_resource_response_pb.rb
260
276
  - lib/kessel/inventory/v1beta2/reporter_reference_pb.rb
@@ -269,7 +285,10 @@ files:
269
285
  - lib/kessel/inventory/v1beta2/streamed_list_subjects_request_pb.rb
270
286
  - lib/kessel/inventory/v1beta2/streamed_list_subjects_response_pb.rb
271
287
  - lib/kessel/inventory/v1beta2/subject_reference_pb.rb
288
+ - lib/kessel/inventory/v1beta2/tuple_service_pb.rb
289
+ - lib/kessel/inventory/v1beta2/tuple_service_services_pb.rb
272
290
  - lib/kessel/inventory/v1beta2/write_visibility_pb.rb
291
+ - lib/kessel/rbac/GUIDELINES.md
273
292
  - lib/kessel/rbac/v2.rb
274
293
  - lib/kessel/rbac/v2_helpers.rb
275
294
  - lib/kessel/rbac/v2_http.rb