Skip to content

Gate rule type engine construction on provider_traits - #6702

Open
intelligent-ears wants to merge 12 commits into
mindersec:mainfrom
intelligent-ears:feat/rtengine-provider-traits
Open

Gate rule type engine construction on provider_traits#6702
intelligent-ears wants to merge 12 commits into
mindersec:mainfrom
intelligent-ears:feat/rtengine-provider-traits

Conversation

@intelligent-ears

Copy link
Copy Markdown
Member

Summary

Widens the gating logic in NewRuleTypeEngine so a rule type carrying provider_traits (added in #6669) is rejected up front — at engine construction time — if the provider doesn't implement every listed trait. This mirrors the existing pattern used for entity-context mismatches (reject at construction, not via a runtime ErrEvaluationSkipped), per the issue.

The trait check is a package-local interface in pkg/engine/v1/rtengine/engine.go:

type traitProvider interface {
    CanImplement(trait minderv1.ProviderType) bool
}

rather than a new method on pkg/engine/v1/interfaces.Provider. The original design called for widening interfaces.Provider directly, but that creates an import cycle: pkg/engine/v1/interfaces can't import minderv1 because minderv1 (via its hand-written datasources.go) imports pkg/datasources/v1, which imports pkg/engine/v1/interfaces. Declaring traitProvider locally in
rtengine and type-asserting against it avoids the cycle; every concrete provider already implements CanImplement with this exact signature, so they satisfy it structurally with zero provider-side changes.

pkg/testkit/v1.TestKit.CanImplement was hardcoded to always return true; added a WithCanImplement functional option so tests can control it per case, defaulting to true so existing callers (including pkg/ruletest/eval.go) are unaffected.

Part of #6650, closes #6652.

Test plan

  • go build ./pkg/engine/... ./internal/providers/...
  • go test ./pkg/engine/v1/rtengine/... -v
  • gofmt -l on all touched files (clean)

Reject rule types at construction time when the provider does not
implement all traits listed in the rule type's provider_traits, the
same way entity-type mismatches are already handled up front rather
than via a runtime ErrEvaluationSkipped.

The trait check is declared as a package-local interface in rtengine
rather than added to interfaces.Provider: that package cannot import
minderv1 without creating an import cycle (minderv1 transitively
imports pkg/datasources/v1, which imports pkg/engine/v1/interfaces).
Every concrete provider already implements CanImplement with this
exact signature, so they satisfy the interface structurally with no
provider-side changes.
Extend TestKit with a configurable CanImplement (via WithCanImplement),
defaulting to true so existing callers are unaffected, and add cases
to engine_test.go covering: no provider_traits (unaffected), all
required traits satisfied (succeeds), and a missing trait (fails with
a clear error).
@intelligent-ears
intelligent-ears requested a review from a team as a code owner August 15, 2026 07:27
@coveralls

Copy link
Copy Markdown

Coverage Status

coverage: 61.708% (+0.005%) from 61.703% — intelligent-ears:feat/rtengine-provider-traits into mindersec:main

@evankanderson evankanderson left a comment

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.

Thanks for adding support in TestKit! Can you add some provider_traits to the tests in pkg/ruletest/testdata, as that's where the mindev test integration tests live.

Comment thread pkg/engine/v1/interfaces/provider.go Outdated
Comment on lines +18 to +24
// Note: this interface intentionally does not depend on
// github.com/mindersec/minder/pkg/api/protobuf/go/minder/v1 (minderv1). That
// package transitively imports pkg/datasources/v1, which imports this
// package for interfaces.Ingested — so importing minderv1 here would create
// an import cycle. Callers that need a minderv1-typed method (e.g.
// CanImplement) should type-assert against a package-local interface
// instead, the way rtengine does.

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.

Hmm -- I feel like I'd rather have the protobuf package not depend on the pkg/datasources/v1, but have the dependency go the other way.

What do you think about moving the two constants to the protobuf package to avoid the loop (and then add the appropriate methods to this interface)?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Agreed, and it turned out to be a one-line dependency — minderv1's hand-written datasources.go imported pkg/datasources/v1 only so GetDriverType() could return those two constants. Moved them into minderv1 (d16e01e) with deprecated aliases left behind in pkg/datasources/v1, since that's a public package. With the cycle gone, interfaces.Provider now declares CanImplement(minderv1.ProviderType) bool directly and the package-local traitProvider interface plus its explanatory comment are deleted (da95a73).

One thing to flag: interfaces.Provider was previously empty, so this is its first method and technically a breaking change for any out-of-tree implementer. Everything in-tree already satisfies it, including TestKit.

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.

We don't guarantee API compatibility on interfaces.Provider, so anyone out of tree needs to be able to deal with this sort of API breakage. Thanks for thinking of it, though!

Comment thread pkg/engine/v1/rtengine/engine.go Outdated
return nil, fmt.Errorf("rule type context must have a project")
}

