Skip to content

Commit

Permalink
Add EntityWithPropertiesByUpstreamHint
Browse files Browse the repository at this point in the history
When processing a webhook payload, we typically only have the
information from the webhook payload available which is to say
information about the upstream entity. We even don't know exactly which
provider and which project are we handling.

We used to sort of wing it in the github webhook handler and were just
searching by upstream ID as if it was globally unique, but that no
longer works with the introduction of multiple providers where several
entities might have the same upstream ID or the same name, just in different providers.

To handle that, we add a new method to the properties service that
searches by upstream properties with a hint. At the moment, the interface
supports providerID, projectID and provider implements as a hint.

The immediate use is that the github provider will search by the
upstream ID with the hint set to ProviderTypeGithub as that's
implemented both by the app and OAuth github providers.

If any attribute of the hint is set, then the entity's attribute must
match the hint, otherwise the entity is filtered out. Additionally, once
the entity is found, the hint might contain a property with a value that
must match. The use-case there would be deleting entities where we want
to ensure that we are deleting an entity with an appropriate upstream
hook ID.

If no entities match the hints or if multiple entities match a hint, an
error is returned.

Related: stacklok#4327
  • Loading branch information
jhrozek committed Sep 19, 2024
1 parent 7e9c9d7 commit 1e1f715
Show file tree
Hide file tree
Showing 4 changed files with 592 additions and 45 deletions.
183 changes: 170 additions & 13 deletions internal/entities/properties/service/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"database/sql"
"errors"
"fmt"
"slices"
"time"

"github.com/google/uuid"
Expand Down Expand Up @@ -143,35 +144,191 @@ func getEntityIdByName(
return ent.ID, nil
}

