diff --git a/README.md b/README.md index 41d5d08..34ec972 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ flowchart TD | `developer` | Generic CLI dev base (the default: git, docker, shell utilities) | | `developer-gui` | VS Code, Ghostty, DBeaver. Privacy + AI-upsell de-nag profile for VSCode/VSCodium/Cursor off unless `-e vscode_privacy_enabled=true` | | `developer-rust` / `-go` / `-python` / `-node` / `-typescript` / `-c` | Language toolchains | -| `infrastructure` | OpenTofu, OpenBao, AWS CLI, helm, terraform-docs, `k8s` (kubectl, kubectx, kubens, k9s, kind, argocd, dive, kustomize, kubeconform, kube-linter), `data` (clickhouse-client, rpk, valkey-cli, vector), `cloudflare` (flarectl, wrangler) | +| `infrastructure` | OpenTofu, OpenBao, AWS CLI, helm, terraform-docs, `k8s` (kubectl, kubectx, kubens, k9s, kind, argocd, dive, kustomize, kubeconform, kube-linter), `data` (clickhouse-client, confluent-cli, rpk, valkey-cli, vector), `cloudflare` (flarectl, wrangler) | | `contributor` | hyperi-ci + its check tools (semgrep, alint), gitleaks, trivy, hadolint, pip-audit, yamllint, ansible-lint, pre-commit, act, git-scrub, macbash | | `soe` / `soe-gui` | HyperI org policy (opt-in) | | `--full-stack` / `--infra` / `--languages [list]` | Persona bundles (see `--help`) | @@ -185,7 +185,7 @@ digest. Read that before changing a role or adding a tool. - `developer-gui`: VS Code, Ghostty (Solarized theme), DBeaver - `vscode-privacy` (off by default): strips the Copilot/AI upsell UI and the telemetry that stock VSCode ships enabled, across VSCode, VSCodium and Cursor. Enable with `-e vscode_privacy_enabled=true`. It merges one marked block into `settings.json` and never touches a comment or a key it does not manage, backs the file up before its first write, and `-e vscode_privacy_uninstall=true` takes only its own keys back out. Where you have set one of those keys yourself further down the file, yours wins and the run tells you which ones -- so it cannot look applied while changing nothing - Languages: Rust, Go, Python, C/C++, Node.js, TypeScript (the Astral suite -- uv, ruff, ty -- ships in the base, as does Node.js: it is core tooling that semantic-release and CI need) -- `infrastructure`: OpenTofu + OpenBao (the OSS forks, no HashiCorp BUSL tools), AWS CLI v2, checkov, and terraform-docs for generating IaC module reference docs (engine-agnostic -- it reads `.tf` whichever binary runs it, and OpenTofu has no native `tofu docs`). Under `k8s`: kubectl + kubectx + kubens + k9s + kind + argocd + dive + kustomize + kubeconform + kube-linter. helm is NOT in that group -- it sits in `cloud`, so plain `--tags infrastructure` gets it whether or not you select `k8s`. The `data` group: clickhouse-client, rpk, valkey-cli, vector. The `cloudflare` group: flarectl + wrangler (flarectl builds from source on both platforms -- Cloudflare ships no binary -- so Linux needs `developer-go`) +- `infrastructure`: OpenTofu + OpenBao (the OSS forks, no HashiCorp BUSL tools), AWS CLI v2, checkov, and terraform-docs for generating IaC module reference docs (engine-agnostic -- it reads `.tf` whichever binary runs it, and OpenTofu has no native `tofu docs`). Under `k8s`: kubectl + kubectx + kubens + k9s + kind + argocd + dive + kustomize + kubeconform + kube-linter. helm is NOT in that group -- it sits in `cloud`, so plain `--tags infrastructure` gets it whether or not you select `k8s`. The `data` group: clickhouse-client, confluent-cli, rpk, valkey-cli, vector. The `cloudflare` group: flarectl + wrangler (flarectl builds from source on both platforms -- Cloudflare ships no binary -- so Linux needs `developer-go`) - `contributor`: hyperi-ci and the tools its checks drive (semgrep, alint), gitleaks, trivy, hadolint, pip-audit, ansible-lint, pre-commit, act, and git-scrub for rewriting AI residue or a leaked secret out of git history -- gitleaks scans FULL history, so a secret removed from HEAD still fails the gate. macbash is here too: it flags the GNU-only bash constructs that break on macOS, and CONTRIBUTING.md asks for it on every shell change - `soe` / `soe-gui`: HyperI org policy: VPN clients, Claude Code, Slack, LibreOffice, RDP client, telemetry-disable, auto-updates, GNOME taskbar - `developer-ai` (off by default): the OpenAI Codex CLI as a second opinion alongside Claude Code rather than a replacement for it, plus OpenAI's Codex plugin FOR Claude Code, so `/codex:review` and `/codex:adversarial-review` are things Claude asks Codex for. The plugin is skipped -- with a warning naming the tag that fixes it -- unless claude, codex and a new enough node are all present for that user, because it installs happily without them and then throws on every invocation. Sign-in stays the person's: `codex login --device-auth` on a box with no browser diff --git a/ansible/roles/developer-rust/defaults/main.yml b/ansible/roles/developer-rust/defaults/main.yml index 2be1a24..8b2c6dd 100644 --- a/ansible/roles/developer-rust/defaults/main.yml +++ b/ansible/roles/developer-rust/defaults/main.yml @@ -169,3 +169,20 @@ rust_governor_cpu_weight: 30 developer_rust_uid: "{{ actual_user_uid | default(ansible_facts['user_uid']) }}" developer_rust_user_env: XDG_RUNTIME_DIR: "/run/user/{{ developer_rust_uid }}" + +# --------------------------------------------------------------------------- +# librdkafka from Confluent's clients repository +# --------------------------------------------------------------------------- +# Confluent publishes no fingerprint for this key. This pins the key served at +# packages.confluent.io/clients/{deb,rpm}/archive.key, which signs that repo's +# metadata -- the same pin scalo-rs's generated images assert. +developer_rust_confluent_clients_key_fingerprint: CBBB821E8FAF364F79835C438B1DA6120C2BF624 # gitleaks:allow -- public key fingerprint + +# Clients-repo apt suites, oldest first; the last entry is the fallback for an +# Ubuntu release Confluent has not published yet. +developer_rust_confluent_clients_suites: + - noble + - resolute + +# Confluent builds the clients rpms for RHEL only; Fedora takes this EL release. +developer_rust_confluent_clients_el_release: 10 diff --git a/ansible/roles/developer-rust/tasks/rust.yml b/ansible/roles/developer-rust/tasks/rust.yml index 089fb35..d08d10b 100644 --- a/ansible/roles/developer-rust/tasks/rust.yml +++ b/ansible/roles/developer-rust/tasks/rust.yml @@ -385,24 +385,240 @@ # ============================================================ # Not for the cargo tools above -- these build the developer's OWN Rust # services: protoc for prost/tonic (gRPC codegen at build time), and the -# librdkafka headers the rdkafka crate links against. Distro packages, so the -# host's normal updates keep them current. +# librdkafka headers the rdkafka crate links against. Package repos either way, +# so the host's normal updates keep them current. -- name: Install gRPC/Kafka build dependencies (Ubuntu) - ansible.builtin.apt: - name: - - protobuf-compiler - - librdkafka-dev +- name: Install the protobuf compiler (Linux) + ansible.builtin.package: + name: protobuf-compiler state: present - when: ansible_facts['distribution'] == 'Ubuntu' + when: ansible_facts['distribution'] in ['Fedora', 'Ubuntu'] -- name: Install gRPC/Kafka build dependencies (Fedora) - ansible.builtin.dnf: - name: - - protobuf-compiler - - librdkafka-devel - state: present - when: ansible_facts['distribution'] == 'Fedora' +# librdkafka comes from Confluent's clients repo, the build scalo-rs images ship, +# because the distro builds trail the Kafka protocol. `latest`, so a host that +# already holds the distro build moves onto Confluent's. +- name: Install librdkafka from Confluent's clients repository (Linux) + vars: + developer_rust_confluent_clients_key: >- + {{ '/etc/apt/keyrings/confluent-clients.asc' if ansible_facts['distribution'] == 'Ubuntu' + else '/etc/pki/rpm-gpg/RPM-GPG-KEY-confluent-clients' }} + developer_rust_confluent_clients_suite: >- + {{ ansible_facts['distribution_release'] + if ansible_facts['distribution_release'] in developer_rust_confluent_clients_suites + else developer_rust_confluent_clients_suites | last }} + # Check mode adds no repo, so there is nothing to install from yet. + developer_rust_confluent_clients_repo_pending: >- + {{ ansible_check_mode and ((developer_rust_confluent_clients_deb_repo | default({})) is changed + or (developer_rust_confluent_clients_rpm_repo | default({})) is changed) }} + block: + # A minimal Ubuntu ships gpgv but not gpg, which the fingerprint read needs. + - name: Ensure gpg is present for the fingerprint check + ansible.builtin.package: + name: "{{ 'gpg' if ansible_facts['distribution'] == 'Ubuntu' else 'gnupg2' }}" + state: present + + # Staged under a name nothing trusts and forced, so a rejected download is + # replaced on the next run instead of kept by an If-Modified-Since 304. + - name: Download the Confluent clients signing key + ansible.builtin.get_url: + url: >- + https://packages.confluent.io/clients/{{ 'deb' if ansible_facts['distribution'] == 'Ubuntu' else 'rpm' }}/archive.key + dest: "{{ developer_rust_confluent_clients_key }}.unverified" + mode: '0644' + force: true + register: developer_rust_confluent_clients_key_download + + # Check mode downloads nothing, so there is no new key to read. + - name: Read the Confluent clients signing key fingerprint + ansible.builtin.command: + argv: [gpg, --show-keys, --with-colons, "{{ developer_rust_confluent_clients_key }}.unverified"] + register: developer_rust_confluent_clients_key_read + changed_when: false + check_mode: false + when: not (ansible_check_mode and developer_rust_confluent_clients_key_download is changed) + + # Fails closed: one primary key, and the pinned one, or nothing is trusted. + - name: Verify the Confluent clients signing key fingerprint + ansible.builtin.assert: + that: + - developer_rust_key_primaries | int == 1 + - developer_rust_key_fpr == developer_rust_confluent_clients_key_fingerprint + fail_msg: >- + Confluent clients key holds {{ developer_rust_key_primaries }} key(s), first + {{ developer_rust_key_fpr or 'missing' }}; expected only + {{ developer_rust_confluent_clients_key_fingerprint }} + quiet: true + when: not (ansible_check_mode and developer_rust_confluent_clients_key_download is changed) + vars: + developer_rust_key_primaries: >- + {{ developer_rust_confluent_clients_key_read.stdout_lines | select('match', '^pub:') | list | length }} + developer_rust_key_fpr: >- + {{ (developer_rust_confluent_clients_key_read.stdout_lines | select('match', '^fpr:') + | first | default('')).split(':')[9] | default('') }} + + - name: Install the verified Confluent clients signing key + ansible.builtin.copy: + src: "{{ developer_rust_confluent_clients_key }}.unverified" + dest: "{{ developer_rust_confluent_clients_key }}" + remote_src: true + owner: root + group: root + mode: '0644' + when: not (ansible_check_mode and developer_rust_confluent_clients_key_download is changed) + + - name: Add the Confluent clients APT repository (Ubuntu) + ansible.builtin.deb822_repository: + name: confluent-clients + types: deb + uris: https://packages.confluent.io/clients/deb + suites: "{{ developer_rust_confluent_clients_suite }}" + components: main + signed_by: "{{ developer_rust_confluent_clients_key }}" + state: present + register: developer_rust_confluent_clients_deb_repo + when: ansible_facts['distribution'] == 'Ubuntu' + + # Ubuntu ships librdkafka under the same package names, so the Confluent + # build wins on priority rather than on whichever version is higher. + - name: Prefer Confluent's librdkafka packages (Ubuntu) + ansible.builtin.copy: + dest: /etc/apt/preferences.d/confluent-clients + owner: root + group: root + mode: '0644' + content: | + Package: librdkafka* + Pin: origin packages.confluent.io + Pin-Priority: 600 + when: ansible_facts['distribution'] == 'Ubuntu' + + - name: Install librdkafka (Ubuntu) + ansible.builtin.apt: + name: + - librdkafka1 + - librdkafka-dev + state: latest + update_cache: true + when: + - ansible_facts['distribution'] == 'Ubuntu' + - not developer_rust_confluent_clients_repo_pending + + - name: Check for Fedora's own librdkafka (Fedora) + ansible.builtin.command: + argv: [rpm, -q, librdkafka] + register: developer_rust_fedora_librdkafka + changed_when: false + failed_when: false + check_mode: false + when: ansible_facts['distribution'] == 'Fedora' + + # librdkafka1 does not provide the name `librdkafka`, so a package that + # requires it by name (python3-confluent-kafka does) blocks the swap. Checked + # before the repo goes in, so a blocked host keeps a working dnf on every run. + - name: Find packages that require Fedora's librdkafka by name (Fedora) + ansible.builtin.command: + argv: [rpm, -q, --whatrequires, librdkafka, --qf, "%{NAME}\n"] + register: developer_rust_librdkafka_dependants + changed_when: false + failed_when: false + check_mode: false + when: + - ansible_facts['distribution'] == 'Fedora' + - developer_rust_fedora_librdkafka.rc == 0 + + - name: Keep Fedora's librdkafka while a package requires it by name (Fedora) + ansible.builtin.fail: + msg: >- + kept Fedora's librdkafka (required by name by: + {{ developer_rust_librdkafka_dependants.stdout_lines | join(', ') }}); + Confluent's was not installed + when: + - ansible_facts['distribution'] == 'Fedora' + - developer_rust_fedora_librdkafka.rc == 0 + - developer_rust_librdkafka_dependants.rc == 0 + + # Confluent publishes RHEL builds only; Fedora takes the newest EL one, + # which links against an older glibc than Fedora ships. The priority masks + # Fedora's librdkafka-devel, which shares the Confluent package's name. + - name: Add the Confluent clients repository (Fedora) + ansible.builtin.yum_repository: + name: confluent-clients + description: Confluent clients + baseurl: "https://packages.confluent.io/clients/rpm/centos/{{ developer_rust_confluent_clients_el_release }}/$basearch" + enabled: true + gpgcheck: true + repo_gpgcheck: true + gpgkey: "file://{{ developer_rust_confluent_clients_key }}" + priority: '10' + register: developer_rust_confluent_clients_rpm_repo + when: ansible_facts['distribution'] == 'Fedora' + + # Fedora's librdkafka and Confluent's librdkafka1 ship the same library + # files under different names, so one has to replace the other in a single + # transaction; packages linked against librdkafka.so.1 stay satisfied. + # No --allowerasing: a package that needs Fedora's build by name fails the + # swap instead of being removed with it. + - name: Swap Fedora's librdkafka for Confluent's (Fedora) + ansible.builtin.command: + argv: [dnf, -y, swap, librdkafka, librdkafka1] + changed_when: true + when: + - ansible_facts['distribution'] == 'Fedora' + - developer_rust_fedora_librdkafka.rc == 0 + + - name: Install librdkafka (Fedora) + ansible.builtin.dnf: + name: + - librdkafka1 + - librdkafka-devel + state: latest + when: + - ansible_facts['distribution'] == 'Fedora' + - not developer_rust_confluent_clients_repo_pending + + rescue: + # After a key rotation the repo no longer verifies against the trusted + # key, and the apt module fails every later cache refresh in the run on it, + # where apt-get only warns. The previously trusted key stays. + # Deleted as a file: deb822_repository state=absent also deletes the + # keyring of the same name, which is the trusted key here. + - name: Remove the Confluent clients APT repository after a key mismatch (Ubuntu) + ansible.builtin.file: + path: /etc/apt/sources.list.d/confluent-clients.sources + state: absent + when: + - ansible_failed_task.action in ['assert', 'ansible.builtin.assert'] + - ansible_facts['distribution'] == 'Ubuntu' + + - name: Check whether Fedora's librdkafka is still installed (Fedora) + ansible.builtin.command: + argv: [rpm, -q, librdkafka] + register: developer_rust_fedora_librdkafka_kept + changed_when: false + failed_when: false + check_mode: false + when: ansible_facts['distribution'] == 'Fedora' + + # Also covers a refused swap: with Fedora's librdkafka kept, the + # higher-priority Confluent librdkafka-devel would fail dnf upgrades on its + # file conflict. + - name: Remove the Confluent clients repository (Fedora) + ansible.builtin.yum_repository: + name: confluent-clients + state: absent + when: + - ansible_facts['distribution'] == 'Fedora' + - (ansible_failed_task.action in ['assert', 'ansible.builtin.assert']) + or developer_rust_fedora_librdkafka_kept.rc == 0 + + - name: Record that librdkafka did not install + # noqa: var-naming[no-role-prefix] -- deploy_warnings is the shared accumulator + ansible.builtin.set_fact: + deploy_warnings: >- + {{ deploy_warnings | default([]) + + ['librdkafka: ' ~ (ansible_failed_result.msg | default('repo/install failed'))] }} + + when: ansible_facts['distribution'] in ['Fedora', 'Ubuntu'] - name: Install gRPC/Kafka build dependencies (macOS) community.general.homebrew: diff --git a/ansible/roles/infrastructure/defaults/main.yml b/ansible/roles/infrastructure/defaults/main.yml index c6cd3aa..d1bb878 100644 --- a/ansible/roles/infrastructure/defaults/main.yml +++ b/ansible/roles/infrastructure/defaults/main.yml @@ -9,6 +9,18 @@ infrastructure_helm_apt_key_fingerprint: DDF78C3E6EBB2D2CC223C95C62BA89D07698DBC6 # gitleaks:allow -- public key fingerprint infrastructure_helm_apt_key_url: https://packages.buildkite.com/helm-linux/helm-debian/gpgkey +# Redpanda, Confluent and ClickHouse publish no fingerprint for their repo keys, +# so these pin the key each serves at the URL in data_tools.yml, checked to sign +# that repo's current metadata. A rotation then fails the install loudly. +# +# Redpanda's repos sit on Google Artifact Registry: its key signs the apt +# metadata, Redpanda's own key signs the rpms. +infrastructure_redpanda_repo_key_fingerprint: 35BAA0B33E9EB396F59CA838C0BA5CE6DC6315A3 # gitleaks:allow -- public key fingerprint +infrastructure_redpanda_rpm_key_fingerprint: 16C58F679A886A0A8468225BC45B532A7981C4D8 # gitleaks:allow -- public key fingerprint +infrastructure_confluent_cli_deb_key_fingerprint: BF154723E8BB4B969BA7BAE1F00BDC5567B31D07 # gitleaks:allow -- public key fingerprint +infrastructure_confluent_cli_rpm_key_fingerprint: 62B65702E8AA3A70B538C8D1E0ECCDAE9610976F # gitleaks:allow -- public key fingerprint +infrastructure_clickhouse_key_fingerprint: 3A9EA1193A97B548BE1457D48919F6BD2B48D754 # gitleaks:allow -- public key fingerprint + # Microsoft publishes the Azure CLI repo per Ubuntu codename and has no resolute # suite, so a resolute host takes the noble build deliberately. # Tracks the supported window (min_ubuntu_version), oldest first; the last entry diff --git a/ansible/roles/infrastructure/tasks/data_tools.yml b/ansible/roles/infrastructure/tasks/data_tools.yml index 8aa83d7..065e37f 100644 --- a/ansible/roles/infrastructure/tasks/data_tools.yml +++ b/ansible/roles/infrastructure/tasks/data_tools.yml @@ -1,16 +1,15 @@ --- -# Data platform tools (Vector, ClickHouse, Confluent CLI) +# Data platform tools (Vector, ClickHouse, Confluent CLI, rpk, valkey-cli) # # macOS notes: -# - Vector: skipped — removed from homebrew-core; would need an unofficial +# - Vector: skipped -- removed from homebrew-core; would need an unofficial # tap. If you need Vector on Mac, install manually with the tap that # works for your current Vector version. -# - ClickHouse: skipped — heavyweight, not part of standard Mac dev workflow. +# - ClickHouse: skipped -- heavyweight, not part of standard Mac dev workflow. # Manual install if needed: `brew install --cask clickhouse-client` or # run via container. -# - Confluent CLI: INSTALLED on macOS — needs the confluentinc/tap added +# - Confluent CLI: INSTALLED on macOS -- needs the confluentinc/tap added # first (which is done below). -# Linux branches below are unchanged. - name: Skipped data platform tools notice (macOS) ansible.builtin.debug: @@ -20,6 +19,13 @@ Confluent CLI IS installed below via the confluentinc/tap. when: ansible_facts['distribution'] == 'MacOSX' +# A minimal Ubuntu ships gpgv but not gpg, which the vendor key checks need. +- name: Ensure gpg is present for the vendor key checks (Linux) + ansible.builtin.package: + name: "{{ 'gpg' if ansible_facts['distribution'] == 'Ubuntu' else 'gnupg2' }}" + state: present + when: ansible_facts['distribution'] in ['Fedora', 'Ubuntu'] + # ============================================================================ # VECTOR - Log aggregation and routing # ============================================================================ @@ -94,43 +100,102 @@ when: ansible_facts['distribution'] == 'Ubuntu' -# Vector intentionally not installed on macOS — see header comment. +# Vector intentionally not installed on macOS -- see header comment. # ============================================================================ # CLICKHOUSE - Analytics database client # ============================================================================ -- name: Install ClickHouse client (Fedora) +- name: Install ClickHouse client (Linux) + vars: + infrastructure_clickhouse_key: + url: https://packages.clickhouse.com/rpm/lts/repodata/repomd.xml.key + dest: >- + {{ '/etc/apt/keyrings/clickhouse.asc' if ansible_facts['distribution'] == 'Ubuntu' + else '/etc/pki/rpm-gpg/RPM-GPG-KEY-clickhouse' }} + fingerprint: "{{ infrastructure_clickhouse_key_fingerprint }}" + # Check mode adds no repo, so there is nothing to install from yet. + infrastructure_clickhouse_repo_pending: >- + {{ ansible_check_mode and ((infrastructure_clickhouse_deb_repo | default({})) is changed + or (infrastructure_clickhouse_rpm_repo | default({})) is changed) }} block: - - name: Add ClickHouse repository + # Runs before any apt call: apt reads a leftover .list beside the .sources + # written below as a second copy of the repo. + - name: Remove the legacy ClickHouse .list (Ubuntu) + ansible.builtin.file: + path: /etc/apt/sources.list.d/clickhouse.list + state: absent + when: ansible_facts['distribution'] == 'Ubuntu' + + # Staged under a name nothing trusts and forced, so a rejected download is + # replaced on the next run instead of kept by an If-Modified-Since 304. + - name: Download the ClickHouse signing key + ansible.builtin.get_url: + url: "{{ infrastructure_clickhouse_key.url }}" + dest: "{{ infrastructure_clickhouse_key.dest }}.unverified" + mode: '0644' + force: true + register: infrastructure_clickhouse_key_download + + # Check mode downloads nothing, so there is no new key to read. + - name: Read the ClickHouse signing key fingerprint + ansible.builtin.command: + argv: [gpg, --show-keys, --with-colons, "{{ infrastructure_clickhouse_key.dest }}.unverified"] + register: infrastructure_clickhouse_key_read + changed_when: false + check_mode: false + when: not (ansible_check_mode and infrastructure_clickhouse_key_download is changed) + + # Fails closed: one primary key, and the pinned one, or nothing is trusted. + - name: Verify the ClickHouse signing key fingerprint + ansible.builtin.assert: + that: + - infrastructure_key_primaries | int == 1 + - infrastructure_key_fpr == infrastructure_clickhouse_key.fingerprint + fail_msg: >- + {{ infrastructure_clickhouse_key.url }} holds {{ infrastructure_key_primaries }} key(s), first + {{ infrastructure_key_fpr or 'missing' }}; expected only {{ infrastructure_clickhouse_key.fingerprint }} + quiet: true + when: not (ansible_check_mode and infrastructure_clickhouse_key_download is changed) + vars: + infrastructure_key_primaries: "{{ infrastructure_clickhouse_key_read.stdout_lines | select('match', '^pub:') | list | length }}" + infrastructure_key_fpr: >- + {{ (infrastructure_clickhouse_key_read.stdout_lines | select('match', '^fpr:') | first | default('')).split(':')[9] | default('') }} + + - name: Install the verified ClickHouse signing key + ansible.builtin.copy: + src: "{{ infrastructure_clickhouse_key.dest }}.unverified" + dest: "{{ infrastructure_clickhouse_key.dest }}" + remote_src: true + owner: root + group: root + mode: '0644' + when: not (ansible_check_mode and infrastructure_clickhouse_key_download is changed) + + # ClickHouse signs its repo metadata but not its rpms, so gpgcheck stays off + # and the signed repomd, with the package checksums it lists, vouches for + # each package. + - name: Add ClickHouse repository (Fedora) ansible.builtin.yum_repository: name: clickhouse-stable description: ClickHouse - Stable Repository baseurl: https://packages.clickhouse.com/rpm/stable/ enabled: true gpgcheck: false + repo_gpgcheck: true + gpgkey: file:///etc/pki/rpm-gpg/RPM-GPG-KEY-clickhouse + register: infrastructure_clickhouse_rpm_repo + when: ansible_facts['distribution'] == 'Fedora' - - name: Install ClickHouse client + - name: Install ClickHouse client (Fedora) ansible.builtin.dnf: name: clickhouse-client state: present + when: + - ansible_facts['distribution'] == 'Fedora' + - not infrastructure_clickhouse_repo_pending - when: ansible_facts['distribution'] == 'Fedora' - -- name: Install ClickHouse client (Ubuntu) - block: - - name: Add ClickHouse GPG key - ansible.builtin.get_url: - url: https://packages.clickhouse.com/rpm/lts/repodata/repomd.xml.key - dest: /etc/apt/keyrings/clickhouse.asc - mode: '0644' - - - name: Remove the legacy ClickHouse .list - ansible.builtin.file: - path: /etc/apt/sources.list.d/clickhouse.list - state: absent - - - name: Add ClickHouse APT repository + - name: Add ClickHouse APT repository (Ubuntu) ansible.builtin.deb822_repository: name: clickhouse types: deb @@ -139,95 +204,269 @@ components: main signed_by: /etc/apt/keyrings/clickhouse.asc state: present + register: infrastructure_clickhouse_deb_repo + when: ansible_facts['distribution'] == 'Ubuntu' - - name: Install ClickHouse client + - name: Install ClickHouse client (Ubuntu) ansible.builtin.apt: name: clickhouse-client state: present update_cache: true + when: + - ansible_facts['distribution'] == 'Ubuntu' + - not infrastructure_clickhouse_repo_pending - when: ansible_facts['distribution'] == 'Ubuntu' + rescue: + # After a key rotation the repo no longer verifies against the trusted + # key, and the apt module fails every later cache refresh in the run on it, + # where apt-get only warns. The previously trusted key stays. + # Deleted as a file: deb822_repository state=absent also deletes the + # keyring of the same name, which is the trusted key here. + - name: Remove the ClickHouse APT repository after a key mismatch (Ubuntu) + ansible.builtin.file: + path: /etc/apt/sources.list.d/clickhouse.sources + state: absent + when: + - ansible_failed_task.action in ['assert', 'ansible.builtin.assert'] + - ansible_facts['distribution'] == 'Ubuntu' -# ClickHouse intentionally not installed on macOS — see header comment. + - name: Remove the ClickHouse repository after a key mismatch (Fedora) + ansible.builtin.yum_repository: + name: clickhouse-stable + state: absent + when: + - ansible_failed_task.action in ['assert', 'ansible.builtin.assert'] + - ansible_facts['distribution'] == 'Fedora' + + - name: Record that the ClickHouse client did not install + # noqa: var-naming[no-role-prefix] -- deploy_warnings is the shared accumulator + ansible.builtin.set_fact: + deploy_warnings: >- + {{ deploy_warnings | default([]) + + ['ClickHouse client: ' ~ (ansible_failed_result.msg | default('repo/install failed'))] }} + + when: ansible_facts['distribution'] in ['Fedora', 'Ubuntu'] + +# ClickHouse intentionally not installed on macOS -- see header comment. # ============================================================================ # CONFLUENT CLI - Kafka client tools # ============================================================================ -- name: Install Confluent CLI (Fedora) +# The CLI has its own unversioned repo. Confluent Platform's per-release repos +# also carry a confluent-cli, numbered by the platform release (8.x) rather than +# the CLI's own (4.x), so it outranks the real CLI on version. +- name: Install Confluent CLI from its package repository (Linux) + vars: + infrastructure_confluent_cli_key: + url: >- + https://packages.confluent.io/confluent-cli/{{ 'deb' if ansible_facts['distribution'] == 'Ubuntu' else 'rpm' }}/archive.key + dest: >- + {{ '/etc/apt/keyrings/confluent-cli.asc' if ansible_facts['distribution'] == 'Ubuntu' + else '/etc/pki/rpm-gpg/RPM-GPG-KEY-confluent-cli' }} + fingerprint: >- + {{ infrastructure_confluent_cli_deb_key_fingerprint if ansible_facts['distribution'] == 'Ubuntu' + else infrastructure_confluent_cli_rpm_key_fingerprint }} + # Check mode adds no repo, so there is nothing to install from yet. + infrastructure_confluent_cli_repo_pending: >- + {{ ansible_check_mode and ((infrastructure_confluent_cli_deb_repo | default({})) is changed + or (infrastructure_confluent_cli_rpm_repo | default({})) is changed) }} block: - - name: Detect latest Confluent version - ansible.builtin.uri: - url: https://packages.confluent.io/rpm/ - return_content: true - register: confluent_page + # The Confluent Platform repo an older revision configured. Runs before any + # apt or dnf call so neither reads it again. + - name: Remove the Confluent Platform apt repo and key (Ubuntu) + ansible.builtin.file: + path: "{{ item }}" + state: absent + loop: + - /etc/apt/sources.list.d/confluent.sources + - /etc/apt/sources.list.d/confluent.list + - /etc/apt/keyrings/confluent.asc + when: ansible_facts['distribution'] == 'Ubuntu' - - name: Extract Confluent version - ansible.builtin.set_fact: - confluent_version: "{{ confluent_page.content | regex_findall('[0-9]+\\.[0-9]+') | sort | last | default('8.1') }}" + - name: Remove the Confluent Platform repository (Fedora) + ansible.builtin.yum_repository: + name: Confluent + state: absent + when: ansible_facts['distribution'] == 'Fedora' + + # Staged under a name nothing trusts and forced, so a rejected download is + # replaced on the next run instead of kept by an If-Modified-Since 304. + - name: Download the Confluent CLI signing key + ansible.builtin.get_url: + url: "{{ infrastructure_confluent_cli_key.url }}" + dest: "{{ infrastructure_confluent_cli_key.dest }}.unverified" + mode: '0644' + force: true + register: infrastructure_confluent_cli_key_download + + # Check mode downloads nothing, so there is no new key to read. + - name: Read the Confluent CLI signing key fingerprint + ansible.builtin.command: + argv: [gpg, --show-keys, --with-colons, "{{ infrastructure_confluent_cli_key.dest }}.unverified"] + register: infrastructure_confluent_cli_key_read + changed_when: false + check_mode: false + when: not (ansible_check_mode and infrastructure_confluent_cli_key_download is changed) + + # Fails closed: one primary key, and the pinned one, or nothing is trusted. + - name: Verify the Confluent CLI signing key fingerprint + ansible.builtin.assert: + that: + - infrastructure_key_primaries | int == 1 + - infrastructure_key_fpr == infrastructure_confluent_cli_key.fingerprint + fail_msg: >- + {{ infrastructure_confluent_cli_key.url }} holds {{ infrastructure_key_primaries }} key(s), first + {{ infrastructure_key_fpr or 'missing' }}; expected only {{ infrastructure_confluent_cli_key.fingerprint }} + quiet: true + when: not (ansible_check_mode and infrastructure_confluent_cli_key_download is changed) + vars: + infrastructure_key_primaries: "{{ infrastructure_confluent_cli_key_read.stdout_lines | select('match', '^pub:') | list | length }}" + infrastructure_key_fpr: >- + {{ (infrastructure_confluent_cli_key_read.stdout_lines | select('match', '^fpr:') | first | default('')).split(':')[9] | default('') }} + + - name: Install the verified Confluent CLI signing key + ansible.builtin.copy: + src: "{{ infrastructure_confluent_cli_key.dest }}.unverified" + dest: "{{ infrastructure_confluent_cli_key.dest }}" + remote_src: true + owner: root + group: root + mode: '0644' + when: not (ansible_check_mode and infrastructure_confluent_cli_key_download is changed) - - name: Import Confluent GPG key - ansible.builtin.rpm_key: - key: "https://packages.confluent.io/rpm/{{ confluent_version }}/archive.key" + - name: Add the Confluent CLI APT repository (Ubuntu) + ansible.builtin.deb822_repository: + name: confluent-cli + types: deb + uris: https://packages.confluent.io/confluent-cli/deb + suites: stable + components: main + signed_by: /etc/apt/keyrings/confluent-cli.asc state: present + register: infrastructure_confluent_cli_deb_repo + when: ansible_facts['distribution'] == 'Ubuntu' - - name: Add Confluent repository + # Both the packages and the repo metadata are signed with the same key. + - name: Add the Confluent CLI repository (Fedora) ansible.builtin.yum_repository: - name: Confluent - description: Confluent repository - baseurl: "https://packages.confluent.io/rpm/{{ confluent_version }}" + name: confluent-cli + description: Confluent CLI + baseurl: https://packages.confluent.io/confluent-cli/rpm enabled: true gpgcheck: true - gpgkey: "https://packages.confluent.io/rpm/{{ confluent_version }}/archive.key" + repo_gpgcheck: true + gpgkey: file:///etc/pki/rpm-gpg/RPM-GPG-KEY-confluent-cli + register: infrastructure_confluent_cli_rpm_repo + when: ansible_facts['distribution'] == 'Fedora' - - name: Install Confluent CLI - ansible.builtin.dnf: - name: confluent-cli - state: present + # Also drops the Platform repo's lists, so the version read below sees only + # what the CLI repo offers. + - name: Refresh the apt cache (Ubuntu) + ansible.builtin.apt: + update_cache: true + changed_when: false + when: ansible_facts['distribution'] == 'Ubuntu' - when: ansible_facts['distribution'] == 'Fedora' + - name: Read the confluent-cli versions the CLI repo offers (Ubuntu) + ansible.builtin.command: + argv: [apt-cache, madison, confluent-cli] + register: infrastructure_confluent_cli_madison + changed_when: false + check_mode: false + when: ansible_facts['distribution'] == 'Ubuntu' -- name: Install Confluent CLI (Ubuntu) - block: - - name: Detect latest Confluent version - ansible.builtin.uri: - url: https://packages.confluent.io/deb/ - return_content: true - register: confluent_page_ubuntu + # -y imports the repo key on first use; without it dnf skips the repo and + # the query comes back empty. + - name: Read the confluent-cli versions the CLI repo offers (Fedora) + ansible.builtin.command: + argv: [dnf, -y, -q, repoquery, --available, --qf, "%{evr}\n", confluent-cli] + register: infrastructure_confluent_cli_repoquery + changed_when: false + check_mode: false + when: ansible_facts['distribution'] == 'Fedora' + + # Empty when confluent-cli is not installed. + - name: Read the installed confluent-cli version + ansible.builtin.command: + argv: >- + {{ ['dpkg-query', '-W', '-f=${Version}', 'confluent-cli'] + if ansible_facts['distribution'] == 'Ubuntu' + else ['rpm', '-q', '--qf', '%{VERSION}-%{RELEASE}', 'confluent-cli'] }} + register: infrastructure_confluent_cli_installed + changed_when: false + failed_when: false check_mode: false - - name: Extract Confluent version + - name: Collect the confluent-cli versions the CLI repo offers ansible.builtin.set_fact: - confluent_version_ubuntu: "{{ confluent_page_ubuntu.content | regex_findall('[0-9]+\\.[0-9]+') | sort | last | default('8.1') }}" + infrastructure_confluent_cli_offered: >- + {{ infrastructure_confluent_cli_madison.stdout_lines | map('split', '|') | map(attribute=1) | map('trim') | list + if ansible_facts['distribution'] == 'Ubuntu' + else infrastructure_confluent_cli_repoquery.stdout_lines | unique }} + + # A Platform-repo build outranks every CLI-repo version, so the package + # manager never replaces it on its own. Pinning the CLI repo's newest build + # with a downgrade swaps the two in one transaction, so a failed download + # leaves the old CLI in place. + - name: Replace a confluent-cli the CLI repo does not carry (Ubuntu) + ansible.builtin.apt: + name: "confluent-cli={{ infrastructure_confluent_cli_offered | community.general.version_sort | last }}" + allow_downgrade: true + when: + - ansible_facts['distribution'] == 'Ubuntu' + - not infrastructure_confluent_cli_repo_pending + - infrastructure_confluent_cli_installed.rc == 0 + - infrastructure_confluent_cli_offered | length > 0 + - infrastructure_confluent_cli_installed.stdout not in infrastructure_confluent_cli_offered + + - name: Replace a confluent-cli the CLI repo does not carry (Fedora) + ansible.builtin.dnf: + name: "confluent-cli-{{ infrastructure_confluent_cli_offered | community.general.version_sort | last }}" + allow_downgrade: true + when: + - ansible_facts['distribution'] == 'Fedora' + - not infrastructure_confluent_cli_repo_pending + - infrastructure_confluent_cli_installed.rc == 0 + - infrastructure_confluent_cli_offered | length > 0 + - infrastructure_confluent_cli_installed.stdout not in infrastructure_confluent_cli_offered - - name: Add Confluent GPG key - ansible.builtin.get_url: - url: "https://packages.confluent.io/deb/{{ confluent_version_ubuntu }}/archive.key" - dest: /etc/apt/keyrings/confluent.asc - mode: '0644' + - name: Install Confluent CLI + ansible.builtin.package: + name: confluent-cli + state: present + when: not infrastructure_confluent_cli_repo_pending - - name: Remove the legacy Confluent .list + rescue: + # After a key rotation the repo no longer verifies against the trusted + # key, and the apt module fails every later cache refresh in the run on it, + # where apt-get only warns. The previously trusted key stays. + # Deleted as a file: deb822_repository state=absent also deletes the + # keyring of the same name, which is the trusted key here. + - name: Remove the Confluent CLI APT repository after a key mismatch (Ubuntu) ansible.builtin.file: - path: /etc/apt/sources.list.d/confluent.list + path: /etc/apt/sources.list.d/confluent-cli.sources state: absent + when: + - ansible_failed_task.action in ['assert', 'ansible.builtin.assert'] + - ansible_facts['distribution'] == 'Ubuntu' - - name: Add Confluent APT repository - ansible.builtin.deb822_repository: - name: confluent - types: deb - uris: "https://packages.confluent.io/deb/{{ confluent_version_ubuntu }}" - suites: stable - components: main - signed_by: /etc/apt/keyrings/confluent.asc - state: present - - - name: Install Confluent CLI - ansible.builtin.apt: + - name: Remove the Confluent CLI repository after a key mismatch (Fedora) + ansible.builtin.yum_repository: name: confluent-cli - state: present - update_cache: true + state: absent + when: + - ansible_failed_task.action in ['assert', 'ansible.builtin.assert'] + - ansible_facts['distribution'] == 'Fedora' - when: ansible_facts['distribution'] == 'Ubuntu' + - name: Record that the Confluent CLI did not install + # noqa: var-naming[no-role-prefix] -- deploy_warnings is the shared accumulator + ansible.builtin.set_fact: + deploy_warnings: >- + {{ deploy_warnings | default([]) + + ['Confluent CLI: ' ~ (ansible_failed_result.msg | default('repo/install failed'))] }} + + when: ansible_facts['distribution'] in ['Fedora', 'Ubuntu'] - name: Tap Confluent formulae (macOS) community.general.homebrew_tap: @@ -248,49 +487,157 @@ # ============================================================================ # RPK - Redpanda / Kafka CLI (data group) # ============================================================================ -# The Redpanda vendor repo (linux.pkg.redpanda.com) is OS-swept BUT its -# `redpanda` package installs the FULL broker to get rpk -- the wrong footprint -# for a dev workstation. We take the standalone rpk zip from GitHub releases -# instead (Tier 3: hyperi-update re-fetches it). If you want the OS-swept vendor -# repo, it is `redpanda` from https://linux.pkg.redpanda.com (broker included). -- name: Install rpk (Linux) +# redpanda-rpk from Redpanda's package repo is rpk alone; the `redpanda` broker +# package depends on it rather than the other way round. Taking it from the repo +# puts rpk under the host's normal package updates. +- name: Install rpk from the Redpanda package repository (Linux) + vars: + infrastructure_redpanda_key: + url: >- + {{ 'https://linux.pkg.redpanda.com/redpanda-deb-signing-public.gpg' + if ansible_facts['distribution'] == 'Ubuntu' + else 'https://linux.pkg.redpanda.com/redpanda-rpm-signing-public.key' }} + dest: >- + {{ '/etc/apt/keyrings/redpanda.asc' if ansible_facts['distribution'] == 'Ubuntu' + else '/etc/pki/rpm-gpg/RPM-GPG-KEY-redpanda' }} + fingerprint: >- + {{ infrastructure_redpanda_repo_key_fingerprint if ansible_facts['distribution'] == 'Ubuntu' + else infrastructure_redpanda_rpm_key_fingerprint }} + # Check mode adds no repo, so there is nothing to install from yet. + infrastructure_redpanda_repo_pending: >- + {{ ansible_check_mode and ((infrastructure_redpanda_deb_repo | default({})) is changed + or (infrastructure_redpanda_rpm_repo | default({})) is changed) }} block: - - name: Ensure unzip is present for rpk - ansible.builtin.package: - name: unzip + # Staged under a name nothing trusts and forced, so a rejected download is + # replaced on the next run instead of kept by an If-Modified-Since 304. + - name: Download the Redpanda signing key + ansible.builtin.get_url: + url: "{{ infrastructure_redpanda_key.url }}" + dest: "{{ infrastructure_redpanda_key.dest }}.unverified" + mode: '0644' + force: true + register: infrastructure_redpanda_key_download + + # Check mode downloads nothing, so there is no new key to read. + - name: Read the Redpanda signing key fingerprint + ansible.builtin.command: + argv: [gpg, --show-keys, --with-colons, "{{ infrastructure_redpanda_key.dest }}.unverified"] + register: infrastructure_redpanda_key_read + changed_when: false + check_mode: false + when: not (ansible_check_mode and infrastructure_redpanda_key_download is changed) + + # Fails closed: one primary key, and the pinned one, or nothing is trusted. + - name: Verify the Redpanda signing key fingerprint + ansible.builtin.assert: + that: + - infrastructure_key_primaries | int == 1 + - infrastructure_key_fpr == infrastructure_redpanda_key.fingerprint + fail_msg: >- + {{ infrastructure_redpanda_key.url }} holds {{ infrastructure_key_primaries }} key(s), first + {{ infrastructure_key_fpr or 'missing' }}; expected only {{ infrastructure_redpanda_key.fingerprint }} + quiet: true + when: not (ansible_check_mode and infrastructure_redpanda_key_download is changed) + vars: + infrastructure_key_primaries: "{{ infrastructure_redpanda_key_read.stdout_lines | select('match', '^pub:') | list | length }}" + infrastructure_key_fpr: >- + {{ (infrastructure_redpanda_key_read.stdout_lines | select('match', '^fpr:') | first | default('')).split(':')[9] | default('') }} + + - name: Install the verified Redpanda signing key + ansible.builtin.copy: + src: "{{ infrastructure_redpanda_key.dest }}.unverified" + dest: "{{ infrastructure_redpanda_key.dest }}" + remote_src: true + owner: root + group: root + mode: '0644' + when: not (ansible_check_mode and infrastructure_redpanda_key_download is changed) + + - name: Add the Redpanda APT repository (Ubuntu) + ansible.builtin.deb822_repository: + name: redpanda + types: deb + uris: https://linux.pkg.redpanda.com/apt + suites: redpanda-apt + components: main + signed_by: /etc/apt/keyrings/redpanda.asc state: present + register: infrastructure_redpanda_deb_repo + when: ansible_facts['distribution'] == 'Ubuntu' - - name: Get latest Redpanda release (carries the rpk asset) - ansible.builtin.uri: - url: https://api.github.com/repos/redpanda-data/redpanda/releases/latest - return_content: true - headers: "{{ hyperi_github_headers }}" - register: rpk_latest - check_mode: false + # dnf rejects the signature Artifact Registry puts on the yum metadata as + # bad, so only the rpms' own signatures are checked. + - name: Add the Redpanda repository (Fedora) + ansible.builtin.yum_repository: + name: redpanda + description: Redpanda + baseurl: https://linux.pkg.redpanda.com/yum/redpanda-yum + enabled: true + gpgcheck: true + repo_gpgcheck: false + gpgkey: file:///etc/pki/rpm-gpg/RPM-GPG-KEY-redpanda + register: infrastructure_redpanda_rpm_repo + when: ansible_facts['distribution'] == 'Fedora' - - name: Download and extract rpk - ansible.builtin.unarchive: - src: "https://github.com/redpanda-data/redpanda/releases/download/{{ rpk_latest.json.tag_name }}/rpk-linux-{{ hyperi_arch_deb }}.zip" - dest: /usr/local/bin - remote_src: true - include: rpk - mode: '0755' - when: not ansible_check_mode + - name: Install rpk (Ubuntu) + ansible.builtin.apt: + name: redpanda-rpk + state: present + update_cache: true + when: + - ansible_facts['distribution'] == 'Ubuntu' + - not infrastructure_redpanda_repo_pending + + - name: Install rpk (Fedora) + ansible.builtin.dnf: + name: redpanda-rpk + state: present + when: + - ansible_facts['distribution'] == 'Fedora' + - not infrastructure_redpanda_repo_pending + + # The GitHub zip an older revision unpacked here shadows the packaged rpk on + # PATH. Reached only once the package above is in, since a failed install + # leaves the block for the rescue. + - name: Remove the superseded /usr/local/bin/rpk + ansible.builtin.file: + path: /usr/local/bin/rpk + state: absent rescue: + # After a key rotation the repo no longer verifies against the trusted + # key, and the apt module fails every later cache refresh in the run on it, + # where apt-get only warns. The previously trusted key stays. + # Deleted as a file: deb822_repository state=absent also deletes the + # keyring of the same name, which is the trusted key here. + - name: Remove the Redpanda APT repository after a key mismatch (Ubuntu) + ansible.builtin.file: + path: /etc/apt/sources.list.d/redpanda.sources + state: absent + when: + - ansible_failed_task.action in ['assert', 'ansible.builtin.assert'] + - ansible_facts['distribution'] == 'Ubuntu' + + - name: Remove the Redpanda repository after a key mismatch (Fedora) + ansible.builtin.yum_repository: + name: redpanda + state: absent + when: + - ansible_failed_task.action in ['assert', 'ansible.builtin.assert'] + - ansible_facts['distribution'] == 'Fedora' + - name: Record that rpk did not install + # noqa: var-naming[no-role-prefix] -- deploy_warnings is the shared accumulator ansible.builtin.set_fact: deploy_warnings: >- {{ deploy_warnings | default([]) - + ['rpk: ' ~ (ansible_failed_result.msg | default('download failed'))] }} + + ['rpk: ' ~ (ansible_failed_result.msg | default('repo/install failed'))] }} when: ansible_facts['distribution'] in ['Fedora', 'Ubuntu'] - name: Install rpk via Homebrew (macOS) block: - # TODO verify: the redpanda-data/tap formula name/availability is UNVERIFIED - # (flagged in the plan). If the tap or formula has moved this warns, and the - # standalone rpk binary from GitHub is the fallback. + # A moved tap or formula lands in the rescue as a warning. - name: Tap redpanda-data formulae (macOS) community.general.homebrew_tap: name: redpanda-data/tap diff --git a/docs/install-matrix.md b/docs/install-matrix.md index 6c25f5d..8827884 100644 --- a/docs/install-matrix.md +++ b/docs/install-matrix.md @@ -127,7 +127,7 @@ without the whole role. |---|---|---|---| | ai | `ai` | developer-ai | Codex CLI, the Codex plugin for Claude Code | | cloudflare | `cloudflare` | infrastructure | flarectl, wrangler | -| data | `data` | infrastructure | clickhouse-client, rpk, valkey-cli, vector | +| data | `data` | infrastructure | clickhouse-client, confluent-cli, rpk, valkey-cli, vector | | forgejo | `forgejo` / `codeberg` | soe | tea (Forgejo/Gitea CLI) | | rdp-client | `rdp-client` | rdp-client | Remmina (Linux), Thincast (macOS) | | vpn-clients | `vpn-clients` | vpn-clients | OpenVPN 3, WireGuard, Tunnelblick (macOS) | @@ -210,7 +210,8 @@ is the meta-role pulling them all. | cargo-audit, cargo-hack, cargo-pgo | all | cargo | | cargo-machete (unused deps), cargo-semver-checks (API breaks) | all | cargo | | cargo-llvm-cov + llvm-tools-preview | all | cargo / rustup | -| protobuf-compiler, librdkafka-dev | Linux | distro repo | +| protobuf-compiler | Linux | distro repo | +| librdkafka1, librdkafka-dev (Fedora: librdkafka-devel) | Linux | vendor-repo (Confluent clients repo) | | mold, clang | Linux | distro repo | | sccache | all | github-binary, latest each run (Tier 3) / brew | | cargo-sweep | all | cargo-binstall / cargo | @@ -314,7 +315,7 @@ table used to say the opposite. | opentofu (`tofu`) | all | vendor-repo (packages.opentofu.org), apt AND dnf / brew | | openbao | all | vendor-repo (pkgs.openbao.org) / Fedora dnf / brew | | azure-cli, google-cloud-cli | all | vendor-repo / cask | -| clickhouse-client, rpk, valkey-cli, vector (the `data` group) | Linux; macOS partial | vendor-repo / distro | +| clickhouse-client, confluent-cli, rpk, valkey-cli, vector (the `data` group) | Linux; macOS partial | vendor-repo / distro | | wrangler (the `cloudflare` group) | all | npm-global / brew | | flarectl (the `cloudflare` group) | all | `go install` from source / brew |