Skip to content

feat: Add resource-info-fetcher - #863

Open
sbernauer wants to merge 80 commits into
mainfrom
spike/rif
Open

feat: Add resource-info-fetcher#863
sbernauer wants to merge 80 commits into
mainfrom
spike/rif

Conversation

@sbernauer

@sbernauer sbernauer commented Jul 21, 2026

Copy link
Copy Markdown
Member

Description

Part of #848

Yes, the diff looks big, but most of it are lockfiles and kuttl tests

CRD change

Basically the same as for the user-info-fetcher. The only difference is the env field, which is DataHub specific.

Look at extra/crds.yaml for the concrete CRD change

  clusterConfig:
    resourceInfo: # <1>
      backend:
        dataHub:
          hostname: datahub-gms.my-namespace.svc.cluster.local
          port: 8080
          rootPath: / # optional, defaults to /
          tls:
            verification:
              server:
                caCert:
                  secretClass: tls # <2>
          credentialsSecretName: resource-info-fetcher-credentials # <3>
          env: PROD # <4>
      cache: # optional, enabled by default
        maxEntries: 42 # optional, defaults to no limit
        entryTimeToLive: 60s # optional, defaults to 60s
---
apiVersion: v1
kind: Secret
metadata:
  name: resource-info-fetcher-credentials
stringData:
  token: <datahub-personal-access-token> # <3>

API of RIF

The rego rules and HTTP API of RIF: https://github.com/stackabletech/opa-operator/blob/spike/rif/docs/modules/opa/pages/usage-guide/resource-info-fetcher.adoc

Definition of Done Checklist

  • Not all of these items are applicable to all PRs, the author should update this template to only leave the boxes in that are relevant
  • Please make sure all these things are done and tick the boxes

Author

  • Changes are OpenShift compatible
  • CRD changes approved
  • CRD documentation for all fields, following the style guide.
  • Helm chart can be installed and deployed operator works
  • Integration tests passed (for non trivial changes)
  • Changes need to be "offline" compatible
  • Links to generated (nightly) docs added
  • Release note snippet added

Reviewer

  • Code contains useful comments
  • Code contains useful logging statements
  • (Integration-)Test cases added
  • Documentation added or updated. Follows the style guide.
  • Changelog updated
  • Cargo.toml only contains references to git tags (not specific commits or branches)

Acceptance

  • Feature Tracker has been updated
  • Proper release label has been added
  • Links to generated (nightly) docs added
  • Release note snippet added
  • Add type/deprecation label & add to the deprecation schedule
  • Add type/experimental label & add to the experimental features tracker

Release notes

Added
  • Add an initial version of resource-info-fetcher, which is similar to user-info-fetcher, but allows to fetch additional metadata about resource information from a data catalog.
    For now only DataHub is supported.
    Also, a rego-rule library has been added to make it easier to call resource-info-fetcher from within OPA.
    The API (especially the response) might change in the future once more data catalogs are supported

Nightly docs

As soon as this PR is merged the docs will show up at https://docs.stackable.tech/home/nightly/opa/usage-guide/resource-info-fetcher/

@sbernauer
sbernauer marked this pull request as ready for review July 30, 2026 13:49
maltesander and others added 26 commits August 17, 2026 13:50
`urn_for_request` and `Entity::into_response` were exercised only by the kuttl
test, which needs DataHub, Trino, Kafka and Superset to run.

Add golden URNs for every request variant, including the database container hash -
computed independently with Python's json.dumps + hashlib.md5 the way DataHub's
`datahub_guid` does, not read back out of this implementation - and one case
pinning that the configured fabric reaches the dataset URN, since a fabric
mismatch is what makes an otherwise correct lookup silently return nothing.

For `into_response`, cover a fully populated entity and each of the paths taken
when DataHub did not populate a `properties` aspect: tags, domains and data
products fall back to their URN, users to the name in their URN, groups to the
whole URN, and an owner with no ownership type entity to its legacy type or
"unknown". Also pin that an absent `active` does not report a user as deactivated,
and that owners sharing an ownership type are grouped together.
The bundle-builder, user-info-fetcher and resource-info-fetcher wrote a single
file log that was never rolled over and never pruned. stackable-telemetry
defaults RotationPeriod to Never, and the operator only set the log level and
directory, so nothing bounded those files. Vector reads them but does not rotate
them, and its remove_after_secs is neither configured nor applicable to a file
that is still being appended to.

