diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8f88c7c22e..756600cc1e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -156,109 +156,221 @@ jobs: with: fetch-depth: 1 fetch-tags: false - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '16' - - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: '3.x' - - - name: Download a Coverage Results - if: ${{ github.event.inputs.skiptests == 'false' || github.ref_name == 'release' }} - uses: actions/download-artifact@v4.1.8 - with: - name: coverage - - name: Make Release Build - env: - DEBIAN_FRONTEND: noninteractive - BROWSERSLIST_IGNORE_OLD_DATA: 1 - run: | - echo "PKG_VERSION: $PKG_VERSION" - echo "GITHUB_SHA: $GITHUB_SHA" - echo "GITHUB_REF_NAME: $GITHUB_REF_NAME" - echo "Running frontend build script..." - echo "Compiling native node packages..." - yarn rebuild - echo "Packaging static assets..." - yarn build --base=https://static.ietf.org/dt/$PKG_VERSION/ - yarn legacy:build - echo "Setting version $PKG_VERSION..." - sed -i -r -e "s|^__version__ += '.*'$|__version__ = '$PKG_VERSION'|" ietf/__init__.py - sed -i -r -e "s|^__release_hash__ += '.*'$|__release_hash__ = '$GITHUB_SHA'|" ietf/__init__.py - sed -i -r -e "s|^__release_branch__ += '.*'$|__release_branch__ = '$GITHUB_REF_NAME'|" ietf/__init__.py - - - name: Set Production Flags - if: ${{ env.SHOULD_DEPLOY == 'true' }} + - name: Launch build VM + id: azlaunch run: | - echo "Setting production flags in settings.py..." - sed -i -r -e 's/^DEBUG *= *.*$/DEBUG = False/' -e "s/^SERVER_MODE *= *.*\$/SERVER_MODE = 'production'/" ietf/settings.py - - - name: Make Release Tarball - env: - DEBIAN_FRONTEND: noninteractive - run: | - echo "Build release tarball..." - mkdir -p /home/runner/work/release - tar -czf /home/runner/work/release/release.tar.gz -X dev/build/exclude-patterns.txt . + echo "Authenticating to Azure..." + az login --service-principal -u ${{ secrets.AZ_BUILD_APP_ID }} -p ${{ secrets.AZ_BUILD_PWD }} --tenant ${{ secrets.AZ_BUILD_TENANT_ID }} + echo "Creating VM..." + vminfo=$(az vm create \ + --resource-group ghaDatatracker \ + --name tmpGhaBuildVM \ + --image Ubuntu2204 \ + --admin-username azureuser \ + --generate-ssh-keys \ + --priority Spot \ + --size Standard_D8ads_v5 \ + --max-price -1 \ + --ephemeral-os-disk \ + --os-disk-size-gb 100 \ + --eviction-policy Delete \ + --nic-delete-option Delete \ + --output tsv \ + --query "publicIpAddress") + echo "ipaddr=$vminfo" >> "$GITHUB_OUTPUT" + echo "VM Public IP: $vminfo" + cat ~/.ssh/id_rsa > ${{ github.workspace }}/prvkey.key + ssh-keyscan -t rsa $vminfo >> ~/.ssh/known_hosts - - name: Collect + Push Statics + - name: Remote SSH into Build VM + uses: appleboy/ssh-action@25ce8cbbcb08177468c7ff7ec5cbfa236f9341e1 env: - DEBIAN_FRONTEND: noninteractive + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_ACTOR: ${{ github.actor }} + GITHUB_SHA: ${{ github.sha }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_RUN_ID: ${{ github.run_id }} AWS_ACCESS_KEY_ID: ${{ secrets.CF_R2_STATIC_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.CF_R2_STATIC_KEY_SECRET }} AWS_DEFAULT_REGION: auto AWS_ENDPOINT_URL: ${{ secrets.CF_R2_ENDPOINT }} + PKG_VERSION: ${{ env.PKG_VERSION }} + SHOULD_DEPLOY: ${{ env.SHOULD_DEPLOY }} + SKIP_TESTS: ${{ github.event.inputs.skiptests }} + DEBIAN_FRONTEND: noninteractive + BROWSERSLIST_IGNORE_OLD_DATA: 1 + with: + host: ${{ steps.azlaunch.outputs.ipaddr }} + port: 22 + username: azureuser + command_timeout: 60m + key_path: ${{ github.workspace }}/prvkey.key + envs: GITHUB_TOKEN,GITHUB_ACTOR,GITHUB_SHA,GITHUB_REF_NAME,GITHUB_RUN_ID,AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY,AWS_DEFAULT_REGION,AWS_ENDPOINT_URL,PKG_VERSION,SHOULD_DEPLOY,SKIP_TESTS,DEBIAN_FRONTEND,BROWSERSLIST_IGNORE_OLD_DATA + script_stop: true + script: | + export DEBIAN_FRONTEND=noninteractive + lsb_release -a + sudo apt-get update + sudo apt-get upgrade -y + sudo apt-get install wget unzip curl -y + + echo "==========================================================================" + echo "Installing Docker..." + echo "==========================================================================" + curl -fsSL https://get.docker.com -o get-docker.sh + sudo sh get-docker.sh + sudo docker buildx create \ + --name container-builder \ + --driver docker-container \ + --bootstrap --use + + echo "==========================================================================" + echo "Login to ghcr.io..." + echo "==========================================================================" + echo $GITHUB_TOKEN | sudo docker login ghcr.io -u $GITHUB_ACTOR --password-stdin + + echo "==========================================================================" + echo "Installing GH CLI..." + echo "==========================================================================" + sudo mkdir -p -m 755 /etc/apt/keyrings \ + && wget -qO- https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null \ + && sudo chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \ + && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null \ + && sudo apt update \ + && sudo apt install gh -y + + echo "==========================================================================" + echo "Installing AWS CLI..." + echo "==========================================================================" + curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" + unzip awscliv2.zip + sudo ./aws/install + + echo "==========================================================================" + echo "Install Node.js..." + echo "==========================================================================" + curl -fsSL https://deb.nodesource.com/setup_18.x -o nodesource_setup.sh + sudo bash nodesource_setup.sh + sudo apt-get install -y nodejs + sudo corepack enable + + echo "==========================================================================" + echo "Install Python 3.x..." + echo "==========================================================================" + sudo apt-get install python3 python3-dev -y + python3 --version + + echo "==========================================================================" + echo "Clone project..." + echo "==========================================================================" + sudo mkdir -p /workspace + sudo chown azureuser /workspace + cd /workspace + gh repo clone ietf-tools/datatracker -- --depth=1 --no-tags + cd datatracker + + if [ "$SKIP_TESTS" = "false" ] || [ "$GITHUB_REF_NAME" = "release" ] ; then + echo "==========================================================================" + echo "Downloading coverage..." + echo "==========================================================================" + gh run download $GITHUB_RUN_ID -n coverage + fi + + echo "==========================================================================" + echo "Building project..." + echo "==========================================================================" + echo "PKG_VERSION: $PKG_VERSION" + echo "GITHUB_SHA: $GITHUB_SHA" + echo "GITHUB_REF_NAME: $GITHUB_REF_NAME" + echo "Running frontend build script..." + echo "Compiling native node packages..." + yarn rebuild + echo "Packaging static assets..." + yarn build --base=https://static.ietf.org/dt/$PKG_VERSION/ + yarn legacy:build + echo "Setting version $PKG_VERSION..." + sed -i -r -e "s|^__version__ += '.*'$|__version__ = '$PKG_VERSION'|" ietf/__init__.py + sed -i -r -e "s|^__release_hash__ += '.*'$|__release_hash__ = '$GITHUB_SHA'|" ietf/__init__.py + sed -i -r -e "s|^__release_branch__ += '.*'$|__release_branch__ = '$GITHUB_REF_NAME'|" ietf/__init__.py + + if [ "$SHOULD_DEPLOY" = "true" ] ; then + echo "==========================================================================" + echo "Setting production flags in settings.py..." + echo "==========================================================================" + sed -i -r -e 's/^DEBUG *= *.*$/DEBUG = False/' -e "s/^SERVER_MODE *= *.*\$/SERVER_MODE = 'production'/" ietf/settings.py + fi + + echo "==========================================================================" + echo "Build release tarball..." + echo "==========================================================================" + mkdir -p /workspace/release + tar -czf /workspace/release.tar.gz -X dev/build/exclude-patterns.txt . + + echo "==========================================================================" + echo "Collecting statics..." + echo "==========================================================================" + sudo docker run --rm --name collectstatics -v $(pwd):/workspace ghcr.io/ietf-tools/datatracker-app-base:latest sh dev/build/collectstatics.sh + echo "Pushing statics..." + cd static + aws s3 sync . s3://static/dt/$PKG_VERSION --only-show-errors + cd .. + + echo "==========================================================================" + echo "Augment dockerignore for docker image build..." + echo "==========================================================================" + cat >> .dockerignore <> .dockerignore <> ~/.ssh/known_hosts - name: Remote SSH into VM - uses: appleboy/ssh-action@029f5b4aeeeb58fdfe1410a5d17f967dacf36262 + uses: appleboy/ssh-action@25ce8cbbcb08177468c7ff7ec5cbfa236f9341e1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 616ffdcc63..64db03db09 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -59,7 +59,7 @@ jobs: path: geckodriver.log - name: Upload Coverage Results to Codecov - uses: codecov/codecov-action@v4.5.0 + uses: codecov/codecov-action@v4.6.0 with: files: coverage.xml diff --git a/LICENSE b/LICENSE index c180fa3f9f..dc6e0c5663 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ BSD 3-Clause License -Copyright (c) 2008-2023, The IETF Trust +Copyright (c) 2008-2024, The IETF Trust All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/dev/coverage-action/package-lock.json b/dev/coverage-action/package-lock.json index 8c9b97e026..f23a9b66c7 100644 --- a/dev/coverage-action/package-lock.json +++ b/dev/coverage-action/package-lock.json @@ -9,19 +9,27 @@ "version": "1.0.0", "license": "BSD-3-Clause", "dependencies": { - "@actions/core": "1.10.1", + "@actions/core": "1.11.1", "@actions/github": "6.0.0", "lodash": "4.17.21", - "luxon": "3.4.4" + "luxon": "3.5.0" } }, "node_modules/@actions/core": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.10.1.tgz", - "integrity": "sha512-3lBR9EDAY+iYIpTnTIXmWcNbX3T2kCkAEQGIQx4NVQ0575nk2k3GRZDTPQG+vVtS2izSLmINlxXf0uLtnrTP+g==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.11.1.tgz", + "integrity": "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==", "dependencies": { - "@actions/http-client": "^2.0.1", - "uuid": "^8.3.2" + "@actions/exec": "^1.1.1", + "@actions/http-client": "^2.0.1" + } + }, + "node_modules/@actions/exec": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz", + "integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==", + "dependencies": { + "@actions/io": "^1.0.1" } }, "node_modules/@actions/github": { @@ -44,6 +52,11 @@ "undici": "^5.25.4" } }, + "node_modules/@actions/io": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.3.tgz", + "integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==" + }, "node_modules/@fastify/busboy": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.0.0.tgz", @@ -196,9 +209,9 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "node_modules/luxon": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", - "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz", + "integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==", "engines": { "node": ">=12" } @@ -235,14 +248,6 @@ "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==" }, - "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -251,12 +256,20 @@ }, "dependencies": { "@actions/core": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.10.1.tgz", - "integrity": "sha512-3lBR9EDAY+iYIpTnTIXmWcNbX3T2kCkAEQGIQx4NVQ0575nk2k3GRZDTPQG+vVtS2izSLmINlxXf0uLtnrTP+g==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.11.1.tgz", + "integrity": "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==", "requires": { - "@actions/http-client": "^2.0.1", - "uuid": "^8.3.2" + "@actions/exec": "^1.1.1", + "@actions/http-client": "^2.0.1" + } + }, + "@actions/exec": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz", + "integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==", + "requires": { + "@actions/io": "^1.0.1" } }, "@actions/github": { @@ -279,6 +292,11 @@ "undici": "^5.25.4" } }, + "@actions/io": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.3.tgz", + "integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==" + }, "@fastify/busboy": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.0.0.tgz", @@ -395,9 +413,9 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "luxon": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", - "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==" + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz", + "integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==" }, "once": { "version": "1.4.0", @@ -425,11 +443,6 @@ "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==" }, - "uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" - }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/dev/coverage-action/package.json b/dev/coverage-action/package.json index 0a8794d2a2..1d03fe3dab 100644 --- a/dev/coverage-action/package.json +++ b/dev/coverage-action/package.json @@ -6,9 +6,9 @@ "author": "IETF Trust", "license": "BSD-3-Clause", "dependencies": { - "@actions/core": "1.10.1", + "@actions/core": "1.11.1", "@actions/github": "6.0.0", "lodash": "4.17.21", - "luxon": "3.4.4" + "luxon": "3.5.0" } } diff --git a/dev/deploy-to-container/package-lock.json b/dev/deploy-to-container/package-lock.json index 7b86986d56..f636a45f05 100644 --- a/dev/deploy-to-container/package-lock.json +++ b/dev/deploy-to-container/package-lock.json @@ -11,7 +11,7 @@ "nanoid": "5.0.7", "nanoid-dictionary": "5.0.0-beta.1", "slugify": "1.6.6", - "tar": "^7.4.0", + "tar": "^7.4.3", "yargs": "^17.7.2" }, "engines": { @@ -788,9 +788,9 @@ } }, "node_modules/tar": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.0.tgz", - "integrity": "sha512-XQs0S8fuAkQWuqhDeCdMlJXDX80D7EOVLDPVFkna9yQfzS+PHKgfxcei0jf6/+QAWcjqrnC8uM3fSAnrQl+XYg==", + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", @@ -1503,9 +1503,9 @@ } }, "tar": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.0.tgz", - "integrity": "sha512-XQs0S8fuAkQWuqhDeCdMlJXDX80D7EOVLDPVFkna9yQfzS+PHKgfxcei0jf6/+QAWcjqrnC8uM3fSAnrQl+XYg==", + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", "requires": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", diff --git a/dev/deploy-to-container/package.json b/dev/deploy-to-container/package.json index 2f582dd2a7..be77fa5cce 100644 --- a/dev/deploy-to-container/package.json +++ b/dev/deploy-to-container/package.json @@ -7,7 +7,7 @@ "nanoid": "5.0.7", "nanoid-dictionary": "5.0.0-beta.1", "slugify": "1.6.6", - "tar": "^7.4.0", + "tar": "^7.4.3", "yargs": "^17.7.2" }, "engines": { diff --git a/dev/diff/package-lock.json b/dev/diff/package-lock.json index 3500ccec48..87aaf3f16f 100644 --- a/dev/diff/package-lock.json +++ b/dev/diff/package-lock.json @@ -15,9 +15,9 @@ "keypress": "^0.2.1", "listr2": "^6.6.1", "lodash-es": "^4.17.21", - "luxon": "^3.4.4", + "luxon": "^3.5.0", "pretty-bytes": "^6.1.1", - "tar": "^7.4.0", + "tar": "^7.4.3", "yargs": "^17.7.2" }, "engines": { @@ -1060,9 +1060,9 @@ } }, "node_modules/luxon": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", - "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz", + "integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==", "engines": { "node": ">=12" } @@ -1493,9 +1493,9 @@ } }, "node_modules/tar": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.0.tgz", - "integrity": "sha512-XQs0S8fuAkQWuqhDeCdMlJXDX80D7EOVLDPVFkna9yQfzS+PHKgfxcei0jf6/+QAWcjqrnC8uM3fSAnrQl+XYg==", + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", @@ -2410,9 +2410,9 @@ "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==" }, "luxon": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", - "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==" + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz", + "integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==" }, "mimic-fn": { "version": "2.1.0", @@ -2691,9 +2691,9 @@ } }, "tar": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.0.tgz", - "integrity": "sha512-XQs0S8fuAkQWuqhDeCdMlJXDX80D7EOVLDPVFkna9yQfzS+PHKgfxcei0jf6/+QAWcjqrnC8uM3fSAnrQl+XYg==", + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", "requires": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", diff --git a/dev/diff/package.json b/dev/diff/package.json index 1b5540e346..4e0e1be8f4 100644 --- a/dev/diff/package.json +++ b/dev/diff/package.json @@ -11,9 +11,9 @@ "keypress": "^0.2.1", "listr2": "^6.6.1", "lodash-es": "^4.17.21", - "luxon": "^3.4.4", + "luxon": "^3.5.0", "pretty-bytes": "^6.1.1", - "tar": "^7.4.0", + "tar": "^7.4.3", "yargs": "^17.7.2" }, "engines": { diff --git a/ietf/api/tests.py b/ietf/api/tests.py index 4f2a7f7d3c..97e928dc51 100644 --- a/ietf/api/tests.py +++ b/ietf/api/tests.py @@ -1,4 +1,4 @@ -# Copyright The IETF Trust 2015-2020, All Rights Reserved +# Copyright The IETF Trust 2015-2024, All Rights Reserved # -*- coding: utf-8 -*- import base64 import datetime @@ -222,6 +222,70 @@ def test_api_set_session_video_url(self): event = doc.latest_event() self.assertEqual(event.by, recman) + def test_api_set_meetecho_recording_name(self): + url = urlreverse("ietf.meeting.views.api_set_meetecho_recording_name") + recmanrole = RoleFactory(group__type_id="ietf", name_id="recman") + recman = recmanrole.person + meeting = MeetingFactory(type_id="ietf") + session = SessionFactory(group__type_id="wg", meeting=meeting) + apikey = PersonalApiKey.objects.create(endpoint=url, person=recman) + name = "testname" + + # error cases + r = self.client.post(url, {}) + self.assertContains(r, "Missing apikey parameter", status_code=400) + + badrole = RoleFactory(group__type_id="ietf", name_id="ad") + badapikey = PersonalApiKey.objects.create(endpoint=url, person=badrole.person) + badrole.person.user.last_login = timezone.now() + badrole.person.user.save() + r = self.client.post(url, {"apikey": badapikey.hash()}) + self.assertContains(r, "Restricted to role: Recording Manager", status_code=403) + + r = self.client.post(url, {"apikey": apikey.hash()}) + self.assertContains(r, "Too long since last regular login", status_code=400) + recman.user.last_login = timezone.now() + recman.user.save() + + r = self.client.get(url, {"apikey": apikey.hash()}) + self.assertContains(r, "Method not allowed", status_code=405) + + r = self.client.post(url, {"apikey": apikey.hash()}) + self.assertContains(r, "Missing session_id parameter", status_code=400) + + r = self.client.post(url, {"apikey": apikey.hash(), "session_id": session.pk}) + self.assertContains(r, "Missing name parameter", status_code=400) + + bad_pk = int(Session.objects.order_by("-pk").first().pk) + 1 + r = self.client.post( + url, + { + "apikey": apikey.hash(), + "session_id": bad_pk, + "name": name, + }, + ) + self.assertContains(r, "Session not found", status_code=400) + + r = self.client.post( + url, + { + "apikey": apikey.hash(), + "session_id": "foo", + "name": name, + }, + ) + self.assertContains(r, "Invalid session_id", status_code=400) + + r = self.client.post( + url, {"apikey": apikey.hash(), "session_id": session.pk, "name": name} + ) + self.assertContains(r, "Done", status_code=200) + + session.refresh_from_db() + self.assertEqual(session.meetecho_recording_name, name) + + def test_api_add_session_attendees_deprecated(self): # Deprecated test - should be removed when we stop accepting a simple list of user PKs in # the add_session_attendees() view diff --git a/ietf/api/urls.py b/ietf/api/urls.py index cb407218e7..661d55d187 100644 --- a/ietf/api/urls.py +++ b/ietf/api/urls.py @@ -1,4 +1,4 @@ -# Copyright The IETF Trust 2017, All Rights Reserved +# Copyright The IETF Trust 2017-2024, All Rights Reserved from django.conf import settings from django.urls import include @@ -39,6 +39,8 @@ url(r'^iesg/position', views_ballot.api_set_position), # Let Meetecho set session video URLs url(r'^meeting/session/video/url$', meeting_views.api_set_session_video_url), + # Let Meetecho tell us the name of its recordings + url(r'^meeting/session/recording-name$', meeting_views.api_set_meetecho_recording_name), # Meeting agenda + floorplan data url(r'^meeting/(?P[A-Za-z0-9._+-]+)/agenda-data$', meeting_views.api_get_agenda_data), # Meeting session materials diff --git a/ietf/community/tests.py b/ietf/community/tests.py index 181e9e0fa6..5329267d8e 100644 --- a/ietf/community/tests.py +++ b/ietf/community/tests.py @@ -95,9 +95,7 @@ def test_view_list_duplicates(self): url = urlreverse(ietf.community.views.view_list, kwargs={ "email_or_name": person.plain_name()}) r = self.client.get(url) - self.assertEqual(r.status_code, 300) - self.assertIn("bazquux@example.com", r.content.decode()) - self.assertIn("foobar@example.com", r.content.decode()) + self.assertEqual(r.status_code, 404) def complex_person(self, *args, **kwargs): person = PersonFactory(*args, **kwargs) diff --git a/ietf/community/views.py b/ietf/community/views.py index 78b8144d60..923ec556f3 100644 --- a/ietf/community/views.py +++ b/ietf/community/views.py @@ -28,11 +28,14 @@ from ietf.utils.http import is_ajax from ietf.utils.response import permission_denied -class MultiplePersonError(Exception): - """More than one Person record matches the given email or name""" - pass def lookup_community_list(request, email_or_name=None, acronym=None): + """Finds a CommunityList for a person or group + + Instantiates an unsaved CommunityList if one is not found. + + If the person or group cannot be found and uniquely identified, raises an Http404 exception + """ assert email_or_name or acronym if acronym: @@ -44,19 +47,14 @@ def lookup_community_list(request, email_or_name=None, acronym=None): if hasattr(request.user, 'person') and request.user.person in persons: person = request.user.person else: - raise MultiplePersonError("\r\n".join([p.user.username for p in persons])) + raise Http404(f"Unable to identify the CommunityList for {email_or_name}") else: person = persons[0] clist = CommunityList.objects.filter(person=person).first() or CommunityList(person=person) - return clist def view_list(request, email_or_name=None): - try: - clist = lookup_community_list(request, email_or_name) - except MultiplePersonError as err: - return HttpResponse(str(err), status=300) - + clist = lookup_community_list(request, email_or_name) # may raise Http404 docs = docs_tracked_by_community_list(clist) docs, meta = prepare_document_table(request, docs, request.GET) @@ -76,10 +74,7 @@ def view_list(request, email_or_name=None): def manage_list(request, email_or_name=None, acronym=None): # we need to be a bit careful because clist may not exist in the # database so we can't call related stuff on it yet - try: - clist = lookup_community_list(request, email_or_name, acronym) - except MultiplePersonError as err: - return HttpResponse(str(err), status=300) + clist = lookup_community_list(request, email_or_name, acronym) # may raise Http404 if not can_manage_community_list(request.user, clist): permission_denied(request, "You do not have permission to access this view") @@ -166,10 +161,7 @@ def track_document(request, name, email_or_name=None, acronym=None): doc = get_object_or_404(Document, name=name) if request.method == "POST": - try: - clist = lookup_community_list(request, email_or_name, acronym) - except MultiplePersonError as err: - return HttpResponse(str(err), status=300) + clist = lookup_community_list(request, email_or_name, acronym) # may raise Http404 if not can_manage_community_list(request.user, clist): permission_denied(request, "You do not have permission to access this view") @@ -191,10 +183,7 @@ def track_document(request, name, email_or_name=None, acronym=None): @login_required def untrack_document(request, name, email_or_name=None, acronym=None): doc = get_object_or_404(Document, name=name) - try: - clist = lookup_community_list(request, email_or_name, acronym) - except MultiplePersonError as err: - return HttpResponse(str(err), status=300) + clist = lookup_community_list(request, email_or_name, acronym) # may raise Http404 if not can_manage_community_list(request.user, clist): permission_denied(request, "You do not have permission to access this view") @@ -214,11 +203,7 @@ def untrack_document(request, name, email_or_name=None, acronym=None): @ignore_view_kwargs("group_type") def export_to_csv(request, email_or_name=None, acronym=None): - try: - clist = lookup_community_list(request, email_or_name, acronym) - except MultiplePersonError as err: - return HttpResponse(str(err), status=300) - + clist = lookup_community_list(request, email_or_name, acronym) # may raise Http404 response = HttpResponse(content_type='text/csv') if clist.group: @@ -259,11 +244,7 @@ def export_to_csv(request, email_or_name=None, acronym=None): @ignore_view_kwargs("group_type") def feed(request, email_or_name=None, acronym=None): - try: - clist = lookup_community_list(request, email_or_name, acronym) - except MultiplePersonError as err: - return HttpResponse(str(err), status=300) - + clist = lookup_community_list(request, email_or_name, acronym) # may raise Http404 significant = request.GET.get('significant', '') == '1' documents = docs_tracked_by_community_list(clist).values_list('pk', flat=True) @@ -299,12 +280,9 @@ def feed(request, email_or_name=None, acronym=None): @login_required @ignore_view_kwargs("group_type") def subscription(request, email_or_name=None, acronym=None): - try: - clist = lookup_community_list(request, email_or_name, acronym) - if clist.pk is None: - raise Http404 - except MultiplePersonError as err: - return HttpResponse(str(err), status=300) + clist = lookup_community_list(request, email_or_name, acronym) # may raise Http404 + if clist.pk is None: + raise Http404 person = request.user.person diff --git a/ietf/ipr/mail.py b/ietf/ipr/mail.py index 842426d820..167b11956c 100644 --- a/ietf/ipr/mail.py +++ b/ietf/ipr/mail.py @@ -171,31 +171,44 @@ def message_from_message(message,by=None): ) return msg + +class UndeliverableIprResponseError(Exception): + """Response email could not be delivered and should be treated as an error""" + + def process_response_email(msg): - """Saves an incoming message. msg=string. Message "To" field is expected to - be in the format ietf-ipr+[identifier]@ietf.org. Expect to find a message with - a matching value in the reply_to field, associated to an IPR disclosure through - IprEvent. Create a Message object for the incoming message and associate it to - the original message via new IprEvent""" + """Save an incoming IPR response email message + + Message "To" field is expected to be in the format ietf-ipr+[identifier]@ietf.org. If + the address or identifier is missing, the message will be silently dropped. + + Expect to find a message with a matching value in the reply_to field, associated to an + IPR disclosure through IprEvent. If it cannot be matched, raises UndeliverableIprResponseError + + Creates a Message object for the incoming message and associates it to + the original message via new IprEvent + """ message = message_from_bytes(force_bytes(msg)) to = message.get('To', '') # exit if this isn't a response we're interested in (with plus addressing) - local,domain = get_base_ipr_request_address().split('@') + local, domain = get_base_ipr_request_address().split('@') if not re.match(r'^{}\+[a-zA-Z0-9_\-]{}@{}'.format(local,'{16}',domain),to): - return None - + _from = message.get("From", "") + log(f"Ignoring IPR email without a message identifier from {_from} to {to}") + return + try: to_message = Message.objects.get(reply_to=to) except Message.DoesNotExist: log('Error finding matching message ({})'.format(to)) - return None + raise UndeliverableIprResponseError(f"Unable to find message matching {to}") try: disclosure = to_message.msgevents.first().disclosure except: log('Error processing message ({})'.format(to)) - return None + raise UndeliverableIprResponseError("Error processing message for {to}") ietf_message = message_from_message(message) IprEvent.objects.create( @@ -207,4 +220,4 @@ def process_response_email(msg): ) log("Received IPR email from %s" % ietf_message.frm) - return ietf_message + diff --git a/ietf/ipr/management/commands/process_email.py b/ietf/ipr/management/commands/process_email.py index 0b15fb0651..616cade5c4 100644 --- a/ietf/ipr/management/commands/process_email.py +++ b/ietf/ipr/management/commands/process_email.py @@ -9,7 +9,7 @@ from django.core.management import CommandError from ietf.utils.management.base import EmailOnFailureCommand -from ietf.ipr.mail import process_response_email +from ietf.ipr.mail import process_response_email, UndeliverableIprResponseError import debug # pyflakes:ignore @@ -31,7 +31,7 @@ def handle(self, *args, **options): self.msg_bytes = sys.stdin.buffer.read() try: process_response_email(self.msg_bytes) - except ValueError as e: + except (ValueError, UndeliverableIprResponseError) as e: raise CommandError(e) failure_subject = 'Error during ipr email processing' diff --git a/ietf/ipr/tests.py b/ietf/ipr/tests.py index 3c70567fd8..d72018f10b 100644 --- a/ietf/ipr/tests.py +++ b/ietf/ipr/tests.py @@ -4,6 +4,7 @@ import datetime import mock +import re from pyquery import PyQuery from urllib.parse import quote, urlparse @@ -35,9 +36,9 @@ ) from ietf.ipr.forms import DraftForm, HolderIprDisclosureForm from ietf.ipr.mail import (process_response_email, get_reply_to, get_update_submitter_emails, - get_pseudo_submitter, get_holders, get_update_cc_addrs) -from ietf.ipr.models import (IprDisclosureBase,GenericIprDisclosure,HolderIprDisclosure, - ThirdPartyIprDisclosure) + get_pseudo_submitter, get_holders, get_update_cc_addrs, UndeliverableIprResponseError) +from ietf.ipr.models import (IprDisclosureBase, GenericIprDisclosure, HolderIprDisclosure, + ThirdPartyIprDisclosure, IprEvent) from ietf.ipr.templatetags.ipr_filters import no_revisions_message from ietf.ipr.utils import get_genitive, get_ipr_summary, ingest_response_email from ietf.mailtrigger.utils import gather_address_lists @@ -712,7 +713,7 @@ def test_notify_generic(self): ) self.assertIn(f'{settings.IDTRACKER_BASE_URL}{urlreverse("ietf.ipr.views.showlist")}', get_payload_text(outbox[1]).replace('\n',' ')) - def send_ipr_email_helper(self): + def send_ipr_email_helper(self) -> tuple[str, IprEvent, HolderIprDisclosure]: ipr = HolderIprDisclosureFactory() url = urlreverse('ietf.ipr.views.email',kwargs={ "id": ipr.id }) self.client.login(username="secretary", password="secretary+password") @@ -730,10 +731,11 @@ def send_ipr_email_helper(self): q = Message.objects.filter(reply_to=data['reply_to']) self.assertEqual(q.count(),1) event = q[0].msgevents.first() + assert event is not None self.assertTrue(event.response_past_due()) self.assertEqual(len(outbox), 1) self.assertTrue('joe@test.com' in outbox[0]['To']) - return data['reply_to'], event + return data['reply_to'], event, ipr uninteresting_ipr_message_strings = [ ("To: {to}\nCc: {cc}\nFrom: joe@test.com\nDate: {date}\nSubject: test\n"), @@ -747,34 +749,46 @@ def send_ipr_email_helper(self): def test_process_response_email(self): # first send a mail - reply_to, event = self.send_ipr_email_helper() + reply_to, event, _ = self.send_ipr_email_helper() # test process response uninteresting messages addrs = gather_address_lists('ipr_disclosure_submitted').as_strings() for message_string in self.uninteresting_ipr_message_strings: - result = process_response_email( + process_response_email( message_string.format( to=addrs.to, cc=addrs.cc, date=timezone.now().ctime() ) ) - self.assertIsNone(result) - + # test process response message_string = """To: {} From: joe@test.com Date: {} Subject: test """.format(reply_to, timezone.now().ctime()) - result = process_response_email(message_string) - - self.assertIsInstance(result, Message) + process_response_email(message_string) self.assertFalse(event.response_past_due()) + # test with an unmatchable message identifier + bad_reply_to = re.sub( + r"\+.{16}@", + '+0123456789abcdef@', + reply_to, + ) + self.assertNotEqual(reply_to, bad_reply_to) + message_string = f"""To: {bad_reply_to} + From: joe@test.com + Date: {timezone.now().ctime()} + Subject: test + """ + with self.assertRaises(UndeliverableIprResponseError): + process_response_email(message_string) + def test_process_response_email_with_invalid_encoding(self): """Interesting emails with invalid encoding should be handled""" - reply_to, _ = self.send_ipr_email_helper() + reply_to, _, disclosure = self.send_ipr_email_helper() # test process response message_string = """To: {} From: joe@test.com @@ -782,8 +796,8 @@ def test_process_response_email_with_invalid_encoding(self): Subject: test """.format(reply_to, timezone.now().ctime()) message_bytes = message_string.encode('utf8') + b'\nInvalid stuff: \xfe\xff\n' - result = process_response_email(message_bytes) - self.assertIsInstance(result, Message) + process_response_email(message_bytes) + result = IprEvent.objects.filter(disclosure=disclosure).first().message # newest # \ufffd is a rhombus character with an inverse ?, used to replace invalid characters self.assertEqual(result.body, 'Invalid stuff: \ufffd\ufffd\n\n', # not sure where the extra \n is from 'Invalid characters should be replaced with \ufffd characters') @@ -798,8 +812,7 @@ def test_process_response_email_uninteresting_with_invalid_encoding(self): cc=addrs.cc, date=timezone.now().ctime(), ).encode('utf8') + b'\nInvalid stuff: \xfe\xff\n' - result = process_response_email(message_bytes) - self.assertIsNone(result) + process_response_email(message_bytes) @override_settings(ADMINS=(("Some Admin", "admin@example.com"),)) @mock.patch("ietf.ipr.utils.process_response_email") @@ -816,8 +829,8 @@ def test_ingest_response_email(self, mock_process_response_email): self.assertEqual(mock_process_response_email.call_args, mock.call(message)) mock_process_response_email.reset_mock() - mock_process_response_email.side_effect = None - mock_process_response_email.return_value = None # rejected message + mock_process_response_email.side_effect = UndeliverableIprResponseError + mock_process_response_email.return_value = None with self.assertRaises(EmailIngestionError) as context: ingest_response_email(message) self.assertIsNone(context.exception.as_emailmessage()) # should not send an email on a clean rejection @@ -825,6 +838,14 @@ def test_ingest_response_email(self, mock_process_response_email): self.assertEqual(mock_process_response_email.call_args, mock.call(message)) mock_process_response_email.reset_mock() + mock_process_response_email.side_effect = None + mock_process_response_email.return_value = None # ignored message + ingest_response_email(message) # should not raise an exception + self.assertIsNone(context.exception.as_emailmessage()) # should not send an email on ignored message + self.assertTrue(mock_process_response_email.called) + self.assertEqual(mock_process_response_email.call_args, mock.call(message)) + mock_process_response_email.reset_mock() + # successful operation mock_process_response_email.return_value = MessageFactory() ingest_response_email(message) diff --git a/ietf/ipr/utils.py b/ietf/ipr/utils.py index 42d485ccad..8f0b9cf3f2 100644 --- a/ietf/ipr/utils.py +++ b/ietf/ipr/utils.py @@ -3,7 +3,7 @@ from textwrap import dedent -from ietf.ipr.mail import process_response_email +from ietf.ipr.mail import process_response_email, UndeliverableIprResponseError from ietf.ipr.models import IprDocRel import debug # pyflakes:ignore @@ -92,7 +92,11 @@ def generate_draft_recursive_txt(): def ingest_response_email(message: bytes): from ietf.api.views import EmailIngestionError # avoid circular import try: - result = process_response_email(message) + process_response_email(message) + except UndeliverableIprResponseError: + # Message was rejected due to some problem the sender can fix, so bounce but don't send + # an email to the admins + raise EmailIngestionError("IPR response rejected", email_body=None) except Exception as err: # Message was rejected due to an unhandled exception. This is likely something # the admins need to address, so send them a copy of the email. @@ -106,8 +110,3 @@ def ingest_response_email(message: bytes): email_original_message=message, email_attach_traceback=True, ) from err - - if result is None: - # Message was rejected due to some problem the sender can fix, so bounce but don't send - # an email to the admins - raise EmailIngestionError("IPR response rejected", email_body=None) diff --git a/ietf/meeting/migrations/0009_session_meetecho_recording_name.py b/ietf/meeting/migrations/0009_session_meetecho_recording_name.py new file mode 100644 index 0000000000..79ca4919a3 --- /dev/null +++ b/ietf/meeting/migrations/0009_session_meetecho_recording_name.py @@ -0,0 +1,20 @@ +# Copyright The IETF Trust 2024, All Rights Reserved + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("meeting", "0008_remove_schedtimesessassignment_notes"), + ] + + operations = [ + migrations.AddField( + model_name="session", + name="meetecho_recording_name", + field=models.CharField( + blank=True, help_text="Name of the meetecho recording", max_length=64 + ), + ), + ] diff --git a/ietf/meeting/models.py b/ietf/meeting/models.py index e169077800..8c6fb97413 100644 --- a/ietf/meeting/models.py +++ b/ietf/meeting/models.py @@ -1042,6 +1042,7 @@ class Session(models.Model): on_agenda = models.BooleanField(default=True, help_text='Is this session visible on the meeting agenda?') has_onsite_tool = models.BooleanField(default=False, help_text="Does this session use the officially supported onsite and remote tooling?") chat_room = models.CharField(blank=True, max_length=32, help_text='Name of Zulip stream, if different from group acronym') + meetecho_recording_name = models.CharField(blank=True, max_length=64, help_text="Name of the meetecho recording") tombstone_for = models.ForeignKey('Session', blank=True, null=True, help_text="This session is the tombstone for a session that was rescheduled", on_delete=models.CASCADE) @@ -1332,17 +1333,23 @@ def onsite_tool_url(self): return None def _session_recording_url_label(self): + otsa = self.official_timeslotassignment() + if otsa is None: + return None if self.meeting.type.slug == "ietf" and self.has_onsite_tool: - session_label = f"IETF{self.meeting.number}-{self.group.acronym.upper()}-{self.official_timeslotassignment().timeslot.time.strftime('%Y%m%d-%H%M')}" + session_label = f"IETF{self.meeting.number}-{self.group.acronym.upper()}-{otsa.timeslot.time.strftime('%Y%m%d-%H%M')}" else: - session_label = f"IETF-{self.group.acronym.upper()}-{self.official_timeslotassignment().timeslot.time.strftime('%Y%m%d-%H%M')}" + session_label = f"IETF-{self.group.acronym.upper()}-{otsa.timeslot.time.strftime('%Y%m%d-%H%M')}" return session_label def session_recording_url(self): url_formatter = getattr(settings, "MEETECHO_SESSION_RECORDING_URL", "") url = None - if url_formatter and self.video_stream_url: - url = url_formatter.format(session_label=self._session_recording_url_label()) + name = self.meetecho_recording_name + if name is None or name.strip() == "": + name = self._session_recording_url_label() + if url_formatter.strip() != "" and name is not None: + url = url_formatter.format(session_label=name) return url diff --git a/ietf/meeting/tests_models.py b/ietf/meeting/tests_models.py index 8457423c51..03b706e1d7 100644 --- a/ietf/meeting/tests_models.py +++ b/ietf/meeting/tests_models.py @@ -1,4 +1,4 @@ -# Copyright The IETF Trust 2021, All Rights Reserved +# Copyright The IETF Trust 2021-2024, All Rights Reserved # -*- coding: utf-8 -*- """Tests of models in the Meeting application""" import datetime @@ -172,6 +172,10 @@ def test_session_recording_url(self, mock): settings.MEETECHO_SESSION_RECORDING_URL = "http://player.example.com?{session_label}" self.assertEqual(session.session_recording_url(), "http://player.example.com?LABEL") + session.meetecho_recording_name="actualname" + session.save() + self.assertEqual(session.session_recording_url(), "http://player.example.com?actualname") + def test_session_recording_url_label_ietf(self): session = SessionFactory( meeting__type_id='ietf', diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py index 211cdec9a5..bce6ccd1bf 100644 --- a/ietf/meeting/views.py +++ b/ietf/meeting/views.py @@ -1840,7 +1840,7 @@ def agenda_extract_slide(item): "id": item.id, "title": item.title, "rev": item.rev, - "url": item.get_versionless_href(), + "url": item.get_href(), "ext": item.file_extension(), } @@ -4270,6 +4270,45 @@ class OldUploadRedirect(RedirectView): def get_redirect_url(self, **kwargs): return reverse_lazy('ietf.meeting.views.session_details',kwargs=self.kwargs) + +@require_api_key +@role_required("Recording Manager") +@csrf_exempt +def api_set_meetecho_recording_name(request): + """Set name for meetecho recording + + parameters: + apikey: the poster's personal API key + session_id: id of the session to update + name: the name to use for the recording at meetecho player + """ + def err(code, text): + return HttpResponse(text, status=code, content_type='text/plain') + + if request.method != "POST": + return HttpResponseNotAllowed( + content="Method not allowed", content_type="text/plain", permitted_methods=('POST',) + ) + + session_id = request.POST.get('session_id', None) + if session_id is None: + return err(400, 'Missing session_id parameter') + name = request.POST.get('name', None) + if name is None: + return err(400, 'Missing name parameter') + + try: + session = Session.objects.get(pk=session_id) + except Session.DoesNotExist: + return err(400, f"Session not found with session_id '{session_id}'") + except ValueError: + return err(400, "Invalid session_id: {session_id}") + + session.meetecho_recording_name = name + session.save() + + return HttpResponse("Done", status=200, content_type='text/plain') + @require_api_key @role_required('Recording Manager') @csrf_exempt diff --git a/ietf/person/tests.py b/ietf/person/tests.py index 9da201b707..61d9b0ed70 100644 --- a/ietf/person/tests.py +++ b/ietf/person/tests.py @@ -173,9 +173,7 @@ def test_person_photo_duplicates(self): url = urlreverse("ietf.person.views.photo", kwargs={ "email_or_name": person.plain_name()}) r = self.client.get(url) - self.assertEqual(r.status_code, 300) - self.assertIn("bazquux@example.com", r.content.decode()) - self.assertIn("foobar@example.com", r.content.decode()) + self.assertEqual(r.status_code, 404) def test_name_methods(self): person = PersonFactory(name="Dr. Jens F. Möller", ) diff --git a/ietf/person/views.py b/ietf/person/views.py index 6d9daf4a81..bb1fa79f84 100644 --- a/ietf/person/views.py +++ b/ietf/person/views.py @@ -76,7 +76,7 @@ def profile(request, email_or_name): def photo(request, email_or_name): persons = lookup_persons(email_or_name) if len(persons) > 1: - return HttpResponse(r"\r\n".join([p.user.username for p in persons]), status=300) + raise Http404("No photo found") person = persons[0] if not person.photo: raise Http404("No photo found") diff --git a/ietf/static/css/document_html_txt.scss b/ietf/static/css/document_html_txt.scss index b0fec7c4d6..a5991056c9 100644 --- a/ietf/static/css/document_html_txt.scss +++ b/ietf/static/css/document_html_txt.scss @@ -344,7 +344,7 @@ div:is(.artwork, .sourcecode) pre { flex: 0 0 content; margin: 0; max-width: 72ch; - overflow: auto; + overflow: auto clip; } div:is(.artwork, .sourcecode) .pilcrow { flex: 0 0 1ch; diff --git a/ietf/submit/forms.py b/ietf/submit/forms.py index 2781d3365a..bed87b77c2 100644 --- a/ietf/submit/forms.py +++ b/ietf/submit/forms.py @@ -659,3 +659,13 @@ def __init__(self, *args, **kwargs): self.fields['frm'].label='From' self.fields['frm'].widget.attrs['readonly'] = True self.fields['reply_to'].widget.attrs['readonly'] = True + + +class SubmissionSearchForm(forms.Form): + """Form used for search_submission""" + + name = forms.CharField(max_length=255, required=True, label="I-D name") + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields["name"].widget.attrs["placeholder"] = "draft-..." diff --git a/ietf/submit/tests.py b/ietf/submit/tests.py index ed28c7ef02..21ed6672df 100644 --- a/ietf/submit/tests.py +++ b/ietf/submit/tests.py @@ -27,11 +27,6 @@ from django.utils.encoding import force_str import debug # pyflakes:ignore -from ietf.submit.utils import (expirable_submissions, expire_submission, find_submission_filenames, - post_submission, validate_submission_name, validate_submission_rev, - process_and_accept_uploaded_submission, SubmissionError, process_submission_text, - process_submission_xml, process_uploaded_submission, - process_and_validate_submission) from ietf.doc.factories import (DocumentFactory, WgDraftFactory, IndividualDraftFactory, ReviewFactory, WgRfcFactory) from ietf.doc.models import ( Document, DocEvent, State, @@ -49,7 +44,12 @@ from ietf.submit.forms import SubmissionBaseUploadForm, SubmissionAutoUploadForm from ietf.submit.models import Submission, Preapproval, SubmissionExtResource from ietf.submit.tasks import cancel_stale_submissions, process_and_accept_uploaded_submission_task -from ietf.submit.utils import apply_yang_checker_to_draft, run_all_yang_model_checks +from ietf.submit.utils import (expirable_submissions, expire_submission, find_submission_filenames, + post_submission, validate_submission_name, validate_submission_rev, + process_and_accept_uploaded_submission, SubmissionError, process_submission_text, + process_submission_xml, process_uploaded_submission, + process_and_validate_submission, apply_yang_checker_to_draft, + run_all_yang_model_checks) from ietf.utils import tool_version from ietf.utils.accesstoken import generate_access_token from ietf.utils.mail import outbox, get_payload_text @@ -3384,3 +3384,29 @@ def test_apply_yang_checker_to_draft(self): apply_yang_checker_to_draft(checker, draft) self.assertEqual(checker.check_file_txt.call_args, mock.call(draft.get_file_name())) + +@override_settings(IDSUBMIT_REPOSITORY_PATH="/some/path/", IDSUBMIT_STAGING_PATH="/some/other/path") +class SubmissionErrorTests(TestCase): + def test_sanitize_message(self): + sanitized = SubmissionError.sanitize_message( + "This refers to /some/path/with-a-file\n" + "and also /some/other/path/with-a-different-file isn't that neat?\n" + "and has /some/path//////with-slashes" + ) + self.assertEqual( + sanitized, + "This refers to **/with-a-file\n" + "and also **/with-a-different-file isn't that neat?\n" + "and has **/with-slashes" + ) + + @mock.patch.object(SubmissionError, "sanitize_message") + def test_submissionerror(self, mock_sanitize_message): + SubmissionError() + self.assertFalse(mock_sanitize_message.called) + SubmissionError("hi", "there") + self.assertTrue(mock_sanitize_message.called) + self.assertCountEqual( + mock_sanitize_message.call_args_list, + [mock.call("hi"), mock.call("there")], + ) diff --git a/ietf/submit/utils.py b/ietf/submit/utils.py index c814b84657..e6cbcb12f7 100644 --- a/ietf/submit/utils.py +++ b/ietf/submit/utils.py @@ -17,6 +17,7 @@ from shutil import move from typing import Optional, Union # pyflakes:ignore from unidecode import unidecode +from xml2rfc import RfcWriterError from xym import xym from django.conf import settings @@ -918,8 +919,51 @@ def accept_submission_requires_group_approval(submission): class SubmissionError(Exception): - """Exception for errors during submission processing""" - pass + """Exception for errors during submission processing + + Sanitizes paths appearing in exception messages. + """ + def __init__(self, *args): + if len(args) > 0: + args = (self.sanitize_message(arg) for arg in args) + super().__init__(*args) + + @staticmethod + def sanitize_message(msg): + # Paths likely to appear in submission-related errors + paths = [ + p for p in ( + getattr(settings, "ALL_ID_DOWNLOAD_DIR", None), + getattr(settings, "BIBXML_BASE_PATH", None), + getattr(settings, "DERIVED_DIR", None), + getattr(settings, "FTP_DIR", None), + getattr(settings, "IDSUBMIT_REPOSITORY_PATH", None), + getattr(settings, "IDSUBMIT_STAGING_PATH", None), + getattr(settings, "INTERNET_ALL_DRAFTS_ARCHIVE_DIR", None), + getattr(settings, "INTERNET_DRAFT_PATH", None), + getattr(settings, "INTERNET_DRAFT_ARCHIVE_DIR", None), + getattr(settings, "INTERNET_DRAFT_PDF_PATH", None), + getattr(settings, "RFC_PATH", None), + getattr(settings, "SUBMIT_YANG_CATALOG_MODEL_DIR", None), + getattr(settings, "SUBMIT_YANG_DRAFT_MODEL_DIR", None), + getattr(settings, "SUBMIT_YANG_IANA_MODEL_DIR", None), + getattr(settings, "SUBMIT_YANG_RFC_MODEL_DIR", None), + "/tmp/", + ) if p is not None + ] + return re.sub(fr"({'|'.join(paths)})/*", "**/", msg) + + +class XmlRfcError(SubmissionError): + """SubmissionError caused by xml2rfc + + Includes the output from xml2rfc, if any, in xml2rfc_stdout / xml2rfc_stderr + """ + def __init__(self, *args, xml2rfc_stdout: str, xml2rfc_stderr: str): + super().__init__(*args) + self.xml2rfc_stderr = xml2rfc_stderr + self.xml2rfc_stdout = xml2rfc_stdout + class InconsistentRevisionError(SubmissionError): """SubmissionError caused by an inconsistent revision""" @@ -937,27 +981,55 @@ def render_missing_formats(submission): If a txt file already exists, leaves it in place. Overwrites an existing html file if there is one. """ - xml2rfc.log.write_out = io.StringIO() # open(os.devnull, "w") - xml2rfc.log.write_err = io.StringIO() # open(os.devnull, "w") + # Capture stdio/stdout from xml2rfc + xml2rfc_stdout = io.StringIO() + xml2rfc_stderr = io.StringIO() + xml2rfc.log.write_out = xml2rfc_stdout + xml2rfc.log.write_err = xml2rfc_stderr xml_path = staging_path(submission.name, submission.rev, '.xml') parser = xml2rfc.XmlRfcParser(str(xml_path), quiet=True) - # --- Parse the xml --- - xmltree = parser.parse(remove_comments=False) + try: + # --- Parse the xml --- + xmltree = parser.parse(remove_comments=False) + except Exception as err: + raise XmlRfcError( + "Error parsing XML", + xml2rfc_stdout=xml2rfc_stdout.getvalue(), + xml2rfc_stderr=xml2rfc_stderr.getvalue(), + ) from err # If we have v2, run it through v2v3. Keep track of the submitted version, though. xmlroot = xmltree.getroot() xml_version = xmlroot.get('version', '2') if xml_version == '2': v2v3 = xml2rfc.V2v3XmlWriter(xmltree) - xmltree.tree = v2v3.convert2to3() + try: + xmltree.tree = v2v3.convert2to3() + except Exception as err: + raise XmlRfcError( + "Error converting v2 XML to v3", + xml2rfc_stdout=xml2rfc_stdout.getvalue(), + xml2rfc_stderr=xml2rfc_stderr.getvalue(), + ) from err # --- Prep the xml --- today = date_today() prep = xml2rfc.PrepToolWriter(xmltree, quiet=True, liberal=True, keep_pis=[xml2rfc.V3_PI_TARGET]) prep.options.accept_prepped = True prep.options.date = today - xmltree.tree = prep.prep() - if xmltree.tree == None: - raise SubmissionError(f'Error from xml2rfc (prep): {prep.errors}') + try: + xmltree.tree = prep.prep() + except RfcWriterError: + raise XmlRfcError( + f"Error during xml2rfc prep: {prep.errors}", + xml2rfc_stdout=xml2rfc_stdout.getvalue(), + xml2rfc_stderr=xml2rfc_stderr.getvalue(), + ) + except Exception as err: + raise XmlRfcError( + "Unexpected error during xml2rfc prep", + xml2rfc_stdout=xml2rfc_stdout.getvalue(), + xml2rfc_stderr=xml2rfc_stderr.getvalue(), + ) from err # --- Convert to txt --- txt_path = staging_path(submission.name, submission.rev, '.txt') @@ -965,7 +1037,14 @@ def render_missing_formats(submission): writer = xml2rfc.TextWriter(xmltree, quiet=True) writer.options.accept_prepped = True writer.options.date = today - writer.write(txt_path) + try: + writer.write(txt_path) + except Exception as err: + raise XmlRfcError( + "Error generating text format from XML", + xml2rfc_stdout=xml2rfc_stdout.getvalue(), + xml2rfc_stderr=xml2rfc_stderr.getvalue(), + ) from err log.log( 'In %s: xml2rfc %s generated %s from %s (version %s)' % ( str(xml_path.parent), @@ -980,7 +1059,14 @@ def render_missing_formats(submission): html_path = staging_path(submission.name, submission.rev, '.html') writer = xml2rfc.HtmlWriter(xmltree, quiet=True) writer.options.date = today - writer.write(str(html_path)) + try: + writer.write(str(html_path)) + except Exception as err: + raise XmlRfcError( + "Error generating HTML format from XML", + xml2rfc_stdout=xml2rfc_stdout.getvalue(), + xml2rfc_stderr=xml2rfc_stderr.getvalue(), + ) from err log.log( 'In %s: xml2rfc %s generated %s from %s (version %s)' % ( str(xml_path.parent), @@ -1263,7 +1349,7 @@ def process_submission_text(filename, revision): def process_and_validate_submission(submission): """Process and validate a submission - Raises SubmissionError if an error is encountered. + Raises SubmissionError or a subclass if an error is encountered. """ if len(set(submission.file_types.split(",")).intersection({".xml", ".txt"})) == 0: raise SubmissionError("Require XML and/or text format to process an Internet-Draft submission.") @@ -1273,7 +1359,16 @@ def process_and_validate_submission(submission): # Parse XML first, if we have it if ".xml" in submission.file_types: xml_metadata = process_submission_xml(submission.name, submission.rev) - render_missing_formats(submission) # makes HTML and text, unless text was uploaded + try: + render_missing_formats(submission) # makes HTML and text, unless text was uploaded + except XmlRfcError as err: + # log stdio/stderr + log.log( + f"xml2rfc failure when rendering missing formats for {submission.name}-{submission.rev}:\n" + f">> stdout:\n{err.xml2rfc_stdout}\n" + f">> stderr:\n{err.xml2rfc_stderr}" + ) + raise # Parse text, whether uploaded or generated from XML text_metadata = process_submission_text(submission.name, submission.rev) @@ -1332,11 +1427,11 @@ def process_and_validate_submission(submission): raise SubmissionError('Checks failed: ' + ' / '.join(errors)) except SubmissionError: raise # pass SubmissionErrors up the stack - except Exception: + except Exception as err: # convert other exceptions into SubmissionErrors log.log(f'Unexpected exception while processing submission {submission.pk}.') log.log(traceback.format_exc()) - raise SubmissionError('A system error occurred while processing the submission.') + raise SubmissionError('A system error occurred while processing the submission.') from err def submitter_is_author(submission): @@ -1428,6 +1523,7 @@ def process_uploaded_submission(submission): create_submission_event(None, submission, desc="Uploaded submission (diverted to manual process)") send_manual_post_request(None, submission, errors=dict(consistency=str(consistency_error))) except SubmissionError as err: + # something generic went wrong submission.refresh_from_db() # guard against incomplete changes in submission validation / processing cancel_submission(submission) # changes Submission.state create_submission_event(None, submission, f"Submission rejected: {err}") diff --git a/ietf/submit/views.py b/ietf/submit/views.py index 3f745741e4..f710bdeb0c 100644 --- a/ietf/submit/views.py +++ b/ietf/submit/views.py @@ -26,17 +26,47 @@ from ietf.ietfauth.utils import has_role, role_required from ietf.mailtrigger.utils import gather_address_lists from ietf.person.models import Email -from ietf.submit.forms import (SubmissionAutoUploadForm, AuthorForm, SubmitterForm, EditSubmissionForm, - PreapprovalForm, ReplacesForm, SubmissionManualUploadForm) +from ietf.submit.forms import ( + SubmissionAutoUploadForm, + AuthorForm, + SubmitterForm, + EditSubmissionForm, + PreapprovalForm, + ReplacesForm, + SubmissionManualUploadForm, + SubmissionSearchForm, +) from ietf.submit.mail import send_full_url, send_manual_post_request -from ietf.submit.models import (Submission, Preapproval, SubmissionExtResource, - DraftSubmissionStateName ) -from ietf.submit.tasks import process_uploaded_submission_task, process_and_accept_uploaded_submission_task, poke -from ietf.submit.utils import ( approvable_submissions_for_user, preapprovals_for_user, - recently_approved_by_user, validate_submission, create_submission_event, docevent_from_submission, - post_submission, cancel_submission, rename_submission_files, remove_submission_files, - get_submission, save_files, clear_existing_files, accept_submission, accept_submission_requires_group_approval, - accept_submission_requires_prev_auth_approval, update_submission_external_resources) +from ietf.submit.models import ( + Submission, + Preapproval, + SubmissionExtResource, + DraftSubmissionStateName, +) +from ietf.submit.tasks import ( + process_uploaded_submission_task, + process_and_accept_uploaded_submission_task, + poke, +) +from ietf.submit.utils import ( + approvable_submissions_for_user, + preapprovals_for_user, + recently_approved_by_user, + validate_submission, + create_submission_event, + docevent_from_submission, + post_submission, + cancel_submission, + rename_submission_files, + remove_submission_files, + get_submission, + save_files, + clear_existing_files, + accept_submission, + accept_submission_requires_group_approval, + accept_submission_requires_prev_auth_approval, + update_submission_external_resources, +) from ietf.stats.utils import clean_country_name from ietf.utils.accesstoken import generate_access_token from ietf.utils.log import log @@ -195,24 +225,33 @@ def api_submit_tombstone(request): def tool_instructions(request): return render(request, 'submit/tool_instructions.html', {'selected': 'instructions'}) + def search_submission(request): - error = None - name = None - if request.method == 'POST': - name = request.POST.get('name', '') - submission = Submission.objects.filter(name=name).order_by('-pk').first() - if submission: - return redirect(submission_status, submission_id=submission.pk) - else: - if re.search(r'-\d\d$', name): - submission = Submission.objects.filter(name=name[:-3]).order_by('-pk').first() - if submission: - return redirect(submission_status, submission_id=submission.pk) - error = 'No valid submission found for %s' % name - return render(request, 'submit/search_submission.html', - {'selected': 'status', - 'error': error, - 'name': name}) + if request.method == "POST": + form = SubmissionSearchForm(request.POST) + if form.is_valid(): + name = form.cleaned_data["name"] + submission = Submission.objects.filter(name=name).order_by("-pk").first() + if submission: + return redirect(submission_status, submission_id=submission.pk) + else: + if re.search(r"-\d\d$", name): + submission = ( + Submission.objects.filter(name=name[:-3]) + .order_by("-pk") + .first() + ) + if submission: + return redirect(submission_status, submission_id=submission.pk) + form.add_error(None, f"No valid submission found for {name}") + else: + form = SubmissionSearchForm() + return render( + request, + "submit/search_submission.html", + {"selected": "status", "form": form}, + ) + def can_edit_submission(user, submission, access_token): key_matched = access_token and submission.access_token() == access_token diff --git a/ietf/templates/submit/search_submission.html b/ietf/templates/submit/search_submission.html index 3f827abbea..807d6d7d86 100644 --- a/ietf/templates/submit/search_submission.html +++ b/ietf/templates/submit/search_submission.html @@ -12,14 +12,8 @@
{% csrf_token %}
- - + {% bootstrap_form form %}
- {% if error %} -

