Skip to content
This repository was archived by the owner on Sep 9, 2026. It is now read-only.

OSAC-1111: StorageBackend API - #728

Merged
openshift-merge-bot[bot] merged 9 commits into
osac-project:mainfrom
rgolangh:feat/OSAC-1111-storage-backend-api
Jun 24, 2026
Merged

openshift-merge-bot[bot] merged 9 commits into
osac-project:mainfrom
rgolangh:feat/OSAC-1111-storage-backend-api

Conversation

@rgolangh

@rgolangh rgolangh commented Jun 18, 2026 •

Copy link
Copy Markdown
Contributor

StorageBackend API

Story type: [DEV]
Design: EP #60

Summary

Implements the StorageBackend private gRPC API for managing storage array registrations in the fulfillment-service. This is phase 1 of the StorageBackend feature, following the NetworkClass pattern: DB-backed entity with full CRUD, no CRD, no reconciler, no public API.

Storage backends are platform-wide infrastructure managed by Cloud Provider Admins — they are not per-tenant resources. The server forces auth.SharedTenant on all StorageBackend objects, following the same pattern as NetworkClass and InstanceType.

Design Decisions

  • Platform scope (SharedTenant): Storage backends represent physical storage arrays shared across tenants. Using auth.SharedTenant makes them visible to all authenticated users through the existing tenancy filter, matching how NetworkClass handles platform-scoped entities.
  • Signal RPC: Defined in proto (required by GenericServer framework which uses proto reflection to find it) but intentionally unimplemented — the embedded UnimplementedStorageBackendsServer returns codes.Unimplemented. Signal will be needed in phase 0.2 for state transition reconciliation.
  • No StorageTier FK in phase 1: StorageTier doesn't exist yet. Delete delegates directly to the generic server. Phase 0.2 will add a referential integrity check to prevent deletion when StorageTiers reference the backend.

Changes

Proto definitions:

  • storage_backend_type.proto — StorageBackend, StorageBackendCredentials, StorageBackendStatus messages; StorageBackendState enum (UNSPECIFIED, READY)
  • storage_backends_service.proto — List/Get/Create/Update/Delete RPCs with HTTP annotations for REST gateway, plus Signal RPC (gRPC-only, no HTTP annotation)
  • event_type.proto — Added StorageBackend storage_backend = 30 to event payload oneof

Database:

  • Migration 57: storage_backends + archived_storage_backends tables with standard generic schema columns, indexes (by_name, by_creator, by_label GIN), unique active name constraint (permits reuse after deletion), immutability trigger on id/name

Server implementation:

  • private_storage_backends_server.go — Builder pattern, CRUD with validation (required: provider, endpoint, credentials.username, credentials.password), immutability enforcement (provider, metadata.name, metadata.tenant), state forced to READY on create, caller-provided ID cleared
  • generic_server.go — Added StorageBackend case to setPayload() switch

Registration:

  • start_grpc_server_cmd.go — Private StorageBackends server registration
  • start_rest_gateway_cmd.go — REST gateway handler registration

Reviewer Focus Areas

  1. Immutability: Both DB trigger (check_immutable_columns('id', 'name')) and server validation (validateStorageBackend) enforce immutability — verify both layers are consistent.
  2. Tenant scoping: Create forces auth.SharedTenant; verify no path allows a caller to set a different tenant.
  3. Validation completeness: Required fields on create: provider, endpoint, credentials.username, credentials.password. Verify Update's applyStorageBackendUpdate field mask paths are complete.
  4. Pattern consistency: This follows NetworkClass closely — if anything diverges from that pattern, it should be intentional.

Testing

  • Unit tests: 27 Ginkgo test cases in private_storage_backends_server_test.go covering CRUD lifecycle, pagination, filtering, ordering, field mask updates, validation, immutability, name uniqueness, name reuse after delete, optimistic locking, UUID generation, state enforcement, tenant enforcement
  • Integration tests: Deferred to phase 0.2 (requires kind cluster setup)
  • Coverage: Core CRUD methods at 90%+ coverage; all behavioral contracts tested through public gRPC interfaces against a real PostgreSQL container