Set FILE_LOG_ROTATION_PERIOD=hourly and FILE_LOG_MAX_FILES=5 for all three,
matching the "x 5" the bundle-builder budget already assumed. This bounds the
number of files, not their size, so the budgeted MAX_*_LOG_FILE_SIZE stays an
estimate. Products get a size-based appender from operator-rs; stackable-telemetry
has no equivalent, see #606.
A lookup that kept failing queried DataHub again on every single request, plus one
WARN line each. Anyone who can name a resource could reach it, and it was worst
exactly when DataHub was already unwell.

Hold both outcomes in the cache: the value becomes Found or Failed, so failures
share the key space and capacity limit with successes and concurrent lookups of a
failing key coalesce onto one backend call. A moka Expiry gives Failed entries a
5s lifetime and leaves Found ones to the configured entryTimeToLive. moka evicts at
the earliest of the per-entry expiry and the TTL, so a TTL below 5s wins on its own.

The short lifetime is deliberate: a stale success is only out of date, while a
stale failure keeps failing after the backend has recovered.
The warning was emitted from `status_code`, which runs while rendering the
response. Failures are now cached, so that produced a line for every request that
hit a cached failure, which is exactly the burst the cache exists to prevent.

Move it into `fetch_resource_info`, so one line is logged per attempt that actually
reached the backend. The remaining half of the existing todo, making the level
depend on the kind of error, is left in place.
`cargo doc` warned about two links to `urn_for_request`, whose module was private,
and about `CachedToken::get` referencing the private `EXPIRY_MARGIN`. Make the
module `pub(crate)` and the constant `pub`; the margin is part of the contract, as
a caller should know the token is refreshed before it expires.

fix: Log a rejected request at DEBUG rather than WARN

Every failed lookup was logged at WARN, including the ones caused by the request
itself: an identifier no DataHub URN can express, for instance. Those say nothing
about the health of this process or of the backend, and any user who can name a
resource can produce them at will.

Pick the level from the error's status code class, so client errors go to DEBUG
with their own message and everything else stays at WARN. This is the second half
of the todo removed in the previous commit; the first half was moving the log line
to where the backend is actually queried.
send_json_request traced every response body verbatim. The Keycloak and
Entra OAuth token endpoints go through the same function, so raising the
sidecar log level to TRACE wrote bearer tokens - and the PII of every
user lookup - into both the console log and the Vector-collected file
log. Log the URL and body size instead; the backends already trace their
own parsed, secret-free payloads where that is useful.
…ilures

DataHub answers 200 OK and reports resolver failures in `errors`, nulling
out the field that failed. The response was deserialized in full before
`errors` was inspected, so such a partial failure surfaced as "failed to
parse json response" and the error messages explaining it were discarded.

Keep `data` unparsed until `errors` has been checked, and read a null
list field as an empty one - a nulled-out list is what a failed resolver
actually returns, and `#[serde(default)]` only covers an absent field.
maxEntries defaulted to no limit, so the moka cache in the user-info-
and resource-info-fetcher sidecars could grow without bound inside their
128Mi memory limit. Cache keys are built from caller-supplied parameters,
which are bounded in size but not in number, so any caller who can name
a resource can mint a distinct entry per request - e.g. by looping over
random table names in Trino - and hold it for entryTimeToLive.

Default the limit to 10000 entries and drop the unbounded setting; going
over the limit now evicts least recently used entries instead of growing.
…ends

The Keycloak and Entra backends each carried their own copy of the
"401 -> invalidate, re-mint, retry once" block, the access_token()
wrapper around CachedToken::get, the OAuthResponse struct and the
mock_token test helper. Logic that is duplicated near-verbatim in two
places drifts, and this is logic where drift is a security-relevant
authentication bug.