func getEntityIdByUpstreamID(
ctx context.Context, projectId uuid.UUID,
func getAllByProperty(
ctx context.Context,
propName string,
propVal any,
entType minderv1.Entity,
projectID uuid.UUID,
providerID uuid.UUID,
upstreamID string, entType minderv1.Entity,
qtx db.ExtendQuerier,
) (uuid.UUID, error) {
) ([]db.EntityInstance, error) {
l := zerolog.Ctx(ctx).With().Any(propName, propVal).Logger()

ents, err := qtx.GetTypedEntitiesByPropertyV1(
ctx,
entities.EntityTypeToDB(entType),
properties.PropertyUpstreamID,
upstreamID,
propName,
propVal,
db.GetTypedEntitiesOptions{
ProjectID: projectId,
ProjectID: projectID,
ProviderID: providerID,
})
if errors.Is(err, sql.ErrNoRows) {
return uuid.Nil, ErrEntityNotFound
return nil, ErrEntityNotFound
} else if err != nil {
return uuid.Nil, fmt.Errorf("error fetching entities by property: %w", err)
return nil, fmt.Errorf("error fetching entities by property: %w", err)
}

if len(ents) == 0 {
l.Debug().Msg("no entity found")
return nil, ErrEntityNotFound
}

return ents, nil
}

func getEntityIdByUpstreamID(
ctx context.Context,
projectID uuid.UUID, providerID uuid.UUID,
upstreamID string, entType minderv1.Entity,
qtx db.ExtendQuerier,
) (uuid.UUID, error) {
ents, err := getAllByProperty(ctx, properties.PropertyUpstreamID, upstreamID, entType, projectID, providerID, qtx)
if err != nil {
return uuid.Nil, err
}

if len(ents) > 1 {
return uuid.Nil, ErrMultipleEntities
} else if len(ents) == 1 {
return ents[0].ID, nil
}

// no entity found
return uuid.Nil, ErrEntityNotFound
return ents[0].ID, nil
}

func matchEntityWithHint(
ctx context.Context,
props *properties.Properties,
entType minderv1.Entity,
hint *ByUpstreamHint,
l zerolog.Logger,
qtx db.ExtendQuerier,
) (*db.EntityInstance, error) {
if !hint.isSet() {
return nil, fmt.Errorf("at least one of projectID, providerID or providerImplements must be set in hint")
}

var ents []db.EntityInstance
var err error

lookupOrder := []string{properties.PropertyUpstreamID, properties.PropertyName}
for _, loopupProp := range lookupOrder {
prop := props.GetProperty(loopupProp)
if prop == nil {
continue
}

l.Debug().Str("lookupProp", loopupProp).Msg("fetching by property")
ents, err = getAllByProperty(ctx, loopupProp, prop.RawValue(), entType, hint.projectID, hint.providerID, qtx)
if errors.Is(err, ErrEntityNotFound) {
l.Debug().Str("lookupProp", loopupProp).Msg("no entity found")
continue
} else if err != nil {
return nil, fmt.Errorf("failed to get entities by upstream ID: %w", err)
}

match, err := findMatchByUpstreamHint(ctx, ents, hint, qtx)
if err != nil {
if errors.Is(err, ErrEntityNotFound) {
l.Error().Msg("no entity matched")
continue
} else if errors.Is(err, ErrMultipleEntities) {
l.Error().Msg("multiple entities matched")
return nil, ErrMultipleEntities
}
return nil, fmt.Errorf("failed to match entity by hint: %w", err)
}
return match, nil
}

return nil, ErrEntityNotFound
}

func findMatchByUpstreamHint(
ctx context.Context, ents []db.EntityInstance, hint *ByUpstreamHint, qtx db.ExtendQuerier,
) (*db.EntityInstance, error) {
var match *db.EntityInstance
for _, ent := range ents {
var thisMatch *db.EntityInstance
zerolog.Ctx(ctx).Debug().Msgf("matching entity %s", ent.ID.String())
if dbEntMatchesUpstreamHint(ctx, ent, hint, qtx) {
zerolog.Ctx(ctx).Debug().Msgf("entity %s matched by hint", ent.ID.String())
thisMatch = &ent
}

if thisMatch != nil {
if match != nil {
zerolog.Ctx(ctx).Error().Msg("multiple entities matched")
return nil, ErrMultipleEntities
}
match = thisMatch
}
}

if match == nil {
zerolog.Ctx(ctx).Debug().Msg("no entity matched")
return nil, ErrEntityNotFound
}

return match, nil
}

func dbEntMatchesUpstreamHint(ctx context.Context, ent db.EntityInstance, hint *ByUpstreamHint, qtx db.ExtendQuerier) bool {
logger := zerolog.Ctx(ctx)

if hint.projectID != uuid.Nil {
if ent.ProjectID != hint.projectID {
logger.Debug().
Str("projectID", ent.ProjectID.String()).
Str("hintProjectID", hint.projectID.String()).
Msg("project mismatch")
return false
}
}

if hint.providerID != uuid.Nil {
if ent.ProviderID != hint.providerID {
logger.Debug().
Str("providerID", ent.ProviderID.String()).
Str("hintProviderID", hint.providerID.String()).
Msg("provider mismatch")
return false
}
}

if hint.ProviderImplements.Valid {
dbProv, err := qtx.GetProviderByID(ctx, ent.ProviderID)
if err != nil {
logger.Error().
Str("providerID", ent.ProviderID.String()).
Err(err).
Msg("error getting provider by ID")
return false
}

if !slices.Contains(dbProv.Implements, hint.ProviderImplements.ProviderType) {
logger.Debug().
Str("ProviderID", ent.ProviderID.String()).
Str("providerType", string(hint.ProviderImplements.ProviderType)).
Msg("provider does not implement hint")
return false
}
}

return true
}

func propsMatchUpstreamHint(props *properties.Properties, hint ByUpstreamHint) bool {
if hint.PropName == "" {
return true
}

prop := props.GetProperty(hint.PropName)
if prop == nil {
return false
}

return prop.RawValue() == hint.PropValue
}

func (ps *propertiesService) areDatabasePropertiesValid(
Expand Down
29 changes: 22 additions & 7 deletions internal/entities/properties/service/mock/service.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 1e1f715

Please sign in to comment.