if traits := ruletype.GetDef().GetProviderTraits(); len(traits) > 0 {

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.

I don't think you need the check for len(traits) > 0; if you simply write range ruletype.GetDef().GetProviderTraits(), the loop will execute 0 times if no traits were declared (and then will be no CanImplement checks).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Removed in da95a73 — you're right, the getters are nil-safe and range over a nil slice is a no-op, so the guard wasn't buying anything.

Comment thread pkg/engine/v1/rtengine/engine.go Outdated
Comment on lines +88 to +90
return nil, fmt.Errorf(
"provider does not implement required trait %s for rule type %s",
trait.String(), ruletype.Name)

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.

What happens in a running Minder instance if this returns error repeatedly? I suspect that's not a common case that we expect, and might cause looping or other evaluation problems.

Can you test the following:

A profile with three ruletypes:

  1. The first declares no provider_traits
  2. The second declares provider_traits that aren't satisfied by any provider (e.g. add a "GitLab" trait, and require both GitHub and GitLab)
  3. The third declares a provider_trait satisfied by the provider.

And then register at least one repository with e.g. the GitHub or GitLab provider that should evaluate the profile.

What should happen as I understand it is that we should get rule evaluation results for the first and third ruletypes, and no evaluation results (not listed, not "skipped" or "error") for the second ruletype.

@evankanderson evankanderson left a comment

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.

Thanks for updating this!

I know that we use special error status to return different types of evaluation results, but this feels like a place where we can simplify to regular methods that return a boolean, rather than needing the additional complexity.

Comment on lines +19 to +22
# RuleType uses plain encoding/json (not protojson) for these fields, so
# provider_traits must be the numeric enum value, not the string name:
# 1 = PROVIDER_TYPE_GITHUB.
provider_traits: [1]

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.

Hmm, this doesn't feel very good. I feel like I'd rather have this be a list of string than numeric enums.

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.

I think we still have time to go back and change the proto definition (even though it will technically be incompatible), since this feature hasn't really been documented or used.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done — provider_traits is now repeated string (SHA1). Field 8 is reserved and the name is reused for field 9, so the number can never come back with a different meaning. Testdata reads provider_traits: [github] and the comment apologising for numeric enums is gone.

Two consequences worth flagging.

buf breaking needed a narrow FIELD_NO_DELETE_UNLESS_NAME_RESERVED exception scoped to minder/v1/minder.proto. WIRE_JSON wants both the number and the name reserved on deletion, which conflicts with reusing provider_traits for field 9. Reserving the number only seemed right, and the alternative — renaming the field — seemed worse. Happy to do it differently if you'd rather.

Dropping the enum also means the compiler stops catching typos, and under the boolean check in the other thread a misspelled trait would silently make a rule type never evaluate: no error, no skipped status, no row. That's the failure mode the enum was implicitly protecting against, so SHA3 adds validation at rule type creation that rejects unknown trait names and lists the valid ones, plus a warning log in NewRuleTypeEngine as a fallback for rule types already stored or any path that bypasses validation.

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.

Since we haven't released a client with this definition, I feel good about disabling or ignoring the buf check for this particular incompatibility.

I'll take a look at the implementation -- I think we should be able to (on ingest) flag typo'ed provider traits based on the server's knowledge of the trait universe. This also fixes a problem where someone using a ruletype which requires a recent trait would need a recent client in order to upload the ruletype to the server. IMO, it's better for the client to be able to treat this as opaque data, and people to be able to expect that e.g. a client from 6 months ago can work reasonably as long as the server is fairly up to date. (This matches, for example, my usage of the gh CLI and other cloud clients.)

Comment thread pkg/ruletest/eval.go Outdated
"profile?", &profileDict, "params?", &paramsDict, "mock_http?", &mockHttpDict,
"mock_fs?", &mockFSDict, "data_sources?", &datasourcesList)
"mock_fs?", &mockFSDict, "data_sources?", &datasourcesList,
"provider_missing_traits?", &providerMissingTraitsList)

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.

I think I'd rather prefer to see a positive list of "provider traits present" in the test, with the default behavior still being to have all traits present if the argument is not set.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Renamed to provider_traits_present with the logic inverted (SHA4). Default is unchanged: when the argument isn't passed, every trait is present.

While inverting it I noticed the argument was taking full enum names (PROVIDER_TYPE_GITHUB) while rule type YAML takes short trait names, so it now takes the same short names as the YAML field — provider_traits_present=["git", "rest"]. An unrecognised name errors out and lists the valid ones rather than silently parsing as absent.

One divergence to call out: the harness reports status: "skip" for a rule type the test provider doesn't support, whereas the executor produces nothing at all for that case. A Starlark test needs eval() to return something assertable, so reproducing the executor's silent no-op didn't seem workable. There's a comment on skippedResult explaining it, but say the word if you'd rather it surfaced differently.

Comment thread internal/engine/executor.go Outdated
// skip record, nothing — stricter than the SkipSilently
// path, which still logs at info level.
return nil
}

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.

Rather than going through the engine cache and back with an error status, what about adding the provider check right above GetRuleEngine, or even in EvalEntityEvent before calling evaluateRule?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Agreed, this is much simpler. The sentinel is gone entirely (SHA2).

NewRuleTypeEngine now computes trait satisfaction once at construction and exposes SupportedByProvider() bool, so it no longer returns an error for something that isn't really an error. The cache's errors.Is/continue is gone and every rule type gets cached again; evaluateRule checks the boolean and returns before createOrUpdateEvalStatus, so there's still no eval status row for an inapplicable rule — not listed, not skipped, not error.

On placement: I put it on the engine rather than above GetRuleEngine or in EvalEntityEvent because the check needs the rule type's provider_traits, so doing it earlier means either an extra DB lookup or reaching for the engine we'd be trying to avoid building. The trade-off is that engines now get constructed for rule types that will never evaluate, so we pay validator/ingester/evaluator setup for them once per cache build. That seemed like the cheaper side of the trade, but if you'd rather pay the lookup and skip construction entirely I'm happy to move it.

RuleTypeEngine now computes whether the provider satisfies the rule
type's provider_traits once at construction time and exposes it via
SupportedByProvider(), instead of NewRuleTypeEngine returning a
wrapped sentinel error that callers had to unwrap with errors.Is.

The rule engine cache no longer excludes unsatisfied rule types from
its population loop, so every rule type in the hierarchy is cached
regardless of applicability. The executor checks
SupportedByProvider() before evaluating a rule and, if false, returns
early ahead of createOrUpdateEvalStatus so no eval status row is
written for a rule type that isn't applicable to the entity's
provider.

An unrecognized trait name (e.g. a typo, or a rule type stored before
provider_traits validation existed) is treated as unsupported and
logs a warning, distinguishing it from a provider that simply doesn't
implement a valid trait.
RuleType.Definition.provider_traits was `repeated ProviderType`,
requiring the numeric enum value in JSON/YAML rule type definitions.
Change it to `repeated string`, using the same short trait names
("github", "git", "rest", "oci", "repo-lister", "image-lister")
already used elsewhere for provider class definitions.

