refactor(chore): Decouple v1 API from storage.Cluster protobuf#19824
refactor(chore): Decouple v1 API from storage.Cluster protobuf#19824
Conversation
|
Skipping CI for Draft Pull Request. |
There was a problem hiding this comment.
Hey - I've found 1 issue, and left some high level feedback:
- There are now three separate mapping implementations between
storage.Clusterandv1.ClusterConfig(central service, sensor generate, plus the implicit proto identity), which makes it easy for fields to drift; consider centralizing these conversions in a shared helper/package so new/changed fields only need to be wired in one place. - The notion of "server‑managed" fields (status, health_status, helm_config, etc.) is currently enforced only implicitly in
convertAPIClusterToStorage; it may be safer to capture this set in a single, clearly named helper or configuration so future contributors don’t accidentally start persisting client-supplied values for those fields.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- There are now three separate mapping implementations between `storage.Cluster` and `v1.ClusterConfig` (central service, sensor generate, plus the implicit proto identity), which makes it easy for fields to drift; consider centralizing these conversions in a shared helper/package so new/changed fields only need to be wired in one place.
- The notion of "server‑managed" fields (status, health_status, helm_config, etc.) is currently enforced only implicitly in `convertAPIClusterToStorage`; it may be safer to capture this set in a single, clearly named helper or configuration so future contributors don’t accidentally start persisting client-supplied values for those fields.
## Individual Comments
### Comment 1
<location path="central/cluster/service/convert.go" line_range="11-20" />
<code_context>
+func convertStorageClusterToAPI(cluster *storage.Cluster) *v1.ClusterConfig {
</code_context>
<issue_to_address>
**suggestion:** Manual 1:1 field mapping in both conversion functions is fragile and may drift as proto definitions evolve.
Both `convertStorageClusterToAPI` and `convertAPIClusterToStorage` manually copy every field, which is easy to miss when new fields are added, contradicting the comment that all fields are copied. Please consider a more robust pattern (shared helper, codegen, or tests/compile-time checks such as round-trip `proto.Equal`) so new fields can’t be accidentally omitted.
Suggested implementation:
```golang
import (
v1 "github.com/stackrox/rox/generated/api/v1"
"github.com/stackrox/rox/generated/storage"
"google.golang.org/protobuf/encoding/protojson"
)
```
```golang
// convertStorageClusterToAPI converts a storage.Cluster to the API representation.
// All fields are copied 1:1 via protojson. The API type is structurally identical to the storage
// type today; they are separated to allow independent evolution in the future. Using protojson
// reduces the risk of fields drifting as proto definitions evolve.
func convertStorageClusterToAPI(cluster *storage.Cluster) *v1.ClusterConfig {
if cluster == nil {
return nil
}
out := &v1.ClusterConfig{}
// Use protojson to perform a structural copy so newly-added fields are automatically handled
// as long as the underlying proto definitions remain structurally compatible.
bytes, err := protojson.Marshal(cluster)
if err != nil {
// In the unlikely event of an error, return an empty config rather than partially populated data.
// Callers should treat a nil cluster input as the primary "no config" signal.
return out
}
if err := protojson.Unmarshal(bytes, out); err != nil {
return out
}
return out
```
To fully implement the suggestion for both directions, the same pattern should be applied to `convertAPIClusterToStorage`, replacing its manual 1:1 field assignments with a protojson-based structural copy (marshal the `*v1.ClusterConfig` to JSON, then unmarshal into `*storage.Cluster`). This will ensure that both conversion functions automatically handle newly added fields as long as the proto messages remain structurally compatible.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| func convertStorageClusterToAPI(cluster *storage.Cluster) *v1.ClusterConfig { | ||
| if cluster == nil { | ||
| return nil | ||
| } | ||
| return &v1.ClusterConfig{ | ||
| Id: cluster.GetId(), | ||
| Name: cluster.GetName(), | ||
| Type: cluster.GetType(), | ||
| Labels: cluster.GetLabels(), | ||
| MainImage: cluster.GetMainImage(), |
There was a problem hiding this comment.
suggestion: Manual 1:1 field mapping in both conversion functions is fragile and may drift as proto definitions evolve.
Both convertStorageClusterToAPI and convertAPIClusterToStorage manually copy every field, which is easy to miss when new fields are added, contradicting the comment that all fields are copied. Please consider a more robust pattern (shared helper, codegen, or tests/compile-time checks such as round-trip proto.Equal) so new fields can’t be accidentally omitted.
Suggested implementation:
import (
v1 "github.com/stackrox/rox/generated/api/v1"
"github.com/stackrox/rox/generated/storage"
"google.golang.org/protobuf/encoding/protojson"
) // convertStorageClusterToAPI converts a storage.Cluster to the API representation.
// All fields are copied 1:1 via protojson. The API type is structurally identical to the storage
// type today; they are separated to allow independent evolution in the future. Using protojson
// reduces the risk of fields drifting as proto definitions evolve.
func convertStorageClusterToAPI(cluster *storage.Cluster) *v1.ClusterConfig {
if cluster == nil {
return nil
}
out := &v1.ClusterConfig{}
// Use protojson to perform a structural copy so newly-added fields are automatically handled
// as long as the underlying proto definitions remain structurally compatible.
bytes, err := protojson.Marshal(cluster)
if err != nil {
// In the unlikely event of an error, return an empty config rather than partially populated data.
// Callers should treat a nil cluster input as the primary "no config" signal.
return out
}
if err := protojson.Unmarshal(bytes, out); err != nil {
return out
}
return outTo fully implement the suggestion for both directions, the same pattern should be applied to convertAPIClusterToStorage, replacing its manual 1:1 field assignments with a protojson-based structural copy (marshal the *v1.ClusterConfig to JSON, then unmarshal into *storage.Cluster). This will ensure that both conversion functions automatically handle newly added fields as long as the proto messages remain structurally compatible.
📝 WalkthroughWalkthroughIntroduces a new API type Changes
Sequence DiagramsequenceDiagram
participant Client
participant Service as Cluster Service
participant Convert as Conversion Layer
participant Storage as Datastore
Client->>Service: PostCluster(v1.ClusterConfig)
Service->>Convert: convertAPIClusterToStorage(config)
Convert-->>Service: storage.Cluster
Service->>Storage: AddCluster(storage.Cluster)
Storage-->>Service: id
Service->>Storage: GetCluster(id)
Storage-->>Service: storage.Cluster
Service->>Convert: convertStorageClusterToAPI(storage.Cluster)
Convert-->>Service: v1.ClusterConfig
Service-->>Client: ClusterResponse(v1.ClusterConfig)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes 🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (2)
roxctl/sensor/generate/k8s.go (1)
33-35: Keep validation and submission in lockstep.Roxctl now validates a synthesized
storage.Cluster, but later submits the originalv1.ClusterConfig. That adds a second API↔storage mapping alongside the server-side converter, so any missed field inclusterConfigToStoragewill make local validation disagree with what Central actually receives. Please either share the mapping or add exhaustive parity tests around the converter.As per coding guidelines, focus on major issues impacting performance, readability, maintainability and security.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@roxctl/sensor/generate/k8s.go` around lines 33 - 35, Validation uses clusterConfigToStorage(k8sCommand.cluster) but the code later submits the original v1.ClusterConfig, causing possible mismatches; convert once and keep in lockstep by validating and submitting the same storage.Cluster instance returned by clusterConfigToStorage(k8sCommand.cluster) (or else validate the v1 type using the exact server-side converter), and add exhaustive parity tests for clusterConfigToStorage to ensure every field is mapped identically to the server converter; refer to clusterValidation.ValidatePartial, clusterConfigToStorage, and k8sCommand.cluster when making this change.proto/api/v1/cluster_service.proto (1)
26-94: Add a descriptor-parity test to guard against future drift.
ClusterConfigis currently field-number-identical tostorage.Cluster, but there is no automated check preventing this contract from diverging. A future refactoring could alter field numbers, types, or reserved ranges in one message without catching the mismatch, breaking version-skewed Central/roxctl/Sensor interop at runtime. Adding a presubmit or CI check that validates structural parity would catch such regressions early.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@proto/api/v1/cluster_service.proto` around lines 26 - 94, Add a descriptor-parity presubmit/CI test that verifies ClusterConfig is field-number-identical to storage.Cluster: implement a test (e.g., TestClusterConfigDescriptorParity) that loads the protobuf descriptors for message names "ClusterConfig" and "storage.Cluster" and asserts equality of each field's number, name, type, label (singular/repeated), and reserved tag ranges; also assert parity for map fields (labels, audit_log_state) and repeated sensor_capabilities and that reserved tags (e.g., reserved 6,8,9-12,14) match. Hook this test into the existing proto test suite so any future changes to ClusterConfig or storage.Cluster fail CI until explicitly reconciled.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@proto/api/v1/cluster_service.proto`:
- Around line 26-94: Add a descriptor-parity presubmit/CI test that verifies
ClusterConfig is field-number-identical to storage.Cluster: implement a test
(e.g., TestClusterConfigDescriptorParity) that loads the protobuf descriptors
for message names "ClusterConfig" and "storage.Cluster" and asserts equality of
each field's number, name, type, label (singular/repeated), and reserved tag
ranges; also assert parity for map fields (labels, audit_log_state) and repeated
sensor_capabilities and that reserved tags (e.g., reserved 6,8,9-12,14) match.
Hook this test into the existing proto test suite so any future changes to
ClusterConfig or storage.Cluster fail CI until explicitly reconciled.
In `@roxctl/sensor/generate/k8s.go`:
- Around line 33-35: Validation uses clusterConfigToStorage(k8sCommand.cluster)
but the code later submits the original v1.ClusterConfig, causing possible
mismatches; convert once and keep in lockstep by validating and submitting the
same storage.Cluster instance returned by
clusterConfigToStorage(k8sCommand.cluster) (or else validate the v1 type using
the exact server-side converter), and add exhaustive parity tests for
clusterConfigToStorage to ensure every field is mapped identically to the server
converter; refer to clusterValidation.ValidatePartial, clusterConfigToStorage,
and k8sCommand.cluster when making this change.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Central YAML (base), Organization UI (inherited)
Review profile: CHILL
Plan: Pro
Run ID: 0cfeadff-3685-4ba7-9dce-7e5c7151ca09
⛔ Files ignored due to path filters (6)
generated/api/v1/cluster_service.pb.gois excluded by!**/*.pb.go,!**/generated/**generated/api/v1/cluster_service.pb.gw.gois excluded by!**/*.pb.gw.go,!**/generated/**generated/api/v1/cluster_service.swagger.jsonis excluded by!**/generated/**generated/api/v1/cluster_service_grpc.pb.gois excluded by!**/*.pb.go,!**/generated/**generated/api/v1/cluster_service_vtproto.pb.gois excluded by!**/*.pb.go,!**/generated/**generated/internalapi/central/v1/token_service.pb.gois excluded by!**/*.pb.go,!**/generated/**
📒 Files selected for processing (13)
central/cluster/service/convert.gocentral/cluster/service/service_impl.goconfig-controller/pkg/client/client.goconfig-controller/pkg/client/client_test.goconfig-controller/pkg/client/mocks/client.goproto/api/v1/cluster_service.protoroxctl/cluster/delete/delete.goroxctl/cluster/delete/delete_test.goroxctl/sensor/generate/convert.goroxctl/sensor/generate/generate.goroxctl/sensor/generate/generate_test.goroxctl/sensor/generate/k8s.goroxctl/sensor/generate/openshift.go
🚀 Build Images ReadyImages are ready for commit 38e59a0. To use with deploy scripts: export MAIN_IMAGE_TAG=4.11.x-560-g38e59a0dc9 |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@central/cluster/service/convert_test.go`:
- Around line 65-67: The test sets AuditLogState in the input fixture but never
verifies it in the "AllFields" test; update the TestAllFields (or the test
function that builds the input struct in convert_test.go) to assert that the
converted output contains the same AuditLogState map (keys and
storage.AuditLogFileState values). Use the same comparison style used elsewhere
in the test (e.g. require.Equal/reflect.DeepEqual/cmp) to compare the expected
AuditLogState against the actual result from the conversion function,
referencing the AuditLogState field and storage.AuditLogFileState type so future
regressions are caught.
🪄 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: Central YAML (base), Organization UI (inherited)
Review profile: CHILL
Plan: Pro
Run ID: 35e087e5-4f43-4841-b55c-394ed16ccd10
📒 Files selected for processing (2)
central/cluster/service/convert_test.goroxctl/sensor/generate/convert_test.go
| AuditLogState: map[string]*storage.AuditLogFileState{ | ||
| "node1": {CollectLogsSince: nil}, | ||
| }, |
There was a problem hiding this comment.
AuditLogState is populated but never asserted in the “AllFields” test.
Line 65 sets AuditLogState, but the verification block does not check it. That leaves this field unprotected against conversion regressions.
Suggested test fix
@@
protoassert.Equal(t, cluster.GetMostRecentSensorId(), result.GetMostRecentSensorId())
+ assert.Equal(t, cluster.GetAuditLogState(), result.GetAuditLogState())
assert.Equal(t, cluster.GetInitBundleId(), result.GetInitBundleId())
assert.Equal(t, cluster.GetSensorCapabilities(), result.GetSensorCapabilities())Also applies to: 74-97
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@central/cluster/service/convert_test.go` around lines 65 - 67, The test sets
AuditLogState in the input fixture but never verifies it in the "AllFields"
test; update the TestAllFields (or the test function that builds the input
struct in convert_test.go) to assert that the converted output contains the same
AuditLogState map (keys and storage.AuditLogFileState values). Use the same
comparison style used elsewhere in the test (e.g.
require.Equal/reflect.DeepEqual/cmp) to compare the expected AuditLogState
against the actual result from the conversion function, referencing the
AuditLogState field and storage.AuditLogFileState type so future regressions are
caught.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #19824 +/- ##
==========================================
+ Coverage 49.60% 49.62% +0.02%
==========================================
Files 2763 2765 +2
Lines 208271 208435 +164
==========================================
+ Hits 103312 103436 +124
- Misses 97292 97333 +41
+ Partials 7667 7666 -1
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Introduce a dedicated v1.ClusterConfig message in cluster_service.proto that is field-number-identical to storage.Cluster, ensuring full gRPC wire and REST/JSON backward compatibility across version-skewed Central/roxctl/Sensor deployments.
Co-Authored-By: Claude Opus 4.6 noreply@anthropic.com
User-facing documentation
Testing and quality
Automated testing
How I validated my change