Compare commits
5 Commits
fcdf259bb9
...
c94c8b3aed
Author | SHA1 | Date |
---|---|---|
R Tyler Croy | c94c8b3aed | |
R Tyler Croy | 72935aef37 | |
R Tyler Croy | 81451f888c | |
R Tyler Croy | 641c774788 | |
R Tyler Croy | f7a3dc3e8d |
|
@ -1,33 +0,0 @@
|
|||
---
|
||||
openapi: "3.0.0"
|
||||
info:
|
||||
description: |
|
||||
Janky Server API defintion
|
||||
version: "1.0.0"
|
||||
title: Janky APIs
|
||||
contact:
|
||||
email: "rtyler+janky@brokenco.de"
|
||||
license:
|
||||
name: "AGPL v3.0"
|
||||
url: "https://www.gnu.org/licenses/agpl-3.0.en.html"
|
||||
servers:
|
||||
- url: 'http://localhost:8000/api/v1'
|
||||
description: Local dev server (APIv1)
|
||||
|
||||
paths:
|
||||
'/projects/{name}':
|
||||
post:
|
||||
summary: 'Trigger execution for this project'
|
||||
description:
|
||||
parameters:
|
||||
- in: path
|
||||
name: name
|
||||
required: true
|
||||
example: 'janky'
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
404:
|
||||
summary: 'No project configured by that name'
|
||||
200:
|
||||
summary: 'Execution has been triggered'
|
|
@ -2,7 +2,7 @@
|
|||
openapi: "3.0.0"
|
||||
info:
|
||||
description: |
|
||||
Janky Agent API defintion
|
||||
Janky API v1 defintion
|
||||
version: "1.0.0"
|
||||
title: Janky APIs
|
||||
contact:
|
||||
|
@ -11,12 +11,42 @@ info:
|
|||
name: "AGPL v3.0"
|
||||
url: "https://www.gnu.org/licenses/agpl-3.0.en.html"
|
||||
servers:
|
||||
- url: 'http://localhost:9000/api/v1'
|
||||
description: Local dev agent (APIv1)
|
||||
- url: 'http://localhost:8000'
|
||||
description: Local dev server
|
||||
- url: 'http://localhost:9000'
|
||||
description: Local dev agent
|
||||
|
||||
tags:
|
||||
- name: 'agent'
|
||||
description: 'Agent APIs'
|
||||
- name: 'server'
|
||||
description: 'Server APIs'
|
||||
|
||||
paths:
|
||||
/capabilities:
|
||||
'/api/v1/projects/{name}':
|
||||
post:
|
||||
tags:
|
||||
- 'server'
|
||||
summary: 'Trigger execution for this project'
|
||||
description:
|
||||
parameters:
|
||||
- in: path
|
||||
name: name
|
||||
required: true
|
||||
example: 'janky'
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
404:
|
||||
summary: 'No project configured by that name'
|
||||
200:
|
||||
summary: 'Execution has been triggered'
|
||||
|
||||
|
||||
'/api/v1/capabilities':
|
||||
get:
|
||||
tags:
|
||||
- 'agent'
|
||||
summary: "Retrieve a list of capabilities of this agent"
|
||||
description:
|
||||
responses:
|
||||
|
@ -26,8 +56,10 @@ paths:
|
|||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/CapsResponse'
|
||||
/execute:
|
||||
'/api/v1/execute':
|
||||
put:
|
||||
tags:
|
||||
- 'agent'
|
||||
summary: "Execute a series of commands on this agent"
|
||||
description:
|
||||
requestBody:
|
|
@ -1 +1 @@
|
|||
{"openapi":"3.0.0","info":{"description":"Janky Agent API defintion\n","version":"1.0.0","title":"Janky APIs","contact":{"email":"rtyler+janky@brokenco.de"},"license":{"name":"AGPL v3.0","url":"https://www.gnu.org/licenses/agpl-3.0.en.html"}},"servers":[{"url":"http://localhost:9000/api/v1","description":"Local dev agent (APIv1)"}],"paths":{"/capabilities":{"get":{"summary":"Retrieve a list of capabilities of this agent","description":null,"responses":{"200":{"description":"Getting capabilities","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CapsResponse"}}}}}}},"/execute":{"put":{"summary":"Execute a series of commands on this agent","description":null,"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommandRequest"},"example":{"commands":[{"script":"echo \"Hi\""}]}}}},"responses":{"201":{"description":"Successfully accepted the commands for execution","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommandResponse"}}}},"409":{"description":"Returned when the agent is busy with another series of commands"}}}}},"components":{"schemas":{"CapsResponse":{"type":"object","properties":{"caps":{"type":"array","items":{"$ref":"#/components/schemas/Capability"}}}},"Capability":{"type":"object","properties":{"name":{"type":"string"},"path":{"type":"string"},"data":{"type":"object"}}},"Command":{"type":"object","properties":{"script":{"type":"string","description":"A script that can be exec()'d on the agent"}}},"CommandRequest":{"type":"object","properties":{"commands":{"type":"array","items":{"$ref":"#/components/schemas/Command"}}}},"CommandResponse":{"type":"object","properties":{"uuid":{"type":"string","format":"uuid"},"stream":{"description":"URL to streaming WebSockets logs","type":"string","format":"url"},"task":{"description":"URL to the task metadata","type":"string","format":"url"},"log":{"description":"URL to the raw log of the task run","type":"string","format":"url"}}}}}}
|
||||
{"openapi":"3.0.0","info":{"description":"Janky API defintion\n","version":"1.0.0","title":"Janky APIs","contact":{"email":"rtyler+janky@brokenco.de"},"license":{"name":"AGPL v3.0","url":"https://www.gnu.org/licenses/agpl-3.0.en.html"}},"servers":[{"url":"http://localhost:8000","description":"Local dev server"},{"url":"http://localhost:9000","description":"Local dev agent"}],"tags":[{"name":"agent","description":"Agent APIs"},{"name":"server","description":"Server APIs"}],"paths":{"/api/v1/projects/{name}":{"post":{"tags":["server"],"summary":"Trigger execution for this project","description":null,"parameters":[{"in":"path","name":"name","required":true,"example":"janky","schema":{"type":"string"}}],"responses":{"404":{"summary":"No project configured by that name"},"200":{"summary":"Execution has been triggered"}}}},"/api/v1/capabilities":{"get":{"tags":["agent"],"summary":"Retrieve a list of capabilities of this agent","description":null,"responses":{"200":{"description":"Getting capabilities","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CapsResponse"}}}}}}},"/api/v1/execute":{"put":{"tags":["agent"],"summary":"Execute a series of commands on this agent","description":null,"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommandRequest"},"example":{"commands":[{"script":"echo \"Hi\""}]}}}},"responses":{"201":{"description":"Successfully accepted the commands for execution","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommandResponse"}}}},"409":{"description":"Returned when the agent is busy with another series of commands"}}}}},"components":{"schemas":{"CapsResponse":{"type":"object","properties":{"caps":{"type":"array","items":{"$ref":"#/components/schemas/Capability"}}}},"Capability":{"type":"object","properties":{"name":{"type":"string"},"path":{"type":"string"},"data":{"type":"object"}}},"Command":{"type":"object","properties":{"script":{"type":"string","description":"A script that can be exec()'d on the agent"}}},"CommandRequest":{"type":"object","properties":{"commands":{"type":"array","items":{"$ref":"#/components/schemas/Command"}}}},"CommandResponse":{"type":"object","properties":{"uuid":{"type":"string","format":"uuid"},"stream":{"description":"URL to streaming WebSockets logs","type":"string","format":"url"},"task":{"description":"URL to the task metadata","type":"string","format":"url"},"log":{"description":"URL to the raw log of the task run","type":"string","format":"url"}}}}}}
|
||||
|
|
|
@ -1,8 +1,21 @@
|
|||
---
|
||||
agents:
|
||||
- 'http://localhost:9000'
|
||||
'Local':
|
||||
url: 'http://localhost:9000'
|
||||
'Duplicate Local':
|
||||
url: 'http://localhost:9000'
|
||||
projects:
|
||||
'janky':
|
||||
description: |
|
||||
Self-hosted Janky project
|
||||
filename: 'ci.janky.yml'
|
||||
scm:
|
||||
github:
|
||||
owner: 'rtyler'
|
||||
repo: 'janky'
|
||||
ref: 'main'
|
||||
'janky with spaces':
|
||||
description: A test configuration with spaces in the name
|
||||
filename: 'ci.janky.yml'
|
||||
scm:
|
||||
github:
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
#!/bin/sh
|
||||
|
||||
exec ruby -ryaml -rjson -e 'puts JSON.dump(YAML.load(STDIN.read))' < api-description-agent.yml > apidocs/api-description.json
|
||||
exec ruby -ryaml -rjson -e 'puts JSON.dump(YAML.load(STDIN.read))' < api-description.yml > apidocs/api-description.json
|
|
@ -1,3 +0,0 @@
|
|||
#!/bin/sh
|
||||
|
||||
exec ruby -ryaml -rjson -e 'puts JSON.dump(YAML.load(STDIN.read))' < api-description-server.yml > apidocs/api-description.json
|
|
@ -186,6 +186,36 @@
|
|||
},
|
||||
"query": "SELECT * FROM scm_info WHERE uuid = ?"
|
||||
},
|
||||
"8482da66fb4c815cf21576e0b5c8121f5cb3a96b0a3f5e8241dbd677860c62af": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "uuid",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"ordinal": 2,
|
||||
"type_info": "Datetime"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
}
|
||||
},
|
||||
"query": "SELECT * FROM projects"
|
||||
},
|
||||
"980b3cb885d26d06b4178df215617e26aecd79f4d813df14770ec8ae540d0ce2": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
|
|
|
@ -3,11 +3,12 @@
|
|||
*/
|
||||
|
||||
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||
use serde::Serialize;
|
||||
use sqlx::sqlite::SqliteQueryResult;
|
||||
use sqlx::{Sqlite, SqlitePool, Transaction};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct Project {
|
||||
uuid: String,
|
||||
name: String,
|
||||
|
@ -39,6 +40,12 @@ impl Project {
|
|||
.await
|
||||
}
|
||||
|
||||
pub async fn list(pool: &SqlitePool) -> Result<Vec<Project>, sqlx::Error> {
|
||||
sqlx::query_as!(Project, "SELECT * FROM projects")
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn create(
|
||||
project: &Project,
|
||||
tx: &SqlitePool,
|
||||
|
|
|
@ -80,9 +80,10 @@ enum Scm {
|
|||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
struct Project {
|
||||
description: String,
|
||||
filename: String,
|
||||
#[serde(with = "serde_yaml::with::singleton_map")]
|
||||
scm: Scm,
|
||||
filename: String,
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -93,6 +94,7 @@ struct Project {
|
|||
*/
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct Agent {
|
||||
name: String,
|
||||
url: Url,
|
||||
capabilities: Vec<janky::Capability>,
|
||||
}
|
||||
|
@ -113,9 +115,14 @@ impl Agent {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
struct AgentConfig {
|
||||
url: Url,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct ServerConfig {
|
||||
agents: Vec<Url>,
|
||||
agents: HashMap<String, AgentConfig>,
|
||||
projects: HashMap<String, Project>,
|
||||
}
|
||||
|
||||
|
@ -128,7 +135,7 @@ impl ServerConfig {
|
|||
impl Default for ServerConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
agents: vec![],
|
||||
agents: HashMap::default(),
|
||||
projects: HashMap::default(),
|
||||
}
|
||||
}
|
||||
|
@ -188,14 +195,15 @@ async fn main() -> Result<(), tide::Error> {
|
|||
}
|
||||
}
|
||||
|
||||
for url in &config.agents {
|
||||
debug!("Requesting capabilities from agent: {}", url);
|
||||
let response: janky::CapsResponse = reqwest::get(url.join("/api/v1/capabilities")?)
|
||||
for (name, agent) in config.agents.iter() {
|
||||
debug!("Requesting capabilities from agent: {:?}", agent);
|
||||
let response: janky::CapsResponse = reqwest::get(agent.url.join("/api/v1/capabilities")?)
|
||||
.await?
|
||||
.json()
|
||||
.await?;
|
||||
state.agents.push(Agent {
|
||||
url: url.clone(),
|
||||
name: name.clone(),
|
||||
url: agent.url.clone(),
|
||||
capabilities: response.caps,
|
||||
});
|
||||
}
|
||||
|
@ -233,8 +241,12 @@ async fn main() -> Result<(), tide::Error> {
|
|||
*/
|
||||
app.at("/apidocs").serve_dir("apidocs/")?;
|
||||
app.at("/static").serve_dir("static/")?;
|
||||
|
||||
debug!("Configuring routes");
|
||||
app.at("/").get(routes::index);
|
||||
app.at("/project/:name").get(routes::project);
|
||||
|
||||
debug!("Configuring API routes");
|
||||
app.at("/api/v1/projects/:name")
|
||||
.post(routes::api::execute_project);
|
||||
app.listen(opts.listen).await?;
|
||||
|
@ -251,6 +263,7 @@ mod tests {
|
|||
let needs: Vec<String> = vec!["rspec".into(), "git".into(), "dotnet".into()];
|
||||
let capabilities = vec![Capability::with_name("rustc")];
|
||||
let agent = Agent {
|
||||
name: "test".into(),
|
||||
url: Url::parse("http://localhost").unwrap(),
|
||||
capabilities,
|
||||
};
|
||||
|
@ -262,6 +275,7 @@ mod tests {
|
|||
let needs: Vec<String> = vec!["dotnet".into()];
|
||||
let capabilities = vec![Capability::with_name("dotnet")];
|
||||
let agent = Agent {
|
||||
name: "test".into(),
|
||||
url: Url::parse("http://localhost").unwrap(),
|
||||
capabilities,
|
||||
};
|
||||
|
@ -273,6 +287,7 @@ mod tests {
|
|||
let needs: Vec<String> = vec!["rspec".into(), "git".into(), "dotnet".into()];
|
||||
let capabilities = vec![Capability::with_name("dotnet")];
|
||||
let agent = Agent {
|
||||
name: "test".into(),
|
||||
url: Url::parse("http://localhost").unwrap(),
|
||||
capabilities,
|
||||
};
|
||||
|
@ -287,6 +302,7 @@ mod tests {
|
|||
Capability::with_name("rspec"),
|
||||
];
|
||||
let agent = Agent {
|
||||
name: "test".into(),
|
||||
url: Url::parse("http://localhost").unwrap(),
|
||||
capabilities,
|
||||
};
|
||||
|
|
|
@ -13,7 +13,9 @@ use tide::{Body, Request};
|
|||
pub async fn index(req: Request<AppState<'_>>) -> Result<Body, tide::Error> {
|
||||
let params = json!({
|
||||
"page": "home",
|
||||
"agents" : req.state().agents,
|
||||
"config" : req.state().config,
|
||||
"projects" : crate::dao::Project::list(&req.state().db).await?,
|
||||
});
|
||||
|
||||
let mut body = req.state().render("index", ¶ms).await?;
|
||||
|
@ -21,16 +23,37 @@ pub async fn index(req: Request<AppState<'_>>) -> Result<Body, tide::Error> {
|
|||
Ok(body)
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /project/:name
|
||||
*/
|
||||
pub async fn project(req: Request<AppState<'_>>) -> Result<Body, tide::Error> {
|
||||
let name: String = req.param("name")?.into();
|
||||
let params = json!({
|
||||
"name" : name,
|
||||
});
|
||||
|
||||
let mut body = req.state().render("project", ¶ms).await?;
|
||||
body.set_mime("text/html");
|
||||
Ok(body)
|
||||
}
|
||||
|
||||
pub mod api {
|
||||
use crate::{AppState, JankyYml, Scm};
|
||||
use log::*;
|
||||
use serde::Deserialize;
|
||||
use tide::{Request, Response, StatusCode};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct RedirectedForm {
|
||||
next: Option<String>,
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /projects/{name}
|
||||
*/
|
||||
pub async fn execute_project(req: Request<AppState<'_>>) -> Result<Response, tide::Error> {
|
||||
pub async fn execute_project(mut req: Request<AppState<'_>>) -> tide::Result {
|
||||
let name: String = req.param("name")?.into();
|
||||
let next: RedirectedForm = req.body_form().await?;
|
||||
let state = req.state();
|
||||
|
||||
if !state.config.has_project(&name) {
|
||||
|
@ -80,6 +103,10 @@ pub mod api {
|
|||
.send()
|
||||
.await?;
|
||||
|
||||
if let Some(red) = &next.next {
|
||||
return Ok(tide::Redirect::new(red).into());
|
||||
}
|
||||
|
||||
return Ok(
|
||||
json!({ "msg": format!("Executing on {}", &agent.url) }).into()
|
||||
);
|
||||
|
|
|
@ -11,38 +11,58 @@
|
|||
{{> _navbar }}
|
||||
|
||||
<div class="cover-container d-flex h-100 p-3 mx-auto flex-column">
|
||||
<main role="main" class="inner cover">
|
||||
<div id="projects">
|
||||
<table class="table table-dark table-striped">
|
||||
<thead>
|
||||
<td>
|
||||
<strong>Name</strong>
|
||||
</td>
|
||||
<td>
|
||||
<strong>Description</strong>
|
||||
</td>
|
||||
<td>
|
||||
<strong>Actions</strong>
|
||||
</td>
|
||||
</thead>
|
||||
{{#each config.projects}}
|
||||
<tr>
|
||||
<td>
|
||||
<a class="text-reset" href="/projects/{{@key}}"><strong>{{@key}}</strong></a>
|
||||
</td>
|
||||
<td>
|
||||
No Description
|
||||
</td>
|
||||
<td>
|
||||
<form method="POST" action="/api/v1/projects/{{@key}}">
|
||||
<input type="submit" value="Execute"/>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</table>
|
||||
</div>
|
||||
</main>
|
||||
<div class="row">
|
||||
<div class="col col-sm-2">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<td>Agents</td>
|
||||
</thead>
|
||||
{{#each agents}}
|
||||
<tr>
|
||||
<td>
|
||||
<span title="Capabilities: {{#each this.capabilities}}
|
||||
* {{this.name}} {{/each}}">
|
||||
{{this.name}}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</table>
|
||||
</div>
|
||||
<div class="col col-lg">
|
||||
<main role="main" class="inner cover"> <div id="projects">
|
||||
<table class="table table-dark table-striped">
|
||||
<thead>
|
||||
<td>
|
||||
<strong>Name</strong>
|
||||
</td>
|
||||
<td>
|
||||
<strong>Description</strong>
|
||||
</td>
|
||||
<td>
|
||||
<strong>Actions</strong>
|
||||
</td>
|
||||
</thead>
|
||||
{{#each projects}}
|
||||
<tr>
|
||||
<td>
|
||||
<a class="text-reset" href="/project/{{this.name}}"><strong>{{this.name}}</strong></a>
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
<td>
|
||||
<form method="POST" action="/api/v1/projects/{{this.name}}">
|
||||
<input type="hidden" name="next" value="/project/{{this.name}}"/>
|
||||
<input type="image" value="Execute" src="/static/icons/actions/view-refresh.svg"/>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</table>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Janky - {{name}}</title>
|
||||
<link type="text/css" rel="stylesheet" href="/static/bootstrap.min.css"/>
|
||||
<script src="/static/bootstrap.bundle.min.js" integrity="sha384-w76AqPfDkMBDXo30jS1Sgez6pr3x5MlQ1ZAGC+nuZB+EYdgRZgiwxhTBTkF7CXvN" crossorigin="anonymous"></script>
|
||||
|
||||
</head>
|
||||
|
||||
<body class="text-center">
|
||||
{{> _navbar }}
|
||||
|
||||
<div class="cover-container d-flex h-100 p-3 mx-auto flex-column">
|
||||
<div class="row">
|
||||
<div class="col col-sm-2">
|
||||
Links go here
|
||||
</div>
|
||||
<div class="col col-lg">
|
||||
<main role="main" class="inner cover"> <div id="projects">
|
||||
Runs go here
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
Loading…
Reference in New Issue