Field 8 (the old enum field) is reserved rather than reused, since
the two field types have different wire encodings and reusing the
number would let old serialized data misparse instead of failing
loudly. The new field is 9. proto/buf.yaml gains a narrowly scoped
buf breaking exception: WIRE_JSON requires the field *name* to also
be reserved on deletion, which conflicts with intentionally reusing
"provider_traits" for the new field. This is called out as a
deliberate, sanctioned break, since the field has never been
documented or used.

ProviderTypeFromString, backed by the same (name) option lookup
ToString already uses, converts a trait string to its ProviderType.
Rule type validation now rejects unknown trait strings at
creation/update time, naming the valid values in the error - this is
the only safeguard against a typo silently making a rule type never
evaluate.
Rename eval()'s provider_missing_traits argument to
provider_traits_present and invert its semantics: it now lists the
traits the test provider does implement, rather than the ones it
doesn't. The default when the argument is omitted is unchanged - all
traits are present.

parseProviderTraitsList now accepts the same short trait names used
in rule type YAML (e.g. "github") via ProviderTypeFromString, instead
of full enum names like "PROVIDER_TYPE_GITHUB", and its error on an
unrecognized value lists the valid trait names.

eval() now checks the rule type engine's SupportedByProvider() before
evaluating and reports a "skip" result if the provider doesn't
implement a required trait, mirroring - but distinct from - the
production executor's zero-footprint no-op for the same case: a test
still needs eval() to return something a test author can assert on.
@intelligent-ears

Copy link
Copy Markdown
Member Author

Thanks for updating this!

I know that we use special error status to return different types of evaluation results, but this feels like a place where we can simplify to regular methods that return a boolean, rather than needing the additional complexity.

That's the right call, and it collapsed a lot of machinery. The sentinel is gone — no error wrapping, no errors.Is at three call sites, no %w chain that breaks silently if someone writes %v. NewRuleTypeEngine computes trait satisfaction once and exposes SupportedByProvider() bool; the cache caches everything again; evaluateRule checks the boolean and returns before writing any eval status.

It also removes the failure mode you were originally asking about more cleanly than my previous version did. Construction can't fail on traits at all now, so the case where one unsatisfiable rule type took down NewRuleEngineCache for the whole project simply doesn't exist rather than being caught and handled.

Also in this round: provider_traits is repeated string with field 8 reserved, unknown trait names are rejected at rule type creation, and the ruletest harness takes a positive provider_traits_present list. Details in the individual threads.

One thing I owe you: I couldn't complete the three-ruletype manual run against a live instance. PAT enrollment fails on my local stack at handlers_oauth.go:581 in ValidateCredentials with invalid credential type: string — nothing reaches GitHub, and CreateProvider succeeds separately so provider list shows a provider row with no token behind it, which made it look enrolled at first. Looks unrelated to this PR; happy to file it separately if it's not already known. In the meantime the behaviour you specified is covered by tests at the cache level (all three rule types cached, SupportedByProvider() false only for the unsatisfied one) rather than by a manual run.

@evankanderson

Copy link
Copy Markdown
Member

One thing I owe you: I couldn't complete the three-ruletype manual run against a live instance. PAT enrollment fails on my local stack at handlers_oauth.go:581 in ValidateCredentials with invalid credential type: string — nothing reaches GitHub, and CreateProvider succeeds separately so provider list shows a provider row with no token behind it, which made it look enrolled at first. Looks unrelated to this PR; happy to file it separately if it's not already known. In the meantime the behaviour you specified is covered by tests at the cache level (all three rule types cached, SupportedByProvider() false only for the unsatisfied one) rather than by a manual run.

I can do a manual run if you want; I already have a GitHub app set up for my docker-compose setup, so I shouldn't be hitting the token case at all.

@evankanderson evankanderson left a comment

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 looking really good, by the way! A few more comments, but only because I'm excited to see this working. I'll also be running an integration test for you and have results probably tomorrow morning.

