Skip to content

refactor: reduce db type coupling in engine actions using adapter helpers#6274

Open
sachin9058 wants to merge 15 commits into
mindersec:mainfrom
sachin9058:refactor-actions-db-decoupling
Open

refactor: reduce db type coupling in engine actions using adapter helpers#6274
sachin9058 wants to merge 15 commits into
mindersec:mainfrom
sachin9058:refactor-actions-db-decoupling

Conversation

@sachin9058
Copy link
Copy Markdown
Contributor

Summary

This PR refactors the internal/engine/actions package to reduce direct coupling with database-specific types.

Previously, engine logic directly compared db status enums (e.g., db.EvalStatusTypes, db.AlertStatusTypes, db.RemediationStatusTypes). This created tight coupling between the engine layer and the database layer.

This PR replaces those comparisons with helper functions in internal/adapters/db, such as:

  • IsEvalFailure
  • IsEvalSuccess
  • IsEvalError
  • IsRemediationSkipped
  • IsAlertOn
  • IsAlertOff

This improves separation of concerns and continues the incremental effort to decouple engine components from database representations.

Additionally, unit tests were added for the adapter helper functions to ensure correctness and maintain coverage.

No additional dependencies are introduced.

Fixes #NONE

Testing

  • Ran full test suite:
    make test

  • Verified build:
    make build-minder-cli

  • Verified lint:
    make lint-fix

  • Added unit tests in:
    internal/adapters/db/errors_test.go

All tests pass and no regressions were observed.

@sachin9058 sachin9058 requested a review from a team as a code owner April 4, 2026 09:47
@coveralls
Copy link
Copy Markdown

coveralls commented Apr 4, 2026

Coverage Status

coverage: 59.299% (-0.02%) from 59.314% — sachin9058:refactor-actions-db-decoupling into mindersec:main

@sachin9058
Copy link
Copy Markdown
Contributor Author

@evankanderson Hi! I’ve completed decoupling status handling in actions, eval_status, and executor using dbadapter.

Would you prefer continuing incremental cleanup in other components, or should I start introducing engine-level domain types to further decouple from database representations?

@evankanderson
Copy link
Copy Markdown
Member

@evankanderson Hi! I’ve completed decoupling status handling in actions, eval_status, and executor using dbadapter.

Would you prefer continuing incremental cleanup in other components, or should I start introducing engine-level domain types to further decouple from database representations?

I would probably try to "peel the onion" by pulling more engine implementation out into pkg/engine, cleaning up leaking database or other abstractions when you would end up adding "unusual" dependencies to pkg/engine. The end goal would be to be able to import pkg/engine and evaluate (without remediation) a Minder policy without needing a database or abstractions that aren't available in pkg (so "provider" is fine, because it's an interface, but internal OCI handling wouldn't be unless it's behind a provider interface).

@sachin9058
Copy link
Copy Markdown
Contributor Author

Got it, that helps a lot. I’ll continue incrementally extracting logic into pkg/engine, cleaning up any leaking DB/internal dependencies along the way, and keeping the engine focused on pure evaluation with interfaces where needed. I’ll work toward making it runnable without a database and share updates as I go.

Copy link
Copy Markdown
Member

@evankanderson evankanderson left a comment

Choose a reason for hiding this comment

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

This doesn't seem to change any of the evaluation logic, but I don't particularly see the maintenance / comprehension benefit of switching from switch/case statements (with comments explaining the rationale behind the choices) to if statements with early returns.

Additionally, by rewriting e.g. if conditions to put the value on the left, this PR is more likely to show up in git blame when someone is investigating the logic here, and then come ask you questions about logic that you didn't change. 😁 I'm sure that you'll later come and touch these when you remove the use of the DB enum, but I'm not sure that this PR adds enough value to merge. (That happens to me, too!)

Comment thread internal/engine/actions/actions.go Outdated
}

// shouldRemediate returns the action command for remediation taking into account previous evaluations
// ✅ FIXED (uses adapter + enum comparisons)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Preference for not using emoji in comments, sorry!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Also, the previous comment explained what the method was intended to do, while the new comment doesn't really explain why the code is the way it is. Preference for keeping the old function comments here (and elsewhere in the PR where they were deleted for internal visibility methods) -- while the linter doesn't require it, they can still be handy.

