Repository navigation
Expand file tree
/
Copy pathsetup-pi-github-app.sh
More file actions
executable file
·222 lines (201 loc) · 9.43 KB
/
Copy pathsetup-pi-github-app.sh
File metadata and controls
executable file
·222 lines (201 loc) · 9.43 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
#!/usr/bin/env bash
# setup-pi-github-app.sh — install the Michelli GitHub App credential helper
# on a Raspberry Pi, or on a Debian/Ubuntu cloud VM (Vultr etc.) — nothing in
# it is Pi-specific. After this runs once, plain `git clone https://github.com/
# GTMichelli-Dev/...` and `git pull` work silently against any private repo
# the App is installed on (foundation, pi-network-setup,
# camera-capture-service, qb-sync-service, ...).
#
# Full walkthrough, including creating the GitHub App the first time:
# README.md in the pi-git-auth repo.
#
# Bootstrap order on a fresh Pi:
# 1. scp the App's PEM private key onto the Pi (e.g. to /tmp/michelli-app.pem)
# 2. Run this script as root:
# sudo bash setup-pi-github-app.sh \
# --install-id <INSTALL_ID> \
# --pem /tmp/michelli-app.pem
# 3. Verify: `git ls-remote https://github.com/GTMichelli-Dev/pi-network-setup.git`
# succeeds with no prompt. Test against a PRIVATE repo — pi-git-auth,
# web-print-service and scale-reader-service are public and succeed
# without any auth at all.
# 4. Shred the source PEM: `shred -u /tmp/michelli-app.pem`
#
# Idempotent. Re-running re-installs the helpers (picks up newer versions
# from the repo) and updates the config without disturbing the PEM if it's
# already in place. Pass `--pem` again to replace the PEM.
#
# Perms model: /etc/michelli is 0755 (traversable), the conf is 0644 (public
# IDs), the PEM is 0644 (readable by any local user). On a single-user
# kiosk/scale Pi that's fine — anyone with a shell on the box already has
# sudo. On a multi-tenant host you'd want group-restricted perms instead.
#
# Requires: bash, openssl (preinstalled on Pi OS), curl + jq (apt-installed
# by this script if missing).
set -euo pipefail
# Baked-in App identity. FILL THESE IN once the Michelli GitHub App is
# created (see the pi-git-auth README) so operators only need to supply
# --install-id and --pem the first time. Until then, pass --client-id.
CLIENT_ID_DEFAULT="Iv23livFZQOXhMbgSTed" # GTMichelli-Dev michelli-fleet App Client ID
APP_ID_DEFAULT="4260960" # GTMichelli-Dev michelli-fleet App numeric App ID
CLIENT_ID=""
APP_ID=""
INSTALL_ID=""
PEM_SRC=""
CONF_DIR="/etc/michelli"
PEM_DST="$CONF_DIR/github-app.pem"
CONF_DST="$CONF_DIR/github-app.conf"
TOKEN_BIN="/usr/local/bin/michelli-github-app-token"
HELPER_BIN="/usr/local/bin/git-credential-michelli"
FETCH_BIN="/usr/local/bin/fetch-release"
# The service repos' release notes say `fetch_release <asset>`, with an
# underscore, because it used to be a shell function you pasted. Both spellings
# land so those instructions work verbatim on a box that has been through this.
FETCH_ALIAS="/usr/local/bin/fetch_release"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
while [[ $# -gt 0 ]]; do
case "$1" in
--client-id) CLIENT_ID="$2"; shift 2 ;;
--app-id) APP_ID="$2"; shift 2 ;;
--install-id) INSTALL_ID="$2"; shift 2 ;;
--pem) PEM_SRC="$2"; shift 2 ;;
-h|--help)
cat <<EOF
Usage: sudo bash setup-pi-github-app.sh [options]
Required on first run (and any time the Installation changes):
--install-id <N> Installation ID (from the install URL on the App's page)
--pem <path> Copy this PEM to $PEM_DST
App identity (bake the defaults into this script after creating the App):
--client-id <ID> Michelli App Client ID${CLIENT_ID_DEFAULT:+ (default: $CLIENT_ID_DEFAULT)}
--app-id <N> Michelli App numeric App ID${APP_ID_DEFAULT:+ (default: $APP_ID_DEFAULT)}
On re-runs (e.g. after \`git pull\` updates the helper scripts), pass no flags
to refresh helpers in place — existing conf + PEM are reused.
After this:
git ls-remote https://github.com/GTMichelli-Dev/pi-network-setup.git
should succeed silently. Then any service install script can \`git clone\` /
\`git pull\` over plain HTTPS — no PAT, no SSH config.
EOF
exit 0 ;;
*) echo "Unknown argument: $1" >&2; exit 2 ;;
esac
done
# Apply baked-in defaults when no override was passed.
CLIENT_ID="${CLIENT_ID:-$CLIENT_ID_DEFAULT}"
APP_ID="${APP_ID:-$APP_ID_DEFAULT}"
if [[ "$(id -u)" -ne 0 ]]; then
echo "This script must run as root (use sudo)." >&2
exit 1
fi
# 1. Apt-install deps if missing. git is one of them: step 6 registers the
# credential helper with `git config --system` and step 7 smoke-tests with
# `git ls-remote`. It used to be safe to assume — every route here went
# through something that had already installed it — until install.sh
# started fetching over curl. A Pi OS Lite box with no git is exactly the
# box this runs on.
need_apt=()
command -v curl >/dev/null 2>&1 || need_apt+=(curl)
command -v jq >/dev/null 2>&1 || need_apt+=(jq)
command -v openssl >/dev/null 2>&1 || need_apt+=(openssl)
command -v git >/dev/null 2>&1 || need_apt+=(git)
#
# A freshly created cloud VM (Vultr, etc.) is still running cloud-init and
# unattended-upgrades for its first few minutes, and they hold the apt lock
# — so a tech who SSHes straight in hits "Could not get lock" and the whole
# install dies. Wait for the lock instead, and keep Ubuntu's needrestart
# from stopping on a "which services should be restarted?" dialog.
if [[ ${#need_apt[@]} -gt 0 ]]; then
echo "Installing: ${need_apt[*]}"
export DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a
APT_WAIT=(-o DPkg::Lock::Timeout=600)
for attempt in 1 2 3 4 5 6; do
if apt-get "${APT_WAIT[@]}" update -qq \
&& apt-get "${APT_WAIT[@]}" install -y -qq "${need_apt[@]}"; then
break
fi
if [[ $attempt -eq 6 ]]; then
echo "apt-get could not install ${need_apt[*]}. If another apt run is still" >&2
echo "going (common on a VM that booted minutes ago), wait for it and re-run." >&2
exit 1
fi
echo "apt is busy or failed (attempt $attempt of 6) — retrying in 20s..."
sleep 20
done
fi
# 2. Conf dir — world-traversable so the helper running as a regular user can
# read the conf + PEM.
install -d -m 0755 "$CONF_DIR"
# 3. Install helpers from the repo (this script lives in scripts/, helpers next to it).
for f in michelli-github-app-token.sh git-credential-michelli.sh fetch-release.sh; do
[[ -f "$SCRIPT_DIR/$f" ]] || {
echo "Cannot find $f alongside $SCRIPT_DIR — make sure you ran this from a repo checkout." >&2
exit 1
}
done
install -m 0755 "$SCRIPT_DIR/michelli-github-app-token.sh" "$TOKEN_BIN"
install -m 0755 "$SCRIPT_DIR/git-credential-michelli.sh" "$HELPER_BIN"
install -m 0755 "$SCRIPT_DIR/fetch-release.sh" "$FETCH_BIN"
ln -sf "$FETCH_BIN" "$FETCH_ALIAS"
echo "Installed $TOKEN_BIN, $HELPER_BIN and $FETCH_BIN"
# 4. Place / update conf. Write whenever an install ID was passed; otherwise
# require an existing conf.
if [[ -n "$INSTALL_ID" ]]; then
if [[ -z "$CLIENT_ID" && -z "$APP_ID" ]]; then
echo "No --client-id / --app-id passed and no default baked into this script." >&2
echo "Create the App first (see the pi-git-auth README), then pass --client-id." >&2
exit 1
fi
cat > "$CONF_DST" <<EOF
# Michelli GitHub App — installation token config. Public IDs only; the secret
# is the PEM. CLIENT_ID is the modern issuer identifier (GitHub's
# recommendation); APP_ID is kept as a legacy alternative. Either works.
CLIENT_ID=$CLIENT_ID
APP_ID=$APP_ID
INSTALL_ID=$INSTALL_ID
EOF
chmod 0644 "$CONF_DST"
echo "Wrote $CONF_DST"
elif [[ ! -f "$CONF_DST" ]]; then
echo "Missing $CONF_DST and no --install-id provided. Re-run with --install-id." >&2
exit 1
fi
# 5. Place PEM if provided. Mode 0644 — see header for the kiosk-Pi rationale.
if [[ -n "$PEM_SRC" ]]; then
[[ -r "$PEM_SRC" ]] || { echo "Cannot read PEM at $PEM_SRC" >&2; exit 1; }
install -m 0644 "$PEM_SRC" "$PEM_DST"
echo "Installed PEM at $PEM_DST (mode 0644)"
echo "Reminder: shred the source PEM at $PEM_SRC when you're done:"
echo " shred -u '$PEM_SRC'"
elif [[ ! -f "$PEM_DST" ]]; then
echo "Missing $PEM_DST and no --pem provided. Place the App PEM first, then re-run." >&2
exit 1
fi
# 6. Register the credential helper system-wide for github.com/GTMichelli-Dev.
# --replace-all keeps the gitconfig idempotent across re-runs. Explicit
# umask 022 in case the surrounding shell tightened it.
umask 022
git config --system --replace-all "credential.https://github.com/GTMichelli-Dev.helper" "$HELPER_BIN"
git config --system --replace-all "credential.https://github.com/GTMichelli-Dev.useHttpPath" "true"
chmod 0644 /etc/gitconfig 2>/dev/null || true
echo "Registered credential helper in /etc/gitconfig for github.com/GTMichelli-Dev"
# 7. Smoke-test by minting a token and hitting ls-remote against a PRIVATE repo.
# It must be private: pi-git-auth, web-print-service and scale-reader-service
# are public, and ls-remote against a public repo succeeds even with no
# credential helper at all, so it would pass on a Pi where the App auth is
# entirely broken.
SMOKE_REPO="pi-network-setup"
echo
echo "Smoke-testing..."
TOKEN=$("$TOKEN_BIN")
if [[ -n "$TOKEN" ]]; then
echo "Token minted ($(printf '%s' "$TOKEN" | wc -c) chars)."
else
echo "Token mint returned empty output." >&2
exit 1
fi
if git ls-remote "https://github.com/GTMichelli-Dev/$SMOKE_REPO.git" HEAD >/dev/null 2>&1; then
echo "git ls-remote succeeded against the private $SMOKE_REPO repo. Setup done."
else
echo "git ls-remote failed — check App permissions (Contents: Read) and Installation repo selection." >&2
echo "Also check the box can reach github.com on port 443 (outbound HTTPS is all this needs)." >&2
exit 1
fi