Deferred to Phase 0.2

  • State transitions: MAINTENANCE and DECOMMISSIONED states (not defined in proto — will be added in phase 0.2)
  • Referential integrity: Delete check for StorageTier references (StorageTier table doesn't exist yet)
  • Integration tests: Full gRPC/REST endpoint tests against kind cluster

Acceptance Criteria

  • AC-1: CreateStorageBackend creates a backend with state READY and returns the created object with a generated ID
  • AC-2: GetStorageBackend retrieves a backend by ID with all fields populated
  • AC-3: ListStorageBackends returns paginated results and supports filtering by field values
  • AC-4: UpdateStorageBackend applies partial updates without modifying unspecified fields
  • AC-5: UpdateStorageBackend rejects concurrent conflicting writes (optimistic locking)
  • AC-6: DeleteStorageBackend permanently deletes the backend (StorageTier ref check deferred to phase 0.2)
  • AC-7: All CRUD RPCs are accessible via both gRPC and REST endpoints
  • AC-8: Tests cover the full CRUD lifecycle, pagination, filtering, and concurrency control

Summary by CodeRabbit

  • New Features

    • Added support for managing storage backends through the private API, including list, get, create, update, and delete operations.
    • Storage backend events now include the backend details, with sensitive credentials sanitized before being sent.
    • Storage backends are now stored with dedicated backend records and support pagination, filtering, ordering, and optimistic locking.
  • Bug Fixes

    • Enforced required fields and immutability rules to prevent invalid storage backend updates.
    • Improved consistency by automatically setting new storage backends to a ready state and platform scope.

@openshift-ci

openshift-ci Bot commented Jun 18, 2026

Copy link
Copy Markdown

Skipping CI for Draft Pull Request.
If you want CI signal for your change, please convert it to an actual PR.
You can still manually trigger a test run with /test all

@openshift-ci-robot

openshift-ci-robot commented Jun 18, 2026 •

Copy link
Copy Markdown

@rgolangh: This pull request references [OSAC-1111](https://redhat.atlassian.net/browse/OS[AC-1](https://redhat.atlassian.net/browse/AC-1)111) which is a valid jira issue.

Warning: The referenced jira issue has an invalid target version for the target branch this PR targets: expected the epic to target the "5.0.0" version, but no target version was set.

Details

In response to this:

[OSAC-1111](https://redhat.atlassian.net/browse/OS[AC-1](https://redhat.atlassian.net/browse/AC-1)111): StorageBackend API

Story type: [DEV]

Summary

Implements the StorageBackend private gRPC API for managing storage array registrations in the fulfillment-service. This is phase 1 of the StorageBackend feature (EP #60), following the NetworkClass pattern: DB-backed entity with full CRUD, no CRD, no reconciler, no public API. Only the READY state is implemented; MAINTENANCE and DECOMMISSIONED are deferred to phase 0.2.

Changes

Proto definitions:

  • storage_backend_type.proto — StorageBackend, StorageBackendCredentials, StorageBackendStatus messages; StorageBackendState enum (UNSPECIFIED, READY)
  • storage_backends_service.proto — List/Get/Create/Update/Delete + Signal RPCs with HTTP annotations for REST gateway
  • event_type.proto — Added StorageBackend storage_backend = 30 to event payload oneof

Database:

  • Migration 57: storage_backends + archived_storage_backends tables with indexes (by_name, by_creator, by_label GIN), unique active name constraint, immutability trigger on id/name

Server implementation:

  • private_storage_backends_server.go — Builder pattern, CRUD with validation (required: provider, endpoint, credentials), immutability enforcement (provider, metadata.name, metadata.tenant), platform-scoped via auth.SharedTenant
  • generic_server.go — Added StorageBackend case to setPayload() switch

Registration:

  • start_grpc_server_cmd.go — Private StorageBackends server registration
  • start_rest_gateway_cmd.go — REST gateway handler registration

Testing

  • Unit tests: 27 Ginkgo test cases in private_storage_backends_server_test.go covering CRUD lifecycle, pagination, filtering, ordering, field mask updates, validation, immutability, name uniqueness, name reuse after delete, optimistic locking, UUID generation, state enforcement, tenant enforcement
  • Integration tests: N/A (deferred to IT suite against kind cluster)
  • Coverage: Core CRUD methods at 90%+ coverage; all behavioral contracts tested through public gRPC interfaces

Acceptance Criteria

  • AC-1: CreateStorageBackend creates a backend with state READY and returns the created object with a generated ID
  • AC-2: GetStorageBackend retrieves a backend by ID with all fields populated
  • AC-3: ListStorageBackends returns paginated results and supports filtering by field values
  • AC-4: UpdateStorageBackend applies partial updates without modifying unspecified fields
  • AC-5: UpdateStorageBackend rejects concurrent conflicting writes (optimistic locking)
  • AC-6: DeleteStorageBackend permanently deletes the backend (StorageTier ref check deferred to phase 0.2)
  • AC-7: All CRUD RPCs are accessible via both gRPC and REST endpoints
  • AC-8: Tests cover the full CRUD lifecycle, pagination, filtering, and concurrency control

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository.

@coderabbitai

coderabbitai Bot commented Jun 18, 2026 •

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds a new StorageBackends private API resource end-to-end: proto type and service definitions, a SQL migration creating the backing tables with immutability triggers, a PrivateStorageBackendsServer gRPC implementation with CRUD validation and field-mask partial updates, event payload sanitization in GenericServer, and registration in both the gRPC server and REST gateway startup paths.

Changes

Private StorageBackends API

Layer / File(s) Summary
StorageBackend proto types and event payload
proto/private/osac/private/v1/storage_backend_type.proto, proto/private/osac/private/v1/event_type.proto
Defines StorageBackend, StorageBackendSpec, StorageBackendCredentials, StorageBackendStatus, and StorageBackendState messages/enum; extends Event.oneof payload with storage_backend = 30.
StorageBackends gRPC service and request/response messages
proto/private/osac/private/v1/storage_backends_service.proto
Defines all CRUD request/response message types including pagination, CEL filter, FieldMask, optimistic-lock lock flag, and the StorageBackends service with HTTP/JSON transcoding bindings. Signal is declared but not implemented.
Database migration
internal/database/migrations/62_create_storage_backends_tables.up.sql
Creates storage_backends and archived_storage_backends tables, adds creator and GIN-label indexes, a global unique index on name, and a before update trigger enforcing immutability of id, name, and tenant.
PrivateStorageBackendsServer implementation
internal/servers/private_storage_backends_server.go, internal/servers/generic_server.go
Implements PrivateStorageBackendsServerBuilder and PrivateStorageBackendsServer with full CRUD delegation to GenericServer, create/update validation (required fields, spec.provider immutability, forced SharedTenant), and a new setPayload case that clones and scrubs credentials.password before event emission.
PrivateStorageBackendsServer test suite
internal/servers/private_storage_backends_server_test.go
Ginkgo suite covering builder requirements, CRUD semantics (READY state, UUID generation, forced tenant), field-mask partial updates, validation errors, immutability rules, name uniqueness, optimistic locking, and update-without-id rejection.
gRPC and REST gateway startup wiring
internal/cmd/service/start/grpcserver/start_grpc_server_cmd.go, internal/cmd/service/start/restgateway/start_rest_gateway_cmd.go
Constructs and registers PrivateStorageBackendsServer on the gRPC server at startup; adds RegisterStorageBackendsHandler to the REST gateway handler slice.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant RESTGateway
  participant PrivateStorageBackendsServer
  participant GenericServer
  participant DB

  rect rgba(70, 130, 180, 0.5)
    note over Client,DB: Create Flow
    Client->>RESTGateway: POST /private/v1/storage_backends
    RESTGateway->>PrivateStorageBackendsServer: Create(StorageBackendsCreateRequest)
    PrivateStorageBackendsServer->>PrivateStorageBackendsServer: validateStorageBackendCreate (provider, endpoint, credentials)
    PrivateStorageBackendsServer->>PrivateStorageBackendsServer: normalize (clear id, set READY, force SharedTenant)
    PrivateStorageBackendsServer->>GenericServer: Create(ctx, object)
    GenericServer->>DB: INSERT into storage_backends
    GenericServer->>GenericServer: setPayload — clone & clear credentials.password for event
    GenericServer-->>PrivateStorageBackendsServer: StorageBackend (sanitized)
    PrivateStorageBackendsServer-->>Client: StorageBackendsCreateResponse
  end

  rect rgba(60, 179, 113, 0.5)
    note over Client,DB: Update Flow
    Client->>RESTGateway: PUT /private/v1/storage_backends/{id}
    RESTGateway->>PrivateStorageBackendsServer: Update(StorageBackendsUpdateRequest)
    PrivateStorageBackendsServer->>GenericServer: Get(ctx, id)
    GenericServer->>DB: SELECT from storage_backends
    PrivateStorageBackendsServer->>PrivateStorageBackendsServer: validateStorageBackendUpdate (provider immutability)
    PrivateStorageBackendsServer->>GenericServer: Update(ctx, object, lock, update_mask)
    GenericServer->>DB: UPDATE storage_backends
    GenericServer-->>Client: StorageBackendsUpdateResponse
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • trewest
  • jhernand
  • akshaynadkarni

Poem

A backend for storage, now private and neat,
With providers immutable, credentials discreet 🔑
SharedTenant enforced, the UUID assigned,
READY from birth, every object aligned ✨
From proto to gateway, the pipeline complete—
No password leaked in the event on the wire! 🎉


Caution

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

  • Ignore

❌ Failed checks (1 error, 1 warning)

Check name Status Explanation Resolution
No-Hardcoded-Secrets ❌ Error New tests hardcode credential literals like Password: "secret" and Password: "new-secret"; the exception for admin/admin does not apply. Replace literal credential values in tests with non-literal fixtures/helpers or injected test data so no password/secret-like string is hardcoded.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (9 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately summarizes the main change: adding the StorageBackend API.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
No-Weak-Crypto ✅ Passed No weak crypto primitives, custom crypto, or non-constant-time secret comparisons appear in the changed code; password handling is only presence checks and test string assertions.
No-Injection-Vectors ✅ Passed No touched file contains shell/eval/pickle/yaml/innerHTML sinks; SQL in the shared filter translator escapes literals and uses schema-derived field names.
Container-Privileges ✅ Passed No privileged settings found; manifests use runAsNonRoot:true and allowPrivilegeEscalation:false, with no hostNetwork/PID/IPC or SYS_ADMIN flags.
No-Sensitive-Data-In-Logs ✅ Passed No new log calls expose secrets: the new server file has none, GenericServer logs only IDs/tenant requests/errors, and StorageBackend event payloads strip credentials.password before notify.
Ai-Attribution ✅ Passed PASS: HEAD includes an Assisted-by trailer for Claude Code, and I found no AI-related Co-Authored-By trailer.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@openshift-ci-robot

openshift-ci-robot commented Jun 18, 2026 •

Copy link
Copy Markdown

@rgolangh: This pull request references [OSAC-1111](https://redhat.atlassian.net/browse/OS[AC-1](https://redhat.atlassian.net/browse/AC-1)111) which is a valid jira issue.

Warning: The referenced jira issue has an invalid target version for the target branch this PR targets: expected the epic to target the "5.0.0" version, but no target version was set.

Details

In response to this:

[OSAC-1111](https://redhat.atlassian.net/browse/OS[AC-1](https://redhat.atlassian.net/browse/AC-1)111): StorageBackend API

Story type: [DEV]
Design: EP #60

Summary

Implements the StorageBackend private gRPC API for managing storage array registrations in the fulfillment-service. This is phase 1 of the StorageBackend feature, following the NetworkClass pattern: DB-backed entity with full CRUD, no CRD, no reconciler, no public API.

Storage backends are platform-wide infrastructure managed by Cloud Provider Admins — they are not per-tenant resources. The server forces auth.SharedTenant on all StorageBackend objects, following the same pattern as NetworkClass and InstanceType.

Design Decisions

  • Platform scope (SharedTenant): Storage backends represent physical storage arrays shared across tenants. Using auth.SharedTenant makes them visible to all authenticated users through the existing tenancy filter, matching how NetworkClass handles platform-scoped entities.
  • Signal RPC: Defined in proto (required by GenericServer framework which uses proto reflection to find it) but intentionally unimplemented — the embedded UnimplementedStorageBackendsServer returns codes.Unimplemented. Signal will be needed in phase 0.2 for state transition reconciliation.
  • No StorageTier FK in phase 1: StorageTier doesn't exist yet. Delete delegates directly to the generic server. Phase 0.2 will add a referential integrity check to prevent deletion when StorageTiers reference the backend.

Changes

Proto definitions:

  • storage_backend_type.proto — StorageBackend, StorageBackendCredentials, StorageBackendStatus messages; StorageBackendState enum (UNSPECIFIED, READY)
  • storage_backends_service.proto — List/Get/Create/Update/Delete RPCs with HTTP annotations for REST gateway, plus Signal RPC (gRPC-only, no HTTP annotation)
  • event_type.proto — Added StorageBackend storage_backend = 30 to event payload oneof

Database:

  • Migration 57: storage_backends + archived_storage_backends tables with standard generic schema columns, indexes (by_name, by_creator, by_label GIN), unique active name constraint (permits reuse after deletion), immutability trigger on id/name

Server implementation:

  • private_storage_backends_server.go — Builder pattern, CRUD with validation (required: provider, endpoint, credentials.username, credentials.password), immutability enforcement (provider, metadata.name, metadata.tenant), state forced to READY on create, caller-provided ID cleared
  • generic_server.go — Added StorageBackend case to setPayload() switch

Registration:

  • start_grpc_server_cmd.go — Private StorageBackends server registration
  • start_rest_gateway_cmd.go — REST gateway handler registration

Reviewer Focus Areas

  1. Immutability: Both DB trigger (check_immutable_columns('id', 'name')) and server validation (validateStorageBackend) enforce immutability — verify both layers are consistent.
  2. Tenant scoping: Create forces auth.SharedTenant; verify no path allows a caller to set a different tenant.
  3. Validation completeness: Required fields on create: provider, endpoint, credentials.username, credentials.password. Verify Update's applyStorageBackendUpdate field mask paths are complete.
  4. Pattern consistency: This follows NetworkClass closely — if anything diverges from that pattern, it should be intentional.

Testing

  • Unit tests: 27 Ginkgo test cases in private_storage_backends_server_test.go covering CRUD lifecycle, pagination, filtering, ordering, field mask updates, validation, immutability, name uniqueness, name reuse after delete, optimistic locking, UUID generation, state enforcement, tenant enforcement
  • Integration tests: Deferred to phase 0.2 (requires kind cluster setup)
  • Coverage: Core CRUD methods at 90%+ coverage; all behavioral contracts tested through public gRPC interfaces against a real PostgreSQL container

Deferred to Phase 0.2

  • State transitions: MAINTENANCE and DECOMMISSIONED states (not defined in proto — will be added in phase 0.2)
  • Referential integrity: Delete check for StorageTier references (StorageTier table doesn't exist yet)
  • Integration tests: Full gRPC/REST endpoint tests against kind cluster

Acceptance Criteria

  • AC-1: CreateStorageBackend creates a backend with state READY and returns the created object with a generated ID
  • AC-2: GetStorageBackend retrieves a backend by ID with all fields populated
  • AC-3: ListStorageBackends returns paginated results and supports filtering by field values
  • AC-4: UpdateStorageBackend applies partial updates without modifying unspecified fields
  • AC-5: UpdateStorageBackend rejects concurrent conflicting writes (optimistic locking)
  • AC-6: DeleteStorageBackend permanently deletes the backend (StorageTier ref check deferred to phase 0.2)
  • AC-7: All CRUD RPCs are accessible via both gRPC and REST endpoints
  • AC-8: Tests cover the full CRUD lifecycle, pagination, filtering, and concurrency control

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository.

@openshift-ci-robot

openshift-ci-robot commented Jun 18, 2026 •

Copy link
Copy Markdown

@rgolangh: This pull request references OSAC-1111 which is a valid jira issue.

Warning: The referenced jira issue has an invalid target version for the target branch this PR targets: expected the epic to target the "5.0.0" version, but no target version was set.

Details

In response to this:

[OSAC-1111](https://redhat.atlassian.net/browse/OS[AC-1](https://redhat.atlassian.net/browse/AC-1)111): StorageBackend API

Story type: [DEV]
Design: EP #60

Summary

Implements the StorageBackend private gRPC API for managing storage array registrations in the fulfillment-service. This is phase 1 of the StorageBackend feature, following the NetworkClass pattern: DB-backed entity with full CRUD, no CRD, no reconciler, no public API.

Storage backends are platform-wide infrastructure managed by Cloud Provider Admins — they are not per-tenant resources. The server forces auth.SharedTenant on all StorageBackend objects, following the same pattern as NetworkClass and InstanceType.

Design Decisions

  • Platform scope (SharedTenant): Storage backends represent physical storage arrays shared across tenants. Using auth.SharedTenant makes them visible to all authenticated users through the existing tenancy filter, matching how NetworkClass handles platform-scoped entities.
  • Signal RPC: Defined in proto (required by GenericServer framework which uses proto reflection to find it) but intentionally unimplemented — the embedded UnimplementedStorageBackendsServer returns codes.Unimplemented. Signal will be needed in phase 0.2 for state transition reconciliation.
  • No StorageTier FK in phase 1: StorageTier doesn't exist yet. Delete delegates directly to the generic server. Phase 0.2 will add a referential integrity check to prevent deletion when StorageTiers reference the backend.

Changes

Proto definitions:

  • storage_backend_type.proto — StorageBackend, StorageBackendCredentials, StorageBackendStatus messages; StorageBackendState enum (UNSPECIFIED, READY)
  • storage_backends_service.proto — List/Get/Create/Update/Delete RPCs with HTTP annotations for REST gateway, plus Signal RPC (gRPC-only, no HTTP annotation)
  • event_type.proto — Added StorageBackend storage_backend = 30 to event payload oneof

Database:

  • Migration 57: storage_backends + archived_storage_backends tables with standard generic schema columns, indexes (by_name, by_creator, by_label GIN), unique active name constraint (permits reuse after deletion), immutability trigger on id/name

Server implementation:

  • private_storage_backends_server.go — Builder pattern, CRUD with validation (required: provider, endpoint, credentials.username, credentials.password), immutability enforcement (provider, metadata.name, metadata.tenant), state forced to READY on create, caller-provided ID cleared
  • generic_server.go — Added StorageBackend case to setPayload() switch

Registration:

  • start_grpc_server_cmd.go — Private StorageBackends server registration
  • start_rest_gateway_cmd.go — REST gateway handler registration

Reviewer Focus Areas

  1. Immutability: Both DB trigger (check_immutable_columns('id', 'name')) and server validation (validateStorageBackend) enforce immutability — verify both layers are consistent.
  2. Tenant scoping: Create forces auth.SharedTenant; verify no path allows a caller to set a different tenant.
  3. Validation completeness: Required fields on create: provider, endpoint, credentials.username, credentials.password. Verify Update's applyStorageBackendUpdate field mask paths are complete.
  4. Pattern consistency: This follows NetworkClass closely — if anything diverges from that pattern, it should be intentional.

Testing

  • Unit tests: 27 Ginkgo test cases in private_storage_backends_server_test.go covering CRUD lifecycle, pagination, filtering, ordering, field mask updates, validation, immutability, name uniqueness, name reuse after delete, optimistic locking, UUID generation, state enforcement, tenant enforcement
  • Integration tests: Deferred to phase 0.2 (requires kind cluster setup)
  • Coverage: Core CRUD methods at 90%+ coverage; all behavioral contracts tested through public gRPC interfaces against a real PostgreSQL container

Deferred to Phase 0.2

  • State transitions: MAINTENANCE and DECOMMISSIONED states (not defined in proto — will be added in phase 0.2)
  • Referential integrity: Delete check for StorageTier references (StorageTier table doesn't exist yet)
  • Integration tests: Full gRPC/REST endpoint tests against kind cluster

Acceptance Criteria

  • AC-1: CreateStorageBackend creates a backend with state READY and returns the created object with a generated ID
  • AC-2: GetStorageBackend retrieves a backend by ID with all fields populated
  • AC-3: ListStorageBackends returns paginated results and supports filtering by field values
  • AC-4: UpdateStorageBackend applies partial updates without modifying unspecified fields
  • AC-5: UpdateStorageBackend rejects concurrent conflicting writes (optimistic locking)
  • AC-6: DeleteStorageBackend permanently deletes the backend (StorageTier ref check deferred to phase 0.2)
  • AC-7: All CRUD RPCs are accessible via both gRPC and REST endpoints
  • AC-8: Tests cover the full CRUD lifecycle, pagination, filtering, and concurrency control

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository.

@rgolangh
rgolangh marked this pull request as ready for review June 18, 2026 14:26
@openshift-ci
openshift-ci Bot requested review from larsks and tzvatot June 18, 2026 14:26
@rgolangh
rgolangh force-pushed the feat/OSAC-1111-storage-backend-api branch 2 times, most recently from a963885 to 3eb2559 Compare June 18, 2026 14:27
string endpoint = 5;

// Credentials for authenticating with the storage management API.
StorageBackendCredentials credentials = 6;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All these fields should be in a status message. The StorageBackend should only have id, metadata, spec and status.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed — I'll restructure to the id/metadata/spec/status pattern. The spec-level fields (provider, description, endpoint, credentials) will move into a StorageBackendSpec message. This is consistent with how Hub puts kubeconfig in HubSpec and User puts credentials in UserSpec.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rgolangh I thought you were going to update the StorageBackend proto to the id/metadata/spec/status pattern in this PR. Did you miss updating it? This will change the protobuf generated files.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in the latest push — restructured StorageBackend to id/metadata/spec/status pattern. Fields provider, description, endpoint, and credentials are now in StorageBackendSpec, with StorageBackendStatus holding state and message. All server code, tests, and field-mask paths updated accordingly.

// Storage provider identifier. For example: "vast", "ceph", "pure".
//
// This value is immutable once the StorageBackend is created.
string provider = 3;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be an enum type.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The design doc (EP #60) intentionally chose string for extensibility — NFR-2 specifies "no provider-specific validation" so that new storage array vendors can be registered without proto/codegen changes. An enum would require a proto update and service redeploy for each new vendor.

That said, if we want to constrain to known providers, we could add a buf.build validation annotation with a regex or use a reference table. Would you prefer enum, or is the rationale for string acceptable?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rationale for string is acceptable.

-- Name uniqueness among active backends (FR-9). Permits name reuse after deletion.
create unique index storage_backends_unique_active_name
on storage_backends (name)
where deletion_timestamp = 'epoch';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you are going to use the identifier as unique name, then you should make the (tenant, name) unique the primary key. And it shouldn't exclude the soft-deleted objects: it shouldn't be possible to create an object with the same name than an existing object, even if it is soft-deleted.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point on name permanence — I'll remove the soft-delete exclusion from the unique index so names are permanently reserved (no reuse after deletion).

Regarding (tenant, name) as PK: StorageBackend uses auth.SharedTenant so tenant is always "shared", and the GenericDAO framework requires id as PK across all entities. I'll keep id as PK with a non-partial unique index on name to enforce permanent global uniqueness.

create trigger check_immutable_columns
before update on storage_backends
for each row
execute function check_immutable_columns('id', 'name');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tenant should also be immutable.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed — adding tenant to the check_immutable_columns trigger.

return nil
}

func cloneStorageBackend(sb *privatev1.StorageBackend) *privatev1.StorageBackend {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd say this can be inlined, no need for such a small function.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — inlined it to proto.Clone(existingSB).(*privatev1.StorageBackend).

}
if newSB.GetMetadata().GetTenant() != existingSB.GetMetadata().GetTenant() {
return grpcstatus.Errorf(grpccodes.InvalidArgument,
"field 'metadata.tenant' is immutable and cannot be changed")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is better checked in the database trigger.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right — the DB trigger already enforces name immutability via check_immutable_columns('id', 'name', 'tenant'). Removing the Go-level check.

if newSB.GetMetadata().GetName() != existingSB.GetMetadata().GetName() {
return grpcstatus.Errorf(grpccodes.InvalidArgument,
"field 'metadata.name' is immutable and cannot be changed from '%s' to '%s'",
existingSB.GetMetadata().GetName(), newSB.GetMetadata().GetName())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let the database trigger check this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

provider lives inside the JSONB data column, so check_immutable_columns can't enforce it (it only operates on top-level table columns). I can add a custom PL/pgSQL trigger that extracts and compares data->'provider' on UPDATE. Would you prefer that, or is the Go-level check acceptable for JSONB-embedded fields?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant the name and tenant fields. Those aren't in the JSONB data column, and there is already a trigger function that you can add to your table to check immutability. It was defined here:

https://github.com/osac-project/fulfillment-service/blob/main/internal/database/migrations/46_add_immutable_column_trigger.up.sql

And an example of how to apply it here:

-- Attach the trigger to the table to enforce immutability of the id, name and tenant columns:
create trigger check_immutable_columns
before update on instance_types
for each row
execute function check_immutable_columns('id', 'name', 'tenant');

That trigger doesn't currently work for fields in the JSONB data column, so you can keep the check for provider in the application.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, understood — name and tenant immutability is already enforced by the check_immutable_columns('id', 'name', 'tenant') trigger in the migration. The server-side provider immutability check is only needed because provider lives inside the JSONB data column where the trigger can't reach.

return proto.Clone(sb).(*privatev1.StorageBackend)
}

func applyStorageBackendUpdate(base, update *privatev1.StorageBackend, mask *fieldmaskpb.FieldMask) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this trying to achieve? Isn't the regular Update method of the generic server enough?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right — the generic server already handles field-mask based partial updates (via compilePaths + selective field application in GenericServer.Update). Removed applyStorageBackendUpdate, validateStorageBackendUpdateMask, and the clone+merge step. The Update method now just validates provider immutability and delegates to s.generic.Update().

@akshaynadkarni

Copy link
Copy Markdown
Contributor

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 18, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/servers/generic_server.go`:
- Around line 883-884: The StorageBackend object being passed to
event.SetStorageBackend() contains plaintext credentials in the
credentials.password field, which leaks sensitive data to notifier consumers.
Before calling event.SetStorageBackend(object) in the privatev1.StorageBackend
case, redact the credentials from a copy of the StorageBackend object by
clearing or removing the password field, then pass the redacted copy to the
event setter instead of the original object.

In `@internal/servers/private_storage_backends_server_test.go`:
- Around line 427-459: The Immutability test block needs explicit regression
coverage for masked metadata field updates that should be immutable. Add two new
test cases within the Immutability describe block following the pattern of the
"Update changing provider fails" test. First test should update metadata.name
with UpdateMask containing "metadata.name", and second test should update
metadata.tenant with UpdateMask containing "metadata.tenant". For each test,
create a storage backend, call server.Update with the respective masked field
update, and assert that the error code equals codes.InvalidArgument and the
error message contains both the field name and the word "immutable" to prevent
mask-path regressions.

In `@internal/servers/private_storage_backends_server.go`:
- Around line 163-171: The field-mask validation can be bypassed because
unsupported update_mask paths are silently ignored at line 224 but the original
request with those paths still gets persisted through s.generic.Update. To fix
this: (1) Add validation to reject any unsupported paths found in the
request.GetUpdateMask() before proceeding with the update, and (2) Ensure the
merged object passed to validateStorageBackend includes application of all
update_mask paths including metadata paths so that immutability checks in
validateStorageBackend validate against the actual state that will be written by
s.generic.Update.

In `@proto/private/osac/private/v1/storage_backends_service.proto`:
- Around line 71-73: Add documentation comments to the id fields in both
StorageBackendsGetRequest and StorageBackendsDeleteRequest messages. Each id
field should include a brief comment explaining that it represents the unique
identifier of the storage backend to retrieve or delete, respectively. This will
improve consistency with the well-documented fields in ListRequest and
UpdateRequest messages.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: osac-project/coderabbit/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: 4499a2d6-943e-4b06-82b1-651af8586901

📥 Commits

Reviewing files that changed from the base of the PR and between fd4b804 and 3eb2559.

⛔ Files ignored due to path filters (8)
  • internal/api/osac/private/v1/event_type.pb.go is excluded by !**/*.pb.go
  • internal/api/osac/private/v1/event_type_protoopaque.pb.go is excluded by !**/*.pb.go
  • internal/api/osac/private/v1/storage_backend_type.pb.go is excluded by !**/*.pb.go
  • internal/api/osac/private/v1/storage_backend_type_protoopaque.pb.go is excluded by !**/*.pb.go
  • internal/api/osac/private/v1/storage_backends_service.pb.go is excluded by !**/*.pb.go
  • internal/api/osac/private/v1/storage_backends_service.pb.gw.go is excluded by !**/*.pb.gw.go
  • internal/api/osac/private/v1/storage_backends_service_grpc.pb.go is excluded by !**/*.pb.go
  • internal/api/osac/private/v1/storage_backends_service_protoopaque.pb.go is excluded by !**/*.pb.go
📒 Files selected for processing (9)
  • internal/cmd/service/start/grpcserver/start_grpc_server_cmd.go
  • internal/cmd/service/start/restgateway/start_rest_gateway_cmd.go
  • internal/database/migrations/57_create_storage_backends_tables.up.sql
  • internal/servers/generic_server.go
  • internal/servers/private_storage_backends_server.go
  • internal/servers/private_storage_backends_server_test.go
  • proto/private/osac/private/v1/event_type.proto
  • proto/private/osac/private/v1/storage_backend_type.proto
  • proto/private/osac/private/v1/storage_backends_service.proto

Comment thread internal/servers/generic_server.go
Comment thread internal/servers/private_storage_backends_server_test.go
Comment thread internal/servers/private_storage_backends_server.go Outdated
Comment thread proto/private/osac/private/v1/storage_backends_service.proto
Comment thread internal/servers/generic_server.go
@rgolangh
rgolangh force-pushed the feat/OSAC-1111-storage-backend-api branch 2 times, most recently from 8af2c7f to 36c5c20 Compare June 22, 2026 12:41

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (1)
internal/servers/private_storage_backends_server_test.go (1)

446-458: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Strengthen metadata immutability regression coverage (masked paths).

This block only checks HaveOccurred() for metadata.name and doesn’t cover masked updates for metadata.name / metadata.tenant. Please add explicit codes.InvalidArgument + message assertions for both masked paths so regressions in update-mask handling are caught.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/servers/private_storage_backends_server_test.go` around lines 446 -
458, The test "Update changing metadata.name fails at DB level" currently only
verifies that an error occurred but doesn't validate the specific error code or
message, leaving it vulnerable to regressions in update-mask handling. Replace
the generic Expect(err).To(HaveOccurred()) assertion with explicit checks that
verify the error code is codes.InvalidArgument and that the error message
clearly indicates which metadata fields (metadata.name and metadata.tenant) are
immutable and cannot be updated. Consider adding separate test cases or
extending this test to cover both metadata.name and metadata.tenant masked paths
to ensure comprehensive regression coverage.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/servers/private_storage_backends_server_test.go`:
- Around line 82-85: Replace all hardcoded credential values in the test file
with generated or redacted test fixtures. For each instance of the
StorageBackendCredentials_builder where Password fields contain literal values
like "secret" or "new-secret" (found at lines 82-85, 100-103, 236-239, 275-278,
295-298, 318-321, 336-339, 357-359, 377-379, 397-399, and 472-475), define test
fixture constants or use a helper function that generates consistent placeholder
credentials instead of embedding the password strings directly. This ensures the
code adheres to the security rule against hardcoding secrets, even in test code.
- Around line 498-525: The test for the stale version with lock=true in the
"Update with stale version and lock=true fails" test case only checks that an
error occurred using Expect(err).To(HaveOccurred()), but does not verify that
the error is specifically due to optimistic locking rejection. Replace the
generic error assertion with a gRPC status assertion that checks for the
expected error code (such as codes.FailedPrecondition) and optionally validates
the error message contains relevant text about version conflicts. This ensures
the test fails only for the intended stale-lock scenario, not for unrelated
errors.

In `@proto/private/osac/private/v1/storage_backends_service.proto`:
- Around line 57-69: The StorageBackendsListResponse is returning full
StorageBackend objects in the items field, which exposes sensitive credentials
including passwords. Create a separate response model (such as
StorageBackendResponse or StorageBackendSanitized) that excludes or redacts the
credentials field, and use this sanitized model for the items field in
StorageBackendsListResponse. Additionally, ensure that all other read response
messages (Get and Create/Update responses) also use this sanitized model instead
of the full StorageBackend to prevent credential exposure across all read APIs.
Keep credentials as write-only input in the request messages.

---

Duplicate comments:
In `@internal/servers/private_storage_backends_server_test.go`:
- Around line 446-458: The test "Update changing metadata.name fails at DB
level" currently only verifies that an error occurred but doesn't validate the
specific error code or message, leaving it vulnerable to regressions in
update-mask handling. Replace the generic Expect(err).To(HaveOccurred())
assertion with explicit checks that verify the error code is
codes.InvalidArgument and that the error message clearly indicates which
metadata fields (metadata.name and metadata.tenant) are immutable and cannot be
updated. Consider adding separate test cases or extending this test to cover
both metadata.name and metadata.tenant masked paths to ensure comprehensive
regression coverage.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: osac-project/coderabbit/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: 2680e468-4299-4e41-a28d-c8b310060595

📥 Commits

Reviewing files that changed from the base of the PR and between 3eb2559 and 8af2c7f.

⛔ Files ignored due to path filters (8)
  • internal/api/osac/private/v1/event_type.pb.go is excluded by !**/*.pb.go
  • internal/api/osac/private/v1/event_type_protoopaque.pb.go is excluded by !**/*.pb.go
  • internal/api/osac/private/v1/storage_backend_type.pb.go is excluded by !**/*.pb.go
  • internal/api/osac/private/v1/storage_backend_type_protoopaque.pb.go is excluded by !**/*.pb.go
  • internal/api/osac/private/v1/storage_backends_service.pb.go is excluded by !**/*.pb.go
  • internal/api/osac/private/v1/storage_backends_service.pb.gw.go is excluded by !**/*.pb.gw.go
  • internal/api/osac/private/v1/storage_backends_service_grpc.pb.go is excluded by !**/*.pb.go
  • internal/api/osac/private/v1/storage_backends_service_protoopaque.pb.go is excluded by !**/*.pb.go
📒 Files selected for processing (9)
  • internal/cmd/service/start/grpcserver/start_grpc_server_cmd.go
  • internal/cmd/service/start/restgateway/start_rest_gateway_cmd.go
  • internal/database/migrations/57_create_storage_backends_tables.up.sql
  • internal/servers/generic_server.go
  • internal/servers/private_storage_backends_server.go
  • internal/servers/private_storage_backends_server_test.go
  • proto/private/osac/private/v1/event_type.proto
  • proto/private/osac/private/v1/storage_backend_type.proto
  • proto/private/osac/private/v1/storage_backends_service.proto

Comment thread internal/servers/private_storage_backends_server_test.go Outdated
Comment thread internal/servers/private_storage_backends_server_test.go
Comment on lines +57 to +69
message StorageBackendsListResponse {
// Actual number of items returned. Note that this may be smaller than the value requested in the `limit` parameter
// of the request if there are not enough items, or if the system decides that returning that number of items isn't
// feasible or convenient for performance reasons.
int32 size = 1;

// Total number of items of the collection that match the search criteria, regardless of the number of results
// requested with the `limit` parameter.
int32 total = 2;

// List of results.
repeated StorageBackend items = 3;
}

@coderabbitai coderabbitai Bot Jun 22, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Do not expose backend credentials in read response models.

List/Get/Create/Update responses return full StorageBackend, which includes credentials.password. That makes storage-management secrets retrievable via standard read APIs. Use a write-only credentials input (or secret reference) and return redacted/sanitized credential fields in read responses and emitted payloads.

Also applies to: 75-99

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@proto/private/osac/private/v1/storage_backends_service.proto` around lines 57
- 69, The StorageBackendsListResponse is returning full StorageBackend objects
in the items field, which exposes sensitive credentials including passwords.
Create a separate response model (such as StorageBackendResponse or
StorageBackendSanitized) that excludes or redacts the credentials field, and use
this sanitized model for the items field in StorageBackendsListResponse.
Additionally, ensure that all other read response messages (Get and
Create/Update responses) also use this sanitized model instead of the full
StorageBackend to prevent credential exposure across all read APIs. Keep
credentials as write-only input in the request messages.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Credential redaction in read responses is tracked as a follow-up. The private API is admin-only and authenticated — the immediate risk is addressed by redacting credentials from event payloads (NOTIFY). Read-side redaction will be handled in a subsequent iteration.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rgolangh, understood — the admin-only scope and event-payload redaction do lower the immediate blast radius. Deferring the read-side sanitization is a reasonable trade-off for phase 1.

Would you like me to open a GitHub issue to track the read-side credential redaction work so it doesn't get lost?


🧠 Learnings used
Learnt from: tzvatot
Repo: osac-project/fulfillment-service PR: 517
File: proto/private/osac/private/v1/cluster_catalog_items_service.proto:105-106
Timestamp: 2026-05-11T12:42:06.581Z
Learning: In this repository, RPCs named `Signal` (e.g., `Clusters.Signal`, `ClusterTemplates.Signal`, `ComputeInstances.Signal`, `ClusterCatalogItems.Signal`, `ComputeInstanceCatalogItems.Signal`) are intentionally gRPC-only internal controller mechanisms and are not exposed via the REST gateway. During code review, do not flag these `Signal` RPCs for missing `google.api.http` annotations. Only require `google.api.http` bindings for non-`Signal` RPCs that are intended to be exposed through the REST gateway.

Learnt from: ygalblum
Repo: osac-project/fulfillment-service PR: 735
File: proto/private/osac/private/v1/compute_instances_service.proto:46-52
Timestamp: 2026-06-19T20:57:35.568Z
Learning: In this repository’s .proto files, treat `response_body: "object"` in the HTTP bindings for gRPC Create/Update as an intentional, cross-service architectural decision. During code review, do NOT flag additional response fields (e.g., `warnings`, other non-`object` fields) as “not accessible over REST” just because the HTTP binding returns only the primary `object`. Changing `response_body` for only individual RPCs would be inconsistent and risks breaking existing REST clients; if non-`object` fields must be exposed over REST, it should be done as a separate cross-cutting change across services.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/database/migrations/58_create_storage_backends_tables.up.sql`:
- Around line 45-51: Remove the redundant non-unique index
storage_backends_by_name that is created on line 45, since the unique index
storage_backends_unique_name created on lines 50-51 already indexes the same
column and can serve both the purpose of enforcing uniqueness and being used for
lookup queries, eliminating unnecessary write and maintenance overhead.

In `@internal/servers/private_storage_backends_server_test.go`:
- Around line 110-131: The test is asserting that plaintext passwords are
returned in API responses from Create and Get operations, which creates a
cross-tenant credential exposure risk for this platform-scoped object. Remove
the assertion on line 130 that expects the password to be returned in the Get
response (the Expect statement checking obj.GetCredentials().GetPassword()).
Make credentials.password write-only by ensuring the field is accepted in write
requests but never returned in response payloads for Create, Get, Update, and
List operations. Apply the same changes to remove all similar password
assertions throughout the test file (also in test cases around lines 230-246 and
309-326).

In `@internal/servers/private_storage_backends_server.go`:
- Around line 160-165: The provider immutability validation in the
validateStorageBackendUpdate method does not account for the update_mask.paths
when determining if a field was explicitly changed. Currently, the check
`newSB.GetProvider() != ""` treats omitted fields and explicitly cleared fields
the same way. To fix this, modify the validation logic to first check if
"provider" is present in the request's update_mask.paths before validating
immutability. Only validate that the provider has not changed if the field is
explicitly included in the update_mask, ensuring that an explicitly cleared
provider field (empty string in the mask) is properly detected and validated as
an immutable field change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: osac-project/coderabbit/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: f6aacbd1-5e36-442f-a34b-d715726405e0

📥 Commits

Reviewing files that changed from the base of the PR and between 8af2c7f and a5abc9c.

⛔ Files ignored due to path filters (8)
  • internal/api/osac/private/v1/event_type.pb.go is excluded by !**/*.pb.go
  • internal/api/osac/private/v1/event_type_protoopaque.pb.go is excluded by !**/*.pb.go
  • internal/api/osac/private/v1/storage_backend_type.pb.go is excluded by !**/*.pb.go
  • internal/api/osac/private/v1/storage_backend_type_protoopaque.pb.go is excluded by !**/*.pb.go
  • internal/api/osac/private/v1/storage_backends_service.pb.go is excluded by !**/*.pb.go
  • internal/api/osac/private/v1/storage_backends_service.pb.gw.go is excluded by !**/*.pb.gw.go
  • internal/api/osac/private/v1/storage_backends_service_grpc.pb.go is excluded by !**/*.pb.go
  • internal/api/osac/private/v1/storage_backends_service_protoopaque.pb.go is excluded by !**/*.pb.go
📒 Files selected for processing (9)
  • internal/cmd/service/start/grpcserver/start_grpc_server_cmd.go
  • internal/cmd/service/start/restgateway/start_rest_gateway_cmd.go
  • internal/database/migrations/58_create_storage_backends_tables.up.sql
  • internal/servers/generic_server.go
  • internal/servers/private_storage_backends_server.go
  • internal/servers/private_storage_backends_server_test.go
  • proto/private/osac/private/v1/event_type.proto
  • proto/private/osac/private/v1/storage_backend_type.proto
  • proto/private/osac/private/v1/storage_backends_service.proto

Comment thread internal/database/migrations/58_create_storage_backends_tables.up.sql Outdated
Comment thread internal/servers/private_storage_backends_server_test.go
Comment thread internal/servers/private_storage_backends_server.go
@rgolangh
rgolangh force-pushed the feat/OSAC-1111-storage-backend-api branch from a5abc9c to c305f76 Compare June 22, 2026 17:12
version integer not null default 0
);

create index storage_backends_by_name on storage_backends (name);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rgolangh Two things on this migration:

  1. The storage_backends_by_name index is redundant. The unique index storage_backends_unique_name on line 50 already provides the same btree lookup on name.
  2. Can you add a down migration (58_create_storage_backends_tables.down.sql) to drop the tables, indexes, and trigger?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both fixed in latest push:

  1. Removed the redundant storage_backends_by_name non-unique index — the unique index covers the same column.
  2. Renumbered migration from 58 to 59 to avoid conflict with 58_rename_organizations_to_tenants.

}

// Signals a storage backend for reconciliation.
rpc Signal(StorageBackendsSignalRequest) returns (StorageBackendsSignalResponse) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Design doc and PRD both state "No Signal RPC — StorageBackend has no reconciler or controller."

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch - The agent implemented this with a no-op signal. I will completely drop it from the service

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correction: Signal RPC is kept in the proto because GenericServer.Build() hard-codes a lookup for the Signal method in the service descriptor — removing it causes a build failure. Added a comment explaining that Signal is required by the generic server infrastructure but returns UNIMPLEMENTED (via the embedded UnimplementedStorageBackendsServer). No actual Signal logic is implemented.

Comment on lines +175 to +194
func (s *PrivateStorageBackendsServer) validateStorageBackendCreate(_ context.Context,
sb *privatev1.StorageBackend) error {

if sb == nil {
return grpcstatus.Errorf(grpccodes.InvalidArgument, "storage backend is mandatory")
}
if sb.GetProvider() == "" {
return grpcstatus.Errorf(grpccodes.InvalidArgument, "field 'provider' is required")
}
if sb.GetEndpoint() == "" {
return grpcstatus.Errorf(grpccodes.InvalidArgument, "field 'endpoint' is required")
}
if sb.GetCredentials() == nil || sb.GetCredentials().GetUsername() == "" {
return grpcstatus.Errorf(grpccodes.InvalidArgument, "field 'credentials.username' is required")
}
if sb.GetCredentials().GetPassword() == "" {
return grpcstatus.Errorf(grpccodes.InvalidArgument, "field 'credentials.password' is required")
}
return nil
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The design doc specifies that metadata.name is a required field on Create and missing name should return INVALID_ARGUMENT.
Should an explicit name check be added here maybe?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added.

@omer-vishlitzky

Copy link
Copy Markdown
Contributor

/retest

@omer-vishlitzky

Copy link
Copy Markdown
Contributor

🔴 CI Triage: pr_issue | Category: BOOT

Root cause: Database migration number collision: the PR adds migration 58, but migration 58 already exists in the main branch.

Explanation: The PR introduces a new database migration file named 58_create_storage_backends_tables.up.sql. However, another PR (PR 730) was recently merged that added 58_rename_organizations_to_tenants.up.sql. When the fulfillment-grpc-server pod starts, it attempts to run database migrations, but the migration library detects two files with the 58_ prefix and fails with a duplicate migration file error. This causes the fulfillment-grpc-server pod to crash, which in turn causes the fulfillment-console-proxy rollout to time out because it depends on the gRPC server.

Evidence:

pod-fulfillment-grpc-server-fff6658b5-dtppp-grpc-server.log:

failed to init driver with path migrations: duplicate migration file: 58_rename_organizations_to_tenants.up.sql

[PR diff](https://gcsweb-ci.apps.ci.l2s4.p1.openshiftapps.com/gcs/test-platform-results/pr-logs/pull/osac-project_fulfillment-service/728/pull-ci-osac-project-fulfillment-service-main-e2e-vmaas/2069586090716565504/artifacts/e2e-vmaas/osac-project-gather/artifacts/osac-logs/PR diff):

+++ b/internal/database/migrations/58_create_storage_backends_tables.up.sql

Suggestion: Rename the migration file 58_create_storage_backends_tables.up.sql to use the next available migration number (e.g., 59_... or higher) and rebase on the latest main branch.


Prow job | Build 2069586090716565504 | 🤖 triagent

For deeper investigation, use the /osac-debug-e2e skill with this build ID.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
internal/servers/private_storage_backends_server.go (1)

142-165: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Validate the merged post-mask object before generic.Update.

Line 160 validates only the sparse patch object, but Line 165 persists the merged result of GenericServer.Update, which clears masked fields that are omitted from the request. That still lets callers blank required fields like spec.endpoint / spec.credentials and clear spec.provider via update_mask with an empty value. Clone existingSB, apply the mask to that clone, and run required-field plus immutability checks against the merged object before delegating.

Also applies to: 203-211

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/servers/private_storage_backends_server.go` around lines 142 - 165,
The Update flow in PrivateStorageBackendsServer currently validates only the
incoming patch object, but generic.Update persists the merged post-mask result,
so required fields can still be blanked out and immutable fields cleared via the
update mask. In Update, after fetching existingSB, clone it, apply the request’s
update mask to the clone, and run the required-field and immutability checks
against that merged object before calling s.generic.Update; reuse
validateStorageBackendUpdate (and the same pattern in the other affected update
path) so the validation matches what will actually be persisted.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/database/migrations/59_create_storage_backends_tables.up.sql`:
- Line 1: The migration number is still conflicting because 59 is already taken
by an existing storage backends migration. Rename this migration to a free
sequential number and update both the .up.sql and matching .down.sql filenames
so the database migration runner sees a unique pair; use the storage backends
migration filenames as the reference point when choosing the new number.

In `@internal/servers/private_storage_backends_server_test.go`:
- Around line 205-256: Add regression coverage in the storage backend update
tests for masked updates that clear required fields: extend the `server.Update`
cases in `internal/servers/private_storage_backends_server_test.go` to use
`update_mask` against `spec.endpoint`, `spec.credentials`, and `spec.provider`,
and assert each returns `codes.InvalidArgument`. Reuse the existing
`createStorageBackend()` helper and
`privatev1.StorageBackendsUpdateRequest_builder` / `fieldmaskpb.FieldMask`
pattern so the new tests exercise the same `Update` path as the passing
partial-update cases. Make sure the assertions verify the server rejects these
masked clears instead of silently accepting them.

---

Duplicate comments:
In `@internal/servers/private_storage_backends_server.go`:
- Around line 142-165: The Update flow in PrivateStorageBackendsServer currently
validates only the incoming patch object, but generic.Update persists the merged
post-mask result, so required fields can still be blanked out and immutable
fields cleared via the update mask. In Update, after fetching existingSB, clone
it, apply the request’s update mask to the clone, and run the required-field and
immutability checks against that merged object before calling s.generic.Update;
reuse validateStorageBackendUpdate (and the same pattern in the other affected
update path) so the validation matches what will actually be persisted.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: osac-project/coderabbit/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: c267339e-7494-408c-8541-fc5a07d68d89

📥 Commits

Reviewing files that changed from the base of the PR and between a5abc9c and 48cc6ae.

⛔ Files ignored due to path filters (9)
  • internal/api/osac/private/v1/event_type.pb.go is excluded by !**/*.pb.go
  • internal/api/osac/private/v1/event_type_protoopaque.pb.go is excluded by !**/*.pb.go
  • internal/api/osac/private/v1/events_service_grpc.pb.go is excluded by !**/*.pb.go
  • internal/api/osac/private/v1/storage_backend_type.pb.go is excluded by !**/*.pb.go
  • internal/api/osac/private/v1/storage_backend_type_protoopaque.pb.go is excluded by !**/*.pb.go
  • internal/api/osac/private/v1/storage_backends_service.pb.go is excluded by !**/*.pb.go
  • internal/api/osac/private/v1/storage_backends_service.pb.gw.go is excluded by !**/*.pb.gw.go
  • internal/api/osac/private/v1/storage_backends_service_grpc.pb.go is excluded by !**/*.pb.go
  • internal/api/osac/private/v1/storage_backends_service_protoopaque.pb.go is excluded by !**/*.pb.go
📒 Files selected for processing (9)
  • internal/cmd/service/start/grpcserver/start_grpc_server_cmd.go
  • internal/cmd/service/start/restgateway/start_rest_gateway_cmd.go
  • internal/database/migrations/59_create_storage_backends_tables.up.sql
  • internal/servers/generic_server.go
  • internal/servers/private_storage_backends_server.go
  • internal/servers/private_storage_backends_server_test.go
  • proto/private/osac/private/v1/event_type.proto
  • proto/private/osac/private/v1/storage_backend_type.proto
  • proto/private/osac/private/v1/storage_backends_service.proto

Comment on lines +205 to +256
It("Update applies partial changes via field mask", func() {
created := createStorageBackend()

updateResponse, err := server.Update(ctx, privatev1.StorageBackendsUpdateRequest_builder{
Object: privatev1.StorageBackend_builder{
Id: created.GetId(),
Spec: privatev1.StorageBackendSpec_builder{
Description: "Updated description",
}.Build(),
}.Build(),
UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"spec.description"}},
}.Build())
Expect(err).ToNot(HaveOccurred())
Expect(updateResponse.GetObject().GetSpec().GetDescription()).To(Equal("Updated description"))
Expect(updateResponse.GetObject().GetSpec().GetProvider()).To(Equal("vast"))
})

It("Update endpoint", func() {
created := createStorageBackend()

updateResponse, err := server.Update(ctx, privatev1.StorageBackendsUpdateRequest_builder{
Object: privatev1.StorageBackend_builder{
Id: created.GetId(),
Spec: privatev1.StorageBackendSpec_builder{
Endpoint: "https://new-storage.example.com:9443",
}.Build(),
}.Build(),
UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"spec.endpoint"}},
}.Build())
Expect(err).ToNot(HaveOccurred())
Expect(updateResponse.GetObject().GetSpec().GetEndpoint()).To(Equal("https://new-storage.example.com:9443"))
})

It("Update credentials", func() {
created := createStorageBackend()

updateResponse, err := server.Update(ctx, privatev1.StorageBackendsUpdateRequest_builder{
Object: privatev1.StorageBackend_builder{
Id: created.GetId(),
Spec: privatev1.StorageBackendSpec_builder{
Credentials: privatev1.StorageBackendCredentials_builder{
Username: "new-admin",
Password: "new-secret",
}.Build(),
}.Build(),
}.Build(),
UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"spec.credentials"}},
}.Build())
Expect(err).ToNot(HaveOccurred())
Expect(updateResponse.GetObject().GetSpec().GetCredentials().GetUsername()).To(Equal("new-admin"))
Expect(updateResponse.GetObject().GetSpec().GetCredentials().GetPassword()).To(Equal("new-secret"))
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Add regression cases for masked updates that clear required fields.

This suite only asserts create-time required-field validation. Add Update cases that use update_mask to clear spec.endpoint, spec.credentials, and spec.provider, and assert codes.InvalidArgument; otherwise the current server bug can ship without a failing test.

Also applies to: 344-451

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/servers/private_storage_backends_server_test.go` around lines 205 -
256, Add regression coverage in the storage backend update tests for masked
updates that clear required fields: extend the `server.Update` cases in
`internal/servers/private_storage_backends_server_test.go` to use `update_mask`
against `spec.endpoint`, `spec.credentials`, and `spec.provider`, and assert
each returns `codes.InvalidArgument`. Reuse the existing
`createStorageBackend()` helper and
`privatev1.StorageBackendsUpdateRequest_builder` / `fieldmaskpb.FieldMask`
pattern so the new tests exercise the same `Update` path as the passing
partial-update cases. Make sure the assertions verify the server rejects these
masked clears instead of silently accepting them.

@omer-vishlitzky

Copy link
Copy Markdown
Contributor

🔴 CI Triage: pr_issue | Category: BOOT

Root cause: The PR introduces a database migration with version 59, but migration 59 is already taken by a recently merged PR, causing a duplicate migration file error during grpc-server startup.

Explanation: During the boot step's refresh phase, the fulfillment-console-proxy deployment timed out waiting to roll out. The console-proxy depends on the fulfillment-grpc-server, which was in a CrashLoopBackOff state. Checking the fulfillment-grpc-server pod logs reveals the fatal error: failed to init driver with path migrations: duplicate migration file: 59_create_storage_backends_tables.up.sql. This happens because the PR adds a migration file numbered 59, but PR 686 (merged recently) already added migration 59 (59_add_catalog_item_ref_triggers.up.sql). Additionally, PR 740 added migration 60. The golang-migrate library fails to initialize when it encounters multiple migration files with the same version number.

Evidence:

pod-fulfillment-grpc-server-f8576b55-h6xmz-grpc-server-previous.log:

failed to init driver with path migrations: duplicate migration file: 59_create_storage_backends_tables.up.sql

build-log.txt:

ERROR: command failed (exit 1): oc rollout status deploy/fulfillment-console-proxy -n osac-e2e-ci --timeout=360s

Suggestion: Rename the migration file 59_create_storage_backends_tables.up.sql to use the next available migration number (likely 61, as PR 740 recently added migration 60).


Prow job | Build 2069656711924289536 | 🤖 triagent

For deeper investigation, use the /osac-debug-e2e skill with this build ID.

rgolangh added 8 commits June 24, 2026 11:58
Add StorageBackend, StorageBackendCredentials, StorageBackendStatus, and
StorageBackendState proto messages under osac.private.v1. Define the
StorageBackends service with List, Get, Create, Update, Delete RPCs
and HTTP annotations for REST gateway. Add storage_backend payload
field (30) to Event oneof.

Assisted-by: Claude Code <noreply@anthropic.com>
Signed-off-by: Roy Golan <rgolan@redhat.com>
Create storage_backends and archived_storage_backends tables with the
standard generic DAO schema. Add indexes for name, creator, and labels.
Add unique partial index on name for active records (FR-9). Add
immutability trigger for id and name columns.

No tenant FK or tenant index — StorageBackend is platform-scoped
with tenant always set to 'shared'.

Assisted-by: Claude Code <noreply@anthropic.com>
Signed-off-by: Roy Golan <rgolan@redhat.com>
Add PrivateStorageBackendsServer following the NetworkClass pattern:
- Builder with SetLogger, SetNotifier, SetAttributionLogic, SetTenancyLogic, SetMetricsRegisterer
- Create validates required fields, sets state=READY, forces tenant="shared"
- Update with field mask merge and immutability checks (provider, metadata.name, metadata.tenant)
- Delete delegates to generic server
- Add setPayload case for StorageBackend events in generic_server.go
- Register in gRPC server and REST gateway
- Add Signal RPC to proto (required by GenericServer framework, not implemented)

Assisted-by: Claude Code <noreply@anthropic.com>
Signed-off-by: Roy Golan <rgolan@redhat.com>
Cover CRUD lifecycle, validation, immutability, name uniqueness,
optimistic locking, tenant enforcement, ID generation, and state
handling with 27 test cases using the shared Ginkgo test suite.

Assisted-by: Claude Code <noreply@anthropic.com>
Signed-off-by: Roy Golan <rgolan@redhat.com>
Assisted-by: Claude Code <noreply@anthropic.com>
Signed-off-by: Roy Golan <rgolan@redhat.com>
- Remove applyStorageBackendUpdate — generic server handles field masks
- Split validation into create/update functions
- Add tenant to DB immutable columns trigger
- Redact credentials password in event payload
- Add metadata.name and metadata.tenant update_mask tests
- Simplify unique index (delete archives rows, so no partial needed)

Assisted-by: Claude Code <noreply@anthropic.com>
Signed-off-by: Roy Golan <rgolan@redhat.com>
Restructure StorageBackend proto to use spec/status pattern, add
metadata.name validation, remove redundant index, and add Signal
RPC comment.

- Restructure StorageBackend proto: move provider, description,
  endpoint, credentials into StorageBackendSpec message with
  separate StorageBackendStatus (per Juan's review)
- Add metadata.name required validation in Create
- Remove redundant non-unique storage_backends_by_name index
  (unique index already covers it)
- Renumber migration from 58 to 59 to avoid conflict
- Update credential redaction in generic_server.go for spec path
- Add comment on Signal RPC explaining it's required by generic
  server infrastructure but returns UNIMPLEMENTED
- Update all tests for spec/status field paths

Signed-off-by: Roy Golan <rgolan@redhat.com>
Assisted-by: Claude Code <noreply@anthropic.com>
Signed-off-by: Roy Golan <rgolan@redhat.com>
Migrations 59-61 were merged to main since our branch was created.
Renumber storage_backends migration from 59 to 62.

Signed-off-by: Roy Golan <rgolan@redhat.com>
Assisted-by: Claude Code <noreply@anthropic.com>
Signed-off-by: Roy Golan <rgolan@redhat.com>
@rgolangh
rgolangh force-pushed the feat/OSAC-1111-storage-backend-api branch from 48cc6ae to 4471005 Compare June 24, 2026 09:00
@openshift-ci openshift-ci Bot added lgtm and removed lgtm labels Jun 24, 2026
Expect(st.Code()).To(Equal(codes.AlreadyExists))
})

It("Create after delete of same name succeeds", func() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't this fail as per this comment?

create index storage_backends_by_creator on storage_backends (creator);
create index storage_backends_by_label on storage_backends using gin (labels);

-- Name uniqueness across all backends (active and soft-deleted). Names are permanently reserved.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rccrdpccl

Copy link
Copy Markdown
Contributor

/approve

@rccrdpccl

Copy link
Copy Markdown
Contributor

/hold

The storage_backends table was created after migration 49 (which
bulk-added tenant foreign keys), so it needed an explicit tenant_fk
constraint. Also adds the required migration test file covering table
creation, tenant FK enforcement, name uniqueness, and column
immutability.

Assisted-by: Claude Code <noreply@anthropic.com>
Signed-off-by: Roy Golan <rgolan@redhat.com>
@rgolangh
rgolangh force-pushed the feat/OSAC-1111-storage-backend-api branch from f35b284 to cedb35d Compare June 24, 2026 13:35
@rccrdpccl

Copy link
Copy Markdown
Contributor

/lgtm
/approve

/unhold

@openshift-ci

openshift-ci Bot commented Jun 24, 2026

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: rccrdpccl, rgolangh, zszabo-rh

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@openshift-merge-bot
openshift-merge-bot Bot merged commit b2823a1 into osac-project:main Jun 24, 2026
14 checks passed
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants