forked from lix-project/hydra
Compare commits
10 commits
4b886d9c45
...
d901bf9724
Author | SHA1 | Date | |
---|---|---|---|
Maximilian Bosch | d901bf9724 | ||
Maximilian Bosch | 337d5bf91e | ||
Maximilian Bosch | d6f531e088 | ||
71rd | 459aa0a598 | ||
eldritch horrors | f1b552ecbf | ||
Pierre Bourdon | db8c2cc4a8 | ||
8858abb1a6 | |||
ef619eca99 | |||
41dfa0e443 | |||
Pierre Bourdon | 4b107e6ff3 |
37
.github/ISSUE_TEMPLATE/bug_report.md
vendored
37
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
@ -1,37 +0,0 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Hydra Server:**
|
||||
|
||||
Please fill out this data as well as you can, but don't worry if you can't -- just do your best.
|
||||
|
||||
- OS and version: [e.g. NixOS 22.05.20211203.ee3794c]
|
||||
- Version of Hydra
|
||||
- Version of Nix Hydra is built against
|
||||
- Version of the Nix daemon
|
||||
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
|
@ -1,20 +0,0 @@
|
|||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
6
.github/dependabot.yml
vendored
6
.github/dependabot.yml
vendored
|
@ -1,6 +0,0 @@
|
|||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
14
.github/workflows/test.yml
vendored
14
.github/workflows/test.yml
vendored
|
@ -1,14 +0,0 @@
|
|||
name: "Test"
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
jobs:
|
||||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: cachix/install-nix-action@v17
|
||||
#- run: nix flake check
|
||||
- run: nix-build -A checks.x86_64-linux.build -A checks.x86_64-linux.validate-openapi
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -5,3 +5,5 @@
|
|||
/src/sql/tmp.sqlite
|
||||
result
|
||||
result-*
|
||||
.hydra-data
|
||||
outputs
|
||||
|
|
|
@ -12,15 +12,14 @@ To enter a shell in which all environment variables (such as `PERL5LIB`)
|
|||
and dependencies can be found:
|
||||
|
||||
```console
|
||||
$ nix-shell
|
||||
$ nix develop
|
||||
```
|
||||
|
||||
To build Hydra, you should then do:
|
||||
|
||||
```console
|
||||
[nix-shell]$ autoreconfPhase
|
||||
[nix-shell]$ configurePhase
|
||||
[nix-shell]$ make
|
||||
[nix-shell]$ just setup
|
||||
[nix-shell]$ just install
|
||||
```
|
||||
|
||||
You start a local database, the webserver, and other components with
|
||||
|
@ -30,6 +29,8 @@ foreman:
|
|||
$ foreman start
|
||||
```
|
||||
|
||||
The Hydra interface will be available on port 63333, with an admin user named "alice" with password "foobar"
|
||||
|
||||
You can run just the Hydra web server in your source tree as follows:
|
||||
|
||||
```console
|
||||
|
@ -39,18 +40,13 @@ $ ./src/script/hydra-server
|
|||
You can run Hydra's test suite with the following:
|
||||
|
||||
```console
|
||||
[nix-shell]$ make check
|
||||
[nix-shell]$ # to run as many tests as you have cores:
|
||||
[nix-shell]$ make check YATH_JOB_COUNT=$NIX_BUILD_CORES
|
||||
[nix-shell]$ just test
|
||||
[nix-shell]$ # or run yath directly:
|
||||
[nix-shell]$ yath test
|
||||
[nix-shell]$ # to run as many tests as you have cores:
|
||||
[nix-shell]$ yath test -j $NIX_BUILD_CORES
|
||||
```
|
||||
|
||||
When using `yath` instead of `make check`, ensure you have run `make`
|
||||
in the root of the repository at least once.
|
||||
|
||||
**Warning**: Currently, the tests can fail
|
||||
if run with high parallelism [due to an issue in
|
||||
`Test::PostgreSQL`](https://github.com/TJC/Test-postgresql/issues/40)
|
||||
|
|
|
@ -312,6 +312,21 @@ Declarative Projects
|
|||
|
||||
see this [chapter](./plugins/declarative-projects.md)
|
||||
|
||||
Private Projects
|
||||
----------------
|
||||
|
||||
By checking the `Private` checkbox in the project creation form, a project
|
||||
and everything related to it (jobsets, evals, builds, etc.) can only be accessed
|
||||
if a user is authenticated. Otherwise, a 404 will be returned by the API and Web
|
||||
UI. This is the main difference to "hidden" projects where everything can
|
||||
be obtained if the URLs are known.
|
||||
|
||||
Please note that the store paths that are realized in evaluations that belong to
|
||||
private projects aren't protected! It is assumed that the hashes are unknown
|
||||
and thus inaccessible. For a real protection of the binary cache it's recommended
|
||||
to either use `nix.sshServe` instead or to protect the routes `/nar/*` and `*.narinfo`
|
||||
with a reverse proxy.
|
||||
|
||||
Email Notifications
|
||||
-------------------
|
||||
|
||||
|
|
26
flake.lock
26
flake.lock
|
@ -48,11 +48,11 @@
|
|||
"pre-commit-hooks": "pre-commit-hooks"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1721091462,
|
||||
"narHash": "sha256-0cmEeoOiB91BviTJHzIyxkY+Gxv3O8ZnnExVAoXEFGI=",
|
||||
"lastModified": 1723331518,
|
||||
"narHash": "sha256-JVnQ3OLbXQAlkOluFc3gWhZMbhared1Rg5YvNEc92m0=",
|
||||
"ref": "refs/heads/main",
|
||||
"rev": "6b4d46e9e0e1dd80e0977684ab20d14bcd1a6bc3",
|
||||
"revCount": 15967,
|
||||
"rev": "5137cea99044d54337e439510a647743110b2d7d",
|
||||
"revCount": 16128,
|
||||
"type": "git",
|
||||
"url": "https://git.lix.systems/lix-project/lix"
|
||||
},
|
||||
|
@ -111,11 +111,11 @@
|
|||
"nix2container": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1712990762,
|
||||
"narHash": "sha256-hO9W3w7NcnYeX8u8cleHiSpK2YJo7ecarFTUlbybl7k=",
|
||||
"lastModified": 1720642556,
|
||||
"narHash": "sha256-qsnqk13UmREKmRT7c8hEnz26X3GFFyIQrqx4EaRc1Is=",
|
||||
"owner": "nlewo",
|
||||
"repo": "nix2container",
|
||||
"rev": "20aad300c925639d5d6cbe30013c8357ce9f2a2e",
|
||||
"rev": "3853e5caf9ad24103b13aa6e0e8bcebb47649fe4",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -126,11 +126,11 @@
|
|||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1720691131,
|
||||
"narHash": "sha256-CWT+KN8aTPyMIx8P303gsVxUnkinIz0a/Cmasz1jyIM=",
|
||||
"lastModified": 1723282977,
|
||||
"narHash": "sha256-oTK91aOlA/4IsjNAZGMEBz7Sq1zBS0Ltu4/nIQdYDOg=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "a046c1202e11b62cbede5385ba64908feb7bfac4",
|
||||
"rev": "a781ff33ae258bbcfd4ed6e673860c3e923bf2cc",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -159,11 +159,11 @@
|
|||
"pre-commit-hooks": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1712055707,
|
||||
"narHash": "sha256-4XLvuSIDZJGS17xEwSrNuJLL7UjDYKGJSbK1WWX2AK8=",
|
||||
"lastModified": 1721042469,
|
||||
"narHash": "sha256-6FPUl7HVtvRHCCBQne7Ylp4p+dpP3P/OYuzjztZ4s70=",
|
||||
"owner": "cachix",
|
||||
"repo": "git-hooks.nix",
|
||||
"rev": "e35aed5fda3cc79f88ed7f1795021e559582093a",
|
||||
"rev": "f451c19376071a90d8c58ab1a953c6e9840527fd",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
|
@ -3,4 +3,4 @@
|
|||
# wait for hydra-server to listen
|
||||
while ! nc -z localhost 63333; do sleep 1; done
|
||||
|
||||
HYDRA_CONFIG=$(pwd)/.hydra-data/hydra.conf exec hydra-evaluator
|
||||
HYDRA_CONFIG=$(pwd)/.hydra-data/hydra.conf exec $(pwd)/outputs/out/bin/hydra-evaluator
|
||||
|
|
|
@ -28,4 +28,4 @@ use-substitutes = true
|
|||
</hydra_notify>
|
||||
EOF
|
||||
fi
|
||||
HYDRA_CONFIG=$(pwd)/.hydra-data/hydra.conf exec hydra-dev-server --port 63333 --restart --debug
|
||||
HYDRA_CONFIG=$(pwd)/.hydra-data/hydra.conf exec $(pwd)/outputs/out/bin/hydra-dev-server --port 63333 --restart --debug
|
||||
|
|
|
@ -3,4 +3,4 @@
|
|||
# wait for hydra-server to listen
|
||||
while ! nc -z localhost 63333; do sleep 1; done
|
||||
|
||||
HYDRA_CONFIG=$(pwd)/.hydra-data/hydra.conf exec hydra-notify
|
||||
HYDRA_CONFIG=$(pwd)/.hydra-data/hydra.conf exec $(pwd)/outputs/out/bin/hydra-notify
|
||||
|
|
|
@ -3,4 +3,4 @@
|
|||
# wait until hydra is listening on port 63333
|
||||
while ! nc -z localhost 63333; do sleep 1; done
|
||||
|
||||
NIX_REMOTE_SYSTEMS="" HYDRA_CONFIG=$(pwd)/.hydra-data/hydra.conf exec hydra-queue-runner
|
||||
NIX_REMOTE_SYSTEMS="" HYDRA_CONFIG=$(pwd)/.hydra-data/hydra.conf exec $(pwd)/outputs/out/bin/hydra-queue-runner
|
||||
|
|
|
@ -184,6 +184,9 @@ paths:
|
|||
visible:
|
||||
description: when set to true the project is displayed in the web interface
|
||||
type: boolean
|
||||
private:
|
||||
description: when set to true the project and all related objects are only accessible to authenticated users
|
||||
type: boolean
|
||||
declarative:
|
||||
description: declarative input configured for this project
|
||||
type: object
|
||||
|
@ -625,6 +628,9 @@ components:
|
|||
hidden:
|
||||
description: when set to true the project is not displayed in the web interface
|
||||
type: boolean
|
||||
private:
|
||||
description: when set to true the project and all related objects are only accessible to authenticated users
|
||||
type: boolean
|
||||
enabled:
|
||||
description: when set to true the project gets scheduled for evaluation
|
||||
type: boolean
|
||||
|
|
11
justfile
Normal file
11
justfile
Normal file
|
@ -0,0 +1,11 @@
|
|||
setup *OPTIONS:
|
||||
meson setup build --prefix="$PWD/outputs/out" $mesonFlags {{ OPTIONS }}
|
||||
|
||||
build *OPTIONS:
|
||||
meson compile -C build {{ OPTIONS }}
|
||||
|
||||
install *OPTIONS: (build OPTIONS)
|
||||
meson install -C build
|
||||
|
||||
test *OPTIONS:
|
||||
meson test -C build --print-errorlogs {{ OPTIONS }}
|
|
@ -37,6 +37,7 @@
|
|||
|
||||
, cacert
|
||||
, foreman
|
||||
, just
|
||||
, glibcLocales
|
||||
, libressl
|
||||
, openldap
|
||||
|
@ -97,6 +98,7 @@ let
|
|||
FileLibMagic
|
||||
FileSlurper
|
||||
FileWhich
|
||||
HTMLTreeBuilderXPath
|
||||
IOCompress
|
||||
IPCRun
|
||||
IPCRun3
|
||||
|
@ -190,6 +192,8 @@ stdenv.mkDerivation (finalAttrs: {
|
|||
postgresql_13
|
||||
pixz
|
||||
nix-eval-jobs
|
||||
perlPackages.PLS
|
||||
just
|
||||
];
|
||||
|
||||
checkInputs = [
|
||||
|
@ -233,8 +237,8 @@ stdenv.mkDerivation (finalAttrs: {
|
|||
shellHook = ''
|
||||
pushd $(git rev-parse --show-toplevel) >/dev/null
|
||||
|
||||
PATH=$(pwd)/src/hydra-evaluator:$(pwd)/src/script:$(pwd)/src/hydra-queue-runner:$PATH
|
||||
PERL5LIB=$(pwd)/src/lib:$PERL5LIB
|
||||
PATH=$(pwd)/outputs/out/bin:$PATH
|
||||
PERL5LIB=$(pwd)/src/lib:$(pwd)/t/lib:$PERL5LIB
|
||||
export HYDRA_HOME="$(pwd)/src/"
|
||||
mkdir -p .hydra-data
|
||||
export HYDRA_DATA="$(pwd)/.hydra-data"
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
# IMPORTANT: if you delete this file your app will not work as
|
||||
# expected. you have been warned
|
||||
use strict;
|
||||
use warnings;
|
||||
use inc::Module::Install;
|
||||
|
||||
name 'Hydra';
|
||||
all_from 'lib/Hydra.pm';
|
||||
|
||||
requires 'Catalyst::Runtime' => '5.7015';
|
||||
requires 'Catalyst::Plugin::ConfigLoader';
|
||||
requires 'Catalyst::Plugin::Static::Simple';
|
||||
requires 'Catalyst::Action::RenderView';
|
||||
requires 'parent';
|
||||
requires 'Config::General'; # This should reflect the config file format you've chosen
|
||||
# See Catalyst::Plugin::ConfigLoader for supported formats
|
||||
catalyst;
|
||||
|
||||
install_script glob('script/*.pl');
|
||||
auto_install;
|
||||
WriteAll;
|
|
@ -368,7 +368,7 @@ static std::map<StorePath, ValidPathInfo> queryPathInfos(
|
|||
auto references = ServeProto::Serialise<StorePathSet>::read(localStore, conn);
|
||||
readLongLong(conn.from); // download size
|
||||
auto narSize = readLongLong(conn.from);
|
||||
auto narHash = Hash::parseAny(readString(conn.from), htSHA256);
|
||||
auto narHash = Hash::parseAny(readString(conn.from), HashType::SHA256);
|
||||
auto ca = ContentAddress::parseOpt(readString(conn.from));
|
||||
readStrings<StringSet>(conn.from); // sigs
|
||||
ValidPathInfo info(localStore.parseStorePath(storePathS), narHash);
|
||||
|
@ -397,8 +397,7 @@ static void copyPathFromRemote(
|
|||
/* Receive the NAR from the remote and add it to the
|
||||
destination store. Meanwhile, extract all the info from the
|
||||
NAR that getBuildOutput() needs. */
|
||||
auto source2 = sinkToSource([&](Sink & sink)
|
||||
{
|
||||
auto coro = [&]() -> WireFormatGenerator {
|
||||
/* Note: we should only send the command to dump the store
|
||||
path to the remote if the NAR is actually going to get read
|
||||
by the destination store, which won't happen if this path
|
||||
|
@ -409,11 +408,11 @@ static void copyPathFromRemote(
|
|||
conn.to << ServeProto::Command::DumpStorePath << localStore.printStorePath(info.path);
|
||||
conn.to.flush();
|
||||
|
||||
TeeSource tee(conn.from, sink);
|
||||
extractNarData(tee, localStore.printStorePath(info.path), narMembers);
|
||||
});
|
||||
co_yield extractNarDataFilter(conn.from, localStore.printStorePath(info.path), narMembers);
|
||||
};
|
||||
GeneratorSource source2{coro()};
|
||||
|
||||
destStore.addToStore(info, *source2, NoRepair, NoCheckSigs);
|
||||
destStore.addToStore(info, source2, NoRepair, NoCheckSigs);
|
||||
}
|
||||
|
||||
static void copyPathsFromRemote(
|
||||
|
|
|
@ -34,11 +34,8 @@ BuildOutput getBuildOutput(
|
|||
auto outputS = store->printStorePath(output);
|
||||
if (!narMembers.count(outputS)) {
|
||||
printInfo("fetching NAR contents of '%s'...", outputS);
|
||||
auto source = sinkToSource([&](Sink & sink)
|
||||
{
|
||||
sink << store->narFromPath(output);
|
||||
});
|
||||
extractNarData(*source, outputS, narMembers);
|
||||
GeneratorSource source{store->narFromPath(output)};
|
||||
extractNarData(source, outputS, narMembers);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -534,7 +534,7 @@ void State::markSucceededBuild(pqxx::work & txn, Build::ptr build,
|
|||
product.type,
|
||||
product.subtype,
|
||||
product.fileSize ? std::make_optional(*product.fileSize) : std::nullopt,
|
||||
product.sha256hash ? std::make_optional(product.sha256hash->to_string(Base16, false)) : std::nullopt,
|
||||
product.sha256hash ? std::make_optional(product.sha256hash->to_string(Base::Base16, false)) : std::nullopt,
|
||||
product.path,
|
||||
product.name,
|
||||
product.defaultPath);
|
||||
|
|
|
@ -42,7 +42,7 @@ struct Extractor : ParseSink
|
|||
void preallocateContents(uint64_t size) override
|
||||
{
|
||||
expectedSize = size;
|
||||
hashSink = std::make_unique<HashSink>(htSHA256);
|
||||
hashSink = std::make_unique<HashSink>(HashType::SHA256);
|
||||
}
|
||||
|
||||
void receiveContents(std::string_view data) override
|
||||
|
@ -76,7 +76,19 @@ void extractNarData(
|
|||
const Path & prefix,
|
||||
NarMemberDatas & members)
|
||||
{
|
||||
Extractor extractor(members, prefix);
|
||||
parseDump(extractor, source);
|
||||
// Note: this point may not be reached if we're in a coroutine.
|
||||
auto parser = extractNarDataFilter(source, prefix, members);
|
||||
while (parser.next()) {
|
||||
// ignore raw data
|
||||
}
|
||||
}
|
||||
|
||||
nix::WireFormatGenerator extractNarDataFilter(
|
||||
Source & source,
|
||||
const Path & prefix,
|
||||
NarMemberDatas & members)
|
||||
{
|
||||
return [](Source & source, const Path & prefix, NarMemberDatas & members) -> WireFormatGenerator {
|
||||
Extractor extractor(members, prefix);
|
||||
co_yield parseAndCopyDump(extractor, source);
|
||||
}(source, prefix, members);
|
||||
}
|
||||
|
|
|
@ -21,3 +21,8 @@ void extractNarData(
|
|||
nix::Source & source,
|
||||
const nix::Path & prefix,
|
||||
NarMemberDatas & members);
|
||||
|
||||
nix::WireFormatGenerator extractNarDataFilter(
|
||||
nix::Source & source,
|
||||
const nix::Path & prefix,
|
||||
NarMemberDatas & members);
|
||||
|
|
|
@ -727,7 +727,7 @@ BuildOutput State::getBuildOutputCached(Connection & conn, nix::ref<nix::Store>
|
|||
product.fileSize = row[2].as<off_t>();
|
||||
}
|
||||
if (!row[3].is_null())
|
||||
product.sha256hash = Hash::parseAny(row[3].as<std::string>(), htSHA256);
|
||||
product.sha256hash = Hash::parseAny(row[3].as<std::string>(), HashType::SHA256);
|
||||
if (!row[4].is_null())
|
||||
product.path = row[4].as<std::string>();
|
||||
product.name = row[5].as<std::string>();
|
||||
|
|
|
@ -23,23 +23,37 @@ sub all : Chained('get_builds') PathPart {
|
|||
$c->stash->{total} = $c->stash->{allBuilds}->search({finished => 1})->count
|
||||
unless defined $c->stash->{total};
|
||||
|
||||
$c->stash->{builds} = [ $c->stash->{allBuilds}->search(
|
||||
{ finished => 1 },
|
||||
{ order_by => "stoptime DESC"
|
||||
my $extra = {
|
||||
order_by => "stoptime DESC"
|
||||
, columns => [@buildListColumns]
|
||||
, rows => $resultsPerPage
|
||||
, page => $page }) ];
|
||||
, page => $page };
|
||||
|
||||
my $criteria = { finished => 1 };
|
||||
|
||||
unless ($c->user_exists) {
|
||||
$extra->{join} = {"jobset" => "project"};
|
||||
$criteria->{"project.private"} = 0;
|
||||
}
|
||||
|
||||
$c->stash->{builds} = [ $c->stash->{allBuilds}->search(
|
||||
$criteria,
|
||||
$extra
|
||||
) ];
|
||||
}
|
||||
|
||||
|
||||
sub nix : Chained('get_builds') PathPart('channel/latest') CaptureArgs(0) {
|
||||
my ($self, $c) = @_;
|
||||
|
||||
my $private = $c->user_exists ? [1,0] : [0];
|
||||
|
||||
$c->stash->{channelName} = $c->stash->{channelBaseName} . "-latest";
|
||||
$c->stash->{channelBuilds} = $c->stash->{latestSucceeded}
|
||||
->search_literal("exists (select 1 from buildproducts where build = me.id and type = 'nix-build')")
|
||||
->search({}, { columns => [@buildListColumns, 'drvpath', 'description', 'homepage']
|
||||
, join => ["buildoutputs"]
|
||||
->search({"project.private" => {-in => $private}},
|
||||
{ columns => [@buildListColumns, 'drvpath', 'description', 'homepage']
|
||||
, join => ["buildoutputs", {"jobset" => "project"}]
|
||||
, order_by => ["me.id", "buildoutputs.name"]
|
||||
, '+select' => ['buildoutputs.path', 'buildoutputs.name'], '+as' => ['outpath', 'outname'] });
|
||||
}
|
||||
|
|
|
@ -49,6 +49,8 @@ sub latestbuilds : Chained('api') PathPart('latestbuilds') Args(0) {
|
|||
error($c, "Parameter not defined!") if !defined $nr;
|
||||
|
||||
my $project = $c->request->params->{project};
|
||||
checkProjectVisibleForGuest($c, $c->stash->{project});
|
||||
|
||||
my $jobset = $c->request->params->{jobset};
|
||||
my $job = $c->request->params->{job};
|
||||
my $system = $c->request->params->{system};
|
||||
|
@ -106,6 +108,8 @@ sub jobsets : Chained('api') PathPart('jobsets') Args(0) {
|
|||
my $project = $c->model('DB::Projects')->find($projectName)
|
||||
or notFound($c, "Project $projectName doesn't exist.");
|
||||
|
||||
checkProjectVisibleForGuest($c, $project);
|
||||
|
||||
my @jobsets = jobsetOverview($c, $project);
|
||||
|
||||
my @list;
|
||||
|
@ -124,7 +128,17 @@ sub queue : Chained('api') PathPart('queue') Args(0) {
|
|||
my $nr = $c->request->params->{nr};
|
||||
error($c, "Parameter not defined!") if !defined $nr;
|
||||
|
||||
my @builds = $c->model('DB::Builds')->search({finished => 0}, {rows => $nr, order_by => ["priority DESC", "id"]});
|
||||
my $criteria = {finished => 0};
|
||||
my $extra = {
|
||||
rows => $nr,
|
||||
order_by => ["priority DESC", "id"]
|
||||
};
|
||||
unless ($c->user_exists) {
|
||||
$criteria->{"project.private"} = 0;
|
||||
$extra->{join} = {"jobset" => "project"};
|
||||
}
|
||||
|
||||
my @builds = $c->model('DB::Builds')->search($criteria, $extra);
|
||||
|
||||
my @list;
|
||||
push @list, buildToHash($_) foreach @builds;
|
||||
|
@ -198,6 +212,16 @@ sub scmdiff : Path('/api/scmdiff') Args(0) {
|
|||
my $rev1 = $c->request->params->{rev1};
|
||||
my $rev2 = $c->request->params->{rev2};
|
||||
|
||||
unless ($c->user_exists) {
|
||||
my $search = $c->model('DB::JobsetEvalInputs')->search(
|
||||
{ "project.private" => 0, "me.uri" => $uri },
|
||||
{ join => { "eval" => { jobset => "project" } } }
|
||||
);
|
||||
if ($search == 0) {
|
||||
die("invalid revisions: [$rev1] [$rev2]")
|
||||
}
|
||||
}
|
||||
|
||||
die("invalid revisions: [$rev1] [$rev2]") if $rev1 !~ m/^[a-zA-Z0-9_.]+$/ || $rev2 !~ m/^[a-zA-Z0-9_.]+$/;
|
||||
|
||||
# FIXME: injection danger.
|
||||
|
|
|
@ -39,6 +39,9 @@ sub buildChain :Chained('/') :PathPart('build') :CaptureArgs(1) {
|
|||
$c->stash->{project} = $c->stash->{build}->project;
|
||||
$c->stash->{jobset} = $c->stash->{build}->jobset;
|
||||
$c->stash->{job} = $c->stash->{build}->job;
|
||||
|
||||
checkProjectVisibleForGuest($c, $c->stash->{project});
|
||||
|
||||
$c->stash->{runcommandlogs} = [$c->stash->{build}->runcommandlogs->search({}, {order_by => ["id DESC"]})];
|
||||
|
||||
$c->stash->{runcommandlogProblem} = undef;
|
||||
|
@ -240,7 +243,7 @@ sub serveFile {
|
|||
# XSS hole.
|
||||
$c->response->header('Content-Security-Policy' => 'sandbox allow-scripts');
|
||||
|
||||
$c->stash->{'plain'} = { data => grab(cmd => ["nix", "--experimental-features", "nix-command",
|
||||
$c->stash->{'plain'} = { data => readIntoSocket(cmd => ["nix", "--experimental-features", "nix-command",
|
||||
"store", "cat", "--store", getStoreUri(), "$path"]) };
|
||||
|
||||
# Detect MIME type.
|
||||
|
|
|
@ -3,6 +3,7 @@ package Hydra::Controller::Channel;
|
|||
use strict;
|
||||
use warnings;
|
||||
use base 'Hydra::Base::Controller::REST';
|
||||
use Hydra::Helper::CatalystUtils;
|
||||
|
||||
|
||||
sub channel : Chained('/') PathPart('channel/custom') CaptureArgs(3) {
|
||||
|
@ -10,6 +11,8 @@ sub channel : Chained('/') PathPart('channel/custom') CaptureArgs(3) {
|
|||
|
||||
$c->stash->{project} = $c->model('DB::Projects')->find($projectName);
|
||||
|
||||
checkProjectVisibleForGuest($c, $c->stash->{project});
|
||||
|
||||
notFound($c, "Project $projectName doesn't exist.")
|
||||
if !$c->stash->{project};
|
||||
|
||||
|
|
|
@ -27,6 +27,8 @@ sub job : Chained('/') PathPart('job') CaptureArgs(3) {
|
|||
|
||||
$c->stash->{job} = $jobName;
|
||||
$c->stash->{project} = $c->stash->{jobset}->project;
|
||||
|
||||
checkProjectVisibleForGuest($c, $c->stash->{project});
|
||||
}
|
||||
|
||||
sub shield :Chained('job') PathPart('shield') Args(0) {
|
||||
|
|
|
@ -17,6 +17,8 @@ sub jobsetChain :Chained('/') :PathPart('jobset') :CaptureArgs(2) {
|
|||
|
||||
$c->stash->{project} = $project;
|
||||
|
||||
checkProjectVisibleForGuest($c, $c->stash->{project});
|
||||
|
||||
$c->stash->{jobset} = $project->jobsets->find({ name => $jobsetName });
|
||||
|
||||
if (!$c->stash->{jobset} && !($c->action->name eq "jobset" and $c->request->method eq "PUT")) {
|
||||
|
|
|
@ -19,6 +19,8 @@ sub evalChain : Chained('/') PathPart('eval') CaptureArgs(1) {
|
|||
$c->stash->{eval} = $eval;
|
||||
$c->stash->{jobset} = $eval->jobset;
|
||||
$c->stash->{project} = $eval->jobset->project;
|
||||
|
||||
checkProjectVisibleForGuest($c, $c->stash->{project});
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -16,6 +16,8 @@ sub projectChain :Chained('/') :PathPart('project') :CaptureArgs(1) {
|
|||
|
||||
$c->stash->{project} = $c->model('DB::Projects')->find($projectName);
|
||||
|
||||
checkProjectVisibleForGuest($c, $c->stash->{project});
|
||||
|
||||
$c->stash->{isProjectOwner} = !$isCreate && isProjectOwner($c, $c->stash->{project});
|
||||
|
||||
notFound($c, "Project ‘$projectName’ doesn't exist.")
|
||||
|
@ -161,6 +163,7 @@ sub updateProject {
|
|||
, homepage => trim($c->stash->{params}->{homepage})
|
||||
, enabled => defined $c->stash->{params}->{enabled} ? 1 : 0
|
||||
, hidden => defined $c->stash->{params}->{visible} ? 0 : 1
|
||||
, private => defined $c->stash->{params}->{private} ? 1 : 0
|
||||
, owner => $owner
|
||||
, enable_dynamic_run_command => $enable_dynamic_run_command
|
||||
, declfile => trim($c->stash->{params}->{declarative}->{file})
|
||||
|
|
|
@ -109,7 +109,13 @@ sub deserialize :ActionClass('Deserialize') { }
|
|||
sub index :Path :Args(0) {
|
||||
my ($self, $c) = @_;
|
||||
$c->stash->{template} = 'overview.tt';
|
||||
$c->stash->{projects} = [$c->model('DB::Projects')->search({}, {order_by => ['enabled DESC', 'name']})];
|
||||
|
||||
my $includePrivate = $c->user_exists ? [1,0] : [0];
|
||||
|
||||
$c->stash->{projects} = [$c->model('DB::Projects')->search(
|
||||
{private => {-in => $includePrivate}},
|
||||
{order_by => ['enabled DESC', 'name']}
|
||||
)];
|
||||
$c->stash->{newsItems} = [$c->model('DB::NewsItems')->search({}, { order_by => ['createtime DESC'], rows => 5 })];
|
||||
$self->status_ok($c,
|
||||
entity => $c->stash->{projects}
|
||||
|
@ -121,15 +127,23 @@ sub queue :Local :Args(0) :ActionClass('REST') { }
|
|||
|
||||
sub queue_GET {
|
||||
my ($self, $c) = @_;
|
||||
my $criteria = {finished => 0};
|
||||
my $extra = {
|
||||
columns => [@buildListColumns],
|
||||
order_by => ["priority DESC", "id"]
|
||||
};
|
||||
unless ($c->user_exists) {
|
||||
$criteria->{"project.private"} = 0;
|
||||
$extra->{join} = {"jobset" => "project"};
|
||||
}
|
||||
$c->stash->{template} = 'queue.tt';
|
||||
$c->stash->{flashMsg} //= $c->flash->{buildMsg};
|
||||
$self->status_ok(
|
||||
$c,
|
||||
entity => [$c->model('DB::Builds')->search(
|
||||
{ finished => 0 },
|
||||
{ order_by => ["globalpriority desc", "id"],
|
||||
, columns => [@buildListColumns]
|
||||
})]
|
||||
$criteria,
|
||||
$extra
|
||||
)]
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -138,10 +152,15 @@ sub queue_summary :Local :Path('queue-summary') :Args(0) {
|
|||
my ($self, $c) = @_;
|
||||
$c->stash->{template} = 'queue-summary.tt';
|
||||
|
||||
my $extra = " where ";
|
||||
unless ($c->user_exists) {
|
||||
$extra = "inner join Projects p on p.name = project where p.private = 0 and ";
|
||||
}
|
||||
|
||||
$c->stash->{queued} = dbh($c)->selectall_arrayref(
|
||||
"select jobsets.project as project, jobsets.name as jobset, count(*) as queued, min(timestamp) as oldest, max(timestamp) as newest from Builds " .
|
||||
"join Jobsets jobsets on jobsets.id = builds.jobset_id " .
|
||||
"where finished = 0 group by jobsets.project, jobsets.name order by queued desc",
|
||||
"$extra finished = 0 group by jobsets.project, jobsets.name order by queued desc",
|
||||
{ Slice => {} });
|
||||
|
||||
$c->stash->{systems} = dbh($c)->selectall_arrayref(
|
||||
|
@ -154,12 +173,19 @@ sub status :Local :Args(0) :ActionClass('REST') { }
|
|||
|
||||
sub status_GET {
|
||||
my ($self, $c) = @_;
|
||||
my $criteria = { "buildsteps.busy" => { '!=', 0 } };
|
||||
my $join = ["buildsteps"];
|
||||
unless ($c->user_exists) {
|
||||
$criteria->{"project.private"} = 0;
|
||||
push @{$join}, {"jobset" => "project"};
|
||||
}
|
||||
|
||||
$self->status_ok(
|
||||
$c,
|
||||
entity => [$c->model('DB::Builds')->search(
|
||||
{ "buildsteps.busy" => { '!=', 0 } },
|
||||
$criteria,
|
||||
{ order_by => ["globalpriority DESC", "id"],
|
||||
join => "buildsteps",
|
||||
join => $join,
|
||||
columns => [@buildListColumns, 'buildsteps.drvpath', 'buildsteps.type']
|
||||
})]
|
||||
);
|
||||
|
@ -201,13 +227,18 @@ sub machines :Local Args(0) {
|
|||
}
|
||||
}
|
||||
|
||||
my $extra = "where";
|
||||
unless ($c->user_exists) {
|
||||
$extra = "inner join Projects p on p.name = jobsets.project where p.private = 0 and ";
|
||||
}
|
||||
|
||||
$c->stash->{machines} = $machines;
|
||||
$c->stash->{steps} = dbh($c)->selectall_arrayref(
|
||||
"select build, stepnr, s.system as system, s.drvpath as drvpath, machine, s.starttime as starttime, jobsets.project as project, jobsets.name as jobset, job, s.busy as busy " .
|
||||
"from BuildSteps s " .
|
||||
"join Builds b on s.build = b.id " .
|
||||
"join Jobsets jobsets on jobsets.id = b.jobset_id " .
|
||||
"where busy != 0 order by machine, stepnr",
|
||||
"$extra busy != 0 order by machine, stepnr",
|
||||
{ Slice => {} });
|
||||
$c->stash->{template} = 'machine-status.tt';
|
||||
$self->status_ok($c, entity => $c->stash->{machines});
|
||||
|
@ -449,16 +480,28 @@ sub steps :Local Args(0) {
|
|||
|
||||
my $resultsPerPage = 20;
|
||||
|
||||
my $criteria = {
|
||||
"me.starttime" => { '!=', undef },
|
||||
"me.stoptime" => { '!=', undef }
|
||||
};
|
||||
|
||||
my $extra = {
|
||||
order_by => [ "me.stoptime desc" ],
|
||||
rows => $resultsPerPage,
|
||||
offset => ($page - 1) * $resultsPerPage,
|
||||
};
|
||||
|
||||
unless ($c->user_exists) {
|
||||
$criteria->{"project.private"} = 0;
|
||||
$extra->{join} = [{"build" => {"jobset" => "project"}}];
|
||||
}
|
||||
|
||||
$c->stash->{page} = $page;
|
||||
$c->stash->{resultsPerPage} = $resultsPerPage;
|
||||
$c->stash->{steps} = [ $c->model('DB::BuildSteps')->search(
|
||||
{ starttime => { '!=', undef },
|
||||
stoptime => { '!=', undef }
|
||||
},
|
||||
{ order_by => [ "stoptime desc" ],
|
||||
rows => $resultsPerPage,
|
||||
offset => ($page - 1) * $resultsPerPage
|
||||
}) ];
|
||||
$criteria,
|
||||
$extra
|
||||
) ];
|
||||
|
||||
$c->stash->{total} = approxTableSize($c, "IndexBuildStepsOnStopTime");
|
||||
}
|
||||
|
@ -479,28 +522,58 @@ sub search :Local Args(0) {
|
|||
|
||||
$c->model('DB')->schema->txn_do(sub {
|
||||
$c->model('DB')->schema->storage->dbh->do("SET LOCAL statement_timeout = 20000");
|
||||
$c->stash->{projects} = [ $c->model('DB::Projects')->search(
|
||||
{ -and =>
|
||||
|
||||
my $projectCriteria = {
|
||||
-and =>
|
||||
[ { -or => [ name => { ilike => "%$query%" }, displayName => { ilike => "%$query%" }, description => { ilike => "%$query%" } ] }
|
||||
, { hidden => 0 }
|
||||
]
|
||||
},
|
||||
{ order_by => ["name"] } ) ];
|
||||
};
|
||||
|
||||
$c->stash->{jobsets} = [ $c->model('DB::Jobsets')->search(
|
||||
{ -and =>
|
||||
my $jobsetCriteria = {
|
||||
-and =>
|
||||
[ { -or => [ "me.name" => { ilike => "%$query%" }, "me.description" => { ilike => "%$query%" } ] }
|
||||
, { "project.hidden" => 0, "me.hidden" => 0 }
|
||||
]
|
||||
},
|
||||
{ order_by => ["project", "name"], join => ["project"] } ) ];
|
||||
};
|
||||
|
||||
$c->stash->{jobs} = [ $c->model('DB::Builds')->search(
|
||||
{ "job" => { ilike => "%$query%" }
|
||||
my $buildCriteria = {
|
||||
"job" => { ilike => "%$query%" }
|
||||
, "project.hidden" => 0
|
||||
, "jobset.hidden" => 0
|
||||
, iscurrent => 1
|
||||
},
|
||||
};
|
||||
|
||||
my $buildSearchExtra = {
|
||||
order_by => ["id desc"]
|
||||
, rows => $c->stash->{limit}, join => []
|
||||
};
|
||||
|
||||
my $outCriteria = {
|
||||
"buildoutputs.path" => { ilike => "%$query%" }
|
||||
};
|
||||
|
||||
my $drvCriteria = { "drvpath" => { ilike => "%$query%" } };
|
||||
|
||||
unless ($c->user_exists) {
|
||||
$projectCriteria->{private} = 0;
|
||||
$jobsetCriteria->{"project.private"} = 0;
|
||||
$buildCriteria->{"project.private"} = 0;
|
||||
push @{$buildSearchExtra->{join}}, {"jobset" => "project"};
|
||||
$outCriteria->{"project.private"} = 0;
|
||||
$drvCriteria->{"project.private"} = 0;
|
||||
}
|
||||
|
||||
$c->stash->{projects} = [ $c->model('DB::Projects')->search(
|
||||
$projectCriteria,
|
||||
{ order_by => ["name"] } ) ];
|
||||
|
||||
$c->stash->{jobsets} = [ $c->model('DB::Jobsets')->search(
|
||||
$jobsetCriteria,
|
||||
{ order_by => ["project", "name"], join => ["project"] } ) ];
|
||||
|
||||
$c->stash->{jobs} = [ $c->model('DB::Builds')->search(
|
||||
$buildCriteria,
|
||||
{
|
||||
order_by => ["jobset.project", "jobset.name", "job"],
|
||||
join => { "jobset" => "project" },
|
||||
|
@ -509,17 +582,16 @@ sub search :Local Args(0) {
|
|||
];
|
||||
|
||||
# Perform build search in separate queries to prevent seq scan on buildoutputs table.
|
||||
my $outExtra = $buildSearchExtra;
|
||||
push @{$outExtra->{join}}, "buildoutputs";
|
||||
$c->stash->{builds} = [ $c->model('DB::Builds')->search(
|
||||
{ "buildoutputs.path" => { ilike => "%$query%" } },
|
||||
{ order_by => ["id desc"], join => ["buildoutputs"]
|
||||
, rows => $c->stash->{limit}
|
||||
} ) ];
|
||||
$outCriteria,
|
||||
$outExtra
|
||||
) ];
|
||||
|
||||
$c->stash->{buildsdrv} = [ $c->model('DB::Builds')->search(
|
||||
{ "drvpath" => { ilike => "%$query%" } },
|
||||
{ order_by => ["id desc"]
|
||||
, rows => $c->stash->{limit}
|
||||
} ) ];
|
||||
$drvCriteria,
|
||||
$buildSearchExtra ) ];
|
||||
|
||||
$c->stash->{resource} = { projects => $c->stash->{projects},
|
||||
jobsets => $c->stash->{jobsets},
|
||||
|
|
|
@ -29,6 +29,7 @@ our @EXPORT = qw(
|
|||
approxTableSize
|
||||
requireLocalStore
|
||||
dbh
|
||||
checkProjectVisibleForGuest
|
||||
);
|
||||
|
||||
|
||||
|
@ -256,6 +257,14 @@ sub requireProjectOwner {
|
|||
unless isProjectOwner($c, $project);
|
||||
}
|
||||
|
||||
sub checkProjectVisibleForGuest {
|
||||
my ($c, $project) = @_;
|
||||
if (defined $project && $project->private == 1 && !$c->user_exists) {
|
||||
my $projectName = $project->name;
|
||||
notFound($c, "Project ‘$projectName’ not found!");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
sub isAdmin {
|
||||
my ($c) = @_;
|
||||
|
|
|
@ -36,6 +36,7 @@ our @EXPORT = qw(
|
|||
jobsetOverview
|
||||
jobsetOverview_
|
||||
pathIsInsidePrefix
|
||||
readIntoSocket
|
||||
readNixFile
|
||||
registerRoot
|
||||
restartBuilds
|
||||
|
@ -181,17 +182,34 @@ sub findLog {
|
|||
my ($c, $drvPath, @outPaths) = @_;
|
||||
|
||||
if (defined $drvPath) {
|
||||
unless ($c->user_exists) {
|
||||
my $existsForGuest = $c->model('DB::BuildSteps')->search(
|
||||
{"me.drvpath" => $drvPath, "project.private" => 0},
|
||||
{join => {build => {"jobset" => "project"}}}
|
||||
);
|
||||
if ($existsForGuest == 0) {
|
||||
notFound($c, "Resource not found");
|
||||
}
|
||||
}
|
||||
|
||||
my $logPath = getDrvLogPath($drvPath);
|
||||
return $logPath if defined $logPath;
|
||||
}
|
||||
|
||||
return undef if scalar @outPaths == 0;
|
||||
|
||||
my $join = ["buildstepoutputs"];
|
||||
my $criteria = { path => { -in => [@outPaths] } };
|
||||
unless ($c->user_exists) {
|
||||
push @{$join}, {"build" => {jobset => "project"}};
|
||||
$criteria->{"project.private"} = 0;
|
||||
}
|
||||
|
||||
my @steps = $c->model('DB::BuildSteps')->search(
|
||||
{ path => { -in => [@outPaths] } },
|
||||
$criteria,
|
||||
{ select => ["drvpath"]
|
||||
, distinct => 1
|
||||
, join => "buildstepoutputs"
|
||||
, join => $join
|
||||
});
|
||||
|
||||
foreach my $step (@steps) {
|
||||
|
@ -284,9 +302,19 @@ sub getEvals {
|
|||
|
||||
my $me = $evals_result_set->current_source_alias;
|
||||
|
||||
my @evals = $evals_result_set->search(
|
||||
{ hasnewbuilds => 1 },
|
||||
{ order_by => "$me.id DESC", rows => $rows, offset => $offset });
|
||||
my $criteria = { hasnewbuilds => 1 };
|
||||
my $extra = {
|
||||
order_by => "$me.id DESC",
|
||||
rows => $rows,
|
||||
offset => $offset
|
||||
};
|
||||
unless ($c->user_exists) {
|
||||
$extra->{join} = {"jobset" => "project"};
|
||||
$criteria->{"project.private"} = 0;
|
||||
}
|
||||
|
||||
my @evals = $evals_result_set->search($criteria, $extra);
|
||||
|
||||
my @res = ();
|
||||
my $cache = {};
|
||||
|
||||
|
@ -406,6 +434,17 @@ sub pathIsInsidePrefix {
|
|||
return $cur;
|
||||
}
|
||||
|
||||
sub readIntoSocket{
|
||||
my (%args) = @_;
|
||||
my $sock;
|
||||
|
||||
eval {
|
||||
my $x= join(" ", @{$args{cmd}});
|
||||
open($sock, "-|", $x) or die q(failed to open socket from command:\n $x);
|
||||
};
|
||||
|
||||
return $sock;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -62,6 +62,12 @@ __PACKAGE__->table("projects");
|
|||
default_value: 0
|
||||
is_nullable: 0
|
||||
|
||||
=head2 private
|
||||
|
||||
data_type: 'integer'
|
||||
default_value: 0
|
||||
is_nullable: 0
|
||||
|
||||
=head2 owner
|
||||
|
||||
data_type: 'text'
|
||||
|
@ -107,6 +113,8 @@ __PACKAGE__->add_columns(
|
|||
{ data_type => "integer", default_value => 1, is_nullable => 0 },
|
||||
"hidden",
|
||||
{ data_type => "integer", default_value => 0, is_nullable => 0 },
|
||||
"private",
|
||||
{ data_type => "integer", default_value => 0, is_nullable => 0 },
|
||||
"owner",
|
||||
{ data_type => "text", is_foreign_key => 1, is_nullable => 0 },
|
||||
"homepage",
|
||||
|
@ -236,8 +244,8 @@ Composing rels: L</projectmembers> -> username
|
|||
__PACKAGE__->many_to_many("usernames", "projectmembers", "username");
|
||||
|
||||
|
||||
# Created by DBIx::Class::Schema::Loader v0.07049 @ 2022-01-24 14:20:32
|
||||
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:PtXDyT8Pc7LYhhdEG39EKQ
|
||||
# Created by DBIx::Class::Schema::Loader v0.07049 @ 2022-11-22 12:51:02
|
||||
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:ppyLpFU2fZASFANhD7vUgg
|
||||
|
||||
use JSON::MaybeXS;
|
||||
|
||||
|
@ -267,6 +275,7 @@ sub as_json {
|
|||
"enabled" => $self->get_column("enabled") ? JSON::MaybeXS::true : JSON::MaybeXS::false,
|
||||
"enable_dynamic_run_command" => $self->get_column("enable_dynamic_run_command") ? JSON::MaybeXS::true : JSON::MaybeXS::false,
|
||||
"hidden" => $self->get_column("hidden") ? JSON::MaybeXS::true : JSON::MaybeXS::false,
|
||||
"private" => $self->get_column("private") ? JSON::MaybeXS::true : JSON::MaybeXS::false,
|
||||
|
||||
"jobsets" => [ map { $_->name } $self->jobsets ]
|
||||
);
|
||||
|
|
|
@ -17,6 +17,13 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3" for="editprojectprivate">Private</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="checkbox" id="editprojectprivate" name="private" [% IF project.private %] checked="checked" [% END %]/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3" for="editprojectidentifier">Identifier</label>
|
||||
<div class="col-sm-9">
|
||||
|
|
|
@ -54,7 +54,7 @@
|
|||
<tbody>
|
||||
[% FOREACH p IN projects %]
|
||||
<tr class="project [% IF !p.enabled %]disabled-project[% END %]">
|
||||
<td><span class="[% IF !p.enabled %]disabled-project[% END %] [%+ IF p.hidden %]hidden-project[% END %]">[% INCLUDE renderProjectName project=p.name inRow=1 %]</span></td>
|
||||
<td>[% IF p.private %]🔒[% END %] <span class="[% IF !p.enabled %]disabled-project[% END %] [%+ IF p.hidden %]hidden-project[% END %]">[% INCLUDE renderProjectName project=p.name inRow=1 %]</span></td>
|
||||
<td>[% HTML.escape(p.displayname) %]</td>
|
||||
<td>[% WRAPPER maybeLink uri=p.homepage %][% HTML.escape(p.description) %][% END %]</td>
|
||||
</tr>
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
[% WRAPPER layout.tt title="Project $project.name" %]
|
||||
[% IF project.private %]
|
||||
[% lock = ' 🔒' %]
|
||||
[% ELSE %]
|
||||
[% lock = '' %]
|
||||
[% END %]
|
||||
[% WRAPPER layout.tt titleHTML="Project $project.name$lock" title="Project $project.name" %]
|
||||
[% PROCESS common.tt %]
|
||||
|
||||
<ul class="nav nav-tabs">
|
||||
|
|
|
@ -373,6 +373,8 @@ sub evalJobs {
|
|||
push @cmd, "--meta";
|
||||
push @cmd, "--force-recurse";
|
||||
push @cmd, ("--option", "allow-import-from-derivation", "false") if $config->{allow_import_from_derivation} // "true" ne "true";
|
||||
push @cmd, ("--workers", $config->{evaluator_workers} // 1);
|
||||
push @cmd, ("--max-memory-size", $config->{evaluator_max_memory_size} // 4096);
|
||||
|
||||
if (defined $ENV{'HYDRA_DEBUG'}) {
|
||||
sub escape {
|
||||
|
|
|
@ -44,6 +44,7 @@ create table Projects (
|
|||
description text,
|
||||
enabled integer not null default 1,
|
||||
hidden integer not null default 0,
|
||||
private integer not null default 0,
|
||||
owner text not null,
|
||||
homepage text, -- URL for the project
|
||||
declfile text, -- File containing declarative jobset specification
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
|
||||
|
||||
-- Records of RunCommand executions
|
||||
--
|
||||
-- The intended flow is:
|
||||
|
|
2
src/sql/upgrade-85.sql
Normal file
2
src/sql/upgrade-85.sql
Normal file
|
@ -0,0 +1,2 @@
|
|||
alter table Projects
|
||||
add column private integer not null default 0;
|
|
@ -51,7 +51,8 @@ subtest "Read project 'tests'" => sub {
|
|||
homepage => "",
|
||||
jobsets => [],
|
||||
name => "tests",
|
||||
owner => "root"
|
||||
owner => "root",
|
||||
"private" => JSON::MaybeXS::false
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -96,7 +97,8 @@ subtest "Transitioning from declarative project to normal" => sub {
|
|||
file => "bogus",
|
||||
type => "boolean",
|
||||
value => "false"
|
||||
}
|
||||
},
|
||||
"private" => JSON::MaybeXS::false
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -135,7 +137,8 @@ subtest "Transitioning from declarative project to normal" => sub {
|
|||
homepage => "",
|
||||
jobsets => [],
|
||||
name => "tests",
|
||||
owner => "root"
|
||||
owner => "root",
|
||||
"private" => JSON::MaybeXS::false
|
||||
});
|
||||
};
|
||||
};
|
||||
|
|
|
@ -101,7 +101,7 @@ sub new {
|
|||
$opts{'before_init'}->($self);
|
||||
}
|
||||
|
||||
expectOkay(5, ("hydra-init"));
|
||||
expectOkay(30, ("hydra-init"));
|
||||
|
||||
return $self;
|
||||
}
|
||||
|
|
|
@ -70,7 +70,7 @@ sub add_user {
|
|||
my $email = $opts{'email'} // "$name\@example";
|
||||
my $password = $opts{'password'} // rand_chars();
|
||||
|
||||
my ($res, $stdout, $stderr) = captureStdoutStderr(1, ("slappasswd", "-s", $password));
|
||||
my ($res, $stdout, $stderr) = captureStdoutStderr(5, ("slappasswd", "-s", $password));
|
||||
if ($res) {
|
||||
die "Failed to execute slappasswd ($res): $stderr, $stdout";
|
||||
}
|
||||
|
@ -178,7 +178,7 @@ sub start {
|
|||
sub validateConfig {
|
||||
my ($self) = @_;
|
||||
|
||||
expectOkay(1, ("slaptest", "-u", "-F", $self->{"_slapd_dir"}));
|
||||
expectOkay(5, ("slaptest", "-u", "-F", $self->{"_slapd_dir"}));
|
||||
}
|
||||
|
||||
sub _spawn {
|
||||
|
@ -218,7 +218,7 @@ sub load_ldif {
|
|||
|
||||
my $path = "${\$self->{'_tmpdir'}}/load.ldif";
|
||||
write_file($path, $content);
|
||||
expectOkay(1, ("slapadd", "-F", $self->{"_slapd_dir"}, "-b", $suffix, "-l", $path));
|
||||
expectOkay(5, ("slapadd", "-F", $self->{"_slapd_dir"}, "-b", $suffix, "-l", $path));
|
||||
$self->validateConfig();
|
||||
}
|
||||
|
||||
|
|
168
t/private-projects.t
Normal file
168
t/private-projects.t
Normal file
|
@ -0,0 +1,168 @@
|
|||
use strict;
|
||||
use warnings;
|
||||
use Setup;
|
||||
use Test2::V0;
|
||||
use HTTP::Request::Common;
|
||||
use HTML::TreeBuilder::XPath;
|
||||
use JSON::MaybeXS;
|
||||
|
||||
my %ctx = test_init(
|
||||
use_external_destination_store => 0
|
||||
);
|
||||
|
||||
require Hydra::Schema;
|
||||
require Hydra::Model::DB;
|
||||
|
||||
require Catalyst::Test;
|
||||
Catalyst::Test->import('Hydra');
|
||||
my $db = Hydra::Model::DB->new;
|
||||
hydra_setup($db);
|
||||
|
||||
my $scratch = "$ctx{tmpdir}/scratch";
|
||||
mkdir $scratch;
|
||||
|
||||
my $uri = "file://$scratch/git-repo";
|
||||
my $jobset = createJobsetWithOneInput('gitea', 'git-input.nix', 'src', 'git', $uri, $ctx{jobsdir});
|
||||
|
||||
ok(request('/project/tests')->is_success, "Project 'tests' exists");
|
||||
my $project = $db->resultset('Projects')->find({name => "tests"})->update({private => 1});
|
||||
ok(
|
||||
!request('/project/tests')->is_success,
|
||||
"Project 'tests' is private now and should be unreachable"
|
||||
);
|
||||
|
||||
my $user = $db->resultset('Users')->create({
|
||||
username => "testing",
|
||||
emailaddress => 'testing@invalid.org',
|
||||
password => ''
|
||||
});
|
||||
$user->setPassword('foobar');
|
||||
|
||||
my $auth = request(
|
||||
POST(
|
||||
'/login',
|
||||
{username => 'testing', 'password' => 'foobar'},
|
||||
Origin => 'http://localhost', Accept => 'application/json'
|
||||
),
|
||||
{host => 'localhost'}
|
||||
);
|
||||
|
||||
ok(
|
||||
$auth->code == 302,
|
||||
"Successfully logged in"
|
||||
);
|
||||
|
||||
my $cookie = (split /;/, $auth->header('set_cookie'))[0];
|
||||
|
||||
ok(
|
||||
request(GET(
|
||||
'/project/tests',
|
||||
Cookie => $cookie
|
||||
))->is_success,
|
||||
"Project visible for authenticated user."
|
||||
);
|
||||
|
||||
updateRepository('gitea', "$ctx{testdir}/jobs/git-update.sh", $scratch);
|
||||
|
||||
ok(evalSucceeds($jobset), "Evaluating nix expression");
|
||||
is(nrQueuedBuildsForJobset($jobset), 1, "Evaluating jobs/runcommand.nix should result in 1 build1");
|
||||
|
||||
ok(
|
||||
request('/eval/1')->code == 404,
|
||||
'Eval of private project not available for unauthenticated user.'
|
||||
);
|
||||
|
||||
ok(
|
||||
request(GET '/eval/1', Cookie => $cookie)->is_success,
|
||||
'Eval available for authenticated User'
|
||||
);
|
||||
|
||||
ok(
|
||||
request(GET '/jobset/tests/gitea', Cookie => $cookie)->is_success,
|
||||
'Jobset available for user'
|
||||
);
|
||||
|
||||
ok(
|
||||
request(GET '/jobset/tests/gitea')->code == 404,
|
||||
'Jobset unavailable for guest'
|
||||
);
|
||||
|
||||
ok(
|
||||
request('/build/1')->code == 404,
|
||||
'Build of private project not available for unauthenticated user.'
|
||||
);
|
||||
|
||||
ok(
|
||||
request(GET '/build/1', Cookie => $cookie)->is_success,
|
||||
'Build available for authenticated User'
|
||||
);
|
||||
|
||||
(my $build) = queuedBuildsForJobset($jobset);
|
||||
ok(runBuild($build), "Build should succeed with exit code 0");
|
||||
|
||||
ok(
|
||||
request(GET '/jobset/tests/gitea/channel/latest', Cookie => $cookie)->is_success,
|
||||
'Channel available for authenticated user'
|
||||
);
|
||||
|
||||
ok(
|
||||
request(GET '/jobset/tests/gitea/channel/latest')->code == 404,
|
||||
'Channel unavailable for guest'
|
||||
);
|
||||
|
||||
updateRepository('gitea', "$ctx{testdir}/jobs/git-update.sh", $scratch);
|
||||
ok(evalSucceeds($jobset), "Evaluating nix expression");
|
||||
|
||||
my $latest_builds_unauth = request(GET "/all");
|
||||
|
||||
my $tree = HTML::TreeBuilder::XPath->new;
|
||||
$tree->parse($latest_builds_unauth->content);
|
||||
ok(!$tree->exists('/html//tbody/tr'), "No builds available");
|
||||
|
||||
my $latest_builds = request(GET "/all", Cookie => $cookie);
|
||||
|
||||
$tree = HTML::TreeBuilder::XPath->new;
|
||||
$tree->parse($latest_builds->content);
|
||||
ok($tree->exists('/html//tbody/tr'), "Builds available");
|
||||
|
||||
my $p2 = $db->resultset("Projects")->create({name => "public", displayname => "public", owner => "root"});
|
||||
my $jobset2 = $p2->jobsets->create({
|
||||
name => "public", nixexprpath => 'basic.nix', nixexprinput => "jobs", emailoverride => ""
|
||||
});
|
||||
|
||||
my $jobsetinput = $jobset2->jobsetinputs->create({name => "jobs", type => "path"});
|
||||
$jobsetinput->jobsetinputalts->create({altnr => 0, value => $ctx{jobsdir}});
|
||||
|
||||
updateRepository('gitea', "$ctx{testdir}/jobs/git-update.sh", $scratch);
|
||||
ok(evalSucceeds($jobset2), "Evaluating nix expression");
|
||||
is(
|
||||
nrQueuedBuildsForJobset($jobset2),
|
||||
3,
|
||||
"Evaluating jobs/runcommand.nix should result in 3 builds"
|
||||
);
|
||||
|
||||
(my $b1, my $b2, my $b3) = queuedBuildsForJobset($jobset2);
|
||||
ok(runBuild($b1), "Build should succeed with exit code 0");
|
||||
ok(runBuild($b2), "Build should succeed with exit code 0");
|
||||
ok(runBuild($b3), "Build should succeed with exit code 0");
|
||||
my $latest_builds_unauth2 = request(GET "/all");
|
||||
|
||||
$tree = HTML::TreeBuilder::XPath->new;
|
||||
$tree->parse($latest_builds_unauth2->content);
|
||||
is(
|
||||
scalar $tree->findvalues('/html//tbody/tr'),
|
||||
3,
|
||||
"Three builds available"
|
||||
);
|
||||
|
||||
my $latest_builds2 = request(GET "/all", Cookie => $cookie);
|
||||
|
||||
$tree = HTML::TreeBuilder::XPath->new;
|
||||
$tree->parse($latest_builds2->content);
|
||||
is(
|
||||
scalar $tree->findvalues('/html//tbody/tr'),
|
||||
4,
|
||||
"Three builds available"
|
||||
);
|
||||
|
||||
done_testing;
|
|
@ -39,7 +39,7 @@ subtest "Building, caching, and then garbage collecting the underlying job" => s
|
|||
|
||||
ok(unlink(Hydra::Helper::Nix::gcRootFor($path)), "Unlinking the GC root for underlying Dependency succeeds");
|
||||
|
||||
(my $ret, my $stdout, my $stderr) = captureStdoutStderr(5, "nix-store", "--delete", $path);
|
||||
(my $ret, my $stdout, my $stderr) = captureStdoutStderr(15, "nix-store", "--delete", $path);
|
||||
is($ret, 0, "Deleting the underlying dependency should succeed");
|
||||
};
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ my $db = $ctx->db();
|
|||
|
||||
subtest "Handling password and password hash creation" => sub {
|
||||
subtest "Creating a user with a plain text password (insecure) stores the password securely" => sub {
|
||||
my ($res, $stdout, $stderr) = captureStdoutStderr(5, ("hydra-create-user", "plain-text-user", "--password", "foobar"));
|
||||
my ($res, $stdout, $stderr) = captureStdoutStderr(15, ("hydra-create-user", "plain-text-user", "--password", "foobar"));
|
||||
is($res, 0, "hydra-create-user should exit zero");
|
||||
like($stderr, qr/Submitting plaintext passwords as arguments is deprecated and will be removed/, "Submitting a plain text password is deprecated.");
|
||||
|
||||
|
@ -23,7 +23,7 @@ subtest "Handling password and password hash creation" => sub {
|
|||
};
|
||||
|
||||
subtest "Creating a user with a sha1 password (still insecure) stores the password as a hashed sha1" => sub {
|
||||
my ($res, $stdout, $stderr) = captureStdoutStderr(5, ("hydra-create-user", "old-password-hash-user", "--password-hash", "8843d7f92416211de9ebb963ff4ce28125932878"));
|
||||
my ($res, $stdout, $stderr) = captureStdoutStderr(15, ("hydra-create-user", "old-password-hash-user", "--password-hash", "8843d7f92416211de9ebb963ff4ce28125932878"));
|
||||
is($res, 0, "hydra-create-user should exit zero");
|
||||
|
||||
my $user = $db->resultset('Users')->find({ username => "old-password-hash-user" });
|
||||
|
@ -36,7 +36,7 @@ subtest "Handling password and password hash creation" => sub {
|
|||
};
|
||||
|
||||
subtest "Creating a user with an argon2 password stores the password as given" => sub {
|
||||
my ($res, $stdout, $stderr) = captureStdoutStderr(5, ("hydra-create-user", "argon2-hash-user", "--password-hash", '$argon2id$v=19$m=262144,t=3,p=1$tMnV5paYjmIrUIb6hylaNA$M8/e0i3NGrjhOliVLa5LqQ'));
|
||||
my ($res, $stdout, $stderr) = captureStdoutStderr(15, ("hydra-create-user", "argon2-hash-user", "--password-hash", '$argon2id$v=19$m=262144,t=3,p=1$tMnV5paYjmIrUIb6hylaNA$M8/e0i3NGrjhOliVLa5LqQ'));
|
||||
is($res, 0, "hydra-create-user should exit zero");
|
||||
|
||||
my $user = $db->resultset('Users')->find({ username => "argon2-hash-user" });
|
||||
|
@ -50,7 +50,7 @@ subtest "Handling password and password hash creation" => sub {
|
|||
|
||||
subtest "Creating a user by prompting for the password" => sub {
|
||||
subtest "with the same password twice" => sub {
|
||||
my ($res, $stdout, $stderr) = captureStdoutStderrWithStdin(5, ["hydra-create-user", "prompted-pass-user", "--password-prompt"], "my-password\nmy-password\n");
|
||||
my ($res, $stdout, $stderr) = captureStdoutStderrWithStdin(15, ["hydra-create-user", "prompted-pass-user", "--password-prompt"], "my-password\nmy-password\n");
|
||||
is($res, 0, "hydra-create-user should exit zero");
|
||||
|
||||
my $user = $db->resultset('Users')->find({ username => "prompted-pass-user" });
|
||||
|
@ -62,7 +62,7 @@ subtest "Handling password and password hash creation" => sub {
|
|||
};
|
||||
|
||||
subtest "With mismatched password confirmation" => sub {
|
||||
my ($res, $stdout, $stderr) = captureStdoutStderrWithStdin(5, ["hydra-create-user", "prompted-pass-user", "--password-prompt"], "my-password\nnot-my-password\n");
|
||||
my ($res, $stdout, $stderr) = captureStdoutStderrWithStdin(15, ["hydra-create-user", "prompted-pass-user", "--password-prompt"], "my-password\nnot-my-password\n");
|
||||
isnt($res, 0, "hydra-create-user should exit non-zero");
|
||||
};
|
||||
};
|
||||
|
@ -76,7 +76,7 @@ subtest "Handling password and password hash creation" => sub {
|
|||
);
|
||||
|
||||
for my $case (@cases) {
|
||||
my ($res, $stdout, $stderr) = captureStdoutStderr(5, (
|
||||
my ($res, $stdout, $stderr) = captureStdoutStderr(15, (
|
||||
"hydra-create-user", "bogus-password-options", @{$case}));
|
||||
like($stderr, qr/please specify only one of --password-prompt or --password-hash/, "We get an error about specifying the password");
|
||||
isnt($res, 0, "hydra-create-user should exit non-zero with conflicting " . join(" ", @{$case}));
|
||||
|
@ -84,7 +84,7 @@ subtest "Handling password and password hash creation" => sub {
|
|||
};
|
||||
|
||||
subtest "A password is not required for creating a Google-based account" => sub {
|
||||
my ($res, $stdout, $stderr) = captureStdoutStderr(5, (
|
||||
my ($res, $stdout, $stderr) = captureStdoutStderr(15, (
|
||||
"hydra-create-user", "google-account", "--type", "google"));
|
||||
is($res, 0, "hydra-create-user should exit zero");
|
||||
};
|
||||
|
|
|
@ -28,7 +28,7 @@ subtest "hydra-init upgrades user's password hashes from sha1 to sha1 inside Arg
|
|||
$janet->setPassword("foobar");
|
||||
|
||||
is($alice->password, "8843d7f92416211de9ebb963ff4ce28125932878", "Alices's sha1 is stored in the database");
|
||||
my ($res, $stdout, $stderr) = captureStdoutStderr(5, ("hydra-init"));
|
||||
my ($res, $stdout, $stderr) = captureStdoutStderr(30, ("hydra-init"));
|
||||
if ($res != 0) {
|
||||
is($stdout, "");
|
||||
is($stderr, "");
|
||||
|
@ -55,7 +55,7 @@ subtest "hydra-init upgrades user's password hashes from sha1 to sha1 inside Arg
|
|||
};
|
||||
|
||||
subtest "Running hydra-init don't break Alice or Janet's passwords" => sub {
|
||||
my ($res, $stdout, $stderr) = captureStdoutStderr(5, ("hydra-init"));
|
||||
my ($res, $stdout, $stderr) = captureStdoutStderr(30, ("hydra-init"));
|
||||
is($res, 0, "hydra-init should exit zero");
|
||||
|
||||
my $updatedAlice = $db->resultset('Users')->find({ username => "alice" });
|
||||
|
|
|
@ -21,7 +21,7 @@ if (defined($ENV{"NIX_BUILD_CORES"})
|
|||
print STDERR "test.pl: Defaulting \$YATH_JOB_COUNT to \$NIX_BUILD_CORES (${\$ENV{'NIX_BUILD_CORES'}})\n";
|
||||
}
|
||||
|
||||
system($^X, find_yath(), '-D', 'test', '--default-search' => './', @ARGV);
|
||||
system($^X, find_yath(), '-D', 'test', '--qvf', '--event-timeout', 240, '--default-search' => './', @ARGV);
|
||||
my $exit = $?;
|
||||
|
||||
# This makes sure it works with prove.
|
||||
|
|
Loading…
Reference in a new issue