From e289365ce84943e4ef039d1b96250f7a7979bc7a Mon Sep 17 00:00:00 2001 From: lorsan Date: Sun, 19 Apr 2026 16:16:03 +0300 Subject: [PATCH] feat: create save new agent in db --- internal/agent/service/agent_service/agent.go | 4 +- .../agent/service/agent_service/agent_test.go | 25 +++++++ internal/domain/{structure.go => agent.go} | 0 internal/hub/app/app.go | 24 ++++-- internal/hub/domain/structure.go | 29 +++++++ internal/hub/rpc/server.go | 10 ++- internal/hub/service/hub_service/hub.go | 61 +++++++++------ internal/hub/service/hub_service/mapper.go | 18 +++++ internal/hub/store/mapper.go | 69 +++++++++++++++++ internal/hub/store/sqlc/gen/agent.sql.go | 75 +++++++++++++++++++ internal/hub/store/sqlc/gen/models.go | 2 +- internal/hub/store/sqlc/queries/agent.sql | 15 +++- internal/hub/store/store.go | 17 ++++- internal/hub/store/structure.go | 37 --------- 14 files changed, 314 insertions(+), 72 deletions(-) create mode 100644 internal/agent/service/agent_service/agent_test.go rename internal/domain/{structure.go => agent.go} (100%) create mode 100644 internal/hub/domain/structure.go create mode 100644 internal/hub/service/hub_service/mapper.go create mode 100644 internal/hub/store/mapper.go delete mode 100644 internal/hub/store/structure.go diff --git a/internal/agent/service/agent_service/agent.go b/internal/agent/service/agent_service/agent.go index 391c62d..af9f050 100644 --- a/internal/agent/service/agent_service/agent.go +++ b/internal/agent/service/agent_service/agent.go @@ -10,6 +10,8 @@ import ( "github.com/rs/zerolog" ) +const AgentVersion = "0.0" + type Collector interface { GatherInfoSystem() (domain.HostInfo, []domain.Capability) } @@ -43,7 +45,7 @@ func (a *AgentService) RegisterAgentConn(ctx context.Context) { info, caps := a.collect.GatherInfoSystem() AgentID := a.settings.AgentID AgentName := a.cfg.AppName - AgentData := domain.RegisterAgentRequest{AgentId: AgentID, AgentName: AgentName, Host: info, Capabilities: caps} + AgentData := domain.RegisterAgentRequest{AgentId: AgentID, AgentName: AgentName, Host: info, Capabilities: caps, AgentVersion: AgentVersion} data, err := a.conn.RegisterAgent(ctx, AgentData) if err != nil { diff --git a/internal/agent/service/agent_service/agent_test.go b/internal/agent/service/agent_service/agent_test.go new file mode 100644 index 0000000..b6852ac --- /dev/null +++ b/internal/agent/service/agent_service/agent_test.go @@ -0,0 +1,25 @@ +package agent_service + +import ( + "context" + + "github.com/lorsanstand/HomeOps-Hub/internal/domain" +) + +type CollectorMock struct { + host domain.HostInfo + caps []domain.Capability +} + +func (c *CollectorMock) GatherInfoSystem() (domain.HostInfo, []domain.Capability) { + return c.host, c.caps +} + +type ConnectionMock struct { + regAgentErr error + regResp domain.RegisterAgentResponse +} + +func (c *ConnectionMock) RegisterAgent(ctx context.Context, RegisterData domain.RegisterAgentRequest) (domain.RegisterAgentResponse, error) { + return c.regResp, c.regAgentErr +} diff --git a/internal/domain/structure.go b/internal/domain/agent.go similarity index 100% rename from internal/domain/structure.go rename to internal/domain/agent.go diff --git a/internal/hub/app/app.go b/internal/hub/app/app.go index 0153c0f..56652b0 100644 --- a/internal/hub/app/app.go +++ b/internal/hub/app/app.go @@ -36,55 +36,65 @@ func NewApp() *App { func (a *App) Run() { ctx := context.Background() + a.log.Info().Str("host", a.cfg.DBHost).Int("port", a.cfg.DBPort).Msg("connecting to database") migratePGConn, err := sql.Open("pgx", a.cfg.GetURLPostgres()) if err != nil { - a.log.Error().Err(err).Msg("failed to connect to the database") + a.log.Error().Err(err).Msg("failed to connect to the database for migrations") return } defer migratePGConn.Close() mgrt, err := migrator.NewMigrator(hubdir.MigrationsFS, "migrations") if err != nil { - a.log.Error().Err(err).Msg("failed create migrator") + a.log.Error().Err(err).Msg("failed to create migrator") return } + a.log.Info().Msg("applying database migrations") if err = mgrt.ApplyMigrations(migratePGConn); err != nil { - a.log.Error().Err(err).Msg("migrations were not applied") + a.log.Error().Err(err).Msg("migrations failed to apply") return } + a.log.Info().Msg("migrations applied successfully") migratePGConn.Close() + a.log.Info().Msg("creating database connection pool") pool, err := pgxpool.New(ctx, a.cfg.GetURLPostgres()) if err != nil { - a.log.Error().Err(err).Msg("failed create db pool") + a.log.Error().Err(err).Msg("failed to create database connection pool") return } + defer pool.Close() + a.log.Info().Msg("database connection pool created") hubStore := store.NewHubStore(pool) - hubService := hub_service.NewHubService(hubStore, a.log) + a.log.Info().Msg("starting hub service") err = a.hubServe(hubService) if err != nil { - a.log.Error().Err(err).Msg("failed to start the server") + a.log.Error().Err(err).Msg("hub service failed to start") return } } func (a *App) hubServe(hubService *hub_service.HubService) error { address := fmt.Sprintf("0.0.0.0:%v", a.cfg.Port) - a.log.Info().Str("address", "http://"+address).Msg("start GRPC server") + a.log.Info().Str("address", address).Msg("starting gRPC server") server := grpcserv.NewHubHandler(hubService, a.log) lis, err := net.Listen("tcp", address) if err != nil { + a.log.Error().Err(err).Str("address", address).Msg("failed to listen on address") return err } + a.log.Info().Str("address", address).Msg("listening on address") + a.log.Info().Msg("gRPC server is running") err = server.GrpcServer.Serve(lis) if err != nil { + a.log.Error().Err(err).Msg("gRPC server error") return err } diff --git a/internal/hub/domain/structure.go b/internal/hub/domain/structure.go new file mode 100644 index 0000000..795fe89 --- /dev/null +++ b/internal/hub/domain/structure.go @@ -0,0 +1,29 @@ +package domain + +import ( + "time" + + "github.com/lorsanstand/HomeOps-Hub/internal/domain" +) + +type CreateAgentModel struct { + AgentID string + AgentName string + Architecture string + System string + Hostname string + Version string + Capabilities []domain.Capability +} + +type AgentModel struct { + ID int + AgentID string + AgentName string + Architecture string + System string + Hostname string + Version string + Capabilities []domain.Capability + RegisteredAt time.Time +} diff --git a/internal/hub/rpc/server.go b/internal/hub/rpc/server.go index bc629b3..1a5aade 100644 --- a/internal/hub/rpc/server.go +++ b/internal/hub/rpc/server.go @@ -33,12 +33,18 @@ func NewHubHandler(HubServ HubService, logger zerolog.Logger) *HubHandler { } func (h *HubHandler) Ping(ctx context.Context, _ *emptypb.Empty) (*pb.PongResponse, error) { - h.log.Info().Msg("pong request") + h.log.Debug().Msg("ping request received") return &pb.PongResponse{Pong: "Pong"}, nil } func (h *HubHandler) RegisterAgent(ctx context.Context, request *pb.RegisterAgentRequest) (*pb.RegisterAgentResponse, error) { + h.log.Debug().Str("agentId", request.AgentId).Str("agentName", request.AgentName).Msg("register agent request received") data := domain.ToDomainAgentRequest(request) resp, err := h.hub.RegisterAgent(ctx, data) - return domain.ToGRPCAgentResponse(resp), err + if err != nil { + h.log.Error().Err(err).Str("agentId", request.AgentId).Msg("register agent request failed") + return domain.ToGRPCAgentResponse(resp), err + } + h.log.Debug().Str("agentId", resp.AgentID).Msg("register agent request completed") + return domain.ToGRPCAgentResponse(resp), nil } diff --git a/internal/hub/service/hub_service/hub.go b/internal/hub/service/hub_service/hub.go index 686c505..aa86b1a 100644 --- a/internal/hub/service/hub_service/hub.go +++ b/internal/hub/service/hub_service/hub.go @@ -2,15 +2,20 @@ package hub_service import ( "context" + "database/sql" + "errors" + "fmt" "github.com/lorsanstand/HomeOps-Hub/internal/domain" - "github.com/lorsanstand/HomeOps-Hub/internal/hub/store" + domainHub "github.com/lorsanstand/HomeOps-Hub/internal/hub/domain" "github.com/lorsanstand/HomeOps-Hub/internal/hub/utils/hasher" "github.com/rs/zerolog" ) type Store interface { - NewAgent(ctx context.Context, agent store.Agent) error + NewAgent(ctx context.Context, agent domainHub.CreateAgentModel) error + GetAgentByAgentID(ctx context.Context, AgentID string) (domainHub.AgentModel, error) + UpdateAgentByID(ctx context.Context, ID int, updateAgent domainHub.CreateAgentModel) error } type HubService struct { @@ -23,31 +28,43 @@ func NewHubService(store Store, logger zerolog.Logger) *HubService { } func (h *HubService) RegisterAgent(ctx context.Context, data domain.RegisterAgentRequest) (domain.RegisterAgentResponse, error) { - h.log.Debug().Msg("registered new agent") - AgentID := data.AgentId - if data.AgentId == "" { - var err error - AgentID, err = hasher.MakeID(data.Host, data.AgentName) - if err != nil { - h.log.Error().Err(err).Msg("failed create agent id") - AgentID = "" + h.log.Debug().Str("agentId", data.AgentId).Str("agentName", data.AgentName).Msg("started registering agent") + agent, err := h.store.GetAgentByAgentID(ctx, data.AgentId) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + h.log.Error().Err(err).Str("agentId", data.AgentId).Msg("failed to get agent from database") + return domain.RegisterAgentResponse{}, fmt.Errorf("failed select agent to db: %w", err) + } + + if data.AgentId != "" && !errors.Is(err, sql.ErrNoRows) { + h.log.Debug().Str("agentId", agent.AgentID).Str("agentName", data.AgentName).Msg("agent exists, updating") + + data.AgentId = agent.AgentID + + agentStore := toCreateAgentModel(data) + + if err := h.store.UpdateAgentByID(ctx, agent.ID, agentStore); err != nil { + h.log.Error().Err(err).Str("agentId", agent.AgentID).Msg("failed to update agent in database") + return domain.RegisterAgentResponse{}, err } + h.log.Info().Str("agentId", agent.AgentID).Msg("agent updated successfully") + return domain.RegisterAgentResponse{AgentID: agent.AgentID, Heartbeat: 5}, nil } - agentStore := store.Agent{ - AgentID: AgentID, - AgentName: data.AgentName, - Architecture: data.Host.Arch, - System: data.Host.System, - Hostname: data.Host.Hostname, - Version: data.AgentVersion, - Capabilities: data.Capabilities, - } - - if err := h.store.NewAgent(ctx, agentStore); err != nil { - h.log.Warn().Err(err).Msg("failed add new agent in db") + AgentID, err := hasher.MakeID(data.Host, data.AgentName) + if err != nil { + h.log.Error().Err(err).Str("agentName", data.AgentName).Str("hostname", data.Host.Hostname).Msg("failed to generate agent id") return domain.RegisterAgentResponse{}, err } + data.AgentId = AgentID + + agentStore := toCreateAgentModel(data) + + if err := h.store.NewAgent(ctx, agentStore); err != nil { + h.log.Error().Err(err).Str("agentId", AgentID).Str("agentName", data.AgentName).Msg("failed to create new agent in database") + return domain.RegisterAgentResponse{}, err + } + + h.log.Info().Str("agentId", AgentID).Str("agentName", data.AgentName).Str("hostname", data.Host.Hostname).Msg("agent registered successfully") return domain.RegisterAgentResponse{AgentID: AgentID, Heartbeat: 5}, nil } diff --git a/internal/hub/service/hub_service/mapper.go b/internal/hub/service/hub_service/mapper.go new file mode 100644 index 0000000..6a020d8 --- /dev/null +++ b/internal/hub/service/hub_service/mapper.go @@ -0,0 +1,18 @@ +package hub_service + +import ( + "github.com/lorsanstand/HomeOps-Hub/internal/domain" + domainHub "github.com/lorsanstand/HomeOps-Hub/internal/hub/domain" +) + +func toCreateAgentModel(agent domain.RegisterAgentRequest) domainHub.CreateAgentModel { + return domainHub.CreateAgentModel{ + AgentID: agent.AgentId, + AgentName: agent.AgentName, + Architecture: agent.Host.Arch, + System: agent.Host.System, + Hostname: agent.Host.Hostname, + Version: agent.AgentVersion, + Capabilities: agent.Capabilities, + } +} diff --git a/internal/hub/store/mapper.go b/internal/hub/store/mapper.go new file mode 100644 index 0000000..cbce0c3 --- /dev/null +++ b/internal/hub/store/mapper.go @@ -0,0 +1,69 @@ +package store + +import ( + "encoding/json" + + "github.com/lorsanstand/HomeOps-Hub/internal/domain" + domainHub "github.com/lorsanstand/HomeOps-Hub/internal/hub/domain" + "github.com/lorsanstand/HomeOps-Hub/internal/hub/store/sqlc/gen" +) + +func toDBAgent(agent domainHub.CreateAgentModel) gen.CreateAgentParams { + return gen.CreateAgentParams{ + AgentID: agent.AgentID, + AgentName: &agent.AgentName, + Architecture: agent.Architecture, + System: agent.System, + Hostname: agent.Hostname, + Version: agent.Version, + Capabilities: toJsonCapabilities(agent.Capabilities), + } +} + +func toUpdateDBAgent(agent domainHub.CreateAgentModel) gen.UpdateAgentByIDParams { + return gen.UpdateAgentByIDParams{ + AgentID: agent.AgentID, + AgentName: &agent.AgentName, + Architecture: agent.Architecture, + System: agent.System, + Hostname: agent.Hostname, + Version: agent.Version, + Capabilities: toJsonCapabilities(agent.Capabilities), + } +} + +func toJsonCapabilities(caps []domain.Capability) []byte { + data, err := json.Marshal(caps) + if err != nil { + // Note: Error is silently handled - consider logging in production + return []byte{} + } + return data +} + +func toAgentModel(dbAgent gen.Agent) domainHub.AgentModel { + var dbAgentName string + if dbAgent.AgentName != nil { + dbAgentName = *dbAgent.AgentName + } + + return domainHub.AgentModel{ + ID: int(dbAgent.ID), + AgentID: dbAgent.AgentID, + AgentName: dbAgentName, + Architecture: dbAgent.Architecture, + System: dbAgent.System, + Hostname: dbAgent.Hostname, + Capabilities: toDomainCapabilities(dbAgent.Capabilities), + } +} + +func toDomainCapabilities(caps []byte) []domain.Capability { + var capabilities []domain.Capability + err := json.Unmarshal(caps, &capabilities) + if err != nil { + // Note: Error is silently handled - consider logging in production + return []domain.Capability{} + } + return capabilities +} diff --git a/internal/hub/store/sqlc/gen/agent.sql.go b/internal/hub/store/sqlc/gen/agent.sql.go index f4d94a7..6a6e23c 100644 --- a/internal/hub/store/sqlc/gen/agent.sql.go +++ b/internal/hub/store/sqlc/gen/agent.sql.go @@ -36,3 +36,78 @@ func (q *Queries) CreateAgent(ctx context.Context, arg CreateAgentParams) error ) return err } + +const getAgentByAgentID = `-- name: GetAgentByAgentID :one +SELECT id, agent_id, agent_name, architecture, system, hostname, version, capabilities, registered_at from agents +WHERE agent_id=$1 +` + +func (q *Queries) GetAgentByAgentID(ctx context.Context, agentID string) (Agent, error) { + row := q.db.QueryRow(ctx, getAgentByAgentID, agentID) + var i Agent + err := row.Scan( + &i.ID, + &i.AgentID, + &i.AgentName, + &i.Architecture, + &i.System, + &i.Hostname, + &i.Version, + &i.Capabilities, + &i.RegisteredAt, + ) + return i, err +} + +const getAgentByID = `-- name: GetAgentByID :one +SELECT id, agent_id, agent_name, architecture, system, hostname, version, capabilities, registered_at from agents +WHERE id=$1 +` + +func (q *Queries) GetAgentByID(ctx context.Context, id int32) (Agent, error) { + row := q.db.QueryRow(ctx, getAgentByID, id) + var i Agent + err := row.Scan( + &i.ID, + &i.AgentID, + &i.AgentName, + &i.Architecture, + &i.System, + &i.Hostname, + &i.Version, + &i.Capabilities, + &i.RegisteredAt, + ) + return i, err +} + +const updateAgentByID = `-- name: UpdateAgentByID :exec +UPDATE agents +SET agent_id=$1, agent_name=$2, architecture=$3, system=$4, hostname=$5, version=$6, capabilities=$7 +WHERE id=$8 +` + +type UpdateAgentByIDParams struct { + AgentID string + AgentName *string + Architecture string + System string + Hostname string + Version string + Capabilities []byte + ID int32 +} + +func (q *Queries) UpdateAgentByID(ctx context.Context, arg UpdateAgentByIDParams) error { + _, err := q.db.Exec(ctx, updateAgentByID, + arg.AgentID, + arg.AgentName, + arg.Architecture, + arg.System, + arg.Hostname, + arg.Version, + arg.Capabilities, + arg.ID, + ) + return err +} diff --git a/internal/hub/store/sqlc/gen/models.go b/internal/hub/store/sqlc/gen/models.go index dff6f06..c5e077d 100644 --- a/internal/hub/store/sqlc/gen/models.go +++ b/internal/hub/store/sqlc/gen/models.go @@ -9,7 +9,7 @@ import ( ) type Agent struct { - ID int64 + ID int32 AgentID string AgentName *string Architecture string diff --git a/internal/hub/store/sqlc/queries/agent.sql b/internal/hub/store/sqlc/queries/agent.sql index 900c564..b0e53ca 100644 --- a/internal/hub/store/sqlc/queries/agent.sql +++ b/internal/hub/store/sqlc/queries/agent.sql @@ -1,3 +1,16 @@ -- name: CreateAgent :exec INSERT INTO agents (agent_id, agent_name, architecture, system, hostname, version, capabilities) -VALUES ($1, $2, $3, $4, $5, $6, $7); \ No newline at end of file +VALUES ($1, $2, $3, $4, $5, $6, $7); + +-- name: GetAgentByID :one +SELECT * from agents +WHERE id=$1; + +-- name: GetAgentByAgentID :one +SELECT * from agents +WHERE agent_id=$1; + +-- name: UpdateAgentByID :exec +UPDATE agents +SET agent_id=$1, agent_name=$2, architecture=$3, system=$4, hostname=$5, version=$6, capabilities=$7 +WHERE id=$8; \ No newline at end of file diff --git a/internal/hub/store/store.go b/internal/hub/store/store.go index a1ac28d..02936d4 100644 --- a/internal/hub/store/store.go +++ b/internal/hub/store/store.go @@ -4,6 +4,7 @@ import ( "context" "github.com/jackc/pgx/v5/pgxpool" + domainHub "github.com/lorsanstand/HomeOps-Hub/internal/hub/domain" "github.com/lorsanstand/HomeOps-Hub/internal/hub/store/sqlc/gen" ) @@ -16,6 +17,20 @@ func NewHubStore(db *pgxpool.Pool) *HubStore { return &HubStore{queries} } -func (h *HubStore) NewAgent(ctx context.Context, agent Agent) error { +func (h *HubStore) NewAgent(ctx context.Context, agent domainHub.CreateAgentModel) error { return h.queries.CreateAgent(ctx, toDBAgent(agent)) } + +func (h *HubStore) GetAgentByAgentID(ctx context.Context, AgentID string) (domainHub.AgentModel, error) { + data, err := h.queries.GetAgentByAgentID(ctx, AgentID) + if err != nil { + return domainHub.AgentModel{}, err + } + return toAgentModel(data), nil +} + +func (h *HubStore) UpdateAgentByID(ctx context.Context, ID int, updateAgent domainHub.CreateAgentModel) error { + data := toUpdateDBAgent(updateAgent) + data.ID = int32(ID) + return h.queries.UpdateAgentByID(ctx, data) +} diff --git a/internal/hub/store/structure.go b/internal/hub/store/structure.go deleted file mode 100644 index b2eaa36..0000000 --- a/internal/hub/store/structure.go +++ /dev/null @@ -1,37 +0,0 @@ -package store - -import ( - "encoding/json" - - "github.com/lorsanstand/HomeOps-Hub/internal/domain" - "github.com/lorsanstand/HomeOps-Hub/internal/hub/store/sqlc/gen" -) - -type Agent struct { - AgentID string - AgentName string - Architecture string - System string - Hostname string - Version string - Capabilities []domain.Capability -} - -func toDBAgent(agent Agent) gen.CreateAgentParams { - return gen.CreateAgentParams{ - AgentID: agent.AgentID, - AgentName: &agent.AgentName, - Architecture: agent.Architecture, - System: agent.System, - Version: agent.Version, - Capabilities: toJsonCapabilities(agent.Capabilities), - } -} - -func toJsonCapabilities(caps []domain.Capability) []byte { - data, err := json.Marshal(caps) - if err != nil { - return []byte{} - } - return data -}