├── .envrc ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── backend ├── Cargo.lock ├── Cargo.toml ├── builder │ ├── Cargo.lock │ ├── Cargo.toml │ └── src │ │ ├── evaluator.rs │ │ ├── lib.rs │ │ └── scheduler.rs ├── cache │ ├── Cargo.toml │ └── src │ │ ├── cacher.rs │ │ └── lib.rs ├── core │ ├── Cargo.toml │ └── src │ │ ├── consts.rs │ │ ├── database.rs │ │ ├── executer.rs │ │ ├── input.rs │ │ ├── lib.rs │ │ ├── permission.rs │ │ ├── sources.rs │ │ └── types.rs ├── entity │ ├── Cargo.toml │ └── src │ │ ├── api.rs │ │ ├── build.rs │ │ ├── build_dependency.rs │ │ ├── build_feature.rs │ │ ├── build_output.rs │ │ ├── build_output_signature.rs │ │ ├── cache.rs │ │ ├── commit.rs │ │ ├── evaluation.rs │ │ ├── feature.rs │ │ ├── lib.rs │ │ ├── organization.rs │ │ ├── organization_cache.rs │ │ ├── organization_user.rs │ │ ├── project.rs │ │ ├── role.rs │ │ ├── server.rs │ │ ├── server_architecture.rs │ │ ├── server_feature.rs │ │ ├── tests │ │ ├── mod.rs │ │ └── test_entity.rs │ │ └── user.rs ├── migration │ ├── Cargo.lock │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── lib.rs │ │ ├── m20241107_135027_create_table_user.rs │ │ ├── m20241107_135442_create_table_organization.rs │ │ ├── m20241107_135941_create_table_project.rs │ │ ├── m20241107_140540_create_table_feature.rs │ │ ├── m20241107_140545_create_table_commit.rs │ │ ├── m20241107_140550_create_table_server.rs │ │ ├── m20241107_140560_create_table_build.rs │ │ ├── m20241107_140600_create_table_evaluation.rs │ │ ├── m20241107_155000_create_table_build_depencdency.rs │ │ ├── m20241107_155020_create_table_api.rs │ │ ├── m20241107_156000_create_table_build_feature.rs │ │ ├── m20241107_156100_create_table_server_feature.rs │ │ ├── m20241107_156110_create_table_server_architecture.rs │ │ ├── m20241107_156120_create_table_cache.rs │ │ ├── m20241107_156130_create_table_organization_cache.rs │ │ ├── m20241107_156140_create_table_role.rs │ │ ├── m20241107_156150_create_table_organization_user.rs │ │ ├── m20241107_156160_create_table_build_output.rs │ │ ├── m20241107_156170_create_table_build_output_signature.rs │ │ └── main.rs ├── src │ └── main.rs └── web │ ├── Cargo.toml │ └── src │ ├── authorization.rs │ ├── endpoints │ ├── auth.rs │ ├── builds.rs │ ├── caches.rs │ ├── commits.rs │ ├── evals.rs │ ├── mod.rs │ ├── orgs.rs │ ├── projects.rs │ ├── servers.rs │ └── user.rs │ └── lib.rs ├── cli ├── Cargo.lock ├── Cargo.toml ├── connector │ ├── Cargo.toml │ └── src │ │ ├── auth.rs │ │ ├── builds.rs │ │ ├── caches.rs │ │ ├── commits.rs │ │ ├── evals.rs │ │ ├── lib.rs │ │ ├── orgs.rs │ │ ├── projects.rs │ │ ├── servers.rs │ │ └── user.rs └── src │ ├── commands │ ├── base.rs │ ├── build.rs │ ├── cache.rs │ ├── mod.rs │ ├── organization.rs │ ├── project.rs │ └── server.rs │ ├── config.rs │ ├── input.rs │ └── main.rs ├── docs ├── gradient-api.yaml └── gradient.png ├── flake.lock ├── flake.nix ├── frontend ├── dashboard │ ├── __init__.py │ ├── api.py │ ├── apps.py │ ├── auth.py │ ├── context_processors.py │ ├── forms.py │ ├── migrations │ │ └── __init__.py │ ├── models.py │ ├── static │ │ └── dashboard │ │ │ ├── css │ │ │ └── styles.css │ │ │ ├── icons │ │ │ ├── activity.svg │ │ │ ├── arrow_back.svg │ │ │ ├── command.svg │ │ │ └── download.svg │ │ │ ├── images │ │ │ ├── check-circle.svg │ │ │ ├── circle.svg │ │ │ ├── favicon.png │ │ │ ├── more-vertical.svg │ │ │ ├── pb.png │ │ │ ├── pb2.png │ │ │ ├── wavelens-LOGO.svg │ │ │ └── x-circle.svg │ │ │ └── js │ │ │ ├── log.js │ │ │ └── main.js │ ├── templates │ │ ├── backHeader.html │ │ ├── dashboard │ │ │ ├── download.html │ │ │ ├── home.html │ │ │ ├── index.html │ │ │ ├── log.html │ │ │ ├── model.html │ │ │ ├── newOrganization.html │ │ │ ├── newProject.html │ │ │ ├── newServer.html │ │ │ └── overview.html │ │ ├── header.html │ │ ├── individualHeader.html │ │ ├── login.html │ │ └── register.html │ ├── tests.py │ ├── urls.py │ └── views.py ├── frontend │ ├── __init__.py │ ├── asgi.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── locale │ └── en │ │ └── LC_MESSAGES │ │ └── django.po └── manage.py ├── nix ├── modules │ ├── gradient-frontend.nix │ └── gradient.nix ├── packages │ ├── gradient-cli.nix │ ├── gradient-frontend.nix │ └── gradient-server.nix ├── scripts │ └── postgres.nix ├── tests │ ├── default.nix │ ├── gradient │ │ ├── api │ │ │ ├── default.nix │ │ │ └── test.py │ │ └── building │ │ │ ├── default.nix │ │ │ └── flake_repository.nix │ └── modules │ │ └── debug-host.nix └── vm │ ├── README.md │ ├── base.nix │ ├── defaults.nix │ ├── mDNS.nix │ ├── monitoring │ ├── destination │ │ └── grafana │ │ │ ├── dashboards │ │ │ └── example.json │ │ │ └── default.nix │ └── source │ │ ├── loki │ │ ├── default.nix │ │ └── loki.yaml │ │ └── prometheus │ │ └── default.nix │ ├── nginx │ ├── default.nix │ └── grafana.nix │ └── postgresql.nix └── start-db.sh /.envrc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if type -P lorri &>/dev/null; then 4 | eval "$(lorri direnv)" 5 | else 6 | echo 'while direnv evaluated .envrc, could not find the command "lorri" [https://github.com/nix-community/lorri]' 7 | use flake 8 | fi 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ---> PostgreSQL 2 | testing/ 3 | 4 | # ---> Python 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | backend/nars/ 33 | 34 | # PyInstaller 35 | # Usually these files are written by a python script from a template 36 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 37 | *.manifest 38 | *.spec 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .nox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *.cover 54 | *.py,cover 55 | .hypothesis/ 56 | .pytest_cache/ 57 | cover/ 58 | 59 | # Translations 60 | *.mo 61 | *.pot 62 | 63 | # Django stuff: 64 | *.log 65 | local_settings.py 66 | db.sqlite3 67 | db.sqlite3-journal 68 | 69 | # Flask stuff: 70 | instance/ 71 | .webassets-cache 72 | 73 | # Scrapy stuff: 74 | .scrapy 75 | 76 | # Sphinx documentation 77 | docs/_build/ 78 | 79 | # PyBuilder 80 | .pybuilder/ 81 | target/ 82 | 83 | # Jupyter Notebook 84 | .ipynb_checkpoints 85 | 86 | # IPython 87 | profile_default/ 88 | ipython_config.py 89 | 90 | # pyenv 91 | # For a library or package, you might want to ignore these files since the code is 92 | # intended to run in multiple environments; otherwise, check them in: 93 | # .python-version 94 | 95 | # pipenv 96 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 97 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 98 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 99 | # install all needed dependencies. 100 | #Pipfile.lock 101 | 102 | # poetry 103 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 104 | # This is especially recommended for binary packages to ensure reproducibility, and is more 105 | # commonly ignored for libraries. 106 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 107 | #poetry.lock 108 | 109 | # pdm 110 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 111 | #pdm.lock 112 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 113 | # in version control. 114 | # https://pdm.fming.dev/#use-with-ide 115 | .pdm.toml 116 | 117 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 118 | __pypackages__/ 119 | 120 | # Celery stuff 121 | celerybeat-schedule 122 | celerybeat.pid 123 | 124 | # SageMath parsed files 125 | *.sage.py 126 | 127 | # Environments 128 | .env 129 | .venv 130 | env/ 131 | venv/ 132 | ENV/ 133 | env.bak/ 134 | venv.bak/ 135 | 136 | # Spyder project settings 137 | .spyderproject 138 | .spyproject 139 | 140 | # Rope project settings 141 | .ropeproject 142 | 143 | # mkdocs documentation 144 | /site 145 | 146 | # mypy 147 | .mypy_cache/ 148 | .dmypy.json 149 | dmypy.json 150 | 151 | # Pyre type checker 152 | .pyre/ 153 | 154 | # pytype static type analyzer 155 | .pytype/ 156 | 157 | # Cython debug symbols 158 | cython_debug/ 159 | 160 | # PyCharm 161 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 162 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 163 | # and can be added to the global gitignore or merged into this file. For a more nuclear 164 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 165 | #.idea/ 166 | 167 | # ---> Rust 168 | # Generated by Cargo 169 | # will have compiled files and executables 170 | debug/ 171 | target/ 172 | backend/debug/ 173 | backend/target/ 174 | 175 | # These are backup files generated by rustfmt 176 | **/*.rs.bk 177 | 178 | # MSVC Windows builds of rustc generate these, which store debugging information 179 | *.pdb 180 | 181 | # ---> Nix 182 | # Ignore build outputs from performing a nix-build or `nix build` command 183 | result 184 | result-* 185 | 186 | .direnv/ 187 | .vscode/ 188 | 189 | # nix run stuff 190 | gradient-persist.img 191 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | education, socio-economic status, nationality, personal appearance, race, 10 | religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at {{ email }}. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to the Project 2 | 3 | We welcome contributions to this project. Please respect the code of conduct. 4 | When contributing to this repository, please first discuss the change you wish to make via issue or any other method with the maintainers of this repository before making a change. 5 | 6 | ## Licensing 7 | 8 | This project is licensed under the Affero General Public License (AGPL) version 3.0. By contributing to this project, you agree that your contributions will be licensed under the AGPL-3.0. 9 | 10 | ## Code of Conduct 11 | 12 | We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. 13 | We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. 14 | 15 | You can read the full [Code of Conduct](./CODE_OF_CONDUCT.md) for more information. 16 | -------------------------------------------------------------------------------- /backend/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gradient-server" 3 | version = "0.2.0" 4 | edition = "2024" 5 | license = "AGPL-3.0" 6 | authors = ["Wavelens UG "] 7 | description = "Nix-based Continuous Integration System" 8 | repository = "https://github.com/wavelens/gradient" 9 | 10 | [workspace] 11 | members = [".", "core", "builder", "web", "entity", "migration"] 12 | 13 | [dependencies] 14 | builder = { path = "builder" } 15 | cache = { path = "cache" } 16 | clap = { version = "4.5", features = ["derive"] } 17 | core = { path = "core" } 18 | sentry = "0.37" 19 | tokio = { version = "1.44", features = ["process", "rt-multi-thread"] } 20 | web = { path = "web" } 21 | -------------------------------------------------------------------------------- /backend/builder/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "builder" 3 | version = "0.2.0" 4 | edition = "2024" 5 | publish = false 6 | license = "AGPL-3.0" 7 | authors = ["Wavelens UG "] 8 | 9 | [lib] 10 | name = "builder" 11 | path = "src/lib.rs" 12 | 13 | [dependencies] 14 | chrono = "0.4" 15 | core = { path = "../core" } 16 | entity = { path = "../entity" } 17 | futures = "0.3" 18 | nix-daemon = { git = "https://github.com/wavelens/nix-daemon", tag = "v0.1.2" } 19 | sea-orm = { version = "1.1", features = ["json-array", "mock", "postgres-array", "runtime-tokio", "sqlx-postgres", "with-uuid"] } 20 | serde_json = "1.0" 21 | tokio = { version = "1.44", features = ["process", "rt-multi-thread"] } 22 | uuid = { version = "1.16", features = ["fast-rng", "macro-diagnostics", "v4"] } 23 | async-trait = "0.1" 24 | -------------------------------------------------------------------------------- /backend/builder/src/lib.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | pub mod evaluator; 8 | pub mod scheduler; 9 | 10 | use core::types::ServerState; 11 | use std::sync::Arc; 12 | 13 | pub async fn start_builder(state: Arc) -> std::io::Result<()> { 14 | tokio::spawn(scheduler::schedule_evaluation_loop(Arc::clone(&state))); 15 | tokio::spawn(scheduler::schedule_build_loop(Arc::clone(&state))); 16 | Ok(()) 17 | } 18 | -------------------------------------------------------------------------------- /backend/cache/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cache" 3 | version = "0.2.0" 4 | edition = "2024" 5 | publish = false 6 | license = "AGPL-3.0" 7 | authors = ["Wavelens UG "] 8 | 9 | [lib] 10 | name = "cache" 11 | path = "src/lib.rs" 12 | 13 | [dependencies] 14 | chrono = "0.4" 15 | core = { path = "../core" } 16 | entity = { path = "../entity" } 17 | sea-orm = { version = "1.1", features = ["json-array", "mock", "postgres-array", "runtime-tokio", "sqlx-postgres", "with-uuid"] } 18 | serde_json = "1.0" 19 | tokio = { version = "1.44", features = ["process", "rt-multi-thread"] } 20 | uuid = { version = "1.16", features = ["fast-rng", "macro-diagnostics", "v4"] } 21 | async-trait = "0.1" 22 | -------------------------------------------------------------------------------- /backend/cache/src/lib.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | pub mod cacher; 8 | 9 | use core::types::ServerState; 10 | use std::sync::Arc; 11 | 12 | pub async fn start_cache(state: Arc) -> std::io::Result<()> { 13 | tokio::spawn(cacher::cache_loop(Arc::clone(&state))); 14 | Ok(()) 15 | } 16 | -------------------------------------------------------------------------------- /backend/core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "core" 3 | version = "0.2.0" 4 | edition = "2024" 5 | publish = false 6 | license = "AGPL-3.0" 7 | authors = ["Wavelens UG "] 8 | 9 | [lib] 10 | name = "core" 11 | path = "src/lib.rs" 12 | 13 | [dependencies] 14 | async-ssh2-lite = { version = "0.5", features = ["tokio"] } 15 | async-stream = "0.3" 16 | base64 = "0.22" 17 | chrono = "0.4" 18 | clap = { version = "4.5", features = ["derive", "env"] } 19 | crypter = "0.2" 20 | entity = { path = "../entity" } 21 | futures = "0.3" 22 | git-url-parse = "0.4" 23 | migration = { path = "../migration" } 24 | nix-daemon = { git = "https://github.com/wavelens/nix-daemon", tag = "v0.1.2" } 25 | rand = "0.8" 26 | sea-orm = { version = "1.1", features = ["json-array", "mock", "postgres-array", "runtime-tokio", "sqlx-postgres", "with-uuid"] } 27 | sea-orm-migration = { version = "1.1", features = ["with-uuid", "with-chrono", "with-json", "sqlx-postgres", "sea-orm-cli", "runtime-tokio"] } 28 | serde = { version = "1", features = ["derive"] } 29 | ssh-key = { version = "0.6", features = ["ed25519"] } 30 | tokio = { version = "1.44", features = ["process", "rt-multi-thread"] } 31 | uuid = { version = "1.16", features = ["fast-rng", "macro-diagnostics", "v4"] } 32 | ed25519-compact = { version = "2.1", features = ["random"] } 33 | -------------------------------------------------------------------------------- /backend/core/src/consts.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | use chrono::{DateTime, NaiveDateTime}; 8 | use std::ops::RangeInclusive; 9 | use std::sync::LazyLock; 10 | use uuid::{uuid, Uuid}; 11 | 12 | pub const PORT_RANGE: RangeInclusive = 1..=65535; 13 | 14 | pub static NULL_TIME: LazyLock = 15 | LazyLock::new(|| DateTime::from_timestamp(0, 0).unwrap().naive_utc()); 16 | 17 | pub const FLAKE_START: [&str; 7] = [ 18 | "checks", 19 | "packages", 20 | "formatter", 21 | "legacyPackages", 22 | "nixosConfigurations", 23 | "devShells", 24 | "hydraJobs", 25 | ]; 26 | 27 | pub const BASE_ROLE_ADMIN_ID: Uuid = uuid!("00000000-0000-0000-0000-000000000001"); 28 | pub const BASE_ROLE_WRITE_ID: Uuid = uuid!("00000000-0000-0000-0000-000000000002"); 29 | pub const BASE_ROLE_VIEW_ID: Uuid = uuid!("00000000-0000-0000-0000-000000000003"); 30 | -------------------------------------------------------------------------------- /backend/core/src/lib.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | pub mod consts; 8 | pub mod database; 9 | pub mod executer; 10 | pub mod input; 11 | pub mod permission; 12 | pub mod sources; 13 | pub mod types; 14 | 15 | use clap::Parser; 16 | use database::connect_db; 17 | use std::sync::Arc; 18 | use types::*; 19 | 20 | pub async fn init_state() -> Arc { 21 | let cli = Cli::parse(); 22 | 23 | println!("Starting Gradient Server on {}:{}", cli.ip, cli.port); 24 | 25 | let db = connect_db(&cli).await; 26 | 27 | Arc::new(ServerState { db, cli }) 28 | } 29 | -------------------------------------------------------------------------------- /backend/core/src/permission.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | use sea_orm::{ 8 | ActiveModelTrait, ActiveValue::Set, ColumnTrait, Condition, EntityTrait, IntoActiveModel, 9 | QueryFilter, 10 | }; 11 | use std::sync::Arc; 12 | 13 | use super::types::*; 14 | 15 | #[derive(Copy, Clone, Debug)] 16 | pub enum Permission { 17 | View = 0, 18 | Edit = 1, 19 | } 20 | 21 | fn get_permission_bit(permissions: i64, permission: Permission) -> bool { 22 | permissions & (1 << permission as i64) != 0 23 | } 24 | 25 | fn set_permission_bit(permissions: i64, permission: Permission, value: bool) -> i64 { 26 | if value { 27 | permissions | (1 << permission as i64) 28 | } else { 29 | permissions & !(1 << permission as i64) 30 | } 31 | } 32 | 33 | pub async fn set_permission( 34 | state: Arc, 35 | role: MRole, 36 | permission: Permission, 37 | value: bool, 38 | ) { 39 | if get_permission_bit(role.permission, permission) == value { 40 | return; 41 | } 42 | 43 | let mut arole = role.clone().into_active_model(); 44 | arole.permission = Set(set_permission_bit(role.permission, permission, value)); 45 | arole.save(&state.db).await.unwrap(); 46 | } 47 | 48 | pub async fn get_permission( 49 | state: Arc, 50 | organization: MOrganization, 51 | user: MUser, 52 | permission: Permission, 53 | ) -> bool { 54 | let organization_user = EOrganizationUser::find() 55 | .filter( 56 | Condition::all() 57 | .add(COrganizationUser::Organization.eq(organization.id)) 58 | .add(COrganizationUser::User.eq(user.id)), 59 | ) 60 | .one(&state.db) 61 | .await 62 | .unwrap() 63 | .unwrap(); 64 | 65 | let role = ERole::find_by_id(organization_user.role) 66 | .one(&state.db) 67 | .await 68 | .unwrap() 69 | .unwrap(); 70 | 71 | get_permission_bit(role.permission, permission) 72 | } 73 | -------------------------------------------------------------------------------- /backend/entity/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "entity" 3 | version = "0.2.0" 4 | edition = "2024" 5 | publish = false 6 | license = "AGPL-3.0" 7 | authors = ["Wavelens UG "] 8 | 9 | [lib] 10 | name = "entity" 11 | path = "src/lib.rs" 12 | 13 | [dependencies] 14 | chrono = "0.4" 15 | serde = { version = "1", features = ["derive"] } 16 | uuid = { version = "1.16", features = ["fast-rng", "macro-diagnostics", "v4"] } 17 | 18 | [dependencies.sea-orm] 19 | version = "1.1" 20 | features = ["json-array", "mock", "postgres-array", "runtime-tokio", "sqlx-postgres", "with-uuid"] 21 | -------------------------------------------------------------------------------- /backend/entity/src/api.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | use chrono::NaiveDateTime; 8 | use sea_orm::entity::prelude::*; 9 | use serde::{Deserialize, Serialize}; 10 | use uuid::Uuid; 11 | 12 | #[derive(Clone, PartialEq, DeriveEntityModel, Deserialize, Serialize)] 13 | #[sea_orm(table_name = "api")] 14 | pub struct Model { 15 | #[sea_orm(primary_key)] 16 | pub id: Uuid, 17 | pub owned_by: Uuid, 18 | pub name: String, 19 | pub key: String, 20 | pub last_used_at: NaiveDateTime, 21 | pub created_at: NaiveDateTime, 22 | } 23 | 24 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] 25 | pub enum Relation { 26 | #[sea_orm( 27 | belongs_to = "super::user::Entity", 28 | from = "Column::OwnedBy", 29 | to = "super::user::Column::Id" 30 | )] 31 | OwnedBy, 32 | } 33 | 34 | impl std::fmt::Debug for Model { 35 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 36 | f.debug_struct("User") 37 | .field("id", &self.id) 38 | .field("owned_by", &self.owned_by) 39 | .field("name", &self.name) 40 | .field("key", &"[redacted]") 41 | .field("last_used_at", &self.last_used_at) 42 | .field("created_at", &self.created_at) 43 | .finish() 44 | } 45 | } 46 | 47 | impl ActiveModelBehavior for ActiveModel {} 48 | -------------------------------------------------------------------------------- /backend/entity/src/build.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | use chrono::NaiveDateTime; 8 | use sea_orm::entity::prelude::*; 9 | use serde::{Deserialize, Serialize}; 10 | use uuid::Uuid; 11 | 12 | #[derive(Debug, Clone, PartialEq, Eq, DeriveActiveEnum, EnumIter, Deserialize, Serialize)] 13 | #[sea_orm(rs_type = "i32", db_type = "Integer")] 14 | pub enum BuildStatus { 15 | #[sea_orm(num_value = 0)] 16 | Created, 17 | #[sea_orm(num_value = 1)] 18 | Queued, 19 | #[sea_orm(num_value = 2)] 20 | Building, 21 | #[sea_orm(num_value = 3)] 22 | Completed, 23 | #[sea_orm(num_value = 4)] 24 | Failed, 25 | #[sea_orm(num_value = 5)] 26 | Aborted, 27 | } 28 | 29 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Deserialize, Serialize)] 30 | #[sea_orm(table_name = "build")] 31 | pub struct Model { 32 | #[sea_orm(primary_key)] 33 | pub id: Uuid, 34 | pub evaluation: Uuid, 35 | pub status: BuildStatus, 36 | pub derivation_path: String, 37 | pub architecture: super::server::Architecture, 38 | pub server: Option, 39 | #[sea_orm(column_type = "Text")] 40 | pub log: Option, 41 | pub created_at: NaiveDateTime, 42 | pub updated_at: NaiveDateTime, 43 | } 44 | 45 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] 46 | pub enum Relation { 47 | #[sea_orm( 48 | belongs_to = "super::evaluation::Entity", 49 | from = "Column::Evaluation", 50 | to = "super::evaluation::Column::Id" 51 | )] 52 | Evaluation, 53 | #[sea_orm( 54 | belongs_to = "super::server::Entity", 55 | from = "Column::Server", 56 | to = "super::server::Column::Id" 57 | )] 58 | Server, 59 | } 60 | 61 | impl Related for Entity { 62 | fn to() -> RelationDef { 63 | super::build_dependency::Relation::Dependency.def() 64 | } 65 | 66 | fn via() -> Option { 67 | Some(super::build_dependency::Relation::Build.def().rev()) 68 | } 69 | } 70 | 71 | impl ActiveModelBehavior for ActiveModel {} 72 | -------------------------------------------------------------------------------- /backend/entity/src/build_dependency.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | use sea_orm::entity::prelude::*; 8 | use serde::{Deserialize, Serialize}; 9 | use uuid::Uuid; 10 | 11 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Deserialize, Serialize)] 12 | #[sea_orm(table_name = "build_dependency")] 13 | pub struct Model { 14 | #[sea_orm(primary_key)] 15 | pub id: Uuid, 16 | pub build: Uuid, 17 | pub dependency: Uuid, 18 | } 19 | 20 | #[derive(Copy, Clone, Debug, EnumIter)] 21 | pub enum Relation { 22 | Build, 23 | Dependency, 24 | } 25 | 26 | impl RelationTrait for Relation { 27 | fn def(&self) -> RelationDef { 28 | match self { 29 | Self::Build => Entity::belongs_to(super::build::Entity) 30 | .from(Column::Build) 31 | .to(super::build::Column::Id) 32 | .into(), 33 | Self::Dependency => Entity::belongs_to(super::build::Entity) 34 | .from(Column::Dependency) 35 | .to(super::build::Column::Id) 36 | .into(), 37 | } 38 | } 39 | } 40 | 41 | impl ActiveModelBehavior for ActiveModel {} 42 | -------------------------------------------------------------------------------- /backend/entity/src/build_feature.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | use sea_orm::entity::prelude::*; 8 | use serde::{Deserialize, Serialize}; 9 | use uuid::Uuid; 10 | 11 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Deserialize, Serialize)] 12 | #[sea_orm(table_name = "build_feature")] 13 | pub struct Model { 14 | #[sea_orm(primary_key)] 15 | pub id: Uuid, 16 | pub build: Uuid, 17 | pub feature: Uuid, 18 | } 19 | 20 | #[derive(Copy, Clone, Debug, EnumIter)] 21 | pub enum Relation { 22 | Build, 23 | Feature, 24 | } 25 | 26 | impl RelationTrait for Relation { 27 | fn def(&self) -> RelationDef { 28 | match self { 29 | Self::Build => Entity::belongs_to(super::build::Entity) 30 | .from(Column::Build) 31 | .to(super::build::Column::Id) 32 | .into(), 33 | Self::Feature => Entity::belongs_to(super::feature::Entity) 34 | .from(Column::Feature) 35 | .to(super::feature::Column::Id) 36 | .into(), 37 | } 38 | } 39 | } 40 | 41 | impl ActiveModelBehavior for ActiveModel {} 42 | -------------------------------------------------------------------------------- /backend/entity/src/build_output.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | use chrono::NaiveDateTime; 8 | use sea_orm::entity::prelude::*; 9 | use serde::{Deserialize, Serialize}; 10 | use uuid::Uuid; 11 | 12 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Deserialize, Serialize)] 13 | #[sea_orm(table_name = "build_output")] 14 | pub struct Model { 15 | #[sea_orm(primary_key)] 16 | pub id: Uuid, 17 | pub build: Uuid, 18 | pub output: String, 19 | pub hash: String, 20 | pub package: String, 21 | pub file_hash: Option, 22 | pub file_size: Option, 23 | pub is_cached: bool, 24 | pub ca: Option, 25 | pub created_at: NaiveDateTime, 26 | } 27 | 28 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] 29 | pub enum Relation { 30 | #[sea_orm( 31 | belongs_to = "super::build::Entity", 32 | from = "Column::Build", 33 | to = "super::build::Column::Id" 34 | )] 35 | Build, 36 | } 37 | 38 | impl ActiveModelBehavior for ActiveModel {} 39 | -------------------------------------------------------------------------------- /backend/entity/src/build_output_signature.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | use chrono::NaiveDateTime; 8 | use sea_orm::entity::prelude::*; 9 | use serde::{Deserialize, Serialize}; 10 | use uuid::Uuid; 11 | 12 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Deserialize, Serialize)] 13 | #[sea_orm(table_name = "build_output_signature")] 14 | pub struct Model { 15 | #[sea_orm(primary_key)] 16 | pub id: Uuid, 17 | pub build_output: Uuid, 18 | pub cache: Uuid, 19 | pub signature: String, 20 | pub created_at: NaiveDateTime, 21 | } 22 | 23 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] 24 | pub enum Relation { 25 | #[sea_orm( 26 | belongs_to = "super::build_output::Entity", 27 | from = "Column::BuildOutput", 28 | to = "super::build_output::Column::Id" 29 | )] 30 | BuildOutput, 31 | #[sea_orm( 32 | belongs_to = "super::cache::Entity", 33 | from = "Column::Cache", 34 | to = "super::cache::Column::Id" 35 | )] 36 | Cache, 37 | } 38 | 39 | impl ActiveModelBehavior for ActiveModel {} 40 | -------------------------------------------------------------------------------- /backend/entity/src/cache.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | use chrono::NaiveDateTime; 8 | use sea_orm::entity::prelude::*; 9 | use serde::{Deserialize, Serialize}; 10 | use uuid::Uuid; 11 | 12 | #[derive(Clone, PartialEq, DeriveEntityModel, Deserialize, Serialize)] 13 | #[sea_orm(table_name = "cache")] 14 | pub struct Model { 15 | #[sea_orm(primary_key)] 16 | pub id: Uuid, 17 | #[sea_orm(unique, indexed)] 18 | pub name: String, 19 | pub display_name: String, 20 | #[sea_orm(column_type = "Text")] 21 | pub description: String, 22 | pub active: bool, 23 | pub priority: i32, 24 | pub signing_key: String, 25 | pub created_by: Uuid, 26 | pub created_at: NaiveDateTime, 27 | } 28 | 29 | impl std::fmt::Debug for Model { 30 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 31 | f.debug_struct("Cache") 32 | .field("id", &self.id) 33 | .field("name", &self.name) 34 | .field("display_name", &self.display_name) 35 | .field("description", &self.description) 36 | .field("active", &self.active) 37 | .field("priority", &self.priority) 38 | .field("signing_key", &"[redacted]") 39 | .field("created_by", &self.created_by) 40 | .field("created_at", &self.created_at) 41 | .finish() 42 | } 43 | } 44 | 45 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] 46 | pub enum Relation { 47 | #[sea_orm( 48 | belongs_to = "super::user::Entity", 49 | from = "Column::CreatedBy", 50 | to = "super::user::Column::Id" 51 | )] 52 | CreatedBy, 53 | } 54 | 55 | impl ActiveModelBehavior for ActiveModel {} 56 | -------------------------------------------------------------------------------- /backend/entity/src/commit.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | use sea_orm::entity::prelude::*; 8 | use serde::{Deserialize, Serialize}; 9 | use uuid::Uuid; 10 | 11 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Deserialize, Serialize)] 12 | #[sea_orm(table_name = "commit")] 13 | pub struct Model { 14 | #[sea_orm(primary_key)] 15 | pub id: Uuid, 16 | pub message: String, 17 | pub hash: Vec, 18 | pub author: Option, 19 | pub author_name: String, 20 | } 21 | 22 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] 23 | pub enum Relation {} 24 | 25 | impl ActiveModelBehavior for ActiveModel {} 26 | -------------------------------------------------------------------------------- /backend/entity/src/evaluation.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | use chrono::NaiveDateTime; 8 | use sea_orm::entity::prelude::*; 9 | use serde::{Deserialize, Serialize}; 10 | use uuid::Uuid; 11 | 12 | #[derive(Debug, Clone, PartialEq, Eq, DeriveActiveEnum, EnumIter, Deserialize, Serialize)] 13 | #[sea_orm(rs_type = "i32", db_type = "Integer")] 14 | pub enum EvaluationStatus { 15 | #[sea_orm(num_value = 0)] 16 | Queued, 17 | #[sea_orm(num_value = 1)] 18 | Evaluating, 19 | #[sea_orm(num_value = 2)] 20 | Building, 21 | #[sea_orm(num_value = 3)] 22 | Completed, 23 | #[sea_orm(num_value = 4)] 24 | Failed, 25 | #[sea_orm(num_value = 5)] 26 | Aborted, 27 | } 28 | 29 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Deserialize, Serialize)] 30 | #[sea_orm(table_name = "evaluation")] 31 | pub struct Model { 32 | #[sea_orm(primary_key)] 33 | pub id: Uuid, 34 | pub project: Uuid, 35 | pub repository: String, 36 | pub commit: Uuid, 37 | pub wildcard: String, 38 | pub status: EvaluationStatus, 39 | pub previous: Option, 40 | pub next: Option, 41 | pub created_at: NaiveDateTime, 42 | } 43 | 44 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] 45 | pub enum Relation { 46 | #[sea_orm( 47 | belongs_to = "super::project::Entity", 48 | from = "Column::Project", 49 | to = "super::project::Column::Id" 50 | )] 51 | Project, 52 | #[sea_orm( 53 | belongs_to = "super::commit::Entity", 54 | from = "Column::Commit", 55 | to = "super::commit::Column::Id" 56 | )] 57 | Commit, 58 | #[sea_orm( 59 | belongs_to = "super::evaluation::Entity", 60 | from = "Column::Previous", 61 | to = "super::evaluation::Column::Id" 62 | )] 63 | PreviousEvaluation, 64 | #[sea_orm( 65 | belongs_to = "super::evaluation::Entity", 66 | from = "Column::Next", 67 | to = "super::evaluation::Column::Id" 68 | )] 69 | NextEvaluation, 70 | } 71 | 72 | impl ActiveModelBehavior for ActiveModel {} 73 | -------------------------------------------------------------------------------- /backend/entity/src/feature.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | use sea_orm::entity::prelude::*; 8 | use serde::{Deserialize, Serialize}; 9 | use uuid::Uuid; 10 | 11 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Deserialize, Serialize)] 12 | #[sea_orm(table_name = "feature")] 13 | pub struct Model { 14 | #[sea_orm(primary_key)] 15 | pub id: Uuid, 16 | pub name: String, 17 | } 18 | 19 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] 20 | pub enum Relation {} 21 | 22 | impl ActiveModelBehavior for ActiveModel {} 23 | -------------------------------------------------------------------------------- /backend/entity/src/lib.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | pub mod tests; 8 | 9 | pub mod api; 10 | pub mod build; 11 | pub mod build_dependency; 12 | pub mod build_feature; 13 | pub mod build_output; 14 | pub mod build_output_signature; 15 | pub mod cache; 16 | pub mod commit; 17 | pub mod evaluation; 18 | pub mod feature; 19 | pub mod organization; 20 | pub mod organization_cache; 21 | pub mod organization_user; 22 | pub mod project; 23 | pub mod role; 24 | pub mod server; 25 | pub mod server_architecture; 26 | pub mod server_feature; 27 | pub mod user; 28 | -------------------------------------------------------------------------------- /backend/entity/src/organization.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | use chrono::NaiveDateTime; 8 | use sea_orm::entity::prelude::*; 9 | use serde::{Deserialize, Serialize}; 10 | use uuid::Uuid; 11 | 12 | #[derive(Clone, PartialEq, DeriveEntityModel, Deserialize, Serialize)] 13 | #[sea_orm(table_name = "organization")] 14 | pub struct Model { 15 | #[sea_orm(primary_key)] 16 | pub id: Uuid, 17 | #[sea_orm(unique, indexed)] 18 | pub name: String, 19 | pub display_name: String, 20 | #[sea_orm(column_type = "Text")] 21 | pub description: String, 22 | pub public_key: String, 23 | pub private_key: String, 24 | pub use_nix_store: bool, 25 | pub created_by: Uuid, 26 | pub created_at: NaiveDateTime, 27 | } 28 | 29 | impl std::fmt::Debug for Model { 30 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 31 | f.debug_struct("Organization") 32 | .field("id", &self.id) 33 | .field("name", &self.name) 34 | .field("display_name", &self.display_name) 35 | .field("description", &self.description) 36 | .field("public_key", &format!("{} {}", self.public_key, self.id)) 37 | .field("private_key", &"[redacted]") 38 | .field("use_nix_store", &self.use_nix_store) 39 | .field("created_by", &self.created_by) 40 | .field("created_at", &self.created_at) 41 | .finish() 42 | } 43 | } 44 | 45 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] 46 | pub enum Relation { 47 | #[sea_orm( 48 | belongs_to = "super::user::Entity", 49 | from = "Column::CreatedBy", 50 | to = "super::user::Column::Id" 51 | )] 52 | CreatedBy, 53 | } 54 | 55 | impl ActiveModelBehavior for ActiveModel {} 56 | -------------------------------------------------------------------------------- /backend/entity/src/organization_cache.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | use sea_orm::entity::prelude::*; 8 | use serde::{Deserialize, Serialize}; 9 | use uuid::Uuid; 10 | 11 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Deserialize, Serialize)] 12 | #[sea_orm(table_name = "organization_cache")] 13 | pub struct Model { 14 | #[sea_orm(primary_key)] 15 | pub id: Uuid, 16 | pub organization: Uuid, 17 | pub cache: Uuid, 18 | } 19 | 20 | #[derive(Copy, Clone, Debug, EnumIter)] 21 | pub enum Relation { 22 | Organization, 23 | Cache, 24 | } 25 | 26 | impl RelationTrait for Relation { 27 | fn def(&self) -> RelationDef { 28 | match self { 29 | Self::Organization => Entity::belongs_to(super::organization::Entity) 30 | .from(Column::Organization) 31 | .to(super::organization::Column::Id) 32 | .into(), 33 | Self::Cache => Entity::belongs_to(super::cache::Entity) 34 | .from(Column::Cache) 35 | .to(super::cache::Column::Id) 36 | .into(), 37 | } 38 | } 39 | } 40 | 41 | impl ActiveModelBehavior for ActiveModel {} 42 | -------------------------------------------------------------------------------- /backend/entity/src/organization_user.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | use sea_orm::entity::prelude::*; 8 | use serde::{Deserialize, Serialize}; 9 | use uuid::Uuid; 10 | 11 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Deserialize, Serialize)] 12 | #[sea_orm(table_name = "organization_user")] 13 | pub struct Model { 14 | #[sea_orm(primary_key)] 15 | pub id: Uuid, 16 | pub organization: Uuid, 17 | pub user: Uuid, 18 | pub role: Uuid, 19 | } 20 | 21 | #[derive(Copy, Clone, Debug, EnumIter)] 22 | pub enum Relation { 23 | Organization, 24 | User, 25 | Role, 26 | } 27 | 28 | impl RelationTrait for Relation { 29 | fn def(&self) -> RelationDef { 30 | match self { 31 | Self::Organization => Entity::belongs_to(super::organization::Entity) 32 | .from(Column::Organization) 33 | .to(super::organization::Column::Id) 34 | .into(), 35 | Self::User => Entity::belongs_to(super::user::Entity) 36 | .from(Column::User) 37 | .to(super::user::Column::Id) 38 | .into(), 39 | Self::Role => Entity::belongs_to(super::role::Entity) 40 | .from(Column::Role) 41 | .to(super::role::Column::Id) 42 | .into(), 43 | } 44 | } 45 | } 46 | 47 | impl ActiveModelBehavior for ActiveModel {} 48 | -------------------------------------------------------------------------------- /backend/entity/src/project.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | use chrono::NaiveDateTime; 8 | use sea_orm::entity::prelude::*; 9 | use serde::{Deserialize, Serialize}; 10 | use uuid::Uuid; 11 | 12 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Deserialize, Serialize)] 13 | #[sea_orm(table_name = "project")] 14 | pub struct Model { 15 | #[sea_orm(primary_key)] 16 | pub id: Uuid, 17 | pub organization: Uuid, 18 | #[sea_orm(indexed)] 19 | pub name: String, 20 | pub active: bool, 21 | pub display_name: String, 22 | #[sea_orm(column_type = "Text")] 23 | pub description: String, 24 | pub repository: String, 25 | pub evaluation_wildcard: String, 26 | pub last_evaluation: Option, 27 | pub last_check_at: NaiveDateTime, 28 | pub force_evaluation: bool, 29 | pub created_by: Uuid, 30 | pub created_at: NaiveDateTime, 31 | } 32 | 33 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] 34 | pub enum Relation { 35 | #[sea_orm( 36 | belongs_to = "super::organization::Entity", 37 | from = "Column::Organization", 38 | to = "super::organization::Column::Id" 39 | )] 40 | Organization, 41 | #[sea_orm( 42 | belongs_to = "super::user::Entity", 43 | from = "Column::CreatedBy", 44 | to = "super::user::Column::Id" 45 | )] 46 | CreatedBy, 47 | #[sea_orm( 48 | belongs_to = "super::evaluation::Entity", 49 | from = "Column::LastEvaluation", 50 | to = "super::evaluation::Column::Id" 51 | )] 52 | LastEvaluation, 53 | } 54 | 55 | impl ActiveModelBehavior for ActiveModel {} 56 | -------------------------------------------------------------------------------- /backend/entity/src/role.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | use sea_orm::entity::prelude::*; 8 | use serde::{Deserialize, Serialize}; 9 | use uuid::Uuid; 10 | 11 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Deserialize, Serialize)] 12 | #[sea_orm(table_name = "role")] 13 | pub struct Model { 14 | #[sea_orm(primary_key)] 15 | pub id: Uuid, 16 | #[sea_orm(indexed)] 17 | pub name: String, 18 | pub organization: Option, 19 | pub permission: i64, 20 | } 21 | 22 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] 23 | pub enum Relation { 24 | #[sea_orm( 25 | belongs_to = "super::organization::Entity", 26 | from = "Column::Organization", 27 | to = "super::organization::Column::Id" 28 | )] 29 | Organization, 30 | } 31 | 32 | impl ActiveModelBehavior for ActiveModel {} 33 | -------------------------------------------------------------------------------- /backend/entity/src/server.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | use chrono::NaiveDateTime; 8 | use sea_orm::entity::prelude::*; 9 | use serde::{Deserialize, Serialize}; 10 | use uuid::Uuid; 11 | 12 | #[derive(Debug, Clone, PartialEq, Eq, DeriveActiveEnum, EnumIter, Deserialize, Serialize)] 13 | #[sea_orm(rs_type = "i16", db_type = "Integer")] 14 | pub enum Architecture { 15 | #[sea_orm(num_value = 0)] 16 | X86_64Linux, 17 | #[sea_orm(num_value = 1)] 18 | Aarch64Linux, 19 | #[sea_orm(num_value = 2)] 20 | X86_64Darwin, 21 | #[sea_orm(num_value = 3)] 22 | Aarch64Darwin, 23 | } 24 | 25 | impl std::str::FromStr for Architecture { 26 | type Err = String; 27 | 28 | fn from_str(s: &str) -> Result { 29 | match s { 30 | "x86_64-linux" => Ok(Architecture::X86_64Linux), 31 | "aarch64-linux" => Ok(Architecture::Aarch64Linux), 32 | "x86_64-darwin" => Ok(Architecture::X86_64Darwin), 33 | "aarch64-darwin" => Ok(Architecture::Aarch64Darwin), 34 | _ => Err(format!("Unknown architecture: {}", s)), 35 | } 36 | } 37 | } 38 | 39 | impl std::convert::TryFrom<&str> for Architecture { 40 | type Error = String; 41 | 42 | fn try_from(s: &str) -> Result { 43 | match s { 44 | "x86_64-linux" => Ok(Architecture::X86_64Linux), 45 | "aarch64-linux" => Ok(Architecture::Aarch64Linux), 46 | "x86_64-darwin" => Ok(Architecture::X86_64Darwin), 47 | "aarch64-darwin" => Ok(Architecture::Aarch64Darwin), 48 | _ => Err(format!("Unknown architecture: {}", s)), 49 | } 50 | } 51 | } 52 | 53 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Deserialize, Serialize)] 54 | #[sea_orm(table_name = "server")] 55 | pub struct Model { 56 | #[sea_orm(primary_key)] 57 | pub id: Uuid, 58 | #[sea_orm(indexed)] 59 | pub name: String, 60 | pub display_name: String, 61 | pub organization: Uuid, 62 | pub active: bool, 63 | pub host: String, 64 | pub port: i32, 65 | pub username: String, 66 | pub last_connection_at: NaiveDateTime, 67 | pub created_by: Uuid, 68 | pub created_at: NaiveDateTime, 69 | } 70 | 71 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] 72 | pub enum Relation { 73 | #[sea_orm( 74 | belongs_to = "super::organization::Entity", 75 | from = "Column::Organization", 76 | to = "super::organization::Column::Id" 77 | )] 78 | Organization, 79 | #[sea_orm( 80 | belongs_to = "super::user::Entity", 81 | from = "Column::CreatedBy", 82 | to = "super::user::Column::Id" 83 | )] 84 | CreatedBy, 85 | } 86 | 87 | impl ActiveModelBehavior for ActiveModel {} 88 | -------------------------------------------------------------------------------- /backend/entity/src/server_architecture.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | use sea_orm::entity::prelude::*; 8 | use serde::{Deserialize, Serialize}; 9 | use uuid::Uuid; 10 | 11 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Deserialize, Serialize)] 12 | #[sea_orm(table_name = "server_architecture")] 13 | pub struct Model { 14 | #[sea_orm(primary_key)] 15 | pub id: Uuid, 16 | pub server: Uuid, 17 | pub architecture: super::server::Architecture, 18 | } 19 | 20 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] 21 | pub enum Relation { 22 | #[sea_orm( 23 | belongs_to = "super::server::Entity", 24 | from = "Column::Server", 25 | to = "super::server::Column::Id" 26 | )] 27 | Server, 28 | } 29 | 30 | impl ActiveModelBehavior for ActiveModel {} 31 | -------------------------------------------------------------------------------- /backend/entity/src/server_feature.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | use sea_orm::entity::prelude::*; 8 | use serde::{Deserialize, Serialize}; 9 | use uuid::Uuid; 10 | 11 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Deserialize, Serialize)] 12 | #[sea_orm(table_name = "server_feature")] 13 | pub struct Model { 14 | #[sea_orm(primary_key)] 15 | pub id: Uuid, 16 | pub server: Uuid, 17 | pub feature: Uuid, 18 | } 19 | 20 | #[derive(Copy, Clone, Debug, EnumIter)] 21 | pub enum Relation { 22 | Server, 23 | Feature, 24 | } 25 | 26 | impl RelationTrait for Relation { 27 | fn def(&self) -> RelationDef { 28 | match self { 29 | Self::Server => Entity::belongs_to(super::server::Entity) 30 | .from(Column::Server) 31 | .to(super::server::Column::Id) 32 | .into(), 33 | Self::Feature => Entity::belongs_to(super::feature::Entity) 34 | .from(Column::Feature) 35 | .to(super::feature::Column::Id) 36 | .into(), 37 | } 38 | } 39 | } 40 | 41 | impl ActiveModelBehavior for ActiveModel {} 42 | -------------------------------------------------------------------------------- /backend/entity/src/tests/mod.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | pub mod test_entity; 8 | -------------------------------------------------------------------------------- /backend/entity/src/tests/test_entity.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | // #[cfg(test)] 8 | // mod tests { 9 | // use sea_orm::{ 10 | // entity::prelude::*, entity::*, tests_cfg::*, 11 | // DatabaseBackend, MockDatabase, Transaction, 12 | // }; 13 | 14 | // #[async_std::test] 15 | // async fn test_find_cake() -> Result<(), DbErr> { 16 | // let db = MockDatabase::new(DatabaseBackend::Postgres) 17 | // .append_query_results([ 18 | // vec![cake::Model { 19 | // id: 1, 20 | // name: "New York Cheese".to_owned(), 21 | // }], 22 | // vec![ 23 | // cake::Model { 24 | // id: 1, 25 | // name: "New York Cheese".to_owned(), 26 | // }, 27 | // cake::Model { 28 | // id: 2, 29 | // name: "Chocolate Forest".to_owned(), 30 | // }, 31 | // ], 32 | // ]) 33 | // .append_query_results([ 34 | // [( 35 | // cake::Model { 36 | // id: 1, 37 | // name: "Apple Cake".to_owned(), 38 | // }, 39 | // fruit::Model { 40 | // id: 2, 41 | // name: "Apple".to_owned(), 42 | // cake_id: Some(1), 43 | // }, 44 | // )], 45 | // ]) 46 | // .into_connection(); 47 | 48 | // assert_eq!( 49 | // cake::Entity::find().one(&db).await?, 50 | // Some(cake::Model { 51 | // id: 1, 52 | // name: "New York Cheese".to_owned(), 53 | // }) 54 | // ); 55 | 56 | // // Find all cakes from MockDatabase 57 | // // Return the second query result 58 | // assert_eq!( 59 | // cake::Entity::find().all(&db).await?, 60 | // [ 61 | // cake::Model { 62 | // id: 1, 63 | // name: "New York Cheese".to_owned(), 64 | // }, 65 | // cake::Model { 66 | // id: 2, 67 | // name: "Chocolate Forest".to_owned(), 68 | // }, 69 | // ] 70 | // ); 71 | 72 | // // Find all cakes with its related fruits 73 | // assert_eq!( 74 | // cake::Entity::find() 75 | // .find_also_related(fruit::Entity) 76 | // .all(&db) 77 | // .await?, 78 | // [( 79 | // cake::Model { 80 | // id: 1, 81 | // name: "Apple Cake".to_owned(), 82 | // }, 83 | // Some(fruit::Model { 84 | // id: 2, 85 | // name: "Apple".to_owned(), 86 | // cake_id: Some(1), 87 | // }) 88 | // )] 89 | // ); 90 | 91 | // // Checking transaction log 92 | // assert_eq!( 93 | // db.into_transaction_log(), 94 | // [ 95 | // Transaction::from_sql_and_values( 96 | // DatabaseBackend::Postgres, 97 | // r#"SELECT "cake"."id", "cake"."name" FROM "cake" LIMIT $1"#, 98 | // [1u64.into()] 99 | // ), 100 | // Transaction::from_sql_and_values( 101 | // DatabaseBackend::Postgres, 102 | // r#"SELECT "cake"."id", "cake"."name" FROM "cake""#, 103 | // [] 104 | // ), 105 | // Transaction::from_sql_and_values( 106 | // DatabaseBackend::Postgres, 107 | // r#"SELECT "cake"."id" AS "A_id", "cake"."name" AS "A_name", "fruit"."id" AS "B_id", "fruit"."name" AS "B_name", "fruit"."cake_id" AS "B_cake_id" FROM "cake" LEFT JOIN "fruit" ON "cake"."id" = "fruit"."cake_id""#, 108 | // [] 109 | // ), 110 | // ] 111 | // ); 112 | 113 | // Ok(()) 114 | // } 115 | // } 116 | -------------------------------------------------------------------------------- /backend/entity/src/user.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | use chrono::NaiveDateTime; 8 | use sea_orm::entity::prelude::*; 9 | use serde::{Deserialize, Serialize}; 10 | use uuid::Uuid; 11 | 12 | #[derive(Clone, PartialEq, DeriveEntityModel, Deserialize, Serialize)] 13 | #[sea_orm(table_name = "user")] 14 | pub struct Model { 15 | #[sea_orm(primary_key)] 16 | pub id: Uuid, 17 | #[sea_orm(unique, indexed)] 18 | pub username: String, 19 | pub name: String, 20 | pub email: String, 21 | pub password: Option, 22 | pub last_login_at: NaiveDateTime, 23 | pub created_at: NaiveDateTime, 24 | } 25 | 26 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] 27 | pub enum Relation {} 28 | 29 | impl std::fmt::Debug for Model { 30 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 31 | f.debug_struct("User") 32 | .field("id", &self.id) 33 | .field("username", &self.username) 34 | .field("name", &self.name) 35 | .field("email", &self.email) 36 | .field("password", &"[redacted]") 37 | .field("created_at", &self.created_at) 38 | .finish() 39 | } 40 | } 41 | 42 | impl ActiveModelBehavior for ActiveModel {} 43 | -------------------------------------------------------------------------------- /backend/migration/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "migration" 3 | version = "0.2.0" 4 | edition = "2024" 5 | publish = false 6 | license = "AGPL-3.0" 7 | authors = ["Wavelens UG "] 8 | 9 | [lib] 10 | name = "migration" 11 | path = "src/lib.rs" 12 | 13 | [dependencies] 14 | async-std = { version = "1", features = ["attributes", "tokio1"] } 15 | 16 | [dependencies.sea-orm-migration] 17 | version = "1.1" 18 | features = ["with-uuid", "with-chrono", "with-json", "sqlx-postgres", "sea-orm-cli", "runtime-tokio"] 19 | -------------------------------------------------------------------------------- /backend/migration/README.md: -------------------------------------------------------------------------------- 1 | # Running Migrator CLI 2 | 3 | - Generate a new migration file 4 | ```sh 5 | cargo run -- generate MIGRATION_NAME 6 | ``` 7 | - Apply all pending migrations 8 | ```sh 9 | cargo run 10 | ``` 11 | ```sh 12 | cargo run -- up 13 | ``` 14 | - Apply first 10 pending migrations 15 | ```sh 16 | cargo run -- up -n 10 17 | ``` 18 | - Rollback last applied migrations 19 | ```sh 20 | cargo run -- down 21 | ``` 22 | - Rollback last 10 applied migrations 23 | ```sh 24 | cargo run -- down -n 10 25 | ``` 26 | - Drop all tables from the database, then reapply all migrations 27 | ```sh 28 | cargo run -- fresh 29 | ``` 30 | - Rollback all applied migrations, then reapply all migrations 31 | ```sh 32 | cargo run -- refresh 33 | ``` 34 | - Rollback all applied migrations 35 | ```sh 36 | cargo run -- reset 37 | ``` 38 | - Check the status of all migrations 39 | ```sh 40 | cargo run -- status 41 | ``` 42 | -------------------------------------------------------------------------------- /backend/migration/src/lib.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | pub use sea_orm_migration::prelude::*; 8 | 9 | mod m20241107_135027_create_table_user; 10 | mod m20241107_135442_create_table_organization; 11 | mod m20241107_135941_create_table_project; 12 | mod m20241107_140540_create_table_feature; 13 | mod m20241107_140545_create_table_commit; 14 | mod m20241107_140550_create_table_server; 15 | mod m20241107_140560_create_table_build; 16 | mod m20241107_140600_create_table_evaluation; 17 | mod m20241107_155000_create_table_build_depencdency; 18 | mod m20241107_155020_create_table_api; 19 | mod m20241107_156000_create_table_build_feature; 20 | mod m20241107_156100_create_table_server_feature; 21 | mod m20241107_156110_create_table_server_architecture; 22 | mod m20241107_156120_create_table_cache; 23 | mod m20241107_156130_create_table_organization_cache; 24 | mod m20241107_156140_create_table_role; 25 | mod m20241107_156150_create_table_organization_user; 26 | mod m20241107_156160_create_table_build_output; 27 | mod m20241107_156170_create_table_build_output_signature; 28 | 29 | pub struct Migrator; 30 | 31 | #[async_trait::async_trait] 32 | impl MigratorTrait for Migrator { 33 | fn migrations() -> Vec> { 34 | vec![ 35 | Box::new(m20241107_135027_create_table_user::Migration), 36 | Box::new(m20241107_135442_create_table_organization::Migration), 37 | Box::new(m20241107_135941_create_table_project::Migration), 38 | Box::new(m20241107_140540_create_table_feature::Migration), 39 | Box::new(m20241107_140545_create_table_commit::Migration), 40 | Box::new(m20241107_140600_create_table_evaluation::Migration), 41 | Box::new(m20241107_140550_create_table_server::Migration), 42 | Box::new(m20241107_140560_create_table_build::Migration), 43 | Box::new(m20241107_155000_create_table_build_depencdency::Migration), 44 | Box::new(m20241107_155020_create_table_api::Migration), 45 | Box::new(m20241107_156000_create_table_build_feature::Migration), 46 | Box::new(m20241107_156100_create_table_server_feature::Migration), 47 | Box::new(m20241107_156110_create_table_server_architecture::Migration), 48 | Box::new(m20241107_156120_create_table_cache::Migration), 49 | Box::new(m20241107_156130_create_table_organization_cache::Migration), 50 | Box::new(m20241107_156140_create_table_role::Migration), 51 | Box::new(m20241107_156150_create_table_organization_user::Migration), 52 | Box::new(m20241107_156160_create_table_build_output::Migration), 53 | Box::new(m20241107_156170_create_table_build_output_signature::Migration), 54 | ] 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /backend/migration/src/m20241107_135027_create_table_user.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | use sea_orm_migration::prelude::*; 8 | 9 | #[derive(DeriveMigrationName)] 10 | pub struct Migration; 11 | 12 | #[async_trait::async_trait] 13 | impl MigrationTrait for Migration { 14 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { 15 | manager 16 | .create_table( 17 | Table::create() 18 | .table(User::Table) 19 | .if_not_exists() 20 | .col(ColumnDef::new(User::Id).uuid().not_null().primary_key()) 21 | .col( 22 | ColumnDef::new(User::Username) 23 | .string() 24 | .not_null() 25 | .unique_key(), 26 | ) 27 | .col(ColumnDef::new(User::Name).string().not_null()) 28 | .col(ColumnDef::new(User::Email).string().not_null()) 29 | .col(ColumnDef::new(User::Password).string()) 30 | .col(ColumnDef::new(User::LastLoginAt).date_time().not_null()) 31 | .col(ColumnDef::new(User::CreatedAt).date_time().not_null()) 32 | .to_owned(), 33 | ) 34 | .await 35 | } 36 | 37 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { 38 | manager 39 | .drop_table(Table::drop().table(User::Table).to_owned()) 40 | .await 41 | } 42 | } 43 | 44 | #[derive(DeriveIden)] 45 | enum User { 46 | Table, 47 | Id, 48 | Username, 49 | Name, 50 | Email, 51 | Password, 52 | LastLoginAt, 53 | CreatedAt, 54 | } 55 | -------------------------------------------------------------------------------- /backend/migration/src/m20241107_135442_create_table_organization.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | use sea_orm_migration::prelude::*; 8 | 9 | #[derive(DeriveMigrationName)] 10 | pub struct Migration; 11 | 12 | #[async_trait::async_trait] 13 | impl MigrationTrait for Migration { 14 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { 15 | manager 16 | .create_table( 17 | Table::create() 18 | .table(Organization::Table) 19 | .if_not_exists() 20 | .col( 21 | ColumnDef::new(Organization::Id) 22 | .uuid() 23 | .not_null() 24 | .primary_key(), 25 | ) 26 | .col( 27 | ColumnDef::new(Organization::Name) 28 | .string() 29 | .not_null() 30 | .unique_key(), 31 | ) 32 | .col( 33 | ColumnDef::new(Organization::DisplayName) 34 | .string() 35 | .not_null(), 36 | ) 37 | .col(ColumnDef::new(Organization::Description).text().not_null()) 38 | .col(ColumnDef::new(Organization::PublicKey).string().not_null()) 39 | .col( 40 | ColumnDef::new(Organization::UseNixStore) 41 | .boolean() 42 | .not_null(), 43 | ) 44 | .col(ColumnDef::new(Organization::PrivateKey).string().not_null()) 45 | .col(ColumnDef::new(Organization::CreatedBy).uuid().not_null()) 46 | .col( 47 | ColumnDef::new(Organization::CreatedAt) 48 | .date_time() 49 | .not_null(), 50 | ) 51 | .foreign_key( 52 | ForeignKey::create() 53 | .name("fk-organization-created_by") 54 | .from(Organization::Table, Organization::CreatedBy) 55 | .to(User::Table, User::Id) 56 | .on_delete(ForeignKeyAction::Cascade), 57 | ) 58 | .to_owned(), 59 | ) 60 | .await 61 | } 62 | 63 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { 64 | manager 65 | .drop_table(Table::drop().table(Organization::Table).to_owned()) 66 | .await 67 | } 68 | } 69 | 70 | #[derive(DeriveIden)] 71 | enum Organization { 72 | Table, 73 | Id, 74 | Name, 75 | DisplayName, 76 | Description, 77 | PublicKey, 78 | PrivateKey, 79 | UseNixStore, 80 | CreatedBy, 81 | CreatedAt, 82 | } 83 | 84 | #[derive(DeriveIden)] 85 | enum User { 86 | Table, 87 | Id, 88 | } 89 | -------------------------------------------------------------------------------- /backend/migration/src/m20241107_135941_create_table_project.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | use sea_orm_migration::prelude::*; 8 | 9 | #[derive(DeriveMigrationName)] 10 | pub struct Migration; 11 | 12 | #[async_trait::async_trait] 13 | impl MigrationTrait for Migration { 14 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { 15 | manager 16 | .create_table( 17 | Table::create() 18 | .table(Project::Table) 19 | .if_not_exists() 20 | .col(ColumnDef::new(Project::Id).uuid().not_null().primary_key()) 21 | .col(ColumnDef::new(Project::Organization).uuid().not_null()) 22 | .col(ColumnDef::new(Project::Name).string().not_null()) 23 | .col(ColumnDef::new(Project::Active).boolean().not_null()) 24 | .col(ColumnDef::new(Project::DisplayName).string().not_null()) 25 | .col(ColumnDef::new(Project::Description).text().not_null()) 26 | .col(ColumnDef::new(Project::Repository).string().not_null()) 27 | .col( 28 | ColumnDef::new(Project::EvaluationWildcard) 29 | .string() 30 | .not_null(), 31 | ) 32 | .col(ColumnDef::new(Project::LastEvaluation).uuid()) 33 | .col(ColumnDef::new(Project::LastCheckAt).date_time().not_null()) 34 | .col( 35 | ColumnDef::new(Project::ForceEvaluation) 36 | .boolean() 37 | .not_null(), 38 | ) 39 | .col(ColumnDef::new(Project::CreatedBy).uuid().not_null()) 40 | .col(ColumnDef::new(Project::CreatedAt).date_time().not_null()) 41 | .foreign_key( 42 | ForeignKey::create() 43 | .name("fk-project-organization") 44 | .from(Project::Table, Project::Organization) 45 | .to(Organization::Table, Organization::Id) 46 | .on_delete(ForeignKeyAction::Cascade), 47 | ) 48 | .foreign_key( 49 | ForeignKey::create() 50 | .name("fk-project-created_by") 51 | .from(Project::Table, Project::CreatedBy) 52 | .to(User::Table, User::Id) 53 | .on_delete(ForeignKeyAction::Cascade), 54 | ) 55 | .to_owned(), 56 | ) 57 | .await 58 | } 59 | 60 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { 61 | manager 62 | .drop_table(Table::drop().table(Project::Table).to_owned()) 63 | .await 64 | } 65 | } 66 | 67 | #[derive(DeriveIden)] 68 | enum Project { 69 | Table, 70 | Id, 71 | Organization, 72 | Name, 73 | Active, 74 | DisplayName, 75 | Description, 76 | Repository, 77 | EvaluationWildcard, 78 | LastEvaluation, 79 | LastCheckAt, 80 | ForceEvaluation, 81 | CreatedBy, 82 | CreatedAt, 83 | } 84 | 85 | #[derive(DeriveIden)] 86 | enum Organization { 87 | Table, 88 | Id, 89 | } 90 | 91 | #[derive(DeriveIden)] 92 | enum User { 93 | Table, 94 | Id, 95 | } 96 | -------------------------------------------------------------------------------- /backend/migration/src/m20241107_140540_create_table_feature.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | use sea_orm_migration::prelude::*; 8 | 9 | #[derive(DeriveMigrationName)] 10 | pub struct Migration; 11 | 12 | #[async_trait::async_trait] 13 | impl MigrationTrait for Migration { 14 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { 15 | manager 16 | .create_table( 17 | Table::create() 18 | .table(Feature::Table) 19 | .if_not_exists() 20 | .col(ColumnDef::new(Feature::Id).uuid().not_null().primary_key()) 21 | .col(ColumnDef::new(Feature::Name).string().not_null()) 22 | .to_owned(), 23 | ) 24 | .await 25 | } 26 | 27 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { 28 | manager 29 | .drop_table(Table::drop().table(Feature::Table).to_owned()) 30 | .await 31 | } 32 | } 33 | 34 | #[derive(DeriveIden)] 35 | enum Feature { 36 | Table, 37 | Id, 38 | Name, 39 | } 40 | -------------------------------------------------------------------------------- /backend/migration/src/m20241107_140545_create_table_commit.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | use sea_orm_migration::prelude::*; 8 | 9 | #[derive(DeriveMigrationName)] 10 | pub struct Migration; 11 | 12 | #[async_trait::async_trait] 13 | impl MigrationTrait for Migration { 14 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { 15 | manager 16 | .create_table( 17 | Table::create() 18 | .table(Commit::Table) 19 | .if_not_exists() 20 | .col(ColumnDef::new(Commit::Id).uuid().not_null().primary_key()) 21 | .col(ColumnDef::new(Commit::Message).string().not_null()) 22 | .col(ColumnDef::new(Commit::Hash).blob().not_null()) 23 | .col(ColumnDef::new(Commit::Author).uuid()) 24 | .col(ColumnDef::new(Commit::AuthorName).string().not_null()) 25 | .to_owned(), 26 | ) 27 | .await 28 | } 29 | 30 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { 31 | manager 32 | .drop_table(Table::drop().table(Commit::Table).to_owned()) 33 | .await 34 | } 35 | } 36 | 37 | #[derive(DeriveIden)] 38 | enum Commit { 39 | Table, 40 | Id, 41 | Message, 42 | Hash, 43 | Author, 44 | AuthorName, 45 | } 46 | -------------------------------------------------------------------------------- /backend/migration/src/m20241107_140550_create_table_server.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | use sea_orm_migration::prelude::*; 8 | 9 | #[derive(DeriveMigrationName)] 10 | pub struct Migration; 11 | 12 | #[async_trait::async_trait] 13 | impl MigrationTrait for Migration { 14 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { 15 | manager 16 | .create_table( 17 | Table::create() 18 | .table(Server::Table) 19 | .if_not_exists() 20 | .col(ColumnDef::new(Server::Id).uuid().not_null().primary_key()) 21 | .col(ColumnDef::new(Server::Name).string().not_null()) 22 | .col(ColumnDef::new(Server::DisplayName).string().not_null()) 23 | .col(ColumnDef::new(Server::Organization).uuid().not_null()) 24 | .col(ColumnDef::new(Server::Active).boolean().not_null()) 25 | .col(ColumnDef::new(Server::Host).string().not_null()) 26 | .col(ColumnDef::new(Server::Port).integer().not_null()) 27 | .col(ColumnDef::new(Server::Username).string().not_null()) 28 | .col( 29 | ColumnDef::new(Server::LastConnectionAt) 30 | .date_time() 31 | .not_null(), 32 | ) 33 | .col(ColumnDef::new(Server::CreatedBy).uuid().not_null()) 34 | .col(ColumnDef::new(Server::CreatedAt).date_time().not_null()) 35 | .foreign_key( 36 | ForeignKey::create() 37 | .name("fk-server-organization") 38 | .from(Server::Table, Server::Organization) 39 | .to(Organization::Table, Organization::Id) 40 | .on_delete(ForeignKeyAction::Cascade), 41 | ) 42 | .foreign_key( 43 | ForeignKey::create() 44 | .name("fk-server-created_by") 45 | .from(Server::Table, Server::CreatedBy) 46 | .to(User::Table, User::Id) 47 | .on_delete(ForeignKeyAction::Cascade), 48 | ) 49 | .to_owned(), 50 | ) 51 | .await 52 | } 53 | 54 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { 55 | manager 56 | .drop_table(Table::drop().table(Server::Table).to_owned()) 57 | .await 58 | } 59 | } 60 | 61 | #[derive(DeriveIden)] 62 | enum Server { 63 | Table, 64 | Id, 65 | Name, 66 | DisplayName, 67 | Organization, 68 | Active, 69 | Host, 70 | Port, 71 | Username, 72 | LastConnectionAt, 73 | CreatedBy, 74 | CreatedAt, 75 | } 76 | 77 | #[derive(DeriveIden)] 78 | enum Organization { 79 | Table, 80 | Id, 81 | } 82 | 83 | #[derive(DeriveIden)] 84 | enum User { 85 | Table, 86 | Id, 87 | } 88 | -------------------------------------------------------------------------------- /backend/migration/src/m20241107_140560_create_table_build.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | use sea_orm_migration::prelude::*; 8 | 9 | #[derive(DeriveMigrationName)] 10 | pub struct Migration; 11 | 12 | #[async_trait::async_trait] 13 | impl MigrationTrait for Migration { 14 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { 15 | manager 16 | .create_table( 17 | Table::create() 18 | .table(Build::Table) 19 | .if_not_exists() 20 | .col(ColumnDef::new(Build::Id).uuid().not_null().primary_key()) 21 | .col(ColumnDef::new(Build::Evaluation).uuid().not_null()) 22 | .col(ColumnDef::new(Build::Status).integer().not_null()) 23 | .col(ColumnDef::new(Build::DerivationPath).string().not_null()) 24 | .col( 25 | ColumnDef::new(Build::Architecture) 26 | .small_integer() 27 | .not_null(), 28 | ) 29 | .col(ColumnDef::new(Build::Server).uuid()) 30 | .col(ColumnDef::new(Build::Log).text()) 31 | .col(ColumnDef::new(Build::CreatedAt).date_time().not_null()) 32 | .col(ColumnDef::new(Build::UpdatedAt).date_time().not_null()) 33 | .foreign_key( 34 | ForeignKey::create() 35 | .name("fk-build-evaluation") 36 | .from(Build::Table, Build::Evaluation) 37 | .to(Evaluation::Table, Evaluation::Id) 38 | .on_delete(ForeignKeyAction::Cascade), 39 | ) 40 | .foreign_key( 41 | ForeignKey::create() 42 | .name("fk-build-server") 43 | .from(Build::Table, Build::Server) 44 | .to(Server::Table, Server::Id) 45 | .on_delete(ForeignKeyAction::Cascade), 46 | ) 47 | .to_owned(), 48 | ) 49 | .await 50 | } 51 | 52 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { 53 | manager 54 | .drop_table(Table::drop().table(Build::Table).to_owned()) 55 | .await 56 | } 57 | } 58 | 59 | #[derive(DeriveIden)] 60 | enum Build { 61 | Table, 62 | Id, 63 | Evaluation, 64 | Status, 65 | DerivationPath, 66 | Architecture, 67 | Server, 68 | Log, 69 | CreatedAt, 70 | UpdatedAt, 71 | } 72 | 73 | #[derive(DeriveIden)] 74 | enum Evaluation { 75 | Table, 76 | Id, 77 | } 78 | 79 | #[derive(DeriveIden)] 80 | enum Server { 81 | Table, 82 | Id, 83 | } 84 | -------------------------------------------------------------------------------- /backend/migration/src/m20241107_140600_create_table_evaluation.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | use sea_orm_migration::prelude::*; 8 | 9 | #[derive(DeriveMigrationName)] 10 | pub struct Migration; 11 | 12 | #[async_trait::async_trait] 13 | impl MigrationTrait for Migration { 14 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { 15 | manager 16 | .create_table( 17 | Table::create() 18 | .table(Evaluation::Table) 19 | .if_not_exists() 20 | .col( 21 | ColumnDef::new(Evaluation::Id) 22 | .uuid() 23 | .not_null() 24 | .primary_key(), 25 | ) 26 | .col(ColumnDef::new(Evaluation::Project).uuid().not_null()) 27 | .col(ColumnDef::new(Evaluation::Repository).string().not_null()) 28 | .col(ColumnDef::new(Evaluation::Commit).uuid().not_null()) 29 | .col(ColumnDef::new(Evaluation::Wildcard).string().not_null()) 30 | .col(ColumnDef::new(Evaluation::Status).integer().not_null()) 31 | .col(ColumnDef::new(Evaluation::Previous).uuid()) 32 | .col(ColumnDef::new(Evaluation::Next).uuid()) 33 | .col(ColumnDef::new(Evaluation::CreatedAt).date_time().not_null()) 34 | .foreign_key( 35 | ForeignKey::create() 36 | .name("fk-evaluation-project") 37 | .from(Evaluation::Table, Evaluation::Project) 38 | .to(Project::Table, Project::Id) 39 | .on_delete(ForeignKeyAction::Cascade), 40 | ) 41 | .foreign_key( 42 | ForeignKey::create() 43 | .name("fk-evaluation-previous") 44 | .from(Evaluation::Table, Evaluation::Previous) 45 | .to(Evaluation::Table, Evaluation::Id) 46 | .on_delete(ForeignKeyAction::Cascade), 47 | ) 48 | .foreign_key( 49 | ForeignKey::create() 50 | .name("fk-evaluation-next") 51 | .from(Evaluation::Table, Evaluation::Next) 52 | .to(Evaluation::Table, Evaluation::Id) 53 | .on_delete(ForeignKeyAction::Cascade), 54 | ) 55 | .to_owned(), 56 | ) 57 | .await 58 | } 59 | 60 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { 61 | manager 62 | .drop_table(Table::drop().table(Evaluation::Table).to_owned()) 63 | .await 64 | } 65 | } 66 | 67 | #[derive(DeriveIden)] 68 | enum Evaluation { 69 | Table, 70 | Id, 71 | Project, 72 | Repository, 73 | Commit, 74 | Wildcard, 75 | Status, 76 | Previous, 77 | Next, 78 | CreatedAt, 79 | } 80 | 81 | #[derive(DeriveIden)] 82 | enum Project { 83 | Table, 84 | Id, 85 | } 86 | -------------------------------------------------------------------------------- /backend/migration/src/m20241107_155000_create_table_build_depencdency.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | use sea_orm_migration::prelude::*; 8 | 9 | #[derive(DeriveMigrationName)] 10 | pub struct Migration; 11 | 12 | #[async_trait::async_trait] 13 | impl MigrationTrait for Migration { 14 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { 15 | manager 16 | .create_table( 17 | Table::create() 18 | .table(BuildDependency::Table) 19 | .if_not_exists() 20 | .col( 21 | ColumnDef::new(BuildDependency::Id) 22 | .uuid() 23 | .not_null() 24 | .primary_key(), 25 | ) 26 | .col(ColumnDef::new(BuildDependency::Build).uuid().not_null()) 27 | .col( 28 | ColumnDef::new(BuildDependency::Dependency) 29 | .uuid() 30 | .not_null(), 31 | ) 32 | .foreign_key( 33 | ForeignKey::create() 34 | .name("fk-build_dependency-build") 35 | .from(BuildDependency::Table, BuildDependency::Build) 36 | .to(Build::Table, Build::Id) 37 | .on_delete(ForeignKeyAction::Cascade) 38 | .on_update(ForeignKeyAction::Cascade), 39 | ) 40 | .foreign_key( 41 | ForeignKey::create() 42 | .name("fk-build_dependency-dependency") 43 | .from(BuildDependency::Table, BuildDependency::Dependency) 44 | .to(Build::Table, Build::Id) 45 | .on_delete(ForeignKeyAction::Cascade) 46 | .on_update(ForeignKeyAction::Cascade), 47 | ) 48 | .to_owned(), 49 | ) 50 | .await 51 | } 52 | 53 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { 54 | manager 55 | .drop_table(Table::drop().table(BuildDependency::Table).to_owned()) 56 | .await 57 | } 58 | } 59 | 60 | #[derive(DeriveIden)] 61 | enum BuildDependency { 62 | Table, 63 | Id, 64 | Build, 65 | Dependency, 66 | } 67 | 68 | #[derive(DeriveIden)] 69 | enum Build { 70 | Table, 71 | Id, 72 | } 73 | -------------------------------------------------------------------------------- /backend/migration/src/m20241107_155020_create_table_api.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | use sea_orm_migration::prelude::*; 8 | 9 | #[derive(DeriveMigrationName)] 10 | pub struct Migration; 11 | 12 | #[async_trait::async_trait] 13 | impl MigrationTrait for Migration { 14 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { 15 | manager 16 | .create_table( 17 | Table::create() 18 | .table(Api::Table) 19 | .if_not_exists() 20 | .col(ColumnDef::new(Api::Id).uuid().not_null().primary_key()) 21 | .col(ColumnDef::new(Api::OwnedBy).uuid().not_null()) 22 | .col(ColumnDef::new(Api::Name).string().not_null()) 23 | .col(ColumnDef::new(Api::Key).string().not_null()) 24 | .col(ColumnDef::new(Api::LastUsedAt).date_time().not_null()) 25 | .col(ColumnDef::new(Api::CreatedAt).date_time().not_null()) 26 | .foreign_key( 27 | ForeignKey::create() 28 | .name("fk-api-owned_by") 29 | .from(Api::Table, Api::OwnedBy) 30 | .to(User::Table, User::Id) 31 | .on_delete(ForeignKeyAction::Cascade) 32 | .on_update(ForeignKeyAction::Cascade), 33 | ) 34 | .to_owned(), 35 | ) 36 | .await 37 | } 38 | 39 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { 40 | manager 41 | .drop_table(Table::drop().table(Api::Table).to_owned()) 42 | .await 43 | } 44 | } 45 | 46 | #[derive(DeriveIden)] 47 | enum Api { 48 | Table, 49 | Id, 50 | OwnedBy, 51 | Name, 52 | Key, 53 | LastUsedAt, 54 | CreatedAt, 55 | } 56 | 57 | #[derive(DeriveIden)] 58 | enum User { 59 | Table, 60 | Id, 61 | } 62 | -------------------------------------------------------------------------------- /backend/migration/src/m20241107_156000_create_table_build_feature.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | use sea_orm_migration::prelude::*; 8 | 9 | #[derive(DeriveMigrationName)] 10 | pub struct Migration; 11 | 12 | #[async_trait::async_trait] 13 | impl MigrationTrait for Migration { 14 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { 15 | manager 16 | .create_table( 17 | Table::create() 18 | .table(BuildFeature::Table) 19 | .if_not_exists() 20 | .col( 21 | ColumnDef::new(BuildFeature::Id) 22 | .uuid() 23 | .not_null() 24 | .primary_key(), 25 | ) 26 | .col(ColumnDef::new(BuildFeature::Build).uuid().not_null()) 27 | .col(ColumnDef::new(BuildFeature::Feature).uuid().not_null()) 28 | .foreign_key( 29 | ForeignKey::create() 30 | .name("fk-build_feature-build") 31 | .from(BuildFeature::Table, BuildFeature::Build) 32 | .to(Build::Table, Build::Id) 33 | .on_delete(ForeignKeyAction::Cascade) 34 | .on_update(ForeignKeyAction::Cascade), 35 | ) 36 | .foreign_key( 37 | ForeignKey::create() 38 | .name("fk-build_feature-feature") 39 | .from(BuildFeature::Table, BuildFeature::Feature) 40 | .to(Feature::Table, Feature::Id) 41 | .on_delete(ForeignKeyAction::Cascade) 42 | .on_update(ForeignKeyAction::Cascade), 43 | ) 44 | .to_owned(), 45 | ) 46 | .await 47 | } 48 | 49 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { 50 | manager 51 | .drop_table(Table::drop().table(BuildFeature::Table).to_owned()) 52 | .await 53 | } 54 | } 55 | 56 | #[derive(DeriveIden)] 57 | enum BuildFeature { 58 | Table, 59 | Id, 60 | Build, 61 | Feature, 62 | } 63 | 64 | #[derive(DeriveIden)] 65 | enum Build { 66 | Table, 67 | Id, 68 | } 69 | 70 | #[derive(DeriveIden)] 71 | enum Feature { 72 | Table, 73 | Id, 74 | } 75 | -------------------------------------------------------------------------------- /backend/migration/src/m20241107_156100_create_table_server_feature.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | use sea_orm_migration::prelude::*; 8 | 9 | #[derive(DeriveMigrationName)] 10 | pub struct Migration; 11 | 12 | #[async_trait::async_trait] 13 | impl MigrationTrait for Migration { 14 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { 15 | manager 16 | .create_table( 17 | Table::create() 18 | .table(ServerFeature::Table) 19 | .if_not_exists() 20 | .col( 21 | ColumnDef::new(ServerFeature::Id) 22 | .uuid() 23 | .not_null() 24 | .primary_key(), 25 | ) 26 | .col(ColumnDef::new(ServerFeature::Server).uuid().not_null()) 27 | .col(ColumnDef::new(ServerFeature::Feature).uuid().not_null()) 28 | .foreign_key( 29 | ForeignKey::create() 30 | .name("fk-server_feature-server") 31 | .from(ServerFeature::Table, ServerFeature::Server) 32 | .to(Server::Table, Server::Id) 33 | .on_delete(ForeignKeyAction::Cascade) 34 | .on_update(ForeignKeyAction::Cascade), 35 | ) 36 | .foreign_key( 37 | ForeignKey::create() 38 | .name("fk-server_feature-feature") 39 | .from(ServerFeature::Table, ServerFeature::Feature) 40 | .to(Feature::Table, Feature::Id) 41 | .on_delete(ForeignKeyAction::Cascade) 42 | .on_update(ForeignKeyAction::Cascade), 43 | ) 44 | .to_owned(), 45 | ) 46 | .await 47 | } 48 | 49 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { 50 | manager 51 | .drop_table(Table::drop().table(ServerFeature::Table).to_owned()) 52 | .await 53 | } 54 | } 55 | 56 | #[derive(DeriveIden)] 57 | enum ServerFeature { 58 | Table, 59 | Id, 60 | Server, 61 | Feature, 62 | } 63 | 64 | #[derive(DeriveIden)] 65 | enum Server { 66 | Table, 67 | Id, 68 | } 69 | 70 | #[derive(DeriveIden)] 71 | enum Feature { 72 | Table, 73 | Id, 74 | } 75 | -------------------------------------------------------------------------------- /backend/migration/src/m20241107_156110_create_table_server_architecture.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | use sea_orm_migration::prelude::*; 8 | 9 | #[derive(DeriveMigrationName)] 10 | pub struct Migration; 11 | 12 | #[async_trait::async_trait] 13 | impl MigrationTrait for Migration { 14 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { 15 | manager 16 | .create_table( 17 | Table::create() 18 | .table(ServerArchitecture::Table) 19 | .if_not_exists() 20 | .col( 21 | ColumnDef::new(ServerArchitecture::Id) 22 | .uuid() 23 | .not_null() 24 | .primary_key(), 25 | ) 26 | .col(ColumnDef::new(ServerArchitecture::Server).uuid().not_null()) 27 | .col( 28 | ColumnDef::new(ServerArchitecture::Architecture) 29 | .small_integer() 30 | .not_null(), 31 | ) 32 | .foreign_key( 33 | ForeignKey::create() 34 | .name("fk-server_architecture-server") 35 | .from(ServerArchitecture::Table, ServerArchitecture::Server) 36 | .to(Server::Table, Server::Id) 37 | .on_delete(ForeignKeyAction::Cascade) 38 | .on_update(ForeignKeyAction::Cascade), 39 | ) 40 | .to_owned(), 41 | ) 42 | .await 43 | } 44 | 45 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { 46 | manager 47 | .drop_table(Table::drop().table(ServerArchitecture::Table).to_owned()) 48 | .await 49 | } 50 | } 51 | 52 | #[derive(DeriveIden)] 53 | enum ServerArchitecture { 54 | Table, 55 | Id, 56 | Server, 57 | Architecture, 58 | } 59 | 60 | #[derive(DeriveIden)] 61 | enum Server { 62 | Table, 63 | Id, 64 | } 65 | -------------------------------------------------------------------------------- /backend/migration/src/m20241107_156120_create_table_cache.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | use sea_orm_migration::prelude::*; 8 | 9 | #[derive(DeriveMigrationName)] 10 | pub struct Migration; 11 | 12 | #[async_trait::async_trait] 13 | impl MigrationTrait for Migration { 14 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { 15 | manager 16 | .create_table( 17 | Table::create() 18 | .table(Cache::Table) 19 | .if_not_exists() 20 | .col(ColumnDef::new(Cache::Id).uuid().not_null().primary_key()) 21 | .col(ColumnDef::new(Cache::Name).string().not_null().unique_key()) 22 | .col(ColumnDef::new(Cache::DisplayName).string().not_null()) 23 | .col(ColumnDef::new(Cache::Description).text().not_null()) 24 | .col(ColumnDef::new(Cache::Active).boolean().not_null()) 25 | .col(ColumnDef::new(Cache::Priority).integer().not_null()) 26 | .col(ColumnDef::new(Cache::SigningKey).string().not_null()) 27 | .col(ColumnDef::new(Cache::CreatedBy).uuid().not_null()) 28 | .col(ColumnDef::new(Cache::CreatedAt).date_time().not_null()) 29 | .foreign_key( 30 | ForeignKey::create() 31 | .name("fk-cache-created_by") 32 | .from(Cache::Table, Cache::CreatedBy) 33 | .to(User::Table, User::Id) 34 | .on_delete(ForeignKeyAction::Cascade), 35 | ) 36 | .to_owned(), 37 | ) 38 | .await 39 | } 40 | 41 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { 42 | manager 43 | .drop_table(Table::drop().table(Cache::Table).to_owned()) 44 | .await 45 | } 46 | } 47 | 48 | #[derive(DeriveIden)] 49 | enum Cache { 50 | Table, 51 | Id, 52 | Name, 53 | DisplayName, 54 | Description, 55 | Active, 56 | Priority, 57 | SigningKey, 58 | CreatedBy, 59 | CreatedAt, 60 | } 61 | 62 | #[derive(DeriveIden)] 63 | enum User { 64 | Table, 65 | Id, 66 | } 67 | -------------------------------------------------------------------------------- /backend/migration/src/m20241107_156130_create_table_organization_cache.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | use sea_orm_migration::prelude::*; 8 | 9 | #[derive(DeriveMigrationName)] 10 | pub struct Migration; 11 | 12 | #[async_trait::async_trait] 13 | impl MigrationTrait for Migration { 14 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { 15 | manager 16 | .create_table( 17 | Table::create() 18 | .table(OrganizationCache::Table) 19 | .if_not_exists() 20 | .col( 21 | ColumnDef::new(OrganizationCache::Id) 22 | .uuid() 23 | .not_null() 24 | .primary_key(), 25 | ) 26 | .col( 27 | ColumnDef::new(OrganizationCache::Organization) 28 | .uuid() 29 | .not_null(), 30 | ) 31 | .col(ColumnDef::new(OrganizationCache::Cache).uuid().not_null()) 32 | .foreign_key( 33 | ForeignKey::create() 34 | .name("fk-organization_cache-organization") 35 | .from(OrganizationCache::Table, OrganizationCache::Organization) 36 | .to(Organization::Table, Organization::Id) 37 | .on_delete(ForeignKeyAction::Cascade) 38 | .on_update(ForeignKeyAction::Cascade), 39 | ) 40 | .foreign_key( 41 | ForeignKey::create() 42 | .name("fk-organization_cache-cache") 43 | .from(OrganizationCache::Table, OrganizationCache::Cache) 44 | .to(Cache::Table, Cache::Id) 45 | .on_delete(ForeignKeyAction::Cascade) 46 | .on_update(ForeignKeyAction::Cascade), 47 | ) 48 | .to_owned(), 49 | ) 50 | .await 51 | } 52 | 53 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { 54 | manager 55 | .drop_table(Table::drop().table(OrganizationCache::Table).to_owned()) 56 | .await 57 | } 58 | } 59 | 60 | #[derive(DeriveIden)] 61 | enum OrganizationCache { 62 | Table, 63 | Id, 64 | Organization, 65 | Cache, 66 | } 67 | 68 | #[derive(DeriveIden)] 69 | enum Organization { 70 | Table, 71 | Id, 72 | } 73 | 74 | #[derive(DeriveIden)] 75 | enum Cache { 76 | Table, 77 | Id, 78 | } 79 | -------------------------------------------------------------------------------- /backend/migration/src/m20241107_156140_create_table_role.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | use sea_orm_migration::prelude::*; 8 | 9 | #[derive(DeriveMigrationName)] 10 | pub struct Migration; 11 | 12 | #[async_trait::async_trait] 13 | impl MigrationTrait for Migration { 14 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { 15 | manager 16 | .create_table( 17 | Table::create() 18 | .table(Role::Table) 19 | .if_not_exists() 20 | .col(ColumnDef::new(Role::Id).uuid().not_null().primary_key()) 21 | .col(ColumnDef::new(Role::Name).string().not_null()) 22 | .col(ColumnDef::new(Role::Organization).uuid()) 23 | .col(ColumnDef::new(Role::Permission).big_integer().not_null()) 24 | .foreign_key( 25 | ForeignKey::create() 26 | .name("fk-role-organization") 27 | .from(Role::Table, Role::Organization) 28 | .to(Organization::Table, Organization::Id) 29 | .on_delete(ForeignKeyAction::Cascade), 30 | ) 31 | .to_owned(), 32 | ) 33 | .await 34 | } 35 | 36 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { 37 | manager 38 | .drop_table(Table::drop().table(Role::Table).to_owned()) 39 | .await 40 | } 41 | } 42 | 43 | #[derive(DeriveIden)] 44 | enum Role { 45 | Table, 46 | Id, 47 | Name, 48 | Organization, 49 | Permission, 50 | } 51 | 52 | #[derive(DeriveIden)] 53 | enum Organization { 54 | Table, 55 | Id, 56 | } 57 | -------------------------------------------------------------------------------- /backend/migration/src/m20241107_156150_create_table_organization_user.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | use sea_orm_migration::prelude::*; 8 | 9 | #[derive(DeriveMigrationName)] 10 | pub struct Migration; 11 | 12 | #[async_trait::async_trait] 13 | impl MigrationTrait for Migration { 14 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { 15 | manager 16 | .create_table( 17 | Table::create() 18 | .table(OrganizationUser::Table) 19 | .if_not_exists() 20 | .col( 21 | ColumnDef::new(OrganizationUser::Id) 22 | .uuid() 23 | .not_null() 24 | .primary_key(), 25 | ) 26 | .col( 27 | ColumnDef::new(OrganizationUser::Organization) 28 | .uuid() 29 | .not_null(), 30 | ) 31 | .col(ColumnDef::new(OrganizationUser::User).uuid().not_null()) 32 | .col(ColumnDef::new(OrganizationUser::Role).uuid().not_null()) 33 | .foreign_key( 34 | ForeignKey::create() 35 | .name("fk-organization_user-organization") 36 | .from(OrganizationUser::Table, OrganizationUser::Organization) 37 | .to(Organization::Table, Organization::Id) 38 | .on_delete(ForeignKeyAction::Cascade) 39 | .on_update(ForeignKeyAction::Cascade), 40 | ) 41 | .foreign_key( 42 | ForeignKey::create() 43 | .name("fk-organization_user-user") 44 | .from(OrganizationUser::Table, OrganizationUser::User) 45 | .to(User::Table, User::Id) 46 | .on_delete(ForeignKeyAction::Cascade) 47 | .on_update(ForeignKeyAction::Cascade), 48 | ) 49 | .foreign_key( 50 | ForeignKey::create() 51 | .name("fk-organization_user-role") 52 | .from(OrganizationUser::Table, OrganizationUser::Role) 53 | .to(Role::Table, Role::Id) 54 | .on_delete(ForeignKeyAction::Cascade) 55 | .on_update(ForeignKeyAction::Cascade), 56 | ) 57 | .to_owned(), 58 | ) 59 | .await 60 | } 61 | 62 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { 63 | manager 64 | .drop_table(Table::drop().table(OrganizationUser::Table).to_owned()) 65 | .await 66 | } 67 | } 68 | 69 | #[derive(DeriveIden)] 70 | enum OrganizationUser { 71 | Table, 72 | Id, 73 | Organization, 74 | User, 75 | Role, 76 | } 77 | 78 | #[derive(DeriveIden)] 79 | enum Organization { 80 | Table, 81 | Id, 82 | } 83 | 84 | #[derive(DeriveIden)] 85 | enum User { 86 | Table, 87 | Id, 88 | } 89 | 90 | #[derive(DeriveIden)] 91 | enum Role { 92 | Table, 93 | Id, 94 | } 95 | -------------------------------------------------------------------------------- /backend/migration/src/m20241107_156160_create_table_build_output.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | use sea_orm_migration::prelude::*; 8 | 9 | #[derive(DeriveMigrationName)] 10 | pub struct Migration; 11 | 12 | #[async_trait::async_trait] 13 | impl MigrationTrait for Migration { 14 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { 15 | manager 16 | .create_table( 17 | Table::create() 18 | .table(BuildOutput::Table) 19 | .if_not_exists() 20 | .col( 21 | ColumnDef::new(BuildOutput::Id) 22 | .uuid() 23 | .not_null() 24 | .primary_key(), 25 | ) 26 | .col(ColumnDef::new(BuildOutput::Build).uuid().not_null()) 27 | .col(ColumnDef::new(BuildOutput::Output).string().not_null()) 28 | .col(ColumnDef::new(BuildOutput::Hash).string().not_null()) 29 | .col(ColumnDef::new(BuildOutput::Package).string().not_null()) 30 | .col(ColumnDef::new(BuildOutput::FileHash).string()) 31 | .col(ColumnDef::new(BuildOutput::FileSize).big_integer()) 32 | .col(ColumnDef::new(BuildOutput::IsCached).boolean().not_null()) 33 | .col(ColumnDef::new(BuildOutput::Ca).string()) 34 | .col( 35 | ColumnDef::new(BuildOutput::CreatedAt) 36 | .date_time() 37 | .not_null(), 38 | ) 39 | .foreign_key( 40 | ForeignKey::create() 41 | .name("fk-build_output-build") 42 | .from(BuildOutput::Table, BuildOutput::Build) 43 | .to(Build::Table, Build::Id) 44 | .on_delete(ForeignKeyAction::Cascade) 45 | .on_update(ForeignKeyAction::Cascade), 46 | ) 47 | .to_owned(), 48 | ) 49 | .await 50 | } 51 | 52 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { 53 | manager 54 | .drop_table(Table::drop().table(BuildOutput::Table).to_owned()) 55 | .await 56 | } 57 | } 58 | 59 | #[derive(DeriveIden)] 60 | enum BuildOutput { 61 | Table, 62 | Id, 63 | Build, 64 | Output, 65 | Hash, 66 | Package, 67 | FileHash, 68 | FileSize, 69 | IsCached, 70 | Ca, 71 | CreatedAt, 72 | } 73 | 74 | #[derive(DeriveIden)] 75 | enum Build { 76 | Table, 77 | Id, 78 | } 79 | -------------------------------------------------------------------------------- /backend/migration/src/m20241107_156170_create_table_build_output_signature.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | use sea_orm_migration::prelude::*; 8 | 9 | #[derive(DeriveMigrationName)] 10 | pub struct Migration; 11 | 12 | #[async_trait::async_trait] 13 | impl MigrationTrait for Migration { 14 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { 15 | manager 16 | .create_table( 17 | Table::create() 18 | .table(BuildOutputSignature::Table) 19 | .if_not_exists() 20 | .col( 21 | ColumnDef::new(BuildOutputSignature::Id) 22 | .uuid() 23 | .not_null() 24 | .primary_key(), 25 | ) 26 | .col( 27 | ColumnDef::new(BuildOutputSignature::BuildOutput) 28 | .uuid() 29 | .not_null(), 30 | ) 31 | .col( 32 | ColumnDef::new(BuildOutputSignature::Cache) 33 | .uuid() 34 | .not_null(), 35 | ) 36 | .col(ColumnDef::new(BuildOutputSignature::Signature).string()) 37 | .col( 38 | ColumnDef::new(BuildOutputSignature::CreatedAt) 39 | .date_time() 40 | .not_null(), 41 | ) 42 | .foreign_key( 43 | ForeignKey::create() 44 | .name("fk-build_output_signature-build_output") 45 | .from( 46 | BuildOutputSignature::Table, 47 | BuildOutputSignature::BuildOutput, 48 | ) 49 | .to(BuildOutput::Table, BuildOutput::Id) 50 | .on_delete(ForeignKeyAction::Cascade) 51 | .on_update(ForeignKeyAction::Cascade), 52 | ) 53 | .foreign_key( 54 | ForeignKey::create() 55 | .name("fk-build_output_signature-cache") 56 | .from(BuildOutputSignature::Table, BuildOutputSignature::Cache) 57 | .to(Cache::Table, Cache::Id) 58 | .on_delete(ForeignKeyAction::Cascade) 59 | .on_update(ForeignKeyAction::Cascade), 60 | ) 61 | .to_owned(), 62 | ) 63 | .await 64 | } 65 | 66 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { 67 | manager 68 | .drop_table(Table::drop().table(BuildOutputSignature::Table).to_owned()) 69 | .await 70 | } 71 | } 72 | 73 | #[derive(DeriveIden)] 74 | enum BuildOutputSignature { 75 | Table, 76 | Id, 77 | BuildOutput, 78 | Cache, 79 | Signature, 80 | CreatedAt, 81 | } 82 | 83 | #[derive(DeriveIden)] 84 | enum BuildOutput { 85 | Table, 86 | Id, 87 | } 88 | 89 | #[derive(DeriveIden)] 90 | enum Cache { 91 | Table, 92 | Id, 93 | } 94 | -------------------------------------------------------------------------------- /backend/migration/src/main.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | use sea_orm_migration::prelude::*; 8 | 9 | #[async_std::main] 10 | async fn main() { 11 | cli::run_cli(migration::Migrator).await; 12 | } 13 | -------------------------------------------------------------------------------- /backend/src/main.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | use core::init_state; 8 | use std::sync::Arc; 9 | 10 | #[tokio::main] 11 | pub async fn main() -> std::io::Result<()> { 12 | let state = init_state().await; 13 | 14 | let _guard = if state.cli.report_errors { 15 | Some(sentry::init( 16 | "https://5895e5a5d35f4dbebbcc47d5a722c402@reports.wavelens.io/1", 17 | )) 18 | } else { 19 | None 20 | }; 21 | 22 | builder::start_builder(Arc::clone(&state)).await?; 23 | cache::start_cache(Arc::clone(&state)).await?; 24 | web::serve_web(Arc::clone(&state)).await?; 25 | 26 | Ok(()) 27 | } 28 | -------------------------------------------------------------------------------- /backend/web/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "web" 3 | version = "0.2.0" 4 | edition = "2024" 5 | publish = false 6 | license = "AGPL-3.0" 7 | authors = ["Wavelens UG "] 8 | 9 | [lib] 10 | name = "web" 11 | path = "src/lib.rs" 12 | 13 | [dependencies] 14 | axum = "0.8" 15 | axum-streams = { version = "0.21", features = ["json"] } 16 | async-stream = "0.3" 17 | chrono = "0.4" 18 | core = { path = "../core" } 19 | builder = { path = "../builder" } 20 | entity = { path = "../entity" } 21 | git-url-parse = "0.4" 22 | jsonwebtoken = "9.3" 23 | password-auth = { version = "1.0.0", features = ["argon2"] } 24 | rand = "0.9" 25 | sea-orm = { version = "1.1", features = ["json-array", "mock", "postgres-array", "runtime-tokio", "sqlx-postgres", "with-uuid"] } 26 | serde = { version = "1", features = ["derive"] } 27 | tokio = { version = "1.44", features = ["process", "rt-multi-thread"] } 28 | tokio-util = { version = "0.7", features = ["io"] } 29 | uuid = { version = "1.16", features = ["fast-rng", "macro-diagnostics", "v4"] } 30 | serde_json = "1.0" 31 | oauth2 = "5.0" 32 | url = "2.5" 33 | email_address = "0.2" 34 | tower-http = { version = "0.6.2", features = ["cors", "trace"] } 35 | http = "1.3" 36 | tower = "0.5" 37 | http-body-util = "0.1" 38 | bytes = "1.10" 39 | tracing = "0.1" 40 | -------------------------------------------------------------------------------- /backend/web/src/endpoints/builds.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | use async_stream::stream; 8 | use axum::extract::{Path, State}; 9 | use axum::http::StatusCode; 10 | use axum::{Extension, Json}; 11 | use axum_streams::StreamBodyAs; 12 | use core::types::*; 13 | use sea_orm::EntityTrait; 14 | use std::sync::Arc; 15 | use uuid::Uuid; 16 | 17 | pub async fn get_build( 18 | state: State>, 19 | Extension(user): Extension, 20 | Path(build_id): Path, 21 | ) -> Result>, (StatusCode, Json>)> { 22 | let build = match EBuild::find_by_id(build_id).one(&state.db).await.unwrap() { 23 | Some(b) => b, 24 | None => { 25 | return Err(( 26 | StatusCode::NOT_FOUND, 27 | Json(BaseResponse { 28 | error: true, 29 | message: "Build not found".to_string(), 30 | }), 31 | )) 32 | } 33 | }; 34 | 35 | let evaluation = EEvaluation::find_by_id(build.evaluation) 36 | .one(&state.db) 37 | .await 38 | .unwrap() 39 | .unwrap(); 40 | let project = EProject::find_by_id(evaluation.project) 41 | .one(&state.db) 42 | .await 43 | .unwrap() 44 | .unwrap(); 45 | let organization = EOrganization::find_by_id(project.organization) 46 | .one(&state.db) 47 | .await 48 | .unwrap() 49 | .unwrap(); 50 | 51 | if organization.created_by != user.id { 52 | return Err(( 53 | StatusCode::NOT_FOUND, 54 | Json(BaseResponse { 55 | error: true, 56 | message: "Build not found".to_string(), 57 | }), 58 | )); 59 | } 60 | 61 | let res = BaseResponse { 62 | error: false, 63 | message: build, 64 | }; 65 | 66 | Ok(Json(res)) 67 | } 68 | 69 | pub async fn post_build( 70 | state: State>, 71 | Extension(user): Extension, 72 | Path(build_id): Path, 73 | ) -> Result, (StatusCode, Json>)> { 74 | let build = match EBuild::find_by_id(build_id).one(&state.db).await.unwrap() { 75 | Some(b) => b, 76 | None => { 77 | return Err(( 78 | StatusCode::NOT_FOUND, 79 | Json(BaseResponse { 80 | error: true, 81 | message: "Build not found".to_string(), 82 | }), 83 | )) 84 | } 85 | }; 86 | 87 | let evaluation = EEvaluation::find_by_id(build.evaluation) 88 | .one(&state.db) 89 | .await 90 | .unwrap() 91 | .unwrap(); 92 | let project = EProject::find_by_id(evaluation.project) 93 | .one(&state.db) 94 | .await 95 | .unwrap() 96 | .unwrap(); 97 | let organization = EOrganization::find_by_id(project.organization) 98 | .one(&state.db) 99 | .await 100 | .unwrap() 101 | .unwrap(); 102 | 103 | if organization.created_by != user.id { 104 | return Err(( 105 | StatusCode::NOT_FOUND, 106 | Json(BaseResponse { 107 | error: true, 108 | message: "Build not found".to_string(), 109 | }), 110 | )); 111 | } 112 | 113 | // TODO: check if build is building 114 | 115 | // watch build.log and stream it 116 | 117 | let stream = stream! { 118 | let mut last_log = build.log.unwrap_or("".to_string()); 119 | let mut first_response: bool = true; 120 | if !last_log.is_empty() { 121 | // TODO: Chunkify past log 122 | first_response = false; 123 | yield last_log.clone(); 124 | } 125 | 126 | loop { 127 | let build = EBuild::find_by_id(build_id).one(&state.db).await.unwrap().unwrap(); 128 | if build.status != entity::build::BuildStatus::Building { 129 | if first_response { 130 | yield "".to_string(); 131 | } 132 | 133 | break; 134 | } 135 | 136 | first_response = false; 137 | 138 | let log = build.log.unwrap_or("".to_string()); 139 | let log_new = log.replace(last_log.as_str(), ""); 140 | if !log_new.is_empty() { 141 | last_log = log.clone(); 142 | yield log_new.clone(); 143 | } 144 | } 145 | }; 146 | 147 | Ok(StreamBodyAs::json_nl(stream)) 148 | } 149 | -------------------------------------------------------------------------------- /backend/web/src/endpoints/commits.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | use axum::extract::{Path, State}; 8 | use axum::http::StatusCode; 9 | use axum::{Extension, Json}; 10 | use core::types::*; 11 | use sea_orm::EntityTrait; 12 | use std::sync::Arc; 13 | use uuid::Uuid; 14 | 15 | pub async fn get_commit( 16 | state: State>, 17 | Extension(_user): Extension, 18 | Path(commit_id): Path, 19 | ) -> Result>, (StatusCode, Json>)> { 20 | let commit = match ECommit::find_by_id(commit_id).one(&state.db).await.unwrap() { 21 | Some(b) => b, 22 | None => { 23 | return Err(( 24 | StatusCode::NOT_FOUND, 25 | Json(BaseResponse { 26 | error: true, 27 | message: "Commit not found".to_string(), 28 | }), 29 | )) 30 | } 31 | }; 32 | 33 | // TODO: Check if user has access to the commit 34 | 35 | let res = BaseResponse { 36 | error: false, 37 | message: commit, 38 | }; 39 | 40 | Ok(Json(res)) 41 | } 42 | -------------------------------------------------------------------------------- /backend/web/src/endpoints/mod.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | pub mod auth; 8 | pub mod builds; 9 | pub mod caches; 10 | pub mod commits; 11 | pub mod evals; 12 | pub mod orgs; 13 | pub mod projects; 14 | pub mod servers; 15 | pub mod user; 16 | 17 | use axum::extract::Json; 18 | use axum::http::StatusCode; 19 | use core::types::BaseResponse; 20 | 21 | pub async fn handle_404() -> (StatusCode, Json>) { 22 | ( 23 | StatusCode::NOT_FOUND, 24 | Json(BaseResponse { 25 | error: true, 26 | message: "Not Found".to_string(), 27 | }), 28 | ) 29 | } 30 | 31 | pub async fn get_health( 32 | ) -> Result>, (StatusCode, Json>)> { 33 | let res = BaseResponse { 34 | error: false, 35 | message: "200 ALIVE".to_string(), 36 | }; 37 | 38 | Ok(Json(res)) 39 | } 40 | -------------------------------------------------------------------------------- /cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gradient-cli" 3 | version = "0.2.0" 4 | edition = "2024" 5 | license = "AGPL-3.0" 6 | authors = ["Wavelens UG "] 7 | description = "CLI-Tool for Gradient" 8 | repository = "https://github.com/wavelens/gradient" 9 | 10 | [[bin]] 11 | name = "gradient" 12 | path = "src/main.rs" 13 | 14 | [workspace] 15 | members = [".", "connector"] 16 | 17 | [dependencies] 18 | clap = { version = "4.5", features = ["derive"] } 19 | rpassword = "7.4" 20 | clap_complete = "4" 21 | strum = "0.27" 22 | strum_macros = "0.27" 23 | serde = { version = "1.0", features = ["derive"] } 24 | dirs = "6.0" 25 | toml = "0.8" 26 | tokio = { version = "1.44", features = ["macros", "process", "rt-multi-thread"] } 27 | serde_json = "1.0" 28 | connector = { path = "connector" } 29 | -------------------------------------------------------------------------------- /cli/connector/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "connector" 3 | version = "0.2.0" 4 | edition = "2024" 5 | publish = false 6 | license = "AGPL-3.0" 7 | authors = ["Wavelens UG "] 8 | 9 | [lib] 10 | name = "connector" 11 | path = "src/lib.rs" 12 | 13 | [dependencies] 14 | futures = "0.3" 15 | reqwest = { version = "0.12", features = ["json"] } 16 | reqwest-streams = { version = "0.10", features=["json"] } 17 | serde = { version = "1.0", features = ["derive"] } 18 | serde_json = "1.0" 19 | -------------------------------------------------------------------------------- /cli/connector/src/auth.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | use crate::*; 8 | use serde::{Deserialize, Serialize}; 9 | 10 | #[derive(Serialize, Deserialize, Debug)] 11 | struct MakeUserRequest { 12 | pub username: String, 13 | pub name: String, 14 | pub email: String, 15 | pub password: String, 16 | } 17 | 18 | #[derive(Serialize, Deserialize, Debug)] 19 | struct MakeLoginRequest { 20 | pub loginname: String, 21 | pub password: String, 22 | } 23 | 24 | pub async fn post_basic_register( 25 | config: RequestConfig, 26 | username: String, 27 | name: String, 28 | email: String, 29 | password: String, 30 | ) -> Result, String> { 31 | let req = MakeUserRequest { 32 | username, 33 | name, 34 | email, 35 | password, 36 | }; 37 | 38 | let res = get_client( 39 | config, 40 | "auth/basic/register".to_string(), 41 | RequestType::POST, 42 | false, 43 | ) 44 | .unwrap() 45 | .json(&req) 46 | .send() 47 | .await 48 | .unwrap(); 49 | 50 | Ok(parse_response(res).await) 51 | } 52 | 53 | pub async fn post_basic_login( 54 | config: RequestConfig, 55 | loginname: String, 56 | password: String, 57 | ) -> Result, String> { 58 | let req = MakeLoginRequest { 59 | loginname, 60 | password, 61 | }; 62 | 63 | let res = get_client( 64 | config, 65 | "auth/basic/login".to_string(), 66 | RequestType::POST, 67 | false, 68 | ) 69 | .unwrap() 70 | .json(&req) 71 | .send() 72 | .await 73 | .unwrap(); 74 | 75 | Ok(parse_response(res).await) 76 | } 77 | 78 | pub async fn get_oauth_authorize(config: RequestConfig) -> Result, String> { 79 | let res = get_client( 80 | config, 81 | "auth/oauth/authorize".to_string(), 82 | RequestType::GET, 83 | false, 84 | ) 85 | .unwrap() 86 | .send() 87 | .await 88 | .unwrap(); 89 | 90 | Ok(parse_response(res).await) 91 | } 92 | 93 | pub async fn post_oauth_authorize( 94 | config: RequestConfig, 95 | code: String, 96 | ) -> Result, String> { 97 | let res = get_client( 98 | config, 99 | "auth/oauth/authorize".to_string(), 100 | RequestType::POST, 101 | false, 102 | ) 103 | .unwrap() 104 | .json(&code) 105 | .send() 106 | .await 107 | .unwrap(); 108 | 109 | Ok(parse_response(res).await) 110 | } 111 | 112 | pub async fn post_logout(config: RequestConfig) -> Result, String> { 113 | let res = get_client(config, "auth/logout".to_string(), RequestType::POST, false) 114 | .unwrap() 115 | .send() 116 | .await 117 | .unwrap(); 118 | 119 | Ok(parse_response(res).await) 120 | } 121 | -------------------------------------------------------------------------------- /cli/connector/src/builds.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | use crate::*; 8 | use futures::stream::StreamExt; 9 | use reqwest_streams::JsonStreamResponse; 10 | use serde::{Deserialize, Serialize}; 11 | 12 | #[derive(Serialize, Deserialize, Debug)] 13 | pub struct BuildResponse { 14 | pub id: String, 15 | pub evaluation: String, 16 | pub status: String, 17 | pub derivation_path: String, 18 | pub architecture: String, 19 | pub server: Option, 20 | pub log: Option, 21 | pub created_at: String, 22 | pub updated_at: String, 23 | } 24 | 25 | pub async fn get_build( 26 | config: RequestConfig, 27 | build_id: String, 28 | ) -> Result, String> { 29 | let res = get_client( 30 | config, 31 | format!("builds/{}", build_id), 32 | RequestType::GET, 33 | true, 34 | ) 35 | .unwrap() 36 | .send() 37 | .await 38 | .unwrap(); 39 | 40 | Ok(parse_response(res).await) 41 | } 42 | 43 | pub async fn post_build(config: RequestConfig, build_id: String) -> Result<(), String> { 44 | let mut stream = get_client( 45 | config, 46 | format!("builds/{}", build_id), 47 | RequestType::POST, 48 | true, 49 | ) 50 | .unwrap() 51 | .send() 52 | .await 53 | .unwrap() 54 | .json_nl_stream::(1024000); 55 | 56 | while let Some(chunk) = stream.next().await { 57 | match chunk { 58 | Ok(chunk) => print!("{}", chunk), 59 | Err(e) => return Err(e.to_string()), 60 | } 61 | } 62 | 63 | Ok(()) 64 | } 65 | -------------------------------------------------------------------------------- /cli/connector/src/caches.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | use crate::*; 8 | use serde::{Deserialize, Serialize}; 9 | 10 | #[derive(Serialize, Deserialize, Debug)] 11 | pub struct CacheResponse { 12 | pub id: String, 13 | pub name: String, 14 | pub active: bool, 15 | pub display_name: String, 16 | pub description: String, 17 | pub priority: i32, 18 | pub created_by: String, 19 | pub created_at: String, 20 | } 21 | 22 | #[derive(Serialize, Deserialize, Debug)] 23 | struct MakeCacheRequest { 24 | pub name: String, 25 | pub display_name: String, 26 | pub description: String, 27 | pub priority: i32, 28 | } 29 | 30 | #[derive(Serialize, Deserialize, Debug)] 31 | struct PatchCacheRequest { 32 | pub name: Option, 33 | pub display_name: Option, 34 | pub description: Option, 35 | pub priority: Option, 36 | } 37 | 38 | pub async fn get(config: RequestConfig) -> Result, String> { 39 | let res = get_client(config, "caches".to_string(), RequestType::GET, true) 40 | .unwrap() 41 | .send() 42 | .await; 43 | 44 | let res = match res { 45 | Ok(res) => res, 46 | Err(e) => return Err(e.to_string()), 47 | }; 48 | 49 | Ok(parse_response(res).await) 50 | } 51 | 52 | pub async fn put( 53 | config: RequestConfig, 54 | name: String, 55 | display_name: String, 56 | description: String, 57 | priority: i32, 58 | ) -> Result, String> { 59 | let req = MakeCacheRequest { 60 | name, 61 | display_name, 62 | description, 63 | priority, 64 | }; 65 | 66 | let res = get_client(config, "caches".to_string(), RequestType::PUT, true) 67 | .unwrap() 68 | .json(&req) 69 | .send() 70 | .await 71 | .unwrap(); 72 | 73 | Ok(parse_response(res).await) 74 | } 75 | 76 | pub async fn get_cache( 77 | config: RequestConfig, 78 | cache: String, 79 | ) -> Result, String> { 80 | let res = get_client( 81 | config, 82 | format!("caches/{}", cache), 83 | RequestType::GET, 84 | true, 85 | ) 86 | .unwrap() 87 | .send() 88 | .await 89 | .unwrap(); 90 | 91 | Ok(parse_response(res).await) 92 | } 93 | 94 | pub async fn patch_cache( 95 | config: RequestConfig, 96 | cache: String, 97 | name: Option, 98 | display_name: Option, 99 | description: Option, 100 | priority: Option, 101 | ) -> Result, String> { 102 | let req = PatchCacheRequest { 103 | name, 104 | display_name, 105 | description, 106 | priority, 107 | }; 108 | 109 | let res = get_client( 110 | config, 111 | format!("caches/{}", cache), 112 | RequestType::PATCH, 113 | true, 114 | ) 115 | .unwrap() 116 | .json(&req) 117 | .send() 118 | .await 119 | .unwrap(); 120 | 121 | Ok(parse_response(res).await) 122 | } 123 | 124 | pub async fn delete_cache( 125 | config: RequestConfig, 126 | cache: String, 127 | ) -> Result, String> { 128 | let res = get_client( 129 | config, 130 | format!("caches/{}", cache), 131 | RequestType::DELETE, 132 | true, 133 | ) 134 | .unwrap() 135 | .send() 136 | .await 137 | .unwrap(); 138 | 139 | Ok(parse_response(res).await) 140 | } 141 | 142 | pub async fn post_cache_active( 143 | config: RequestConfig, 144 | cache: String, 145 | ) -> Result, String> { 146 | let res = get_client( 147 | config, 148 | format!("caches/{}/active", cache), 149 | RequestType::POST, 150 | true, 151 | ) 152 | .unwrap() 153 | .send() 154 | .await 155 | .unwrap(); 156 | 157 | Ok(parse_response(res).await) 158 | } 159 | 160 | pub async fn delete_cache_active( 161 | config: RequestConfig, 162 | cache: String, 163 | ) -> Result, String> { 164 | let res = get_client( 165 | config, 166 | format!("caches/{}/active", cache), 167 | RequestType::DELETE, 168 | true, 169 | ) 170 | .unwrap() 171 | .send() 172 | .await 173 | .unwrap(); 174 | 175 | Ok(parse_response(res).await) 176 | } 177 | 178 | pub async fn get_cache_key( 179 | config: RequestConfig, 180 | cache: String, 181 | ) -> Result, String> { 182 | let res = get_client( 183 | config, 184 | format!("caches/{}/key", cache), 185 | RequestType::GET, 186 | true, 187 | ) 188 | .unwrap() 189 | .send() 190 | .await 191 | .unwrap(); 192 | 193 | Ok(parse_response(res).await) 194 | } 195 | -------------------------------------------------------------------------------- /cli/connector/src/commits.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | use crate::*; 8 | use serde::{Deserialize, Serialize}; 9 | 10 | #[derive(Serialize, Deserialize, Debug)] 11 | pub struct CommitResponse { 12 | pub id: String, 13 | pub message: String, 14 | pub hash: String, 15 | } 16 | 17 | pub async fn get_commit( 18 | config: RequestConfig, 19 | commit_id: String, 20 | ) -> Result, String> { 21 | let res = get_client( 22 | config, 23 | format!("commits/{}", commit_id), 24 | RequestType::GET, 25 | true, 26 | ) 27 | .unwrap() 28 | .send() 29 | .await 30 | .unwrap(); 31 | 32 | Ok(parse_response(res).await) 33 | } 34 | -------------------------------------------------------------------------------- /cli/connector/src/evals.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | use crate::*; 8 | use futures::stream::StreamExt; 9 | use reqwest_streams::JsonStreamResponse; 10 | use serde::{Deserialize, Serialize}; 11 | 12 | #[derive(Serialize, Deserialize, Debug)] 13 | pub struct EvaluationResponse { 14 | pub id: String, 15 | pub project: String, 16 | pub repository: String, 17 | pub commit: String, 18 | pub wildcard: String, 19 | pub status: String, 20 | pub previous: Option, 21 | pub next: Option, 22 | pub created_at: String, 23 | } 24 | 25 | pub async fn get_evaluation( 26 | config: RequestConfig, 27 | evaluation_id: String, 28 | ) -> Result, String> { 29 | let res = get_client( 30 | config, 31 | format!("evals/{}", evaluation_id), 32 | RequestType::GET, 33 | true, 34 | ) 35 | .unwrap() 36 | .send() 37 | .await 38 | .unwrap(); 39 | 40 | Ok(parse_response(res).await) 41 | } 42 | 43 | pub async fn post_evaluation( 44 | config: RequestConfig, 45 | evaluation_id: String, 46 | ) -> Result, String> { 47 | let res = get_client( 48 | config, 49 | format!("evals/{}", evaluation_id), 50 | RequestType::POST, 51 | true, 52 | ) 53 | .unwrap() 54 | .send() 55 | .await 56 | .unwrap(); 57 | 58 | Ok(parse_response(res).await) 59 | } 60 | 61 | pub async fn get_evaluation_builds( 62 | config: RequestConfig, 63 | evaluation_id: String, 64 | ) -> Result, String> { 65 | let res = get_client( 66 | config, 67 | format!("evals/{}/builds", evaluation_id), 68 | RequestType::GET, 69 | true, 70 | ) 71 | .unwrap() 72 | .send() 73 | .await 74 | .unwrap(); 75 | 76 | Ok(parse_response(res).await) 77 | } 78 | 79 | pub async fn post_evaluation_builds( 80 | config: RequestConfig, 81 | evaluation_id: String, 82 | ) -> Result<(), String> { 83 | let mut stream = get_client( 84 | config, 85 | format!("evals/{}/builds", evaluation_id), 86 | RequestType::POST, 87 | true, 88 | ) 89 | .unwrap() 90 | .send() 91 | .await 92 | .unwrap() 93 | .json_nl_stream::(1024000); 94 | 95 | while let Some(chunk) = stream.next().await { 96 | match chunk { 97 | Ok(chunk) => print!("{}", chunk), 98 | Err(e) => return Err(e.to_string()), 99 | } 100 | } 101 | 102 | Ok(()) 103 | } 104 | -------------------------------------------------------------------------------- /cli/connector/src/lib.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | pub mod auth; 8 | pub mod builds; 9 | pub mod caches; 10 | pub mod commits; 11 | pub mod evals; 12 | pub mod orgs; 13 | pub mod projects; 14 | pub mod servers; 15 | pub mod user; 16 | 17 | use serde::de::DeserializeOwned; 18 | use serde::{Deserialize, Serialize}; 19 | 20 | #[derive(Debug, Clone)] 21 | pub struct RequestConfig { 22 | pub server_url: String, 23 | pub token: Option, 24 | } 25 | 26 | #[derive(Serialize, Deserialize, Debug)] 27 | pub struct BaseResponse { 28 | pub error: bool, 29 | pub message: T, 30 | } 31 | 32 | #[derive(Debug, Clone, Serialize, Deserialize)] 33 | pub struct ListItem { 34 | pub id: String, 35 | pub name: String, 36 | } 37 | 38 | pub type ListResponse = Vec; 39 | pub type RequestType = reqwest::Method; 40 | 41 | async fn parse_response(res: reqwest::Response) -> BaseResponse { 42 | let bytes = match res.bytes().await { 43 | Ok(b) => b, 44 | Err(e) => { 45 | eprintln!("Failed to read response body: {}", e); 46 | std::process::exit(1); 47 | } 48 | }; 49 | 50 | match serde_json::from_slice::>(&bytes) { 51 | Ok(parsed_res) => parsed_res, 52 | Err(_) => match serde_json::from_slice::>(&bytes) { 53 | Ok(error_res) => { 54 | eprintln!("{}", error_res.message); 55 | std::process::exit(1); 56 | } 57 | Err(_) => { 58 | eprintln!("{}", String::from_utf8_lossy(&bytes)); 59 | std::process::exit(1); 60 | } 61 | }, 62 | } 63 | } 64 | 65 | // TODO: Better error handling for "connection refused" 66 | fn get_client( 67 | config: RequestConfig, 68 | endpoint: String, 69 | request_type: RequestType, 70 | login: bool, 71 | ) -> Result { 72 | let client = reqwest::Client::new(); 73 | let mut client = client.request( 74 | request_type, 75 | format!("{}/api/v1/{}", config.server_url, endpoint), 76 | ); 77 | 78 | client = client.header("Content-Type", "application/json"); 79 | 80 | if !login { 81 | return Ok(client); 82 | } 83 | 84 | let token = if let Some(token) = config.token { 85 | token 86 | } else { 87 | return Err("Token not set. Use `gradient login` to set it.\nIf you have an API Key use `gradient config authtoken [YourApiKeyHere]`.".to_string()); 88 | }; 89 | 90 | client = client.header("Authorization", format!("Bearer {}", token)); 91 | 92 | Ok(client) 93 | } 94 | 95 | pub async fn health(config: RequestConfig) -> Result, String> { 96 | let res = get_client(config, "health".to_string(), RequestType::GET, false) 97 | .unwrap() 98 | .send() 99 | .await; 100 | 101 | let res = match res { 102 | Ok(res) => res, 103 | Err(e) => return Err(e.to_string()), 104 | }; 105 | 106 | Ok(parse_response(res).await) 107 | } 108 | -------------------------------------------------------------------------------- /cli/connector/src/user.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | use crate::*; 8 | use serde::{Deserialize, Serialize}; 9 | 10 | #[derive(Serialize, Deserialize, Debug)] 11 | pub struct UserInfoResponse { 12 | pub id: String, 13 | pub username: String, 14 | pub name: String, 15 | pub email: String, 16 | } 17 | 18 | #[derive(Serialize, Deserialize, Debug)] 19 | struct PatchUserSettingsRequest { 20 | pub username: Option, 21 | pub name: Option, 22 | pub email: Option, 23 | } 24 | 25 | #[derive(Serialize, Deserialize, Debug)] 26 | pub struct GetUserSettingsResponse { 27 | pub username: String, 28 | pub name: String, 29 | pub email: String, 30 | } 31 | 32 | #[derive(Serialize, Deserialize, Debug)] 33 | struct MakeApiKeyRequest { 34 | pub name: String, 35 | } 36 | 37 | pub async fn get(config: RequestConfig) -> Result, String> { 38 | let res = get_client(config, "user".to_string(), RequestType::GET, true) 39 | .unwrap() 40 | .send() 41 | .await 42 | .unwrap(); 43 | 44 | Ok(parse_response(res).await) 45 | } 46 | 47 | pub async fn delete(config: RequestConfig) -> Result, String> { 48 | let res = get_client(config, "user".to_string(), RequestType::DELETE, true) 49 | .unwrap() 50 | .send() 51 | .await 52 | .unwrap(); 53 | 54 | Ok(parse_response(res).await) 55 | } 56 | 57 | pub async fn get_keys(config: RequestConfig) -> Result, String> { 58 | let res = get_client(config, "user/keys".to_string(), RequestType::GET, true) 59 | .unwrap() 60 | .send() 61 | .await 62 | .unwrap(); 63 | 64 | Ok(parse_response(res).await) 65 | } 66 | 67 | pub async fn post_key(config: RequestConfig, name: String) -> Result, String> { 68 | let req = MakeApiKeyRequest { name }; 69 | 70 | let res = get_client(config, "user/keys".to_string(), RequestType::POST, true) 71 | .unwrap() 72 | .json(&req) 73 | .send() 74 | .await 75 | .unwrap(); 76 | 77 | Ok(parse_response(res).await) 78 | } 79 | 80 | pub async fn delete_key( 81 | config: RequestConfig, 82 | name: String, 83 | ) -> Result, String> { 84 | let req = MakeApiKeyRequest { name }; 85 | 86 | let res = get_client(config, "user/keys".to_string(), RequestType::DELETE, true) 87 | .unwrap() 88 | .json(&req) 89 | .send() 90 | .await 91 | .unwrap(); 92 | 93 | Ok(parse_response(res).await) 94 | } 95 | 96 | pub async fn get_settings( 97 | config: RequestConfig, 98 | ) -> Result, String> { 99 | let res = get_client(config, "user/settings".to_string(), RequestType::GET, true) 100 | .unwrap() 101 | .send() 102 | .await 103 | .unwrap(); 104 | 105 | Ok(parse_response(res).await) 106 | } 107 | 108 | pub async fn patch_settings( 109 | config: RequestConfig, 110 | username: Option, 111 | name: Option, 112 | email: Option, 113 | ) -> Result, String> { 114 | let req = PatchUserSettingsRequest { 115 | username, 116 | name, 117 | email, 118 | }; 119 | 120 | let res = get_client( 121 | config, 122 | "user/settings".to_string(), 123 | RequestType::PATCH, 124 | true, 125 | ) 126 | .unwrap() 127 | .json(&req) 128 | .send() 129 | .await 130 | .unwrap(); 131 | 132 | Ok(parse_response(res).await) 133 | } 134 | -------------------------------------------------------------------------------- /cli/src/commands/build.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | use crate::config::*; 8 | use crate::input::*; 9 | use clap::{arg, Subcommand}; 10 | use connector::*; 11 | use std::process::exit; 12 | 13 | pub async fn handle_build(derivation: String) {} 14 | -------------------------------------------------------------------------------- /cli/src/commands/mod.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * spdx-filecopyrighttext: 2025 wavelens ug 3 | * 4 | * spdx-license-identifier: agpl-3.0-only 5 | */ 6 | 7 | pub mod base; 8 | pub mod build; 9 | pub mod cache; 10 | pub mod organization; 11 | pub mod project; 12 | pub mod server; 13 | -------------------------------------------------------------------------------- /cli/src/input.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | use super::config::*; 8 | use connector::RequestConfig; 9 | use rpassword::read_password; 10 | use std::collections::HashMap; 11 | use std::io::Write; 12 | use std::process::exit; 13 | use std::process::Command; 14 | use std::{fs, io}; 15 | 16 | pub fn handle_input(values: Vec<(String, Option)>, skip: bool) -> HashMap { 17 | if values.is_empty() { 18 | println!("No input fields"); 19 | exit(1); 20 | } 21 | 22 | if skip && !values.iter().any(|(_, v)| v.is_none()) { 23 | return values 24 | .iter() 25 | .map(|(k, v)| (k.clone(), v.clone().unwrap())) 26 | .collect(); 27 | } 28 | 29 | let input_fields: String = values 30 | .iter() 31 | .map(|(k, v)| { 32 | format!( 33 | "{}: {}\n", 34 | k, 35 | if let Some(val) = v { 36 | val.clone() 37 | } else { 38 | "".to_string() 39 | } 40 | ) 41 | }) 42 | .collect(); 43 | 44 | let name = format!("/tmp/GRADIENT-CONFIGURATOR-{}", std::process::id()); 45 | 46 | let mut file = fs::File::create(name.clone()).unwrap(); 47 | file.write_all(input_fields.as_bytes()).unwrap(); 48 | 49 | let editor = std::env::var("EDITOR").unwrap(); 50 | let output = Command::new(editor.clone()) 51 | .arg(name.clone()) 52 | .status() 53 | .unwrap(); 54 | 55 | if !output.success() { 56 | println!("Failed to open editor {}", editor); 57 | exit(1); 58 | } 59 | 60 | let contents = fs::read_to_string(name.clone()).unwrap(); 61 | fs::remove_file(name).unwrap(); 62 | 63 | let mut result: HashMap = HashMap::new(); 64 | for line in contents.lines() { 65 | let parts: Vec<&str> = line.split(":").map(|v| v.trim()).collect(); 66 | 67 | if !values.iter().any(|(k, _)| k == parts[0]) { 68 | eprintln!("Invalid input field: {}", parts[0]); 69 | exit(1); 70 | } 71 | 72 | if parts[1].is_empty() { 73 | eprintln!("{} cannot be empty.", parts[0]); 74 | exit(1); 75 | } 76 | 77 | result.insert(parts[0].to_string(), parts[1..].join(":").to_string()); 78 | } 79 | 80 | result 81 | } 82 | 83 | pub fn ask_for_password() -> String { 84 | print!("Password: "); 85 | std::io::stdout().flush().unwrap(); 86 | let inp = read_password().unwrap(); 87 | 88 | if inp.is_empty() { 89 | eprintln!("Password cannot be empty."); 90 | exit(1); 91 | } 92 | 93 | inp 94 | } 95 | 96 | pub fn ask_for_input(prompt: &str) -> String { 97 | print!("{}: ", prompt); 98 | std::io::stdout().flush().unwrap(); 99 | let mut inp = String::new(); 100 | io::stdin() 101 | .read_line(&mut inp) 102 | .expect(format!("Failed to read {}.", prompt).as_str()); 103 | let inp = inp.trim().to_string(); 104 | 105 | if inp.is_empty() { 106 | eprintln!("{} cannot be empty.", prompt); 107 | exit(1); 108 | } 109 | 110 | inp 111 | } 112 | 113 | pub fn get_request_config( 114 | config: HashMap>, 115 | ) -> Result { 116 | let server_url: String = 117 | if let Some(server_url) = config.get(&ConfigKey::Server).unwrap().clone() { 118 | server_url 119 | } else { 120 | return Err( 121 | "Server URL not set. Use `gradient config server ` to set it.".to_string(), 122 | ); 123 | }; 124 | 125 | let token = set_get_value(ConfigKey::AuthToken, None, true); 126 | 127 | Ok(RequestConfig { server_url, token }) 128 | } 129 | -------------------------------------------------------------------------------- /cli/src/main.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | mod commands; 8 | mod config; 9 | mod input; 10 | 11 | use commands::base; 12 | 13 | #[tokio::main] 14 | pub async fn main() -> std::io::Result<()> { 15 | base::run_cli().await 16 | } 17 | -------------------------------------------------------------------------------- /docs/gradient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wavelens/gradient/6eaf2657f5252df7c176527f0f5ceb14e60a9ffa/docs/gradient.png -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1731533236, 9 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "flake-utils_2": { 22 | "inputs": { 23 | "systems": "systems_2" 24 | }, 25 | "locked": { 26 | "lastModified": 1731533236, 27 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 28 | "owner": "numtide", 29 | "repo": "flake-utils", 30 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 31 | "type": "github" 32 | }, 33 | "original": { 34 | "owner": "numtide", 35 | "repo": "flake-utils", 36 | "type": "github" 37 | } 38 | }, 39 | "microvm": { 40 | "inputs": { 41 | "flake-utils": "flake-utils_2", 42 | "nixpkgs": [ 43 | "nixpkgs" 44 | ], 45 | "spectrum": "spectrum" 46 | }, 47 | "locked": { 48 | "lastModified": 1745262696, 49 | "narHash": "sha256-hbk/u7Tyl7PUw+e9fa2Vk3VKchy7zovEAjichIoZvTM=", 50 | "owner": "astro", 51 | "repo": "microvm.nix", 52 | "rev": "ae53cb29425c3077d7b088bec5d2bd9275594db3", 53 | "type": "github" 54 | }, 55 | "original": { 56 | "owner": "astro", 57 | "repo": "microvm.nix", 58 | "type": "github" 59 | } 60 | }, 61 | "nixpkgs": { 62 | "locked": { 63 | "lastModified": 1745930157, 64 | "narHash": "sha256-y3h3NLnzRSiUkYpnfvnS669zWZLoqqI6NprtLQ+5dck=", 65 | "owner": "NixOS", 66 | "repo": "nixpkgs", 67 | "rev": "46e634be05ce9dc6d4db8e664515ba10b78151ae", 68 | "type": "github" 69 | }, 70 | "original": { 71 | "owner": "NixOS", 72 | "ref": "nixos-unstable", 73 | "repo": "nixpkgs", 74 | "type": "github" 75 | } 76 | }, 77 | "root": { 78 | "inputs": { 79 | "flake-utils": "flake-utils", 80 | "microvm": "microvm", 81 | "nixpkgs": "nixpkgs" 82 | } 83 | }, 84 | "spectrum": { 85 | "flake": false, 86 | "locked": { 87 | "lastModified": 1733308308, 88 | "narHash": "sha256-+RcbMAjSxV1wW5UpS9abIG1lFZC8bITPiFIKNnE7RLs=", 89 | "ref": "refs/heads/main", 90 | "rev": "80c9e9830d460c944c8f730065f18bb733bc7ee2", 91 | "revCount": 792, 92 | "type": "git", 93 | "url": "https://spectrum-os.org/git/spectrum" 94 | }, 95 | "original": { 96 | "type": "git", 97 | "url": "https://spectrum-os.org/git/spectrum" 98 | } 99 | }, 100 | "systems": { 101 | "locked": { 102 | "lastModified": 1681028828, 103 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 104 | "owner": "nix-systems", 105 | "repo": "default", 106 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 107 | "type": "github" 108 | }, 109 | "original": { 110 | "owner": "nix-systems", 111 | "repo": "default", 112 | "type": "github" 113 | } 114 | }, 115 | "systems_2": { 116 | "locked": { 117 | "lastModified": 1681028828, 118 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 119 | "owner": "nix-systems", 120 | "repo": "default", 121 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 122 | "type": "github" 123 | }, 124 | "original": { 125 | "owner": "nix-systems", 126 | "repo": "default", 127 | "type": "github" 128 | } 129 | } 130 | }, 131 | "root": "root", 132 | "version": 7 133 | } 134 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | { 8 | description = "nix-based continuous integration system"; 9 | 10 | inputs = { 11 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 12 | flake-utils.url = "github:numtide/flake-utils"; 13 | microvm = { 14 | url = "github:astro/microvm.nix"; 15 | inputs.nixpkgs.follows = "nixpkgs"; 16 | }; 17 | }; 18 | 19 | outputs = { self, nixpkgs, microvm, flake-utils, ... }@inputs: flake-utils.lib.eachDefaultSystem (system: let 20 | pkgs = import nixpkgs { 21 | inherit system; 22 | overlays = map (v: self.overlays.${v}) (builtins.attrNames self.overlays); 23 | config = { allowUnfree = true; }; 24 | }; 25 | 26 | rustEnv = with pkgs.rustPackages; [ 27 | clippy 28 | ]; 29 | 30 | py = pkgs.python3.override { 31 | packageOverrides = python-final: python-prev: { 32 | django = python-final.django_4; 33 | }; 34 | }; 35 | 36 | pythonEnv = py.withPackages (ps: with ps; [ 37 | bleach 38 | celery 39 | channels 40 | channels-redis 41 | django 42 | django-compression-middleware 43 | django-debug-toolbar 44 | django-parler 45 | django-redis 46 | django-rosetta 47 | django-scheduler 48 | gunicorn 49 | mysqlclient 50 | redis 51 | requests 52 | selenium 53 | sentry-sdk 54 | uritemplate 55 | urllib3 56 | whitenoise 57 | xstatic-bootstrap 58 | xstatic-jquery 59 | xstatic-jquery-ui 60 | ]); 61 | in 62 | { 63 | checks = import ./nix/tests { inherit inputs system pkgs; }; 64 | packages = rec { 65 | gradient-server = pkgs.callPackage ./nix/packages/gradient-server.nix { }; 66 | gradient-frontend = pkgs.callPackage ./nix/packages/gradient-frontend.nix { }; 67 | gradient-cli = pkgs.callPackage ./nix/packages/gradient-cli.nix { }; 68 | gradient = gradient-cli; 69 | default = gradient-server; 70 | }; 71 | 72 | devShells.default = with pkgs; mkShell { 73 | buildInputs = [ 74 | stdenv.cc.cc.lib 75 | pam 76 | ]; 77 | 78 | packages = [ 79 | cargo 80 | pkg-config 81 | rustc 82 | rustfmt 83 | sea-orm-cli 84 | rustEnv 85 | 86 | boost 87 | gettext 88 | libsodium 89 | openssl 90 | sqlite 91 | pythonEnv 92 | postgresql_17 93 | pgadmin4-desktopmode 94 | nixVersions.latest 95 | zstd 96 | ]; 97 | 98 | nativeBuildInputs = [ 99 | pkg-config 100 | ]; 101 | 102 | EXTRA_CCFLAGS = "-I/usr/include"; 103 | RUST_BACKTRACE = 1; 104 | 105 | GRADIENT_DEBUG = "true"; 106 | GRADIENT_SERVE_URL = "http://localhost:3000"; 107 | GRADIENT_DATABASE_URL = "postgres://postgres:postgres@localhost:54321/gradient"; 108 | GRADIENT_MAX_CONCURRENT_EVALUATIONS = 1; 109 | GRADIENT_MAX_CONCURRENT_BUILDS = 10; 110 | GRADIENT_STORE_PATH = "./testing/store"; 111 | GRADIENT_CRYPT_SECRET_FILE = pkgs.writeText "crypt_secret_file" "aW52YWxpZAo="; 112 | GRADIENT_JWT_SECRET_FILE = pkgs.writeText "jwt_secret_file" "8a2eb7ba959570ff8842f148207524c7b8d731d7a1998584105e951599221f9d"; 113 | GRADIENT_SERVE_CACHE = "true"; 114 | GRADIENT_REPORT_ERRORS = "false"; 115 | }; 116 | }) // { 117 | overlays = { 118 | gradient-server = final: prev: { inherit (self.packages.${final.system}) gradient-server; }; 119 | gradient-frontend = final: prev: { inherit (self.packages.${final.system}) gradient-frontend; }; 120 | gradient-cli = final: prev: { inherit (self.packages.${final.system}) gradient-cli; }; 121 | default = final: prev: { inherit (self.packages.${final.system}) gradient-server gradient-frontend gradient-cli; }; 122 | }; 123 | 124 | nixosModules = rec { 125 | gradient = { config, lib, ... }: { 126 | imports = [ ./nix/modules/gradient.nix ]; 127 | nixpkgs.overlays = lib.mkIf config.services.gradient.enable [ 128 | self.overlays.default 129 | ]; 130 | }; 131 | 132 | default = gradient; 133 | }; 134 | 135 | nixosConfigurations."gradient-dev" = nixpkgs.lib.nixosSystem { 136 | system = "x86_64-linux"; 137 | modules = [ 138 | microvm.nixosModules.microvm 139 | ./nix/vm/base.nix 140 | ./nix/vm/defaults.nix 141 | ./nix/vm/postgresql.nix 142 | ./nix/vm/mDNS.nix 143 | ./nix/vm/nginx/default.nix 144 | ./nix/vm/nginx/grafana.nix 145 | ./nix/vm/monitoring/source/prometheus/default.nix 146 | ./nix/vm/monitoring/source/loki/default.nix 147 | ./nix/vm/monitoring/destination/grafana/default.nix 148 | ]; 149 | }; 150 | }; 151 | } 152 | -------------------------------------------------------------------------------- /frontend/dashboard/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wavelens/gradient/6eaf2657f5252df7c176527f0f5ceb14e60a9ffa/frontend/dashboard/__init__.py -------------------------------------------------------------------------------- /frontend/dashboard/apps.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 Wavelens UG 2 | # 3 | # SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | from django.apps import AppConfig 6 | 7 | 8 | class DashboardConfig(AppConfig): 9 | default_auto_field = 'django.db.models.BigAutoField' 10 | name = 'dashboard' 11 | -------------------------------------------------------------------------------- /frontend/dashboard/context_processors.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 Wavelens UG 2 | # 3 | # SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | def global_variables(request): 6 | return { 7 | 'projectname' : 'vWorkflow', 8 | 'model' : 'vModel', 9 | 'user' : 'testuser', 10 | 'success' : 'waiting', 11 | } 12 | -------------------------------------------------------------------------------- /frontend/dashboard/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wavelens/gradient/6eaf2657f5252df7c176527f0f5ceb14e60a9ffa/frontend/dashboard/migrations/__init__.py -------------------------------------------------------------------------------- /frontend/dashboard/models.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 Wavelens UG 2 | # 3 | # SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | from django.db import models 6 | 7 | # Create your models here. 8 | -------------------------------------------------------------------------------- /frontend/dashboard/static/dashboard/icons/activity.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/dashboard/static/dashboard/icons/arrow_back.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/dashboard/static/dashboard/icons/command.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/dashboard/static/dashboard/icons/download.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/dashboard/static/dashboard/images/check-circle.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | -------------------------------------------------------------------------------- /frontend/dashboard/static/dashboard/images/circle.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/dashboard/static/dashboard/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wavelens/gradient/6eaf2657f5252df7c176527f0f5ceb14e60a9ffa/frontend/dashboard/static/dashboard/images/favicon.png -------------------------------------------------------------------------------- /frontend/dashboard/static/dashboard/images/more-vertical.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/dashboard/static/dashboard/images/pb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wavelens/gradient/6eaf2657f5252df7c176527f0f5ceb14e60a9ffa/frontend/dashboard/static/dashboard/images/pb.png -------------------------------------------------------------------------------- /frontend/dashboard/static/dashboard/images/pb2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wavelens/gradient/6eaf2657f5252df7c176527f0f5ceb14e60a9ffa/frontend/dashboard/static/dashboard/images/pb2.png -------------------------------------------------------------------------------- /frontend/dashboard/static/dashboard/images/wavelens-LOGO.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /frontend/dashboard/static/dashboard/images/x-circle.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | -------------------------------------------------------------------------------- /frontend/dashboard/static/dashboard/js/log.js: -------------------------------------------------------------------------------- 1 | async function makeRequest() { 2 | try { 3 | fetch(url, { 4 | method: "POST", 5 | credentials: "include", 6 | withCredentials: true, 7 | mode: "cors", 8 | headers: { 9 | "Authorization": `Bearer ${token}`, 10 | "Content-Type": "application/jsonstream", 11 | }, 12 | }).then(async (response) => { 13 | const reader = response.body.getReader(); 14 | const logContainer = document.querySelector(".details-content"); 15 | 16 | while (true) { 17 | const { done, value } = await reader.read(); 18 | const text = new TextDecoder("utf-8").decode(value); 19 | 20 | if (done) { 21 | logContainer.innerHTML += `
End of Log
`; 22 | break; 23 | } 24 | 25 | if (text) { 26 | try { 27 | const data = JSON.parse(text); 28 | 29 | if (data.hasOwnProperty("error")) { 30 | console.error(data["message"]); 31 | } else { 32 | logContainer.innerHTML += `
${data.message}
`; 33 | logContainer.scrollTop = logContainer.scrollHeight; 34 | } 35 | } catch (err) { 36 | console.error("JSON parsing error:", err); 37 | } 38 | } 39 | } 40 | }); 41 | } catch (error) { 42 | console.error("Error during fetch:", error); 43 | } 44 | } 45 | 46 | makeRequest(); 47 | -------------------------------------------------------------------------------- /frontend/dashboard/static/dashboard/js/main.js: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | document.addEventListener("DOMContentLoaded", function() { 8 | const links = document.querySelectorAll('.nav-link'); 9 | const currentURL = window.location.href; 10 | links.forEach(link => { 11 | if (link.href === currentURL) { 12 | link.classList.add('active'); 13 | } 14 | }); 15 | }); 16 | 17 | function toggleDropdown(openDropdownId, openTooltipId, closeDropdownId, closeTooltipId) { 18 | const openDropdown = document.getElementById(openDropdownId); 19 | const openTooltip = document.getElementById(openTooltipId); 20 | const closeDropdown = document.getElementById(closeDropdownId); 21 | const closeTooltip = document.getElementById(closeTooltipId); 22 | 23 | if (closeDropdown.classList.contains("show")) { 24 | closeDropdown.classList.remove("show"); 25 | closeTooltip.classList.remove("hidden"); 26 | } 27 | 28 | openDropdown.classList.toggle("show"); 29 | 30 | if (openDropdown.classList.contains("show")) { 31 | openTooltip.classList.add("hidden"); 32 | } else { 33 | openTooltip.classList.remove("hidden"); 34 | } 35 | } 36 | 37 | window.onclick = function (event) { 38 | if (!event.target.closest(".dropdown")) { 39 | document.querySelectorAll(".dropdown-content").forEach(dropdown => dropdown.classList.remove("show")); 40 | document.querySelectorAll(".tooltiptext").forEach(tooltip => tooltip.classList.remove("hidden")); 41 | } 42 | }; 43 | 44 | -------------------------------------------------------------------------------- /frontend/dashboard/templates/backHeader.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | {% load static %} 9 | {% load i18n %} 10 | 11 | 12 |
13 | 14 |

15 | {% if success != 'waiting' %} 16 |
17 | 18 | {% if success == 'true' %}check_circle{% else %}cancel{% endif %} 19 | 20 | {% else %} 21 |
22 |
23 | {% endif %} 24 | {{ built_name }} 25 | #{{ id }} 26 |
27 |

28 |
29 | 30 | -------------------------------------------------------------------------------- /frontend/dashboard/templates/dashboard/download.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | {% extends 'dashboard/index.html' %} 8 | 9 | 10 | {% load static %} 11 | {% load i18n %} 12 | 13 | 14 | {% block title %}{% trans 'Download' %}{% endblock %} 15 | 16 | {% block content %} 17 | 18 | {% include "../individualHeader.html" %} 19 | 20 | 21 | 22 |
23 | 33 |
34 |
35 | {% trans 'Type' %} {% trans 'Link' %} {% trans 'Actions' %} 36 |
37 | {% for file in files %} 38 | 39 |
40 |
41 | 42 | {{ file.file }} 43 | 44 | 45 | {{ file.type }} 46 | 47 | 48 | {{ file.link }} 49 | 50 | 51 | {{ file.actions }} 52 | 53 |
54 |
55 | {% endfor %} 56 |
57 | 59 |
60 | {% endblock %} -------------------------------------------------------------------------------- /frontend/dashboard/templates/dashboard/home.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | {% extends 'dashboard/index.html' %} 8 | 9 | 10 | 11 | {% load static %} 12 | {% load i18n %} 13 | 14 | 15 | {% block title %}{% trans 'Home' %}{% endblock %} 16 | 17 | {% block content %} 18 | 19 |
20 |
21 |
22 |

{% trans 'Organizations' %}

23 |
24 |
25 |
26 |
27 |
28 | 29 | search 30 | 31 | 32 |
33 |
34 |
35 | 36 | 37 |
38 | {% for block in details_blocks %} 39 |
40 | 41 | {{ block.display_name }}
{% trans block.description %} 42 |
43 |
44 | 50 | 51 | 52 | graph_1 53 | 54 | {{ block.id }} 55 | 56 |
57 |
58 | {% endfor %} 59 |
60 |
61 | 62 | 81 | {% endblock %} 82 | -------------------------------------------------------------------------------- /frontend/dashboard/templates/dashboard/index.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | {% block title %}{% endblock %} 14 | {% load static %} 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | {% include "../header.html" %} 25 |
26 | {% block content %} 27 | 28 | {% endblock %} 29 |
30 | 31 |
32 | 33 |
34 | 35 | 36 | -------------------------------------------------------------------------------- /frontend/dashboard/templates/dashboard/model.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | {% extends 'dashboard/index.html' %} 8 | 9 | 10 | 11 | {% load static %} 12 | {% load i18n %} 13 | 14 | 15 | {% block title %}{% trans 'Models' %}{% endblock %} 16 | 17 | {% block content %} 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | {% for model in models %} 29 | 30 | 31 | 32 | 33 | {% endfor %} 34 | 35 |
{% trans 'Name' %}{% trans 'Description' %}
{{ model.name }}{{ model.description }}
36 |
37 | {% endblock %} 38 | -------------------------------------------------------------------------------- /frontend/dashboard/templates/dashboard/newOrganization.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | {% extends 'dashboard/index.html' %} 8 | 9 | 10 | {% load static %} 11 | {% load i18n %} 12 | 13 | 14 | {% block title %}{% trans 'New Organization' %}{% endblock %} 15 | 16 | {% block content %} 17 |
18 |
19 |

{% trans 'New Organization' %}

20 |
21 | {% csrf_token %} 22 | {% for field in form %} 23 | {% if field.field.widget|default:"" == form.fields.remember_me.widget %} 24 |
25 | {{ field }} 26 | 27 |
28 | {% else %} 29 |
30 | 31 | {{ field }} 32 | {% if field.errors %} 33 |
{{ field.errors }}
34 | {% endif %} 35 |
36 | {% endif %} 37 | {% endfor %} 38 | 39 |
40 |
41 |
42 | 45 | {% endblock %} -------------------------------------------------------------------------------- /frontend/dashboard/templates/dashboard/newProject.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | {% extends 'dashboard/index.html' %} 8 | 9 | 10 | {% load static %} 11 | {% load i18n %} 12 | 13 | 14 | {% block title %}{% trans 'New Project' %}{% endblock %} 15 | 16 | {% block content %} 17 |
18 |
19 |

{% trans 'New Projekt' %}

20 | {% if form.non_field_errors %} 21 |
22 | {{ form.non_field_errors }} 23 |
24 | {% endif %} 25 |
26 | {% csrf_token %} 27 | {% for field in form %} 28 | {% if field.field.widget|default:"" == form.fields.remember_me.widget %} 29 |
30 | {{ field }} 31 | 32 |
33 | {% else %} 34 |
35 | 36 | {{ field }} 37 | {% if field.errors %} 38 |
{{ field.errors }}
39 | {% endif %} 40 |
41 | {% endif %} 42 | {% endfor %} 43 | 44 |
45 |
46 |
47 | 50 | {% endblock %} -------------------------------------------------------------------------------- /frontend/dashboard/templates/dashboard/newServer.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | {% extends 'dashboard/index.html' %} 8 | 9 | 10 | {% load static %} 11 | {% load i18n %} 12 | 13 | 14 | {% block title %}{% trans 'New Server' %}{% endblock %} 15 | 16 | {% block content %} 17 |
18 |
19 |

{% trans 'New Server' %}

20 |
21 | {% csrf_token %} 22 | {% for field in form %} 23 | {% if field.field.widget|default:"" == form.fields.remember_me.widget %} 24 |
25 | {{ field }} 26 | 27 |
28 | {% else %} 29 |
30 | 31 | {{ field }} 32 | {% if field.errors %} 33 |
{% trans field.errors %}
34 | {% endif %} 35 |
36 | {% endif %} 37 | {% endfor %} 38 | 39 |
40 |
41 |
42 | 45 | {% endblock %} -------------------------------------------------------------------------------- /frontend/dashboard/templates/header.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | {% load i18n %} 9 | {% load static %} 10 | 11 | 12 |
13 |
14 |
15 | Logo 16 |
17 | {% if request.user.is_authenticated %} 18 |
19 | 35 | 54 |
55 | {% endif %} 56 |
57 |
58 | -------------------------------------------------------------------------------- /frontend/dashboard/templates/individualHeader.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | {% load i18n %} 8 | {% load static %} 9 | 10 | {% if org_id %} 11 |
12 |
13 | 14 | 15 | {% if project_id %} 16 | {{ org_id }}/ {{ project_id }} 17 | {% else %} 18 | {{ org_id }} 19 | {% endif %} 20 |
21 | 34 |
35 | {% endif %} 36 | -------------------------------------------------------------------------------- /frontend/dashboard/templates/login.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | {% extends 'dashboard/index.html' %} 8 | 9 | 10 | {% load static %} 11 | {% load i18n %} 12 | 13 | 14 | {% block title %}{% trans 'Login' %}{% endblock %} 15 | 16 | {% block content %} 17 |
18 |
19 |

{% trans 'Login' %}

20 | {% if form.non_field_errors %} 21 |
22 | {{ form.non_field_errors }} 23 |
24 | {% endif %} 25 |
26 | {% csrf_token %} 27 | {% for field in form %} 28 | {% if field.field.widget|default:"" == form.fields.remember_me.widget %} 29 |
30 | {{ field }} 31 | 32 |
33 | {% else %} 34 |
35 | 36 | {{ field }} 37 | {% if field.errors %} 38 |
{% trans field.errors %}
39 | {% endif %} 40 |
41 | {% endif %} 42 | {% endfor %} 43 | 44 |
45 | {% trans 'Register' %} 46 |
47 |
48 | 51 | {% endblock %} -------------------------------------------------------------------------------- /frontend/dashboard/templates/register.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | {% extends 'dashboard/index.html' %} 8 | 9 | 10 | {% load static %} 11 | {% load i18n %} 12 | 13 | 14 | {% block title %}{% trans 'Register' %}{% endblock %} 15 | 16 | {% block content %} 17 |
18 |
19 |

{% trans 'Register' %}

20 |
21 | {% csrf_token %} 22 | {% for field in form %} 23 |
24 | 25 | {{ field }} 26 | {% if field.errors %} 27 |
{% trans field.errors %}
28 | {% endif %} 29 |
30 | {% endfor %} 31 | 32 |
33 |
34 |
35 | 38 | {% endblock %} -------------------------------------------------------------------------------- /frontend/dashboard/tests.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 Wavelens UG 2 | # 3 | # SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | from django.test import TestCase 6 | 7 | # Create your tests here. 8 | -------------------------------------------------------------------------------- /frontend/dashboard/urls.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 Wavelens UG 2 | # 3 | # SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | from django.urls import path 6 | 7 | from .views import * 8 | 9 | urlpatterns = [ 10 | path("account/login", UserLoginView.as_view(), name="login"), 11 | path("account/register", register, name="register"), 12 | path("account/logout", logout_view, name="logout"), 13 | 14 | path("", workflow, name="workflow"), 15 | path("/log", log, name="log"), 16 | path("/log/", log, name="log-eval"), 17 | path("/download", download, name="download"), 18 | path("/download/", download, name="download-eval"), 19 | path("/model", model, name="model"), 20 | path("/model/", model, name="model-eval"), 21 | 22 | path("new/organization", new_organization, name="new_organization"), 23 | path("new/project", new_project, name="new_project"), 24 | path("new/server", new_server, name="new_server"), 25 | 26 | path("", home, name="home"), 27 | ] 28 | -------------------------------------------------------------------------------- /frontend/frontend/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wavelens/gradient/6eaf2657f5252df7c176527f0f5ceb14e60a9ffa/frontend/frontend/__init__.py -------------------------------------------------------------------------------- /frontend/frontend/asgi.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 Wavelens UG 2 | # 3 | # SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | """ 6 | ASGI config for frontend project. 7 | 8 | It exposes the ASGI callable as a module-level variable named ``application``. 9 | 10 | For more information on this file, see 11 | https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/ 12 | """ 13 | 14 | import os 15 | from django.core.asgi import get_asgi_application 16 | 17 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'frontend.settings') 18 | django_asgi_app = get_asgi_application() 19 | 20 | from channels.routing import ProtocolTypeRouter 21 | 22 | application = ProtocolTypeRouter( 23 | { 24 | "http": django_asgi_app, 25 | } 26 | ) 27 | -------------------------------------------------------------------------------- /frontend/frontend/urls.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 Wavelens UG 2 | # 3 | # SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | """ 6 | URL configuration for frontend project. 7 | 8 | The `urlpatterns` list routes URLs to views. For more information please see: 9 | https://docs.djangoproject.com/en/4.2/topics/http/urls/ 10 | Examples: 11 | Function views 12 | 1. Add an import: from my_app import views 13 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 14 | Class-based views 15 | 1. Add an import: from other_app.views import Home 16 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 17 | Including another URLconf 18 | 1. Import the include() function: from django.urls import include, path 19 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 20 | """ 21 | from django.urls import path 22 | from django.urls import include, path 23 | 24 | urlpatterns = [ 25 | path('', include('dashboard.urls')), 26 | ] 27 | -------------------------------------------------------------------------------- /frontend/frontend/wsgi.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 Wavelens UG 2 | # 3 | # SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | """ 6 | WSGI config for frontend project. 7 | 8 | It exposes the WSGI callable as a module-level variable named ``application``. 9 | 10 | For more information on this file, see 11 | https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/ 12 | """ 13 | 14 | import os 15 | 16 | from django.core.wsgi import get_wsgi_application 17 | 18 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'frontend.settings') 19 | 20 | application = get_wsgi_application() 21 | -------------------------------------------------------------------------------- /frontend/manage.py: -------------------------------------------------------------------------------- 1 | #!/nix/store/l014xp1qxdl6gim3zc0jv3mpxhbp346s-python3-3.12.4/bin/python 2 | # SPDX-FileCopyrightText: 2025 Wavelens UG 3 | # 4 | # SPDX-License-Identifier: AGPL-3.0-only 5 | 6 | """Django's command-line utility for administrative tasks.""" 7 | import os 8 | import sys 9 | 10 | 11 | def main(): 12 | """Run administrative tasks.""" 13 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'frontend.settings') 14 | try: 15 | from django.core.management import execute_from_command_line 16 | except ImportError as exc: 17 | raise ImportError( 18 | "Couldn't import Django. Are you sure it's installed and " 19 | "available on your PYTHONPATH environment variable? Did you " 20 | "forget to activate a virtual environment?" 21 | ) from exc 22 | execute_from_command_line(sys.argv) 23 | 24 | 25 | if __name__ == '__main__': 26 | main() 27 | -------------------------------------------------------------------------------- /nix/modules/gradient-frontend.nix: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | { lib, pkgs, config, ... }: let 8 | gradientCfg = config.services.gradient; 9 | cfg = config.services.gradient.frontend; 10 | in { 11 | options = { 12 | services.gradient.frontend = { 13 | enable = lib.mkEnableOption "Enable Gradient Frontend"; 14 | package = lib.mkPackageOption pkgs "gradient-frontend" { }; 15 | listenAddr = lib.mkOption { 16 | description = "The IP address on which Gradient listens."; 17 | type = lib.types.str; 18 | default = gradientCfg.listenAddr; 19 | }; 20 | 21 | port = lib.mkOption { 22 | description = "The port on which Gradient listens."; 23 | type = lib.types.port; 24 | default = 3001; 25 | }; 26 | 27 | apiUrl = lib.mkOption { 28 | description = "The URL of the Gradient API."; 29 | type = lib.types.str; 30 | default = "http://127.0.0.1:${toString gradientCfg.port}"; 31 | }; 32 | }; 33 | }; 34 | 35 | config = lib.mkIf (cfg.enable && gradientCfg.enable) { 36 | systemd.services.gradient-frontend = { 37 | wantedBy = [ "multi-user.target" ]; 38 | after = [ 39 | "network.target" 40 | "gradient-server.service" 41 | ]; 42 | 43 | preStart = '' 44 | ${lib.getExe cfg.package} migrate 45 | ''; 46 | 47 | serviceConfig = { 48 | ExecStart = "${lib.getExe cfg.package.python.pkgs.gunicorn} --bind ${cfg.listenAddr}:${toString cfg.port} --worker-tmp-dir /dev/shm frontend.wsgi:application"; 49 | StateDirectory = "gradient"; 50 | User = "gradient"; 51 | Group = "gradient"; 52 | ProtectHome = true; 53 | ProtectHostname = true; 54 | ProtectKernelLogs = true; 55 | ProtectKernelModules = true; 56 | ProtectKernelTunables = true; 57 | ProtectProc = "invisible"; 58 | ProtectSystem = "strict"; 59 | Restart = "on-failure"; 60 | RestartSec = 10; 61 | RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ]; 62 | RestrictNamespaces = true; 63 | RestrictRealtime = true; 64 | RestrictSUIDSGID = true; 65 | LoadCredential = [ 66 | "gradient_crypt_secret:${gradientCfg.cryptSecretFile}" 67 | ]; 68 | }; 69 | 70 | environment = { 71 | PYTHONPATH = "${cfg.package.python.pkgs.makePythonPath cfg.package.propagatedBuildInputs}:${cfg.package}/lib/gradient-frontend"; 72 | GRADIENT_DEBUG = "false"; 73 | GRADIENT_FRONTEND_IP = cfg.listenAddr; 74 | GRADIENT_FRONTEND_PORT = toString cfg.port; 75 | GRADIENT_API_URL = cfg.apiUrl; 76 | GRADIENT_SERVE_URL = "https://${gradientCfg.domain}"; 77 | GRADIENT_BASE_PATH = gradientCfg.baseDir; 78 | GRADIENT_OAUTH_ENABLE = lib.boolToString gradientCfg.oauth.enable; 79 | GRADIENT_DISABLE_REGISTER = lib.boolToString gradientCfg.settings.disableRegistration; 80 | GRADIENT_MAX_CONCURRENT_EVALUATIONS = toString gradientCfg.settings.maxConcurrentEvaluations; 81 | GRADIENT_MAX_CONCURRENT_BUILDS = toString gradientCfg.settings.maxConcurrentBuilds; 82 | GRADIENT_CRYPT_SECRET_FILE = "%d/gradient_crypt_secret"; 83 | GRADIENT_SERVE_CACHE = lib.boolToString gradientCfg.serveCache; 84 | GRADIENT_REPORT_ERRORS = lib.boolToString gradientCfg.reportErrors; 85 | } // lib.optionalAttrs gradientCfg.oauth.enable { 86 | GRADIENT_OAUTH_REQUIRED = lib.boolToString cfg.oauth.required; 87 | }; 88 | }; 89 | }; 90 | } 91 | -------------------------------------------------------------------------------- /nix/packages/gradient-cli.nix: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | { lib 8 | , git 9 | , installShellFiles 10 | , nix 11 | , nixVersions 12 | , openssl 13 | , pkg-config 14 | , rustPlatform 15 | }: let 16 | ignoredPaths = [ ".github" "target" ]; 17 | in rustPlatform.buildRustPackage { 18 | pname = "gradient-cli"; 19 | version = "0.2.0"; 20 | 21 | src = lib.cleanSourceWith { 22 | filter = name: type: !(type == "directory" && builtins.elem (baseNameOf name) ignoredPaths); 23 | src = lib.cleanSource ../../cli; 24 | }; 25 | 26 | nativeBuildInputs = [ 27 | installShellFiles 28 | pkg-config 29 | ]; 30 | 31 | buildInputs = [ 32 | git 33 | nix 34 | nixVersions.latest 35 | openssl 36 | pkg-config 37 | ]; 38 | 39 | cargoLock.lockFile = ../../cli/Cargo.lock; 40 | 41 | NIX_INCLUDE_PATH = "${lib.getDev nix}/include"; 42 | 43 | meta = { 44 | description = "Gradient Server "; 45 | homepage = "https://github.com/wavelens/gradient"; 46 | license = lib.licenses.agpl3Only; 47 | platforms = lib.platforms.unix; 48 | mainProgram = "gradient"; 49 | }; 50 | } 51 | -------------------------------------------------------------------------------- /nix/packages/gradient-frontend.nix: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | { lib 8 | , gettext 9 | , python3 10 | }: let 11 | python = python3; 12 | ignoredPaths = [ ".github" "target" ]; 13 | in python.pkgs.buildPythonApplication rec { 14 | pname = "gradient-frontend"; 15 | version = "0.2.0"; 16 | pyproject = false; 17 | 18 | src = lib.cleanSourceWith { 19 | filter = name: type: !(type == "directory" && builtins.elem (baseNameOf name) ignoredPaths); 20 | src = lib.cleanSource ../../frontend; 21 | }; 22 | 23 | nativeBuildInputs = [ 24 | gettext 25 | ]; 26 | 27 | dependencies = with python.pkgs; [ 28 | bleach 29 | celery 30 | channels 31 | channels-redis 32 | django 33 | django-compression-middleware 34 | django-debug-toolbar 35 | django-parler 36 | django-redis 37 | django-rosetta 38 | django-scheduler 39 | gunicorn 40 | mysqlclient 41 | redis 42 | requests 43 | selenium 44 | sentry-sdk 45 | uritemplate 46 | urllib3 47 | whitenoise 48 | xstatic-bootstrap 49 | xstatic-jquery 50 | xstatic-jquery-ui 51 | ]; 52 | 53 | postBuild = '' 54 | ${python.pythonOnBuildForHost.interpreter} -OO -m compileall . 55 | ${python.pythonOnBuildForHost.interpreter} manage.py collectstatic --clear --no-input 56 | ${python.pythonOnBuildForHost.interpreter} manage.py compilemessages 57 | ''; 58 | 59 | installPhase = let 60 | pythonPath = python.pkgs.makePythonPath dependencies; 61 | in '' 62 | runHook preInstall 63 | 64 | mkdir -p $out/lib/gradient-frontend/static/dashboard 65 | cp -r {dashboard,static,frontend,locale,manage.py} $out/lib/gradient-frontend 66 | chmod +x $out/lib/gradient-frontend/manage.py 67 | 68 | makeWrapper $out/lib/gradient-frontend/manage.py $out/bin/gradient-frontend \ 69 | --prefix PYTHONPATH : "${pythonPath}" 70 | 71 | runHook postInstall 72 | ''; 73 | 74 | passthru = { 75 | inherit python; 76 | }; 77 | 78 | meta = { 79 | description = "Nix Continuous Integration System Frontend"; 80 | homepage = "https://github.com/wavelens/gradient"; 81 | license = lib.licenses.agpl3Only; 82 | platforms = lib.platforms.unix; 83 | mainProgram = "gradient-frontend"; 84 | }; 85 | } 86 | -------------------------------------------------------------------------------- /nix/packages/gradient-server.nix: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | { lib 8 | , git 9 | , installShellFiles 10 | , nix 11 | , nixVersions 12 | , openssl 13 | , pkg-config 14 | , rustPlatform 15 | , zstd 16 | }: let 17 | ignoredPaths = [ ".github" "target" ]; 18 | in rustPlatform.buildRustPackage { 19 | pname = "gradient-server"; 20 | version = "0.2.0"; 21 | 22 | src = lib.cleanSourceWith { 23 | filter = name: type: !(type == "directory" && builtins.elem (baseNameOf name) ignoredPaths); 24 | src = lib.cleanSource ../../backend; 25 | }; 26 | 27 | nativeBuildInputs = [ 28 | installShellFiles 29 | pkg-config 30 | ]; 31 | 32 | buildInputs = [ 33 | git 34 | nix 35 | nixVersions.latest 36 | openssl 37 | pkg-config 38 | zstd 39 | ]; 40 | 41 | cargoLock = { 42 | lockFile = ../../backend/Cargo.lock; 43 | outputHashes."nix-daemon-0.1.2" = "sha256-VOvtYN1+QwHmLqoGS5N5e9Wrtba+RY9vSPuBw/7hu9o="; 44 | allowBuiltinFetchGit = true; 45 | }; 46 | 47 | NIX_INCLUDE_PATH = "${lib.getDev nix}/include"; 48 | 49 | meta = { 50 | description = "Nix Continuous Integration System Backend"; 51 | homepage = "https://github.com/wavelens/gradient"; 52 | license = lib.licenses.agpl3Only; 53 | platforms = lib.platforms.unix; 54 | mainProgram = "gradient-server"; 55 | }; 56 | } 57 | -------------------------------------------------------------------------------- /nix/scripts/postgres.nix: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | { pkgs, stdenv, ... }: let 8 | psql = pkgs.postgresql_17; 9 | in 10 | stdenv.mkDerivation { 11 | pname = "start-postgres"; 12 | version = "0"; 13 | 14 | src = pkgs.fetchurl { 15 | executable = true; 16 | url = "https://raw.githubusercontent.com/sapcc/keppel/refs/heads/master/testing/with-postgres-db.sh"; 17 | hash = "sha256-zso9toCgwDwkzHga01gEYPCNYg6GeAp4JcdnmPBgopM="; 18 | }; 19 | 20 | dontUnpack = true; 21 | 22 | installPhase = '' 23 | mkdir -p $out/bin 24 | cp $src $out/bin/start-postgres 25 | 26 | substituteInPlace $out/bin/start-postgres --replace pg_ctl ${psql}/bin/pg_ctl 27 | substituteInPlace $out/bin/start-postgres --replace initdb ${psql}/bin/initdb 28 | 29 | cat $out/bin/start-postgres 30 | 31 | chmod +x $out/bin/start-postgres 32 | ''; 33 | 34 | buildInputs = [ pkgs.bash psql ]; 35 | 36 | } 37 | -------------------------------------------------------------------------------- /nix/tests/default.nix: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | { inputs, pkgs, ... }: 8 | let 9 | inherit (inputs) nixpkgs; 10 | inherit (nixpkgs) lib; 11 | 12 | map-folder = path: builtins.map (name: path + "/" + name); 13 | 14 | tests-gradient = builtins.attrNames (lib.filterAttrs (_: type: type == "directory") (builtins.readDir ./gradient)); 15 | tests = map-folder "gradient" tests-gradient; 16 | in builtins.listToAttrs (map (test: { 17 | name = builtins.replaceStrings [ "/" ] [ "-" ] test; 18 | value = pkgs.testers.runNixOSTest ./${test}; 19 | }) tests) 20 | -------------------------------------------------------------------------------- /nix/tests/gradient/api/default.nix: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | { ... }: 8 | { 9 | name = "gradient-api"; 10 | globalTimeout = 120; 11 | nodes = { 12 | machine = { config, pkgs, lib, ... }: { 13 | imports = [ ../../../modules/gradient.nix ]; 14 | services = { 15 | gradient = { 16 | enable = true; 17 | listenAddr = "0.0.0.0"; 18 | domain = "gradient.local"; 19 | jwtSecretFile = toString (pkgs.writeText "jwtSecret" "b68a8eaa8ebcff23ebaba1bd74ecb8a2eb7ba959570ff8842f148207524c7b8d731d7a1998584105e951599221f9dcd20e41223be17275ca70ab6f7e6ecafa8d4f8905623866edb2b344bd15de52ccece395b3546e2f00644eb2679cf7bdaa156fd75cc5f47c34448cba19d903e68015b1ad3c8e9d04862de0a2c525b6676779012919fa9551c4746f9323ab207aedae86c28ada67c901cae821eef97b69ca4ebe1260de31add34d8265f17d9c547e3bbabe284d9cadcc22063ee625b104592403368090642a41967f8ada5791cb09703d0762a3175d0fe06ec37822e9e41d0a623a6349901749673735fdb94f2c268ac08a24216efb058feced6e785f34185a"); 20 | cryptSecretFile = toString (pkgs.writeText "cryptSecret" "aW52YWxpZAo="); 21 | }; 22 | 23 | postgresql = { 24 | enable = true; 25 | package = pkgs.postgresql_17; 26 | enableJIT = true; 27 | enableTCPIP = true; 28 | ensureDatabases = [ "gradient" ]; 29 | authentication = '' 30 | #... 31 | #type database DBuser origin-address auth-method 32 | # ipv4 33 | host all all 0.0.0.0/0 trust 34 | # ipv6 35 | host all all ::0/0 trust 36 | ''; 37 | 38 | settings = { 39 | log_connections = true; 40 | logging_collector = true; 41 | log_disconnections = true; 42 | log_destination = lib.mkForce "syslog"; 43 | }; 44 | }; 45 | }; 46 | }; 47 | }; 48 | 49 | interactive.nodes = { 50 | machine = import ../../modules/debug-host.nix; 51 | }; 52 | 53 | testScript = builtins.readFile ./test.py; 54 | } 55 | -------------------------------------------------------------------------------- /nix/tests/gradient/building/flake_repository.nix: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | { 7 | description = "Test Repository for Gradient Build Server"; 8 | outputs = { self, ... }: { 9 | packages.x86_64-linux.buildWait5Sec = builtins.derivation { 10 | name = "buildWait5Sec"; 11 | system = "x86_64-linux"; 12 | builder = "/bin/sh"; 13 | args = [ "-c" "sleep 5 && echo hello world > $out" ]; 14 | }; 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /nix/tests/modules/debug-host.nix: -------------------------------------------------------------------------------- 1 | { 2 | services.openssh = { 3 | enable = true; 4 | settings = { 5 | PermitRootLogin = "yes"; 6 | PermitEmptyPasswords = "yes"; 7 | }; 8 | }; 9 | security.pam.services.sshd.allowNullPassword = true; 10 | virtualisation.forwardPorts = [ 11 | { 12 | from = "host"; 13 | host.port = 2222; 14 | guest.port = 22; 15 | } 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /nix/vm/README.md: -------------------------------------------------------------------------------- 1 | # Setup an virtual bridge interface, according to this document. 2 | 3 | Import the following config to allow the VM to connect to the Internet and you be able to open the postgres DB. 4 | ``` 5 | { ... }: 6 | { 7 | systemd.network = { 8 | netdevs = { 9 | # Create the bridge interface 10 | "20-virbr0" = { 11 | bridgeConfig.STP = true; 12 | netdevConfig = { 13 | Kind = "bridge"; 14 | Name = "virbr0"; 15 | }; 16 | }; 17 | }; 18 | networks = { 19 | # Connect the bridge ports to the bridge 20 | "30-skyflake" = { 21 | matchConfig.Name = [ 22 | "enp*" 23 | "vm-*" 24 | ]; 25 | bridge = [ "virbr0" ]; 26 | linkConfig.RequiredForOnline = "enslaved"; 27 | }; 28 | # Configure the bridge for its desired function 29 | "40-virbr0" = { 30 | name = "virbr0"; 31 | DHCP = "yes"; 32 | bridgeConfig = {}; 33 | # Disable address autoconfig when no IP configuration is required 34 | networkConfig.LinkLocalAddressing = "no"; 35 | linkConfig = { 36 | # or "routable" with IP addresses configured 37 | RequiredForOnline = "routable"; 38 | }; 39 | }; 40 | }; 41 | }; 42 | } 43 | ``` 44 | # Connecting to the VM 45 | connect with `psql -U postgres -h ${IP_OF_VM}` 46 | or use the domain `gradient-dev.local` 47 | -------------------------------------------------------------------------------- /nix/vm/base.nix: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | { config, pkgs, ... }: 8 | { 9 | systemd.services."serial-getty@".serviceConfig.ExecStartPre = "${pkgs.systemd}/bin/networkctl status"; 10 | services.getty.greetingLine = "EXTREMELY UNSECURE SERVER, DO NOT STORE ANY IMPORTNANT DATA ON IT!!!"; 11 | users.users.root.password = "root"; 12 | system.stateVersion = config.system.nixos.version; 13 | microvm = { 14 | vcpu = 4; 15 | mem = 4096; 16 | writableStoreOverlay = "/nix/.rw-store"; 17 | # hypervisor = "cloud-hypervisor"; 18 | shares = [ 19 | { 20 | tag = "store"; 21 | source = "/nix/store"; 22 | mountPoint = "/nix/.ro-store"; 23 | # proto = "virtiofs"; 24 | } 25 | # { 26 | # tag = "root"; 27 | # source = "/tmp/gradient/${config.networking.hostName}"; 28 | # mountPoint = "/"; 29 | # # proto = "virtiofs"; 30 | # } 31 | 32 | ]; 33 | 34 | # # Persistent Storage 35 | # volumes = [{ 36 | # image = "gradient-persist.img"; 37 | # mountPoint = "/"; 38 | # size = 1024; # 1GB 39 | # }]; 40 | 41 | interfaces = [{ 42 | id = "enp0s1"; 43 | type = "bridge"; 44 | mac = "02:01:00:00:00:01"; 45 | bridge = "virbr0"; 46 | }]; 47 | }; 48 | 49 | networking = { 50 | domain = "local"; 51 | hostName = "gradient-dev"; 52 | nftables.enable = true; 53 | useNetworkd = true; 54 | }; 55 | 56 | systemd.network = { 57 | netdevs = { 58 | "br0" = { 59 | # bridgeConfig.STP = true; # Takes forever to activate interface 60 | netdevConfig = { 61 | Kind = "bridge"; 62 | Name = "br0"; 63 | }; 64 | }; 65 | }; 66 | networks = { 67 | # uplink 68 | "10-eth" = { 69 | matchConfig.Name = [ 70 | "enp*" 71 | "eth*" 72 | ]; 73 | networkConfig.Bridge = "br0"; 74 | }; 75 | # bridge is a dumb switch without addresses on the host 76 | "11-br0" = { 77 | matchConfig.Name = "br0"; 78 | networkConfig = { 79 | DHCP = "yes"; 80 | IPv6AcceptRA = true; 81 | }; 82 | }; 83 | }; 84 | }; 85 | 86 | services.openssh = { 87 | enable = true; 88 | settings.PermitRootLogin = "yes"; 89 | }; 90 | 91 | environment.systemPackages = with pkgs; [ 92 | systemctl-tui 93 | tcpdump 94 | ]; 95 | } 96 | -------------------------------------------------------------------------------- /nix/vm/defaults.nix: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | { 8 | nix = { 9 | settings = { 10 | # fetch github-prebuilt microvm-kernels 11 | substituters = [ "https://microvm.cachix.org" ]; 12 | trusted-public-keys = [ "microvm.cachix.org-1:oXnBc6hRE3eX5rSYdRyMYXnfzcCxC7yKPTbZXALsqys=" ]; 13 | }; 14 | 15 | extraOptions = '' 16 | experimental-features = nix-command flakes 17 | builders-use-substitutes = true 18 | ''; 19 | }; 20 | } 21 | 22 | -------------------------------------------------------------------------------- /nix/vm/mDNS.nix: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | { ... }: 8 | { 9 | services.avahi = { 10 | enable = true; 11 | ipv4 = true; 12 | nssmdns4 = true; 13 | ipv6 = true; 14 | nssmdns6 = true; 15 | publish = { 16 | enable = true; 17 | addresses = true; 18 | }; 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /nix/vm/monitoring/destination/grafana/dashboards/example.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wavelens/gradient/6eaf2657f5252df7c176527f0f5ceb14e60a9ffa/nix/vm/monitoring/destination/grafana/dashboards/example.json -------------------------------------------------------------------------------- /nix/vm/monitoring/destination/grafana/default.nix: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | { config, ... }: 8 | { 9 | #imports = [ 10 | # ../../nginx/grafana.nix 11 | #]; 12 | 13 | services.grafana = { 14 | enable = true; 15 | settings = { 16 | analytics.reporting_enabled = false; 17 | "auth.anonymous" = { 18 | enabled = true; 19 | # org_name = "Chaos"; 20 | org_role = "Admin"; 21 | }; 22 | users = { 23 | allow_sign_up = false; 24 | login_hint = "admin"; 25 | password_hint = "admin"; 26 | }; 27 | server = { 28 | http_addr = "127.0.0.1"; 29 | http_port = 3000; 30 | enforce_domain = false; 31 | enable_gzip = true; 32 | domain = "${config.networking.fqdn}"; 33 | }; 34 | }; 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /nix/vm/monitoring/source/loki/default.nix: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | { ... }: 8 | { 9 | services = { 10 | loki = { 11 | enable = true; 12 | configFile = ./loki.yaml; 13 | }; 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /nix/vm/monitoring/source/loki/loki.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 Wavelens UG 2 | # 3 | # SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | auth_enabled: false 6 | 7 | server: 8 | http_listen_port: 3100 9 | log_level: "warn" 10 | 11 | ingester: 12 | lifecycler: 13 | address: 0.0.0.0 14 | ring: 15 | kvstore: 16 | store: inmemory 17 | replication_factor: 1 18 | final_sleep: 0s 19 | chunk_idle_period: 1h # Any chunk not receiving new logs in this time will be flushed 20 | max_chunk_age: 1h # All chunks will be flushed when they hit this age, default is 1h 21 | chunk_target_size: 1048576 # Loki will attempt to build chunks up to 1.5MB, flushing first if chunk_idle_period or max_chunk_age is reached first 22 | chunk_retain_period: 30s # Must be greater than index read cache TTL if using an index cache (Default index read cache TTL is 5m) 23 | 24 | schema_config: 25 | configs: 26 | - from: 2025-01-01 27 | store: tsdb 28 | object_store: filesystem 29 | schema: v13 30 | index: 31 | prefix: index_ 32 | period: 24h 33 | 34 | storage_config: 35 | tsdb_shipper: 36 | active_index_directory: /var/lib/loki/boltdb-shipper-active 37 | cache_location: /var/lib/loki/boltdb-shipper-cache 38 | cache_ttl: 24h # Can be increased for faster performance over longer query periods, uses more disk space 39 | filesystem: 40 | directory: /var/lib/loki/chunks 41 | 42 | limits_config: 43 | reject_old_samples: true 44 | reject_old_samples_max_age: 168h 45 | 46 | table_manager: 47 | retention_deletes_enabled: false 48 | retention_period: 0s 49 | 50 | compactor: 51 | working_directory: /var/lib/loki/compactor 52 | compaction_interval: 5m 53 | -------------------------------------------------------------------------------- /nix/vm/monitoring/source/prometheus/default.nix: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | { config, ... }: 8 | { 9 | services.prometheus = { 10 | enable = true; 11 | retentionTime = "3y"; 12 | 13 | globalConfig.scrape_interval = "1s"; #1m 14 | exporters = { 15 | postgres = { 16 | enable = true; 17 | runAsLocalSuperUser = true; 18 | extraFlags = [ "--no-collector.stat_bgwriter" ]; # broken as of, 10.11.24. See: `https://github.com/prometheus-community/postgres_exporter/issues/1060`. 19 | }; 20 | node = { 21 | enable = true; 22 | enabledCollectors = [ "systemd" "ethtool" ]; 23 | }; 24 | }; 25 | scrapeConfigs = [ 26 | { 27 | job_name = "postgres"; 28 | static_configs = [{ 29 | targets = [ "localhost:${toString config.services.prometheus.exporters.postgres.port}" ]; 30 | }]; 31 | } 32 | { 33 | job_name = "node"; 34 | static_configs = [{ 35 | targets = [ "localhost:${toString config.services.prometheus.exporters.node.port}" ]; 36 | }]; 37 | } 38 | ]; 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /nix/vm/nginx/default.nix: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | { config, ... }: 8 | { 9 | services.nginx = { 10 | enable = true; 11 | recommendedTlsSettings = true; 12 | recommendedOptimisation = true; 13 | recommendedGzipSettings = true; 14 | commonHttpConfig = '' 15 | #types_hash_max_size 1024; 16 | server_names_hash_bucket_size 128; 17 | ''; 18 | 19 | virtualHosts = { 20 | "${config.networking.domain}" = { 21 | forceSSL = false; 22 | globalRedirect = "grafana.${config.networking.fqdn}"; 23 | }; 24 | }; 25 | }; 26 | networking.firewall.allowedTCPPorts = [ 80 ]; 27 | } 28 | -------------------------------------------------------------------------------- /nix/vm/nginx/grafana.nix: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | { config, ... }: 8 | { 9 | services.nginx = { 10 | virtualHosts = { 11 | "grafana.${config.networking.domain}" = { 12 | forceSSL = false; 13 | locations."/" = { 14 | proxyPass = "http://${toString config.services.grafana.settings.server.http_addr}:${toString config.services.grafana.settings.server.http_port}"; 15 | proxyWebsockets = true; 16 | extraConfig = "proxy_pass_header Authorization;"; 17 | recommendedProxySettings = true; 18 | }; 19 | }; 20 | }; 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /nix/vm/postgresql.nix: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Wavelens UG 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only 5 | */ 6 | 7 | { pkgs, lib, ... }: 8 | { 9 | # EXTREMELY UNSECURE Postgres DB setup. 10 | services.postgresql = { 11 | enable = true; 12 | package = pkgs.postgresql_17; 13 | enableJIT = true; 14 | enableTCPIP = true; 15 | authentication = '' 16 | #... 17 | #type database DBuser origin-address auth-method 18 | # ipv4 19 | host all all 0.0.0.0/0 trust 20 | # ipv6 21 | host all all ::0/0 trust 22 | ''; 23 | 24 | settings = { 25 | # ssl = true; 26 | log_connections = true; 27 | logging_collector = true; 28 | log_disconnections = true; 29 | log_destination = lib.mkForce "syslog"; 30 | }; 31 | }; 32 | 33 | # open firewall, needs to forwared port through the VM to. 34 | # allow communication from microvm port 5432 (postgres). 35 | networking.firewall.allowedTCPPorts = [ 5432 ]; 36 | } 37 | -------------------------------------------------------------------------------- /start-db.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Copyright 2022 SAP SE 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | # shellcheck shell=ash 18 | set -euo pipefail 19 | 20 | # Darwin compatibility 21 | if hash greadlink >/dev/null 2>/dev/null; then 22 | readlink() { greadlink "$@"; } 23 | fi 24 | 25 | step() { 26 | printf '\x1B[1;36m>>\x1B[0;36m %s...\x1B[0m\n' "$1" 27 | } 28 | 29 | if [ ! -d testing/postgresql-data/ ]; then 30 | step "First-time setup: Creating PostgreSQL database for testing" 31 | initdb -A trust -U postgres testing/postgresql-data/ 32 | fi 33 | mkdir -p testing/postgresql-run/ 34 | 35 | step "Configuring PostgreSQL" 36 | sed -ie '/^#\?\(external_pid_file\|unix_socket_directories\|port\)\b/d' testing/postgresql-data/postgresql.conf 37 | ( 38 | echo "external_pid_file = '${PWD}/testing/postgresql-run/pid'" 39 | echo "unix_socket_directories = '${PWD}/testing/postgresql-run'" 40 | echo "port = 54321" 41 | ) >> testing/postgresql-data/postgresql.conf 42 | 43 | # usage in trap is not recognized 44 | # shellcheck disable=SC2317 45 | stop_postgres() { 46 | EXIT_CODE=$? 47 | step "Stopping PostgreSQL" 48 | pg_ctl stop -D testing/postgresql-data/ -w -s 49 | # rm -rf testing 50 | exit "${EXIT_CODE}" 51 | } 52 | 53 | step "Starting PostgreSQL" 54 | rm -f -- testing/postgresql.log 55 | trap stop_postgres EXIT INT TERM 56 | pg_ctl start -D testing/postgresql-data/ -l testing/postgresql.log -w -s 57 | createdb -U postgres -h localhost -p 54321 gradient >> /dev/null 2>&1 || true 58 | 59 | step "Running command: $*" 60 | set +e 61 | "$@" 62 | EXIT_CODE=$? 63 | set -e 64 | 65 | exit "${EXIT_CODE}" 66 | 67 | --------------------------------------------------------------------------------