diff --git a/README.md b/README.md index 5b2258d7..38fa2c06 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,8 @@ Go Go Reference Container +Artifact Hub +OperatorHub.io

Simple to start · platform-grade to grow

@@ -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 diff --git a/artifacthub-repo.yml b/artifacthub-repo.yml index 5f0e027e..1d11f168 100644 --- a/artifacthub-repo.yml +++ b/artifacthub-repo.yml @@ -1,13 +1,28 @@ # Artifact Hub repository metadata (ADR-0708), pushed to # ghcr.io//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)$' diff --git a/config/olm/ci.yaml b/config/olm/ci.yaml index 13dd941b..e16efb0d 100644 --- a/config/olm/ci.yaml +++ b/config/olm/ci.yaml @@ -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 diff --git a/config/olm/template/manifests/kollect.clusterserviceversion.yaml b/config/olm/template/manifests/kollect.clusterserviceversion.yaml index 5b0f287e..8036686a 100644 --- a/config/olm/template/manifests/kollect.clusterserviceversion.yaml +++ b/config/olm/template/manifests/kollect.clusterserviceversion.yaml @@ -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 diff --git a/docs/getting-started/install.md b/docs/getting-started/install.md index 213bbcc0..cccf3947 100644 --- a/docs/getting-started/install.md +++ b/docs/getting-started/install.md @@ -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, diff --git a/hack/operatorhub-pr.sh b/hack/operatorhub-pr.sh index d35801f3..961b265a 100755 --- a/hack/operatorhub-pr.sh +++ b/hack/operatorhub-pr.sh @@ -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) @@ -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}/" @@ -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..." diff --git a/hack/test/dist_install_docs_test.sh b/hack/test/dist_install_docs_test.sh index 9ffa0455..2bfae06b 100755 --- a/hack/test/dist_install_docs_test.sh +++ b/hack/test/dist_install_docs_test.sh @@ -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" diff --git a/hack/test/dist_operatorhub_pr_test.sh b/hack/test/dist_operatorhub_pr_test.sh index ecfaa025..cd00c98e 100755 --- a/hack/test/dist_operatorhub_pr_test.sh +++ b/hack/test/dist_operatorhub_pr_test.sh @@ -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