Comment thread proto/buf.yaml
Comment on lines +12 to +22
# RuleType.Definition.provider_traits (field 8) was removed in favor of a
# new field 9 of the same name but a different type (repeated ProviderType
# -> repeated string). Field 8 is reserved so its number can never be
# reused, but WIRE_JSON also requires the *name* to be reserved on
# deletion, which would conflict with reusing "provider_traits" for field
# 9. This is a deliberate, sanctioned exception: provider_traits has never
# been documented or used, so no real caller depends on the old field 8
# wire/JSON shape.
ignore_only:
FIELD_NO_DELETE_UNLESS_NAME_RESERVED:
- minder/v1/minder.proto

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.

We should be able to delete this after this PR is merged, as the buf incompatibility check only compares between the PR base and current (as it should).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Agreed. Updated the comment in 82b6b46 to say so explicitly rather than removing it now, since deleting it before merge turns the check red. Happy to open the follow-up to drop it once this lands, or leave it to whoever's next in that file.

Comment thread pkg/ruletypes/service.go Outdated
// validProviderTraitNames lists the trait names a rule type's
// provider_traits may declare: the (name) option of every defined
// ProviderType value.
var validProviderTraitNames = func() []string {

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.

I'd prefer to explicitly use an init method to initialize this at startup, rather than implicitly through an anonymous function evaluation. It seems a little more clear to me.

@intelligent-ears intelligent-ears Aug 21, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done in c0a0f1a. It's an explicit init() now, and it moved to the minderv1 package — between your comment here and the one on pkg/ruletest/eval.go, it was clear this list had no business existing in three places. So this local copy is gone entirely and both callers use minderv1.ValidProviderTraitNames().

Comment thread pkg/ruletypes/service.go Outdated
Comment on lines +382 to +385
// This validation is the only safeguard against a typo in provider_traits:
// an unrecognized string parses fine but matches no trait, silently making
// the rule type never evaluate (see rtengine.RuleTypeEngine.SupportedByProvider),
// with no error, skipped status, or eval status row to reveal the mistake.

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.

In particular, I think this means that any stored ruletype with a provider_trait name that is later renamed would stop evaluating? (Because we're using the JSON-serialized proto definition stored in the definition row.)

I think that's acceptable, but if we do another set of commits, you might want to add that comment to the proto definition for the provider types.

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.

Changing the name of the provider traits would also break all the clients using provider_trait, so I think it's fair to consider that a fixed-ish interface.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Correct — the definition is stored as serialized proto, so a renamed trait name would stop matching for anything already in the database, and your second point is the sharper one: it'd break every client constructing or parsing those names too. That makes them a public interface whether or not we intended them as one.

Added that to the ProviderType enum in 82b6b46: the (name) values are a stable interface, renaming one silently stops it matching stored rule types as well as breaking clients, so add new values rather than renaming existing ones.

The comment you're commenting on is also now out of date — this validation is no longer the only safeguard. c2dd129 catches an unrecognized name at evaluation time too and reports it as an eval status error, so a rule type stored before this validation existed no longer fails silently. Updated the comment to say that instead.

Comment on lines +13 to +15
// ProviderTypeFromString returns the ProviderType whose (name) option
// matches s (e.g. "github", "git"), and true if one was found. It is the
// inverse of ProviderType.ToString.

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.

Given the cost of iterating, I'd prefer to have this use a map generated at init time, rather than called in a loop (like we do in pkg/ruletypes/service.go).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done in c0a0f1aproviderTypeByName is built once in init() and this is a map lookup now. enumFromStringViaDescriptor is deleted since it had no other callers; enumToStringViaDescriptor stays, it's still used by ToString() on several enums.

The same init() also builds the sorted valid-name list, exported as ValidProviderTraitNames() for error messages, which let me delete the duplicate copies in pkg/ruletypes and pkg/ruletest you flagged elsewhere. It returns a copy rather than the package slice, so a caller sorting or mutating it can't affect anyone else.

Comment thread pkg/engine/v1/rtengine/engine.go Outdated
Comment on lines +85 to +90
zerolog.Ctx(ctx).Warn().
Str("rule_type", ruletype.GetName()).
Str("provider_trait", trait).
Msg("rule type declares an unknown provider trait; treating as unsupported")
supportedByProvider = false
break

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.

If we mismatch here, can we make this return an error to the user? I'd need to think about the mechanics, but it "feels" right, since the user can fix it by adjusting the ruletype definition.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Agreed, and done in c2dd129 using the mechanics you sketched in the executor thread — the engine caches the unknown names rather than trying to report from here.

NewRuleTypeEngine now collects every unrecognized trait name into unknownProviderTraits and exposes it via UnknownProviderTraits(). The loop no longer breaks on the first one, so someone with two typos sees both rather than fixing one and rediscovering the other on the next run. The two conditions stay distinct instead of both collapsing into supportedByProvider = false, since only one of them is something the user can act on.

Dropped the warning log — it was a consolation prize for not being able to tell the user, and now we can.

Comment thread internal/engine/executor.go Outdated
Comment on lines +181 to +187
if !ruleEngine.SupportedByProvider() {
// This rule type doesn't apply to this entity's provider.
// Produce zero evaluation-status footprint: no error, no
// skip record, nothing — stricter than the SkipSilently
// path, which still logs at info level.
return nil
}

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.

If you can cache the "unknown provider trait" in the ruleEngine, you can use it here to record the error only for ruletypes that have unrecognized provider traits.

(Sorry to move the goals on you -- your observation that a completely unknown provider_trait has a silent failure mode made me look to make that loud.)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

No apology needed — I flagged the silent failure and then left it silent, so this is the better end of that. Done in c2dd129, exactly as you described.

There are three states now rather than two:

  • Unknown trait name → evalErr is set to an error naming the rule type and every offending trait, which flows through the existing SetEvalErrDoActionscreateOrUpdateEvalStatus path, so it lands as a normal eval status error. No parallel reporting mechanism.
  • Known trait the provider doesn't implement → unchanged, silent early return, no row at all.
  • Everything satisfied → evaluates as before.

The silent return is guarded on len(unknownTraits) == 0 && !SupportedByProvider(), so the two can't collapse into each other. Worth noting the error is deliberately not wrapped in ErrEvaluationFailed or ErrEvaluationSkipped, so it classifies as EvalStatusTypesError rather than a rule failure — the rule didn't fail, the rule type is malformed.

Comment thread pkg/datasources/v1/datasources.go Outdated
Comment on lines +19 to +29
// DataSourceDriverStruct is the driver type for the structured data source.
//
// Deprecated: use minderv1.DataSourceDriverStruct. Kept here as an alias so
// existing callers of this package don't break; the canonical definition
// lives in minderv1 to avoid an import cycle (minderv1 -> this package ->
// pkg/engine/v1/interfaces).
DataSourceDriverStruct = minderv1.DataSourceDriverStruct
// DataSourceDriverRest is the driver type for a REST data source.
DataSourceDriverRest = "rest"
//
// Deprecated: use minderv1.DataSourceDriverRest. See DataSourceDriverStruct.
DataSourceDriverRest = minderv1.DataSourceDriverRest

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.

IMO, since the package is pre-1.0, it would be okay to remove these entirely.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Removed entirely in 82b6b46. Turned out to be free — nothing referenced the aliases at all; every caller was already using the minderv1 constants directly, so there was no migration to do and the minderv1 import in this file went with them.

Comment thread pkg/ruletest/eval.go Outdated
Comment on lines +229 to +243
// validProviderTraitNames lists the trait names accepted by
// provider_traits_present: the (name) option of every defined ProviderType
// value.
func validProviderTraitNames() []string {
names := make([]string, 0, len(minderv1.ProviderType_name))
for v := range minderv1.ProviderType_name {
t := minderv1.ProviderType(v)
if t == minderv1.ProviderType_PROVIDER_TYPE_UNSPECIFIED {
continue
}
names = append(names, t.ToString())
}
sort.Strings(names)
return names
}

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.

Given that we use this a few different places, what do you think about creating this as a map in the proto package?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes — this was the third copy, which is two too many. c0a0f1a builds providerTypeByName once in an init() in minderv1 and exports ValidProviderTraitNames() alongside it; this function and the one in pkg/ruletypes/service.go are both gone, and ProviderTypeFromString is a map lookup rather than a descriptor walk.

The accessor returns a copy rather than the package slice, so a caller sorting or mutating it can't affect anyone else.

Replaces per-call descriptor iteration in ProviderTypeFromString and the
duplicated valid-name lists in ruletypes and ruletest.
An unrecognized trait name is a rule type authoring problem the user can
fix, so it now surfaces as an eval status error naming every offending
trait. A known trait the provider doesn't implement still produces no
eval status row at all.
Also removes the pkg/datasources/v1 driver constants outright rather than
leaving deprecated aliases, and notes the buf exception is removable once
this merges.
@intelligent-ears

Copy link
Copy Markdown
Member Author

One thing I owe you: I couldn't complete the three-ruletype manual run against a live instance. PAT enrollment fails on my local stack at handlers_oauth.go:581 in ValidateCredentials with invalid credential type: string — nothing reaches GitHub, and CreateProvider succeeds separately so provider list shows a provider row with no token behind it, which made it look enrolled at first. Looks unrelated to this PR; happy to file it separately if it's not already known. In the meantime the behaviour you specified is covered by tests at the cache level (all three rule types cached, SupportedByProvider() false only for the unsatisfied one) rather than by a manual run.

I can do a manual run if you want; I already have a GitHub app set up for my docker-compose setup, so I shouldn't be hitting the token case at all.

That'd be great, thank you — yes please.

A few things that changed under you since I first described the setup:

  • Trait names in rule type YAML are plain strings now, not numeric enums: provider_traits: [github].
  • There's no GitLab value in ProviderType (it's github, rest, git, oci, repo-lister, image-lister), so for your unsatisfiable rule type [github, oci] against a GitHub provider is the pair — a GitHub provider implements github, rest, git, repo-lister, image-lister but not oci.
  • Anything created earlier with the numeric form won't parse, so a fresh project is less hassle than reusing an existing one.

A fourth case worth adding to your three, if it's cheap: a rule type with a deliberately misspelled trait, e.g. provider_traits: [gihtub]. Creating it should be rejected outright now, and if you can get one into the database bypassing validation it should evaluate to an error status naming the bad trait rather than disappearing.

Also worth pushing on: register the unsatisfiable rule type but leave it out of every profile. That was the case I was most worried about — the engine cache builds an engine for every rule type in the project hierarchy regardless of profile membership, so an unused broken one used to be able to take down evaluation for everything.

@intelligent-ears

Copy link
Copy Markdown
Member Author

This is looking really good, by the way! A few more comments, but only because I'm excited to see this working. I'll also be running an integration test for you and have results probably tomorrow morning.

Thanks — and no complaints about the comment volume, the PR is meaningfully better for them. Pushed in three commits:

  • c0a0f1a — trait name lookup is a map built in an explicit init() in minderv1, with ValidProviderTraitNames() exported. The two duplicate copies of that list in pkg/ruletypes and pkg/ruletest are gone, and ProviderTypeFromString is a map lookup rather than a descriptor walk.
  • c2dd129 — the unknown-trait case is loud now. Three states instead of two: an unrecognized trait name records an eval status error naming every offending trait, a known-but-unimplemented trait stays silent with no row, everything satisfied evaluates normally.
  • 82b6b46ProviderType (name) values documented as a stable interface, pkg/datasources/v1 driver constants removed outright, buf exception marked temporary.

The unknown-trait change is the one worth a second look, since it's the only behavioural change of the three and it's new since you last read this.

Taken you up on the integration test in the other thread — there are a couple of things worth knowing before you run it, including a fourth case that didn't exist when you offered.

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.

rtengine: gate rule evaluation on provider_traits

3 participants