refactor: 폴더 구조 변경
This commit is contained in:
Executable
+389
@@ -0,0 +1,389 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -Eeuo pipefail
|
||||
|
||||
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "${repo_root}"
|
||||
|
||||
errors=0
|
||||
|
||||
pass() {
|
||||
printf '[pass] %s\n' "$1"
|
||||
}
|
||||
|
||||
fail() {
|
||||
printf '[fail] %s\n' "$1" >&2
|
||||
errors=$((errors + 1))
|
||||
}
|
||||
|
||||
required_files=(
|
||||
README.md
|
||||
SECURITY.md
|
||||
infrastructure/.iac-engine.example
|
||||
)
|
||||
|
||||
required_directories=(
|
||||
bootstrap/foundation
|
||||
bootstrap/gitops
|
||||
infrastructure/components/_template
|
||||
infrastructure/stacks/_template
|
||||
infrastructure/live/_template
|
||||
gitops/clusters/_template
|
||||
gitops/platform/_template
|
||||
gitops/policies/_template
|
||||
gitops/tenants/_template
|
||||
gitops/apps/_template
|
||||
docs/architecture
|
||||
docs/decisions
|
||||
docs/runbooks
|
||||
scripts
|
||||
tests
|
||||
)
|
||||
|
||||
stage_errors="${errors}"
|
||||
for path in "${required_files[@]}"; do
|
||||
if [[ ! -f "${path}" || -L "${path}" ]]; then
|
||||
fail "required regular file is missing or has the wrong type: ${path}"
|
||||
fi
|
||||
done
|
||||
|
||||
for path in "${required_directories[@]}"; do
|
||||
if [[ ! -d "${path}" || -L "${path}" ]]; then
|
||||
fail "required directory is missing or has the wrong type: ${path}"
|
||||
fi
|
||||
done
|
||||
|
||||
if ((errors == stage_errors)); then
|
||||
pass "required repository structure"
|
||||
fi
|
||||
|
||||
stage_errors="${errors}"
|
||||
while IFS= read -r directory; do
|
||||
name="${directory##*/}"
|
||||
if [[ "${name}" == "_template" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
if [[ ! "${name}" =~ ^[a-z0-9][a-z0-9-]*$ ]]; then
|
||||
fail "directory must use lowercase kebab-case: ${directory}"
|
||||
fi
|
||||
done < <(
|
||||
find bootstrap docs gitops infrastructure scripts tests \
|
||||
\( \
|
||||
-name .build -o \
|
||||
-name .cache -o \
|
||||
-name .git -o \
|
||||
-name .terraform -o \
|
||||
-name .terragrunt-cache -o \
|
||||
-name dist -o \
|
||||
-name rendered -o \
|
||||
-name tmp \
|
||||
\) -prune -o \
|
||||
-type d -print \
|
||||
| sort
|
||||
)
|
||||
|
||||
if ((errors == stage_errors)); then
|
||||
pass "directory naming"
|
||||
fi
|
||||
|
||||
collect_source_files() {
|
||||
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||
git ls-files --cached --others --exclude-standard -z
|
||||
else
|
||||
find . \
|
||||
\( \
|
||||
-type d \
|
||||
\( \
|
||||
-name .build -o \
|
||||
-name .cache -o \
|
||||
-name .git -o \
|
||||
-name .terraform -o \
|
||||
-name .terragrunt-cache -o \
|
||||
-name dist -o \
|
||||
-name rendered -o \
|
||||
-name tmp \
|
||||
\) -prune \
|
||||
\) -o \
|
||||
-type f -print0
|
||||
fi
|
||||
}
|
||||
|
||||
source_files=()
|
||||
while IFS= read -r -d '' file; do
|
||||
file="${file#./}"
|
||||
if [[ -f "${file}" ]]; then
|
||||
source_files+=("${file}")
|
||||
fi
|
||||
done < <(collect_source_files)
|
||||
|
||||
is_sensitive_filename() {
|
||||
local file="$1"
|
||||
local name="${file##*/}"
|
||||
|
||||
case "${file}" in
|
||||
*/.decrypted/*)
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
case "${name}" in
|
||||
.env | .env.*)
|
||||
[[ "${name}" == ".env.example" ]] && return 1
|
||||
return 0
|
||||
;;
|
||||
*.dec.yaml | *.decrypted.yaml | *.jks | *.key | *.kubeconfig | *.p12 | *.pem | *.pfx | \
|
||||
*.tfplan | *.tfstate | *.tfstate.* | credentials | credentials.* | id_dsa | id_ecdsa | \
|
||||
id_ed25519 | id_rsa | kubeconfig | kubeconfig.* | plan.out | service-account.json | \
|
||||
service_account.json)
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
stage_errors="${errors}"
|
||||
for file in "${source_files[@]}"; do
|
||||
if is_sensitive_filename "${file}"; then
|
||||
fail "sensitive/local artifact must not be stored: ${file}"
|
||||
fi
|
||||
|
||||
if grep -Eq -- '-----BEGIN (DSA |EC |OPENSSH |RSA )?PRIVATE KEY-----' "${file}" 2>/dev/null; then
|
||||
fail "private key material must not be stored: ${file}"
|
||||
fi
|
||||
done
|
||||
|
||||
if ((errors == stage_errors)); then
|
||||
pass "sensitive/local artifact checks"
|
||||
fi
|
||||
|
||||
yaml_secret_kind_pattern="^['\"]?kind['\"]?[[:space:]]*:[[:space:]]*['\"]?Secret['\"]?([[:space:]]*(#.*)?)?$"
|
||||
json_secret_kind_pattern="['\"]kind['\"][[:space:]]*:[[:space:]]*['\"]Secret['\"]"
|
||||
yaml_sops_metadata_pattern='^sops:[[:space:]]*(#.*)?$'
|
||||
json_sops_metadata_pattern='^[[:space:]]*"sops"[[:space:]]*:'
|
||||
yaml_sops_mac_pattern='^[[:space:]]*mac:[[:space:]]*ENC\[AES256_GCM,'
|
||||
json_sops_mac_pattern='^[[:space:]]*"mac"[[:space:]]*:[[:space:]]*"ENC\[AES256_GCM,'
|
||||
|
||||
stage_errors="${errors}"
|
||||
for file in "${source_files[@]}"; do
|
||||
case "${file}" in
|
||||
bootstrap/*.yaml | bootstrap/*.yml | bootstrap/*.json | \
|
||||
gitops/*.yaml | gitops/*.yml | gitops/*.json)
|
||||
secret_manifest=0
|
||||
case "${file}" in
|
||||
*.json)
|
||||
grep -Eq "${json_secret_kind_pattern}" "${file}" && secret_manifest=1
|
||||
;;
|
||||
*)
|
||||
grep -Eq "${yaml_secret_kind_pattern}" "${file}" && secret_manifest=1
|
||||
;;
|
||||
esac
|
||||
|
||||
if ((secret_manifest == 1)); then
|
||||
case "${file}" in
|
||||
*.sops.yaml | *.sops.yml)
|
||||
if ! grep -Eq "${yaml_sops_metadata_pattern}" "${file}" \
|
||||
|| ! grep -Eq "${yaml_sops_mac_pattern}" "${file}"; then
|
||||
fail "SOPS Secret is missing encrypted metadata/MAC: ${file}"
|
||||
fi
|
||||
;;
|
||||
*.sops.json)
|
||||
if ! grep -Eq "${json_sops_metadata_pattern}" "${file}" \
|
||||
|| ! grep -Eq "${json_sops_mac_pattern}" "${file}"; then
|
||||
fail "SOPS Secret is missing encrypted metadata/MAC: ${file}"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
fail "plain Kubernetes Secret is not allowed; use an external reference or a *.sops.yaml file: ${file}"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if ((errors == stage_errors)); then
|
||||
pass "plain Kubernetes Secret manifests"
|
||||
fi
|
||||
|
||||
stage_errors="${errors}"
|
||||
for file in "${source_files[@]}"; do
|
||||
case "${file}" in
|
||||
*/_template/*)
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
|
||||
if grep -Eq '__REPLACE_ME_[A-Z0-9_]+__' "${file}"; then
|
||||
fail "unresolved replacement token: ${file}"
|
||||
fi
|
||||
done
|
||||
|
||||
if ((errors == stage_errors)); then
|
||||
pass "replacement tokens outside _template"
|
||||
fi
|
||||
|
||||
stage_errors="${errors}"
|
||||
while IFS= read -r script; do
|
||||
if ! bash -n "${script}"; then
|
||||
fail "shell syntax: ${script}"
|
||||
fi
|
||||
done < <(find scripts -type f -name '*.sh' | sort)
|
||||
|
||||
if ((errors == stage_errors)); then
|
||||
pass "shell syntax"
|
||||
fi
|
||||
|
||||
kustomizations=()
|
||||
for file in "${source_files[@]}"; do
|
||||
case "${file}" in
|
||||
bootstrap/*/kustomization.yaml | bootstrap/*/kustomization.yml | \
|
||||
gitops/*/kustomization.yaml | gitops/*/kustomization.yml)
|
||||
kustomizations+=("${file}")
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if ((${#kustomizations[@]} > 0)); then
|
||||
stage_errors="${errors}"
|
||||
if ! command -v kubectl >/dev/null 2>&1; then
|
||||
fail "kubectl is required to render Kustomize roots"
|
||||
else
|
||||
for file in "${kustomizations[@]}"; do
|
||||
rendered=""
|
||||
if ! rendered="$(kubectl kustomize "$(dirname "${file}")")"; then
|
||||
fail "Kustomize render: ${file}"
|
||||
continue
|
||||
fi
|
||||
|
||||
case "${file}" in
|
||||
gitops/clusters/_template/*)
|
||||
;;
|
||||
gitops/clusters/*)
|
||||
if [[ -z "${rendered}" ]]; then
|
||||
fail "actual cluster root renders no resources: ${file}"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done
|
||||
fi
|
||||
|
||||
if ((errors == stage_errors)); then
|
||||
pass "Kustomize render (${#kustomizations[@]} roots)"
|
||||
fi
|
||||
fi
|
||||
|
||||
charts=()
|
||||
for file in "${source_files[@]}"; do
|
||||
if [[ "${file##*/}" == "Chart.yaml" ]]; then
|
||||
charts+=("${file}")
|
||||
fi
|
||||
done
|
||||
|
||||
if ((${#charts[@]} > 0)); then
|
||||
stage_errors="${errors}"
|
||||
if ! command -v helm >/dev/null 2>&1; then
|
||||
fail "Helm is required because Chart.yaml files exist"
|
||||
else
|
||||
for chart in "${charts[@]}"; do
|
||||
if ! helm lint "$(dirname "${chart}")"; then
|
||||
fail "Helm lint: ${chart}"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if ((errors == stage_errors)); then
|
||||
pass "Helm lint (${#charts[@]} charts)"
|
||||
fi
|
||||
fi
|
||||
|
||||
iac_files=()
|
||||
iac_directories=()
|
||||
has_tofu_syntax=0
|
||||
for file in "${source_files[@]}"; do
|
||||
case "${file}" in
|
||||
bootstrap/*.tofu | bootstrap/*.tofu.json | infrastructure/*.tofu | infrastructure/*.tofu.json)
|
||||
iac_files+=("${file}")
|
||||
has_tofu_syntax=1
|
||||
;;
|
||||
bootstrap/*.tf | bootstrap/*.tf.json | infrastructure/*.tf | infrastructure/*.tf.json)
|
||||
iac_files+=("${file}")
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
for file in "${iac_files[@]}"; do
|
||||
directory="${file%/*}"
|
||||
directory_seen=0
|
||||
for existing_directory in "${iac_directories[@]}"; do
|
||||
if [[ "${directory}" == "${existing_directory}" ]]; then
|
||||
directory_seen=1
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if ((directory_seen == 0)); then
|
||||
iac_directories+=("${directory}")
|
||||
fi
|
||||
done
|
||||
|
||||
if ((${#iac_files[@]} > 0)); then
|
||||
stage_errors="${errors}"
|
||||
iac_engine=""
|
||||
|
||||
if [[ ! -f infrastructure/.iac-engine || -L infrastructure/.iac-engine ]]; then
|
||||
fail "select terraform or tofu in the tracked infrastructure/.iac-engine file"
|
||||
else
|
||||
iac_engine_values=()
|
||||
while IFS= read -r line; do
|
||||
case "${line}" in
|
||||
"" | \#*)
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
iac_engine_values+=("${line}")
|
||||
done <infrastructure/.iac-engine
|
||||
|
||||
if ((${#iac_engine_values[@]} != 1)); then
|
||||
fail "infrastructure/.iac-engine must contain exactly one uncommented value"
|
||||
else
|
||||
iac_engine="${iac_engine_values[0]}"
|
||||
case "${iac_engine}" in
|
||||
terraform | tofu)
|
||||
;;
|
||||
*)
|
||||
fail "infrastructure/.iac-engine must contain exactly terraform or tofu"
|
||||
iac_engine=""
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "${iac_engine}" == "terraform" && "${has_tofu_syntax}" == "1" ]]; then
|
||||
fail "Terraform cannot format .tofu/.tofu.json files; select tofu or use compatible .tf files"
|
||||
fi
|
||||
|
||||
if [[ -n "${iac_engine}" ]]; then
|
||||
if ! command -v "${iac_engine}" >/dev/null 2>&1; then
|
||||
fail "${iac_engine} is selected but is not installed"
|
||||
elif ((errors == stage_errors)); then
|
||||
for directory in "${iac_directories[@]}"; do
|
||||
if ! "${iac_engine}" fmt -check "${directory}"; then
|
||||
fail "${iac_engine} format check: ${directory}"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
||||
if ((errors == stage_errors)); then
|
||||
pass "${iac_engine} format (${#iac_files[@]} files)"
|
||||
fi
|
||||
fi
|
||||
|
||||
if ((errors > 0)); then
|
||||
printf '\nValidation failed with %d error(s).\n' "${errors}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf '\nValidation passed.\n'
|
||||
Reference in New Issue
Block a user