Comment thread internal/engine/actions/actions.go Outdated
func shouldRemediate(prevEvalFromDb *db.ListRuleEvaluationsByProfileIdRow, evalErr error) engif.ActionCmd {
// Get current evaluation status

newEval := dbadapter.ErrorAsEvalStatus(evalErr)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

For a future PR: it would be great to simply evaluate these (in the case statement) as error types, rather than converting to DB types for the comparison.

But not in this PR. 😁

// Case 1 - Do not try to be smart about it by doing nothing if the evaluation status has not changed

// Proceed with use cases where the evaluation changed
switch newEval {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

One advantage of switch/case over the current logic with if statements is that it is easier to verify you've covered all the cases. Since this block is still "thinking" in terms of DB enums, is there a particular benefit to the "nested-if-with-early-exit" style?

@sachin9058
Copy link
Copy Markdown
Contributor Author

Thanks, this is helpful.

You're right this change is mostly stylistic and doesn’t add much value. I’ll revert back to switch/case, restore the original comments.

For next steps, I’ll focus on moving toward error/domain-based evaluation and further decoupling in pkg/engine.

Let me know if you’d like me to update this PR or handle it in a follow-up.

@sachin9058
Copy link
Copy Markdown
Contributor Author

@evankanderson Thanks for the detailed feedback!

I’ve updated the PR to restore the switch/case structure, brought back more descriptive comments, and handled all enum cases to satisfy exhaustive checks. I’ve also removed the stylistic changes that didn’t add much value.

Let me know if this looks better now.

Comment on lines +124 to 145
// processAction safely executes an action
func (rae *RuleActionsEngine) processAction(
ctx context.Context,
actionType engif.ActionType,
cmd engif.ActionCmd,
ent protoreflect.ProtoMessage,
params engif.ActionsParams,
metadata *json.RawMessage,
) (jmsg json.RawMessage, finalErr error) {
) (json.RawMessage, error) {

defer func() {
if r := recover(); r != nil {
zerolog.Ctx(ctx).Error().Interface("recovered", r).
zerolog.Ctx(ctx).Error().
Interface("recovered", r).
Bytes("stack", debug.Stack()).
Msg("panic in action execution")
finalErr = enginerr.ErrInternal
}
}()
zerolog.Ctx(ctx).Debug().Str("action", string(actionType)).Str("cmd", string(cmd)).Msg("invoking action")
// Get action engine

action := rae.actions[actionType]
// Return the result of the action
return action.Do(ctx, cmd, ent, params, metadata)
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It looks like we're no longer returning an ErrInternal if the action.Do panics. (Yes, we should probably add a test to cover this.)

Comment on lines -158 to -160
// Case 2 - Evaluation changed from something else to ERROR -> Remediation should be OFF
// Case 3 - Evaluation changed from something else to PASSING -> Remediation should be OFF
// The Remediation should be OFF (if it wasn't already)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why are we removing these comments?

Comment thread internal/engine/actions/actions.go Outdated
case db.EvalStatusTypesSkipped:
case db.EvalStatusTypesPending:

case db.EvalStatusTypesSkipped, db.EvalStatusTypesPending:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is making me wonder if the original author was thinking of C-style switch/case when they wrote the original line 156 in #2833. Looking at the code there, I'm thinking that line 156/157 of the original should be case db.EvalStatusTypesError, db.EvalStatusTypesSuccess:.

(not excessively rewriting code makes it easier to do this kind of archaelogy with the "blame" view, e.g. https://github.com/mindersec/minder/blame/main/internal/engine/actions/actions.go#L158 leads to #2865, which references #2862. That, in turn, references #2833, which was the original change that seems to have caused a problem during the migration step.

Looking through #2833, it seems like commit a33f2f0 originally had the same logic for db.EvalStatusTypesError and db.EvalStatusTypesSuccess, and we attempted to collapse them into one case but did so incorrectly.

Thanks for nudging me to dig into the history of this code!

Comment thread internal/engine/actions/actions.go Outdated
Comment on lines 234 to 236
logger := zerolog.Ctx(ctx).Info().
Str("eval_status", string(dbadapter.ErrorAsEvalStatus(evalErr))).
Str("action", string(actionType))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It's weird to build this logger (actually a zerolog.Event) here to only use it on line 240. Maybe move it to that location?

Comment thread internal/engine/actions/actions.go Outdated
Comment on lines 300 to 275
// Even though meta is an empty json struct by default, there's no risk of overwriting
// any existing meta entry since we don't upsert in case of conflict while skipping
m, err := json.Marshal(&map[string]any{})
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This seems like a pretty wasteful way to write []byte("{}"). Feel free to fix it if you want (and then you can avoid needing to log anything, and then getDefaultResult doesn't need a ctx argument at all...).

@sachin9058
Copy link
Copy Markdown
Contributor Author

“Thanks for the feedback. I see that recent upstream changes have significantly altered the relevant parts of the codebase, and this PR may no longer align well with the current direction. I’ll pause here and revisit with a more aligned contribution.”

@evankanderson
Copy link
Copy Markdown
Member

Thanks for looking at the bigger picture here; you may also want to check your Claude / ChatGPT settings to encourage it to produce smaller diffs where possible -- there seems to be a lot of rewriting of existing comments and code which makes it more difficult for future maintainers to search backwards (using blame) to understand the context of existing code.

If you don't plan to continue with this branch, go ahead and close this PR (to help reduce the amount of noise in the PR review pane).

…s, and exhaustive handling)

- revert if-style logic back to switch/case for clarity and coverage
- restore meaningful function comments explaining intent
- handle all EvalStatusTypes cases to satisfy exhaustive lint
- remove unnecessary stylistic changes

No functional changes intended
- restore ErrInternal on panic in processAction
- simplify logger usage in isSkippable
- replace empty JSON marshal with static RawMessage
- keep changes minimal and preserve existing behavior
@sachin9058 sachin9058 force-pushed the refactor-actions-db-decoupling branch from 8dc40a5 to af3b673 Compare April 10, 2026 20:44
Copy link
Copy Markdown
Member

@evankanderson evankanderson left a comment

Choose a reason for hiding this comment

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

I'm marking this PR as "request changes" (lint error and excessive comment rewriting) to help track which outstanding PRs need maintainer action vs contributor action.

Alternatively, if you feel like this PR is no longer as useful, feel free to close it.

"github.com/rs/zerolog"
"google.golang.org/protobuf/reflect/protoreflect"

"strings"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This standard library import needs to be with the other standard library imports for lint.

)

// ErrorAsEvalStatus returns the evaluation status for a given error
// ErrorAsEvalStatus returns the evaluation status for a given error.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

There are a lot of these spurious (e.g. "type I - formatting") changes mixed in with the "type II - refactoring" changes. As mentioned previously; if you feel strongly about the type I changes (as opposed to simply having an over-active LLM configuration), please move them to a separate PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants