Skip to content

Commit

Permalink
Consider project hierarchy in profile evaluation (#3499)
Browse files Browse the repository at this point in the history
This makes it so that profiles now take into account the project
hierarchy when evaluating for an entity. That is, profiles that apply to
the entity via the hierarchy (same project and parents) are evaluated
when the executor gets an event.

This iterates in steps, so the profile may only use rule types that
exist in its same project. A Further iteration will also take the
hierarchy into account when searching for rule types.

Note that hierarchy operations are still under a feature flag, so this
is not generally available.

Signed-off-by: Juan Antonio Osorio <ozz@stacklok.com>
  • Loading branch information
JAORMX authored Jun 3, 2024
1 parent daa6d81 commit 3afa50e
Show file tree
Hide file tree
Showing 2 changed files with 73 additions and 33 deletions.
102 changes: 69 additions & 33 deletions internal/engine/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,49 +177,79 @@ func (e *Executor) evalEntityEvent(ctx context.Context, inf *entities.EntityInfo

defer e.releaseLockAndFlush(ctx, inf)

// Get profiles relevant to project
dbpols, err := e.querier.ListProfilesByProjectID(ctx, inf.ProjectID)
if err != nil {
return fmt.Errorf("error getting profiles: %w", err)
}

for _, profile := range MergeDatabaseListIntoProfiles(dbpols) {
// Get only these rules that are relevant for this entity type
relevant, err := GetRulesForEntity(profile, inf.Type)
if err != nil {
return fmt.Errorf("error getting rules for entity: %w", err)
}

// Let's evaluate all the rules for this profile
err = TraverseRules(relevant, func(rule *pb.Profile_Rule) error {
// Get the engine evaluator for this rule type
evalParams, rte, err := e.getEvaluator(ctx, inf, provider, profile, rule, ingestCache)
err = e.forProjectsInHierarchy(
ctx, inf, func(ctx context.Context, profile *pb.Profile, hierarchy []uuid.UUID) error {
// Get only these rules that are relevant for this entity type
relevant, err := GetRulesForEntity(profile, inf.Type)
if err != nil {
return err
return fmt.Errorf("error getting rules for entity: %w", err)
}

// Update the lock lease at the end of the evaluation
defer e.updateLockLease(ctx, *inf.ExecutionID, evalParams)
// Let's evaluate all the rules for this profile
err = TraverseRules(relevant, func(rule *pb.Profile_Rule) error {
// Get the engine evaluator for this rule type
evalParams, rte, err := e.getEvaluator(
ctx, inf, provider, profile, rule, hierarchy, ingestCache)
if err != nil {
return err
}

// Evaluate the rule
evalParams.SetEvalErr(rte.Eval(ctx, inf, evalParams))
// Update the lock lease at the end of the evaluation
defer e.updateLockLease(ctx, *inf.ExecutionID, evalParams)

// Perform actions, if any
evalParams.SetActionsErr(ctx, rte.Actions(ctx, inf, evalParams))
// Evaluate the rule
evalParams.SetEvalErr(rte.Eval(ctx, inf, evalParams))

// Log the evaluation
logEval(ctx, inf, evalParams)
// Perform actions, if any
evalParams.SetActionsErr(ctx, rte.Actions(ctx, inf, evalParams))

// Create or update the evaluation status
return e.createOrUpdateEvalStatus(ctx, evalParams)
// Log the evaluation
logEval(ctx, inf, evalParams)

// Create or update the evaluation status
return e.createOrUpdateEvalStatus(ctx, evalParams)
})

if err != nil {
p := profile.Name
if profile.Id != nil {
p = *profile.Id
}
return fmt.Errorf("error traversing rules for profile %s: %w", p, err)
}

return nil
})

if err != nil {
return fmt.Errorf("error evaluating entity event: %w", err)
}

return nil
}

func (e *Executor) forProjectsInHierarchy(
ctx context.Context,
inf *entities.EntityInfoWrapper,
f func(context.Context, *pb.Profile, []uuid.UUID) error,
) error {
projList, err := e.querier.GetParentProjects(ctx, inf.ProjectID)
if err != nil {
return fmt.Errorf("error getting parent projects: %w", err)
}

for idx, projID := range projList {
projectHierarchy := projList[idx:]
// Get profiles relevant to project
dbpols, err := e.querier.ListProfilesByProjectID(ctx, projID)
if err != nil {
p := profile.Name
if profile.Id != nil {
p = *profile.Id
return fmt.Errorf("error getting profiles: %w", err)
}

for _, profile := range MergeDatabaseListIntoProfiles(dbpols) {
if err := f(ctx, profile, projectHierarchy); err != nil {
return err
}
return fmt.Errorf("error traversing rules for profile %s: %w", p, err)
}
}

Expand All @@ -232,6 +262,7 @@ func (e *Executor) getEvaluator(
provider provinfv1.Provider,
profile *pb.Profile,
rule *pb.Profile_Rule,
hierarchy []uuid.UUID,
ingestCache ingestcache.Cache,
) (*engif.EvalStatusParams, *RuleTypeEngine, error) {
// Create eval status params
Expand All @@ -240,11 +271,16 @@ func (e *Executor) getEvaluator(
return nil, nil, fmt.Errorf("error creating eval status params: %w", err)
}

// NOTE: We're only using the first project in the hierarchy for now.
// This means that a rule type must exist in the same project as the profile.
// This will be revisited in the future.
projID := hierarchy[0]

// Load Rule Class from database
// TODO(jaosorior): Rule types should be cached in memory so
// we don't have to query the database for each rule.
dbrt, err := e.querier.GetRuleTypeByName(ctx, db.GetRuleTypeByNameParams{
ProjectID: inf.ProjectID,
ProjectID: projID,
Name: rule.Type,
})
if err != nil {
Expand Down
4 changes: 4 additions & 0 deletions internal/engine/executor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,10 @@ func TestExecutor_handleEntityEvent(t *testing.T) {
marshalledCRS, err := json.Marshal(crs)
require.NoError(t, err, "expected no error")

mockStore.EXPECT().
GetParentProjects(gomock.Any(), projectID).
Return([]uuid.UUID{projectID}, nil)

mockStore.EXPECT().
ListProfilesByProjectID(gomock.Any(), projectID).
Return([]db.ListProfilesByProjectIDRow{
Expand Down

0 comments on commit 3afa50e

Please # to comment.