-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathinstall.sh
More file actions
executable file
·496 lines (433 loc) · 18.1 KB
/
install.sh
File metadata and controls
executable file
·496 lines (433 loc) · 18.1 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
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
#!/bin/sh
# shellcheck shell=sh
#
# Steer install script
# Repo: https://github.com/enforcegrid/steer
# Docs: https://github.com/enforcegrid/steer#readme
#
# Downloads the appropriate Steer binary for the host OS/arch from GitHub
# Releases, verifies its SHA256 against the published SHA256SUMS file, and
# installs it to a sane location on $PATH.
#
# Usage:
# curl -fsSL https://raw.githubusercontent.com/enforcegrid/steer/main/install.sh | sh
#
# Environment variables:
# STEER_VERSION Pin to a specific release tag (e.g. v0.1.0).
# Default: latest GitHub release.
# STEER_INSTALL_DIR Override install directory.
# Default: /usr/local/bin if writable, else
# $HOME/.local/bin.
# STEER_NO_MODIFY_PATH If "1", suppress the PATH warning when installing
# to a directory that is not on $PATH.
# STEER_DRY_RUN If "1", print what would be done and exit before
# downloading anything. Safe for CI smoke tests.
#
# This script is POSIX sh. No bashisms. Verified with `shellcheck -s sh`.
set -eu
# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
REPO_OWNER="enforcegrid"
REPO_NAME="steer"
REPO_URL="https://github.com/${REPO_OWNER}/${REPO_NAME}"
DOCS_URL="${REPO_URL}#readme"
QUICKSTART_URL="${REPO_URL}#quick-start"
# Set by main(); declared up front so cleanup() can reference safely.
TMPDIR_STEER=""
# ---------------------------------------------------------------------------
# Output helpers
# ---------------------------------------------------------------------------
# Detect TTY for color output. Disabled when stdout is not a terminal,
# when NO_COLOR is set, or when TERM is dumb.
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ] && [ "${TERM:-}" != "dumb" ]; then
BOLD=$(printf '\033[1m')
DIM=$(printf '\033[2m')
RED=$(printf '\033[31m')
GREEN=$(printf '\033[32m')
YELLOW=$(printf '\033[33m')
BLUE=$(printf '\033[34m')
RESET=$(printf '\033[0m')
else
BOLD=""
DIM=""
RED=""
GREEN=""
YELLOW=""
BLUE=""
RESET=""
fi
info() {
printf '%s%sinfo%s: %s\n' "$BOLD" "$BLUE" "$RESET" "$1"
}
warn() {
printf '%s%swarn%s: %s\n' "$BOLD" "$YELLOW" "$RESET" "$1" >&2
}
err() {
printf '%s%serror%s: %s\n' "$BOLD" "$RED" "$RESET" "$1" >&2
exit 1
}
success() {
printf '%s%s%s\n' "$BOLD$GREEN" "$1" "$RESET"
}
# ---------------------------------------------------------------------------
# Cleanup
# ---------------------------------------------------------------------------
cleanup() {
if [ -n "$TMPDIR_STEER" ] && [ -d "$TMPDIR_STEER" ]; then
rm -rf "$TMPDIR_STEER"
fi
}
trap cleanup EXIT INT TERM HUP
# ---------------------------------------------------------------------------
# Command/tool helpers
# ---------------------------------------------------------------------------
has_cmd() {
command -v "$1" >/dev/null 2>&1
}
require_cmd() {
if ! has_cmd "$1"; then
err "required command not found: $1"
fi
}
# ---------------------------------------------------------------------------
# OS/arch detection
# ---------------------------------------------------------------------------
detect_target() {
_ostype=$(uname -s 2>/dev/null || echo unknown)
_cputype=$(uname -m 2>/dev/null || echo unknown)
case "$_ostype" in
Darwin)
_os="apple-darwin"
;;
Linux)
_os="unknown-linux-gnu"
;;
FreeBSD|OpenBSD|NetBSD|DragonFly)
err "unsupported OS: $_ostype. Supported: macOS (Darwin), Linux. See $DOCS_URL"
;;
MINGW*|MSYS*|CYGWIN*|Windows_NT)
err "Windows is not supported by this script. Download the .zip from $REPO_URL/releases and extract manually."
;;
*)
err "unsupported OS: $_ostype. Supported: macOS, Linux. See $DOCS_URL"
;;
esac
case "$_cputype" in
x86_64|amd64)
_arch="x86_64"
;;
arm64|aarch64)
_arch="aarch64"
;;
*)
err "unsupported CPU architecture: $_cputype. Supported: x86_64, aarch64/arm64. See $DOCS_URL"
;;
esac
# Apple-Darwin uses 'aarch64' in Rust triples; macOS uname reports 'arm64'.
# Our mapping above normalises both to 'aarch64', then the triple is built
# consistently. (e.g. aarch64-apple-darwin, x86_64-apple-darwin)
TARGET="${_arch}-${_os}"
}
# ---------------------------------------------------------------------------
# Version resolution
# ---------------------------------------------------------------------------
# Validate that a version string looks like a release tag: vMAJOR.MINOR.PATCH
# with optional pre-release suffix. We are strict because this string is
# interpolated into URLs.
validate_version() {
case "$1" in
v[0-9]*)
# Reject anything containing shell metacharacters or whitespace.
case "$1" in
*[!A-Za-z0-9._+-]*)
err "invalid STEER_VERSION '$1': contains disallowed characters"
;;
esac
;;
*)
err "invalid STEER_VERSION '$1': must start with 'v' (e.g. v0.1.0)"
;;
esac
}
# Resolve latest tag via the GitHub API. We query `/releases?per_page=1`
# which returns the most recent release including prereleases — unlike the
# `/releases/latest` redirect, which only resolves to stable (non-prerelease)
# releases and 404s onto the bare /releases page when only prereleases exist.
#
# Tradeoff: the GitHub API is rate-limited to 60 req/hr unauthenticated.
# We surface that explicitly so the user can pin `STEER_VERSION` and skip
# this call entirely.
#
# We parse `tag_name` from the JSON with POSIX sed — no `jq` dependency.
resolve_latest_version() {
_api_url="https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/releases?per_page=1"
# Use a dedicated temp file. TMPDIR_STEER may not yet be set at this
# point (resolve_latest_version runs before main() provisions it).
_tmp_body=$(mktemp 2>/dev/null || mktemp -t steer-latest) || \
err "failed to create temp file for GitHub API response"
# Capture HTTP status separately so we can distinguish rate-limit from
# "no releases yet" (200 + empty array `[]`) from transient network errors.
_http_code=$(curl -sS --tlsv1.2 --proto '=https' \
--retry 1 --retry-delay 2 \
-H 'Accept: application/vnd.github+json' \
-H 'User-Agent: steer-install.sh' \
-o "$_tmp_body" \
-w '%{http_code}' \
"$_api_url" 2>/dev/null || echo "000")
case "$_http_code" in
200)
;;
403|429)
err "GitHub API rate-limited (HTTP $_http_code) at $_api_url.
The unauthenticated GitHub API allows 60 requests/hour per IP.
Set STEER_VERSION explicitly (e.g. STEER_VERSION=v0.1.0) to skip this call, or retry later."
;;
404)
err "repository not found at $_api_url (HTTP 404). Verify REPO_OWNER/REPO_NAME or set STEER_VERSION explicitly."
;;
000)
err "failed to reach GitHub API at $_api_url. Check your network connection or set STEER_VERSION explicitly."
;;
*)
err "unexpected HTTP $_http_code from $_api_url. Set STEER_VERSION explicitly to work around this."
;;
esac
if [ ! -s "$_tmp_body" ]; then
err "GitHub API returned empty body at $_api_url. Set STEER_VERSION explicitly to work around this."
fi
# Detect the "no releases at all" case: API returns `[]` (or whitespace
# plus `[]`). We test this before parsing tag_name to give a clearer
# error than "failed to parse".
_body_compact=$(tr -d ' \t\r\n' < "$_tmp_body")
if [ "$_body_compact" = "[]" ]; then
err "no releases published yet at $REPO_URL. Wait for the first release or set STEER_VERSION explicitly."
fi
# Extract the first tag_name value. The response is an array of release
# objects; tag_name appears once per object. With `per_page=1` we get
# at most one. POSIX sed: capture between the first pair of quotes after
# `"tag_name":`.
VERSION=$(sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$_tmp_body" | head -n 1 | tr -d '\r\n')
rm -f "$_tmp_body"
if [ -z "$VERSION" ]; then
err "failed to parse tag_name from GitHub API response at $_api_url. Set STEER_VERSION explicitly to work around this."
fi
}
# ---------------------------------------------------------------------------
# Download with one retry on failure
# ---------------------------------------------------------------------------
download() {
# $1 = url, $2 = output path
# --tlsv1.2 + --proto '=https' refuse downgrade/redirect to plaintext HTTP.
# Critical for security-tool installers: prevents `-L` from following a
# hostile redirect to http://. Mirrors rustup-init.sh's behavior.
_url=$1
_out=$2
if curl -fsSL --tlsv1.2 --proto '=https' --retry 1 --retry-delay 2 -o "$_out" "$_url"; then
return 0
fi
warn "download failed, retrying once: $_url"
sleep 2
if curl -fsSL --tlsv1.2 --proto '=https' --retry 1 --retry-delay 4 -o "$_out" "$_url"; then
return 0
fi
err "failed to download $_url after retry. Check your network connection."
}
# ---------------------------------------------------------------------------
# Checksum verification
# ---------------------------------------------------------------------------
# Detect which sha256 tool is available. Linux ships sha256sum; macOS ships
# shasum. We feature-detect rather than branching on OS.
sha256_of() {
if has_cmd sha256sum; then
sha256sum "$1" | awk '{print $1}'
elif has_cmd shasum; then
shasum -a 256 "$1" | awk '{print $1}'
else
err "no sha256 tool found (need sha256sum or shasum)"
fi
}
verify_checksum() {
# $1 = path to artifact, $2 = path to SHA256SUMS, $3 = artifact filename as it appears in SHA256SUMS
_file=$1
_sums=$2
_name=$3
# Lines in SHA256SUMS look like: "<hex> <filename>"
# awk with exact field-2 string equality — avoids the '.' regex-wildcard
# collision that grep would have on filenames like "steer-v0.1.0-...tar.gz"
_expected=$(awk -v n="$_name" '$2 == n { print $1; exit }' "$_sums")
if [ -z "$_expected" ]; then
err "checksum entry for '${_name}' not found in SHA256SUMS. Release may be incomplete or tampered."
fi
_actual=$(sha256_of "$_file")
if [ "$_expected" != "$_actual" ]; then
err "SHA256 mismatch for ${_name}!
expected: $_expected
actual: $_actual
Refusing to install. The download may be corrupt or tampered with."
fi
info "SHA256 verified: ${_name}"
}
# ---------------------------------------------------------------------------
# Install dir resolution
# ---------------------------------------------------------------------------
# Returns 0 if the directory exists and is writable without sudo.
dir_is_writable() {
[ -d "$1" ] && [ -w "$1" ]
}
resolve_install_dir() {
if [ -n "${STEER_INSTALL_DIR:-}" ]; then
INSTALL_DIR=$STEER_INSTALL_DIR
mkdir -p "$INSTALL_DIR" 2>/dev/null || err "STEER_INSTALL_DIR ($INSTALL_DIR) is not creatable."
if ! dir_is_writable "$INSTALL_DIR"; then
err "STEER_INSTALL_DIR ($INSTALL_DIR) is not writable."
fi
return 0
fi
if dir_is_writable "/usr/local/bin"; then
INSTALL_DIR="/usr/local/bin"
return 0
fi
_user_bin="${HOME}/.local/bin"
if mkdir -p "$_user_bin" 2>/dev/null && dir_is_writable "$_user_bin"; then
INSTALL_DIR=$_user_bin
return 0
fi
err "no writable install directory. Tried /usr/local/bin and \$HOME/.local/bin.
Set STEER_INSTALL_DIR to a writable location or re-run with elevated privileges."
}
# Check whether install dir is on $PATH (colon-separated, exact-match component).
dir_on_path() {
case ":${PATH}:" in
*":$1:"*) return 0 ;;
*) return 1 ;;
esac
}
# ---------------------------------------------------------------------------
# Config asset placement (steer.example.yaml, default.cedar)
# ---------------------------------------------------------------------------
# Place bundled config samples into $HOME/.config/steer/ if they don't already
# exist. We never overwrite existing user config.
install_config_assets() {
_src=$1 # extracted tarball root
_cfg_dir="${HOME}/.config/steer"
_pol_dir="${_cfg_dir}/policies"
mkdir -p "$_pol_dir" 2>/dev/null || {
warn "could not create ${_cfg_dir}; skipping bundled config install."
return 0
}
if [ -f "${_src}/steer.example.yaml" ] && [ ! -f "${_cfg_dir}/steer.example.yaml" ]; then
cp "${_src}/steer.example.yaml" "${_cfg_dir}/steer.example.yaml"
info "installed sample config: ${_cfg_dir}/steer.example.yaml"
fi
if [ -f "${_src}/dsl/policies/default.cedar" ] && [ ! -f "${_pol_dir}/default.cedar" ]; then
cp "${_src}/dsl/policies/default.cedar" "${_pol_dir}/default.cedar"
info "installed default policy: ${_pol_dir}/default.cedar"
fi
# Bootstrap a working steer.yaml on first install. Without this, the
# binary defaults to looking for ./steer.yaml in CWD or this XDG path —
# and the example file has a CWD-relative policy_dir ("./dsl/policies")
# that won't resolve from anywhere the binary is realistically launched.
# We sed the relative path to the operator's XDG policies dir so
# `steer` (no args) just works after install.
#
# We never overwrite an existing steer.yaml — operator edits are sacred.
if [ -f "${_src}/steer.example.yaml" ] && [ ! -f "${_cfg_dir}/steer.yaml" ]; then
# The | delimiter avoids escaping forward slashes in _pol_dir.
sed "s|policy_dir: \"./dsl/policies\"|policy_dir: \"${_pol_dir}\"|" \
"${_src}/steer.example.yaml" > "${_cfg_dir}/steer.yaml"
info "bootstrapped working config: ${_cfg_dir}/steer.yaml"
fi
}
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
main() {
require_cmd uname
require_cmd curl
require_cmd tar
require_cmd grep
require_cmd awk
require_cmd sed
detect_target
info "detected target: ${TARGET}"
if [ -n "${STEER_VERSION:-}" ]; then
validate_version "$STEER_VERSION"
VERSION=$STEER_VERSION
info "using pinned version: ${VERSION}"
else
info "resolving latest version..."
resolve_latest_version
validate_version "$VERSION"
info "latest version: ${VERSION}"
fi
ARTIFACT="steer-${VERSION}-${TARGET}.tar.gz"
ARTIFACT_URL="${REPO_URL}/releases/download/${VERSION}/${ARTIFACT}"
SUMS_URL="${REPO_URL}/releases/download/${VERSION}/SHA256SUMS"
resolve_install_dir
info "install dir: ${INSTALL_DIR}"
if [ "${STEER_DRY_RUN:-}" = "1" ]; then
printf '\n%sDry run summary:%s\n' "$BOLD" "$RESET"
printf ' target: %s\n' "$TARGET"
printf ' version: %s\n' "$VERSION"
printf ' artifact: %s\n' "$ARTIFACT"
printf ' artifact url: %s\n' "$ARTIFACT_URL"
printf ' sums url: %s\n' "$SUMS_URL"
printf ' install dir: %s\n' "$INSTALL_DIR"
printf ' sha256 tool: %s\n' "$(has_cmd sha256sum && echo sha256sum || (has_cmd shasum && echo 'shasum -a 256') || echo none)"
printf '%s(dry run: nothing was downloaded or installed)%s\n' "$DIM" "$RESET"
return 0
fi
# Create temp workspace.
TMPDIR_STEER=$(mktemp -d 2>/dev/null || mktemp -d -t steer-install)
[ -d "$TMPDIR_STEER" ] || err "failed to create temp directory"
info "downloading ${ARTIFACT}..."
download "$ARTIFACT_URL" "${TMPDIR_STEER}/${ARTIFACT}"
info "downloading SHA256SUMS..."
download "$SUMS_URL" "${TMPDIR_STEER}/SHA256SUMS"
verify_checksum "${TMPDIR_STEER}/${ARTIFACT}" "${TMPDIR_STEER}/SHA256SUMS" "${ARTIFACT}"
info "extracting..."
if ! tar -xzf "${TMPDIR_STEER}/${ARTIFACT}" -C "$TMPDIR_STEER"; then
err "tarball extraction failed. Archive may be corrupt."
fi
# The tarball contains a top-level directory: steer-vX.Y.Z-<target>/
_extracted="${TMPDIR_STEER}/steer-${VERSION}-${TARGET}"
if [ ! -d "$_extracted" ]; then
err "expected directory ${_extracted} not present after extraction. Release layout mismatch."
fi
if [ ! -f "${_extracted}/steer" ]; then
err "binary 'steer' not found in extracted tarball at ${_extracted}/steer."
fi
info "installing to ${INSTALL_DIR}/steer..."
# Install via temp move + rename to keep target dir consistent on failure.
_dest="${INSTALL_DIR}/steer"
if ! cp "${_extracted}/steer" "${_dest}.new"; then
err "failed to copy binary to ${INSTALL_DIR}. Permissions?"
fi
chmod +x "${_dest}.new"
mv "${_dest}.new" "$_dest"
install_config_assets "$_extracted"
# Post-install summary
printf '\n'
success "Installed steer ${VERSION} to ${_dest}"
if ! dir_on_path "$INSTALL_DIR" && [ "${STEER_NO_MODIFY_PATH:-}" != "1" ]; then
# All three lines route to stderr so they stay grouped with the
# `warn` header. Mixing stdout and stderr here interleaves under
# block-buffered pipes (curl ... | sh inside Docker without a TTY).
warn "${INSTALL_DIR} is not on your \$PATH."
printf ' Add it by appending one of these to your shell profile:\n' >&2
# The literal $PATH below is intentional — it is shell guidance for the user.
# shellcheck disable=SC2016
printf ' %sexport PATH="%s:$PATH"%s\n' "$DIM" "$INSTALL_DIR" "$RESET" >&2
printf ' Then reload your shell, e.g. %ssource ~/.bashrc%s or %ssource ~/.zshrc%s.\n' "$DIM" "$RESET" "$DIM" "$RESET" >&2
fi
printf '\n'
printf '%sNext steps:%s\n' "$BOLD" "$RESET"
printf ' steer --version\n'
printf ' steer --port 8080\n'
printf '\n'
printf ' Quick start: %s\n' "$QUICKSTART_URL"
printf ' Docs: %s\n' "$DOCS_URL"
printf '\n'
}
main "$@"