-
Notifications
You must be signed in to change notification settings - Fork 52
Implement RegisterEntity generic API endpoint #5959
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
20b5b70 to
2ed5dc3
Compare
Implements the RegisterEntity gRPC endpoint to provide a unified, synchronous API for registering any entity type (repositories, releases, artifacts, pull requests) in Minder. This change extracts common entity creation logic into a reusable EntityCreator service that is used by both synchronous (RegisterEntity) and asynchronous (webhook-based) entity registration flows. Key changes: - Add RegisterEntity RPC handler with generic entity creation - Create EntityCreator service to unify entity creation logic - Implement pluggable validator framework (RepositoryValidator) - Refactor RepositoryService to use EntityCreator (reduced from ~90 to ~30 lines) - Refactor async entity handler to use EntityCreator - Update proto to use google.protobuf.Struct for type-safe properties - Add comprehensive test coverage (27 new tests) Security improvements: - Input validation for property count (max 100) and key length (max 200) - Context cancellation protection in cleanup operations - Improved error wrapping for better debugging The implementation maintains backward compatibility with existing RegisterRepository RPC while providing a foundation for registering other entity types through a single unified API. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
2ed5dc3 to
5760cfa
Compare
evankanderson
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry about the delay, it took a little while to go over the related code and understand what was going on here.
Despite the number of comments, I'm pretty bullish on this change -- thanks for doing it!
| if errors.Is(err, validators.ErrPrivateRepoForbidden) || | ||
| errors.Is(err, validators.ErrArchivedRepoForbidden) { | ||
| return nil, util.UserVisibleError(codes.InvalidArgument, "%s", err.Error()) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If err is already a UserVisibleError, we should just pass it back. That will allow providers and EntityCreator to add further errors without needing to keep updating this allow-list of user-visible errors.
| projectDeleter projects.ProjectDeleter, | ||
| projectCreator projects.ProjectCreator, | ||
| entityService entitySvc.EntityService, | ||
| entityCreator entitySvc.EntityCreator, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is entityCreator separate from entityService?
In particular, it seems like we might want sub-interfaces like EntityCreator and EntityReader for mocks / other parts, but it feels like there should be a rolled-up interface that can do all the CRUD operations.
| // identifying_properties uniquely identifies the entity in the provider. | ||
| // For example, for a GitHub repository use github/repo_owner and github/repo_name, | ||
| // or use upstream_id to identify by provider's internal ID. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How do users discover the appropriate property? Is it expected that there are several different possible properties that might be combined to locate an entity (e.g. region=us-east-1 and account=231571814 and registry=ecr.us-east-1/myname and image=abc)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm guessing that this would be handled by per-provider and per-entity_type documentation?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we simplify this to a map<string, string>? Struct is a very broad interface.
| propSvc, | ||
| providerManager, | ||
| evt, | ||
| []entityService.EntityValidator{repoValidator}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should the providerManager provide the validators? I'm just thinking that e.g. there might be different providers for the same entity type which use different parameters (e.g. an AWS API probably has a region component, while DockerHub or GHCR.io do not).
| // Validate reasonable property count to prevent resource exhaustion | ||
| const maxPropertyCount = 100 | ||
| if len(propsMap) > maxPropertyCount { | ||
| return nil, fmt.Errorf("too many identifying properties: got %d, max %d", | ||
| len(propsMap), maxPropertyCount) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you're using a Struct, a single key could contain an arbitrarily large value. It may be simpler to use proto.Size(req.GetIdentifyingProperties()) to establish an upper bound. (Or use the suggestion of a map<string, string>)
| ewp.Properties = props | ||
|
|
||
| // insert the repository into the DB | ||
| dbID, pbRepo, err := r.persistRepository(ctx, ewp) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
persistRepository is only used by this function. Remove it?
| ctx, | ||
| entMsg.Originator.Type, entMsg.Originator.GetByProps, entMsg.Hint, | ||
| a.propSvc, | ||
| nil) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was in a transaction and now isn't. Is that safe (I don't know -- it might well be)?
It looks like we're doing a bunch of reads (parent entity, provider ID) and then starting a transaction which writes data based on the reads. I don't have a strong sense of the semantics (if any) we need here, though.
| return nil, fmt.Errorf("error converting properties to proto message: %w", err) | ||
| } | ||
|
|
||
| legacyId, err := a.upsertLegacyEntity(ctx, entMsg.Entity.Type, parentEwp, pbEnt, t) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
upsertLegacyEntity is a no-op which is also now unused. Remove it?
| &entityService.EntityCreationOptions{ | ||
| OriginatingEntityID: &parentEwp.Entity.ID, | ||
| RegisterWithProvider: false, // No webhooks for child entities | ||
| PublishReconciliationEvent: false, // No reconciliation for child entities |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not reconcile the child entities? Is that simply because we didn't publish the events before?
It looks like maybe the GetEntity is called from a message handler already, so we're trying to avoid a loop?
| } | ||
|
|
||
| // Save properties | ||
| if err := e.propSvc.SaveAllProperties(ctx, ent.ID, registeredProps, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CreateRepository previously called ReplaceAllProperties, while AddOriginatingEntityStrategy called SaveAllProperties without the delete in Replace.
Replace feels more correct (and we could make "save" internal). WDYT?
|
OK, I somehow missed that you had reviewed this! I'll get back to this. Sorry about that |
Summary
Implements the
RegisterEntitygRPC endpoint to provide a unified, synchronous API for registering any entity type (repositories, releases, artifacts, pull requests) in Minder.This PR extracts common entity creation logic into a reusable
EntityCreatorservice that eliminates code duplication between synchronous and asynchronous entity registration flows.Key Changes
Core Implementation
POST /api/v1/entityinternal/entities/service/entity_creator.go)RepositoryValidatorwith extensible designidentifier_propertyfromstringtogoogle.protobuf.Structfor type safetyRefactoring
Security Improvements
Test Coverage
Added 27 new tests across 5 test files:
entity_creator_simple_test.go- Provider validation tests (4 tests)repository_validator_test.go- Validator logic tests (6 tests)handlers_entity_instances_test.go- RegisterEntity handler tests (12 tests)service_integration_test.go- RepositoryService integration tests (5 tests)All tests passing ✅
Benefits
Backward Compatibility
✅ Fully backward compatible
RegisterRepositoryRPC continues to work unchangedCode Review Notes
Both automated code quality and security reviews were conducted:
🤖 Generated with Claude Code