From b6fe5dbe3580d0e24c0aad8a78d0f2b2763d701b Mon Sep 17 00:00:00 2001 From: Derek Date: Tue, 6 Oct 2026 12:24:25 +1100 Subject: [PATCH 1/2] fix(infrastructure): rpk, Confluent from repos rpk now comes from Redpanda's package repo as redpanda-rpk, the CLI alone, instead of a GitHub zip in /usr/local/bin that nothing ever updated. The old binary is removed once the package is in. The Confluent CLI moves to its own unversioned repo. The Confluent Platform repo it came from was picked by a lexical sort of version strings, and it carries a confluent-cli numbered by the platform release (8.3.2) that wraps an older CLI (4.72) and outranks the real one on version. The old repo and key are removed, and a confluent-cli the CLI repo does not carry is replaced. librdkafka for the rdkafka crate comes from Confluent's clients repo on Ubuntu and Fedora, matching what scalo-rs images ship, instead of the distro builds (2.3.0 on noble). An apt pin keeps Confluent's build ahead of Ubuntu's same-named packages, and on Fedora a dnf swap replaces the differently named distro package in one transaction. Every new repo key is downloaded to an untrusted path, checked to hold exactly one primary key with the pinned fingerprint, and only then trusted, with failures recorded as deploy warnings. The ClickHouse repo on Fedora now checks its signed metadata; its rpms are unsigned, so package gpgcheck stays off. --- .../roles/developer-rust/defaults/main.yml | 17 + ansible/roles/developer-rust/tasks/rust.yml | 170 ++++++- .../roles/infrastructure/defaults/main.yml | 12 + .../roles/infrastructure/tasks/data_tools.yml | 426 +++++++++++++----- 4 files changed, 509 insertions(+), 116 deletions(-) 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..43c3b43 100644 --- a/ansible/roles/developer-rust/tasks/rust.yml +++ b/ansible/roles/developer-rust/tasks/rust.yml @@ -385,24 +385,164 @@ # ============================================================ # 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 }} + 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 + + - 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 + + # 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 + 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' + + - 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 + 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' + + # 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' + when: ansible_facts['distribution'] == 'Fedora' + + - 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' + + # 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. + - name: Swap Fedora's librdkafka for Confluent's (Fedora) + ansible.builtin.command: + argv: [dnf, -y, swap, --allowerasing, 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' + + rescue: + - 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..e6d727d 100644 --- a/ansible/roles/infrastructure/tasks/data_tools.yml +++ b/ansible/roles/infrastructure/tasks/data_tools.yml @@ -1,5 +1,5 @@ --- -# 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 @@ -10,7 +10,6 @@ # run via container. # - 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 # ============================================================================ @@ -100,37 +106,84 @@ # 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 }}" 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 + + - 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 + + # 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 + 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' + + # 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 + 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' - 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,14 +192,24 @@ components: main signed_by: /etc/apt/keyrings/clickhouse.asc state: present + 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' - when: ansible_facts['distribution'] == 'Ubuntu' + rescue: + - 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. @@ -154,80 +217,167 @@ # 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 }} block: - - name: Detect latest Confluent version - ansible.builtin.uri: - url: https://packages.confluent.io/rpm/ - return_content: true - register: confluent_page - - - 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: Import Confluent GPG key - ansible.builtin.rpm_key: - key: "https://packages.confluent.io/rpm/{{ confluent_version }}/archive.key" - state: present + # 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: Add Confluent repository + - name: Remove the Confluent Platform repository (Fedora) ansible.builtin.yum_repository: name: Confluent - description: Confluent repository - baseurl: "https://packages.confluent.io/rpm/{{ confluent_version }}" - enabled: true - gpgcheck: true - gpgkey: "https://packages.confluent.io/rpm/{{ confluent_version }}/archive.key" - - - name: Install Confluent CLI - ansible.builtin.dnf: - name: confluent-cli - state: present + state: absent + when: ansible_facts['distribution'] == 'Fedora' - 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 -- 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 + - 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 - - name: Extract Confluent version - ansible.builtin.set_fact: - confluent_version_ubuntu: "{{ confluent_page_ubuntu.content | regex_findall('[0-9]+\\.[0-9]+') | sort | last | default('8.1') }}" - - - 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 + # 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 + 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' - - name: Remove the legacy Confluent .list - ansible.builtin.file: - path: /etc/apt/sources.list.d/confluent.list - state: absent - - - name: Add Confluent APT repository + - name: Add the Confluent CLI APT repository (Ubuntu) ansible.builtin.deb822_repository: - name: confluent + name: confluent-cli types: deb - uris: "https://packages.confluent.io/deb/{{ confluent_version_ubuntu }}" + uris: https://packages.confluent.io/confluent-cli/deb suites: stable components: main - signed_by: /etc/apt/keyrings/confluent.asc + signed_by: /etc/apt/keyrings/confluent-cli.asc state: present + when: ansible_facts['distribution'] == 'Ubuntu' - - name: Install Confluent CLI + # 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-cli + description: Confluent CLI + baseurl: https://packages.confluent.io/confluent-cli/rpm + enabled: true + gpgcheck: true + repo_gpgcheck: true + gpgkey: file:///etc/pki/rpm-gpg/RPM-GPG-KEY-confluent-cli + when: ansible_facts['distribution'] == 'Fedora' + + # 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' + + - 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: Read the confluent-cli versions the CLI repo offers (Fedora) + ansible.builtin.command: + argv: [dnf, -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 + + # A Platform-repo build outranks every CLI-repo version, so the package + # manager never replaces it; removing it lets the CLI repo's build in. + - name: Remove a confluent-cli the CLI repo does not carry + ansible.builtin.package: + name: confluent-cli + state: absent + when: + - infrastructure_confluent_cli_installed.rc == 0 + - infrastructure_confluent_cli_installed.stdout not in infrastructure_confluent_cli_offered + vars: + 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 }} + + - name: Install Confluent CLI + ansible.builtin.package: name: confluent-cli state: present - update_cache: true - when: ansible_facts['distribution'] == 'Ubuntu' + rescue: + - 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,41 +398,115 @@ # ============================================================================ # 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 }} block: - - name: Ensure unzip is present for rpk - ansible.builtin.package: - name: unzip - 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 Redpanda signing key + ansible.builtin.get_url: + url: "{{ infrastructure_redpanda_key.url }}" + dest: "{{ infrastructure_redpanda_key.dest }}.unverified" + mode: '0644' + force: true - - 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 + - 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 - - 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 + # 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 + 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 - include: rpk - mode: '0755' - when: not ansible_check_mode + owner: root + group: root + mode: '0644' + + - 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 + when: ansible_facts['distribution'] == 'Ubuntu' + + # 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 + when: ansible_facts['distribution'] == 'Fedora' + + - name: Install rpk (Ubuntu) + ansible.builtin.apt: + name: redpanda-rpk + state: present + update_cache: true + when: ansible_facts['distribution'] == 'Ubuntu' + + - name: Install rpk (Fedora) + ansible.builtin.dnf: + name: redpanda-rpk + state: present + when: ansible_facts['distribution'] == 'Fedora' + + # 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: - 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'] From afa21210a9a72e48de56eda746a95130301c01ca Mon Sep 17 00:00:00 2001 From: Derek Date: Tue, 6 Oct 2026 13:12:48 +1100 Subject: [PATCH 2/2] fix(infrastructure): safer vendor repo migrations The Fedora librdkafka swap no longer passes --allowerasing, which silently removed python3-confluent-kafka (it requires librdkafka by name, which Confluent's librdkafka1 does not provide) along with its python dependants. A host with such a package now keeps Fedora's librdkafka, the Confluent clients repo stays off it so dnf upgrade does not hit the file conflict, and the run records a warning naming the package. Later converges repeat the same check without changing anything. A confluent-cli from the Confluent Platform repo is now replaced in one transaction, a downgrade to the CLI repo's newest build, instead of a remove then install that left no CLI when the download failed. On Fedora the repo query passes -y so the new repo's key is imported on first use. The Platform repo definition is removed, and on Ubuntu its key; on Fedora its key stays in the rpm database, because it is the same key that signs the Confluent clients repo librdkafka now comes from. When a pinned fingerprint check fails, that vendor repo is removed so the apt module's cache refresh does not fail for the rest of the run, and the previously trusted key is left in place. The Ubuntu repo is removed as a file because deb822_repository state=absent also deletes the keyring of the same name. In check mode on a fresh host the key read, verify and install steps and the package installs are skipped instead of failing into a deploy warning. The install docs list the Confluent CLI in the data group and librdkafka under the Confluent clients repo. The data_tools.yml header is ASCII and the stale macOS rpk fallback note is gone. --- README.md | 4 +- ansible/roles/developer-rust/tasks/rust.yml | 100 +++++++++-- .../roles/infrastructure/tasks/data_tools.yml | 169 +++++++++++++++--- docs/install-matrix.md | 7 +- 4 files changed, 240 insertions(+), 40 deletions(-) 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/tasks/rust.yml b/ansible/roles/developer-rust/tasks/rust.yml index 43c3b43..d08d10b 100644 --- a/ansible/roles/developer-rust/tasks/rust.yml +++ b/ansible/roles/developer-rust/tasks/rust.yml @@ -406,6 +406,10 @@ {{ 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 @@ -422,13 +426,16 @@ 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 @@ -441,6 +448,7 @@ {{ 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 }} @@ -456,6 +464,7 @@ 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: @@ -466,6 +475,7 @@ 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 @@ -489,7 +499,43 @@ - librdkafka-dev state: latest update_cache: true - when: ansible_facts['distribution'] == 'Ubuntu' + 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 @@ -504,23 +550,17 @@ repo_gpgcheck: true gpgkey: "file://{{ developer_rust_confluent_clients_key }}" priority: '10' - when: ansible_facts['distribution'] == 'Fedora' - - - 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 + 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, --allowerasing, librdkafka, librdkafka1] + argv: [dnf, -y, swap, librdkafka, librdkafka1] changed_when: true when: - ansible_facts['distribution'] == 'Fedora' @@ -532,9 +572,45 @@ - librdkafka1 - librdkafka-devel state: latest - when: ansible_facts['distribution'] == 'Fedora' + 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: diff --git a/ansible/roles/infrastructure/tasks/data_tools.yml b/ansible/roles/infrastructure/tasks/data_tools.yml index e6d727d..065e37f 100644 --- a/ansible/roles/infrastructure/tasks/data_tools.yml +++ b/ansible/roles/infrastructure/tasks/data_tools.yml @@ -2,13 +2,13 @@ # 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). - name: Skipped data platform tools notice (macOS) @@ -100,7 +100,7 @@ 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 @@ -114,6 +114,10 @@ {{ '/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: # Runs before any apt call: apt reads a leftover .list beside the .sources # written below as a second copy of the repo. @@ -131,13 +135,16 @@ 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 @@ -149,6 +156,7 @@ {{ 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: >- @@ -162,6 +170,7 @@ 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 @@ -175,13 +184,16 @@ 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 (Fedora) ansible.builtin.dnf: name: clickhouse-client state: present - when: ansible_facts['distribution'] == 'Fedora' + when: + - ansible_facts['distribution'] == 'Fedora' + - not infrastructure_clickhouse_repo_pending - name: Add ClickHouse APT repository (Ubuntu) ansible.builtin.deb822_repository: @@ -192,6 +204,7 @@ 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 (Ubuntu) @@ -199,9 +212,32 @@ name: clickhouse-client state: present update_cache: true - when: ansible_facts['distribution'] == 'Ubuntu' + when: + - ansible_facts['distribution'] == 'Ubuntu' + - not infrastructure_clickhouse_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 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' + + - 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: @@ -211,7 +247,7 @@ when: ansible_facts['distribution'] in ['Fedora', 'Ubuntu'] -# ClickHouse intentionally not installed on macOS — see header comment. +# ClickHouse intentionally not installed on macOS -- see header comment. # ============================================================================ # CONFLUENT CLI - Kafka client tools @@ -231,6 +267,10 @@ 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: # The Confluent Platform repo an older revision configured. Runs before any # apt or dnf call so neither reads it again. @@ -258,13 +298,16 @@ 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 @@ -276,6 +319,7 @@ {{ 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: >- @@ -289,6 +333,7 @@ owner: root group: root mode: '0644' + when: not (ansible_check_mode and infrastructure_confluent_cli_key_download is changed) - name: Add the Confluent CLI APT repository (Ubuntu) ansible.builtin.deb822_repository: @@ -299,6 +344,7 @@ components: main signed_by: /etc/apt/keyrings/confluent-cli.asc state: present + register: infrastructure_confluent_cli_deb_repo when: ansible_facts['distribution'] == 'Ubuntu' # Both the packages and the repo metadata are signed with the same key. @@ -311,6 +357,7 @@ gpgcheck: true 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' # Also drops the Platform repo's lists, so the version read below sees only @@ -329,9 +376,11 @@ check_mode: false when: ansible_facts['distribution'] == '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, -q, repoquery, --available, --qf, "%{evr}\n", confluent-cli] + argv: [dnf, -y, -q, repoquery, --available, --qf, "%{evr}\n", confluent-cli] register: infrastructure_confluent_cli_repoquery changed_when: false check_mode: false @@ -349,27 +398,67 @@ failed_when: false check_mode: false + - name: Collect the confluent-cli versions the CLI repo offers + ansible.builtin.set_fact: + 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; removing it lets the CLI repo's build in. - - name: Remove a confluent-cli the CLI repo does not carry - ansible.builtin.package: - name: confluent-cli - state: absent + # 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 - vars: - 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 }} - name: Install Confluent CLI ansible.builtin.package: name: confluent-cli state: present + when: not infrastructure_confluent_cli_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 CLI APT repository after a key mismatch (Ubuntu) + ansible.builtin.file: + 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: Remove the Confluent CLI repository after a key mismatch (Fedora) + ansible.builtin.yum_repository: + name: confluent-cli + state: absent + when: + - ansible_failed_task.action in ['assert', 'ansible.builtin.assert'] + - ansible_facts['distribution'] == 'Fedora' + - 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: @@ -414,6 +503,10 @@ 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: # 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. @@ -423,13 +516,16 @@ 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 @@ -441,6 +537,7 @@ {{ 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: >- @@ -454,6 +551,7 @@ 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: @@ -464,6 +562,7 @@ components: main signed_by: /etc/apt/keyrings/redpanda.asc state: present + register: infrastructure_redpanda_deb_repo when: ansible_facts['distribution'] == 'Ubuntu' # dnf rejects the signature Artifact Registry puts on the yum metadata as @@ -477,6 +576,7 @@ 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: Install rpk (Ubuntu) @@ -484,13 +584,17 @@ name: redpanda-rpk state: present update_cache: true - when: ansible_facts['distribution'] == 'Ubuntu' + 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' + 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 @@ -501,6 +605,27 @@ 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: @@ -512,9 +637,7 @@ - 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 |