├── .envrc
├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── ci
├── jobset.nix
├── jobsets-declarative.nix
└── spec.json
├── default.nix
├── hydra-cli
├── Cargo.lock
├── Cargo.toml
├── data
│ ├── eval-1525352.json
│ └── search-build.json
└── src
│ ├── hydra
│ ├── client.rs
│ ├── example.rs
│ ├── mod.rs
│ ├── reqwest_client.rs
│ └── types.rs
│ ├── lib.rs
│ ├── main.rs
│ ├── ops
│ ├── jobset_create.rs
│ ├── jobset_delete.rs
│ ├── jobset_eval.rs
│ ├── jobset_wait.rs
│ ├── mod.rs
│ ├── project.rs
│ ├── project_create.rs
│ ├── project_list.rs
│ ├── reproduce.rs
│ └── search.rs
│ └── pretty.rs
├── logo.png
├── nix
├── nixpkgs.nix
├── sources.json
└── sources.nix
├── package.nix
├── shell.nix
└── tests
└── vm.nix
/.envrc:
--------------------------------------------------------------------------------
1 | eval "$(lorri direnv)"
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | target
2 | **/*.rs.bk
3 | *~
4 | result
5 | result-*
6 | .gitignore
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | matrix:
2 | include:
3 | - language: nix
4 | nix: 2.2.2
5 | if: type = pull_request
6 | script:
7 | # This is because the unstable channel has not been updated yet
8 | - nix-env -f https://github.com/NixOS/nixpkgs/archive/1f5fa9a8298ec7411431da981b4f1a79e10f2a8e.tar.gz -i hydra-cli
9 | - hydra-cli -H https://hydra.nix.corp.cloudwatt.com jobset-wait hydra-cli $TRAVIS_PULL_REQUEST
10 | - language: rust
11 | rust:
12 | - stable
13 | cache: cargo
14 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Orange
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # hydra-cli
2 |
3 | [](https://travis-ci.com/nlewo/hydra-cli)
4 |
5 | `hydra-cli` lets you talk to the JSON API of [hydra](https://nixos.org/hydra) on the command line. It
6 | includes commands for querying and creating projects and jobsets.
7 |
8 |
9 |
12 |
13 |
14 | ## Installation
15 |
16 | Use `nix-env` to install:
17 |
18 | ```
19 | nix-env -f https://github.com/nlewo/hydra-cli/archive/master.tar.gz -iA hydra-cli
20 | ```
21 |
22 | ## Build
23 |
24 | Inside the provided nix shell:
25 |
26 | ```bash
27 | $ cargo build
28 | ```
29 |
30 | ## Usage
31 |
32 | `hydra-cli` talks to the JSON API of hydra and allows you to query, create and wait for
33 | projects and jobsets:
34 |
35 | - [project-create](#project-create) Creates a project.
36 | - [project-list](#project-list) Lists existing projects.
37 | - [project-show](#project-show) Shows the projects' information.
38 | - [jobset-create](#jobset-create) Creates a jobset in a project.
39 | - [jobset-wait](#jobset-wait) Waits for a jobset's completion.
40 | - [reproduce](#reproduce) Retrieves information for reproducing an output path.
41 | - [search](#search) Searches for an output path.
42 |
43 | By default `hydra-cli` talks to https://hydra.nixos.org. The default can be
44 | overwritten by setting the `HYDRA_HOST` environment variable or by passing `--host ` on the
45 | command line.
46 |
47 | ### Commands
48 |
49 | `$ hydra-cli --help`
50 | ```
51 | hydra-cli 0.3.0
52 | lewo
53 | CLI Hydra client
54 |
55 | USAGE:
56 | hydra-cli [FLAGS] [OPTIONS] [SUBCOMMAND]
57 |
58 | FLAGS:
59 | -h, --help Prints help information
60 | --no-check-certificate Disable TLS certificate check for the Hydra host
61 | -V, --version Prints version information
62 |
63 | OPTIONS:
64 | -H, --host Hydra host URL [env: HYDRA_HOST=] [default: https://hydra.nixos.org]
65 |
66 | SUBCOMMANDS:
67 | help Prints this message or the help of the given subcommand(s)
68 | jobset-create Add a jobset to a project
69 | jobset-eval Evaluate a jobset
70 | jobset-wait Wait for jobset completion
71 | project-create Create a new project
72 | project-list List projects
73 | project-show Get information of a project
74 | reproduce Retrieve information to reproduce an output path
75 | search Search by output paths
76 |
77 | A client to query Hydra through its JSON API.
78 | ```
79 | #### project-create
80 |
81 | The `project-create` command creates a new project under the name specified. The created project
82 | will be _enabled_ and _visible_. _Note_: this command requires user authentication.
83 |
84 | `$ hydra-cli project-create --help`
85 | ```
86 | hydra-cli-project-create
87 | Create a new project
88 |
89 | USAGE:
90 | hydra-cli project-create --password --user
91 |
92 | FLAGS:
93 | -h, --help Prints help information
94 | -V, --version Prints version information
95 |
96 | OPTIONS:
97 | --password A user password [env: HYDRA_PASSWORD=]
98 | --user A user name [env: HYDRA_USER=]
99 |
100 | ARGS:
101 | The name of the project in which to create the jobset
102 | ```
103 | #### project-list
104 |
105 | The `project-list` command retrieves a list of all configured projects.
106 |
107 | `$ hydra-cli project-list --help`
108 | ```
109 | hydra-cli-project-list
110 | List projects
111 |
112 | USAGE:
113 | hydra-cli project-list [FLAGS]
114 |
115 | FLAGS:
116 | -h, --help Prints help information
117 | -j JSON output
118 | -V, --version Prints version information
119 | ```
120 | #### project-show
121 |
122 | The `project-show` command displays information on a given project.
123 |
124 | `$ hydra-cli project-show --help`
125 | ```
126 | hydra-cli-project-show
127 | Get information of a project
128 |
129 | USAGE:
130 | hydra-cli project-show [FLAGS]
131 |
132 | FLAGS:
133 | -h, --help Prints help information
134 | -j JSON output
135 | -V, --version Prints version information
136 |
137 | ARGS:
138 | A project name
139 | ```
140 | #### jobset-create
141 |
142 | The `jobset-create` command creates a new jobset and adds it to the project specified.
143 |
144 | `$ hydra-cli jobset-create --help`
145 | ```
146 | hydra-cli-jobset-create
147 | Add a jobset to a project
148 |
149 | USAGE:
150 | hydra-cli jobset-create --password --user
151 |
152 | FLAGS:
153 | -h, --help Prints help information
154 | -V, --version Prints version information
155 |
156 | OPTIONS:
157 | --password A user password [env: HYDRA_PASSWORD=]
158 | --user A user name [env: HYDRA_USER=]
159 |
160 | ARGS:
161 | The project to add the jobset to
162 | The name of the jobset to create
163 | Project configuration JSON filepath
164 |
165 | Here is an example JSON config:
166 |
167 | {
168 | "description": "hydra-cli master jobset",
169 | "checkinterval": 60,
170 | "enabled": 1,
171 | "visible": true,
172 | "keepnr": 3,
173 | "nixexprinput": "src",
174 | "nixexprpath": "default.nix",
175 | "inputs": {
176 | "src": {
177 | "value": "https://github.com/nlewo/hydra-cli.git master",
178 | "type": "git",
179 | "revision": null,
180 | "uri": null
181 | }
182 | }
183 | }
184 | ```
185 | #### jobset-eval
186 |
187 | The `jobset-eval` command starts the evaluation of a jobset.
188 |
189 | `$ hydra-cli jobset-eval --help`
190 | ```
191 | hydra-cli-jobset-eval
192 | Evaluate a jobset
193 |
194 | USAGE:
195 | hydra-cli jobset-eval
196 |
197 | FLAGS:
198 | -h, --help Prints help information
199 | -V, --version Prints version information
200 |
201 | ARGS:
202 | The project to evaluate the jobset from
203 | The jobset to evaluate
204 | ```
205 | #### jobset-wait
206 |
207 | The `jobset-create` command waits until the specified jobset has been evaluated.
208 |
209 | `$ hydra-cli jobset-wait --help`
210 | ```
211 | hydra-cli-jobset-wait
212 | Wait for jobset completion
213 |
214 | USAGE:
215 | hydra-cli jobset-wait [OPTIONS]
216 |
217 | FLAGS:
218 | -h, --help Prints help information
219 | -V, --version Prints version information
220 |
221 | OPTIONS:
222 | --timeout Maximum time to wait for (in seconds and infinite by default)
223 |
224 | ARGS:
225 | The project of the jobset to wait for
226 | The name of the jobset to wait for
227 | ```
228 | #### reproduce
229 |
230 | The `reproduce` command retrieves information on how to reproduce a given output-path.
231 |
232 | `$ hydra-cli reproduce --help`
233 | ```
234 | hydra-cli-reproduce
235 | Retrieve information to reproduce an output path
236 |
237 | USAGE:
238 | hydra-cli reproduce [FLAGS]
239 |
240 | FLAGS:
241 | -h, --help Prints help information
242 | -j JSON output
243 | -V, --version Prints version information
244 |
245 | ARGS:
246 | Piece of an output path (hash, name,...)
247 | ```
248 | #### search
249 |
250 | The `search` command searches for a jobset based on the output-path specified.
251 |
252 | `$ hydra-cli search --help`
253 | ```
254 | hydra-cli-search
255 | Search by output paths
256 |
257 | USAGE:
258 | hydra-cli search [limit]
259 |
260 | FLAGS:
261 | -h, --help Prints help information
262 | -V, --version Prints version information
263 |
264 | ARGS:
265 | Piece of an output path (hash, name,...)
266 | How many results to return [default: 10]
267 | ```
268 | ### Contributing
269 |
270 | Contributions to the project are welcome in the form of GitHub PRs. Please consider
271 | the following guidelines before creating PRs:
272 |
273 | - Please make sure to format your code using `rustfmt`
274 | - If you are planning to make any considerable changes, you should first present your plans in a GitHub issue so it can be discussed.
275 | - If you are adding features please consider the possibility of adding a test in [tests/vm.nix](./tests/vm.nix)
276 |
277 | #### Updating the README
278 |
279 | `hydra-cli` uses [mdsh](https://github.com/zimbatm/mdsh) to create and verify the README.md you are looking at. Changes to the `hydra-cli`
280 | command syntax will failing tests on [hydra](https://hydra.nix.corp.cloudwatt.com/project/hydra-cli).
281 |
282 | You can execute the tests locally by running:
283 |
284 | ```
285 | nix-build -A tests.readme
286 | ```
287 |
288 | If you want to update the README.md after having made relevant changes to `hydra-cli` you can do so as follows:
289 |
290 | ```
291 | cp $(nix-build -A readme) ./README.md
292 | ```
293 |
294 |
295 | ### License
296 |
297 | - Licensed under [MIT](./LICENSE).
298 | - The `hydra-cli` logo has been created using [https://game-icons.net](https://game-icons.net/1x1/lorc/hydra.html).
299 |
--------------------------------------------------------------------------------
/ci/jobset.nix:
--------------------------------------------------------------------------------
1 | import ../default.nix {}
2 |
--------------------------------------------------------------------------------
/ci/jobsets-declarative.nix:
--------------------------------------------------------------------------------
1 | { nixpkgs, declInput, pulls }:
2 | let
3 | pkgs = import nixpkgs {};
4 | prs = builtins.fromJSON (builtins.readFile pulls);
5 | prJobsets = pkgs.lib.mapAttrs (num: info:
6 | { enabled = 1;
7 | hidden = false;
8 | description = "PR ${num}: ${info.title}";
9 | nixexprinput = "nixexpr";
10 | nixexprpath = "ci/jobset.nix";
11 | checkinterval = 60;
12 | schedulingshares = 20;
13 | enableemail = false;
14 | emailoverride = "";
15 | keepnr = 1;
16 | inputs = {
17 | nixexpr = {
18 | type = "git";
19 | value = "git://github.com/${info.base.repo.owner.login}/${info.base.repo.name}.git pull/${num}/head";
20 | emailresponsible = false;
21 | };
22 | nixpkgs = {
23 | value = "https://github.com/NixOS/nixpkgs f52505fac8c82716872a616c501ad9eff188f97f";
24 | type = "git";
25 | emailresponsible = false;
26 | };
27 | };
28 | }
29 | ) prs;
30 | desc = prJobsets // {
31 | trunk = {
32 | description = "Build master of nlewo/hydra-cli";
33 | checkinterval = 60;
34 | enabled = 1;
35 | nixexprinput = "nixexpr";
36 | nixexprpath = "ci/jobset.nix";
37 | schedulingshares = 100;
38 | enableemail = false;
39 | emailoverride = "";
40 | keepnr = 3;
41 | hidden = false;
42 | inputs = {
43 | nixexpr = {
44 | value = "https://github.com/nlewo/hydra-cli";
45 | type = "git";
46 | emailresponsible = false;
47 | };
48 | # Only used by Niv to get its fetchers.
49 | # Belongs to branch 19.03.
50 | nixpkgs = {
51 | value = "https://github.com/NixOS/nixpkgs f52505fac8c82716872a616c501ad9eff188f97f";
52 | type = "git";
53 | emailresponsible = false;
54 | };
55 | };
56 | };
57 | };
58 |
59 | in {
60 | jobsets = pkgs.runCommand "spec.json" {} ''
61 | cat >$out <", "Tobias Pflug "]
5 | edition = "2018"
6 | license = "MIT"
7 |
8 | [dependencies]
9 | clap = "2.33.3"
10 | reqwest = { version = "0.10.8", features = ["blocking", "json", "cookies"] }
11 | serde = { version = "1.0", features = ["derive"] }
12 | serde_json = "1.0"
13 | chrono = "0.4.19"
14 | log = "0.4.11"
15 | prettytable-rs = "0.10.0"
16 | mockito = "0.27.0"
17 |
--------------------------------------------------------------------------------
/hydra-cli/data/eval-1525352.json:
--------------------------------------------------------------------------------
1 | {
2 | "jobsetevalinputs": {
3 | "nix": {
4 | "value": null,
5 | "type": "git",
6 | "revision": "26bc876ae6e7ceff8b37ba5c2c1043ce216cf384",
7 | "dependency": null,
8 | "uri": "https://github.com/NixOS/nix.git"
9 | },
10 | "officialRelease": {
11 | "uri": null,
12 | "dependency": null,
13 | "revision": null,
14 | "value": "false",
15 | "type": "boolean"
16 | },
17 | "nixpkgs": {
18 | "revision": "a7e559a5504572008567383c3dc8e142fa7a8633",
19 | "dependency": null,
20 | "uri": "https://github.com/NixOS/nixpkgs-channels.git",
21 | "type": "git",
22 | "value": null
23 | }
24 | },
25 | "id": 1525352,
26 | "hasnewbuilds": 1,
27 | "builds": [
28 | 94858417,
29 | 94858423,
30 | 94858416,
31 | 94858427,
32 | 94858420,
33 | 94858413,
34 | 94858426,
35 | 94858433,
36 | 94858430,
37 | 94858415,
38 | 94858418,
39 | 94858412,
40 | 94858424,
41 | 94858425,
42 | 94858431,
43 | 94858428,
44 | 94858422,
45 | 94858414,
46 | 94858434,
47 | 94858419,
48 | 94858421,
49 | 94858432,
50 | 94858429
51 | ]
52 | }
53 |
--------------------------------------------------------------------------------
/hydra-cli/data/search-build.json:
--------------------------------------------------------------------------------
1 | {
2 | "builds": [
3 | {
4 | "buildstatus": 2,
5 | "jobset": "trunk",
6 | "jobsetevals": [
7 | 1523141,
8 | 1523060,
9 | 1523028,
10 | 1523021,
11 | 1523006,
12 | 1522960,
13 | 1522914,
14 | 1522875,
15 | 1522829,
16 | 1522819,
17 | 1522786,
18 | 1522735,
19 | 1522658,
20 | 1522611,
21 | 1522605,
22 | 1522575,
23 | 1522529,
24 | 1522491,
25 | 1522453,
26 | 1522436,
27 | 1522405,
28 | 1522394,
29 | 1522367,
30 | 1522333,
31 | 1522292,
32 | 1522272,
33 | 1522256,
34 | 1522230,
35 | 1522201,
36 | 1522177
37 | ],
38 | "system": "x86_64-darwin",
39 | "buildmetrics": {},
40 | "project": "nixpkgs",
41 | "drvpath": "/nix/store/ppihpm4vvkcr49m7nr9q8ikxpfgkx005-afew-1.3.0.drv",
42 | "starttime": 1559613988,
43 | "id": 94250155,
44 | "buildoutputs": {
45 | "out": {
46 | "path": "/nix/store/dx2i55cj2avkl1hhigz08kv5iy2r62k7-afew-1.3.0"
47 | }
48 | },
49 | "finished": 1,
50 | "stoptime": 1559614425,
51 | "releasename": null,
52 | "nixname": "afew-1.3.0",
53 | "buildproducts": {},
54 | "timestamp": 1559122416,
55 | "job": "afew.x86_64-darwin",
56 | "priority": 100
57 | },
58 | {
59 | "priority": 100,
60 | "job": "afew.x86_64-darwin",
61 | "timestamp": 1558862192,
62 | "buildproducts": {},
63 | "nixname": "afew-1.3.0",
64 | "releasename": null,
65 | "finished": 1,
66 | "stoptime": 1559053538,
67 | "buildoutputs": {
68 | "out": {
69 | "path": "/nix/store/dx2i55cj2avkl1hhigz08kv5iy2r62k7-afew-1.3.0"
70 | }
71 | },
72 | "id": 94098485,
73 | "starttime": 1559053054,
74 | "drvpath": "/nix/store/ppihpm4vvkcr49m7nr9q8ikxpfgkx005-afew-1.3.0.drv",
75 | "project": "nixpkgs",
76 | "buildmetrics": {},
77 | "system": "x86_64-darwin",
78 | "jobsetevals": [
79 | 1521962,
80 | 1521636
81 | ],
82 | "buildstatus": 2,
83 | "jobset": "staging-next"
84 | }
85 | ],
86 | "buildsdrv": [],
87 | "jobsets": [],
88 | "projects": []
89 | }
90 |
--------------------------------------------------------------------------------
/hydra-cli/src/hydra/client.rs:
--------------------------------------------------------------------------------
1 | //! Hydra CI REST API Library
2 | //!
3 | //! The HydraClient trait includes operations for querying and creating resources on
4 | //! a Hydra endpoint.
5 |
6 | pub use crate::hydra::types::{
7 | Build, Eval, Jobset, JobsetConfig, JobsetOverview, Project, ProjectConfig, Reproduce, Search,
8 | };
9 | use crate::ops::OpError;
10 | use serde::{Deserialize, Serialize};
11 |
12 | /// Stores combination of user and password as required by login
13 | #[derive(Serialize, Deserialize)]
14 | pub struct Creds {
15 | pub username: String,
16 | pub password: String,
17 | }
18 |
19 | /// Errors occuring while talking to the hydra endpoint
20 | #[derive(PartialEq, Debug)]
21 | pub enum ClientError {
22 | /// Generic error
23 | Error(String),
24 | /// Received data that could not be parsed
25 | InvalidResponse(String),
26 | }
27 |
28 | impl From for OpError {
29 | fn from(e: ClientError) -> Self {
30 | match e {
31 | ClientError::Error(s) => OpError::RequestError(s),
32 | ClientError::InvalidResponse(s) => OpError::RequestError(s),
33 | }
34 | }
35 | }
36 |
37 | pub trait HydraClient {
38 | /// Authenticates with the server using username and password provided by `Creds`
39 | fn login(&self, creds: Creds) -> Result<(), ClientError>;
40 |
41 | /// Returns the Hydra host
42 | fn host(&self) -> String;
43 |
44 | /// Searches the host for the nix store path `query`
45 | fn search(&self, query: &str) -> Result;
46 |
47 | /// Retrieves evaluation information for the build specified by `number`
48 | fn eval(&self, number: i64) -> Result;
49 |
50 | /// Retrieves the jobset specified by `project` / `jobset`
51 | fn jobset(&self, project: &str, jobset: &str) -> Result;
52 |
53 | /// Creates a jobset called `jobset_name` in the project `project_name` using
54 | /// the configuration from `jobset_config`
55 | fn jobset_create(
56 | &self,
57 | project_name: &str,
58 | jobset_name: &str,
59 | jobset_config: &JobsetConfig,
60 | ) -> Result<(), ClientError>;
61 |
62 | /// Delete a jobset
63 | fn jobset_delete(&self, project_name: &str, jobset_name: &str) -> Result<(), ClientError>;
64 |
65 | /// Evaluate a jobset called `jobset_name` in the project `project_name`
66 | fn jobset_eval(&self, project_name: &str, jobset_name: &str) -> Result<(), ClientError>;
67 |
68 | /// Retrieves information on all jobsets belonging to `project`
69 | fn jobset_overview(&self, project: &str) -> Result, ClientError>;
70 |
71 | /// Retrieves all configured projects
72 | fn projects(&self) -> Result, ClientError>;
73 |
74 | /// Retrieves a project given by `name`
75 | fn project_create(&self, name: &str) -> Result<(), ClientError>;
76 | }
77 |
--------------------------------------------------------------------------------
/hydra-cli/src/hydra/example.rs:
--------------------------------------------------------------------------------
1 | //! Examples intended for the CLI help sections
2 |
3 | pub use crate::hydra::types::{Input, JobsetConfig, JobsetEnabled};
4 | use std::collections::HashMap;
5 |
6 | pub fn jobset_config() -> JobsetConfig {
7 | JobsetConfig {
8 | description: "hydra-cli master jobset".to_string(),
9 | checkinterval: 60,
10 | enabled: JobsetEnabled::Enabled,
11 | visible: true,
12 | keepnr: 3,
13 | nixexprinput: "src".to_string(),
14 | nixexprpath: "default.nix".to_string(),
15 | inputs: {
16 | let mut map = HashMap::::new();
17 | let input = Input {
18 | value: Some("https://github.com/nlewo/hydra-cli.git master".to_string()),
19 | input_type: "git".to_string(),
20 | revision: None,
21 | uri: None,
22 | };
23 | map.insert("src".to_string(), input);
24 | map
25 | },
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/hydra-cli/src/hydra/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod client;
2 | pub mod example;
3 | pub mod reqwest_client;
4 | mod types;
5 |
--------------------------------------------------------------------------------
/hydra-cli/src/hydra/reqwest_client.rs:
--------------------------------------------------------------------------------
1 | use crate::hydra::client::*;
2 |
3 | use reqwest::blocking::Client as ReqwestClient;
4 | use reqwest::header::{CONTENT_TYPE, REFERER};
5 | use serde::de::DeserializeOwned;
6 | use serde_json::Value;
7 | #[cfg(test)]
8 | use std::collections::HashMap;
9 |
10 | #[cfg(test)]
11 | use mockito;
12 |
13 | #[cfg(test)]
14 | use crate::hydra::types::JobsetEnabled;
15 |
16 | impl From for ClientError {
17 | fn from(e: reqwest::Error) -> Self {
18 | let msg = format!("{}", e);
19 | ClientError::Error(msg)
20 | }
21 | }
22 |
23 | #[derive(Clone)]
24 | pub struct Client {
25 | pub host: String,
26 | pub client: ReqwestClient,
27 | }
28 |
29 | impl Client {
30 | pub fn new(client: ReqwestClient, host: String) -> Client {
31 | Client { client, host }
32 | }
33 | }
34 |
35 | /// Performs a GET request retrieving a deserializable response
36 | fn get_json(client: &ReqwestClient, url: &str) -> Result {
37 | let res = client
38 | .get(url)
39 | .header(reqwest::header::CONTENT_TYPE, "application/json")
40 | .send()?;
41 |
42 | if res.status().is_success() {
43 | let v: Value = res.json()?;
44 | match serde_json::from_value(v) {
45 | Ok(x) => Ok(x),
46 | Err(x) => Err(ClientError::InvalidResponse(format!("{}", x))),
47 | }
48 | } else {
49 | Err(ClientError::Error(format!("{}", res.status())))
50 | }
51 | }
52 |
53 | impl HydraClient for Client {
54 | fn project_create(&self, name: &str) -> Result<(), ClientError> {
55 | let create_proj_url = format!("{}/project/{}", &self.host, name);
56 | let proj: ProjectConfig = ProjectConfig {
57 | displayname: String::from(name),
58 | enabled: true,
59 | visible: true,
60 | };
61 | let res = self
62 | .client
63 | .put(&create_proj_url)
64 | .header(REFERER, self.host.as_str())
65 | .json(&proj)
66 | .send()?;
67 |
68 | if res.status().is_success() {
69 | Ok(())
70 | } else {
71 | Err(ClientError::Error(format!("{}", res.status())))
72 | }
73 | }
74 |
75 | fn host(&self) -> String {
76 | self.host.clone()
77 | }
78 | fn projects(&self) -> Result, ClientError> {
79 | get_json(&self.client, &self.host)
80 | }
81 |
82 | fn search(&self, query: &str) -> Result {
83 | let request_url = format!("{}/search?query={}", &self.host, query);
84 | get_json(&self.client, &request_url)
85 | }
86 |
87 | fn jobset_overview(&self, project: &str) -> Result, ClientError> {
88 | let request_url = format!("{}/api/jobsets?project={}", &self.host, project);
89 | get_json(&self.client, &request_url)
90 | }
91 |
92 | fn jobset(&self, project: &str, jobset: &str) -> Result {
93 | let request_url = format!("{}/jobset/{}/{}", &self.host, project, jobset);
94 | get_json(&self.client, &request_url)
95 | }
96 |
97 | fn eval(&self, number: i64) -> Result {
98 | let request_url = format!("{}/eval/{}", &self.host, number);
99 | get_json(&self.client, &request_url)
100 | }
101 |
102 | fn jobset_create(
103 | &self,
104 | project_name: &str,
105 | jobset_name: &str,
106 | jobset_config: &JobsetConfig,
107 | ) -> Result<(), ClientError> {
108 | let request_url = format!("{}/jobset/{}/{}", &self.host, project_name, jobset_name);
109 | let res = self
110 | .client
111 | .put(&request_url)
112 | .header(REFERER, self.host.as_str())
113 | .json(&jobset_config)
114 | .send()?;
115 |
116 | if res.status().is_success() {
117 | Ok(())
118 | } else {
119 | Err(ClientError::Error(format!("{}", res.status())))
120 | }
121 | }
122 |
123 | fn jobset_delete(&self, project_name: &str, jobset_name: &str) -> Result<(), ClientError> {
124 | let request_url = format!("{}/jobset/{}/{}", &self.host, project_name, jobset_name);
125 | let res = self
126 | .client
127 | .delete(&request_url)
128 | .header(REFERER, self.host.as_str())
129 | .header(CONTENT_TYPE, "application/json")
130 | .send()?;
131 |
132 | if res.status().is_success() {
133 | Ok(())
134 | } else {
135 | Err(ClientError::Error(format!("{}", res.status())))
136 | }
137 | }
138 |
139 | fn jobset_eval(&self, project_name: &str, jobset_name: &str) -> Result<(), ClientError> {
140 | let request_url = format!(
141 | "{}/api/push?jobsets={}:{}",
142 | &self.host, project_name, jobset_name
143 | );
144 | let res = self
145 | .client
146 | .put(&request_url)
147 | .header(REFERER, self.host.as_str())
148 | .send()?;
149 |
150 | if res.status().is_success() {
151 | Ok(())
152 | } else {
153 | Err(ClientError::Error(format!("{}", res.status())))
154 | }
155 | }
156 |
157 | fn login(&self, creds: Creds) -> Result<(), ClientError> {
158 | let login_request_url = format!("{}/login", &self.host);
159 | let login_res = self
160 | .client
161 | .post(&login_request_url)
162 | .header(REFERER, self.host.as_str())
163 | .json(&creds)
164 | .send();
165 |
166 | match login_res {
167 | Ok(r) => {
168 | if r.status().is_success() {
169 | Ok(())
170 | } else if r.status().is_redirection() {
171 | Ok(())
172 | } else {
173 | Err(ClientError::Error(format!("Response Error: {}", r.status())))
174 | }
175 | }
176 | Err(err) => Err(ClientError::Error(format!("Request Error: {}", err))),
177 | }
178 | }
179 | }
180 |
181 | #[cfg(test)]
182 | mod tests {
183 | use super::*;
184 | use mockito::{mock, Matcher};
185 |
186 | fn client() -> Client {
187 | let url = &mockito::server_url();
188 | let c = reqwest::blocking::Client::builder()
189 | .cookie_store(true)
190 | .build()
191 | .unwrap();
192 |
193 | Client::new(c, String::from(url))
194 | }
195 |
196 | #[test]
197 | fn get_json_yields_err_on_non_200_response() {
198 | let _m = mock("GET", "/")
199 | .with_status(500)
200 | .with_header("content-type", "application/json")
201 | .with_body("[]")
202 | .create();
203 |
204 | let c = client();
205 | let res: Result = get_json(&c.client, &c.host);
206 | assert_eq!(
207 | res,
208 | Err(ClientError::Error("500 Internal Server Error".to_string()))
209 | );
210 | }
211 |
212 | #[test]
213 | fn get_json_yields_invalid_response_on_invalid_json() {
214 | let _m = mock("GET", "/")
215 | .with_status(200)
216 | .with_header("content-type", "application/json")
217 | .with_body("a{x")
218 | .create();
219 |
220 | let c = client();
221 | let res: Result, ClientError> = get_json(&c.client, &c.host);
222 |
223 | assert_eq!(
224 | res,
225 | Err(ClientError::Error(
226 | "error decoding response body: expected value at line 1 column 1".to_string()
227 | ))
228 | )
229 | }
230 |
231 | #[test]
232 | fn projects_lists_single_project() {
233 | let _m = mock("GET", "/")
234 | .with_status(200)
235 | .with_header("content-type", "application/json")
236 | .with_body("[{ \"owner\": \"admin\", \"displayname\": \"hydra-cli\", \"hidden\": 0, \"description\": \"Hydra Command Line Interface\", \"jobsets\": [ \"20\", \"21\"], \"releases\": [], \"enabled\": 1, \"name\": \"hydra-cli\" } ]")
237 | .create();
238 |
239 | let ps = client().projects();
240 | assert_eq!(
241 | ps.unwrap(),
242 | vec![Project {
243 | enabled: true,
244 | name: "hydra-cli".to_string(),
245 | displayname: "hydra-cli".to_string(),
246 | hidden: false,
247 | owner: "admin".to_string(),
248 | description: Some("Hydra Command Line Interface".to_string()),
249 | jobsets: vec!["20".to_string(), "21".to_string()]
250 | }]
251 | );
252 | }
253 |
254 | #[test]
255 | fn login_posts_creds_to_login_path() {
256 | let _m = mock("POST", "/login")
257 | .with_status(200)
258 | .match_body(Matcher::JsonString(
259 | "{\"username\": \"user\", \"password\": \"pw\"}".to_string(),
260 | ))
261 | .create();
262 |
263 | let res = client().login(Creds {
264 | username: "user".to_string(),
265 | password: "pw".to_string(),
266 | });
267 | assert_eq!(res.unwrap(), ())
268 | }
269 |
270 | #[test]
271 | fn login_yields_err_when_response_status_is_not_200() {
272 | let _m = mock("POST", "/login")
273 | .with_status(500)
274 | .match_body(Matcher::JsonString(
275 | "{\"username\": \"user\", \"password\": \"pw\"}".to_string(),
276 | ))
277 | .create();
278 |
279 | let res = client().login(Creds {
280 | username: "user".to_string(),
281 | password: "pw".to_string(),
282 | });
283 |
284 | assert_eq!(
285 | res,
286 | Err(ClientError::Error("Response Error: 500 Internal Server Error".to_string()))
287 | );
288 | }
289 |
290 | #[test]
291 | fn jobset_create_posts_config_to_correct_path() {
292 | let jobset = JobsetConfig {
293 | description: "desc".to_string(),
294 | checkinterval: 100,
295 | enabled: JobsetEnabled::Enabled,
296 | visible: true,
297 | nixexprinput: "input".to_string(),
298 | nixexprpath: "path".to_string(),
299 | keepnr: 10,
300 | inputs: HashMap::new(),
301 | };
302 | let _m = mock("PUT", "/jobset/foo-project/foo-jobset")
303 | .with_status(200)
304 | .match_body(Matcher::JsonString(serde_json::to_string(&jobset).unwrap()))
305 | .create();
306 |
307 | let res = client().jobset_create("foo-project", "foo-jobset", &jobset);
308 | assert_eq!(res.unwrap(), ())
309 | }
310 |
311 | #[test]
312 | fn jobset_create_yields_err_when_response_status_is_not_200() {
313 | let jobset = JobsetConfig {
314 | description: "desc".to_string(),
315 | checkinterval: 100,
316 | enabled: JobsetEnabled::Enabled,
317 | visible: true,
318 | nixexprinput: "input".to_string(),
319 | nixexprpath: "path".to_string(),
320 | keepnr: 10,
321 | inputs: HashMap::new(),
322 | };
323 | let _m = mock("PUT", "/jobset/foo-project/foo-jobset")
324 | .with_status(500)
325 | .match_body(Matcher::JsonString(serde_json::to_string(&jobset).unwrap()))
326 | .create();
327 |
328 | let res = client().jobset_create("foo-project", "foo-jobset", &jobset);
329 | assert_eq!(
330 | res,
331 | Err(ClientError::Error("500 Internal Server Error".to_string()))
332 | );
333 | }
334 | }
335 |
--------------------------------------------------------------------------------
/hydra-cli/src/hydra/types.rs:
--------------------------------------------------------------------------------
1 | use serde::de::{self, Deserializer, Unexpected};
2 | use serde::{Deserialize, Serialize, Serializer};
3 | pub use serde_json::Value;
4 | use std::collections::HashMap;
5 |
6 | #[derive(Serialize, Deserialize, Debug)]
7 | pub struct Input {
8 | pub value: Option,
9 | #[serde(rename = "type")]
10 | pub input_type: String,
11 | pub revision: Option,
12 | pub uri: Option,
13 | }
14 |
15 | #[derive(Serialize, Deserialize, Debug)]
16 | pub struct Eval {
17 | pub jobsetevalinputs: HashMap,
18 | }
19 |
20 | #[derive(Serialize, Deserialize, PartialEq, Debug)]
21 | pub struct Project {
22 | #[serde(deserialize_with = "bool_from_int")]
23 | pub enabled: bool,
24 | pub name: String,
25 | pub description: Option,
26 | #[serde(deserialize_with = "bool_from_int")]
27 | pub hidden: bool,
28 | pub owner: String,
29 | pub displayname: String,
30 | pub jobsets: Vec,
31 | }
32 |
33 | struct BoolFromInt {}
34 |
35 | impl de::Visitor<'_> for BoolFromInt {
36 | type Value = bool;
37 |
38 | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
39 | formatter.write_str("Expecting a boolean either encoded as an unsigned int or as a bool")
40 | }
41 |
42 | fn visit_u64(self, v: u64) -> Result
43 | where
44 | E: de::Error,
45 | {
46 | match v {
47 | 0 => Ok(false),
48 | 1 => Ok(true),
49 | other => Err(de::Error::invalid_value(
50 | Unexpected::Unsigned(u64::from(other)),
51 | &"zero or one",
52 | )),
53 | }
54 | }
55 |
56 | fn visit_bool(self, v: bool) -> Result
57 | where
58 | E: de::Error,
59 | {
60 | Ok(v)
61 | }
62 | }
63 |
64 | fn bool_from_int<'de, D>(deserializer: D) -> Result
65 | where
66 | D: Deserializer<'de>,
67 | {
68 | deserializer.deserialize_any(BoolFromInt {})
69 | }
70 |
71 | #[derive(Serialize, Deserialize, Debug)]
72 | pub struct Jobset {
73 | pub nixexprpath: String,
74 | pub nixexprinput: String,
75 | }
76 |
77 | #[derive(Serialize, Deserialize, Debug, Clone)]
78 | pub struct Path {
79 | pub path: String,
80 | }
81 |
82 | #[derive(Serialize, Deserialize, Debug, Clone)]
83 | pub struct Build {
84 | pub id: i64,
85 | pub project: String,
86 | pub drvpath: String,
87 | pub job: String,
88 | pub jobset: String,
89 | pub buildoutputs: HashMap,
90 | pub stoptime: i64,
91 | pub jobsetevals: Vec,
92 | }
93 |
94 | #[derive(Serialize, Deserialize, Debug)]
95 | pub struct Search {
96 | pub builds: Vec,
97 | }
98 |
99 | #[derive(Serialize, Deserialize, Debug)]
100 | pub struct Reproduce {
101 | pub build: Build,
102 | pub eval: Eval,
103 | pub jobset: Jobset,
104 | }
105 |
106 | pub type PosixTimestamp = u64;
107 |
108 | #[derive(Serialize, Deserialize, Debug)]
109 | pub struct JobsetOverview {
110 | pub nrscheduled: i64,
111 | pub nrtotal: i64,
112 | pub nrsucceeded: i64,
113 | pub project: String,
114 | pub name: String,
115 | pub nrfailed: i64,
116 | pub starttime: Option,
117 | pub lastcheckedtime: Option,
118 | pub haserrormsg: Option,
119 | }
120 |
121 | #[derive(PartialEq, Debug)]
122 | #[repr(u8)]
123 | pub enum JobsetEnabled {
124 | Disabled = 0,
125 | Enabled = 1,
126 | OneShot = 2,
127 | }
128 |
129 | impl Serialize for JobsetEnabled {
130 | fn serialize(&self, serializer: S) -> Result
131 | where
132 | S: Serializer,
133 | {
134 | serializer.serialize_u8(match self {
135 | JobsetEnabled::Disabled => 0,
136 | JobsetEnabled::Enabled => 1,
137 | JobsetEnabled::OneShot => 2,
138 | })
139 | }
140 | }
141 |
142 | impl<'de> Deserialize<'de> for JobsetEnabled {
143 | fn deserialize(deserializer: D) -> Result
144 | where
145 | D: Deserializer<'de>,
146 | {
147 | match u8::deserialize(deserializer)? {
148 | 0 => Ok(JobsetEnabled::Disabled),
149 | 1 => Ok(JobsetEnabled::Enabled),
150 | 2 => Ok(JobsetEnabled::OneShot),
151 | other => Err(de::Error::invalid_value(
152 | Unexpected::Unsigned(u64::from(other)),
153 | &"zero, one, or two",
154 | )),
155 | }
156 | }
157 | }
158 |
159 | #[derive(Serialize, Deserialize, Debug)]
160 | pub struct JobsetConfig {
161 | pub description: String,
162 | pub checkinterval: i64,
163 | pub enabled: JobsetEnabled,
164 | #[serde(skip_serializing_if = "is_not_visible")]
165 | pub visible: bool,
166 | pub keepnr: i64,
167 | pub nixexprinput: String,
168 | pub nixexprpath: String,
169 | pub inputs: HashMap,
170 | }
171 |
172 | fn is_not_visible(visible: &bool) -> bool {
173 | !visible
174 | }
175 |
176 | #[derive(Serialize, Deserialize, Debug)]
177 | pub struct ProjectConfig {
178 | pub displayname: String,
179 | pub enabled: bool,
180 | pub visible: bool,
181 | }
182 |
--------------------------------------------------------------------------------
/hydra-cli/src/lib.rs:
--------------------------------------------------------------------------------
1 | extern crate chrono;
2 | extern crate clap;
3 | extern crate reqwest;
4 |
5 | #[macro_use]
6 | extern crate prettytable;
7 |
8 | pub mod hydra;
9 | pub mod ops;
10 | pub mod pretty;
11 |
--------------------------------------------------------------------------------
/hydra-cli/src/main.rs:
--------------------------------------------------------------------------------
1 | extern crate hydra_cli;
2 |
3 | use clap::{App, Arg, SubCommand};
4 | use reqwest::redirect;
5 | use std::str::FromStr;
6 | use std::time::Duration;
7 |
8 | use hydra_cli::hydra::example::jobset_config;
9 | use hydra_cli::hydra::reqwest_client::Client as ReqwestHydraClient;
10 | use hydra_cli::ops::{
11 | jobset_create, jobset_delete, jobset_eval, jobset_wait, project, project_create, project_list,
12 | reproduce, search, OpError, OpResult,
13 | };
14 |
15 | fn main() {
16 | let config_after_help = &format!(
17 | "Here is an example JSON config:\n\n{}",
18 | serde_json::to_string_pretty(&jobset_config()).unwrap()
19 | )[..];
20 | let app = App::new("hydra-cli")
21 | .version("0.3.0")
22 | .about("CLI Hydra client")
23 | .author("lewo")
24 | .after_help("A client to query Hydra through its JSON API.")
25 | .arg(
26 | Arg::with_name("host")
27 | .short("H")
28 | .long("host")
29 | .default_value("https://hydra.nixos.org")
30 | .env("HYDRA_HOST")
31 | .help("Hydra host URL"),
32 | )
33 | .arg(
34 | Arg::with_name("no-check-certificate")
35 | .long("no-check-certificate")
36 | .help("Disable TLS certificate check for the Hydra host"),
37 | )
38 | .subcommand(
39 | SubCommand::with_name("search")
40 | .about("Search by output paths")
41 | .arg(
42 | Arg::with_name("query")
43 | .required(true)
44 | .help("Piece of an output path (hash, name,...)"),
45 | )
46 | .arg(
47 | Arg::with_name("limit")
48 | .default_value("10")
49 | .help("How many results to return"),
50 | ),
51 | )
52 | .subcommand(
53 | SubCommand::with_name("reproduce")
54 | .about("Retrieve information to reproduce an output path")
55 | .arg(
56 | Arg::with_name("query")
57 | .required(true)
58 | .help("Piece of an output path (hash, name,...)"),
59 | )
60 | .arg(Arg::with_name("json").short("j").help("JSON output")),
61 | )
62 | .subcommand(
63 | SubCommand::with_name("project-list")
64 | .about("List projects")
65 | .arg(Arg::with_name("json").short("j").help("JSON output")),
66 | )
67 | .subcommand(
68 | SubCommand::with_name("project-show")
69 | .about("Get information of a project")
70 | .arg(
71 | Arg::with_name("project")
72 | .required(true)
73 | .help("A project name"),
74 | )
75 | .arg(Arg::with_name("json").short("j").help("JSON output")),
76 | )
77 | .subcommand(
78 | SubCommand::with_name("project-create")
79 | .about("Create a new project")
80 | .arg(
81 | Arg::with_name("project")
82 | .required(true)
83 | .help("The name of the project in which to create the jobset"),
84 | )
85 | .arg(
86 | Arg::with_name("user")
87 | .takes_value(true)
88 | .required(true)
89 | .long("user")
90 | .env("HYDRA_USER")
91 | .help("A user name"),
92 | )
93 | .arg(
94 | Arg::with_name("password")
95 | .takes_value(true)
96 | .required(true)
97 | .long("password")
98 | .env("HYDRA_PASSWORD")
99 | .help("A user password"),
100 | ),
101 | )
102 | .subcommand(
103 | SubCommand::with_name("jobset-create")
104 | .about("Add a jobset to a project")
105 | .arg(
106 | Arg::with_name("project")
107 | .required(true)
108 | .help("The project to add the jobset to"),
109 | )
110 | .arg(
111 | Arg::with_name("jobset")
112 | .required(true)
113 | .help("The name of the jobset to create"),
114 | )
115 | .arg(
116 | Arg::with_name("config")
117 | .required(true)
118 | .help("Project configuration JSON filepath"),
119 | )
120 | .arg(
121 | Arg::with_name("user")
122 | .takes_value(true)
123 | .required(true)
124 | .long("user")
125 | .env("HYDRA_USER")
126 | .help("A user name"),
127 | )
128 | .arg(
129 | Arg::with_name("password")
130 | .takes_value(true)
131 | .required(true)
132 | .long("password")
133 | .env("HYDRA_PASSWORD")
134 | .help("A user password"),
135 | )
136 | .after_help(config_after_help),
137 | )
138 | .subcommand(
139 | SubCommand::with_name("jobset-delete")
140 | .about("Delete a jobset")
141 | .arg(
142 | Arg::with_name("project")
143 | .required(true)
144 | .help("The project to add the jobset to"),
145 | )
146 | .arg(
147 | Arg::with_name("jobset")
148 | .required(true)
149 | .help("The name of the jobset to create"),
150 | )
151 | .arg(
152 | Arg::with_name("user")
153 | .takes_value(true)
154 | .required(true)
155 | .long("user")
156 | .env("HYDRA_USER")
157 | .help("A user name"),
158 | )
159 | .arg(
160 | Arg::with_name("password")
161 | .takes_value(true)
162 | .required(true)
163 | .long("password")
164 | .env("HYDRA_PASSWORD")
165 | .help("A user password"),
166 | )
167 | .after_help(config_after_help),
168 | )
169 | .subcommand(
170 | SubCommand::with_name("jobset-eval")
171 | .about("Evaluate a jobset")
172 | .arg(
173 | Arg::with_name("project")
174 | .required(true)
175 | .help("The project to evaluate the jobset from"),
176 | )
177 | .arg(
178 | Arg::with_name("jobset")
179 | .required(true)
180 | .help("The jobset to evaluate"),
181 | ),
182 | )
183 | .subcommand(
184 | SubCommand::with_name("jobset-wait")
185 | .about("Wait for jobset completion")
186 | .arg(
187 | Arg::with_name("project")
188 | .required(true)
189 | .help("The project of the jobset to wait for"),
190 | )
191 | .arg(
192 | Arg::with_name("jobset")
193 | .required(true)
194 | .help("The name of the jobset to wait for"),
195 | )
196 | .arg(
197 | Arg::with_name("timeout")
198 | .long("timeout")
199 | .takes_value(true)
200 | .help("Maximum time to wait for (in seconds and infinite by default)"),
201 | ),
202 | );
203 |
204 | let mut help_buffer = Vec::new();
205 | app.write_help(&mut help_buffer).unwrap();
206 | let help_string = String::from_utf8(help_buffer).unwrap();
207 |
208 | let matches = app.get_matches();
209 | let host = matches.value_of("host").unwrap();
210 | let no_check_certs = matches.is_present("no-check-certificate");
211 |
212 | let custom = redirect::Policy::none();
213 |
214 | let c = reqwest::blocking::Client::builder()
215 | .cookie_store(true)
216 | .danger_accept_invalid_certs(no_check_certs)
217 | .redirect(custom)
218 | .build()
219 | .unwrap();
220 | let client = ReqwestHydraClient::new(c, String::from(host));
221 |
222 | let cmd_res: OpResult = match matches.subcommand() {
223 | ("search", Some(args)) => search::run(
224 | &client,
225 | args.value_of("query").unwrap(),
226 | args.value_of("limit").unwrap().parse().unwrap(),
227 | ),
228 |
229 | ("reproduce", Some(args)) => reproduce::run(
230 | &client,
231 | host,
232 | args.value_of("query").unwrap(),
233 | args.is_present("json"),
234 | ),
235 |
236 | ("project-list", Some(args)) => project_list::run(&client, args.is_present("json")),
237 |
238 | ("project-show", Some(args)) => project::run(
239 | &client,
240 | args.value_of("project").unwrap(),
241 | args.is_present("json"),
242 | ),
243 |
244 | ("project-create", Some(args)) => project_create::run(
245 | &client,
246 | args.value_of("project").unwrap(),
247 | args.value_of("user").unwrap(),
248 | args.value_of("password").unwrap(),
249 | ),
250 |
251 | ("jobset-create", Some(args)) => jobset_create::run(
252 | &client,
253 | args.value_of("config").unwrap(),
254 | args.value_of("project").unwrap(),
255 | args.value_of("jobset").unwrap(),
256 | args.value_of("user").unwrap(),
257 | args.value_of("password").unwrap(),
258 | ),
259 |
260 | ("jobset-delete", Some(args)) => jobset_delete::run(
261 | &client,
262 | args.value_of("project").unwrap(),
263 | args.value_of("jobset").unwrap(),
264 | args.value_of("user").unwrap(),
265 | args.value_of("password").unwrap(),
266 | ),
267 |
268 | ("jobset-eval", Some(args)) => jobset_eval::run(
269 | &client,
270 | args.value_of("project").unwrap(),
271 | args.value_of("jobset").unwrap(),
272 | ),
273 |
274 | ("jobset-wait", Some(args)) => jobset_wait::run(
275 | &client,
276 | args.value_of("project").unwrap(),
277 | args.value_of("jobset").unwrap(),
278 | args.value_of("timeout")
279 | .map(|t| Duration::from_secs(u64::from_str(t).unwrap())),
280 | ),
281 |
282 | _ => {
283 | println!("{}", help_string);
284 | Err(OpError::CmdErr)
285 | }
286 | };
287 |
288 | match cmd_res {
289 | Ok(_) => std::process::exit(0),
290 | Err(OpError::AuthError) => {
291 | eprintln!("ERROR: Failed to login. Please check your credentials");
292 | std::process::exit(1)
293 | }
294 | Err(OpError::CmdErr) => {
295 | eprintln!("ERROR: hydra-cli called with invalid arguments");
296 | std::process::exit(1)
297 | }
298 | Err(OpError::RequestError(m)) => {
299 | eprintln!("ERROR: {}", m);
300 | std::process::exit(1)
301 | }
302 | Err(OpError::TimeoutError) => {
303 | eprintln!("ERROR: Timeout");
304 | std::process::exit(124)
305 | }
306 | Err(OpError::Error(m)) => {
307 | eprintln!("ERROR: {}", m);
308 | std::process::exit(1)
309 | }
310 | }
311 | }
312 |
--------------------------------------------------------------------------------
/hydra-cli/src/ops/jobset_create.rs:
--------------------------------------------------------------------------------
1 | use crate::hydra::client::{Creds, HydraClient, JobsetConfig};
2 | use crate::ops::{ok_msg, OpResult};
3 | use std::fs::read_to_string;
4 |
5 | fn load_config(config_path: &str) -> JobsetConfig {
6 | let cfg = read_to_string(config_path).expect("Failed to read config file");
7 | serde_json::from_str(&cfg).expect("Failed to parse jobset configuration")
8 | }
9 |
10 | pub fn run(
11 | client: &dyn HydraClient,
12 | config_path: &str,
13 | project_name: &str,
14 | jobset_name: &str,
15 | user: &str,
16 | password: &str,
17 | ) -> OpResult {
18 | let jobset_cfg = load_config(config_path);
19 | let creds = Creds {
20 | username: String::from(user),
21 | password: String::from(password),
22 | };
23 |
24 | client.login(creds)?;
25 | client.jobset_create(project_name, jobset_name, &jobset_cfg)?;
26 |
27 | ok_msg("jobset__create")
28 | }
29 |
--------------------------------------------------------------------------------
/hydra-cli/src/ops/jobset_delete.rs:
--------------------------------------------------------------------------------
1 | use crate::hydra::client::{Creds, HydraClient};
2 | use crate::ops::{ok_msg, OpResult};
3 |
4 | pub fn run(
5 | client: &dyn HydraClient,
6 | project_name: &str,
7 | jobset_name: &str,
8 | user: &str,
9 | password: &str,
10 | ) -> OpResult {
11 | let creds = Creds {
12 | username: String::from(user),
13 | password: String::from(password),
14 | };
15 |
16 | client.login(creds)?;
17 | client.jobset_delete(project_name, jobset_name)?;
18 |
19 | ok_msg("jobset_delete")
20 | }
21 |
--------------------------------------------------------------------------------
/hydra-cli/src/ops/jobset_eval.rs:
--------------------------------------------------------------------------------
1 | use crate::hydra::client::HydraClient;
2 | use crate::ops::{ok_msg, OpResult};
3 |
4 | pub fn run(client: &dyn HydraClient, project_name: &str, jobset_name: &str) -> OpResult {
5 | client.jobset_eval(project_name, jobset_name)?;
6 |
7 | ok_msg("jobset_eval")
8 | }
9 |
--------------------------------------------------------------------------------
/hydra-cli/src/ops/jobset_wait.rs:
--------------------------------------------------------------------------------
1 | use crate::hydra::client::HydraClient;
2 | use crate::hydra::client::JobsetOverview;
3 | use crate::ops::{ok, OpError, OpResult};
4 | use std::io;
5 | use std::io::Write;
6 | use std::option::Option;
7 | use std::thread;
8 | use std::time::SystemTime;
9 | use std::time::{Duration, UNIX_EPOCH};
10 |
11 | fn evaluation_started_since(jobset: &JobsetOverview) -> Option {
12 | let starttime = jobset.starttime?;
13 | SystemTime::now()
14 | .duration_since(UNIX_EPOCH + Duration::from_secs(starttime))
15 | .ok()
16 | }
17 |
18 | fn is_evaluation_finished_after(jobset: &JobsetOverview, start: SystemTime) -> bool {
19 | match jobset.lastcheckedtime {
20 | None => false,
21 | Some(t) => (UNIX_EPOCH + Duration::from_secs(t)) > start,
22 | }
23 | }
24 |
25 | fn is_jobset_built(client: &dyn HydraClient, jobset: &JobsetOverview) -> Result {
26 | if jobset.haserrormsg.unwrap_or(false) {
27 | println!();
28 | Err(OpError::Error(format!(
29 | "evaluation of jobset {} failed; for details: {}/jobset/{}/{}",
30 | jobset.name,
31 | client.host(),
32 | jobset.project,
33 | jobset.name
34 | )))
35 | } else if jobset.nrfailed != 0 {
36 | println!();
37 | Err(OpError::Error(format!(
38 | "Jobset {} has {} failed jobs; for details: {}/jobset/{}/{}",
39 | jobset.name,
40 | jobset.nrfailed.to_string(),
41 | client.host(),
42 | jobset.project,
43 | jobset.name
44 | )))
45 | } else if jobset.nrsucceeded == jobset.nrtotal {
46 | println!(
47 | "\nall jobs of jobset {} have been built",
48 | jobset.name.to_string()
49 | );
50 | Ok(true)
51 | } else {
52 | Ok(false)
53 | }
54 | }
55 |
56 | fn jobset_find(
57 | client: &dyn HydraClient,
58 | project_name: &str,
59 | jobset_name: &str,
60 | ) -> Result {
61 | let jobsets = client.jobset_overview(project_name)?;
62 | jobsets
63 | .into_iter()
64 | .find(|j| j.name == jobset_name)
65 | .ok_or_else(|| {
66 | OpError::Error(format!(
67 | "Project {} doesn't have a jobset {}",
68 | project_name, jobset_name
69 | ))
70 | })
71 | }
72 |
73 | // To know if a jobset has been successfully built, five steps are required:
74 | // 0. wait for the jobset creation : WaitingForJobset
75 | // 1. ensure a evaluation is not running : WaitingForPreviousEval
76 | // 2. wait for a new evaluation to start : WaitingForNewEval
77 | // 3. wait for this evaluation to be terminated : Evaluation
78 | // 4. wait for all scheduled builds to terminate : Building
79 | //
80 | // If a build or the evaluation fails, it immediately returns an error.
81 | //
82 | // There are several improvements, such as
83 | // - use the checkinterval to know when the next evaluation will start
84 | // - use the push Hydra API to trigger an evaluation (but this needs credentials)
85 | pub fn run(
86 | client: &dyn HydraClient,
87 | project_name: &str,
88 | jobset_name: &str,
89 | timeout: Option,
90 | ) -> OpResult {
91 | #[derive(PartialEq)]
92 | enum State {
93 | WaitingForJobset,
94 | WaitingForPreviousEval,
95 | WaitingForNewEval,
96 | Evaluating,
97 | Building,
98 | }
99 | let sleep = Duration::from_secs(2);
100 | let mut state = State::WaitingForJobset;
101 | let mut start = SystemTime::now();
102 | let timeout_start = start;
103 | let mut nrscheduled = 0;
104 |
105 | println!(
106 | "waiting for jobset {}/{} ({}/jobset/{}/{})",
107 | project_name,
108 | jobset_name,
109 | client.host(),
110 | project_name,
111 | jobset_name
112 | );
113 | loop {
114 | match timeout {
115 | Some(t) if SystemTime::now().duration_since(timeout_start).unwrap() > t => {
116 | return Err(OpError::TimeoutError)
117 | }
118 | _ => {}
119 | }
120 |
121 | match jobset_find(client, project_name, jobset_name) {
122 | Err(_) => {
123 | // The jobset should not disappear if it has already been seen!
124 | if state != State::WaitingForJobset {
125 | return Err(OpError::Error(format!(
126 | "jobset {}/{} failed",
127 | project_name, jobset_name
128 | )));
129 | }
130 | }
131 | Ok(jobset) => match state {
132 | State::WaitingForJobset => {
133 | state = State::WaitingForPreviousEval;
134 | println!("waiting for a potential evaluation to terminate");
135 | }
136 | State::WaitingForPreviousEval => match evaluation_started_since(&jobset) {
137 | Some(_) => {}
138 | None => {
139 | println!("\nwaiting for an new evaluation");
140 | start = SystemTime::now();
141 | state = State::WaitingForNewEval;
142 | }
143 | },
144 | State::WaitingForNewEval => {
145 | if is_evaluation_finished_after(&jobset, start) {
146 | println!("\njobset has been evaluated");
147 | // we skip the evaluation step since the evaluation is already finished
148 | state = State::Building;
149 | } else if let Some(d) = evaluation_started_since(&jobset) {
150 | println!(
151 | "\nevaluation is started since {} seconds",
152 | d.as_secs().to_string()
153 | );
154 | state = State::Evaluating;
155 | }
156 | }
157 | State::Evaluating => {
158 | if is_evaluation_finished_after(&jobset, start) {
159 | println!("\njobset has been evaluated");
160 | state = State::Building;
161 | }
162 | }
163 | State::Building => {
164 | if is_jobset_built(client, &jobset)? {
165 | break;
166 | } else if nrscheduled != jobset.nrscheduled {
167 | nrscheduled = jobset.nrscheduled;
168 | println!(
169 | "\njobset {} has still {} jobs scheduled",
170 | jobset_name.to_string(),
171 | jobset.nrscheduled.to_string()
172 | );
173 | }
174 | }
175 | },
176 | };
177 | print!(".");
178 | io::stdout().flush().unwrap();
179 | thread::sleep(sleep);
180 | }
181 | ok()
182 | }
183 |
--------------------------------------------------------------------------------
/hydra-cli/src/ops/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod jobset_create;
2 | pub mod jobset_delete;
3 | pub mod jobset_eval;
4 | pub mod jobset_wait;
5 | pub mod project;
6 | pub mod project_create;
7 | pub mod project_list;
8 | pub mod reproduce;
9 | pub mod search;
10 |
11 | pub enum OpError {
12 | AuthError,
13 | CmdErr,
14 | TimeoutError,
15 | Error(String),
16 | RequestError(String),
17 | }
18 |
19 | pub type OpResult = Result