Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
<a href="https://pkg.go.dev/github.com/platformrelay/kollect"><img src="https://img.shields.io/github/go-mod/go-version/platformrelay/kollect" alt="Go"></a>
<a href="https://pkg.go.dev/github.com/platformrelay/kollect"><img src="https://pkg.go.dev/badge/github.com/platformrelay/kollect.svg" alt="Go Reference"></a>
<a href="https://github.com/orgs/platformrelay/packages?repo_name=kollect"><img src="https://img.shields.io/badge/ghcr.io-platformrelay%2Fkollect-2496ED?logo=docker&logoColor=white" alt="Container"></a>
<a href="https://artifacthub.io/packages/search?repo=kollect"><img src="https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/kollect" alt="Artifact Hub"></a>
<a href="https://operatorhub.io/operator/kollect"><img src="https://img.shields.io/badge/OperatorHub.io-kollect-1D8FE1?logo=redhat&logoColor=white" alt="OperatorHub.io"></a>
</p>

<p align="center"><em>Simple to start · platform-grade to grow</em></p>
Expand All @@ -40,9 +42,12 @@ the live cluster.
**Read the docs:** **[platformrelay.github.io/Kollect](https://platformrelay.github.io/Kollect/)** — architecture,
quick start, CR reference, ADRs, and examples. This README is the front door; the site is the map.

Install paths: **Helm OCI on GHCR** is primary (`oci://ghcr.io/platformrelay/kollect`). **Artifact Hub**
and **OperatorHub** discoverability are wired at release ([ADR-0708](https://platformrelay.github.io/Kollect/adr/0708-operator-distribution-hubs/));
use those hubs once listings are live — badge URLs stay out of this README until then.
Install paths: **Helm OCI on GHCR** is primary (`oci://ghcr.io/platformrelay/kollect`). The chart is
listed on **[Artifact Hub](https://artifacthub.io/packages/search?repo=kollect)**, and the OLM bundle
is submitted to the community operator catalogs at release
([ADR-0708](https://platformrelay.github.io/Kollect/adr/0708-operator-distribution-hubs/)). The
**[OperatorHub.io](https://operatorhub.io/operator/kollect)** listing goes live when the
community-operators submission merges upstream.

> **Pre-1.0.** Kollect uses a `v1alpha1` API. Breaking API or default changes may ship in minor
> releases before 1.0; release notes and migration guidance call them out. See the
Expand Down
33 changes: 24 additions & 9 deletions artifacthub-repo.yml
Original file line number Diff line number Diff line change
@@ -1,13 +1,28 @@
# Artifact Hub repository metadata (ADR-0708), pushed to
# ghcr.io/<owner>/kollect:artifacthub.io by the release workflow.
#
# 2026-08-07 — PENDING OPERATOR ACTION: repositoryID below is still the
# placeholder. Register the chart repository at https://artifacthub.io and paste
# the real repository ID from the Artifact Hub control panel here; Verified
# Publisher status only activates once the pushed ID matches the registered one.
# hack/test/dist_artifacthub_release_test.sh accepts ANY well-formed UUID, so
# filling in the real value does not turn CI red.
repositoryID: 00000000-0000-4000-8000-000000000000
# 2026-08-18 — repository registered on Artifact Hub as `kollect`
# (https://artifacthub.io/packages/search?repo=kollect); Verified Publisher active.
#
# Verified Publisher requires BOTH: the pushed repositoryID matches the registered
# one, AND the owner email matches the email on the Artifact Hub account that owns
# the repository (account `konih`). Artifact Hub compares the two addresses directly
# — it does not send a verification mail.
repositoryID: cb3be9a6-8e3b-4419-9de5-1184fe349c29
owners:
- name: kollect contributors
email: maintainers@platformrelay.github.io
- name: Konrad Heimel
email: konrad.heimel@gmail.com

# DR-FIND-07: this ONE OCI repository holds two different artifact kinds — the Helm
# chart at bare semver tags (0.18.0) and the multi-arch controller image at
# v-prefixed tags (v0.18.0). Artifact Hub treats every semver-ish tag as a chart, so
# without this it tries to load the image tags as charts and the tracker reports
# "layer not found" for each one. Bare 0.12.0 is included because that single release
# published the IMAGE to the chart's bare tag; every other bare tag is a real chart.
#
# `version` is a REGULAR EXPRESSION matched against the tag; `name` is an exact match
# on the package name. Ignored packages are applied the next time the repository is
# processed, not immediately.
ignore:
- name: kollect
version: '^(v[0-9].*|0\.12\.0)$'
6 changes: 6 additions & 0 deletions config/olm/ci.yaml
Original file line number Diff line number Diff line change
@@ -1,2 +1,8 @@
---
# NOTE: this file is NOT what gets submitted. hack/operatorhub-pr.sh writes its own
# copy of ci.yaml into the community-operators PR. Keep the two in sync.
# `reviewers` takes GitHub usernames (an org does not resolve) — the PR author must
# appear in the upstream copy for `authorized-changes` / self-merge to apply.
updateGraph: semver-mode
reviewers:
- konih
Original file line number Diff line number Diff line change
Expand Up @@ -121,11 +121,15 @@ spec:
- base64data: __ICON_BASE64__
mediatype: image/svg+xml

# Kollect's controller watches cluster-wide: it never reads olm.targetNamespaces
# and sets no WATCH_NAMESPACE, so a namespace-scoped install would silently
# collect from the whole cluster. Advertise AllNamespaces only until that
# plumbing exists (ADR-0708 follow-up D9).
installModes:
- type: OwnNamespace
supported: true
supported: false
- type: SingleNamespace
supported: true
supported: false
- type: MultiNamespace
supported: false
- type: AllNamespaces
Expand Down
7 changes: 4 additions & 3 deletions docs/getting-started/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,10 @@ for a constrained development environment. The chart does not generate certifica
Helm OCI on GHCR remains the primary install path ([ADR-0705](../adr/0705-release-supply-chain.md)).
Additional distribution wiring is landing under [ADR-0708](../adr/0708-operator-distribution-hubs.md):

- **Artifact Hub** — once the chart repository is registered and Verified Publisher metadata is
live, the same OCI chart (`oci://ghcr.io/platformrelay/kollect`) will appear in Artifact Hub
search. Until listing is confirmed, install from GHCR as above.
- **Artifact Hub** — the chart repository is registered and listed as
[`kollect`](https://artifacthub.io/packages/search?repo=kollect); it indexes the same OCI chart
(`oci://ghcr.io/platformrelay/kollect`), so Artifact Hub is a discovery surface, not a separate
install path — `helm install` from GHCR exactly as above.
- **OperatorHub / OpenShift** — OLM bundles are generated at release and submitted to the community
operator catalogs when `OPERATORHUB_PAT` is configured. After upstream merge, install via
OperatorHub or the OpenShift console using package **`kollect`**, channel **`stable`**. Until then,
Expand Down
131 changes: 114 additions & 17 deletions hack/operatorhub-pr.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@
# Required env vars:
# VERSION - Release version without 'v' prefix (e.g., 0.17.0)
# IMAGE_DIGEST - Controller image digest (e.g., sha256:abc...)
# GH_TOKEN - PAT with public_repo scope for fork push and upstream PRs
# GH_TOKEN - classic PAT for fork push and upstream PRs. Scopes: `public_repo`
# (clone/push the public forks, open + edit upstream PRs) AND
# `workflow` — the submission branch is cut from upstream/main, which
# carries .github/workflows/*, and GitHub rejects a PAT push that
# introduces workflow files without it once the fork drifts behind.
#
# Optional env vars:
# FORK_OWNER - GitHub org owning the forks (default: platformrelay)
Expand Down Expand Up @@ -96,14 +100,27 @@ submit_bundle() {
cp "${CHECKOUT_DIR}/${BUNDLE_DIR}/metadata/"* "${OPERATOR_DIR}/${VERSION}/metadata/"

if [[ -n "${openshift_versions}" ]]; then
sed -i '/^annotations:/a\ com.redhat.openshift.versions: "'"${openshift_versions}"'"' \
"${OPERATOR_DIR}/${VERSION}/metadata/annotations.yaml"
# Portable insert-after. GNU and BSD/macOS sed disagree on BOTH `-i` (BSD reads the
# next argument as the backup suffix) and the one-line `a\text` form, so the previous
# `sed -i` worked on the CI runner and died locally with "invalid command code".
# awk behaves identically on both, which matters because docs/RELEASE.md and the
# workflow's own failure warning both tell operators to re-run this script by hand.
local annotations_file="${OPERATOR_DIR}/${VERSION}/metadata/annotations.yaml"
local tmp_annotations
tmp_annotations="$(mktemp)"
awk -v line=" com.redhat.openshift.versions: \"${openshift_versions}\"" \
'{ print } /^annotations:/ { print line }' \
"${annotations_file}" >"${tmp_annotations}"
mv "${tmp_annotations}" "${annotations_file}"
fi

# `reviewers` takes GitHub USERNAMES, not orgs. The PR author (the OPERATORHUB_PAT
# owner) must appear here on the UPSTREAM default branch for the pipeline to set
# `authorized-changes` and self-merge later version bumps; an org never resolves.
cat > "${OPERATOR_DIR}/ci.yaml" <<'CIEOF'
updateGraph: semver-mode
reviewers:
- platformrelay
- konih
CIEOF

git add "${OPERATOR_DIR}/"
Expand All @@ -117,33 +134,113 @@ CIEOF
fi

local pr_title="operator ${pr_tag} [CI] kollect (${VERSION})"
# Quoted heredoc: no expansion, so markdown backticks are literal and cannot become
# command substitution. ${VERSION} is substituted afterwards via the placeholder.
local pr_body
pr_body="### New Submission
pr_body="$(cat <<'PRBODY'
### New Submission

**Operator:** kollect
**Version:** ${VERSION}
**Operator:** kollect · **Version:** __VERSION__ · **Channel:** stable · **Capability level:** Full Lifecycle

Update Kollect operator to version ${VERSION}.
---

## What Kollect does

Kollect turns live Kubernetes state into **durable, queryable inventory**. Operators declare once
what matters -- via `KollectProfile` (which fields to extract) and `KollectTarget` (which resources
to watch) -- and Kollect keeps that inventory current, exporting the same canonical snapshot to Git,
relational databases, object storage, and event streams in parallel.

The motivating problem: everything that wants to know "what is running in this cluster" ends up
querying the apiserver. That couples every consumer to cluster RBAC, risks watch storms, and pushes
teams toward storing derived state in etcd, where it does not belong. Kollect inverts this --
consumers query a **sink**, never the apiserver.

## Features

* **Decoupled read model** -- consumers query a sink, not the apiserver: no RBAC blast radius, no
watch-storm risk, no etcd size pressure.
* **Event-driven collection** -- one shared informer per GVK keeps inventory current as the cluster
changes. No polling loops.
* **Schema-flexible extraction** -- declare the attributes you want with CEL expressions or
JSONPath. No bespoke collector per resource kind.
* **Pluggable sinks** -- snapshot, database, and event sink families fan the same snapshot out to
Git, Postgres, object stores, or event streams. No privileged backend tier.
* **Multi-tenant by design** -- `KollectScope` gates which teams, namespaces, and sinks each tenant
may use, so inventory collection can be delegated safely.
* **Fleet-ready** -- N single-mode operators feed one shared sink, partitioned by `spec.cluster`.
There is no central hub tier to operate.
* **Scale-aware** -- shared informers, export sharding, and tunable reconcile/dispatch concurrency.

## Security

**Runtime hardening.** The manager runs `runAsNonRoot` with a `RuntimeDefault` seccomp profile, a
read-only root filesystem, `allowPrivilegeEscalation: false`, and **all** Linux capabilities dropped.

See [release notes](https://github.com/platformrelay/kollect/releases/tag/v${VERSION}) for changes.
**Least privilege and data handling.**

* Reads only the resources its RBAC allows, with **SubjectAccessReview** checks configurable per
target -- collection cannot be used to escalate beyond what the requester may already read.
* Writes to external sinks using credentials sourced from **`Secret` references only**; credentials
never appear in CR specs or logs.
* Stores **aggregated summaries** in CR `status`, not full resource payloads.
* Documented guidance to restrict egress with `NetworkPolicy` and require verified TLS to sinks.

**Supply chain.** Every release publishes multi-arch images with **cosign keyless signatures**,
**SPDX SBOMs**, and **SLSA provenance attestations**, plus `sha256sum` manifests for install YAML
and the chart tarball. The CSV deployment and `relatedImages` in this bundle are **digest-pinned**,
not tag-pinned.

**Assurance.** The project runs OpenSSF Scorecard, `govulncheck`, golangci-lint SAST, and dependency
and license (SCA) policy checks, publishes VEX statements for vulnerability exceptions, and accepts
private vulnerability reports. See `SECURITY.md` and the published security architecture for trust
boundaries, tenancy, redaction, and shared-responsibility detail.

## Notes for reviewers

* **Install mode:** `AllNamespaces` only. The controller watches cluster-wide and does not consume
`olm.targetNamespaces`, so advertising a namespace-scoped mode would silently collect from the
whole cluster. It is deliberately not offered.
* **Webhooks:** this bundle runs with validating webhooks disabled -- no webhook `Service` or
certificate ships in it. CRD schema validation still applies. The Helm chart path offers the full
admission stack for users who want it.
* **Prerequisites:** Kubernetes 1.28+.

Source: https://github.com/platformrelay/kollect ·
Docs: https://platformrelay.github.io/Kollect/ ·
Release notes: https://github.com/platformrelay/kollect/releases/tag/v__VERSION__

---
*This PR was automatically created by the Kollect release workflow.*"
*Submitted by the Kollect release workflow (`hack/operatorhub-pr.sh`).*
PRBODY
)"
pr_body="${pr_body//__VERSION__/${VERSION}}"

# Look the PR up by branch name and match the owner case-INSENSITIVELY. GitHub stores
# the canonical org casing ("PlatformRelay"), so the old `--head "${FORK_OWNER}:${BRANCH}"`
# filter silently returned nothing whenever FORK_OWNER differed in case, and the script
# then tried to create a duplicate PR and died with "a pull request already exists".
local fork_owner_lc
fork_owner_lc="$(printf '%s' "${FORK_OWNER}" | tr '[:upper:]' '[:lower:]')"
local existing_pr
existing_pr=$(GH_TOKEN="${GH_TOKEN}" gh pr list \
--repo "${upstream_repo}" \
--head "${FORK_OWNER}:${BRANCH}" \
--head "${BRANCH}" \
--state open \
--json number \
--jq '.[0].number // empty' 2>/dev/null || true)
--json number,headRepositoryOwner \
--jq "[.[] | select((.headRepositoryOwner.login // \"\" | ascii_downcase) == \"${fork_owner_lc}\")] | .[0].number // empty" 2>/dev/null || true)

if [[ -n "${existing_pr}" ]]; then
echo "Updating existing PR #${existing_pr}"
GH_TOKEN="${GH_TOKEN}" gh pr edit "${existing_pr}" \
--repo "${upstream_repo}" \
--title "${pr_title}" \
--body "${pr_body}"
# Use the REST endpoint, NOT `gh pr edit`. The latter resolves assignees/labels/
# reviewers via GraphQL and so demands `read:org` ("The 'login' field requires one of
# the following scopes: ['read:org']"), which would force a broader PAT than this
# script needs. PATCH .../pulls/{n} updates title+body with `public_repo` alone.
GH_TOKEN="${GH_TOKEN}" gh api \
--method PATCH \
"repos/${upstream_repo}/pulls/${existing_pr}" \
-f title="${pr_title}" \
-f body="${pr_body}" >/dev/null
echo "PR updated: https://github.com/${upstream_repo}/pull/${existing_pr}"
else
echo "Creating new PR..."
Expand Down
17 changes: 12 additions & 5 deletions hack/test/dist_install_docs_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,18 @@ for file in "${INSTALL}" "${README}"; do
fail "${file} must mention OperatorHub discoverability"
done

for file in "${INSTALL}" "${README}"; do
if grep -E 'artifacthub\.io/badge|operatorhub\.io/operator/kollect|artifacthub\.io/packages/helm' "${file}"; then
fail "${file} must not ship live hub badge/listing URLs before registration"
fi
done
# ADR-0708 originally forbade ANY hub URL that 404s before that hub lists us. Artifact Hub
# registration completed 2026-08-18 (repo `kollect`, oci://ghcr.io/platformrelay/kollect,
# Verified Publisher active), so its badge is unambiguously legitimate.
#
# The OperatorHub.io badge ships ahead of the listing by explicit operator decision: the
# community-operators submission is open and green, and operatorhub.io soft-404s (it serves
# HTTP 200 with the generic landing page for unknown operators) rather than showing a broken
# link. Both badges are asserted PRESENT so neither can silently regress.
grep -Fq 'artifacthub.io/badge/repository/kollect' "${README}" ||
fail "${README} must carry the Artifact Hub badge (repository is registered)"
grep -Fq 'operatorhub.io/operator/kollect' "${README}" ||
fail "${README} must carry the OperatorHub.io badge"

pass "install docs describe hub paths without premature listing URLs"

Expand Down
27 changes: 27 additions & 0 deletions hack/test/dist_operatorhub_pr_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,33 @@ grep -Fq 'redhat-openshift-ecosystem/community-operators-prod' "${SCRIPT}" ||
fail "operatorhub-pr.sh must submit to community-operators-prod"
grep -Fq 'v4.19' "${SCRIPT}" ||
fail "operatorhub-pr.sh must annotate OpenShift v4.19 for prod catalog"

# The three checks below inspect EXECUTABLE lines only — the comments in operatorhub-pr.sh
# legitimately name the very anti-patterns being banned, and a naive grep matches its own docs.
CODE="$(grep -v '^[[:space:]]*#' "${SCRIPT}")"

# GNU-only `sed -i <script>` works on the CI runner and fails on BSD/macOS sed, where -i
# consumes the next argument as a backup suffix. This script is documented as manually
# re-runnable, so in-place edits must stay portable (awk, or an explicit -i.bak suffix).
if printf '%s\n' "${CODE}" | grep -Fq "sed -i '"; then
fail "operatorhub-pr.sh must not use GNU-only 'sed -i <script>' (breaks on BSD/macOS sed)"
fi

# The EXISTING-PR LOOKUP must match the head owner case-insensitively. GitHub stores the
# canonical org casing ("PlatformRelay"), so a `gh pr list --head "<owner>:<branch>"` filter
# built from a lower-cased FORK_OWNER matches nothing and the script then tries to open a
# duplicate PR and dies. (`gh pr create --head <owner>:<branch>` is fine — creation resolves
# the owner case-insensitively; only the list filter is a literal string match.)
printf '%s\n' "${CODE}" | grep -Fq 'headRepositoryOwner' ||
fail "operatorhub-pr.sh must resolve an existing PR via headRepositoryOwner (case-insensitive), not a literal \${FORK_OWNER}:\${BRANCH} list filter"
printf '%s\n' "${CODE}" | grep -Fq 'ascii_downcase' ||
fail "operatorhub-pr.sh must compare the PR head owner case-insensitively (ascii_downcase)"

# `gh pr edit` resolves assignees/labels/reviewers over GraphQL and requires `read:org`,
# forcing a broader PAT than this cross-repo submission needs. PATCH via `gh api` instead.
if printf '%s\n' "${CODE}" | grep -Fq 'gh pr edit'; then
fail "operatorhub-pr.sh must not use 'gh pr edit' (requires read:org); PATCH the PR via gh api"
fi
grep -Fq 'FORK_OWNER="${FORK_OWNER:-platformrelay}"' "${SCRIPT}" ||
fail "operatorhub-pr.sh default FORK_OWNER must be platformrelay"
grep -Fq 'DRY_RUN' "${SCRIPT}" ||
Expand Down
Loading