Refactor the project configuration to take in a github configuration

This allows for fetching the Jankyfile directly via REST API so the server
doesn't have to do any Git work just yet
This commit is contained in:
R Tyler Croy 2023-01-28 20:33:20 -08:00
parent 909dbe587d
commit 19126ed88b
No known key found for this signature in database
GPG Key ID: E5C92681BEF6CEA2
7 changed files with 104 additions and 29 deletions

View File

@ -0,0 +1,33 @@
---
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'

View File

@ -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":{"200":{"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"},"task":{"description":"URL to the task metadata","type":"string"}}}}}}
{"openapi":"3.0.0","info":{"description":"Janky Server 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/api/v1","description":"Local dev server (APIv1)"}],"paths":{"/projects/{name}":{"post":{"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"}}}}}}

View File

@ -2,10 +2,13 @@
agents:
- 'http://localhost:9000'
projects:
- type: 'github'
url: 'https://github.com/rtyler/janky'
ref: 'main'
'janky':
filename: 'Jankyfile'
scm:
github:
owner: 'rtyler'
repo: 'janky'
ref: 'main'
# The filetype Git is not yet supported
#- type: 'git'
# url: 'https://github.com/rtyler/jdp'

View File

@ -1,3 +1,3 @@
#!/bin/sh
exec ruby -ryaml -rjson -e 'puts JSON.dump(YAML.load(STDIN.read))' < agent-api-description.yml > apidocs/api-description.json
exec ruby -ryaml -rjson -e 'puts JSON.dump(YAML.load(STDIN.read))' < api-description-agent.yml > apidocs/api-description.json

3
scripts/prepare-server-api Executable file
View File

@ -0,0 +1,3 @@
#!/bin/sh
exec ruby -ryaml -rjson -e 'puts JSON.dump(YAML.load(STDIN.read))' < api-description-server.yml > apidocs/api-description.json

View File

@ -5,6 +5,7 @@
#[macro_use]
extern crate serde_json;
use std::collections::HashMap;
use std::path::PathBuf;
use async_std::sync::{Arc, RwLock};
@ -19,13 +20,15 @@ use url::Url;
#[derive(Clone, Debug)]
pub struct AppState<'a> {
pub db: SqlitePool,
pub config: ServerConfig,
hb: Arc<RwLock<Handlebars<'a>>>,
}
impl AppState<'_> {
fn new(db: SqlitePool) -> Self {
fn new(db: SqlitePool, config: ServerConfig) -> Self {
Self {
db,
config,
hb: Arc::new(RwLock::new(Handlebars::new())),
}
}
@ -74,52 +77,84 @@ mod routes {
"page": "home"
});
let res = octocrab::instance()
.repos("rtyler", "janky")
.raw_file(
octocrab::params::repos::Commitish("main".into()),
"Jankyfile",
)
.await?;
debug!("jank: {:?}", res);
debug!("text: {:?}", res.text().await?);
let mut body = req.state().render("index", &params).await?;
body.set_mime("text/html");
Ok(body)
}
pub mod api {}
pub mod api {
use crate::{AppState, Scm};
use log::*;
use tide::{Body, StatusCode, Request, Response};
/**
* POST /projects/{name}
*/
pub async fn execute_project(req: Request<AppState<'_>>) -> Result<Response, tide::Error> {
let name: String = req.param("name")?.into();
if ! req.state().config.has_project(&name) {
debug!("Could not find project named: {}", name);
return Ok(Response::new(StatusCode::NotFound));
}
if let Some(project) = req.state().config.projects.get(&name) {
match &project.scm {
Scm::GitHub { owner, repo, scm_ref } => {
debug!("Fetching the file {} from {}/{}", &project.filename, owner, repo);
let res = octocrab::instance()
.repos(owner, repo)
.raw_file(
octocrab::params::repos::Commitish(scm_ref.into()),
&project.filename
)
.await?;
debug!("text: {:?}", res.text().await?);
},
}
return Ok("{}".into());
}
Ok(Response::new(StatusCode::InternalServerError))
}
}
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "lowercase")]
enum Scm {
GitHub,
GitHub {
owner: String,
repo: String,
#[serde(rename = "ref")]
scm_ref: String,
},
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "lowercase")]
struct Project {
#[serde(rename = "type")]
scm_type: Scm,
url: Url,
#[serde(rename = "ref")]
scm_ref: String,
filename: PathBuf,
#[serde(with = "serde_yaml::with::singleton_map")]
scm: Scm,
filename: String,
}
#[derive(Clone, Debug, Deserialize)]
struct ServerConfig {
pub struct ServerConfig {
agents: Vec<Url>,
projects: Vec<Project>,
projects: HashMap<String, Project>,
}
impl ServerConfig {
fn has_project(&self, name: &str) -> bool {
self.projects.contains_key(name)
}
}
impl Default for ServerConfig {
fn default() -> Self {
Self {
agents: vec![],
projects: vec![],
projects: HashMap::default(),
}
}
}
@ -160,7 +195,7 @@ async fn main() -> Result<(), tide::Error> {
sqlx::migrate!().run(&pool).await?;
}
let state = AppState::new(pool);
let state = AppState::new(pool, config);
state.register_templates().await;
let mut app = tide::with_state(state);
@ -193,6 +228,7 @@ async fn main() -> Result<(), tide::Error> {
app.at("/static").serve_dir("static/")?;
debug!("Configuring routes");
app.at("/").get(routes::index);
app.at("/api/v1/projects/:name").post(routes::api::execute_project);
app.listen(opts.listen).await?;
Ok(())
}