diff --git a/README.md b/README.md index 34ec972..0878479 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,7 @@ flowchart TD | `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, confluent-cli, rpk, valkey-cli, vector), `cloudflare` (flarectl, wrangler) | +| `packer` | HashiCorp Packer from HashiCorp's repo or tap. Opt-in, in no persona | | `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 +186,8 @@ 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, 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`) +- `infrastructure`: OpenTofu + OpenBao (the OSS forks in place of terraform and vault), 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`) +- `packer` (off by default, and in no persona): HashiCorp Packer, the one HashiCorp tool we install, because it is BUSL with no open-source fork. Ubuntu and Fedora take HashiCorp's repo, with its signing key checked against the fingerprint HashiCorp publishes before it is trusted; macOS takes `hashicorp/tap`. `--tags removals` removes that repo only where Packer is not installed -- see [roles/packer/README.md](ansible/roles/packer/README.md) - `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/molecule/matrix/molecule.yml b/ansible/molecule/matrix/molecule.yml index e406148..aadbc0f 100644 --- a/ansible/molecule/matrix/molecule.yml +++ b/ansible/molecule/matrix/molecule.yml @@ -5,7 +5,8 @@ # installs, which is why Fedora belongs here -- there are no deployed Fedora # clients to have drifted, but a fresh Fedora box must come up correctly. # -# Scoped to the CLI base (`repository`, `utilities`, `git`). Docker, snap and the +# Scoped to the CLI base (`repository`, `utilities`, `git`) plus `packer`, the +# opt-in role with its own vendor repo on every platform. Docker, snap and the # GNOME paths need a daemon or a session a container does not have, so widening # the tag set means solving that first -- privileged containers or systemd # images -- not just adding a tag. @@ -69,7 +70,7 @@ ansible: ansible_playbook: - --diff - --tags - - repository,utilities,git + - repository,utilities,git,packer playbooks: converge: converge.yml verify: verify.yml diff --git a/ansible/molecule/matrix/verify.yml b/ansible/molecule/matrix/verify.yml index 805f6cc..24a792e 100644 --- a/ansible/molecule/matrix/verify.yml +++ b/ansible/molecule/matrix/verify.yml @@ -26,6 +26,7 @@ - {name: git, cmd: git --version} - {name: tmux, cmd: tmux -V} - {name: age, cmd: age --version} + - {name: packer, cmd: packer version} loop_control: label: "{{ item.name }}" register: matrix_tools diff --git a/ansible/molecule/remediation/prepare.yml b/ansible/molecule/remediation/prepare.yml index 9591521..29a7d01 100644 --- a/ansible/molecule/remediation/prepare.yml +++ b/ansible/molecule/remediation/prepare.yml @@ -288,9 +288,8 @@ # The Fedora yq superseded by dnf is NOT planted: its removal sits in the # install path of utilities.yml, which a removals-only run never reaches. # kustomize is planted above, in the superseded-by-a-package section. - # The HashiCorp repo is the half that matters: left behind it keeps serving - # BUSL packages and updates. Removing it is a remediation, so only a - # `--tags removals` run does it -- which is what this scenario converges. + # The HashiCorp repo goes on a removals run unless Packer is installed, which + # the gosrc host fakes below so both branches are exercised. - name: Plant the HashiCorp apt repository (Ubuntu) ansible.builtin.copy: content: | @@ -309,7 +308,7 @@ # provisioned before that migration carries both. # A parseable line, because apt refuses EVERY operation when any source list # is malformed -- including the removal that is supposed to clear it. - # `trusted=yes` keeps the fixture off the network and off the keyring. + # `trusted=yes` keeps the fixture off the keyring. - name: Plant the legacy HashiCorp .list (Ubuntu) ansible.builtin.copy: content: "deb [trusted=yes] https://apt.releases.hashicorp.com noble main\n" @@ -331,6 +330,50 @@ - /usr/share/keyrings/hashicorp-archive-keyring.gpg when: ansible_facts['distribution'] == 'Ubuntu' + # An empty package named packer is all the removals check reads, and it + # keeps the fixture off HashiCorp's BUSL binary. + - name: Create the stand-in packer package tree (gosrc host) + ansible.builtin.file: + path: /root/packer-fixture/DEBIAN + state: directory + owner: root + group: root + mode: '0755' + when: + - ansible_facts['distribution'] == 'Ubuntu' + - "'gosrc' in inventory_hostname" + + - name: Write the control file for a stand-in packer package (gosrc host) + ansible.builtin.copy: + content: | + Package: packer + Version: 0.0.0-fixture + Architecture: all + Maintainer: fixture + Description: Stand-in for HashiCorp Packer in the remediation fixture + dest: /root/packer-fixture/DEBIAN/control + owner: root + group: root + mode: '0644' + when: + - ansible_facts['distribution'] == 'Ubuntu' + - "'gosrc' in inventory_hostname" + + - name: Build the stand-in packer package (gosrc host) + ansible.builtin.command: + argv: [dpkg-deb, --build, /root/packer-fixture, /root/packer-fixture.deb] + creates: /root/packer-fixture.deb + when: + - ansible_facts['distribution'] == 'Ubuntu' + - "'gosrc' in inventory_hostname" + + - name: Install the stand-in packer package (gosrc host) + ansible.builtin.apt: + deb: /root/packer-fixture.deb + when: + - ansible_facts['distribution'] == 'Ubuntu' + - "'gosrc' in inventory_hostname" + - name: Install the DBeaver flatpak superseded by the vendor apt repo (Ubuntu) community.general.flatpak: name: io.dbeaver.DBeaverCommunity diff --git a/ansible/molecule/remediation/verify.yml b/ansible/molecule/remediation/verify.yml index cac2283..564c7f9 100644 --- a/ansible/molecule/remediation/verify.yml +++ b/ansible/molecule/remediation/verify.yml @@ -198,7 +198,8 @@ # The Fedora yq migration is not asserted here: its removal sits in the # install path of utilities.yml, which a removals-only run never reaches. # kustomize is covered in the superseded-by-a-package section above. - # The repo is the half that keeps serving BUSL packages if it survives. + # Without Packer the repo is unhooked, because one left trusting a superseded + # key fails every apt refresh. With Packer (the gosrc host) it stays. - name: Stat the HashiCorp repository artefacts (Ubuntu) ansible.builtin.stat: path: "{{ item }}" @@ -210,19 +211,36 @@ register: verify_hashicorp when: ansible_facts['distribution'] == 'Ubuntu' - - name: Assert the HashiCorp repository was unhooked + - name: Assert the HashiCorp repository was unhooked where Packer is absent ansible.builtin.assert: that: - not item.stat.exists fail_msg: >- - {{ item.item }} survived remediation. The HashiCorp repo goes on - serving BUSL packages and updates for as long as it is configured, - so leaving it is worse than leaving the binaries. + {{ item.item }} survived remediation on a host without Packer. A + HashiCorp repo whose key the host no longer trusts fails every apt + refresh, and nothing else here removes it. success_msg: "{{ item.item }} removed" loop: "{{ verify_hashicorp.results | default([]) }}" loop_control: label: "{{ item.item | default('skipped') }}" - when: ansible_facts['distribution'] == 'Ubuntu' + when: + - ansible_facts['distribution'] == 'Ubuntu' + - "'gosrc' not in inventory_hostname" + + - name: Assert the HashiCorp repository was kept where Packer is installed + ansible.builtin.assert: + that: + - item.stat.exists + fail_msg: >- + {{ item.item }} was removed from a host with Packer installed, which + leaves Packer with no update source. + success_msg: "{{ item.item }} kept" + loop: "{{ verify_hashicorp.results | default([]) }}" + loop_control: + label: "{{ item.item | default('skipped') }}" + when: + - ansible_facts['distribution'] == 'Ubuntu' + - "'gosrc' in inventory_hostname" - name: Stat the retired vendor repository artefacts (Ubuntu) ansible.builtin.stat: diff --git a/ansible/playbooks/main.yml b/ansible/playbooks/main.yml index b97e62a..4e4e36b 100644 --- a/ansible/playbooks/main.yml +++ b/ansible/playbooks/main.yml @@ -252,6 +252,11 @@ become: "{{ ansible_facts['distribution'] != 'MacOSX' }}" tags: ['infrastructure', 'never'] + # Packer from HashiCorp's repo (opt-in, in no persona): BUSL, no fork. + - role: packer + become: "{{ ansible_facts['distribution'] != 'MacOSX' }}" + tags: ['packer', 'never'] + # ======================================================================== # PERSONAS (meta-roles) — opinionated bundles that resolve to the roles # above via meta/dependencies. They pull the CLEAN developer base: its diff --git a/ansible/roles/infrastructure/tasks/cloud.yml b/ansible/roles/infrastructure/tasks/cloud.yml index 93cb6b4..ecf3f49 100644 --- a/ansible/roles/infrastructure/tasks/cloud.yml +++ b/ansible/roles/infrastructure/tasks/cloud.yml @@ -13,10 +13,9 @@ # `aws-vault` below is NOT HashiCorp Vault -- it is aws-vault (the ByteNess fork # of 99designs/aws-vault), an unrelated tool for keeping AWS credentials in the OS keychain. It stays. # -# Packer has no viable fork (the community never produced one) and is -# deliberately NOT installed here. hyperi-infra needs it for image builds; its -# developers install it themselves rather than have us ship a BUSL binary to -# every workstation. +# Packer has no viable fork (the community never produced one), so it comes +# from HashiCorp's repo in its own opt-in role, `packer`, rather than shipping a +# BUSL binary to every box that asks for the IaC tools. # ============================================================================ # OPENTOFU REPOSITORY (Ubuntu and Fedora) @@ -27,8 +26,8 @@ # upstream 1.12.6), which is the wrong side of "almost current" for the tool # that plans and applies infrastructure. # -# The apt suite is literally "any" and the packages are arch-generic, so unlike -# the HashiCorp repo this needs no LTS-codename mapping and no arch handling. +# The apt suite is literally "any" and the packages are arch-generic, so this +# needs no codename and no arch handling. # # The package is `tofu` on both. Fedora's own package is `opentofu`, and the two # conflict over /usr/bin/tofu, so the distro one is removed below. diff --git a/ansible/roles/infrastructure/tasks/main.yml b/ansible/roles/infrastructure/tasks/main.yml index a1b285c..bfc54b8 100644 --- a/ansible/roles/infrastructure/tasks/main.yml +++ b/ansible/roles/infrastructure/tasks/main.yml @@ -3,13 +3,11 @@ # Not HyperI-specific. Org tooling lives in `soe`. # Before the installs: removing terraform/vault must not race the tofu/bao -# install, and the HashiCorp repo has to go before an apt update reads it again. +# install. # # `removals` ONLY, and never on a propagated tag. Installing the IaC tools is -# not a request to delete someone's Terraform or unhook their HashiCorp repo -- -# that is a remediation, asked for explicitly. `--tags infrastructure` and -# `--tags cloud` used to fire it too, which meant a routine IaC install silently -# took both away. +# not a request to delete someone's Terraform -- that is a remediation, asked +# for explicitly. - name: Remove the retired HashiCorp tools (opt-in) ansible.builtin.include_tasks: file: removals.yml diff --git a/ansible/roles/infrastructure/tasks/removals.yml b/ansible/roles/infrastructure/tasks/removals.yml index 6a4a6b5..7451e21 100644 --- a/ansible/roles/infrastructure/tasks/removals.yml +++ b/ansible/roles/infrastructure/tasks/removals.yml @@ -5,39 +5,30 @@ # vault -> bao (OpenBao, MPL-2.0) # # Ansible cannot remove what we simply stop declaring, so every host we have -# ever provisioned keeps terraform, vault AND the HashiCorp repo until we say -# otherwise. Leaving the repo behind is the worse half: it would go on serving -# BUSL packages and updates forever. +# ever provisioned keeps terraform and vault until we say otherwise. +# +# The HashiCorp repo, its key and the Homebrew tap go only where Packer is not +# installed, since the opt-in `packer` role installs from them. A repo left +# behind with a key the host no longer trusts fails every apt refresh. # # Removing terraform WILL break tooling that still shells out to it -- notably # hyperi-infra, which invokes `terraform` directly. That is why this runs on # `--tags removals` alone: installing the IaC tools is not a request to delete # somebody's Terraform. -# -# apt refuses every operation while any source list is malformed, so a host with -# a broken hashicorp.list cannot run the removal that would clear it. -- name: Remove Terraform and Vault (Ubuntu) - ansible.builtin.apt: - name: - - terraform - - vault - state: absent - purge: false +# Before the package removals, which refresh the apt cache and fail on a repo +# signed by a key the host no longer trusts. +- name: Check whether Packer is installed (Ubuntu) + ansible.builtin.command: + argv: [dpkg-query, --show, '--showformat=${Status}', packer] + register: infrastructure_packer_deb + changed_when: false + failed_when: false + check_mode: false when: ansible_facts['distribution'] == 'Ubuntu' -- name: Remove Terraform and Vault (Fedora) - ansible.builtin.dnf: - name: - - terraform - - vault - state: absent - when: ansible_facts['distribution'] == 'Fedora' - -# The repo itself. Both filenames are removed: deb822_repository writes -# .sources, and older revisions of this role used apt_repository, which wrote -# .list. A host provisioned before that migration carries the .list. -- name: Remove the HashiCorp APT repository (Ubuntu) +# Both filenames: deb822_repository writes .sources, and older setups wrote .list. +- name: Remove the HashiCorp APT repository where Packer is not installed (Ubuntu) ansible.builtin.file: path: "{{ item }}" state: absent @@ -45,9 +36,12 @@ - /etc/apt/sources.list.d/hashicorp.sources - /etc/apt/sources.list.d/hashicorp.list - /usr/share/keyrings/hashicorp-archive-keyring.asc + - /usr/share/keyrings/hashicorp-archive-keyring.asc.unverified - /usr/share/keyrings/hashicorp-archive-keyring.gpg register: infrastructure_hashicorp_repo_removed - when: ansible_facts['distribution'] == 'Ubuntu' + when: + - ansible_facts['distribution'] == 'Ubuntu' + - "'install ok installed' not in infrastructure_packer_deb.stdout | default('')" - name: Refresh the APT cache after removing the repository ansible.builtin.apt: @@ -56,14 +50,44 @@ - ansible_facts['distribution'] == 'Ubuntu' - infrastructure_hashicorp_repo_removed is changed -- name: Remove the HashiCorp YUM repository (Fedora) +- name: Check whether Packer is installed (Fedora) + ansible.builtin.command: + argv: [rpm, -q, packer] + register: infrastructure_packer_rpm + changed_when: false + failed_when: false + check_mode: false + when: ansible_facts['distribution'] == 'Fedora' + +- name: Remove the HashiCorp YUM repository where Packer is not installed (Fedora) ansible.builtin.file: - path: /etc/yum.repos.d/hashicorp.repo + path: "{{ item }}" + state: absent + loop: + - /etc/yum.repos.d/hashicorp.repo + - /etc/pki/rpm-gpg/RPM-GPG-KEY-hashicorp + - /etc/pki/rpm-gpg/RPM-GPG-KEY-hashicorp.unverified + when: + - ansible_facts['distribution'] == 'Fedora' + - infrastructure_packer_rpm.rc | default(0) != 0 + +- name: Remove Terraform and Vault (Ubuntu) + ansible.builtin.apt: + name: + - terraform + - vault + state: absent + purge: false + when: ansible_facts['distribution'] == 'Ubuntu' + +- name: Remove Terraform and Vault (Fedora) + ansible.builtin.dnf: + name: + - terraform + - vault state: absent when: ansible_facts['distribution'] == 'Fedora' -# macOS. Untap only after the formulae are gone, or brew refuses. -# # NOT touched: `aws-vault`. It is the ByteNess aws-vault, nothing to do with # HashiCorp Vault despite the name, and it is still installed on purpose. - name: Remove Terraform and Vault (macOS) @@ -77,11 +101,25 @@ failed_when: false when: ansible_facts['distribution'] == 'MacOSX' -- name: Remove the HashiCorp Homebrew tap (macOS) +- name: Check whether Packer is installed (macOS) + ansible.builtin.command: + argv: [brew, list, hashicorp/tap/packer] + register: infrastructure_packer_brew + become: false + environment: "{{ homebrew_env }}" + changed_when: false + failed_when: false + check_mode: false + when: ansible_facts['distribution'] == 'MacOSX' + +# Untap only after the formulae are gone, or brew refuses. +- name: Remove the HashiCorp Homebrew tap where Packer is not installed (macOS) community.general.homebrew_tap: name: hashicorp/tap state: absent become: false environment: "{{ homebrew_env }}" failed_when: false - when: ansible_facts['distribution'] == 'MacOSX' + when: + - ansible_facts['distribution'] == 'MacOSX' + - infrastructure_packer_brew.rc | default(0) != 0 diff --git a/ansible/roles/packer/README.md b/ansible/roles/packer/README.md new file mode 100644 index 0000000..67be9f4 --- /dev/null +++ b/ansible/roles/packer/README.md @@ -0,0 +1,39 @@ +# packer + +HashiCorp Packer, from HashiCorp's own package repositories. **Opt-in** -- not in the default install, not in any persona, not in `contributor` or `soe`. + + ./install.sh --tags packer + +## Why its own role + +Packer is the one HashiCorp tool we install. HashiCorp moved its tools to BUSL in 2023; terraform and vault have open-source forks (OpenTofu, OpenBao) and the `infrastructure` role installs those instead. Packer has no fork, so it ships only to boxes that ask for it -- machine image builds are the usual reason. + +## What it touches + +| Platform | Repository | Key | Package | +|---|---|---|---| +| Ubuntu | `/etc/apt/sources.list.d/hashicorp.sources`, suite = the host's codename | `/usr/share/keyrings/hashicorp-archive-keyring.asc` | `packer` | +| Fedora | `/etc/yum.repos.d/hashicorp.repo` | `/etc/pki/rpm-gpg/RPM-GPG-KEY-hashicorp` | `packer` | +| macOS | `hashicorp/tap` | - | `hashicorp/tap/packer` | + +On Ubuntu it also removes `/etc/apt/sources.list.d/hashicorp.list`, the older format of the same repo, which apt would otherwise read as a duplicate source. + +Other tooling also writes the Ubuntu key path, so a host it set up converges onto one trusted file. A tool that writes an unverified key there replaces the pinned one until this role runs again. + +## Key pinning + +The signing key is downloaded to `.unverified` and only copied to the trusted path when it holds exactly one primary key and that key's fingerprint is `packer_hashicorp_key_fingerprint`. The default is the Linux repository key HashiCorp publishes at https://www.hashicorp.com/en/trust/security: + + D55C 0D1A C78A 8D81 26CB 631C FC9C A96A CA02 6560 + +It replaced `798A EC65 4E5C 1542 8C8E 42EE AA16 FCBC A621 E701`, which HashiCorp lists as superseded and which signed the repos until 2026-09-10. A host still trusting the old key gets the new one on the next run, even when its apt cache can no longer refresh against the old key. + +A mismatch fails closed: the run removes the HashiCorp repo it would have used, leaves the previously trusted key alone, records a warning in the end-of-run report and carries on. Update the pin after checking the new fingerprint against HashiCorp's page. + +## Removals + +`--tags removals` takes terraform and vault away. It removes the HashiCorp repo, its key and the tap only where Packer is not installed, since this role installs from them. + +## Verifying + + packer version diff --git a/ansible/roles/packer/defaults/main.yml b/ansible/roles/packer/defaults/main.yml new file mode 100644 index 0000000..a4cc039 --- /dev/null +++ b/ansible/roles/packer/defaults/main.yml @@ -0,0 +1,9 @@ +--- +# Packer role defaults + +# Primary key fingerprint HashiCorp publishes for its Linux package repos at +# https://www.hashicorp.com/en/trust/security, checked before the key is trusted. +# The same key signs apt.releases.hashicorp.com and rpm.releases.hashicorp.com. +packer_hashicorp_key_fingerprint: D55C0D1AC78A8D8126CB631CFC9CA96ACA026560 # gitleaks:allow -- public key fingerprint +packer_hashicorp_apt_key_url: https://apt.releases.hashicorp.com/gpg +packer_hashicorp_rpm_key_url: https://rpm.releases.hashicorp.com/gpg diff --git a/ansible/roles/packer/tasks/main.yml b/ansible/roles/packer/tasks/main.yml new file mode 100644 index 0000000..d3f9b1e --- /dev/null +++ b/ansible/roles/packer/tasks/main.yml @@ -0,0 +1,200 @@ +--- +# Packer is BUSL with no open-source fork, so it installs only on `--tags packer`, and a failure lands in deploy_warnings rather than ending the run. + +# ============================================================================ +# LINUX - HashiCorp's apt repo on Ubuntu, its dnf repo on Fedora +# ============================================================================ +- name: Install Packer from the HashiCorp repository (Linux) + vars: + # Check mode adds no repo, so there is nothing to install from yet. + packer_repo_pending: >- + {{ ansible_check_mode and ((packer_deb_repo | default({})) is changed + or (packer_rpm_repo | default({})) is changed) }} + when: ansible_facts['distribution'] in ['Fedora', 'Ubuntu'] + block: + # Runs before any apt call, so apt does not read this repo twice as a duplicate source. + - name: Remove the legacy HashiCorp .list (Ubuntu) + ansible.builtin.file: + path: /etc/apt/sources.list.d/hashicorp.list + state: absent + when: ansible_facts['distribution'] == 'Ubuntu' + + # A minimal Ubuntu lacks gpg and python3-debian, which the fingerprint read and deb822_repository need. + - name: Install the key-check prerequisites (Ubuntu) + when: ansible_facts['distribution'] == 'Ubuntu' + block: + - name: Ensure gpg and python3-debian are present (Ubuntu) + ansible.builtin.apt: + name: [gpg, python3-debian] + state: present + update_cache: true + cache_valid_time: 3600 + + rescue: + # A HashiCorp source signed by a key the host no longer trusts fails every cache refresh, and the verified repo is written again below. + - name: Remove the HashiCorp sources apt cannot verify (Ubuntu) + ansible.builtin.file: + path: "{{ item }}" + state: absent + loop: + - /etc/apt/sources.list.d/hashicorp.sources + - /etc/apt/sources.list.d/hashicorp.list + + - name: Retry gpg and python3-debian without the HashiCorp sources (Ubuntu) + ansible.builtin.apt: + name: [gpg, python3-debian] + state: present + update_cache: true + + - name: Ensure gpg is present for the fingerprint check (Fedora) + ansible.builtin.dnf: + name: gnupg2 + state: present + 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 HashiCorp signing key + ansible.builtin.get_url: + url: >- + {{ packer_hashicorp_apt_key_url if ansible_facts['distribution'] == 'Ubuntu' + else packer_hashicorp_rpm_key_url }} + dest: "{{ packer_hashicorp_key_dest }}.unverified" + mode: '0644' + force: true + register: packer_key_download + + # Check mode downloads nothing, so there is no new key to read. + - name: Read the HashiCorp signing key fingerprint + ansible.builtin.command: + argv: [gpg, --show-keys, --with-colons, "{{ packer_hashicorp_key_dest }}.unverified"] + register: packer_key_read + changed_when: false + check_mode: false + when: not (ansible_check_mode and packer_key_download is changed) + + # Fails closed: apt and rpm trust every key in the file, so a second key + # riding along with the pinned one would be trusted too. + - name: Verify the HashiCorp signing key fingerprint + ansible.builtin.assert: + that: + - packer_key_primaries | int == 1 + - packer_key_fpr == packer_hashicorp_key_fingerprint | upper | replace(' ', '') + fail_msg: >- + HashiCorp signing key holds {{ packer_key_primaries }} key(s), first + {{ packer_key_fpr or 'missing' }}; expected only {{ packer_hashicorp_key_fingerprint }} + quiet: true + when: not (ansible_check_mode and packer_key_download is changed) + vars: + packer_key_primaries: "{{ packer_key_read.stdout_lines | select('match', '^pub:') | list | length }}" + packer_key_fpr: >- + {{ (packer_key_read.stdout_lines | select('match', '^fpr:') | first | default('')).split(':')[9] | default('') }} + + - name: Install the verified HashiCorp signing key + ansible.builtin.copy: + src: "{{ packer_hashicorp_key_dest }}.unverified" + dest: "{{ packer_hashicorp_key_dest }}" + remote_src: true + owner: root + group: root + mode: '0644' + when: not (ansible_check_mode and packer_key_download is changed) + + # HashiCorp publishes a suite per Ubuntu codename, 24.04 and 26.04 included. + - name: Add the HashiCorp APT repository (Ubuntu) + ansible.builtin.deb822_repository: + name: hashicorp + types: deb + uris: https://apt.releases.hashicorp.com + suites: "{{ ansible_facts['distribution_release'] }}" + components: main + signed_by: "{{ packer_hashicorp_key_dest }}" + state: present + register: packer_deb_repo + when: ansible_facts['distribution'] == 'Ubuntu' + + - name: Install Packer (Ubuntu) + ansible.builtin.apt: + name: packer + state: present + update_cache: true + when: + - ansible_facts['distribution'] == 'Ubuntu' + - not packer_repo_pending + + - name: Add the HashiCorp repository (Fedora) + ansible.builtin.yum_repository: + name: hashicorp + description: HashiCorp Stable - $basearch + baseurl: https://rpm.releases.hashicorp.com/fedora/$releasever/$basearch/stable + enabled: true + gpgcheck: true + repo_gpgcheck: true + gpgkey: "file://{{ packer_hashicorp_key_dest }}" + register: packer_rpm_repo + when: ansible_facts['distribution'] == 'Fedora' + + - name: Install Packer (Fedora) + ansible.builtin.dnf: + name: packer + state: present + when: + - ansible_facts['distribution'] == 'Fedora' + - not packer_repo_pending + + - name: Check that Packer runs (Linux) + ansible.builtin.command: + argv: [packer, version] + changed_when: false + when: not packer_repo_pending + + rescue: + # A repo the trusted key cannot verify fails every later cache refresh in the run, so it goes and the trusted key stays. + - name: Remove the HashiCorp APT repository after a key mismatch (Ubuntu) + ansible.builtin.file: + path: /etc/apt/sources.list.d/hashicorp.sources + state: absent + when: + - ansible_failed_task.action in ['assert', 'ansible.builtin.assert'] + - ansible_facts['distribution'] == 'Ubuntu' + + - name: Remove the HashiCorp repository after a key mismatch (Fedora) + ansible.builtin.yum_repository: + name: hashicorp + state: absent + when: + - ansible_failed_task.action in ['assert', 'ansible.builtin.assert'] + - ansible_facts['distribution'] == 'Fedora' + + - name: Record that Packer did not install + # noqa: var-naming[no-role-prefix] -- deploy_warnings is the shared accumulator + ansible.builtin.set_fact: + deploy_warnings: >- + {{ deploy_warnings | default([]) + + ['Packer: ' ~ (ansible_failed_result.msg | default('repo/install failed'))] }} + +# ============================================================================ +# MACOS - HashiCorp's tap; homebrew-core does not ship BUSL tools +# ============================================================================ +- name: Install Packer from the HashiCorp tap (macOS) + become: false + environment: "{{ packer_homebrew_env }}" + when: ansible_facts['distribution'] == 'MacOSX' + block: + - name: Tap hashicorp/tap (macOS) + community.general.homebrew_tap: + name: hashicorp/tap + state: present + + - name: Install Packer (macOS) + community.general.homebrew: + name: hashicorp/tap/packer + state: present + + rescue: + - name: Record that Packer did not install (macOS) + # noqa: var-naming[no-role-prefix] -- deploy_warnings is the shared accumulator + ansible.builtin.set_fact: + deploy_warnings: >- + {{ deploy_warnings | default([]) + + ['Packer: ' ~ (ansible_failed_result.msg | default('brew install failed'))] }} diff --git a/ansible/roles/packer/vars/main.yml b/ansible/roles/packer/vars/main.yml new file mode 100644 index 0000000..a1f7f02 --- /dev/null +++ b/ansible/roles/packer/vars/main.yml @@ -0,0 +1,9 @@ +--- +# Homebrew lives outside the default PATH of a non-login Ansible shell. +packer_homebrew_env: + PATH: "/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:{{ ansible_facts['env'].PATH }}" + +# Ubuntu shares this key path with other tooling, so a host it configured converges onto one trusted file. +packer_hashicorp_key_dest: >- + {{ '/usr/share/keyrings/hashicorp-archive-keyring.asc' if ansible_facts['distribution'] == 'Ubuntu' + else '/etc/pki/rpm-gpg/RPM-GPG-KEY-hashicorp' }} diff --git a/docs/install-matrix.md b/docs/install-matrix.md index 519fcad..fda0f33 100644 --- a/docs/install-matrix.md +++ b/docs/install-matrix.md @@ -97,6 +97,7 @@ flowchart TD | developer-typescript | `developer-typescript` | developer-node | opt-in | | developer-languages | `developer-languages` | all `developer-` | opt-in (meta) | | infrastructure | `infrastructure` | - | opt-in | +| packer | `packer` | - | opt-in, in no persona | | contributor | `contributor` | developer | opt-in | | soe | `soe` | contributor | opt-in | | soe-gui | `soe-gui` | astral, rdp-client | opt-in | @@ -333,6 +334,14 @@ from v4 that SDK is generated and carries no `cmd/` directory. The Linux build needs a Go toolchain (`--tags developer-go`); without one the run records a warning and continues. +### packer + +| Tool(s) | Platforms | Method | +|---|---|---| +| packer | all | vendor-repo (apt.releases.hashicorp.com, rpm.releases.hashicorp.com), signing key fingerprint-pinned / brew (`hashicorp/tap`) | + +The one HashiCorp tool installed: BUSL, with no open-source fork, so it is its own opt-in role rather than part of `infrastructure`. `--tags removals` takes terraform and vault, and removes this repo only where Packer is not installed. + ### contributor (CI toolchain - what `hyperi-ci check` drives) `*` = blocking CI gate. diff --git a/install.sh b/install.sh index 095aebb..f045e11 100755 --- a/install.sh +++ b/install.sh @@ -182,6 +182,10 @@ Infrastructure (infrastructure): cloudflare cloudflare group: flarectl, wrangler (flarectl builds from source; Linux needs developer-go) +Packer (packer) - opt-in, in no persona: + packer HashiCorp Packer from HashiCorp's repo / tap (BUSL, + no open-source fork) + Contributor (contributor) - to work ON a HyperI product, no org policy: hyperi-ci hyperi-ci + semgrep, alint gitleaks Secret scanner