From fb0754b3b20f062098a41f0ce09a9eb6dc1b2987 Mon Sep 17 00:00:00 2001 From: despiegk Date: Fri, 31 Jan 2025 17:13:56 +0300 Subject: [PATCH] ... --- lib/core/jobs/agent.v | 60 +++++++++ lib/core/jobs/agent_manager.v | 93 ++++++++++++++ lib/core/jobs/agent_manager_test.v | 75 +++++++++++ lib/core/jobs/factory.v | 37 ++++++ lib/core/jobs/group.v | 10 ++ lib/core/jobs/group_manager.v | 104 +++++++++++++++ lib/core/jobs/group_manager_test.v | 67 ++++++++++ lib/core/jobs/job.v | 52 ++++++++ lib/core/jobs/job_manager.v | 70 ++++++++++ lib/core/jobs/job_manager_test.v | 47 +++++++ lib/core/jobs/service.v | 44 +++++++ lib/core/jobs/service_manager.v | 124 ++++++++++++++++++ lib/core/jobs/service_manager_test.v | 84 ++++++++++++ lib/core/jobs/specs.md | 186 +++++++++++++++++++++++++++ 14 files changed, 1053 insertions(+) create mode 100644 lib/core/jobs/agent.v create mode 100644 lib/core/jobs/agent_manager.v create mode 100644 lib/core/jobs/agent_manager_test.v create mode 100644 lib/core/jobs/factory.v create mode 100644 lib/core/jobs/group.v create mode 100644 lib/core/jobs/group_manager.v create mode 100644 lib/core/jobs/group_manager_test.v create mode 100644 lib/core/jobs/job.v create mode 100644 lib/core/jobs/job_manager.v create mode 100644 lib/core/jobs/job_manager_test.v create mode 100644 lib/core/jobs/service.v create mode 100644 lib/core/jobs/service_manager.v create mode 100644 lib/core/jobs/service_manager_test.v create mode 100644 lib/core/jobs/specs.md diff --git a/lib/core/jobs/agent.v b/lib/core/jobs/agent.v new file mode 100644 index 00000000..c5fadeed --- /dev/null +++ b/lib/core/jobs/agent.v @@ -0,0 +1,60 @@ +module jobs + +import freeflowuniverse.herolib.data.ourtime + +// Agent represents a service provider that can execute jobs +pub struct Agent { +pub mut: + pubkey string // pubkey using ed25519 + address string // where we can find the agent + port int // default 9999 + description string // optional + status AgentStatus + services []AgentService // these are the public services + signature string // signature as done by private key of $address+$port+$description+$status +} + +// AgentStatus represents the current state of an agent +pub struct AgentStatus { +pub mut: + guid string // unique id for the job + timestamp_first ourtime.Time // when agent came online + timestamp_last ourtime.Time // last time agent let us know that he is working + status AgentState // current state of the agent +} + +// AgentService represents a service provided by an agent +pub struct AgentService { +pub mut: + actor string // name of the actor providing the service + actions []AgentServiceAction // available actions for this service + description string // optional description + status AgentServiceState // current state of the service +} + +// AgentServiceAction represents an action that can be performed by a service +pub struct AgentServiceAction { +pub mut: + action string // which action + description string // optional description + params map[string]string // e.g. name:'name of the vm' ... + params_example map[string]string // e.g. name:'myvm' + status AgentServiceState // current state of the action + public bool // if everyone can use then true, if restricted means only certain people can use +} + +// AgentState represents the possible states of an agent +pub enum AgentState { + ok // agent is functioning normally + down // agent is not responding + error // agent encountered an error + halted // agent has been manually stopped +} + +// AgentServiceState represents the possible states of an agent service or action +pub enum AgentServiceState { + ok // service/action is functioning normally + down // service/action is not available + error // service/action encountered an error + halted // service/action has been manually stopped +} diff --git a/lib/core/jobs/agent_manager.v b/lib/core/jobs/agent_manager.v new file mode 100644 index 00000000..e4f28ed8 --- /dev/null +++ b/lib/core/jobs/agent_manager.v @@ -0,0 +1,93 @@ +module jobs + +import freeflowuniverse.herolib.core.redisclient +import freeflowuniverse.herolib.data.ourtime +import json + +const ( + agents_key = 'herorunner:agents' // Redis key for storing agents +) + +// AgentManager handles all agent-related operations +pub struct AgentManager { +mut: + redis &redisclient.Redis +} + +// new creates a new Agent instance +pub fn (mut m AgentManager) new() Agent { + return Agent{ + pubkey: '' // Empty pubkey to be filled by caller + port: 9999 // Default port + status: AgentStatus{ + guid: '' + timestamp_first: ourtime.Time{} + timestamp_last: ourtime.Time{} + status: .ok + } + services: []AgentService{} + } +} + +// add adds a new agent to Redis +pub fn (mut m AgentManager) set(agent Agent) ! { + // Store agent in Redis hash where key is agent.pubkey and value is JSON of agent + agent_json := json.encode(agent) + m.redis.hset(agents_key, agent.pubkey, agent_json)! +} + +// get retrieves an agent by its public key +pub fn (mut m AgentManager) get(pubkey string) !Agent { + agent_json := m.redis.hget(agents_key, pubkey)! + return json.decode(Agent, agent_json) +} + +// list returns all agents +pub fn (mut m AgentManager) list() ![]Agent { + mut agents := []Agent{} + + // Get all agents from Redis hash + agents_map := m.redis.hgetall(agents_key)! + + // Convert each JSON value to Agent struct + for _, agent_json in agents_map { + agent := json.decode(Agent, agent_json)! + agents << agent + } + + return agents +} + +// delete removes an agent by its public key +pub fn (mut m AgentManager) delete(pubkey string) ! { + m.redis.hdel(agents_key, pubkey)! +} + +// update_status updates just the status of an agent +pub fn (mut m AgentManager) update_status(pubkey string, status AgentState) ! { + mut agent := m.get(pubkey)! + agent.status.status = status + m.update(agent)! +} + +// get_by_service returns all agents that provide a specific service +pub fn (mut m AgentManager) get_by_service(actor string, action string) ![]Agent { + mut matching_agents := []Agent{} + + agents := m.list()! + for agent in agents { + for service in agent.services { + if service.actor != actor { + continue + } + for act in service.actions { + if act.action == action { + matching_agents << agent + break + } + } + } + } + + return matching_agents +} diff --git a/lib/core/jobs/agent_manager_test.v b/lib/core/jobs/agent_manager_test.v new file mode 100644 index 00000000..de1ae991 --- /dev/null +++ b/lib/core/jobs/agent_manager_test.v @@ -0,0 +1,75 @@ +module jobs + +import freeflowuniverse.herolib.core.redisclient +import freeflowuniverse.herolib.data.ourtime + +fn test_runner.agents() { + + mut runner:=new()! + + // Create a new agent using the manager + mut agent := runner.agents.new() + agent.pubkey = 'test-agent-1' + agent.address = '127.0.0.1' + agent.description = 'Test Agent' + + // Create a service action + mut action := AgentServiceAction{ + action: 'start' + description: 'Start a VM' + params: { + 'name': 'string' + } + params_example: { + 'name': 'myvm' + } + status: .ok + public: true + } + + // Create a service + mut service := AgentService{ + actor: 'vm_manager' + actions: [action] + description: 'VM Management Service' + status: .ok + } + + agent.services = [service] + + // Add the agent + runner.agents.set(agent)! + + // Get the agent and verify fields + retrieved_agent := runner.agents.get(agent.pubkey)! + assert retrieved_agent.pubkey == agent.pubkey + assert retrieved_agent.address == agent.address + assert retrieved_agent.description == agent.description + assert retrieved_agent.services.len == 1 + assert retrieved_agent.services[0].actor == 'vm_manager' + assert retrieved_agent.status.status == .ok + + // Update agent status + runner.agents.update_status(agent.pubkey, .down)! + updated_agent := runner.agents.get(agent.pubkey)! + assert updated_agent.status.status == .down + + // Test get_by_service + agents := runner.agents.get_by_service('vm_manager', 'start')! + assert agents.len > 0 + assert agents[0].pubkey == agent.pubkey + + // List all agents + all_agents := runner.agents.list()! + assert all_agents.len > 0 + assert all_agents[0].pubkey == agent.pubkey + + // Delete the agent + runner.agents.delete(agent.pubkey)! + + // Verify deletion + agents_after := runner.agents.list()! + for a in agents_after { + assert a.pubkey != agent.pubkey + } +} diff --git a/lib/core/jobs/factory.v b/lib/core/jobs/factory.v new file mode 100644 index 00000000..1e5634c9 --- /dev/null +++ b/lib/core/jobs/factory.v @@ -0,0 +1,37 @@ +module jobs + +import freeflowuniverse.herolib.core.redisclient + +// HeroRunner is the main factory for managing jobs, agents, services and groups +pub struct HeroRunner { +mut: + redis &redisclient.Redis +pub mut: + jobs &JobManager + agents &AgentManager + services &ServiceManager + groups &GroupManager +} + +// new creates a new HeroRunner instance +pub fn new() !&HeroRunner { + mut redis := redisclient.core_get()! + + mut hr := &HeroRunner{ + redis: redis + jobs: &JobManager{ + redis: redis + } + agents: &AgentManager{ + redis: redis + } + services: &ServiceManager{ + redis: redis + } + groups: &GroupManager{ + redis: redis + } + } + + return hr +} diff --git a/lib/core/jobs/group.v b/lib/core/jobs/group.v new file mode 100644 index 00000000..75bde6ac --- /dev/null +++ b/lib/core/jobs/group.v @@ -0,0 +1,10 @@ +module jobs + +// Group represents a collection of members (users or other groups) +pub struct Group { +pub mut: + guid string // unique id + name string // name of the group + description string // optional description + members []string // can be other group or member which is defined by pubkey +} diff --git a/lib/core/jobs/group_manager.v b/lib/core/jobs/group_manager.v new file mode 100644 index 00000000..e6182b6f --- /dev/null +++ b/lib/core/jobs/group_manager.v @@ -0,0 +1,104 @@ +module jobs + +import freeflowuniverse.herolib.core.redisclient +import json + +const ( + groups_key = 'herorunner:groups' // Redis key for storing groups +) + +// GroupManager handles all group-related operations +pub struct GroupManager { +mut: + redis &redisclient.Redis +} + +// new creates a new Group instance +pub fn (mut m GroupManager) new() Group { + return Group{ + guid: '' // Empty GUID to be filled by caller + members: []string{} + } +} + +// add adds a new group to Redis +pub fn (mut m GroupManager) set(group Group) ! { + // Store group in Redis hash where key is group.guid and value is JSON of group + group_json := json.encode(group) + m.redis.hset(groups_key, group.guid, group_json)! +} + +// get retrieves a group by its GUID +pub fn (mut m GroupManager) get(guid string) !Group { + group_json := m.redis.hget(groups_key, guid)! + return json.decode(Group, group_json) +} + +// list returns all groups +pub fn (mut m GroupManager) list() ![]Group { + mut groups := []Group{} + + // Get all groups from Redis hash + groups_map := m.redis.hgetall(groups_key)! + + // Convert each JSON value to Group struct + for _, group_json in groups_map { + group := json.decode(Group, group_json)! + groups << group + } + + return groups +} +// delete removes a group by its GUID +pub fn (mut m GroupManager) delete(guid string) ! { + m.redis.hdel(groups_key, guid)! +} + +// add_member adds a member (user pubkey or group GUID) to a group +pub fn (mut m GroupManager) add_member(guid string, member string) ! { + mut group := m.get(guid)! + if member !in group.members { + group.members << member + m.update(group)! + } +} + +// remove_member removes a member from a group +pub fn (mut m GroupManager) remove_member(guid string, member string) ! { + mut group := m.get(guid)! + group.members = group.members.filter(it != member) + m.update(group)! +} + +// get_user_groups returns all groups that a user is a member of (directly or indirectly) +pub fn (mut m GroupManager) get_user_groups(user_pubkey string) ![]Group { + mut user_groups := []Group{} + mut checked_groups := map[string]bool{} + + groups := m.list()! + + // Recursive function to check group membership + fn check_group_membership(group Group, user string, groups []Group, mut checked map[string]bool, mut result []Group) { + if group.guid in checked { + return + } + checked[group.guid] = true + + if user in group.members { + result << group + // Check parent groups + for parent_group in groups { + if group.guid in parent_group.members { + check_group_membership(parent_group, user, groups, mut checked, mut result) + } + } + } + } + + // Check each group + for group in groups { + check_group_membership(group, user_pubkey, groups, mut checked_groups, mut user_groups) + } + + return user_groups +} diff --git a/lib/core/jobs/group_manager_test.v b/lib/core/jobs/group_manager_test.v new file mode 100644 index 00000000..b8e2c3b4 --- /dev/null +++ b/lib/core/jobs/group_manager_test.v @@ -0,0 +1,67 @@ +module jobs + +import freeflowuniverse.herolib.core.redisclient + +fn test_runner.groups() { + mut runner:=new()! + + // Create a new group using the manager + mut group := runner.groups.new() + group.guid = 'admin-group' + group.name = 'Administrators' + group.description = 'Administrator group with full access' + + // Add the group + runner.groups.set(group)! + + // Create a subgroup + mut subgroup := runner.groups.new() + subgroup.guid = 'vm-admins' + subgroup.name = 'VM Administrators' + subgroup.description = 'VM management administrators' + + runner.groups.add(subgroup)! + + // Add subgroup to main group + runner.groups.add_member(group.guid, subgroup.guid)! + + // Add a user to the subgroup + runner.groups.add_member(subgroup.guid, 'user-1-pubkey')! + + // Get the groups and verify fields + retrieved_group := runner.groups.get(group.guid)! + assert retrieved_group.guid == group.guid + assert retrieved_group.name == group.name + assert retrieved_group.description == group.description + assert retrieved_group.members.len == 1 + assert retrieved_group.members[0] == subgroup.guid + + retrieved_subgroup := runner.groups.get(subgroup.guid)! + assert retrieved_subgroup.members.len == 1 + assert retrieved_subgroup.members[0] == 'user-1-pubkey' + + // Test recursive group membership + user_groups := runner.groups.get_user_groups('user-1-pubkey')! + assert user_groups.len == 1 + assert user_groups[0].guid == subgroup.guid + + // Remove member from subgroup + runner.groups.remove_member(subgroup.guid, 'user-1-pubkey')! + updated_subgroup := runner.groups.get(subgroup.guid)! + assert updated_subgroup.members.len == 0 + + // List all groups + groups := runner.groups.list()! + assert groups.len == 2 + + // Delete the groups + runner.groups.delete(subgroup.guid)! + runner.groups.delete(group.guid)! + + // Verify deletion + groups_after := runner.groups.list()! + for g in groups_after { + assert g.guid != group.guid + assert g.guid != subgroup.guid + } +} diff --git a/lib/core/jobs/job.v b/lib/core/jobs/job.v new file mode 100644 index 00000000..e122832f --- /dev/null +++ b/lib/core/jobs/job.v @@ -0,0 +1,52 @@ +module jobs + +import freeflowuniverse.herolib.data.ourtime + +// Job represents a task to be executed by an agent +pub struct Job { +pub mut: + guid string // unique id for the job + agents []string // the pub key of the agent(s) which will execute the command, only 1 will execute + source string // pubkey from the agent who asked for the job + circle string = 'default' // our digital life is organized in circles + context string = 'default' // is the high level context in which actors will execute the work inside a circle + actor string // e.g. vm_manager + action string // e.g. start + params map[string]string // e.g. id:10 + timeout_schedule u16 = 60 // timeout before its picked up + timeout u16 = 3600 // timeout in sec + log bool = true + ignore_error bool // means if error will just exit and not raise, there will be no error reporting + ignore_error_codes []int // of we want to ignore certain error codes + debug bool // if debug will get more context + retry int // default there is no debug + status JobStatus + dependencies []JobDependency // will not execute until other jobs are done +} + +// JobStatus represents the current state of a job +pub struct JobStatus { +pub mut: + guid string // unique id for the job + created ourtime.Time // when we created the job + start ourtime.Time // when the job needs to start + end ourtime.Time // when the job ended, can be in error + status Status // current status of the job +} + +// JobDependency represents a dependency on another job +pub struct JobDependency { +pub mut: + guid string // unique id for the job + agents []string // the pub key of the agent(s) which can execute the command +} + +// Status represents the possible states of a job +pub enum Status { + created // initial state + scheduled // job has been scheduled + planned // arrived where actor will execute the job + running // job is currently running + error // job encountered an error + ok // job completed successfully +} diff --git a/lib/core/jobs/job_manager.v b/lib/core/jobs/job_manager.v new file mode 100644 index 00000000..2c383ad2 --- /dev/null +++ b/lib/core/jobs/job_manager.v @@ -0,0 +1,70 @@ +module jobs + +import freeflowuniverse.herolib.core.redisclient +import freeflowuniverse.herolib.data.ourtime +import json + +const ( + jobs_key = 'herorunner:jobs' // Redis key for storing jobs +) + +// JobManager handles all job-related operations +pub struct JobManager { +mut: + redis &redisclient.Redis +} + +// new creates a new Job instance +pub fn (mut m JobManager) new() Job { + return Job{ + guid: '' // Empty GUID to be filled by caller + status: JobStatus{ + guid: '' + created: ourtime.Time{} + start: ourtime.Time{} + end: ourtime.Time{} + status: .created + } + } +} + +// add adds a new job to Redis +pub fn (mut m JobManager) set(job Job) ! { + // Store job in Redis hash where key is job.guid and value is JSON of job + job_json := json.encode(job) + m.redis.hset(jobs_key, job.guid, job_json)! +} + +// get retrieves a job by its GUID +pub fn (mut m JobManager) get(guid string) !Job { + job_json := m.redis.hget(jobs_key, guid)! + return json.decode(Job, job_json) +} + +// list returns all jobs +pub fn (mut m JobManager) list() ![]Job { + mut jobs := []Job{} + + // Get all jobs from Redis hash + jobs_map := m.redis.hgetall(jobs_key)! + + // Convert each JSON value to Job struct + for _, job_json in jobs_map { + job := json.decode(Job, job_json)! + jobs << job + } + + return jobs +} + +// delete removes a job by its GUID +pub fn (mut m JobManager) delete(guid string) ! { + m.redis.hdel(jobs_key, guid)! +} + +// update_status updates just the status of a job +pub fn (mut m JobManager) update_status(guid string, status Status) ! { + mut job := m.get(guid)! + job.status.status = status + m.update(job)! +} diff --git a/lib/core/jobs/job_manager_test.v b/lib/core/jobs/job_manager_test.v new file mode 100644 index 00000000..cb551366 --- /dev/null +++ b/lib/core/jobs/job_manager_test.v @@ -0,0 +1,47 @@ +module jobs + +import freeflowuniverse.herolib.core.redisclient +import freeflowuniverse.herolib.data.ourtime + +fn test_runner.jobs() { + mut runner:=new()! + + // Create a new job using the manager + mut job := runner.jobs.new() + job.guid = 'test-job-1' + job.actor = 'vm_manager' + job.action = 'start' + job.params = { + 'id': '10' + } + + // Add the job + runner.jobs.set(job)! + + // Get the job and verify fields + retrieved_job := runner.jobs.get(job.guid)! + assert retrieved_job.guid == job.guid + assert retrieved_job.actor == job.actor + assert retrieved_job.action == job.action + assert retrieved_job.params['id'] == job.params['id'] + assert retrieved_job.status.status == .created + + // Update job status + runner.jobs.update_status(job.guid, .running)! + updated_job := runner.jobs.get(job.guid)! + assert updated_job.status.status == .running + + // List all jobs + jobs := runner.jobs.list()! + assert jobs.len > 0 + assert jobs[0].guid == job.guid + + // Delete the job + runner.jobs.delete(job.guid)! + + // Verify deletion + jobs_after := runner.jobs.list()! + for j in jobs_after { + assert j.guid != job.guid + } +} diff --git a/lib/core/jobs/service.v b/lib/core/jobs/service.v new file mode 100644 index 00000000..5260df6d --- /dev/null +++ b/lib/core/jobs/service.v @@ -0,0 +1,44 @@ +module jobs + +// Service represents a service that can be provided by agents +pub struct Service { +pub mut: + actor string // name of the actor providing the service + actions []ServiceAction // available actions for this service + description string // optional description + status ServiceState // current state of the service + acl ?ACL // access control list for the service +} + +// ServiceAction represents an action that can be performed by a service +pub struct ServiceAction { +pub mut: + action string // which action + description string // optional description + params map[string]string // e.g. name:'name of the vm' ... + params_example map[string]string // e.g. name:'myvm' + acl ?ACL // if not used then everyone can use +} + +// ACL represents an access control list +pub struct ACL { +pub mut: + name string + ace []ACE +} + +// ACE represents an access control entry +pub struct ACE { +pub mut: + groups []string // guid's of the groups who have access + users []string // in case groups are not used then is users + right string // e.g. read, write, admin, block +} + +// ServiceState represents the possible states of a service +pub enum ServiceState { + ok // service is functioning normally + down // service is not available + error // service encountered an error + halted // service has been manually stopped +} diff --git a/lib/core/jobs/service_manager.v b/lib/core/jobs/service_manager.v new file mode 100644 index 00000000..863b63de --- /dev/null +++ b/lib/core/jobs/service_manager.v @@ -0,0 +1,124 @@ +module jobs + +import freeflowuniverse.herolib.core.redisclient +import json + +const ( + services_key = 'herorunner:services' // Redis key for storing services +) + +// ServiceManager handles all service-related operations +pub struct ServiceManager { +mut: + redis &redisclient.Redis +} + +// new creates a new Service instance +pub fn (mut m ServiceManager) new() Service { + return Service{ + actor: '' // Empty actor name to be filled by caller + actions: []ServiceAction{} + status: .ok + } +} + +// add adds a new service to Redis +pub fn (mut m ServiceManager) set(service Service) ! { + // Store service in Redis hash where key is service.actor and value is JSON of service + service_json := json.encode(service) + m.redis.hset(services_key, service.actor, service_json)! +} + +// get retrieves a service by its actor name +pub fn (mut m ServiceManager) get(actor string) !Service { + service_json := m.redis.hget(services_key, actor)! + return json.decode(Service, service_json) +} + +// list returns all services +pub fn (mut m ServiceManager) list() ![]Service { + mut services := []Service{} + + // Get all services from Redis hash + services_map := m.redis.hgetall(services_key)! + + // Convert each JSON value to Service struct + for _, service_json in services_map { + service := json.decode(Service, service_json)! + services << service + } + + return services +} + +// delete removes a service by its actor name +pub fn (mut m ServiceManager) delete(actor string) ! { + m.redis.hdel(services_key, actor)! +} + +// update_status updates just the status of a service +pub fn (mut m ServiceManager) update_status(actor string, status ServiceState) ! { + mut service := m.get(actor)! + service.status = status + m.update(service)! +} + +// get_by_action returns all services that provide a specific action +pub fn (mut m ServiceManager) get_by_action(action string) ![]Service { + mut matching_services := []Service{} + + services := m.list()! + for service in services { + for act in service.actions { + if act.action == action { + matching_services << service + break + } + } + } + + return matching_services +} + +// check_access verifies if a user has access to a service action +pub fn (mut m ServiceManager) check_access(actor string, action string, user_pubkey string, groups []string) !bool { + service := m.get(actor)! + + // Find the specific action + mut service_action := ServiceAction{} + mut found := false + for act in service.actions { + if act.action == action { + service_action = act + found = true + break + } + } + if !found { + return error('Action ${action} not found in service ${actor}') + } + + // If no ACL is defined, access is granted + if service_action.acl == none { + return true + } + + acl := service_action.acl or { return true } + + // Check each ACE in the ACL + for ace in acl.ace { + // Check if user is directly listed + if user_pubkey in ace.users { + return ace.right != 'block' + } + + // Check if any of user's groups are listed + for group in groups { + if group in ace.groups { + return ace.right != 'block' + } + } + } + + return false +} diff --git a/lib/core/jobs/service_manager_test.v b/lib/core/jobs/service_manager_test.v new file mode 100644 index 00000000..aa997bad --- /dev/null +++ b/lib/core/jobs/service_manager_test.v @@ -0,0 +1,84 @@ +module jobs + +import freeflowuniverse.herolib.core.redisclient + +fn test_runner.services() { + mut runner:=new()! + + // Create a new service using the manager + mut service := runner.services.new() + service.actor = 'vm_manager' + service.description = 'VM Management Service' + + // Create an ACL + mut ace := ACE{ + groups: ['admin-group'] + users: ['user-1-pubkey'] + right: 'write' + } + + mut acl := ACL{ + name: 'vm-acl' + ace: [ace] + } + + // Create a service action + mut action := ServiceAction{ + action: 'start' + description: 'Start a VM' + params: { + 'name': 'string' + } + params_example: { + 'name': 'myvm' + } + acl: acl + } + + service.actions = [action] + + // Add the service + runner.services.set(service)! + + // Get the service and verify fields + retrieved_service := runner.services.get(service.actor)! + assert retrieved_service.actor == service.actor + assert retrieved_service.description == service.description + assert retrieved_service.actions.len == 1 + assert retrieved_service.actions[0].action == 'start' + assert retrieved_service.status == .ok + + // Update service status + runner.services.update_status(service.actor, .down)! + updated_service := runner.services.get(service.actor)! + assert updated_service.status == .down + + // Test get_by_action + services := runner.services.get_by_action('start')! + assert services.len > 0 + assert services[0].actor == service.actor + + // Test access control + has_access := runner.services.check_access(service.actor, 'start', 'user-1-pubkey', [])! + assert has_access == true + + has_group_access := runner.services.check_access(service.actor, 'start', 'user-2-pubkey', ['admin-group'])! + assert has_group_access == true + + no_access := runner.services.check_access(service.actor, 'start', 'user-3-pubkey', [])! + assert no_access == false + + // List all services + all_services := runner.services.list()! + assert all_services.len > 0 + assert all_services[0].actor == service.actor + + // Delete the service + runner.services.delete(service.actor)! + + // Verify deletion + services_after := runner.services.list()! + for s in services_after { + assert s.actor != service.actor + } +} diff --git a/lib/core/jobs/specs.md b/lib/core/jobs/specs.md new file mode 100644 index 00000000..9d5964f6 --- /dev/null +++ b/lib/core/jobs/specs.md @@ -0,0 +1,186 @@ +create a job manager in +lib/core/jobs + + +## some definitions + +- agent: is a self contained set of processes which can execute on actions or actions to be executed by others +- action: what needs to be executed +- circle: each action happens in a circle +- context: a context inside a circle is optional +- job, what gets executed by an agent, is one action, can depend on other actions +- herorunner: is the process which uses redis to manage all open jobs, checks for timeouts, does the forwards if needed (if remote agent need to schedule, ...) + +## jobs + +are executed by processes can be in different languages and they are identified by agent pub key (the one who executes) +as part of heroscript we know what to executed on which actor inside the agent, defined with method and its arguments + +```v + +//the description of what needs to be executed +pub struct Job { +pub mut: + guid string //unique id for the job + agents []string //the pub key of the agent(s) which will execute the command, only 1 will execute, the herorunner will try the different agents if needed till it has success + source string //pubkey from the agent who asked for the job + circle string = "default" //our digital life is organized in circles + context string = "default" //is the high level context in which actors will execute the work inside a circle + actor string //e.g. vm_manager + action string //e.g. start + params map[string]string //e.g. id:10 + timeout_schedule u16 = 60 //timeout before its picked up + timeout u16 = 3600 // timeout in sec + log bool = true + ignore_error bool // means if error will just exit and not raise, there will be no error reporting + ignore_error_codes []int // of we want to ignore certain error codes + debug bool // if debug will get more context + retry int // default there is no debug + status JobStatus + dependencies []JobDependency //will not execute untill other jobs are done + +} + +pub struct JobStatus { +pub mut: + guid string //unique id for the job + created u32 //epoch when we created the job + start u32 //epoch when the job needs to start + end u32 //epoch when the job ended, can be in error + status //ENUM: create scheduled, planned (means arrived where actor will execute the job), running, error, ok +} + +pub struct JobDependency { +pub mut: + guid string //unique id for the job + agents []string //the pub key of the agent(s) which can execute the command +} + + + +``` + +the Job object is stored in redis in hset herorunner:jobs where key is the job guid and the val is the json of Job + +## Agent Registration Services + +Each agent (the one who hosts the different actors which execute the methods with params) register themselves to all participants. + +the structs below are available to everyone and are public + +```v + +pub struct Agent { +pub mut: + pubkey string //pubkey using ed25519 + address string //where we can gind the agent + port int //default 9999 + description string //optional + status AgentStatus + services []AgentService //these are the public services + signature string //signature as done by private key of $address+$port+$description+$status (this allows everyone to verify that the data is ok) + + +} + +pub struct AgentStatus { +pub mut: + guid string //unique id for the job + timestamp_first u32 //when agent came online + timestamp_last u32 //last time agent let us know that he is working + status //ENUM: ok, down, error, halted +} + +pub struct AgentService { +pub mut: + actor string + actions []AgentServiceAction + description string + status //ENUM: ok, down, error, halted +} + +pub struct AgentServiceAction { +pub mut: + action string //which action + description string //optional descroption + params map[string]string //e.g. name:'name of the vm' ... + params_example map[string]string // e.g. name:'myvm' + status //ENUM: ok, down, error, halted + public bool //if everyone can use then true, if restricted means only certain people can use +} + + + + + +``` + +the Agent object is stored in redis in hset herorunner:agents where key is the agent pubkey and the val is the json of Agent + + +### Services Info + +The agent and its actors register their capability to the herorunner + +We have a mechanism to be specific on who can execute which, this is sort of ACL system, for now its quite rough + + + +```v + +pub struct Group { +pub mut: + guid string //unique id + name string + description string + members []string //can be other group or member which is defined by pubkey +} + + +``` + +this info is stored in in redis on herorunner:groups + + + +```v + +pub struct Service { +pub mut: + actor string + actions []AgentServiceAction + description string + status //ENUM: ok, down, error, halted + acl ?ACL +} + +pub struct ServiceAction { +pub mut: + action string //which action + description string //optional descroption + params map[string]string //e.g. name:'name of the vm' ... + params_example map[string]string // e.g. name:'myvm' + acl ?ACL //if not used then everyone can use +} + +pub struct ACL { +pub mut: + name string + ace []ACE +} + + +pub struct ACE { +pub mut: + groups []string //guid's of the groups who have access + users []string //in case groups are not used then is users + right string e.g. read, write, admin, block +} + + + + +``` + +The info for the herorunner to function is in redis on herorunner:services +