#!/usr/bin/env bash # test_simple_function.sh — интеграционные тесты fission_simple_function # Запуск: bash test_simple_function.sh # Требования: terraform + kubectl в PATH, kubeconfig в ~/.kube/config set -uo pipefail PASS=0; FAIL=0; SKIP=0 NAMESPACE="default" TESTDIR="/tmp/sless-sf-tests" KUBECONFIG_PATH="/home/naeel/.kube/config" PROVIDER_NS="default" # уникальный префикс — не пересекаемся с prod RUN_ID="sf$(date +%s | tail -c 6)" TF_PROVIDER=' terraform { required_providers { fission = { source = "nail/fission" } } } provider "fission" { kubeconfig_path = "/home/naeel/.kube/config" namespace = "default" } ' green() { echo -e "\033[32m$*\033[0m"; } red() { echo -e "\033[31m$*\033[0m"; } yellow(){ echo -e "\033[33m$*\033[0m"; } pass() { PASS=$((PASS+1)); green " PASS: $1"; } fail() { FAIL=$((FAIL+1)); red " FAIL: $1"; } skip() { SKIP=$((SKIP+1)); yellow " SKIP: $1"; } # ── helpers ────────────────────────────────────────────────────────────────── kube_exists() { local kind=$1 name=$2 kubectl get "${kind}" -n "${NAMESPACE}" "${name}" &>/dev/null } kube_gone() { local kind=$1 name=$2 ! kubectl get "${kind}" -n "${NAMESPACE}" "${name}" &>/dev/null } tf_apply() { local dir=$1 terraform -chdir="${dir}" apply -auto-approve -no-color 2>&1 } tf_destroy() { local dir=$1 terraform -chdir="${dir}" destroy -auto-approve -no-color 2>&1 } tf_plan_exitcode() { # 0=no-changes, 1=error, 2=changes local dir=$1 terraform -chdir="${dir}" plan -detailed-exitcode -no-color 2>&1 echo $? } wait_package_ready() { local name=$1 retries=30 for ((i=0; i/dev/null || echo "") # для literal packages buildstatus пустой — это нормально [[ "$status" == "succeeded" || "$status" == "" ]] && return 0 sleep 2 done return 1 } # ── setup / teardown ───────────────────────────────────────────────────────── } EOF setup_workdir() { local name=$1 local dir="${TESTDIR}/${name}" rm -rf "${dir}" mkdir -p "${dir}" echo "${dir}" } write_provider() { local dir=$1 cat > "${dir}/main.tf" < "${dir}/main.js" <<'EOF' exports.handler = async (ctx) => ({ body: JSON.stringify({ msg: "hello-v1", ts: Date.now() }), headers: { "Content-Type": "application/json" }, }); EOF } write_js_v2() { local dir=$1 mkdir -p "${dir}" cat > "${dir}/main.js" <<'EOF' exports.handler = async (ctx) => ({ body: JSON.stringify({ msg: "hello-v2-updated", ts: Date.now() }), headers: { "Content-Type": "application/json" }, }); EOF } write_python() { local dir=$1 mkdir -p "${dir}" cat > "${dir}/main.py" <<'EOF' def main(): return {"status": 200, "body": '{"msg":"hello-python"}'} EOF } write_ruby() { local dir=$1 mkdir -p "${dir}" cat > "${dir}/main.rb" <<'EOF' def main(ctx) { body: "hello-ruby", status_code: 200 } end EOF } write_php() { local dir=$1 mkdir -p "${dir}" cat > "${dir}/main.php" <<'EOF' "hello-php"]; } EOF } write_perl() { local dir=$1 mkdir -p "${dir}" cat > "${dir}/main.pl" <<'EOF' sub main { my ($context) = @_; return { body => "hello-perl" }; } 1; EOF } <<<<<<< HEAD ======= EOF cat > "${dir}/go.mod" <<'EOF' module fn EOF } >>>>>>> d21e1ac (feat: restore cron page, email in navbar, metrics pipeline v1.3.19) # ───────────────────────────────────────────────────────────────────────────── echo "" echo "================================================================" echo " fission_simple_function — интеграционные тесты" echo " Run ID: ${RUN_ID}" echo "================================================================" mkdir -p "${TESTDIR}" # ════════════════════════════════════════════════════════════════════════ echo "" echo ">>> T01: nodejs — create с HTTPTrigger, verify k8s, destroy" # ════════════════════════════════════════════════════════════════════════ T01="${RUN_ID}-node-basic" D=$(setup_workdir "t01") write_provider "${D}" write_js_v1 "${D}/code" cat >> "${D}/main.tf" <>> T02: python — create БЕЗ url (нет HTTPTrigger)" # ════════════════════════════════════════════════════════════════════════ T02="${RUN_ID}-py-notrigger" D=$(setup_workdir "t02") write_provider "${D}" write_python "${D}/code" cat >> "${D}/main.tf" <>> T04: ruby — create с URL" # ════════════════════════════════════════════════════════════════════════ T04="${RUN_ID}-ruby" D=$(setup_workdir "t04") write_provider "${D}" write_ruby "${D}/code" cat >> "${D}/main.tf" <>> T05: php — create с URL" # ════════════════════════════════════════════════════════════════════════ T05="${RUN_ID}-php" D=$(setup_workdir "t05") write_provider "${D}" write_php "${D}/code" cat >> "${D}/main.tf" <>> T06: perl — create с URL" # ════════════════════════════════════════════════════════════════════════ T06="${RUN_ID}-perl" D=$(setup_workdir "t06") write_provider "${D}" write_perl "${D}/code" cat >> "${D}/main.tf" <>> T07: code_hash drift detection — update пересчитывает хеш и обновляет Package" # ════════════════════════════════════════════════════════════════════════ T07="${RUN_ID}-drift" D=$(setup_workdir "t07") write_provider "${D}" write_js_v1 "${D}/code" cat >> "${D}/main.tf" </dev/null \ | python3 -c "import sys,json; r=json.load(sys.stdin)['values']['root_module']['resources']; print(r[0]['values']['code_hash'])" 2>/dev/null || echo "?") # Обновляем код (v2) write_js_v2 "${D}/code" OUT=$(tf_apply "${D}") echo "${OUT}" | grep -q "Apply complete" && pass "T07: apply v2 OK" || fail "T07: apply v2 FAILED" HASH2=$(terraform -chdir="${D}" show -json 2>/dev/null \ | python3 -c "import sys,json; r=json.load(sys.stdin)['values']['root_module']['resources']; print(r[0]['values']['code_hash'])" 2>/dev/null || echo "?") [ "${HASH1}" != "${HASH2}" ] && [ "${HASH1}" != "?" ] && [ "${HASH2}" != "?" ] \ && pass "T07: code_hash изменился (${HASH1:0:8}… → ${HASH2:0:8}…)" \ || fail "T07: code_hash не изменился или не читается (h1=${HASH1:0:8}, h2=${HASH2:0:8})" echo "${OUT}" | grep -qE "fission_simple_function.fn.*updated|updated.*fission_simple_function.fn" \ && pass "T07: Package был обновлён (update in apply output)" \ || { echo "${OUT}" | grep -q "1 updated" && pass "T07: 1 ресурс обновлён" || fail "T07: Обновление не обнаружено в output"; } tf_destroy "${D}" | grep -q "Destroy complete" && pass "T07: destroy OK" || fail "T07: destroy FAILED" # ════════════════════════════════════════════════════════════════════════ echo "" echo ">>> T08: добавление url (update — создание Trigger)" # ════════════════════════════════════════════════════════════════════════ T08="${RUN_ID}-addurl" D=$(setup_workdir "t08") write_provider "${D}" write_js_v1 "${D}/code" # Apply без url cat > "${D}/main.tf" < "${D}/main.tf" <>> T09: удаление url (update — удаление Trigger)" # ════════════════════════════════════════════════════════════════════════ T09="${RUN_ID}-rmurl" D=$(setup_workdir "t09") write_provider "${D}" write_js_v1 "${D}/code" # Apply с url cat > "${D}/main.tf" < "${D}/main.tf" <>> T10: изменение url (update Trigger)" # ════════════════════════════════════════════════════════════════════════ T10="${RUN_ID}-churl" D=$(setup_workdir "t10") write_provider "${D}" write_js_v1 "${D}/code" cat > "${D}/main.tf" </dev/null || echo "?") cat > "${D}/main.tf" </dev/null || echo "?") [ "${URL_BEFORE}" = "/test/${T10}/v1" ] && [ "${URL_AFTER}" = "/test/${T10}/v2" ] \ && pass "T10: URL обновлён (/v1 → /v2)" \ || fail "T10: URL не обновлён (before='${URL_BEFORE}', after='${URL_AFTER}')" tf_destroy "${D}" | grep -q "Destroy complete" && pass "T10: destroy OK" || fail "T10: destroy FAILED" # ════════════════════════════════════════════════════════════════════════ echo "" echo ">>> T11: plan без изменений → 0 changes" # ════════════════════════════════════════════════════════════════════════ T11="${RUN_ID}-nochange" D=$(setup_workdir "t11") write_provider "${D}" write_js_v1 "${D}/code" cat >> "${D}/main.tf" <&1) echo "${PLAN_OUT}" | grep -qE "No changes|0 to add, 0 to change, 0 to destroy" \ && pass "T11: повторный plan показывает 0 changes" \ || fail "T11: повторный plan показывает изменения (не idempotent)" tf_destroy "${D}" | grep -q "Destroy complete" && pass "T11: destroy OK" || fail "T11: destroy FAILED" # ════════════════════════════════════════════════════════════════════════ echo "" echo ">>> T12: auto-env idempotency — два ресурса делят один environment" # ════════════════════════════════════════════════════════════════════════ T12A="${RUN_ID}-shared-a" T12B="${RUN_ID}-shared-b" D=$(setup_workdir "t12") write_provider "${D}" write_js_v1 "${D}/code-a" write_js_v1 "${D}/code-b" cat >> "${D}/main.tf" </dev/null \ | grep -c "simple-nodejs-env" || echo "0") [ "${ENV_COUNT}" -eq 1 ] \ && pass "T12: ровно 1 экземпляр simple-nodejs-env (shared)" \ || fail "T12: ENV_COUNT=${ENV_COUNT} (ожидалось 1)" tf_destroy "${D}" | grep -q "Destroy complete" && pass "T12: destroy OK" || fail "T12: destroy FAILED" kube_gone "functions.fission.io" "${T12A}" && pass "T12: fn_a удалена" || fail "T12: fn_a осталась" kube_gone "functions.fission.io" "${T12B}" && pass "T12: fn_b удалена" || fail "T12: fn_b осталась" # ════════════════════════════════════════════════════════════════════════ echo "" echo ">>> T13: невалидный runtime → apply завершается ошибкой" # ════════════════════════════════════════════════════════════════════════ T13="${RUN_ID}-badruntime" D=$(setup_workdir "t13") write_provider "${D}" write_js_v1 "${D}/code" cat >> "${D}/main.tf" <&1 || true) echo "${OUT}" | grep -qiE "Error|error|cobol|неподдерживаемый|unsupported" \ && pass "T13: apply с runtime=cobol выдал ошибку" \ || fail "T13: apply не выдал ошибку для runtime=cobol" kube_gone "functions.fission.io" "${T13}" \ && pass "T13: Function не создана" || fail "T13: Function создана для плохого runtime" # ════════════════════════════════════════════════════════════════════════ echo "" echo ">>> T14: terraform import (namespace/name)" # ════════════════════════════════════════════════════════════════════════ T14="${RUN_ID}-import" D=$(setup_workdir "t14") write_provider "${D}" write_js_v1 "${D}/code" cat >> "${D}/main.tf" </dev/null || true IMP_OUT=$(terraform -chdir="${D}" import -no-color \ fission_simple_function.fn "default/${T14}" 2>&1) echo "${IMP_OUT}" | grep -qiE "Import successful|import" \ && pass "T14: import выполнен" || fail "T14: import не прошёл: $(echo "${IMP_OUT}" | tail -5)" # Plan после import должен показать изменения (code_dir/hash не в k8s state) # — это нормально, просто убеждаемся что plan не падает с ошибкой PLAN_OUT=$(terraform -chdir="${D}" plan -no-color 2>&1) echo $? | grep -qE "^[012]$" && pass "T14: plan после import не падает" || fail "T14: plan после import упал" tf_destroy "${D}" | grep -q "Destroy complete" && pass "T14: destroy OK" || fail "T14: destroy FAILED" # ════════════════════════════════════════════════════════════════════════ echo "" echo ">>> T15: nodejs — методы по умолчанию [GET]" # ════════════════════════════════════════════════════════════════════════ T15="${RUN_ID}-defmethods" D=$(setup_workdir "t15") write_provider "${D}" write_js_v1 "${D}/code" cat >> "${D}/main.tf" </dev/null || echo "?") echo "${METHODS}" | grep -q "GET" \ && pass "T15: default method=GET в k8s: '${METHODS}'" \ || fail "T15: методы не GET: '${METHODS}'" tf_destroy "${D}" | grep -q "Destroy complete" && pass "T15: destroy OK" || fail "T15: destroy FAILED" # ════════════════════════════════════════════════════════════════════════ echo "" echo ">>> T16: Package.spec для nodejs — deployment (не source)" # ════════════════════════════════════════════════════════════════════════ T16="${RUN_ID}-nodepkg" D=$(setup_workdir "t16") write_provider "${D}" write_js_v1 "${D}/code" cat >> "${D}/main.tf" </dev/null || echo "") DEP=$(kubectl get packages.fission.io -n "${NAMESPACE}" "${T16}-pkg" \ -o jsonpath='{.spec.deployment.type}' 2>/dev/null || echo "") [ -z "${SRC}" ] && [ "${DEP}" = "literal" ] \ && pass "T16: nodejs Package использует deployment.literal (не source)" \ || fail "T16: pkg spec — source.type='${SRC}', deployment.type='${DEP}'" tf_destroy "${D}" | grep -q "Destroy complete" && pass "T16: destroy OK" || fail "T16: destroy FAILED" # ════════════════════════════════════════════════════════════════════════ echo "" echo ">>> T17: namespace вычисляется из провайдера" # ════════════════════════════════════════════════════════════════════════ T17="${RUN_ID}-ns" D=$(setup_workdir "t17") write_js_v1 "${D}/code" # Провайдер без явного namespace в ресурсе cat > "${D}/main.tf" </dev/null \ | python3 -c "import sys,json; r=json.load(sys.stdin)['values']['root_module']['resources']; print(r[0]['values']['namespace'])" 2>/dev/null || echo "?") [ "${NS}" = "default" ] \ && pass "T17: namespace=default из провайдера (state: ${NS})" \ || fail "T17: namespace='${NS}' (ожидалось 'default')" tf_destroy "${D}" | grep -q "Destroy complete" && pass "T17: destroy OK" || fail "T17: destroy FAILED" # ════════════════════════════════════════════════════════════════════════ echo "" echo ">>> T18: computed поля environment_name/package_name/trigger_name в state" # ════════════════════════════════════════════════════════════════════════ T18="${RUN_ID}-computed" D=$(setup_workdir "t18") write_provider "${D}" write_js_v1 "${D}/code" cat >> "${D}/main.tf" </dev/null \ | python3 -c " import sys, json r = json.load(sys.stdin)['values']['root_module']['resources'][0]['values'] print(r.get('environment_name','?')) print(r.get('package_name','?')) print(r.get('trigger_name','?')) print(r.get('code_hash','?')) " 2>/dev/null || echo "? ? ? ?") ENV_NAME=$(echo "${SHOW}" | sed -n '1p') PKG_NAME=$(echo "${SHOW}" | sed -n '2p') TRG_NAME=$(echo "${SHOW}" | sed -n '3p') HASH_VAL=$(echo "${SHOW}" | sed -n '4p') [ "${ENV_NAME}" = "simple-nodejs-env" ] && pass "T18: environment_name='${ENV_NAME}'" || fail "T18: environment_name='${ENV_NAME}'" [ "${PKG_NAME}" = "${T18}-pkg" ] && pass "T18: package_name='${PKG_NAME}'" || fail "T18: package_name='${PKG_NAME}'" [ "${TRG_NAME}" = "${T18}-trigger" ] && pass "T18: trigger_name='${TRG_NAME}'" || fail "T18: trigger_name='${TRG_NAME}'" [[ "${HASH_VAL}" =~ ^[0-9a-f]{64}$ ]] && pass "T18: code_hash выглядит как sha256" || fail "T18: code_hash='${HASH_VAL}'" tf_destroy "${D}" | grep -q "Destroy complete" && pass "T18: destroy OK" || fail "T18: destroy FAILED" # ════════════════════════════════════════════════════════════════════════ echo "" echo ">>> T19: повторный apply одного ресурса — idempotency (no drift)" # ════════════════════════════════════════════════════════════════════════ T19="${RUN_ID}-idem" D=$(setup_workdir "t19") write_provider "${D}" write_js_v1 "${D}/code" cat >> "${D}/main.tf" <>> T20: destroy без trigger (url не задан) — нет паники" # ════════════════════════════════════════════════════════════════════════ T20="${RUN_ID}-notrig-del" D=$(setup_workdir "t20") write_provider "${D}" write_python "${D}/code" cat >> "${D}/main.tf" <