349 lines
12 KiB
Bash
Executable File
349 lines
12 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|
# lint-infra.sh — Infrastructure validation for the ByteLyst ecosystem
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|
# Validates all Dockerfiles (hadolint) and Helm charts (helm lint + template)
|
|
# across the 11 ByteLyst repos.
|
|
#
|
|
# Usage:
|
|
# ./scripts/lint-infra.sh # Lint everything (Dockerfiles + Helm charts)
|
|
# ./scripts/lint-infra.sh --docker # Dockerfiles only
|
|
# ./scripts/lint-infra.sh --helm # Helm charts only
|
|
# ./scripts/lint-infra.sh --fix # Show suggested fixes alongside errors
|
|
# ./scripts/lint-infra.sh path/to/Dockerfile [path/to/Chart-dir] ...
|
|
#
|
|
# Prerequisites:
|
|
# brew install hadolint helm
|
|
#
|
|
# Exit codes:
|
|
# 0 All checks passed
|
|
# 1 One or more checks failed
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|
set -euo pipefail
|
|
|
|
# ── Colors ─────────────────────────────────────────────────────────────
|
|
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
|
|
CYAN='\033[0;36m'; BOLD='\033[1m'; DIM='\033[2m'; NC='\033[0m'
|
|
|
|
# ── Globals ────────────────────────────────────────────────────────────
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
WORKSPACE_ROOT="$(cd "$REPO_ROOT/.." && pwd)"
|
|
|
|
LINT_DOCKER=true
|
|
LINT_HELM=true
|
|
SHOW_FIX=false
|
|
EXPLICIT_PATHS=()
|
|
|
|
PASS=0
|
|
FAIL=0
|
|
SKIP=0
|
|
FAILURES=()
|
|
|
|
# ── Known Hadolint false-positive suppressions ─────────────────────────
|
|
# DL3045: COPY to relative path — all our Dockerfiles set WORKDIR first
|
|
# DL3018: Pin versions in apk add — we use node:22-alpine base images
|
|
# DL3008: Pin versions in apt-get — impractical for Python sidecar base images
|
|
# DL3059: Multiple consecutive RUN — intentional for layer caching
|
|
# SC2155: Declare and assign separately — false positive for BuildKit secret mount pattern
|
|
# (export VAR="$(cat /run/secrets/...)" is the canonical Docker secret idiom)
|
|
HADOLINT_IGNORE="DL3045,DL3018,DL3008,DL3059,SC2155"
|
|
|
|
# ── ByteLyst ecosystem repos (relative to workspace root) ─────────────
|
|
ECOSYSTEM_REPOS=(
|
|
learning_ai_common_plat
|
|
learning_voice_ai_agent
|
|
learning_multimodal_memory_agents
|
|
learning_ai_clock
|
|
learning_ai_jarvis_jr
|
|
learning_ai_fastgap
|
|
learning_ai_peakpulse
|
|
learning_ai_flowmonk
|
|
learning_ai_notes
|
|
learning_ai_trails
|
|
learning_ai_local_memory_gpt
|
|
)
|
|
|
|
# ── Helpers ────────────────────────────────────────────────────────────
|
|
log() { echo -e "${CYAN}[lint-infra]${NC} $*"; }
|
|
ok() { echo -e " ${GREEN}✓${NC} $*"; PASS=$((PASS + 1)); }
|
|
fail() { echo -e " ${RED}✗${NC} $*"; FAIL=$((FAIL + 1)); FAILURES+=("$1"); }
|
|
skip() { echo -e " ${DIM}⊘ $*${NC}"; SKIP=$((SKIP + 1)); }
|
|
warn() { echo -e " ${YELLOW}⚠${NC} $*"; }
|
|
|
|
usage() {
|
|
echo "Usage: $(basename "$0") [OPTIONS] [PATH ...]"
|
|
echo ""
|
|
echo "Options:"
|
|
echo " --docker Lint Dockerfiles only"
|
|
echo " --helm Lint Helm charts only"
|
|
echo " --fix Show suggested fixes alongside errors"
|
|
echo " -h, --help Show this help"
|
|
echo ""
|
|
echo "If PATHs are given, only those files/dirs are checked."
|
|
echo "Otherwise, auto-discovers across all 11 ByteLyst repos."
|
|
echo ""
|
|
echo "Prerequisites: brew install hadolint helm"
|
|
}
|
|
|
|
# Check if a tool is available, print install instructions if not
|
|
require_tool() {
|
|
local tool="$1" install_cmd="$2"
|
|
if ! command -v "$tool" &>/dev/null; then
|
|
echo -e "${RED}Error:${NC} '${tool}' is not installed."
|
|
echo -e " Install: ${BOLD}${install_cmd}${NC}"
|
|
echo ""
|
|
return 1
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
# Return a short relative path for display
|
|
short_path() {
|
|
local full="$1"
|
|
# Try to make path relative to workspace root
|
|
if [[ "$full" == "$WORKSPACE_ROOT"/* ]]; then
|
|
echo "${full#$WORKSPACE_ROOT/}"
|
|
else
|
|
echo "$full"
|
|
fi
|
|
}
|
|
|
|
# ── Auto-discovery ─────────────────────────────────────────────────────
|
|
discover_dockerfiles() {
|
|
local files=()
|
|
for repo in "${ECOSYSTEM_REPOS[@]}"; do
|
|
local repo_dir="${WORKSPACE_ROOT}/${repo}"
|
|
[ -d "$repo_dir" ] || continue
|
|
while IFS= read -r -d '' f; do
|
|
files+=("$f")
|
|
done < <(find "$repo_dir" -maxdepth 4 -name 'Dockerfile*' \
|
|
-not -path '*/node_modules/*' \
|
|
-not -path '*/.git/*' \
|
|
-not -path '*/_deferred*' \
|
|
-print0 2>/dev/null | sort -z)
|
|
done
|
|
echo "${files[@]}"
|
|
}
|
|
|
|
discover_helm_charts() {
|
|
local dirs=()
|
|
for repo in "${ECOSYSTEM_REPOS[@]}"; do
|
|
local repo_dir="${WORKSPACE_ROOT}/${repo}"
|
|
[ -d "$repo_dir" ] || continue
|
|
while IFS= read -r -d '' f; do
|
|
dirs+=("$(dirname "$f")")
|
|
done < <(find "$repo_dir" -maxdepth 4 -name 'Chart.yaml' \
|
|
-not -path '*/node_modules/*' \
|
|
-not -path '*/.git/*' \
|
|
-print0 2>/dev/null | sort -z)
|
|
done
|
|
echo "${dirs[@]+"${dirs[@]}"}"
|
|
}
|
|
|
|
# ── Lint functions ─────────────────────────────────────────────────────
|
|
|
|
lint_dockerfile() {
|
|
local file="$1"
|
|
local display
|
|
display=$(short_path "$file")
|
|
|
|
if [ ! -f "$file" ]; then
|
|
skip "${display} (not found)"
|
|
return
|
|
fi
|
|
|
|
local output exit_code=0
|
|
# Build hadolint args
|
|
local hadolint_args=(--no-color)
|
|
IFS=',' read -ra ignore_rules <<< "$HADOLINT_IGNORE"
|
|
for rule in "${ignore_rules[@]}"; do
|
|
hadolint_args+=(--ignore "$rule")
|
|
done
|
|
|
|
output=$(hadolint "${hadolint_args[@]}" "$file" 2>&1) || exit_code=$?
|
|
|
|
if [ $exit_code -eq 0 ]; then
|
|
ok "${display}"
|
|
else
|
|
fail "${display}"
|
|
# Print each warning/error indented
|
|
while IFS= read -r line; do
|
|
if [[ "$line" == *"error"* ]]; then
|
|
echo -e " ${RED}${line}${NC}"
|
|
elif [[ "$line" == *"warning"* ]]; then
|
|
echo -e " ${YELLOW}${line}${NC}"
|
|
else
|
|
echo -e " ${DIM}${line}${NC}"
|
|
fi
|
|
done <<< "$output"
|
|
|
|
if $SHOW_FIX; then
|
|
echo -e " ${CYAN}Fix: review hadolint wiki — https://github.com/hadolint/hadolint/wiki${NC}"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
lint_helm_chart() {
|
|
local chart_dir="$1"
|
|
local display
|
|
display=$(short_path "$chart_dir")
|
|
|
|
if [ ! -f "${chart_dir}/Chart.yaml" ]; then
|
|
skip "${display} (no Chart.yaml)"
|
|
return
|
|
fi
|
|
|
|
local output exit_code
|
|
|
|
# 1. helm lint (syntax + structure)
|
|
output=$(helm lint "$chart_dir" 2>&1) || exit_code=$?
|
|
if [ "${exit_code:-0}" -ne 0 ]; then
|
|
fail "${display} (helm lint)"
|
|
echo "$output" | while IFS= read -r line; do
|
|
echo -e " ${DIM}${line}${NC}"
|
|
done
|
|
return
|
|
fi
|
|
|
|
# 2. helm template (renders without errors)
|
|
output=$(helm template test-release "$chart_dir" 2>&1) || exit_code=$?
|
|
if [ "${exit_code:-0}" -ne 0 ]; then
|
|
fail "${display} (helm template)"
|
|
echo "$output" | tail -10 | while IFS= read -r line; do
|
|
echo -e " ${DIM}${line}${NC}"
|
|
done
|
|
return
|
|
fi
|
|
|
|
ok "${display}"
|
|
}
|
|
|
|
# ── Parse CLI ──────────────────────────────────────────────────────────
|
|
parse_args() {
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--docker)
|
|
LINT_DOCKER=true; LINT_HELM=false ;;
|
|
--helm)
|
|
LINT_DOCKER=false; LINT_HELM=true ;;
|
|
--fix)
|
|
SHOW_FIX=true ;;
|
|
-h|--help)
|
|
usage; exit 0 ;;
|
|
-*)
|
|
echo "Unknown option: $1"; usage; exit 1 ;;
|
|
*)
|
|
EXPLICIT_PATHS+=("$1") ;;
|
|
esac
|
|
shift
|
|
done
|
|
}
|
|
|
|
# ── Main ───────────────────────────────────────────────────────────────
|
|
main() {
|
|
parse_args "$@"
|
|
|
|
echo ""
|
|
echo -e "${BOLD}╔═══════════════════════════════════════════════════════════╗${NC}"
|
|
echo -e "${BOLD}║ ByteLyst Infrastructure Lint ║${NC}"
|
|
echo -e "${BOLD}╚═══════════════════════════════════════════════════════════╝${NC}"
|
|
echo ""
|
|
|
|
# ── Tool checks ────────────────────────────────────────────────────
|
|
local missing=false
|
|
if $LINT_DOCKER; then
|
|
require_tool hadolint "brew install hadolint" || missing=true
|
|
fi
|
|
if $LINT_HELM; then
|
|
require_tool helm "brew install helm" || missing=true
|
|
fi
|
|
if $missing; then
|
|
echo -e "${RED}Missing required tools. Install them and re-run.${NC}"
|
|
exit 1
|
|
fi
|
|
|
|
# ── Explicit paths mode ────────────────────────────────────────────
|
|
if [ ${#EXPLICIT_PATHS[@]} -gt 0 ]; then
|
|
log "Linting ${#EXPLICIT_PATHS[@]} explicit path(s)..."
|
|
echo ""
|
|
for p in "${EXPLICIT_PATHS[@]}"; do
|
|
if [[ "$(basename "$p")" == Dockerfile* ]]; then
|
|
lint_dockerfile "$p"
|
|
elif [ -f "${p}/Chart.yaml" ]; then
|
|
lint_helm_chart "$p"
|
|
else
|
|
warn "Unknown target: $p (expected Dockerfile* or dir with Chart.yaml)"
|
|
fi
|
|
done
|
|
else
|
|
# ── Auto-discover mode ─────────────────────────────────────────
|
|
if $LINT_DOCKER; then
|
|
log "Discovering Dockerfiles across ${#ECOSYSTEM_REPOS[@]} repos..."
|
|
echo ""
|
|
|
|
local dockerfiles
|
|
IFS=' ' read -ra dockerfiles <<< "$(discover_dockerfiles)"
|
|
|
|
if [ ${#dockerfiles[@]} -eq 0 ]; then
|
|
warn "No Dockerfiles found."
|
|
else
|
|
log "Found ${#dockerfiles[@]} Dockerfile(s)"
|
|
echo ""
|
|
for df in "${dockerfiles[@]}"; do
|
|
lint_dockerfile "$df"
|
|
done
|
|
fi
|
|
echo ""
|
|
fi
|
|
|
|
if $LINT_HELM; then
|
|
log "Discovering Helm charts across ${#ECOSYSTEM_REPOS[@]} repos..."
|
|
echo ""
|
|
|
|
local charts
|
|
IFS=' ' read -ra charts <<< "$(discover_helm_charts)"
|
|
|
|
if [ ${#charts[@]} -eq 0 ] || [ -z "${charts[0]:-}" ]; then
|
|
log "No Helm charts found (none in this workspace)."
|
|
else
|
|
log "Found ${#charts[@]} Helm chart(s)"
|
|
echo ""
|
|
for chart in "${charts[@]}"; do
|
|
lint_helm_chart "$chart"
|
|
done
|
|
fi
|
|
echo ""
|
|
fi
|
|
fi
|
|
|
|
# ── Summary ──────────────────────────────────────────────────────
|
|
echo ""
|
|
echo -e "${BOLD}═══ Summary ═══${NC}"
|
|
echo -e " ${GREEN}Passed:${NC} ${PASS}"
|
|
echo -e " ${RED}Failed:${NC} ${FAIL}"
|
|
if [ $SKIP -gt 0 ]; then
|
|
echo -e " ${DIM}Skipped: ${SKIP}${NC}"
|
|
fi
|
|
echo ""
|
|
|
|
if [ $FAIL -gt 0 ]; then
|
|
echo -e "${RED}${BOLD}FAILED${NC} — ${FAIL} issue(s) found:"
|
|
for f in "${FAILURES[@]}"; do
|
|
echo -e " ${RED}✗${NC} ${f}"
|
|
done
|
|
echo ""
|
|
echo -e "${DIM}Suppressed rules: ${HADOLINT_IGNORE}${NC}"
|
|
echo -e "${DIM}To see fix suggestions, re-run with --fix${NC}"
|
|
exit 1
|
|
else
|
|
echo -e "${GREEN}${BOLD}ALL PASSED${NC} — ${PASS} check(s) clean."
|
|
if [ $SKIP -gt 0 ]; then
|
|
echo -e "${DIM}(${SKIP} skipped)${NC}"
|
|
fi
|
|
exit 0
|
|
fi
|
|
}
|
|
|
|
main "$@"
|