tidied
This commit is contained in:
@@ -77,9 +77,9 @@
|
|||||||
|
|
||||||
pigeon = pkgs.writeShellApplication {
|
pigeon = pkgs.writeShellApplication {
|
||||||
name = "pigeon";
|
name = "pigeon";
|
||||||
runtimeInputs = with pkgs; [ coreutils curl e2fsprogs firecracker gcc git gnumake ];
|
runtimeInputs = with pkgs; [ coreutils curl diffutils e2fsprogs firecracker git jq sqlite ];
|
||||||
text = builtins.replaceStrings
|
text = builtins.replaceStrings
|
||||||
[ "@kernel@" "@rootfs@" ] [ "${kernel}" "${rootfs}"]
|
[ "@kernel@" "@rootfs@" "@toolchain@" ] [ "${kernel}" "${rootfs}" "${toolchain}" ]
|
||||||
(builtins.readFile ./pigeon.sh);
|
(builtins.readFile ./pigeon.sh);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -92,8 +92,7 @@
|
|||||||
pass-arguments-to-command = [
|
pass-arguments-to-command = [
|
||||||
{ source = "string"; name = "${pigeon}/bin/pigeon"; }
|
{ source = "string"; name = "${pigeon}/bin/pigeon"; }
|
||||||
{ source = "payload"; name = "project.id"; }
|
{ source = "payload"; name = "project.id"; }
|
||||||
{ source = "payload"; name = "project.namespace"; }
|
{ source = "payload"; name = "project.path_with_namespace"; }
|
||||||
{ source = "payload"; name = "project.name"; }
|
|
||||||
{ source = "payload"; name = "commit.id"; }
|
{ source = "payload"; name = "commit.id"; }
|
||||||
{ source = "payload"; name = "tag"; }
|
{ source = "payload"; name = "tag"; }
|
||||||
{ source = "payload"; name = "created_at"; }
|
{ source = "payload"; name = "created_at"; }
|
||||||
@@ -117,7 +116,7 @@
|
|||||||
match = {
|
match = {
|
||||||
type = "value";
|
type = "value";
|
||||||
parameter = { source = "header"; name = "X-Gitlab-Token"; };
|
parameter = { source = "header"; name = "X-Gitlab-Token"; };
|
||||||
value = "{{ getenv `PIGEON_GITLAB_SECRET` | js }}";
|
value = "{{ getenv `SECRET` | js }}";
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
@@ -127,13 +126,13 @@
|
|||||||
name = "pigeon-webhook";
|
name = "pigeon-webhook";
|
||||||
runtimeInputs = with pkgs; [ coreutils jq nq sqlite webhook ];
|
runtimeInputs = with pkgs; [ coreutils jq nq sqlite webhook ];
|
||||||
text = ''
|
text = ''
|
||||||
: "''${PIGEON_GITLAB_SECRET:?}"
|
: "''${SECRET:?}"
|
||||||
: "''${PIGEON_GITLAB_TOKEN:?}"
|
: "''${TOKEN:?}"
|
||||||
: "''${PIGEON_PROJECTS:?}"
|
: "''${GRADER:?}"
|
||||||
: "''${PIGEON_STATE}"
|
: "''${STATE:?}"
|
||||||
|
|
||||||
mkdir -p "$PIGEON_STATE"
|
mkdir -p "$STATE"
|
||||||
export NQDIR="$PIGEON_STATE/nqdir"
|
export NQDIR="$STATE/nqdir"
|
||||||
exec webhook -hooks ${hooks} -template -port 3000 -verbose -debug
|
exec webhook -hooks ${hooks} -template -port 3000 -verbose -debug
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,22 +13,27 @@ mount -t ext4 /dev/vdd /root/results
|
|||||||
cp -a /root/student/. /tmp/
|
cp -a /root/student/. /tmp/
|
||||||
cp -a --remove-destination /root/project/build/. /tmp/
|
cp -a --remove-destination /root/project/build/. /tmp/
|
||||||
chmod -R 777 /tmp
|
chmod -R 777 /tmp
|
||||||
cd /tmp
|
cd /tmp || exit 1
|
||||||
|
|
||||||
(
|
sandbox() {
|
||||||
ulimit -t 30 -u 256 -n 256 -f 2048
|
LIMIT="$1" ; shift
|
||||||
timeout -s 9 30 \
|
(
|
||||||
|
ulimit -t "$LIMIT" -u 256 -n 256 -f 2048
|
||||||
setpriv --reuid=1 --regid=1 --clear-groups --no-new-privs --inh-caps=-all \
|
setpriv --reuid=1 --regid=1 --clear-groups --no-new-privs --inh-caps=-all \
|
||||||
make >/root/results/build.log 2>&1
|
timeout -s 9 "$LIMIT" "$@"
|
||||||
echo $? >/root/results/build.status
|
)
|
||||||
)
|
}
|
||||||
|
|
||||||
|
sandbox 30 make >/root/results/build.log 2>&1
|
||||||
|
echo $? >/root/results/build.status
|
||||||
|
|
||||||
for TEST in /root/project/tests/* ; do
|
for TEST in /root/project/tests/* ; do
|
||||||
(
|
[ -f "$TEST" ] || continue
|
||||||
ulimit -t 2 -u 256 -n 256 -f 2048
|
NAME="$(basename "$TEST")"
|
||||||
timeout -s 9 2 \
|
|
||||||
setpriv --reuid=1 --regid=1 --clear-groups --no-new-privs --inh-caps=-all \
|
ARGS="/root/project/args/$NAME"
|
||||||
"$TEST" >"/root/results/$(basename "$TEST")" 2>/dev/null
|
[ -f "$ARGS" ] || ARGS=/dev/null
|
||||||
echo $? >"/root/results/$(basename "$TEST").status"
|
|
||||||
)
|
sandbox 2 "$TEST" <"$ARGS" >"/root/results/$NAME" 2>/dev/null
|
||||||
|
echo $? >"/root/results/$NAME.status"
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -1,67 +1,38 @@
|
|||||||
PROJECT_ID="$1"
|
PROJECT="${2##*/}"
|
||||||
PROJECT_NAMESPACE="$2"
|
STUDENT="${2%/*}"
|
||||||
PROJECT_NAME="$3"
|
GRADER="$(realpath -m "${GRADER:?}")/$PROJECT"
|
||||||
RELEASE_COMMIT="$4"
|
DATABASE="$(realpath -m "${STATE:?}")/db"
|
||||||
RELEASE_TAG="$5"
|
|
||||||
RELEASE_TIME="$6"
|
|
||||||
|
|
||||||
: "${PIGEON_GITLAB_TOKEN:?}"
|
|
||||||
: "${PIGEON_PROJECTS:?}"
|
|
||||||
: "${PIGEON_STATE:?}"
|
|
||||||
|
|
||||||
RATE_LIMIT=3
|
|
||||||
|
|
||||||
PIGEON_DATABASE="$(realpath -m "$PIGEON_STATE/db")"
|
|
||||||
PIGEON_PROJECTS="$(realpath -m "$PIGEON_PROJECTS")"
|
|
||||||
RELEASE_TIME="$(date -d "$RELEASE_TIME" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || true)"
|
|
||||||
|
|
||||||
db() {
|
|
||||||
sqlite3 -bail -cmd '.timeout 10000' "$PIGEON_DATABASE"
|
|
||||||
}
|
|
||||||
q() {
|
|
||||||
printf "'%s'" "$(printf %s "$1" | sed "s/'/''/g")"
|
|
||||||
}
|
|
||||||
|
|
||||||
db << 'EOF'
|
|
||||||
CREATE TABLE IF NOT EXISTS results (
|
|
||||||
id INTEGER PRIMARY KEY,
|
|
||||||
student TEXT NOT NULL,
|
|
||||||
project TEXT NOT NULL,
|
|
||||||
hash TEXT NOT NULL,
|
|
||||||
time TEXT NOT NULL DEFAULT (datetime('now')),
|
|
||||||
score INTEGER CHECK (score >= 0),
|
|
||||||
token INTEGER NOT NULL
|
|
||||||
);
|
|
||||||
EOF
|
|
||||||
|
|
||||||
DIRECTORY="$(mktemp -d /tmp/pigeon.XXXXXXXXXX)"
|
DIRECTORY="$(mktemp -d /tmp/pigeon.XXXXXXXXXX)"
|
||||||
trap 'rm -rf "$DIRECTORY"' EXIT
|
trap 'rm -rf "$DIRECTORY"' EXIT
|
||||||
cd "$DIRECTORY"
|
cd "$DIRECTORY"
|
||||||
|
|
||||||
git -c advice.detachedHead=false clone -q --rev "$RELEASE_COMMIT" --depth 1 \
|
git -c advice.detachedHead=false clone -q --rev "$3" --depth 1 \
|
||||||
"https://oauth2:$PIGEON_GITLAB_TOKEN@gitlab.com/$PROJECT_NAMESPACE/$PROJECT_NAME.git" student/
|
"https://oauth2:${TOKEN:?}@gitlab.com/$2.git" student/
|
||||||
rm -rf student/.git
|
rm -rf student/.git
|
||||||
|
|
||||||
TESTS="$PIGEON_PROJECTS/$PROJECT_NAME/tests"
|
mkdir -p project/tests project/build project/args
|
||||||
mkdir -p project/tests project/build
|
for DIR in "$GRADER"/tests/*/ ; do
|
||||||
for DIR in "$TESTS"/*/ ; do
|
NAME="$(basename "$DIR")"
|
||||||
cp -a "$DIR/input.sh" "project/tests/$(basename "$DIR")"
|
cp -a "$DIR/input.sh" "project/tests/$NAME"
|
||||||
|
[ -f "$DIR/output.txt" ] ||
|
||||||
|
head -c 4096 /dev/urandom | LC_ALL=C tr -dc ' -~' >"project/args/$NAME"
|
||||||
done
|
done
|
||||||
cp -a "$PIGEON_PROJECTS/$PROJECT_NAME"/build/. project/build/
|
cp -a "$GRADER"/build/. project/build/
|
||||||
|
|
||||||
CANONICAL=""
|
|
||||||
if [ -d "$PIGEON_PROJECTS/$PROJECT_NAME/canonical" ] ; then
|
|
||||||
mkdir -p canonical
|
|
||||||
cp -a "$PIGEON_PROJECTS/$PROJECT_NAME"/build/. canonical/
|
|
||||||
cp -a --remove-destination "$PIGEON_PROJECTS/$PROJECT_NAME"/canonical/. canonical/
|
|
||||||
( cd canonical && timeout 30 make >/dev/null 2>&1 ) || true
|
|
||||||
[ -x canonical/program ] && CANONICAL="$(realpath canonical/program)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
mkfs.ext4 -q -F -d student student.img 16M
|
mkfs.ext4 -q -F -d student student.img 16M
|
||||||
mkfs.ext4 -q -F -d project project.img 16M
|
mkfs.ext4 -q -F -d project project.img 16M
|
||||||
mkfs.ext4 -q -F results.img 16M
|
mkfs.ext4 -q -F results.img 16M
|
||||||
|
|
||||||
|
if [ -d "$GRADER/canonical" ] ; then
|
||||||
|
mkdir -p canonical
|
||||||
|
cp -a "$GRADER"/canonical/. canonical/
|
||||||
|
cp -a --remove-destination "$GRADER"/build/. canonical/
|
||||||
|
(
|
||||||
|
export PATH=@toolchain@/bin
|
||||||
|
cd canonical && timeout 30 make
|
||||||
|
) >/dev/null 2>&1 || exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
cat >vm.json <<'EOF'
|
cat >vm.json <<'EOF'
|
||||||
{
|
{
|
||||||
"boot-source": {
|
"boot-source": {
|
||||||
@@ -78,76 +49,90 @@ cat >vm.json <<'EOF'
|
|||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
firecracker --no-api --level Error --config-file vm.json
|
timeout 60 firecracker --no-api --level Error --config-file vm.json
|
||||||
|
|
||||||
SCORE=0
|
SCORE=0
|
||||||
PUBLIC_PASS=1
|
PUBLIC=1
|
||||||
: >public
|
for DIR in "$GRADER"/tests/*/ ; do
|
||||||
: >release
|
|
||||||
|
|
||||||
for DIR in "$TESTS"/*/ ; do
|
|
||||||
NAME="$(basename "$DIR")"
|
NAME="$(basename "$DIR")"
|
||||||
|
|
||||||
debugfs -R "cat /$NAME" results.img >scratch 2>/dev/null
|
debugfs -R "cat /$NAME" results.img >scratch 2>/dev/null
|
||||||
EXIT="$(debugfs -R "cat /$NAME.status" results.img 2>/dev/null)"
|
EXIT="$(debugfs -R "cat /$NAME.status" results.img 2>/dev/null)"
|
||||||
EXPECTED_EXIT="$(cat "$DIR/status.txt" 2>/dev/null)"
|
EXPECTED="$(cat "$DIR/status.txt" 2>/dev/null || true)"
|
||||||
case "$EXIT" in '' | *[!0-9]*) EXIT=0 ;; esac
|
case "$EXPECTED" in '' | *[!0-9]*) EXPECTED=0 ;; esac
|
||||||
case "$EXPECTED_EXIT" in '' | *[!0-9]*) EXPECTED_EXIT=0 ;; esac
|
|
||||||
|
ARGS="project/args/$NAME"
|
||||||
|
[ -f "$ARGS" ] || ARGS=/dev/null
|
||||||
|
|
||||||
if [ -f "$DIR/output.txt" ] ; then
|
if [ -f "$DIR/output.txt" ] ; then
|
||||||
cmp -s scratch "$DIR/output.txt" && [ "$EXIT" -eq "$EXPECTED_EXIT" ] && PASS=1 || PASS=0
|
cat "$DIR/output.txt"
|
||||||
elif [ -n "$CANONICAL" ] ; then
|
elif [ -d canonical ] ; then
|
||||||
PASS=1
|
(
|
||||||
while IFS="$(printf '\t')" read -r ARG OUT ; do
|
export PATH=@toolchain@/bin
|
||||||
[ -n "$ARG" ] || continue
|
cd canonical && timeout 2 sh "$DIRECTORY/project/tests/$NAME"
|
||||||
[ "$OUT" = "$("$CANONICAL" "$ARG" 2>/dev/null)" ] || PASS=0
|
) <"$ARGS" 2>/dev/null || true
|
||||||
done <scratch
|
fi >expected
|
||||||
else
|
|
||||||
PASS=0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$PASS" = 1 ] ; then
|
if cmp -s scratch expected && [ "$EXIT" -eq "$EXPECTED" ] ; then
|
||||||
ANSWER=YES
|
RESULT=YES
|
||||||
SCORE=$((SCORE + 1))
|
SCORE=$((SCORE + 1))
|
||||||
elif [ "$EXIT" -eq 124 ] || [ "$EXIT" -eq 137 ] ; then
|
elif [ "$EXIT" -eq 124 ] || [ "$EXIT" -eq 137 ] ; then
|
||||||
ANSWER="NO (timed out)"
|
RESULT="NO (timed out)"
|
||||||
elif [ "$EXIT" -eq "$EXPECTED_EXIT" ] ; then
|
|
||||||
ANSWER="NO (wrong output)"
|
|
||||||
elif [ "$EXIT" -gt 128 ] ; then
|
elif [ "$EXIT" -gt 128 ] ; then
|
||||||
ANSWER="NO (crashed, signal $((EXIT - 128)))"
|
RESULT="NO (crashed with signal $((EXIT - 128)))"
|
||||||
|
elif [ "$EXIT" -ne "$EXPECTED" ] ; then
|
||||||
|
RESULT="NO (exited with status $EXIT, expected $EXPECTED)"
|
||||||
else
|
else
|
||||||
ANSWER="NO (exited with status $EXIT, expected $EXPECTED_EXIT)"
|
RESULT="NO (wrong output)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
case "$NAME" in
|
case "$NAME" in
|
||||||
p*) [ "$PASS" = 1 ] || PUBLIC_PASS=0; echo "$(cat "$DIR/test.txt") $ANSWER " >>public ;;
|
p*)
|
||||||
r*) echo "$(cat "$DIR/test.txt") $ANSWER " >>release ;;
|
if [ "$RESULT" != YES ] ; then PUBLIC=0 ; fi
|
||||||
|
echo "$(cat "$DIR/test.txt") $RESULT " >>public ;;
|
||||||
|
r*)
|
||||||
|
echo "$(cat "$DIR/test.txt") $RESULT " >>release ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
TOKEN="$(db <<EOF
|
q() {
|
||||||
|
printf "'%s'" "${1//\'/\'\'}"
|
||||||
|
}
|
||||||
|
|
||||||
|
MESSAGE="$(cat public || true)"
|
||||||
|
[ "$(sqlite3 -bail -cmd '.timeout 10000' "$DATABASE" <<EOF
|
||||||
|
CREATE TABLE IF NOT EXISTS results (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
student TEXT NOT NULL,
|
||||||
|
project TEXT NOT NULL,
|
||||||
|
hash TEXT NOT NULL,
|
||||||
|
time TEXT NOT NULL,
|
||||||
|
score INTEGER NOT NULL,
|
||||||
|
token BOOLEAN NOT NULL
|
||||||
|
);
|
||||||
INSERT INTO results (student, project, hash, time, score, token)
|
INSERT INTO results (student, project, hash, time, score, token)
|
||||||
SELECT $(q "$PROJECT_NAMESPACE"), $(q "$PROJECT_NAME"), $(q "$RELEASE_COMMIT"),
|
SELECT
|
||||||
COALESCE(NULLIF($(q "$RELEASE_TIME"), ''), datetime('now')),
|
$(q "$STUDENT"),
|
||||||
|
$(q "$PROJECT"),
|
||||||
|
$(q "$3"),
|
||||||
|
$(q "$(date -u -d "$5" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || date -u '+%Y-%m-%d %H:%M:%S')"),
|
||||||
$SCORE,
|
$SCORE,
|
||||||
CASE WHEN $PUBLIC_PASS = 1 AND (
|
($PUBLIC = 1) AND (
|
||||||
SELECT COUNT(*) FROM results
|
SELECT COUNT(*) FROM results
|
||||||
WHERE student = $(q "$PROJECT_NAMESPACE") AND project = $(q "$PROJECT_NAME")
|
WHERE student = $(q "$STUDENT") AND project = $(q "$PROJECT")
|
||||||
AND token = 1 AND time > datetime('now', '-1 day')
|
AND token AND time > datetime('now', '-1 day')
|
||||||
) < $RATE_LIMIT THEN 1 ELSE 0 END
|
) < 3
|
||||||
RETURNING token;
|
RETURNING token;
|
||||||
EOF
|
EOF
|
||||||
)"
|
)" = 1 ] && MESSAGE="$(cat public release || true)"
|
||||||
|
|
||||||
if [ "$TOKEN" = 1 ] ; then
|
if [ "$(debugfs -R "cat /build.status" results.img 2>/dev/null)" -ne 0 ] ; then
|
||||||
MESSAGE="$(cat public release)"
|
debugfs -R "cat /build.log" results.img >build.log 2>/dev/null
|
||||||
else
|
MESSAGE="$(printf '````\n%s\n````' "$(head -c 8192 build.log)")"
|
||||||
MESSAGE="$(cat public)"
|
|
||||||
[ "$PUBLIC_PASS" = 1 ] && MESSAGE="${MESSAGE}"$'\n'"All public tests passed, but you have used all $RATE_LIMIT submissions for today. Release test results are withheld until tomorrow."
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
curl -s --show-error --out-null --request PUT \
|
curl -s --show-error --out-null --request PUT \
|
||||||
--header "PRIVATE-TOKEN: $PIGEON_GITLAB_TOKEN" \
|
--header "PRIVATE-TOKEN: ${TOKEN:?}" \
|
||||||
--header 'Content-Type: application/json' \
|
--header 'Content-Type: application/json' \
|
||||||
--data "$(jq -n --arg d "$MESSAGE" '{"description": $d}')" \
|
--data "$(jq -n --arg d "$MESSAGE" '{"description": $d}')" \
|
||||||
--url "https://gitlab.com/api/v4/projects/$PROJECT_ID/releases/$RELEASE_TAG"
|
--url "https://gitlab.com/api/v4/projects/$1/releases/$4"
|
||||||
|
|||||||
Reference in New Issue
Block a user