Move the retry into CachedToken::get_with_retry and OAuthResponse next
to MintedToken in info-fetcher-commons, and the token endpoint mock into
a shared test module. No behavior change beyond the warning naming the
issuer in a field rather than in the message.
…telemetry

tracing-appender prunes old log files before it writes the first one, and
that pass prints "Error reading the log directory/files: No such file or
directory" to stderr when the directory does not exist yet. It then
creates the directory itself, so logging works and the message is
harmless - but it is unstructured stderr output that reads like a startup
failure and reaches the log collector as an unparsable line.

The other containers avoid this because their bash entrypoint runs
mkdir -p first; the info-fetcher sidecars exec their binary directly. Add
a shared helper and call it before Tracing::init() in both fetchers.
The user-info-fetcher and resource-info-fetcher sidecars only ever read
their config, credential and kerberos volumes - the credential ones hold
a DataHub PAT and OAuth2 client credentials respectively - but mounted
all of them writable. Mount them read-only.

The kerberos mount is safe to include because KRB5CCNAME is MEMORY:, so
the credential cache never lands in that directory. The log volume stays
writable, as the sidecars write their file logs to it.
The userinfo and resourceinfo libraries returned http.send(...).body
without checking the status code, so the error envelope a fetcher answers
a failed lookup with reached the policy as if it were user or resource
information. A rule defaulting a missing field (for example
object.get(user, "groups", [])) then read a failed lookup as "this user
is in no groups" and allowed what it should have denied. A resource name
containing a URN delimiter is enough to trigger this, and any user who
can name a table can produce one.

Only return a value for 200, so such a lookup is undefined instead, and
set the http.send timeout explicitly. It has to stay well below the
fetcher's own 60s backend budget, because an authorization decision has
to be answered promptly.
@maltesander

Copy link
Copy Markdown
Member
--- PASS: kuttl (549.95s)
    --- PASS: kuttl/harness (0.00s)
        --- PASS: kuttl/harness/smoke_opa-1.12.3_openshift-false_use-tls-true (56.38s)
        --- PASS: kuttl/harness/resources_opa-latest-1.16.2_openshift-false (20.21s)
        --- PASS: kuttl/harness/logging_opa-1.16.2_openshift-false (61.86s)
        --- PASS: kuttl/harness/logging_opa-1.12.3_openshift-false (61.94s)
        --- PASS: kuttl/harness/smoke_opa-1.16.2_openshift-false_use-tls-true (56.42s)
        --- PASS: kuttl/harness/smoke_opa-1.16.2_openshift-false_use-tls-false (52.11s)
        --- PASS: kuttl/harness/cluster-operation_opa-latest-1.16.2_openshift-false (33.07s)
        --- PASS: kuttl/harness/smoke_opa-1.12.3_openshift-false_use-tls-false (52.29s)
        --- PASS: kuttl/harness/data-hub-resource-info_opa-latest-1.16.2_trino-latest-481_kafka-latest-4.2.1_superset-latest-6.1.0_data-hub-1.0.3_data-hub-prerequisites-0.3.0_openshift-false (398.77s)
        --- PASS: kuttl/harness/config-overrides_opa-latest-1.16.2_openshift-false (49.57s)
        --- PASS: kuttl/harness/openldap-user-info_opa-latest-1.16.2_openshift-false (62.96s)
        --- PASS: kuttl/harness/aas-user-info_opa-latest-1.16.2_openshift-false (52.12s)
        --- PASS: kuttl/harness/keycloak-user-info_opa-latest-1.16.2_keycloak-23.0.1_openshift-false (92.70s)
PASS

@maltesander maltesander added release-note Denotes a PR that will be considered when it comes time to generate release notes. release-note/action-required Denotes a PR that introduces potentially breaking changes that require user action. scheduled-for/26.11.0 labels Aug 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release-note/action-required Denotes a PR that introduces potentially breaking changes that require user action. release-note Denotes a PR that will be considered when it comes time to generate release notes. scheduled-for/26.11.0

Projects

Status: Development: In Review

Development

Successfully merging this pull request may close these issues.

4 participants