initial openrpc server
This commit is contained in:
1
lib/core/jobs/openrpc/.gitignore
vendored
Normal file
1
lib/core/jobs/openrpc/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
server
|
||||
183
lib/core/jobs/openrpc/examples/job_client.vsh
Executable file
183
lib/core/jobs/openrpc/examples/job_client.vsh
Executable file
@@ -0,0 +1,183 @@
|
||||
#!/usr/bin/env -S v -n -w -gc none -cc tcc -d use_openssl -enable-globals run
|
||||
|
||||
import freeflowuniverse.herolib.core.jobs.model
|
||||
import net.websocket
|
||||
import json
|
||||
import rand
|
||||
import time
|
||||
import term
|
||||
|
||||
const ws_url = 'ws://localhost:8080'
|
||||
|
||||
|
||||
// Helper function to send request and receive response
|
||||
fn send_request(mut ws websocket.Client, request OpenRPCRequest) !OpenRPCResponse {
|
||||
// Send request
|
||||
request_json := json.encode(request)
|
||||
println(request_json)
|
||||
ws.write_string(request_json) or {
|
||||
eprintln(term.red('Failed to send request: ${err}'))
|
||||
return err
|
||||
}
|
||||
|
||||
// Wait for response
|
||||
mut msg := ws.read_next_message() or {
|
||||
eprintln(term.red('Failed to read response: ${err}'))
|
||||
return err
|
||||
}
|
||||
|
||||
if msg.opcode != websocket.OPCode.text_frame {
|
||||
return error('Invalid response type: expected text frame')
|
||||
}
|
||||
|
||||
response_text := msg.payload.bytestr()
|
||||
|
||||
// Parse response
|
||||
response := json.decode(OpenRPCResponse, response_text) or {
|
||||
eprintln(term.red('Failed to decode response: ${err}'))
|
||||
return err
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
// OpenRPC request/response structures (copied from handler.v)
|
||||
struct OpenRPCRequest {
|
||||
jsonrpc string [required]
|
||||
method string [required]
|
||||
params []string
|
||||
id int [required]
|
||||
}
|
||||
|
||||
struct OpenRPCResponse {
|
||||
jsonrpc string [required]
|
||||
result string
|
||||
error string
|
||||
id int [required]
|
||||
}
|
||||
|
||||
|
||||
// Initialize and configure WebSocket client
|
||||
fn init_client() !&websocket.Client {
|
||||
mut ws := websocket.new_client(ws_url)!
|
||||
|
||||
ws.on_open(fn (mut ws websocket.Client) ! {
|
||||
println(term.green('Connected to WebSocket server and ready...'))
|
||||
})
|
||||
|
||||
ws.on_error(fn (mut ws websocket.Client, err string) ! {
|
||||
eprintln(term.red('WebSocket error: ${err}'))
|
||||
})
|
||||
|
||||
ws.on_close(fn (mut ws websocket.Client, code int, reason string) ! {
|
||||
println(term.yellow('WebSocket connection closed: ${reason}'))
|
||||
})
|
||||
|
||||
ws.on_message(fn (mut ws websocket.Client, msg &websocket.Message) ! {
|
||||
if msg.payload.len > 0 {
|
||||
println(term.blue('Received message: ${msg.payload.bytestr()}'))
|
||||
}
|
||||
})
|
||||
|
||||
ws.connect() or {
|
||||
eprintln(term.red('Failed to connect: ${err}'))
|
||||
return err
|
||||
}
|
||||
|
||||
spawn ws.listen()
|
||||
return ws
|
||||
}
|
||||
|
||||
// Main client logic
|
||||
mut ws := init_client()!
|
||||
defer {
|
||||
ws.close(1000, 'normal') or {
|
||||
eprintln(term.red('Error closing connection: ${err}'))
|
||||
}
|
||||
}
|
||||
println(term.green('Connected to ${ws_url}'))
|
||||
|
||||
// Create a new job
|
||||
println(term.blue('\nCreating new job...'))
|
||||
new_job := send_request(mut ws, OpenRPCRequest{
|
||||
jsonrpc: '2.0'
|
||||
method: 'job.new'
|
||||
params: []string{}
|
||||
id: rand.i32_in_range(1,10000000)!
|
||||
}) or {
|
||||
eprintln(term.red('Failed to create new job: ${err}'))
|
||||
exit(1)
|
||||
}
|
||||
println(term.green('Created new job:'))
|
||||
println(json.encode_pretty(new_job))
|
||||
|
||||
// Parse job from response
|
||||
job := json.decode(model.Job, new_job.result) or {
|
||||
eprintln(term.red('Failed to parse job: ${err}'))
|
||||
exit(1)
|
||||
}
|
||||
|
||||
// Set job properties
|
||||
println(term.blue('\nSetting job properties...'))
|
||||
mut updated_job := job
|
||||
updated_job.guid = 'test-job-1'
|
||||
updated_job.actor = 'vm_manager'
|
||||
updated_job.action = 'start'
|
||||
updated_job.params = {
|
||||
'name': 'test-vm'
|
||||
'memory': '2048'
|
||||
}
|
||||
|
||||
// Save job
|
||||
set_response := send_request(mut ws, OpenRPCRequest{
|
||||
jsonrpc: '2.0'
|
||||
method: 'job.set'
|
||||
params: [json.encode(updated_job)]
|
||||
id: rand.int()
|
||||
}) or {
|
||||
eprintln(term.red('Failed to save job: ${err}'))
|
||||
exit(1)
|
||||
}
|
||||
println(term.green('Saved job:'))
|
||||
println(json.encode_pretty(set_response))
|
||||
|
||||
// Update job status to running
|
||||
println(term.blue('\nUpdating job status...'))
|
||||
update_response := send_request(mut ws, OpenRPCRequest{
|
||||
jsonrpc: '2.0'
|
||||
method: 'job.update_status'
|
||||
params: ['test-job-1', 'running']
|
||||
id: rand.int()
|
||||
}) or {
|
||||
eprintln(term.red('Failed to update job status: ${err}'))
|
||||
exit(1)
|
||||
}
|
||||
println(term.green('Updated job status:'))
|
||||
println(json.encode_pretty(update_response))
|
||||
|
||||
// Get job to verify changes
|
||||
println(term.blue('\nRetrieving job...'))
|
||||
get_response := send_request(mut ws, OpenRPCRequest{
|
||||
jsonrpc: '2.0'
|
||||
method: 'job.get'
|
||||
params: ['test-job-1']
|
||||
id: rand.int()
|
||||
}) or {
|
||||
eprintln(term.red('Failed to retrieve job: ${err}'))
|
||||
exit(1)
|
||||
}
|
||||
println(term.green('Retrieved job:'))
|
||||
println(json.encode_pretty(get_response))
|
||||
|
||||
// List all jobs
|
||||
println(term.blue('\nListing all jobs...'))
|
||||
list_response := send_request(mut ws, OpenRPCRequest{
|
||||
jsonrpc: '2.0'
|
||||
method: 'job.list'
|
||||
params: []string{}
|
||||
id: rand.int()
|
||||
}) or {
|
||||
eprintln(term.red('Failed to list jobs: ${err}'))
|
||||
exit(1)
|
||||
}
|
||||
println(term.green('All jobs:'))
|
||||
println(json.encode_pretty(list_response))
|
||||
41
lib/core/jobs/openrpc/examples/server.vsh
Executable file
41
lib/core/jobs/openrpc/examples/server.vsh
Executable file
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env -S v -n -w -gc none -cc tcc -d use_openssl -enable-globals run
|
||||
|
||||
import freeflowuniverse.herolib.core.jobs.openrpc
|
||||
import freeflowuniverse.herolib.core.jobs.model
|
||||
import time
|
||||
import sync
|
||||
import os
|
||||
|
||||
fn start_rpc_server( mut wg sync.WaitGroup) ! {
|
||||
defer { wg.done() }
|
||||
|
||||
// Create OpenRPC server
|
||||
openrpc.server_start()!
|
||||
|
||||
}
|
||||
|
||||
fn start_ws_server( mut wg sync.WaitGroup) ! {
|
||||
defer { wg.done() }
|
||||
|
||||
// Get port from environment variable or use default
|
||||
port := if ws_port := os.getenv_opt('WS_PORT') {
|
||||
ws_port.int()
|
||||
} else {
|
||||
8080
|
||||
}
|
||||
|
||||
// Create and start WebSocket server
|
||||
mut ws_server := openrpc.new_ws_server(port)!
|
||||
ws_server.start()!
|
||||
}
|
||||
|
||||
// Create wait group for servers
|
||||
mut wg := sync.new_waitgroup()
|
||||
wg.add(2)
|
||||
|
||||
// Start servers in separate threads
|
||||
spawn start_rpc_server(mut wg)
|
||||
spawn start_ws_server(mut wg)
|
||||
|
||||
// Wait for servers to finish (they run forever)
|
||||
wg.wait()
|
||||
27
lib/core/jobs/openrpc/factory.v
Normal file
27
lib/core/jobs/openrpc/factory.v
Normal file
@@ -0,0 +1,27 @@
|
||||
module openrpc
|
||||
import freeflowuniverse.herolib.core.redisclient
|
||||
import freeflowuniverse.herolib.core.jobs.model
|
||||
|
||||
// Generic OpenRPC server that handles all managers
|
||||
pub struct OpenRPCServer {
|
||||
mut:
|
||||
redis &redisclient.Redis
|
||||
queue &redisclient.RedisQueue
|
||||
runner &model.HeroRunner
|
||||
}
|
||||
|
||||
// Create new OpenRPC server with Redis connection
|
||||
pub fn server_start() ! {
|
||||
redis := redisclient.core_get()!
|
||||
mut runner := model.new()!
|
||||
mut s:= &OpenRPCServer{
|
||||
redis: redis
|
||||
queue: &redisclient.RedisQueue{
|
||||
key: rpc_queue
|
||||
redis: redis
|
||||
|
||||
}
|
||||
runner:runner
|
||||
}
|
||||
s.start()!
|
||||
}
|
||||
71
lib/core/jobs/openrpc/handler.v
Normal file
71
lib/core/jobs/openrpc/handler.v
Normal file
@@ -0,0 +1,71 @@
|
||||
module openrpc
|
||||
|
||||
import freeflowuniverse.herolib.core.redisclient
|
||||
import json
|
||||
|
||||
|
||||
|
||||
// Start the server and listen for requests
|
||||
pub fn (mut s OpenRPCServer) start() ! {
|
||||
println('Starting OpenRPC server.')
|
||||
|
||||
for {
|
||||
// Get message from queue
|
||||
msg := s.queue.get(5000)!
|
||||
|
||||
if msg.len==0{
|
||||
println("queue '${rpc_queue}' empty")
|
||||
continue
|
||||
}
|
||||
|
||||
println("process '${msg}'")
|
||||
|
||||
// Parse OpenRPC request
|
||||
request := json.decode(OpenRPCRequest, msg) or {
|
||||
println('Error decoding request: ${err}')
|
||||
continue
|
||||
}
|
||||
|
||||
// Process request with appropriate handler
|
||||
response := s.handle_request(request)!
|
||||
|
||||
// Send response back to Redis using response queue
|
||||
response_json := json.encode(response)
|
||||
key:='${rpc_queue}:${request.id}'
|
||||
println("response: ${} put on return queue ${key} ")
|
||||
mut response_queue := &redisclient.RedisQueue{
|
||||
key: key
|
||||
redis: s.redis
|
||||
}
|
||||
response_queue.add(response_json)!
|
||||
}
|
||||
}
|
||||
|
||||
// Get the handler for a specific method based on its prefix
|
||||
fn (mut s OpenRPCServer) handle_request(request OpenRPCRequest) !OpenRPCResponse {
|
||||
method := request.method.to_lower()
|
||||
println("process: method: '${method}'")
|
||||
if method.starts_with("job.") {
|
||||
return s.handle_request_job(request) or {
|
||||
return rpc_response_error(request.id,"error in request job:\n${err}")
|
||||
}
|
||||
}
|
||||
if method.starts_with("agent.") {
|
||||
return s.handle_request_agent(request) or {
|
||||
return rpc_response_error(request.id,"error in request agent:\n${err}")
|
||||
}
|
||||
}
|
||||
if method.starts_with("group.") {
|
||||
return s.handle_request_group(request) or {
|
||||
return rpc_response_error(request.id,"error in request group:\n${err}")
|
||||
}
|
||||
}
|
||||
if method.starts_with("service.") {
|
||||
return s.handle_request_service(request) or {
|
||||
return rpc_response_error(request.id,"error in request service:\n${err}")
|
||||
}
|
||||
}
|
||||
|
||||
return rpc_response_error(request.id,"Could not find handler for ${method}")
|
||||
|
||||
}
|
||||
69
lib/core/jobs/openrpc/handler_agent_manager.v
Normal file
69
lib/core/jobs/openrpc/handler_agent_manager.v
Normal file
@@ -0,0 +1,69 @@
|
||||
module openrpc
|
||||
|
||||
import freeflowuniverse.herolib.core.jobs.model
|
||||
import json
|
||||
|
||||
|
||||
pub fn (mut h OpenRPCServer) handle_request_agent(request OpenRPCRequest) !OpenRPCResponse {
|
||||
|
||||
mut response:=rpc_response_new(request.id)
|
||||
|
||||
match request.method {
|
||||
'new' {
|
||||
agent := h.runner.agents.new()
|
||||
response.result = json.encode(agent)
|
||||
}
|
||||
'set' {
|
||||
if request.params.len < 1 {
|
||||
return error('Missing agent parameter')
|
||||
}
|
||||
agent := json.decode(model.Agent, request.params[0])!
|
||||
h.runner.agents.set(agent)!
|
||||
response.result = 'true'
|
||||
}
|
||||
'get' {
|
||||
if request.params.len < 1 {
|
||||
return error('Missing pubkey parameter')
|
||||
}
|
||||
agent := h.runner.agents.get(request.params[0])!
|
||||
response.result = json.encode(agent)
|
||||
}
|
||||
'list' {
|
||||
agents := h.runner.agents.list()!
|
||||
response.result = json.encode(agents)
|
||||
}
|
||||
'delete' {
|
||||
if request.params.len < 1 {
|
||||
return error('Missing pubkey parameter')
|
||||
}
|
||||
h.runner.agents.delete(request.params[0])!
|
||||
response.result = 'true'
|
||||
}
|
||||
'update_status' {
|
||||
if request.params.len < 2 {
|
||||
return error('Missing pubkey or status parameters')
|
||||
}
|
||||
status := match request.params[1] {
|
||||
'ok' { model.AgentState.ok }
|
||||
'down' { model.AgentState.down }
|
||||
'error' { model.AgentState.error }
|
||||
'halted' { model.AgentState.halted }
|
||||
else { return error('Invalid status: ${request.params[1]}') }
|
||||
}
|
||||
h.runner.agents.update_status(request.params[0], status)!
|
||||
response.result = 'true'
|
||||
}
|
||||
'get_by_service' {
|
||||
if request.params.len < 2 {
|
||||
return error('Missing actor or action parameters')
|
||||
}
|
||||
agents := h.runner.agents.get_by_service(request.params[0], request.params[1])!
|
||||
response.result = json.encode(agents)
|
||||
}
|
||||
else {
|
||||
return error('Unknown method: ${request.method}')
|
||||
}
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
66
lib/core/jobs/openrpc/handler_group_manager.v
Normal file
66
lib/core/jobs/openrpc/handler_group_manager.v
Normal file
@@ -0,0 +1,66 @@
|
||||
module openrpc
|
||||
|
||||
import freeflowuniverse.herolib.core.jobs.model
|
||||
import json
|
||||
|
||||
pub fn (mut h OpenRPCServer) handle_request_group(request OpenRPCRequest) !OpenRPCResponse {
|
||||
mut response:=rpc_response_new(request.id)
|
||||
match request.method {
|
||||
'new' {
|
||||
group := h.runner.groups.new()
|
||||
response.result = json.encode(group)
|
||||
}
|
||||
'set' {
|
||||
if request.params.len < 1 {
|
||||
return error('Missing group parameter')
|
||||
}
|
||||
group := json.decode(model.Group, request.params[0])!
|
||||
h.runner.groups.set(group)!
|
||||
response.result = 'true'
|
||||
}
|
||||
'get' {
|
||||
if request.params.len < 1 {
|
||||
return error('Missing guid parameter')
|
||||
}
|
||||
group := h.runner.groups.get(request.params[0])!
|
||||
response.result = json.encode(group)
|
||||
}
|
||||
'list' {
|
||||
groups := h.runner.groups.list()!
|
||||
response.result = json.encode(groups)
|
||||
}
|
||||
'delete' {
|
||||
if request.params.len < 1 {
|
||||
return error('Missing guid parameter')
|
||||
}
|
||||
h.runner.groups.delete(request.params[0])!
|
||||
response.result = 'true'
|
||||
}
|
||||
'add_member' {
|
||||
if request.params.len < 2 {
|
||||
return error('Missing guid or member parameters')
|
||||
}
|
||||
h.runner.groups.add_member(request.params[0], request.params[1])!
|
||||
response.result = 'true'
|
||||
}
|
||||
'remove_member' {
|
||||
if request.params.len < 2 {
|
||||
return error('Missing guid or member parameters')
|
||||
}
|
||||
h.runner.groups.remove_member(request.params[0], request.params[1])!
|
||||
response.result = 'true'
|
||||
}
|
||||
'get_user_groups' {
|
||||
if request.params.len < 1 {
|
||||
return error('Missing user_pubkey parameter')
|
||||
}
|
||||
groups := h.runner.groups.get_user_groups(request.params[0])!
|
||||
response.result = json.encode(groups)
|
||||
}
|
||||
else {
|
||||
return error('Unknown method: ${request.method}')
|
||||
}
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
63
lib/core/jobs/openrpc/handler_job_manager.v
Normal file
63
lib/core/jobs/openrpc/handler_job_manager.v
Normal file
@@ -0,0 +1,63 @@
|
||||
module openrpc
|
||||
|
||||
import freeflowuniverse.herolib.core.jobs.model
|
||||
import json
|
||||
|
||||
pub fn (mut h OpenRPCServer) handle_request_job(request OpenRPCRequest) !OpenRPCResponse {
|
||||
mut response:=rpc_response_new(request.id)
|
||||
println(request)
|
||||
match request.method {
|
||||
'new' {
|
||||
job := h.runner.jobs.new()
|
||||
response.result = json.encode(job)
|
||||
}
|
||||
'set' {
|
||||
if request.params.len < 1 {
|
||||
return error('Missing job parameter')
|
||||
}
|
||||
job := json.decode(model.Job, request.params[0])!
|
||||
h.runner.jobs.set(job)!
|
||||
response.result = 'true'
|
||||
}
|
||||
'get' {
|
||||
if request.params.len < 1 {
|
||||
return error('Missing guid parameter')
|
||||
}
|
||||
job := h.runner.jobs.get(request.params[0])!
|
||||
response.result = json.encode(job)
|
||||
}
|
||||
'list' {
|
||||
jobs := h.runner.jobs.list()!
|
||||
response.result = json.encode(jobs)
|
||||
}
|
||||
'delete' {
|
||||
if request.params.len < 1 {
|
||||
return error('Missing guid parameter')
|
||||
}
|
||||
h.runner.jobs.delete(request.params[0])!
|
||||
response.result = 'true'
|
||||
}
|
||||
'update_status' {
|
||||
if request.params.len < 2 {
|
||||
return error('Missing guid or status parameters')
|
||||
}
|
||||
status := match request.params[1] {
|
||||
'created' { model.Status.created }
|
||||
'scheduled' { model.Status.scheduled }
|
||||
'planned' { model.Status.planned }
|
||||
'running' { model.Status.running }
|
||||
'error' { model.Status.error }
|
||||
'ok' { model.Status.ok }
|
||||
else { return error('Invalid status: ${request.params[1]}') }
|
||||
}
|
||||
h.runner.jobs.update_status(request.params[0], status)!
|
||||
job := h.runner.jobs.get(request.params[0])! // Get updated job to return
|
||||
response.result = json.encode(job)
|
||||
}
|
||||
else {
|
||||
return error('Unknown method: ${request.method}')
|
||||
}
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
81
lib/core/jobs/openrpc/handler_service_manager.v
Normal file
81
lib/core/jobs/openrpc/handler_service_manager.v
Normal file
@@ -0,0 +1,81 @@
|
||||
module openrpc
|
||||
|
||||
import freeflowuniverse.herolib.core.jobs.model
|
||||
import json
|
||||
|
||||
pub fn (mut h OpenRPCServer) handle_request_service(request OpenRPCRequest) !OpenRPCResponse {
|
||||
mut response:=rpc_response_new(request.id)
|
||||
|
||||
match request.method {
|
||||
'new' {
|
||||
service := h.runner.services.new()
|
||||
response.result = json.encode(service)
|
||||
}
|
||||
'set' {
|
||||
if request.params.len < 1 {
|
||||
return error('Missing service parameter')
|
||||
}
|
||||
service := json.decode(model.Service, request.params[0])!
|
||||
h.runner.services.set(service)!
|
||||
response.result = 'true'
|
||||
}
|
||||
'get' {
|
||||
if request.params.len < 1 {
|
||||
return error('Missing actor parameter')
|
||||
}
|
||||
service := h.runner.services.get(request.params[0])!
|
||||
response.result = json.encode(service)
|
||||
}
|
||||
'list' {
|
||||
services := h.runner.services.list()!
|
||||
response.result = json.encode(services)
|
||||
}
|
||||
'delete' {
|
||||
if request.params.len < 1 {
|
||||
return error('Missing actor parameter')
|
||||
}
|
||||
h.runner.services.delete(request.params[0])!
|
||||
response.result = 'true'
|
||||
}
|
||||
'update_status' {
|
||||
if request.params.len < 2 {
|
||||
return error('Missing actor or status parameters')
|
||||
}
|
||||
status := match request.params[1] {
|
||||
'ok' { model.ServiceState.ok }
|
||||
'down' { model.ServiceState.down }
|
||||
'error' { model.ServiceState.error }
|
||||
'halted' { model.ServiceState.halted }
|
||||
else { return error('Invalid status: ${request.params[1]}') }
|
||||
}
|
||||
h.runner.services.update_status(request.params[0], status)!
|
||||
response.result = 'true'
|
||||
}
|
||||
'get_by_action' {
|
||||
if request.params.len < 1 {
|
||||
return error('Missing action parameter')
|
||||
}
|
||||
services := h.runner.services.get_by_action(request.params[0])!
|
||||
response.result = json.encode(services)
|
||||
}
|
||||
'check_access' {
|
||||
if request.params.len < 4 {
|
||||
return error('Missing parameters: requires actor, action, user_pubkey, and groups')
|
||||
}
|
||||
// Parse groups array from JSON string
|
||||
groups := json.decode([]string, request.params[3])!
|
||||
has_access := h.runner.services.check_access(
|
||||
request.params[0], // actor
|
||||
request.params[1], // action
|
||||
request.params[2], // user_pubkey
|
||||
groups
|
||||
)!
|
||||
response.result = json.encode(has_access)
|
||||
}
|
||||
else {
|
||||
return error('Unknown method: ${request.method}')
|
||||
}
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
39
lib/core/jobs/openrpc/model.v
Normal file
39
lib/core/jobs/openrpc/model.v
Normal file
@@ -0,0 +1,39 @@
|
||||
module openrpc
|
||||
|
||||
// Generic OpenRPC request/response structures
|
||||
pub struct OpenRPCRequest {
|
||||
pub mut:
|
||||
jsonrpc string @[required]
|
||||
method string @[required]
|
||||
params []string
|
||||
id int @[required]
|
||||
}
|
||||
|
||||
pub struct OpenRPCResponse {
|
||||
pub mut:
|
||||
jsonrpc string @[required]
|
||||
result string
|
||||
error string
|
||||
id int @[required]
|
||||
}
|
||||
|
||||
|
||||
fn rpc_response_new(id int)OpenRPCResponse {
|
||||
mut response := OpenRPCResponse{
|
||||
jsonrpc: '2.0'
|
||||
id: id
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
fn rpc_response_error(id int, errormsg string)OpenRPCResponse {
|
||||
mut response := OpenRPCResponse{
|
||||
jsonrpc: '2.0'
|
||||
id: id
|
||||
error:errormsg
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
|
||||
const rpc_queue = 'herorunner:q:rpc'
|
||||
302
lib/core/jobs/openrpc/specs/agent_manager_openrpc.json
Normal file
302
lib/core/jobs/openrpc/specs/agent_manager_openrpc.json
Normal file
@@ -0,0 +1,302 @@
|
||||
{
|
||||
"openrpc": "1.2.6",
|
||||
"info": {
|
||||
"title": "AgentManager Service",
|
||||
"version": "1.0.0",
|
||||
"description": "OpenRPC specification for the AgentManager module and its methods."
|
||||
},
|
||||
"methods": [
|
||||
{
|
||||
"name": "new",
|
||||
"summary": "Create a new Agent instance",
|
||||
"description": "Returns a new Agent with default or empty fields set. Caller can then fill in details.",
|
||||
"params": [],
|
||||
"result": {
|
||||
"name": "Agent",
|
||||
"description": "A freshly created Agent object.",
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Agent"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "set",
|
||||
"summary": "Add or update an Agent in the system",
|
||||
"description": "Stores an Agent in Redis by pubkey. Overwrites any previous entry with the same pubkey.",
|
||||
"params": [
|
||||
{
|
||||
"name": "agent",
|
||||
"description": "The Agent instance to be added or updated.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Agent"
|
||||
}
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "success",
|
||||
"description": "Indicates success. No data returned on success.",
|
||||
"schema": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "get",
|
||||
"summary": "Retrieve an Agent by its public key",
|
||||
"description": "Looks up a single Agent using its pubkey.",
|
||||
"params": [
|
||||
{
|
||||
"name": "pubkey",
|
||||
"description": "The public key to look up.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "Agent",
|
||||
"description": "The Agent that was requested, if found.",
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Agent"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "list",
|
||||
"summary": "List all Agents",
|
||||
"description": "Returns an array of all known Agents.",
|
||||
"params": [],
|
||||
"result": {
|
||||
"name": "Agents",
|
||||
"description": "A list of all Agents in the system.",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Agent"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "delete",
|
||||
"summary": "Delete an Agent by its public key",
|
||||
"description": "Removes an Agent from the system by pubkey.",
|
||||
"params": [
|
||||
{
|
||||
"name": "pubkey",
|
||||
"description": "The public key of the Agent to be deleted.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "success",
|
||||
"description": "Indicates success. No data returned on success.",
|
||||
"schema": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "update_status",
|
||||
"summary": "Update the status of an Agent",
|
||||
"description": "Updates only the status field of the specified Agent.",
|
||||
"params": [
|
||||
{
|
||||
"name": "pubkey",
|
||||
"description": "Public key of the Agent to update.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "status",
|
||||
"description": "The new status to set for the Agent.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/AgentState"
|
||||
}
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "success",
|
||||
"description": "Indicates success. No data returned on success.",
|
||||
"schema": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "get_by_service",
|
||||
"summary": "Retrieve all Agents that provide a specific service action",
|
||||
"description": "Filters Agents by matching actor and action in any of their declared services.",
|
||||
"params": [
|
||||
{
|
||||
"name": "actor",
|
||||
"description": "The actor name to match.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "action",
|
||||
"description": "The action name to match.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "Agents",
|
||||
"description": "A list of Agents that match the specified service actor and action.",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Agent"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"components": {
|
||||
"schemas": {
|
||||
"Agent": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"pubkey": {
|
||||
"type": "string",
|
||||
"description": "Public key (ed25519) of the Agent."
|
||||
},
|
||||
"address": {
|
||||
"type": "string",
|
||||
"description": "Network address or domain where the Agent can be reached."
|
||||
},
|
||||
"port": {
|
||||
"type": "integer",
|
||||
"description": "Network port for the Agent (default: 9999)."
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Optional human-readable description of the Agent."
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/components/schemas/AgentStatus"
|
||||
},
|
||||
"services": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/AgentService"
|
||||
},
|
||||
"description": "List of public services provided by the Agent."
|
||||
},
|
||||
"signature": {
|
||||
"type": "string",
|
||||
"description": "Signature (by the Agent's private key) of address+port+description+status."
|
||||
}
|
||||
},
|
||||
"required": ["pubkey", "status", "services"]
|
||||
},
|
||||
"AgentStatus": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"guid": {
|
||||
"type": "string",
|
||||
"description": "Unique ID for the job or session."
|
||||
},
|
||||
"timestamp_first": {
|
||||
"$ref": "#/components/schemas/OurTime",
|
||||
"description": "Timestamp when this Agent first came online."
|
||||
},
|
||||
"timestamp_last": {
|
||||
"$ref": "#/components/schemas/OurTime",
|
||||
"description": "Timestamp of the last heartbeat or update from the Agent."
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/components/schemas/AgentState"
|
||||
}
|
||||
}
|
||||
},
|
||||
"AgentService": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"actor": {
|
||||
"type": "string",
|
||||
"description": "The actor name providing the service."
|
||||
},
|
||||
"actions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/AgentServiceAction"
|
||||
},
|
||||
"description": "List of actions available for this service."
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Optional human-readable description for the service."
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/components/schemas/AgentServiceState"
|
||||
}
|
||||
},
|
||||
"required": ["actor", "actions", "status"]
|
||||
},
|
||||
"AgentServiceAction": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": {
|
||||
"type": "string",
|
||||
"description": "Action name."
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Optional description of this action."
|
||||
},
|
||||
"params": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Dictionary of parameter names to parameter descriptions."
|
||||
},
|
||||
"params_example": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Example values for the parameters."
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/components/schemas/AgentServiceState"
|
||||
},
|
||||
"public": {
|
||||
"type": "boolean",
|
||||
"description": "Indicates if the action is publicly accessible to all or restricted."
|
||||
}
|
||||
},
|
||||
"required": ["action", "status", "public"]
|
||||
},
|
||||
"AgentState": {
|
||||
"type": "string",
|
||||
"enum": ["ok", "down", "error", "halted"],
|
||||
"description": "Possible states of an Agent."
|
||||
},
|
||||
"AgentServiceState": {
|
||||
"type": "string",
|
||||
"enum": ["ok", "down", "error", "halted"],
|
||||
"description": "Possible states of an Agent service or action."
|
||||
},
|
||||
"OurTime": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "Represents a date/time or timestamp value."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
218
lib/core/jobs/openrpc/specs/group_manager_openrpc.json
Normal file
218
lib/core/jobs/openrpc/specs/group_manager_openrpc.json
Normal file
@@ -0,0 +1,218 @@
|
||||
{
|
||||
"openrpc": "1.2.6",
|
||||
"info": {
|
||||
"title": "Group Manager API",
|
||||
"version": "1.0.0",
|
||||
"description": "An OpenRPC specification for Group Manager methods"
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"name": "Local",
|
||||
"url": "http://localhost:8080"
|
||||
}
|
||||
],
|
||||
"methods": [
|
||||
{
|
||||
"name": "GroupManager.new",
|
||||
"summary": "Create a new (in-memory) Group instance",
|
||||
"description": "Creates a new group object. Note that this does NOT store it in Redis. The caller must set the group’s GUID and then call `GroupManager.set` if they wish to persist it.",
|
||||
"params": [],
|
||||
"result": {
|
||||
"name": "group",
|
||||
"description": "The newly-created group instance",
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Group"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "GroupManager.set",
|
||||
"summary": "Add or update a Group in Redis",
|
||||
"description": "Stores the specified group in Redis using the group’s GUID as the key.",
|
||||
"params": [
|
||||
{
|
||||
"name": "group",
|
||||
"description": "The group object to store",
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Group"
|
||||
}
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "result",
|
||||
"description": "No return value",
|
||||
"schema": {
|
||||
"type": "null"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "GroupManager.get",
|
||||
"summary": "Retrieve a Group by GUID",
|
||||
"description": "Fetches the group from Redis using the provided GUID.",
|
||||
"params": [
|
||||
{
|
||||
"name": "guid",
|
||||
"description": "The group’s unique identifier",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "group",
|
||||
"description": "The requested group",
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Group"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "GroupManager.list",
|
||||
"summary": "List all Groups",
|
||||
"description": "Returns an array containing all groups stored in Redis.",
|
||||
"params": [],
|
||||
"result": {
|
||||
"name": "groups",
|
||||
"description": "All currently stored groups",
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/GroupList"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "GroupManager.delete",
|
||||
"summary": "Delete a Group by GUID",
|
||||
"description": "Removes the specified group from Redis by its GUID.",
|
||||
"params": [
|
||||
{
|
||||
"name": "guid",
|
||||
"description": "The group’s unique identifier",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "result",
|
||||
"description": "No return value",
|
||||
"schema": {
|
||||
"type": "null"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "GroupManager.add_member",
|
||||
"summary": "Add a member to a Group",
|
||||
"description": "Adds a user pubkey or another group’s GUID to the member list of the specified group. Does not add duplicates.",
|
||||
"params": [
|
||||
{
|
||||
"name": "guid",
|
||||
"description": "The target group’s unique identifier",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "member",
|
||||
"description": "Pubkey or group GUID to be added to the group",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "result",
|
||||
"description": "No return value",
|
||||
"schema": {
|
||||
"type": "null"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "GroupManager.remove_member",
|
||||
"summary": "Remove a member from a Group",
|
||||
"description": "Removes a user pubkey or another group’s GUID from the member list of the specified group.",
|
||||
"params": [
|
||||
{
|
||||
"name": "guid",
|
||||
"description": "The target group’s unique identifier",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "member",
|
||||
"description": "Pubkey or group GUID to be removed from the group",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "result",
|
||||
"description": "No return value",
|
||||
"schema": {
|
||||
"type": "null"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "GroupManager.get_user_groups",
|
||||
"summary": "List Groups that a user belongs to (directly or indirectly)",
|
||||
"description": "Checks each group (and nested groups) to see if the user pubkey is a member, returning all groups in which the user is included (including membership through nested groups).",
|
||||
"params": [
|
||||
{
|
||||
"name": "user_pubkey",
|
||||
"description": "The pubkey of the user to check",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "groups",
|
||||
"description": "A list of groups to which the user belongs",
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/GroupList"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"components": {
|
||||
"schemas": {
|
||||
"Group": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"guid": {
|
||||
"type": "string",
|
||||
"description": "Unique ID for the group"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Name of the group"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Optional description of the group"
|
||||
},
|
||||
"members": {
|
||||
"type": "array",
|
||||
"description": "List of user pubkeys or other group GUIDs",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["guid", "members"]
|
||||
},
|
||||
"GroupList": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Group"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
304
lib/core/jobs/openrpc/specs/job_manager_openrpc.json
Normal file
304
lib/core/jobs/openrpc/specs/job_manager_openrpc.json
Normal file
@@ -0,0 +1,304 @@
|
||||
{
|
||||
"openrpc": "1.2.6",
|
||||
"info": {
|
||||
"title": "JobManager OpenRPC Specification",
|
||||
"version": "1.0.0",
|
||||
"description": "OpenRPC specification for the JobManager module which handles job operations."
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"name": "Local",
|
||||
"url": "http://localhost:8080/rpc"
|
||||
}
|
||||
],
|
||||
"methods": [
|
||||
{
|
||||
"name": "newJob",
|
||||
"summary": "Create a new Job instance",
|
||||
"description": "Creates a new Job with default/empty values. The GUID is left empty for the caller to fill.",
|
||||
"params": [],
|
||||
"result": {
|
||||
"name": "job",
|
||||
"description": "A newly created Job object, not yet persisted.",
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Job"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "setJob",
|
||||
"summary": "Add or update a Job in the system (Redis)",
|
||||
"description": "Persists the given Job into the data store. If the GUID already exists, the existing job is overwritten.",
|
||||
"params": [
|
||||
{
|
||||
"name": "job",
|
||||
"description": "The Job object to store or update.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Job"
|
||||
}
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "success",
|
||||
"description": "Indicates if the operation was successful.",
|
||||
"schema": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "getJob",
|
||||
"summary": "Retrieve a Job by its GUID",
|
||||
"description": "Fetches an existing Job from the data store using its unique GUID.",
|
||||
"params": [
|
||||
{
|
||||
"name": "guid",
|
||||
"description": "The GUID of the Job to retrieve.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "job",
|
||||
"description": "The retrieved Job object.",
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Job"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "listJobs",
|
||||
"summary": "List all Jobs",
|
||||
"description": "Returns an array of all Jobs present in the data store.",
|
||||
"params": [],
|
||||
"result": {
|
||||
"name": "jobs",
|
||||
"description": "Array of all Job objects found.",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Job"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "deleteJob",
|
||||
"summary": "Remove a Job by its GUID",
|
||||
"description": "Deletes a specific Job from the data store by its GUID.",
|
||||
"params": [
|
||||
{
|
||||
"name": "guid",
|
||||
"description": "The GUID of the Job to delete.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "success",
|
||||
"description": "Indicates if the job was successfully deleted.",
|
||||
"schema": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "updateJobStatus",
|
||||
"summary": "Update the status of a Job",
|
||||
"description": "Sets the status field of a Job in the data store.",
|
||||
"params": [
|
||||
{
|
||||
"name": "guid",
|
||||
"description": "The GUID of the Job to update.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "status",
|
||||
"description": "The new status for the Job.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Status"
|
||||
}
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "job",
|
||||
"description": "The updated Job object with new status applied.",
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Job"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"components": {
|
||||
"schemas": {
|
||||
"Job": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"guid": {
|
||||
"type": "string",
|
||||
"description": "Unique ID for the Job."
|
||||
},
|
||||
"agents": {
|
||||
"type": "array",
|
||||
"description": "Public keys of the agent(s) which will execute the command.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"source": {
|
||||
"type": "string",
|
||||
"description": "Pubkey of the agent who requested the job."
|
||||
},
|
||||
"circle": {
|
||||
"type": "string",
|
||||
"description": "Digital-life circle name this Job belongs to.",
|
||||
"default": "default"
|
||||
},
|
||||
"context": {
|
||||
"type": "string",
|
||||
"description": "High-level context for the Job inside a circle.",
|
||||
"default": "default"
|
||||
},
|
||||
"actor": {
|
||||
"type": "string",
|
||||
"description": "Actor name that will handle the Job (e.g. `vm_manager`)."
|
||||
},
|
||||
"action": {
|
||||
"type": "string",
|
||||
"description": "Action to be taken by the actor (e.g. `start`)."
|
||||
},
|
||||
"params": {
|
||||
"type": "object",
|
||||
"description": "Key-value parameters for the action to be performed.",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"timeout_schedule": {
|
||||
"type": "integer",
|
||||
"description": "Timeout (in seconds) before the job is picked up by an agent.",
|
||||
"default": 60
|
||||
},
|
||||
"timeout": {
|
||||
"type": "integer",
|
||||
"description": "Timeout (in seconds) for the job to complete.",
|
||||
"default": 3600
|
||||
},
|
||||
"log": {
|
||||
"type": "boolean",
|
||||
"description": "Whether to log job details.",
|
||||
"default": true
|
||||
},
|
||||
"ignore_error": {
|
||||
"type": "boolean",
|
||||
"description": "If true, job errors do not cause an exception to be raised."
|
||||
},
|
||||
"ignore_error_codes": {
|
||||
"type": "array",
|
||||
"description": "Array of error codes to ignore.",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"debug": {
|
||||
"type": "boolean",
|
||||
"description": "If true, additional debug information is provided.",
|
||||
"default": false
|
||||
},
|
||||
"retry": {
|
||||
"type": "integer",
|
||||
"description": "Number of retries allowed on error.",
|
||||
"default": 0
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/components/schemas/JobStatus"
|
||||
},
|
||||
"dependencies": {
|
||||
"type": "array",
|
||||
"description": "List of job dependencies that must complete before this job executes.",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/JobDependency"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"guid",
|
||||
"status"
|
||||
]
|
||||
},
|
||||
"JobStatus": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"guid": {
|
||||
"type": "string",
|
||||
"description": "Unique ID for the Job (mirrors the parent job GUID)."
|
||||
},
|
||||
"created": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "When the job was created."
|
||||
},
|
||||
"start": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "When the job was picked up to start."
|
||||
},
|
||||
"end": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "When the job ended."
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/components/schemas/Status"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"guid",
|
||||
"created",
|
||||
"status"
|
||||
]
|
||||
},
|
||||
"JobDependency": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"guid": {
|
||||
"type": "string",
|
||||
"description": "Unique ID of the Job this dependency points to."
|
||||
},
|
||||
"agents": {
|
||||
"type": "array",
|
||||
"description": "Possible agent(s) who can execute the dependency.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"guid"
|
||||
]
|
||||
},
|
||||
"Status": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"created",
|
||||
"scheduled",
|
||||
"planned",
|
||||
"running",
|
||||
"error",
|
||||
"ok"
|
||||
],
|
||||
"description": "Enumerates the possible states of a Job."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
301
lib/core/jobs/openrpc/specs/service_manager_openrpc.json
Normal file
301
lib/core/jobs/openrpc/specs/service_manager_openrpc.json
Normal file
@@ -0,0 +1,301 @@
|
||||
{
|
||||
"openrpc": "1.2.6",
|
||||
"info": {
|
||||
"title": "ServiceManager API",
|
||||
"version": "1.0.0",
|
||||
"description": "OpenRPC 2.0 spec for managing services with ServiceManager."
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"name": "Local",
|
||||
"url": "http://localhost:8080"
|
||||
}
|
||||
],
|
||||
"methods": [
|
||||
{
|
||||
"name": "ServiceManager_new",
|
||||
"summary": "Create a new Service instance (not saved to Redis yet).",
|
||||
"description": "Creates and returns a new empty Service object with default values. The `actor` field remains empty until the caller sets it.",
|
||||
"params": [],
|
||||
"result": {
|
||||
"name": "service",
|
||||
"$ref": "#/components/schemas/Service"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ServiceManager_set",
|
||||
"summary": "Add or update a Service in Redis.",
|
||||
"description": "Stores the Service in Redis, identified by its `actor` property.",
|
||||
"params": [
|
||||
{
|
||||
"name": "service",
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Service"
|
||||
}
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "success",
|
||||
"schema": {
|
||||
"type": "boolean",
|
||||
"description": "True if operation succeeds."
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ServiceManager_get",
|
||||
"summary": "Retrieve a Service by actor name.",
|
||||
"description": "Gets the Service object from Redis using the given actor name.",
|
||||
"params": [
|
||||
{
|
||||
"name": "actor",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "service",
|
||||
"$ref": "#/components/schemas/Service"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ServiceManager_list",
|
||||
"summary": "List all Services.",
|
||||
"description": "Returns an array of all Services stored in Redis.",
|
||||
"params": [],
|
||||
"result": {
|
||||
"name": "services",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Service"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ServiceManager_delete",
|
||||
"summary": "Delete a Service by actor name.",
|
||||
"description": "Removes the Service from Redis using the given actor name.",
|
||||
"params": [
|
||||
{
|
||||
"name": "actor",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "success",
|
||||
"schema": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ServiceManager_update_status",
|
||||
"summary": "Update the status of a given Service.",
|
||||
"description": "Updates only the `status` field of a Service specified by its actor name.",
|
||||
"params": [
|
||||
{
|
||||
"name": "actor",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "status",
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ServiceState"
|
||||
}
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "success",
|
||||
"schema": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ServiceManager_get_by_action",
|
||||
"summary": "Retrieve Services by action name.",
|
||||
"description": "Returns all Services that provide the specified action.",
|
||||
"params": [
|
||||
{
|
||||
"name": "action",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "services",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Service"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ServiceManager_check_access",
|
||||
"summary": "Check if a user has access to a Service action.",
|
||||
"description": "Verifies if a user (and any groups they belong to) has the right to invoke a specified action on a given Service.",
|
||||
"params": [
|
||||
{
|
||||
"name": "actor",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "action",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "user_pubkey",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "groups",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "hasAccess",
|
||||
"schema": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"components": {
|
||||
"schemas": {
|
||||
"Service": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"actor": {
|
||||
"type": "string",
|
||||
"description": "The actor (unique name) providing the service."
|
||||
},
|
||||
"actions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ServiceAction"
|
||||
},
|
||||
"description": "A list of actions available in this service."
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Optional description of the service."
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/components/schemas/ServiceState",
|
||||
"description": "The current state of the service."
|
||||
},
|
||||
"acl": {
|
||||
"$ref": "#/components/schemas/ACL",
|
||||
"description": "An optional access control list for the entire service."
|
||||
}
|
||||
},
|
||||
"required": ["actor", "actions", "status"]
|
||||
},
|
||||
"ServiceAction": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": {
|
||||
"type": "string",
|
||||
"description": "A unique identifier for the action."
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Optional description of this action."
|
||||
},
|
||||
"params": {
|
||||
"type": "object",
|
||||
"description": "Parameter definitions for this action.",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"params_example": {
|
||||
"type": "object",
|
||||
"description": "Example parameters for this action.",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"acl": {
|
||||
"$ref": "#/components/schemas/ACL",
|
||||
"description": "Optional ACL specifically for this action."
|
||||
}
|
||||
},
|
||||
"required": ["action"]
|
||||
},
|
||||
"ACL": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "A friendly name for the ACL."
|
||||
},
|
||||
"ace": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ACE"
|
||||
},
|
||||
"description": "A list of Access Control Entries."
|
||||
}
|
||||
},
|
||||
"required": ["ace"]
|
||||
},
|
||||
"ACE": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"groups": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "List of group IDs that have this permission."
|
||||
},
|
||||
"users": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "List of user public keys that have this permission."
|
||||
},
|
||||
"right": {
|
||||
"type": "string",
|
||||
"description": "Permission type (e.g. 'read', 'write', 'admin', 'block')."
|
||||
}
|
||||
},
|
||||
"required": ["right"]
|
||||
},
|
||||
"ServiceState": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ok",
|
||||
"down",
|
||||
"error",
|
||||
"halted"
|
||||
],
|
||||
"description": "Possible states of a service."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
82
lib/core/jobs/openrpc/ws_server.v
Normal file
82
lib/core/jobs/openrpc/ws_server.v
Normal file
@@ -0,0 +1,82 @@
|
||||
module openrpc
|
||||
|
||||
import net.websocket
|
||||
import freeflowuniverse.herolib.core.redisclient
|
||||
import json
|
||||
import rand
|
||||
|
||||
// WebSocket server that receives RPC requests
|
||||
pub struct WSServer {
|
||||
mut:
|
||||
redis &redisclient.Redis
|
||||
port int = 8080 // Default port, can be configured
|
||||
}
|
||||
|
||||
// Create new WebSocket server
|
||||
pub fn new_ws_server( port int) !&WSServer {
|
||||
return &WSServer{
|
||||
redis: redisclient.core_get()!
|
||||
port: port
|
||||
}
|
||||
}
|
||||
|
||||
// Start the WebSocket server
|
||||
pub fn (mut s WSServer) start() ! {
|
||||
mut ws_server := websocket.new_server(.ip, s.port, '')
|
||||
|
||||
// Handle new WebSocket connections
|
||||
ws_server.on_connect(fn (mut ws websocket.ServerClient) !bool {
|
||||
println('New WebSocket client connected')
|
||||
return true
|
||||
})!
|
||||
|
||||
// Handle client disconnections
|
||||
ws_server.on_close(fn (mut ws websocket.Client, code int, reason string) ! {
|
||||
println('WebSocket client disconnected (code: ${code}, reason: ${reason})')
|
||||
})
|
||||
|
||||
// Handle incoming messages
|
||||
ws_server.on_message(fn [mut s] (mut ws websocket.Client, msg &websocket.Message) ! {
|
||||
if msg.opcode != .text_frame {
|
||||
println('WebSocket unknown msg opcode (code: ${msg.opcode})')
|
||||
return
|
||||
}
|
||||
|
||||
// Parse request
|
||||
request := json.decode(OpenRPCRequest, msg.payload.bytestr()) or {
|
||||
error_msg := '{"jsonrpc":"2.0","error":"Invalid JSON-RPC request","id":null}'
|
||||
println(error_msg)
|
||||
ws.write(error_msg.bytes(), websocket.OPCode.text_frame) or { panic(err) }
|
||||
return
|
||||
}
|
||||
|
||||
// Generate unique request ID if not provided
|
||||
mut req_id := request.id
|
||||
if req_id == 0 {
|
||||
req_id = rand.i32_in_range(1,10000000)!
|
||||
}
|
||||
|
||||
println('WebSocket put on queue: \'${rpc_queue}\' (msg: ${msg.payload.bytestr()})')
|
||||
// Send request to Redis queue
|
||||
s.redis.lpush(rpc_queue, msg.payload.bytestr())!
|
||||
|
||||
// Wait for response
|
||||
response := s.redis.brpop(['${rpc_queue}:${req_id}'], 30)! // 30 second timeout
|
||||
if response.len < 2 {
|
||||
error_msg := '{"jsonrpc":"2.0","error":"Timeout waiting for response","id":${req_id}}'
|
||||
println('WebSocket error response (err: ${response})')
|
||||
ws.write(error_msg.bytes(), websocket.OPCode.text_frame) or { panic(err) }
|
||||
return
|
||||
}
|
||||
|
||||
println('WebSocket ok response (msg: ${response[1].bytes()})')
|
||||
// Send response back to WebSocket client
|
||||
ws.write(response[1].bytes(), websocket.OPCode.text_frame) or { panic(err) }
|
||||
})
|
||||
|
||||
// Start server
|
||||
println('WebSocket server listening on port ${s.port}')
|
||||
ws_server.listen() or {
|
||||
return error('Failed to start WebSocket server: ${err}')
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user