diff --git a/.dockerignore b/.dockerignore index 92f9f6820..eb2d768a1 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,14 +1,24 @@ +services/ +!/services/GlobalSign.crt + +build/ +dist/ +.cache/ + data* data/ *.sock -*.egg-info/, +*.egg-info/ */__pycache__/ .git/ */node_modules/ +*/.npm/ */build/ .dockerignore Dockerfile +*.yml + __pycache__ *.pyc *.pyo @@ -16,3 +26,6 @@ __pycache__ .cache *.log .git + +*/.venv +*/.cache \ No newline at end of file diff --git a/.envrc b/.envrc deleted file mode 100755 index 84d5e7f16..000000000 --- a/.envrc +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash -export LANG=en_US.utf8 -export LC_ALL=en_US.utf8 - -# Load secrets tokens (gitlab, jenkins) used to start the backend -if [[ -r $HOME/.tokens ]]; then - source $HOME/.tokens -fi - -# Enable building with SSH agent forwarding -export DOCKER_BUILDKIT=1 - -# Makes it easier to run the unit tests -export PYTHONPATH=$(pwd) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..78d3d077d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,32 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. Linux] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..bbcbbe7d6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 000000000..9f41eccdf --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,91 @@ +--- +# https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions +# https://help.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions +# https://help.github.com/en/actions/configuring-and-managing-workflows/authenticating-with-the-github_token +# https://hynek.me/articles/python-github-actions/ +# https://medium.com/swlh/fast-docker-build-in-kubernetes-f52088854f45 + +name: CI +on: [push, pull_request] + +env: + DOCKER_BUILDKIT: 1 + COMPOSE_DOCKER_CLI_BUILD: 1 + +jobs: + code-checks: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install .[dev] + - name: Lint with flake8 + run: | + # stop the build if there are Python syntax errors or undefined names + flake8 backend qaboard --count --select=E9,F63,F7,F82 --show-source --statistics + # --exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + # flake8 backend qaboard --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + - name: Type Checks with MyPy + run: mypy -p qaboard --ignore-missing-imports + - name: Unit Tests + run: | + # git is needed in the tests and setup to fail if not setup + git config --global user.name "Arthur Flam" + git config --global user.email "arthur.flam@samsung.com" + green -vvv --quiet-stdout + + build-webapp: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Use Node.js 18.x + uses: actions/setup-node@v4 + with: + node-version: 18.x + - uses: actions/cache@v4 + with: + path: ~/.npm + key: node-${{ hashFiles('webapp/npm-shrinkwrap.json') }} + restore-keys: ${{ runner.os }}-node- + - run: cd webapp && npm ci + - run: cd webapp && npm run build --if-present + # - run: cd webapp && npm test + + test-docker-images: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Build the docker compose stack + run: docker compose build --build-arg BUILDKIT_INLINE_CACHE=1 + - name: Up the docker compose stack + run: docker compose up -d + - name: Check running containers + run: docker ps -a + - name: Check logs + run: sleep 10 && docker compose logs + - name: Smoke Test @app + run: curl -s --retry 10 --retry-connrefused http://localhost:5151/ + - name: sleep 10 && Smoke Test @api + run: sleep 10 && curl -s --retry 10 --retry-connrefused http://localhost:5151/api/v1/projects + + publish-docker-images: + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/master' + needs: + - code-checks + - build-webapp + - test-docker-images + steps: + - uses: actions/checkout@v4 + - name: Build the docker compose stack + run: docker compose build --build-arg BUILDKIT_INLINE_CACHE=1 + - name: Publish images + run: | + echo ${{ secrets.GITHUB_TOKEN }} | docker login https://docker.pkg.github.com -u arthur-flam --password-stdin + docker compose push diff --git a/qaboard-backend/slamvizapp/api/__init__.py b/.github/workflows/publish.yaml similarity index 100% rename from qaboard-backend/slamvizapp/api/__init__.py rename to .github/workflows/publish.yaml diff --git a/.github/workflows/pypy.yml b/.github/workflows/pypy.yml new file mode 100644 index 000000000..b97b9e6c6 --- /dev/null +++ b/.github/workflows/pypy.yml @@ -0,0 +1,27 @@ +# https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries +name: Upload Python Package + +on: + release: + types: [created] + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install setuptools wheel twine + - name: Build and publish + env: + TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} + TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + run: | + python setup.py sdist bdist_wheel + twine upload dist/* diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml new file mode 100644 index 000000000..35af5cf34 --- /dev/null +++ b/.github/workflows/website.yml @@ -0,0 +1,38 @@ +name: Deploy to GitHub Pages + +on: + push: + branches: [master] + paths: [website/**] + +jobs: + deploy: + name: Deploy to GitHub Pages + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 18.x + cache: yarn + cache-dependency-path: website/yarn.lock + - name: Build website + working-directory: website + run: | + yarn install --frozen-lockfile + yarn build + + # Popular action to deploy to GitHub Pages: + # Docs: https://github.com/peaceiris/actions-gh-pages#%EF%B8%8F-docusaurus + - name: Deploy to GitHub Pages + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + # Build output to publish to the `gh-pages` branch: + publish_dir: ./website/build + # Assign commit authorship to the official GH-Actions bot for deploys to `gh-pages` branch: + # https://github.com/actions/checkout/issues/13#issuecomment-724415212 + # The GH actions bot is used by default if you didn't specify the two fields. + # You can swap them out with your own user credentials. + user_name: github-actions[bot] + user_email: 41898282+github-actions[bot]@users.noreply.github.com \ No newline at end of file diff --git a/.gitignore b/.gitignore index 193122f29..fb30e4e0e 100755 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,28 @@ +certs/ +saml/*.json +passwd* +*.bkp +.config/configstore/ +*.patch +.npm/ +migration/ +.npm/ +cache.*.json +user-quotas.json build/ build_qadocs/ log.lsf.txt log.txt ssl/ +commits/ +user/ +drafts/ + +backend/data/ + +image.batches.yaml +iter.batches.yaml +sub.batches.yaml *.png *.jpg @@ -10,6 +30,8 @@ ssl/ *.svg *.gif +# ssh-agent files to build container images +ssh-agent.pid # recommended by docusaurus # generated files @@ -24,6 +46,12 @@ npm-debug.log* yarn-debug.log* yarn-error.log* +.vscode + +# generated by tests. TODO: use /tmp +qaboard/sample_project/image.batches.yaml + +.DS_Store # Created by https://www.gitignore.io/api/linux,python,windows,sublimetext,visualstudio diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c258a4f35..f83681a95 100755 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,102 +1,123 @@ -# TODO: Replace with a different CI for github.com... +default: + tags: [lsf] + +variables: + SIRC_LSF_QUEUE: alg_isp_q + SIRC_USER: ispq + # https://docs.gitlab.com/ee/ci/yaml/#git-clean-flags + GIT_CLEAN_FLAGS: -ffdx --exclude=webapp/node_modules/ + GIT_SUBMODULE_STRATEGY: recursive + stages: - test - deploy # Internally - publish # To Pypi before_script: - - source .envrc + - source ./.envrc + # Use ssh-agent to build container images that access private repositories + - eval $(ssh-agent -s) + - ssh-add $HOME/.ssh/id_rsa + - echo "${SSH_AGENT_PID}" > ssh-agent.pid +after_script: + # don't leave ssh agents running + - if [ -f ssh-agent.pid ] ; then kill $(cat ssh-agent.pid) ; fi -unit:tests: + +unit-tests: stage: test + retry: 2 # solves frequent filesystem sync issues script: - - python -munittest + - green -vvv --quiet-stdout --run-coverage -cli:tests: +type-check: stage: test - retry: 2 # solves frequent filesystem sync issues script: - # TODO: turn into a standalone script / tests. - - cd qatools/sample_project - - python -mqatools --help - - mkdir -p cli_tests/dir; touch cli_tests/a.jpg; touch cli_tests/b.jpg; touch cli_tests/dir/c.jpg - - export QA_DATABASE=$(pwd) - - export QA_OFFLINE=true - - python -mqatools run -i cli_tests/a.jpg 'echo "{absolute_input_path} => {output_directory}"' - # expect 1 run - - python -mqatools --dryrun batch cli_tests - # expect 3 runs - - sed -i 's/# globs:/globs:/' qatools.yaml - - python -mqatools --dryrun batch cli_tests --list | jq '.' - - python -mqatools batch cli_tests 'echo "{absolute_input_path} => {output_directory}"' - - python -mqatools batch --runner=local cli_tests 'echo "{absolute_input_path} => {output_directory}"' - # other CLI tests - # - python -mqatools save-artifacts # Gitlab: 404: Project not found - - python -mqatools get commit_id + - mypy -p qaboard --ignore-missing-imports + +lint: + stage: test + script: + - flake8 backend qaboard --count --select=E9,F63,F7,F82 --show-source --statistics + deploy:qa: stage: deploy only: - master script: - - pip install . + - export PROXY_STUFF='--trusted-host pypi.org --trusted-host files.pythonhosted.org --trusted-host pypi.python.org' + - umask 022 + - pip install $PROXY_STUFF . +deploy:qa:windows: + stage: deploy + only: + - master + before_script: [] # Overrides the global before_script since it changes LD_LIBRARY_PATH which causes curl to fail + script: + - - 'curl --header "Content-Type: application/json" --request POST --data ''{"build_url": "http://jenmaster1:8080/job/ALGO/job/Install_qaboard_on_windows_nodes", "cause": "cde-python updated", "params": {}}'' http://qa/api/v1/jenkins/build/trigger/' publish:PyPi: stage: publish when: manual script: - - cd qatools - - python setup.py sdist bdist_wheel - - twine upload --verbose -u __token__ dist/* + - ssh arthurf@planet31 'cd qaboard; twine upload --verbose -u __token__ dist/*' -variables: - DOCKER_IMAGE: qaboard - # https://docs.gitlab.com/ee/ci/yaml/#git-clean-flags - GIT_CLEAN_FLAGS: -ffdx --exclude=qaboard-webapp/node_modules/ +#variables: +# # https://docs.gitlab.com/ee/ci/yaml/#git-clean-flags +# GIT_CLEAN_FLAGS: -ffdx --exclude=webapp/node_modules/ +# GIT_SUBMODULE_STRATEGY: recursive # CI for the web application and the backend # TODO: enable it, make it work... +# image: gitlab-srv.transchip.com:4567/common-infrastructure/qaboard # backend:tests: # stage: test # script: -# - cd qaboard-backend +# - cd backend # # we only check that the syntax is correct -# - pip install . +# - pip install --trusted-host pypi.org --trusted-host files.pythonhosted.org . # webapp:tests: # stage: test # script: -# - cd qaboard-webapp +# - cd webapp # - npm ci # # - npm test - -# we deploy to the "qa" host. -# Internally we need to deal with proxy issues, tcsh as default shell, etc... .deploy: &deploy stage: deploy - script: - - cd qaboard-webapp && ssh qa "bash -c 'cd $(pwd) && source ../.envrc && npm install --no-audit && GENERATE_SOURCEMAP=true npm run build'" && cd .. - - cd qaboard-webapp && rsync -r build/* /home/ispq/qaboard/webapp_builds && cd .. - - ssh qa "bash -c \"cd $(pwd) && source .envrc && docker build --ssh default -m 12g --tag $DOCKER_IMAGE:$CI_ENVIRONMENT_SLUG .\"" - - ssh qa "docker stop qaboard-$CI_ENVIRONMENT_SLUG; docker rm qaboard-$CI_ENVIRONMENT_SLUG" || true - - ssh qa "bash -c \"cd $(pwd) && source .envrc && CI_ENVIRONMENT_SLUG=$CI_ENVIRONMENT_SLUG $(pwd)/qaboard-backend/deployment/start-docker.sh\"" + variables: + DOCKER_HOST: alginfra1 + COMPOSE_PROJECT_NAME: $CI_ENVIRONMENT_SLUG + # script: + # - ssh $DOCKER_HOST "bash -c \"cd $(pwd) && source .envrc && ./at-sirc-before-up.py && docker compose -f docker-compose.yml -f production.yml -f sirc.yml up -d --build\"" + +# deploy:staging: +# <<: *deploy +# environment: +# name: staging +# url: http://qa:9000 +# only: +# - master +# Assuming you +# - cloned at /home/ispq/qabaord +# - filled .env with the jenkins/gitlab tokens +# - put the SSL keys in services/nginx/ssl/qa deploy:production: - <<: *deploy + # <<: *deploy + stage: deploy + variables: + DOCKER_HOST: qa + CI_ENVIRONMENT_SLUG: "" + script: + - ssh $DOCKER_HOST "bash -c \"cd /home/ispq/qaboard_prod && source .envrc && git reset --hard && git pull && ./at-sirc-before-up.py > /dev/null && docker compose -f docker-compose.yml -f production.yml -f sirc.yml up -d --build\"" environment: name: production url: https://qa only: - master when: manual - -deploy:staging: - <<: *deploy - environment: - name: staging - url: http://qa:9000 - only: - - master diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..0bae60561 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "third_party/cde-python"] + path = third_party/cde-python + url = git@gitlab-srv:cde/cde-python.git diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..7b03ef9e3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,117 @@ +# CLAUDE.md +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + + +## QA-Board Architecture + +QA-Board is an experiment tracking framework with advanced viewers for algorithm, ML, and performance engineers. It consists of: + +- **`qaboard/`** - Python CLI package that wraps user code and manages experiments +- **`backend/`** - Flask API with PostgreSQL database for managing runs and data +- **`webapp/`** - React.js frontend for visualizing and comparing results +- **`services/`** - Infrastructure services (nginx, databases, message queues) +- **`website/`** - Docusaurus documentation site + +## Development Commands + +### Environment Setup +```bash +# Start full development environment +docker compose -f docker-compose.yml -f development.yml up -d + +# At SIRC, run this first to mount important folders +./at-sirc-before-up.py + +# With SIRC-specific config +docker compose -f docker-compose.yml -f development.yml -f sirc.yml up -d +``` + +### Frontend Development (React) +```bash +cd webapp +npm install +npm start # Start dev server +npm run build # Production build +npm test # Run tests +``` + +### Backend Development (Python Flask) +```bash +cd backend +# Backend runs in Docker, see docker-compose.yml +``` + +### Documentation Site +```bash +cd website +yarn install +yarn start # Start Docusaurus dev server +yarn build # Build static site +``` + +### Python CLI Package +```bash +# Main qaboard package uses uv for dependencies +# Development dependencies include testing tools +uv sync --extra dev # Install with dev dependencies +``` + +## Testing + +### Python Tests +- **Framework**: `green` test runner (configured in pyproject.toml) +- **Backend tests**: `pytest` (in backend/pyproject.toml dev dependencies) +- **Type checking**: `mypy` +- **Linting**: `flake8` +- **Test files**: Located in `tests/` directory + +```bash +# Run Python tests (use green test runner) +green + +# Type checking +mypy qaboard/ + +# Linting +flake8 qaboard/ +``` + +### Frontend Tests +```bash +cd webapp +npm test # React test suite +``` + +## Key Technologies + +- **Backend**: Python 3.11+, Flask, PostgreSQL, SQLAlchemy, Celery, Redis +- **Frontend**: React 18, Redux, TypeScript, Blueprint UI, D3.js, Plotly.js +- **Infrastructure**: Docker Compose, nginx, RabbitMQ +- **CLI**: Python with Click framework +- **Package Management**: `uv` for Python, `npm` for JavaScript + +## Development Workflow + +1. **Environment**: Use Docker Compose for full stack development +2. **Database**: PostgreSQL with Alembic migrations +3. **Task Queue**: LSF or Celery with RabbitMQ for background jobs +4. **Image Serving**: Cantaloupe IIIF server for advanced image viewing +5. **Authentication**: Supports local, LDAP, and SAML authentication + +## Special Features + +- **LSF Integration**: High-performance computing cluster support for batch jobs +- **Advanced Visualizations**: Images, videos, plots, 3D point clouds, flame graphs +- **Git Integration**: Version control awareness and commit tracking +- **Parameter Tuning**: Built-in optimization workflows with scikit-optimize +- **Bit Accuracy Testing**: Automated regression testing for algorithm validation + +## Architecture Notes + +The system follows a microservices pattern with Docker containers. The CLI submits jobs and uploads results, the backend processes data and stores in PostgreSQL, and the frontend fetches data via REST API to render visualizations. Services provide infrastructure like reverse proxy, image serving, and task processing. + +Main entry points: +- CLI: `qa` command (from qaboard package) +- Backend API: Flask application served via uWSGI +- Frontend: React SPA served by nginx +- Documentation: Static Docusaurus site \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..02ec40665 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,40 @@ +# Contributing to QA-Board +👍🎉 First off, thanks for taking the time to contribute! 🎉👍 + +The following is a set of guidelines for contributing to QA-Board. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request. + +## Code of Conduct +This project and everyone participating in it is governed by the QA-Board [Code of Conduct](https://github.com/Samsung/qaboard/blob/master/CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to arthur.flam@samsung.com. + +## Questions? +If you've got questions about anything (setup, contributing...) or just want to chat with the developers, please feel free to [start a thread in our Spectrum community](https://spectrum.chat/qaboard)! You can also contact us [by mail](arthur.flam@samsung.com). + +## Code organization +Each section has its own README: +- [qaboard](qaboard/): python package (`qaboard`) for the `qa` CLI wrapper than runs your code +- [backend](backend/) exposes an HTTP API to manage runs. Built with python's `flask` and `postgreSQL` as database. +- [webapp](webapp/) is the web frontend that displays results, based on `reactjs`. +- [services](services/): the web application composes a reverse proxy (`nginx`), an image server ([`cantaloupe`](https://medusa-project.github.io/cantaloupe/)), etc. This folder stores all the relevant `Dockerfile`s and settings +- [website](website/) is the [QA-Board website](https://samsung.github.io/qaboard) and [docs](https://samsung.github.io/qaboard/docs) + +## Where to start +If you want to contribute to the project but do not know where to start, or what to work on, don't hesitate to chat with the maintainers. QA-Board has many parts and much can be improved. We'll do our best to find something that matches your experience and has a meaningful impact on the project. Before you work on a big feature, don't hesitate to open an issue and discuss it. + +**To start a dev server:** + +```bash +# check requirements in webapp/README.md +cd webapp +npm install + +# At SIRC we need to make sure important folders are mounted before starting containers... +./at-sirc-before-up.py + +# see more into in docker-compose.yml and backend/README.md +docker compose -f docker-compose.yml -f development.yml -f sirc.yml up -d +``` + +## Openness +Currently, we use internally at Samsung a private fork of QA-Board. The differences are very small, mainly having to do with hardcoded configuration and CI. Our goal to move to a process where we first contribute to the public repository, then merge back the changes. + +We want to develop QA-Board in the open, and started asking our users to submit issues on GitHub.com. We plan on using the public issue tracker to discuss the roadmap. \ No newline at end of file diff --git a/Dockerfile b/Dockerfile deleted file mode 100755 index 917f3a120..000000000 --- a/Dockerfile +++ /dev/null @@ -1,168 +0,0 @@ -# TODO: Use a lighter base image like alpine-linux -# Possibly let's do it when we split the application -# with docker-compose into database+backend+frontend+image-servers -FROM ubuntu:bionic -LABEL maintainer="arthurf.flam@samsung.com" - - - -ENV DEBIAN_FRONTEND noninteractive -RUN apt-get update && \ - echo exit 0 > /usr/sbin/policy-rc.d && \ - # Essential utilities - # Using --no-install-recommends doesn't work out-of-the-box, e.g. apt-key misses dirmngr later - apt-get install -y \ - sudo \ - wget curl sudo \ - software-properties-common build-essential \ - libc6-dev \ - python-dev && \ - # Useful utilities when debugging the container - apt-get install -y zsh htop tree less nano && \ - # Remove the cache - rm -rf /var/lib/apt/lists/* - - -# Trust various SSL certificates used by Samsung's IT -COPY qaboard-backend/deployment/DLP-TRITON.crt /usr/local/share/ca-certificates/samsung/DLP-TRITON.crt -COPY qaboard-backend/deployment/sirc-ca.cer /usr/local/share/ca-certificates/samsung/sirc-ca.cer -COPY qaboard-backend/deployment/sirc-ca.crt /usr/local/share/ca-certificates/samsung/sirc-ca.crt -RUN update-ca-certificates && \ - yes | dpkg-reconfigure ca-certificates -- - - -# Install git, up-to-date. -# Since the application manages a cache of all projects' repos, it is preferable. -# If we ran into scale issues, we could look into a service like Gitlab's gitaly. -# Note: if we didn't have proxy issues we would just -# add-apt-repository -y ppa:git-core/ppa -RUN echo "deb http://ppa.launchpad.net/git-core/ppa/ubuntu trusty main" >> /etc/apt/sources.list && \ - echo "deb-src http://ppa.launchpad.net/git-core/ppa/ubuntu trusty main" >> /etc/apt/sources.list && \ - apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys A1715D88E1DF1F24 && \ - apt-get update -qq && apt-get install -y git - - -# Install a complete Python environment -RUN wget --no-check-certificate https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh && \ - bash Miniconda3-latest-Linux-x86_64.sh -f -b -p /opt/anaconda3 -ENV PATH /opt/anaconda3/bin:${PATH} -# TODO: -# Ideally we should freeze dependencies, use requirement.txt/requirement.lock.txt, etc, but we there was no time to spend on this... -# To save build time dependencies are installed early in the dockerfile - now. -# We ran into issues with uwsgi segfaulting at runtime, issues with the pandas from pip... -# Some day we should clean this! -RUN conda install -k -c conda-forge libiconv -RUN conda install -k -c conda-forge uwsgi -RUN conda install -k pandas -RUN pip install --trusted-host pypi.org --trusted-host files.pythonhosted.org \ - pip pipenv \ - gitpython click flask flask_cors flask-admin sqlalchemy alembic sqlalchemy_utils ujson sklearn scikit-image scikit-learn scikit-optimize - - -# TODO: -# Projects can define their own iter_inputs() function to find inputs -# The use case it to connect to databases. However, at this stage the function is executed -# directly by the server. Not only is it unsecure (on our network let's say it's allright...), -# but it introduces a strong coupling between dependencies needed by projects and the server. -# Solutions could be: -# - [x] Short term, make those users call in a subprocess *their* python with dependencies, and read from STDOUT. -# we could read their projects's .envrc -# - [ ] Middle term, execute those functions in a docker container used by users to define their environment -# - [ ] The above makes things *slow* (?). What do we do? A sort of iter_input server? -# Limit the logic/connections available to users? -# For now, some projects need this to connect to MySQL: -RUN apt-get update -qq && apt-get install --no-install-recommends -y libssl-dev default-libmysqlclient-dev && \ - pip install --trusted-host pypi.org --trusted-host files.pythonhosted.org \ - # https://github.com/PyMySQL/mysqlclient-python - # https://github.com/ContinuumIO/anaconda-issues/issues/10646 - mysqlclient \ - # We still ran into issues with missing libs.. this is python only - PyMySQL[rsa] - - -# nginx as reverse proxy -RUN echo 'deb http://nginx.org/packages/ubuntu/ bionic nginx' > /etc/apt/sources.list.d/nginx.list && \ - echo 'deb-src http://nginx.org/packages/ubuntu/ bionic nginx' >> /etc/apt/sources.list.d/nginx.list && \ - apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys ABF5BD827BD9BF62 && \ - # nginx-extra instead of just -full or smaller for WebDav and DAV Ext - apt-get update -qq && apt-get install -y --no-install-recommends nginx-extras && \ - rm /etc/nginx/sites-enabled/default -EXPOSE 5000 80 443 - - -# PostgreSQL Database -# TODO: compare to the official dockerfile, even replace with it... -# https://github.com/docker-library/postgres/blob/f19a74ec301fe755b70a822f905c8f537f67bc9a/11/Dockerfile -RUN echo 'deb http://apt.postgresql.org/pub/repos/apt/ bionic-pgdg main' > /etc/apt/sources.list.d/pgdg.list && \ - wget --quiet --no-check-certificate -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - && \ - apt-get update -qq && apt-get install -y --no-install-recommends postgresql-10 postgresql-contrib-10 - # Allow connections from the outside world - with passwords -RUN echo "listen_addresses = '*'" >> /etc/postgresql/10/main/postgresql.conf && \ - echo "shared_preload_libraries = 'pg_stat_statements'" >> /etc/postgresql/10/main/postgresql.conf && \ - echo 'host all all ::/0 md5' >> /etc/postgresql/10/main/pg_hba.conf && \ - echo 'host all all 0.0.0.0/0 md5' >> /etc/postgresql/10/main/pg_hba.conf -USER postgres -RUN /etc/init.d/postgresql start && sleep 10 && psql --command "CREATE USER ci WITH SUPERUSER PASSWORD 'dvsdvs';" -USER root -VOLUME ["/etc/postgresql", "/var/log/postgresql", "/var/lib/postgresql"] -EXPOSE 5432 -# Solves issues when using old backups with an undefined uid. The postgres dockerfile fixes the uid in advance... -RUN groupadd -g 107 postgresold -# Python PostgreSQL driver -RUN apt-get install -y --no-install-recommends libpq-dev && \ - pg_config --version && \ - conda install -k -c conda-forge psycopg2 - - -# nodejs -RUN curl -ksL https://deb.nodesource.com/setup_10.x | \ - sed 's/wget -/wget --no-check-certificate -/g' | \ - sed 's/curl -/curl -k -/g' | \ - bash - && \ - apt-get install -y nodejs && \ - npm config set strict-ssl false && \ - npm config set cafile /usr/local/share/ca-certificates/samsung/DLP-TRITON.crt - - -# Frontend's dependencies -WORKDIR /qaboard/qaboard-webapp -COPY qaboard-webapp/package.json qaboard-webapp/npm-shrinkwrap.json ./ -## FIXME #################################### -# ENV NODE_ENV production -# # At the moment we don't build the app from the container because of frequent issues: -# # - ulimit would kick in (solvable via ENV AFAIK) -# # - network issues would cause always one of the 1000 dependencies to fail fetching -# # solvable via an internal pip proxy (e.g. artifactory) -# # As a user, you are expected to build it yourself with: -# # $ cd qaboard-webapp; npm ci; npm build -# # Then mount the build/ folder to /qaboard/qaboard-webapp/build -# We used to have things like -# RUN ulimit -n 2000 && npm ci -ddd # install exactly as in the lock-file (prefered...) -# RUN ulimit -n 2000 && npm install -ddd # install compatible dependencies -# RUN npm run build -COPY . /qaboard/ - - -# Backend API -ENV LANG 'C.UTF-8' -ENV LC_ALL 'C.UTF-8' -WORKDIR /qaboard -RUN pip install --trusted-host pypi.org --trusted-host files.pythonhosted.org --editable . ./qaboard-backend - -WORKDIR /qaboard -VOLUME /var/qaboard - - -# Some of our NFS mounts seem to use squash_root, eg /stage/algo_data -# It forces us to acces them with a regular SIRC user and dance around with sudo -# FIXME: use a different user, possibly use ARG/.env to parametrize -RUN useradd -u 11611 -g 10 arthurf --shell /bin/bash --no-create-home; \ - echo 'arthurf ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers -USER arthurf - - -# reverse proxy settings -COPY qaboard-backend/deployment/nginx/mime.types qaboard-backend/deployment/nginx/nginx.conf /etc/nginx/ -COPY qaboard-backend/deployment/nginx/conf.d/qaboard.conf /etc/nginx/conf.d/ - -CMD ["/qaboard/qaboard-backend/deployment/init.sh"] diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100755 index 1d3b31228..000000000 --- a/MANIFEST.in +++ /dev/null @@ -1,3 +0,0 @@ -recursive-include qatools/sample_project/qa * -include qatools/sample_project/qatools.yaml - diff --git a/MIGRATION.md b/MIGRATION.md new file mode 100644 index 000000000..3b489dc71 --- /dev/null +++ b/MIGRATION.md @@ -0,0 +1,61 @@ +# Migration Guide: Branch Unification + +This document covers breaking changes from the branch unification +(merging master-sirc, master-korea into a single master). + +## For SIRC Users + +### CLI +- After the regular install with: `pip install git+ssh://git@gitlab-srv/common-infrastructure/qaboard` +- Also install the site config: +```bash + pip install --upgrade "qaboard-site-sirc @ git+ssh://git@gitlab-srv/common-infrastructure/qaboard#subdirectory=deployments/sirc/cli" +``` +- This auto-configures API URL (https://qa), port (5000), secrets path (! we dont support it anymore as part of the project config) +- All existing ENV var overrides continue to work + +### Server +- Deploy with: `docker compose -f docker-compose.yml -f production.yml -f deployments/sirc/sirc.yml -f deployments/sirc/prod.yml up up` + +### Docker Builds +Proxy/cert configuration is no longer hardcoded in Dockerfiles. Instead, `sirc.yml` +passes build args automatically. **No action needed** — just rebuild as usual with +the SIRC overlay: +```bash +docker compose -f docker-compose.yml -f deployments/sirc/sirc.yml build +``` +The overlay provides `PROXY_URL`, `CA_CERT_URL`, `NO_PROXY`, `GIT_SSL_VERIFY`, +`NODE_TLS_REJECT_UNAUTHORIZED`, and `QABOARD_EXTRA=sirc` as build args. + +If you have custom certs in `services/cantaloupe/cert/`, those files are now +gitignored (only `.gitkeep` is tracked). Copy your certs back after cloning. + +### Breaking Changes +- `--lsf-threads` renamed to `--lsf-max-threads` (already done on master-sirc) + +## For DSK Users + +### CLI +- Install with: `pip install qaboard[dsk]` (was: `pip install qaboard`) +- This auto-configures API URL (https://qaboard.samsungds.net) + +### Server +- Deploy with: `docker compose -f docker-compose.yml -f production.yml -f deployments/dsk/dsk.yml up` + +### Docker Builds +Proxy/cert values are no longer hardcoded. Set `PROXY_URL`, `CA_CERT_URL`, and +`NO_PROXY` in your `.env` file or override them in `deployments/dsk/dsk.yml`. + +### Breaking Changes +- DB migration required: `is_ldap`/`is_sso` booleans -> `login_type` string field + Run: `alembic upgrade head` +- `--lsf-threads` renamed to `--lsf-max-threads` + +## For Open-Source Users + +No breaking changes. The default behavior is unchanged. +New features available: LDAP/SAML auth, LSF/celery runners, multiple +image servers -- all opt-in via ENV vars. + +All Dockerfiles now build cleanly with no build args (proxy/cert blocks +are skipped when args are empty). diff --git a/README.md b/README.md index b91b4779c..b7e9eb48e 100755 --- a/README.md +++ b/README.md @@ -1,54 +1,68 @@ -# QA-Board -**QA-Board** helps Algorithms/QA engineers build great products with powerful *quality evaluation* and *collaboration* tools. +
+
-> QA-Board is not released yet, and likely won't work *yet* for you because it expects running on our infra...
->
-> We're working on it! **Status: https://github.com/Samsung/qaboard/issues/1**
+
The artifacts folder does not exist.
+ {ci_commit.repo_artifacts_dir}
+
For tuning to work, you can manually call
+
+ git checkout {commit_id}
+ # build whatever is needed
+ qa save-artifacts
+
+ Normally it is done by the CI, but maybe you only worked on this commit locally, or something deleted the folder...
+ """ + else: + commit_batches_paths = get_commit_batches_paths(ci_commit) + if not commit_batches_paths: + message = f""" +Could not load the inputs.batches files defined in qaboard.yaml.
+ {ci_commit.repo_artifacts_dir}
+
For tuning to work, you can manually call
+
+ git checkout {commit_id}
+ # build whatever is needed
+ qa save-artifacts
+
+
+ Normally it is done by the CI, but maybe you only worked on this commit locally, or something deleted the folder...
+ """ + batches_paths = [*commit_batches_paths, *batches_paths] + else: + qatools_config = project.data.get("qatools_config", {}) + + + has_custom_iter_inputs = False + # TODO: make it more robust in case of "from iters import *" + qatools_config['project']['entrypoint'] = ci_commit.repo_artifacts_dir / qatools_config['project']['entrypoint'] + if qatools_config['project']['entrypoint'].exists(): + with qatools_config['project']['entrypoint'].open() as f: + entrypoint_source = f.read() + has_custom_iter_inputs = re.search(r'^\s*(def iter_inputs\(|from .* import.* iter_inputs)', entrypoint_source, re.MULTILINE) + # project fallback? + if has_custom_iter_inputs: + cwd = ci_commit.artifacts_dir + parent_including_cwd = [*list(reversed(list(cwd.parents))), cwd] + envrcs = [f'source "{p}/.envrc"\n' for p in parent_including_cwd if (p / '.envrc').exists()] + cmd = ' '.join([ + 'qa', + 'batch', + *list(itertools.chain.from_iterable((('--batches-file', f'"{f}"') for f in batches_paths))), + '--list', + request.args["name"], + ]) + cmd = '\n'.join([*envrcs, cmd]) + print(cmd) + try: + process = subprocess.run( + ['bash', '-c', cmd], + cwd=cwd, + encoding="utf-8", + capture_output=True, + ) + # print(cmd) + # print(process.stdout) + print(process.stderr) + process.check_returncode() + except: + return jsonify({"error": str(process.stdout), "cmd": str(cmd)}), 500 + return jsonify({"tests": json.loads(process.stdout), "message": message}) + + # We don't need to seperate the two cases, but + # doing so might let us avoid a fork and qa startup... + # like in qaboard/config.py + config_inputs = qatools_config.get('inputs', {}) + config_inputs_types = config_inputs.get('types', {}) + default_input_type = config_inputs_types.get('default', 'default') + from qaboard.conventions import get_settings + input_settings = get_settings(default_input_type, qatools_config) + # like in qaboard/qa.py + from qaboard.config import get_default_configuration, get_default_database + default_configuration = get_default_configuration(input_settings) + default_configurations = deserialize_config(default_configuration) + default_database = get_default_database(input_settings) + print('group', request.args["name"], batches_paths) + try: + tests = list( + iter_inputs( + [request.args["name"]], # batches + batches_paths, # batches_files, + default_database, # database + default_configurations, # default_configuration + 'linux', # platform + {"type": 'lsf'}, # default_job_configuration + qatools_config, + default_inputs_settings=input_settings, + ) + ) + return jsonify({ + "tests": [{"input_path": str(run_context.rel_input_path), "configurations": run_context.configurations} for run_context in tests], + "message": message, + }) + except Exception as e: + print(f'Error: {e}') + return jsonify({"tests": [], "error": str(e)}) + + +def _generate_batch_script(ci_commit, user, working_directory, command_id, batch_command, data): + """Generate the qa_batch.sh script (shared across all runners).""" + parent_including_cwd = [*list(reversed(list(working_directory.parents))), working_directory] + envrcs = [f'source "{p}/.envrc"\n' for p in parent_including_cwd if (p / '.envrc').exists()] + + default_user = os.environ.get('QABOARD_DEFAULT_USER', 'qaboard') + outputs_dir_prefix = str(ci_commit.outputs_dir).replace(f'/outputs/{default_user}/', f'/outputs/{user}/') + script = "".join([ + "#!/bin/bash\n", + 'export LC_ALL=en_US.utf8;\n', + 'export LANG=en_US.utf8;\n\n', + 'export MPLBACKEND=agg;\n', + ('\n'.join(envrcs) + '\n') if envrcs else "", + "set -xe\n\n", + f'cd "{working_directory}";\n\n', + f"\nexport CI=true;\n", + f"\nexport GIT_COMMIT='{ci_commit.hexsha}';\n", + f"export QABOARD_TUNING=true;\n\n", + f"export QA_OUTPUTS_COMMIT='{outputs_dir_prefix}';\n\n", + f"export QATOOLS_CI_COMMIT_DIR='{ci_commit.outputs_dir}';\n\n", + f"export QA_BATCH_COMMAND_ID='{command_id}';\n\n", + f"{batch_command};\n\n", + ]) + return script + + +def _dispatch_local(qa_batch_path, batch_dir): + """Run batch script locally via subprocess.""" + cmd = ['bash', '-c', f'bash "{qa_batch_path}" &>> "{batch_dir}/log.txt"'] + print(cmd) + out = subprocess.run(cmd, encoding='utf-8', stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + out.check_returncode() + + +def _dispatch_celery(qa_batch_path, batch_dir): + """Run batch script via celery worker. Injects broker URL into script.""" + broker_url = os.environ.get('CELERY_BROKER_URL', 'pyamqp://guest:guest@qaboard:5672//') + qaboard_host = os.environ.get('QABOARD_HOST', 'localhost') + qaboard_protocol = os.environ.get('QABOARD_PROTOCOL', 'http') + + celery_env = "".join([ + f"export QABOARD_PROTOCOL={qaboard_protocol}\n", + f"export QABOARD_HOST={qaboard_host}\n", + f"export CELERY_BROKER_URL={broker_url}\n", + f"export no_proxy={qaboard_host},proxy,rabbitmq,qaboard\n", + ]) + with qa_batch_path.open("r") as f: + content = f.read() + content = content.replace("#!/bin/bash\n", f"#!/bin/bash\n{celery_env}", 1) + with qa_batch_path.open("w") as f: + f.write(content) + + cmd = ['bash', '-c', f'bash "{qa_batch_path}" &>> "{batch_dir}/log.txt"'] + print(cmd) + out = subprocess.run(cmd, encoding='utf-8', stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + out.check_returncode() + + +def _dispatch_lsf(qa_batch_path, batch_dir, user, ci_commit, do_optimize): + """Run batch script via LSF job submission (SSH + bsub).""" + # TODO: We use a bridge server to submit - ideally we should use + # some LSF API to do it, but their docs/auth are terrible. + qatools_config = ci_commit.project.data.get("qatools_config", {}) + lsf_config = qatools_config.get('runners', qatools_config).get("lsf", {}) + default_queue = lsf_config.get('queue', 'default') + queue = lsf_config.get('long_queue', 'default') if do_optimize else default_queue + # TODO: this is SIRC-specific to switch user - would need a better solution + # for LSF but also for other runners... + bsub = "bsub" if os.environ.get("QABOARD_DEFAULT_USER") != "ispq" else f'bsub_su "{user}"' + start_script = "\n".join([ + "#!/bin/bash", + "set -xe", + "", + f'mkdir -p "{batch_dir}"', + f'{bsub} -q "{queue}" -o "{batch_dir}/log.lsf.txt" -sp 4000 ' + f"'bash \"{qa_batch_path}\" &>> \"{batch_dir}/log.txt\"'", + ]) + print(start_script) + + start_path = batch_dir / "start.sh" + with start_path.open("w") as f: + f.write(start_script) + + lsf_bridge = os.environ.get('QA_RUNNERS_LSF_BRIDGE', '') + if lsf_bridge: + cmd = lsf_bridge.replace('{command}', f'bash "{start_path}"') + else: + cmd = " ".join([ + "LC_ALL=en_US.utf8 LANG=en_US.utf8", + "ssh", "-q", "-tt", + "-o StrictHostKeyChecking=no", + os.environ.get('QA_LSF_SSH_TARGET', 'localhost'), + f'\'bash "{start_path}"\'', + ]) + print(cmd) + out = subprocess.run(cmd, shell=True, encoding="utf-8", stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + out.check_returncode() + + +@app.route("/api/v1/commit/Could not load the inputs.batches files defined in qatools.yaml.
For tuning to work, qa save-artifacts needs to be called.