diff --git a/lib/core/jobs/model/agent.v b/lib/core/jobs/model/agent.v index 0bf4d395..5da454cc 100644 --- a/lib/core/jobs/model/agent.v +++ b/lib/core/jobs/model/agent.v @@ -5,10 +5,10 @@ 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 + 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 @@ -17,30 +17,30 @@ pub mut: // 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 + guid string // unique id for the job + timestamp_first ourtime.OurTime // when agent came online + timestamp_last ourtime.OurTime // 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 + 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 + 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 diff --git a/lib/core/jobs/model/agent_manager.v b/lib/core/jobs/model/agent_manager.v index dc292757..350157da 100644 --- a/lib/core/jobs/model/agent_manager.v +++ b/lib/core/jobs/model/agent_manager.v @@ -4,9 +4,7 @@ import freeflowuniverse.herolib.core.redisclient import freeflowuniverse.herolib.data.ourtime import json -const ( - agents_key = 'herorunner:agents' // Redis key for storing agents -) +const agents_key = 'herorunner:agents' // Redis key for storing agents // AgentManager handles all agent-related operations pub struct AgentManager { @@ -17,13 +15,13 @@ mut: // 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 + pubkey: '' // Empty pubkey to be filled by caller + port: 9999 // Default port + status: AgentStatus{ + guid: '' + timestamp_first: ourtime.now() + timestamp_last: ourtime.OurTime{} + status: .ok } services: []AgentService{} } @@ -45,16 +43,16 @@ pub fn (mut m AgentManager) get(pubkey string) !Agent { // 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 } @@ -67,13 +65,13 @@ pub fn (mut m AgentManager) delete(pubkey string) ! { pub fn (mut m AgentManager) update_status(pubkey string, status AgentState) ! { mut agent := m.get(pubkey)! agent.status.status = status - m.update(agent)! + m.set(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 { @@ -88,6 +86,6 @@ pub fn (mut m AgentManager) get_by_service(actor string, action string) ![]Agent } } } - + return matching_agents } diff --git a/lib/core/jobs/model/agent_manager_test.v b/lib/core/jobs/model/agent_manager_test.v index 580437d4..742e03a9 100644 --- a/lib/core/jobs/model/agent_manager_test.v +++ b/lib/core/jobs/model/agent_manager_test.v @@ -4,42 +4,41 @@ import freeflowuniverse.herolib.core.redisclient import freeflowuniverse.herolib.data.ourtime fn test_agents_model() { - - mut runner:=new()! + mut runner := new()! // Create a new agent using the manager - mut agent := runner.agents.new() + 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: { + action: 'start' + description: 'Start a VM' + params: { 'name': 'string' } params_example: { 'name': 'myvm' } - status: .ok - public: true + status: .ok + public: true } - + // Create a service mut service := AgentService{ - actor: 'vm_manager' - actions: [action] + actor: 'vm_manager' + actions: [action] description: 'VM Management Service' - status: .ok + 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 @@ -48,25 +47,25 @@ fn test_agents_model() { 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 { diff --git a/lib/core/jobs/model/factory.v b/lib/core/jobs/model/factory.v index cd5fa043..7a697472 100644 --- a/lib/core/jobs/model/factory.v +++ b/lib/core/jobs/model/factory.v @@ -7,31 +7,31 @@ pub struct HeroRunner { mut: redis &redisclient.Redis pub mut: - jobs &JobManager - agents &AgentManager + jobs &JobManager + agents &AgentManager services &ServiceManager - groups &GroupManager + 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 + jobs: &JobManager{ redis: redis } - agents: &AgentManager{ + agents: &AgentManager{ redis: redis } services: &ServiceManager{ redis: redis } - groups: &GroupManager{ + groups: &GroupManager{ redis: redis } } - + return hr } diff --git a/lib/core/jobs/model/group_manager.v b/lib/core/jobs/model/group_manager.v index 1bf8fccf..ba7f94f2 100644 --- a/lib/core/jobs/model/group_manager.v +++ b/lib/core/jobs/model/group_manager.v @@ -3,9 +3,7 @@ module model import freeflowuniverse.herolib.core.redisclient import json -const ( - groups_key = 'herorunner:groups' // Redis key for storing groups -) +const groups_key = 'herorunner:groups' // Redis key for storing groups // GroupManager handles all group-related operations pub struct GroupManager { @@ -16,7 +14,7 @@ mut: // new creates a new Group instance pub fn (mut m GroupManager) new() Group { return Group{ - guid: '' // Empty GUID to be filled by caller + guid: '' // Empty GUID to be filled by caller members: []string{} } } @@ -37,18 +35,19 @@ pub fn (mut m GroupManager) get(guid string) !Group { // 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)! @@ -59,7 +58,7 @@ 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)! + m.set(group)! } } @@ -67,38 +66,34 @@ pub fn (mut m GroupManager) add_member(guid string, member string) ! { 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)! + m.set(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 } + +// 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) + } + } + } +} diff --git a/lib/core/jobs/model/group_manager_test.v b/lib/core/jobs/model/group_manager_test.v index 943e84c1..24e08716 100644 --- a/lib/core/jobs/model/group_manager_test.v +++ b/lib/core/jobs/model/group_manager_test.v @@ -3,31 +3,31 @@ module model import freeflowuniverse.herolib.core.redisclient fn test_groups() { - mut runner:=new()! + 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)! - + + runner.groups.set(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 @@ -35,29 +35,29 @@ fn test_groups() { 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 { diff --git a/lib/core/jobs/model/job.v b/lib/core/jobs/model/job.v index 4448d04f..4819ece2 100644 --- a/lib/core/jobs/model/job.v +++ b/lib/core/jobs/model/job.v @@ -5,33 +5,33 @@ 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 + 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 + 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 + 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 + 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 + guid string // unique id for the job + created ourtime.OurTime // when we created the job + start ourtime.OurTime // when the job needs to start + end ourtime.OurTime // when the job ended, can be in error + status Status // current status of the job } // JobDependency represents a dependency on another job diff --git a/lib/core/jobs/model/job_manager.v b/lib/core/jobs/model/job_manager.v index 1ba06556..66e15999 100644 --- a/lib/core/jobs/model/job_manager.v +++ b/lib/core/jobs/model/job_manager.v @@ -4,9 +4,7 @@ import freeflowuniverse.herolib.core.redisclient import freeflowuniverse.herolib.data.ourtime import json -const ( - jobs_key = 'herorunner:jobs' // Redis key for storing jobs -) +const jobs_key = 'herorunner:jobs' // Redis key for storing jobs // JobManager handles all job-related operations pub struct JobManager { @@ -17,13 +15,13 @@ mut: // new creates a new Job instance pub fn (mut m JobManager) new() Job { return Job{ - guid: '' // Empty GUID to be filled by caller + guid: '' // Empty GUID to be filled by caller status: JobStatus{ - guid: '' - created: ourtime.Time{} - start: ourtime.Time{} - end: ourtime.Time{} - status: .created + guid: '' + created: ourtime.now() + start: ourtime.OurTime{} + end: ourtime.OurTime{} + status: .created } } } @@ -44,16 +42,16 @@ pub fn (mut m JobManager) get(guid string) !Job { // 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 } @@ -66,5 +64,5 @@ pub fn (mut m JobManager) delete(guid string) ! { pub fn (mut m JobManager) update_status(guid string, status Status) ! { mut job := m.get(guid)! job.status.status = status - m.update(job)! + m.set(job)! } diff --git a/lib/core/jobs/model/job_manager_test.v b/lib/core/jobs/model/job_manager_test.v index f24df5ea..f20b18bd 100644 --- a/lib/core/jobs/model/job_manager_test.v +++ b/lib/core/jobs/model/job_manager_test.v @@ -4,7 +4,7 @@ import freeflowuniverse.herolib.core.redisclient import freeflowuniverse.herolib.data.ourtime fn test_jobs() { - mut runner:=new()! + mut runner := new()! // Create a new job using the manager mut job := runner.jobs.new() @@ -14,10 +14,10 @@ fn test_jobs() { 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 @@ -25,20 +25,20 @@ fn test_jobs() { 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 { diff --git a/lib/core/jobs/model/service.v b/lib/core/jobs/model/service.v index 38bd04d4..589d59d9 100644 --- a/lib/core/jobs/model/service.v +++ b/lib/core/jobs/model/service.v @@ -3,21 +3,21 @@ module model // 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 + 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 + 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 diff --git a/lib/core/jobs/model/service_manager.v b/lib/core/jobs/model/service_manager.v index c1c8d86f..322fc718 100644 --- a/lib/core/jobs/model/service_manager.v +++ b/lib/core/jobs/model/service_manager.v @@ -3,9 +3,7 @@ module model import freeflowuniverse.herolib.core.redisclient import json -const ( - services_key = 'herorunner:services' // Redis key for storing services -) +const services_key = 'herorunner:services' // Redis key for storing services // ServiceManager handles all service-related operations pub struct ServiceManager { @@ -16,9 +14,9 @@ mut: // new creates a new Service instance pub fn (mut m ServiceManager) new() Service { return Service{ - actor: '' // Empty actor name to be filled by caller + actor: '' // Empty actor name to be filled by caller actions: []ServiceAction{} - status: .ok + status: .ok } } @@ -38,16 +36,16 @@ pub fn (mut m ServiceManager) get(actor string) !Service { // 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 } @@ -60,13 +58,13 @@ pub fn (mut m ServiceManager) delete(actor string) ! { pub fn (mut m ServiceManager) update_status(actor string, status ServiceState) ! { mut service := m.get(actor)! service.status = status - m.update(service)! + m.set(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 { @@ -76,14 +74,14 @@ pub fn (mut m ServiceManager) get_by_action(action string) ![]Service { } } } - + 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 @@ -97,21 +95,21 @@ pub fn (mut m ServiceManager) check_access(actor string, action string, user_pub 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 { @@ -119,6 +117,6 @@ pub fn (mut m ServiceManager) check_access(actor string, action string, user_pub } } } - + return false } diff --git a/lib/core/jobs/model/service_manager_test.v b/lib/core/jobs/model/service_manager_test.v index e97b7d8f..fe3718ce 100644 --- a/lib/core/jobs/model/service_manager_test.v +++ b/lib/core/jobs/model/service_manager_test.v @@ -3,43 +3,43 @@ module model import freeflowuniverse.herolib.core.redisclient fn test_services() { - mut runner:=new()! + 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' + users: ['user-1-pubkey'] + right: 'write' } - + mut acl := ACL{ name: 'vm-acl' - ace: [ace] + ace: [ace] } - + // Create a service action mut action := ServiceAction{ - action: 'start' - description: 'Start a VM' - params: { + action: 'start' + description: 'Start a VM' + params: { 'name': 'string' } params_example: { 'name': 'myvm' } - acl: acl + 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 @@ -47,35 +47,38 @@ fn test_services() { 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', [])! + 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'])! + + 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', [])! + + 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 {