- {{ error }} -

- {% endif %}
{% endblock %} \ No newline at end of file diff --git a/ietf/utils/meetecho.py b/ietf/utils/meetecho.py index 2f5f146766..0dbf75736a 100644 --- a/ietf/utils/meetecho.py +++ b/ietf/utils/meetecho.py @@ -481,12 +481,10 @@ def delete_conference(self, conf: Conference): class SlidesManager(Manager): """Interface between Datatracker models and Meetecho API - Note: The URL we send comes from get_versionless_href(). This should match what we use as the - URL in api_get_session_materials(). Additionally, it _must_ give the right result for a Document - instance that has not yet been persisted to the database. This is because upload_session_slides() - (as of 2024-03-07) SessionPresentations before saving its updated Documents. This means, for - example, using get_absolute_url() will cause bugs. (We should refactor upload_session_slides() to - avoid this requirement.) + Note: the URL sent for a slide deck comes from DocumentInfo.get_href() and includes the revision + of the slides being sent. Be sure that 1) the URL matches what api_get_session_materials() returns + for the slides; and 2) the URL is valid if it is fetched immediately - possibly even before the call + to SlidesManager.add() or send_update() returns. """ def __init__(self, api_config): @@ -521,7 +519,7 @@ def add(self, session: "Session", slides: "Document", order: int): deck={ "id": slides.pk, "title": slides.title, - "url": slides.get_versionless_href(), # see above note re: get_versionless_href() + "url": slides.get_href(), "rev": slides.rev, "order": order, } @@ -575,7 +573,7 @@ def send_update(self, session: "Session"): { "id": deck.document.pk, "title": deck.document.title, - "url": deck.document.get_versionless_href(), # see note above re: get_versionless_href() + "url": deck.document.get_href(), "rev": deck.document.rev, "order": deck.order, } diff --git a/ietf/utils/tests_meetecho.py b/ietf/utils/tests_meetecho.py index 1aef5894e2..a10ac68c27 100644 --- a/ietf/utils/tests_meetecho.py +++ b/ietf/utils/tests_meetecho.py @@ -558,7 +558,7 @@ def test_add(self, mock_add, mock_wg_token): deck={ "id": slides_doc.pk, "title": slides_doc.title, - "url": slides_doc.get_versionless_href(), + "url": slides_doc.get_href(session.meeting), "rev": slides_doc.rev, "order": 13, }, @@ -597,7 +597,7 @@ def test_delete(self, mock_delete, mock_update, mock_wg_token): { "id": slides_doc.pk, "title": slides_doc.title, - "url": slides_doc.get_versionless_href(), + "url": slides_doc.get_href(session.meeting), "rev": slides_doc.rev, "order": 1, }, @@ -635,7 +635,7 @@ def test_revise(self, mock_add, mock_delete, mock_wg_token): deck={ "id": slides_doc.pk, "title": slides_doc.title, - "url": slides_doc.get_versionless_href(), + "url": slides_doc.get_href(slides.session.meeting), "rev": slides_doc.rev, "order": 23, }, @@ -660,7 +660,7 @@ def test_send_update(self, mock_send_update, mock_wg_token): { "id": slides.document_id, "title": slides.document.title, - "url": slides.document.get_versionless_href(), + "url": slides.document.get_href(slides.session.meeting), "rev": slides.document.rev, "order": 0, } diff --git a/k8s/auth.yaml b/k8s/auth.yaml index c92ed05163..2bdb064447 100644 --- a/k8s/auth.yaml +++ b/k8s/auth.yaml @@ -84,7 +84,7 @@ spec: mountPath: /etc/nginx/conf.d/00logging.conf subPath: nginx-logging.conf - name: dt-cfg - mountPath: /etc/nginx/conf.d/auth.conf + mountPath: /etc/nginx/conf.d/default.conf subPath: nginx-auth.conf # ----------------------------------------------------- # ScoutAPM Container diff --git a/k8s/datatracker.yaml b/k8s/datatracker.yaml index a8a9675687..3d9e86a29d 100644 --- a/k8s/datatracker.yaml +++ b/k8s/datatracker.yaml @@ -84,7 +84,8 @@ spec: mountPath: /etc/nginx/conf.d/00logging.conf subPath: nginx-logging.conf - name: dt-cfg - mountPath: /etc/nginx/conf.d/datatracker.conf + # Replaces the original default.conf + mountPath: /etc/nginx/conf.d/default.conf subPath: nginx-datatracker.conf # ----------------------------------------------------- # ScoutAPM Container diff --git a/playwright/tests/status/status.spec.js b/playwright/tests/status/status.spec.js index 7b3b90bfa4..daef7a88f1 100644 --- a/playwright/tests/status/status.spec.js +++ b/playwright/tests/status/status.spec.js @@ -20,6 +20,10 @@ test.describe('site status', () => { by: 'Exile is a cool Amiga game' } + test.beforeEach(({ browserName }) => { + test.skip(browserName === 'firefox', 'bypassing flaky tests on Firefox') + }) + test('Renders server status as Notification', async ({ page }) => { await page.route('/status/latest.json', route => { route.fulfill({