16 Commits

Author SHA1 Message Date
zero@thinky 428140ff15 feat(backend): add job metrics
ci-agent / build (push) Failing after 3m1s
2026-04-05 00:44:57 +03:00
zero@thinky 7be99f8e91 feat: big ahh commit
- agent+proto+backend: transfer service status
- agent: fix returning empty message on nonzero exit status
- backend: refactor collector+commander and handlers dependent on them: implement agent accounting via grpc stats handler
2026-04-05 00:44:56 +03:00
d3m0k1d b516a54c17 fixsess and logic for web ide
ci-agent / build (push) Failing after 2m42s
2026-04-04 23:56:28 +03:00
d3m0k1d 1e4e65bb84 fix: agent init
ci-agent / build (push) Failing after 2m41s
2026-04-04 21:22:37 +03:00
zero@thinky 3389df740c feat!(proto): change service monitor from stream to unary
ci-agent / build (push) Failing after 2m34s
2026-04-04 20:46:28 +03:00
d3m0k1d d535831fc1 fix: fcking activate account
ci-agent / build (push) Failing after 2m55s
2026-04-04 20:39:48 +03:00
d3m0k1d f8c413a498 fix: reg
ci-agent / build (push) Failing after 2m36s
2026-04-04 20:17:51 +03:00
zero@thinky 134777de10 feat(backend): add sqlite to dockerfile for manual intervention
ci-agent / build (push) Failing after 3m0s
2026-04-04 20:07:41 +03:00
zero@thinky 4ea1aec6e2 feat(backend): implement service monitor proto & connect it to http /agents
ci-agent / build (push) Failing after 2m30s
2026-04-04 20:01:30 +03:00
zero@thinky 1d75935a08 feat(proto): add service monitor
ci-agent / build (push) Failing after 2m30s
2026-04-04 19:56:08 +03:00
d3m0k1d 0f8b148279 fix: linter and docs
ci-agent / build (push) Failing after 2m50s
2026-04-04 19:44:16 +03:00
zero@thinky fe7e41e4af fix(commander): missing job id on errors
ci-agent / build (push) Failing after 3m4s
2026-04-04 19:32:04 +03:00
zero@thinky 81d8f71937 feat(backend): drop default on jobs 2026-04-04 19:32:04 +03:00
d3m0k1d a71fde67e4 fix: user reg
ci-agent / build (push) Failing after 3m0s
2026-04-04 18:49:05 +03:00
zero@thinky 398c688fed fix race
ci-agent / build (push) Failing after 2m42s
2026-04-04 18:15:45 +03:00
zero@thinky 958211198c feat(backend): add cors 2026-04-04 17:53:35 +03:00
42 changed files with 4858 additions and 748 deletions
+81
View File
@@ -0,0 +1,81 @@
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
version: 2
project_name: BanForge
gitea_urls:
api: https://gitea.d3m0k1d.ru/api/v1
download: https://gitea.d3m0k1d.ru/d3m0k1d/HellreigN/releases/download
skip_tls_verify: false
builds:
- id: banforge
main: ./cmd/banforge/main.go
binary: banforge
ignore:
- goos: windows
- goos: darwin
- goos: freebsd
goos:
- linux
goarch:
- amd64
- arm64
ldflags:
- "-s -w"
env:
- CGO_ENABLED=0
archives:
- formats: [tar.gz]
name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
nfpms:
- id: banforge
package_name: banforge
file_name_template: "{{ .PackageName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
homepage: https://gitea.d3m0k1d.ru/d3m0k1d/HellreigN
description: HellreigN agent
maintainer: d3m0k1d <contact@d3m0k1d.ru>
license: GPLv3.0
formats:
- apk
- deb
- rpm
- archlinux
bindir: /usr/bin
scripts:
postinstall: build/postinstall.sh
postremove: build/postremove.sh
contents:
- src: docs/man/banforge.1
dst: /usr/share/man/man1/banforge.1
file_info:
mode: 0644
- src: docs/man/banforge.5
dst: /usr/share/man/man5/banforge.5
file_info:
mode: 0644
release:
gitea:
owner: d3m0k1d
name: BanForge
mode: keep-existing
changelog:
sort: asc
filters:
exclude:
- "^docs:"
- "^test:"
checksum:
name_template: "{{ .ProjectName }}_{{ .Version }}_checksums.txt"
algorithm: sha256
sboms:
- artifacts: archive
documents:
- "{{ .ArtifactName }}.spdx.json"
cmd: syft
args: ["$artifact", "--output", "spdx-json=$document"]
+1 -1
View File
@@ -3,7 +3,7 @@ module gitea.d3m0k1d.ru/d3m0k1d/HellreigN/agent
go 1.26.1 go 1.26.1
require ( require (
gitea.d3m0k1d.ru/d3m0k1d/HellreigN/proto v0.0.0-20260403214837-94be9799f47d gitea.d3m0k1d.ru/d3m0k1d/HellreigN/proto v0.0.0-20260404174628-3389df740c20
github.com/hpcloud/tail v1.0.0 github.com/hpcloud/tail v1.0.0
github.com/samber/lo v1.53.0 github.com/samber/lo v1.53.0
golang.org/x/sync v0.20.0 golang.org/x/sync v0.20.0
+19 -18
View File
@@ -3,22 +3,19 @@ package commander
import ( import (
"bytes" "bytes"
"errors" "errors"
"io"
"os/exec"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/proto/proto" "gitea.d3m0k1d.ru/d3m0k1d/HellreigN/proto/proto"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
"io"
"os/exec"
) )
type CommandExecutor struct { type CommandExecutor struct{}
}
func (*CommandExecutor) Execute(command *proto.Command) (*proto.FinishedCommand, error) { func (*CommandExecutor) Execute(command *proto.Command) (fc *proto.FinishedCommand, err error) {
fc = new(proto.FinishedCommand)
fc.Id = command.Id
cmd := exec.Command(command.Command[0], command.Command[1:]...) cmd := exec.Command(command.Command[0], command.Command[1:]...)
var ( var stdin io.WriteCloser
stdin io.WriteCloser
err error
)
if command.Stdin != nil { if command.Stdin != nil {
stdin, err = cmd.StdinPipe() stdin, err = cmd.StdinPipe()
if err != nil { if err != nil {
@@ -50,16 +47,20 @@ func (*CommandExecutor) Execute(command *proto.Command) (*proto.FinishedCommand,
_, err := io.Copy(stderrbuf, stderr) _, err := io.Copy(stderrbuf, stderr)
return err return err
}) })
if err := cmd.Wait(); err != nil { if waitErr := cmd.Wait(); waitErr != nil {
return nil, err var exitErr *exec.ExitError
if !errors.As(waitErr, &exitErr) {
return nil, waitErr
}
fc.Status = int32(exitErr.ExitCode())
} else {
fc.Status = int32(cmd.ProcessState.ExitCode())
} }
if err := eg.Wait(); err != nil { if err := eg.Wait(); err != nil {
return nil, err return nil, err
} }
return &proto.FinishedCommand{ fc.Status = int32(cmd.ProcessState.ExitCode())
Id: command.Id, fc.Stdout = stdoutbuf.String()
Status: int32(cmd.ProcessState.ExitCode()), fc.Stderr = stderrbuf.String()
Stdout: stdoutbuf.String(), return
Stderr: stderrbuf.String(),
}, nil
} }
+57
View File
@@ -22,6 +22,7 @@ import (
"github.com/samber/lo" "github.com/samber/lo"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/metadata" "google.golang.org/grpc/metadata"
) )
@@ -110,6 +111,13 @@ func main() {
return ccli.HandleCommands(ctx, grpcAddr, creds) return ccli.HandleCommands(ctx, grpcAddr, creds)
}) })
// Start services update stream
if len(cfg.Services) > 0 {
wg.Go(func() error {
return reportServices(ctx, grpcAddr, creds, cfg.Label, cfg.Services, lgr)
})
}
// Start log collectors // Start log collectors
if len(cfg.Services) > 0 { if len(cfg.Services) > 0 {
wg.Go(func() error { wg.Go(func() error {
@@ -301,3 +309,52 @@ func reconnectStream(
return fmt.Errorf("failed to reconnect after 5 attempts for service %s", service) return fmt.Errorf("failed to reconnect after 5 attempts for service %s", service)
} }
// reportServices periodically sends service status updates to the backend via gRPC.
// For now, all configured services are reported as "up" every 5 seconds.
func reportServices(
ctx context.Context,
grpcAddr string,
creds credentials.TransportCredentials,
label string,
services []config.ServiceConfig,
lgr *logger.Logger,
) error {
conn, err := grpc.NewClient(grpcAddr, grpc.WithTransportCredentials(creds))
if err != nil {
return fmt.Errorf("failed to connect for services report: %w", err)
}
defer conn.Close()
ccli := proto.NewCollectorClient(conn)
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
// Send immediately on start, then every 5 seconds
for {
svcUpdates := make([]*proto.ServicesUpdate_ServiceUpdate, 0, len(services))
for _, svc := range services {
svcUpdates = append(svcUpdates, &proto.ServicesUpdate_ServiceUpdate{
Name: svc.Name,
Status: "up",
})
}
md := metadata.New(map[string]string{"whoami": label})
_, err := ccli.ReportServices(
metadata.NewOutgoingContext(ctx, md),
&proto.ServicesUpdate{Services: svcUpdates},
)
if err != nil {
lgr.Warn("Failed to report services", "err", err)
} else {
lgr.Debug("Services reported successfully", "count", len(services))
}
select {
case <-ctx.Done():
return ctx.Err()
case <-ticker.C:
}
}
}
+55 -12
View File
@@ -82,19 +82,29 @@ func main() {
}() }()
} }
// Initialize Collector gRPC service // Initialize Collector (log streaming) with its own ConnTracker
coll := collector.New(logRepo) collTracker := collector.NewConnTracker()
coll := collector.New(logRepo, collTracker)
cmdr := commander.New(jobRepo) // Initialize ConnTracker for Commander agent lifecycle
cmdTracker := commander.NewConnTracker()
cmdr := commander.New(jobRepo, cmdTracker)
// Initialize script interpreter repository and service // Initialize script interpreter repository and service
scriptRepo := repository.NewScriptInterpreterRepo(db) scriptRepo := repository.NewScriptInterpreterRepo(db)
if err := scriptRepo.Init(context.Background()); err != nil { if err := scriptRepo.Init(context.Background()); err != nil {
log.Printf("Warning: failed to initialize script interpreters table: %v", err) log.Printf("Warning: failed to initialize script interpreters table: %v", err)
} }
scriptSvc := service.NewScriptService(scriptRepo) scriptSvc := service.NewScriptServiceWithInterpreters(h.Repo, scriptRepo)
scriptHandlers := handlers.NewScriptHandlers(scriptSvc, cmdr) scriptHandlers := handlers.NewScriptHandlers(scriptSvc, cmdTracker)
jobsHandlers := handlers.NewJobsHandlers(cmdr, scriptSvc) jobsHandlers := handlers.NewJobsHandlers(cmdTracker, scriptSvc,
os.Getenv("WHEREAMI"), /* our address for redirects */
jobRepo,
)
// Initialize script management service and handlers
scriptManageSvc := service.NewScriptService(h.Repo)
scriptManageHandlers := handlers.NewScriptHandlersGroup(scriptManageSvc, cmdr)
agents := handlers.NewAgentsGroup(h, coll) agents := handlers.NewAgentsGroup(h, coll)
auth := handlers.AuthGroup{Handlers: h} auth := handlers.AuthGroup{Handlers: h}
@@ -130,6 +140,7 @@ func main() {
} }
router := gin.Default() router := gin.Default()
router.Use(handlers.CorsMiddleware("http://127.0.0.1:5173;http://localhost:5173"))
docs.SwaggerInfo.BasePath = "/api/v1" docs.SwaggerInfo.BasePath = "/api/v1"
docs.SwaggerInfo.Title = "HellreigN" docs.SwaggerInfo.Title = "HellreigN"
docs.SwaggerInfo.Version = "1.0" docs.SwaggerInfo.Version = "1.0"
@@ -143,13 +154,14 @@ func main() {
authGroup := v1.Group("/auth") authGroup := v1.Group("/auth")
{ {
authGroup.POST("/login", auth.Login) authGroup.POST("/login", auth.Login)
authGroup.POST("/register", auth.RegisterUser)
} }
// Auth token management (requires auth) // Auth token management (requires auth)
authTokenGroup := v1.Group("/auth") authTokenGroup := v1.Group("/auth")
authTokenGroup.Use(auth.AuthMiddleware()) authTokenGroup.Use(auth.AuthMiddleware())
{ {
authTokenGroup.POST("/token", handlers.RequireAdmin(), auth.CreateToken) authTokenGroup.POST("/token", auth.CreateToken)
authTokenGroup.GET("/validate", auth.ValidateToken) authTokenGroup.GET("/validate", auth.ValidateToken)
authTokenGroup.GET("/tokens", handlers.RequireAdmin(), auth.ListTokens) authTokenGroup.GET("/tokens", handlers.RequireAdmin(), auth.ListTokens)
authTokenGroup.DELETE("/token", auth.DeleteMyToken) authTokenGroup.DELETE("/token", auth.DeleteMyToken)
@@ -158,12 +170,28 @@ func main() {
// User management (admin only) - Full CRUD // User management (admin only) - Full CRUD
authTokenGroup.GET("/users/:login", handlers.RequireAdmin(), auth.GetUser) authTokenGroup.GET("/users/:login", handlers.RequireAdmin(), auth.GetUser)
authTokenGroup.PUT("/users/:login", handlers.RequireAdmin(), auth.UpdateUser) authTokenGroup.PUT("/users/:login", handlers.RequireAdmin(), auth.UpdateUser)
authTokenGroup.PUT("/users/:login/permissions", handlers.RequireAdmin(), auth.UpdateUserPermissions) authTokenGroup.PUT(
authTokenGroup.PUT("/users/:login/password", handlers.RequireAdmin(), auth.ResetUserPassword) "/users/:login/permissions",
handlers.RequireAdmin(),
auth.UpdateUserPermissions,
)
authTokenGroup.PUT(
"/users/:login/password",
handlers.RequireAdmin(),
auth.ResetUserPassword,
)
// User activation management (admin only) // User activation management (admin only)
authTokenGroup.POST("/users/:login/activate", handlers.RequireAdmin(), auth.ActivateUser) authTokenGroup.POST(
authTokenGroup.POST("/users/:login/deactivate", handlers.RequireAdmin(), auth.DeactivateUser) "/users/:login/activate",
handlers.RequireAdmin(),
auth.ActivateUser,
)
authTokenGroup.POST(
"/users/:login/deactivate",
handlers.RequireAdmin(),
auth.DeactivateUser,
)
authTokenGroup.GET("/users/inactive", handlers.RequireAdmin(), auth.ListInactiveUsers) authTokenGroup.GET("/users/inactive", handlers.RequireAdmin(), auth.ListInactiveUsers)
} }
@@ -179,6 +207,9 @@ func main() {
jobsGroup.Use(auth.AuthMiddleware(), handlers.RequireAdmin()) jobsGroup.Use(auth.AuthMiddleware(), handlers.RequireAdmin())
{ {
jobsGroup.POST("", jobsHandlers.AddJob) jobsGroup.POST("", jobsHandlers.AddJob)
jobsGroup.POST("/:id/wait", jobsHandlers.WaitJob)
jobsGroup.GET("/metrics", jobsHandlers.GetJobMetrics)
jobsGroup.POST("/check_cmd", jobsHandlers.CheckCmd)
} }
// Agent registration // Agent registration
@@ -221,6 +252,14 @@ func main() {
scriptsGroup.GET("/interpreters/:id", scriptHandlers.GetInterpreter) scriptsGroup.GET("/interpreters/:id", scriptHandlers.GetInterpreter)
scriptsGroup.PUT("/interpreters/:id", scriptHandlers.UpdateInterpreter) scriptsGroup.PUT("/interpreters/:id", scriptHandlers.UpdateInterpreter)
scriptsGroup.DELETE("/interpreters/:id", scriptHandlers.DeleteInterpreter) scriptsGroup.DELETE("/interpreters/:id", scriptHandlers.DeleteInterpreter)
// Script management (tree, CRUD)
scriptsGroup.GET("/tree", scriptManageHandlers.GetTree)
scriptsGroup.POST("", scriptManageHandlers.CreateScript)
scriptsGroup.GET("/:id", scriptManageHandlers.GetScript)
scriptsGroup.PUT("/:id", scriptManageHandlers.UpdateScript)
scriptsGroup.DELETE("/:id", scriptManageHandlers.DeleteScript)
scriptsGroup.POST("/:id/run", scriptManageHandlers.RunScriptByID)
} }
} }
@@ -260,7 +299,11 @@ func main() {
MinVersion: tls.VersionTLS12, MinVersion: tls.VersionTLS12,
} }
grpcServer := grpc.NewServer(grpc.Creds(credentials.NewTLS(tlsConfig))) grpcServer := grpc.NewServer(
grpc.Creds(credentials.NewTLS(tlsConfig)),
grpc.StatsHandler(collTracker),
grpc.StatsHandler(cmdTracker),
)
proto.RegisterCommanderServer(grpcServer, cmdr) proto.RegisterCommanderServer(grpcServer, cmdr)
proto.RegisterCollectorServer(grpcServer, coll) proto.RegisterCollectorServer(grpcServer, coll)
+1 -1
View File
@@ -14,7 +14,7 @@ RUN --mount=type=cache,target=/go/pkg/mod \
FROM alpine:3.23.0 FROM alpine:3.23.0
RUN apk add --no-cache curl openssl bash ansible RUN apk add --no-cache curl openssl bash ansible sqlite
COPY --from=builder /app/backend/backend . COPY --from=builder /app/backend/backend .
COPY --from=builder /app/backend/scripts /etc/hellreign/scripts COPY --from=builder /app/backend/scripts /etc/hellreign/scripts
+1090 -125
View File
File diff suppressed because it is too large Load Diff
+1090 -125
View File
File diff suppressed because it is too large Load Diff
+706 -96
View File
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -3,9 +3,10 @@ module gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend
go 1.26.1 go 1.26.1
require ( require (
gitea.d3m0k1d.ru/d3m0k1d/HellreigN/proto v0.0.0-20260403210401-a6212c89fc0e gitea.d3m0k1d.ru/d3m0k1d/HellreigN/proto v0.0.0-20260404174628-3389df740c20
github.com/ClickHouse/clickhouse-go/v2 v2.44.0 github.com/ClickHouse/clickhouse-go/v2 v2.44.0
github.com/gin-gonic/gin v1.12.0 github.com/gin-gonic/gin v1.12.0
github.com/samber/lo v1.53.0
github.com/swaggo/files v1.0.1 github.com/swaggo/files v1.0.1
github.com/swaggo/gin-swagger v1.6.1 github.com/swaggo/gin-swagger v1.6.1
github.com/swaggo/swag v1.16.6 github.com/swaggo/swag v1.16.6
+2
View File
@@ -138,6 +138,8 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/samber/lo v1.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM=
github.com/samber/lo v1.53.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0= github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
+26 -12
View File
@@ -12,10 +12,10 @@ import (
// Executor handles running Ansible playbooks // Executor handles running Ansible playbooks
type Executor struct { type Executor struct {
workDir string workDir string
grpcServerHost string grpcServerHost string
grpcServerPort string grpcServerPort string
backendURL string backendURL string
} }
// ExecutorConfig holds configuration for the Executor // ExecutorConfig holds configuration for the Executor
@@ -23,26 +23,26 @@ type ExecutorConfig struct {
WorkDir string WorkDir string
GRPCServerHost string GRPCServerHost string
GRPCServerPort string GRPCServerPort string
BackendURL string BackendURL string
} }
// NewExecutor creates a new Ansible executor // NewExecutor creates a new Ansible executor
func NewExecutor(cfg ExecutorConfig) *Executor { func NewExecutor(cfg ExecutorConfig) *Executor {
return &Executor{ return &Executor{
workDir: cfg.WorkDir, workDir: cfg.WorkDir,
grpcServerHost: cfg.GRPCServerHost, grpcServerHost: cfg.GRPCServerHost,
grpcServerPort: cfg.GRPCServerPort, grpcServerPort: cfg.GRPCServerPort,
backendURL: cfg.BackendURL, backendURL: cfg.BackendURL,
} }
} }
// DeployResult holds the result of a deployment // DeployResult holds the result of a deployment
type DeployResult struct { type DeployResult struct {
Host string Host string
Success bool Success bool
Stdout string Stdout string
Stderr string Stderr string
Err error Err error
} }
// WorkDir returns the work directory path // WorkDir returns the work directory path
@@ -50,8 +50,17 @@ func (e *Executor) WorkDir() string {
return e.workDir return e.workDir
} }
// GRPCURL returns the gRPC server URL (host:port)
func (e *Executor) GRPCURL() string {
return e.grpcServerHost + ":" + e.grpcServerPort
}
// Deploy runs Ansible playbook for the given inventory // Deploy runs Ansible playbook for the given inventory
func (e *Executor) Deploy(ctx context.Context, inventoryPath string, deployType string) ([]DeployResult, error) { func (e *Executor) Deploy(
ctx context.Context,
inventoryPath string,
deployType string,
) ([]DeployResult, error) {
playbookName := "binary_deploy.yml" playbookName := "binary_deploy.yml"
if deployType == "docker" { if deployType == "docker" {
playbookName = "docker_deploy.yml" playbookName = "docker_deploy.yml"
@@ -62,6 +71,7 @@ func (e *Executor) Deploy(ctx context.Context, inventoryPath string, deployType
cmd := exec.CommandContext(ctx, "ansible-playbook", cmd := exec.CommandContext(ctx, "ansible-playbook",
"-i", inventoryPath, "-i", inventoryPath,
"-e", fmt.Sprintf("backend_url=%s", e.backendURL), "-e", fmt.Sprintf("backend_url=%s", e.backendURL),
"-e", fmt.Sprintf("grpc_url=%s", e.grpcServerHost+":"+e.grpcServerPort),
playbookPath, playbookPath,
) )
@@ -84,7 +94,11 @@ func (e *Executor) Deploy(ctx context.Context, inventoryPath string, deployType
} }
// DeployParallel runs Ansible playbook for multiple inventories in parallel // DeployParallel runs Ansible playbook for multiple inventories in parallel
func (e *Executor) DeployParallel(ctx context.Context, inventoryPaths []string, deployType string) (map[string][]DeployResult, error) { func (e *Executor) DeployParallel(
ctx context.Context,
inventoryPaths []string,
deployType string,
) (map[string][]DeployResult, error) {
var wg sync.WaitGroup var wg sync.WaitGroup
results := make(map[string][]DeployResult) results := make(map[string][]DeployResult)
errCh := make(chan error, len(inventoryPaths)) errCh := make(chan error, len(inventoryPaths))
+2
View File
@@ -18,6 +18,7 @@ type InventoryHost struct {
Password string Password string
DeployType string DeployType string
Token string Token string
GRPCURL string
} }
// Inventory represents an Ansible inventory file // Inventory represents an Ansible inventory file
@@ -32,6 +33,7 @@ const inventoryTemplateText = `{{ range .Hosts }}
deploy_type={{ .DeployType }} deploy_type={{ .DeployType }}
agent_token={{ .Token }} agent_token={{ .Token }}
agent_label={{ .Name }} agent_label={{ .Name }}
grpc_url={{ .GRPCURL }}
{{ end }}` {{ end }}`
+23 -33
View File
@@ -1,6 +1,8 @@
package ansible package ansible
// BinaryDeployPlaybook returns the Ansible playbook for binary deployment // BinaryDeployPlaybook returns the Ansible playbook for binary deployment.
// Downloads the agent binary, writes config, and starts it directly (no systemd).
// systemd unit is managed separately (e.g. via goreleaser .deb/.rpm packages).
const BinaryDeployPlaybook = `--- const BinaryDeployPlaybook = `---
- name: Deploy HellreigN Agent (Binary) - name: Deploy HellreigN Agent (Binary)
hosts: all hosts: all
@@ -11,7 +13,6 @@ const BinaryDeployPlaybook = `---
backend_url: "{{ backend_url }}" backend_url: "{{ backend_url }}"
install_dir: /opt/hellreign install_dir: /opt/hellreign
bin_name: hellreign-agent bin_name: hellreign-agent
service_name: hellreign-agent
cert_dir: "{{ install_dir }}/certs" cert_dir: "{{ install_dir }}/certs"
tasks: tasks:
@@ -37,45 +38,29 @@ const BinaryDeployPlaybook = `---
copy: copy:
content: | content: |
backend_url: "{{ backend_url }}" backend_url: "{{ backend_url }}"
grpc_url: "{{ grpc_url | default('localhost:9001') }}"
label: "{{ agent_label }}" label: "{{ agent_label }}"
registration_token: "{{ agent_token }}" registration_token: "{{ agent_token }}"
cert_dir: "{{ cert_dir }}" cert_dir: "{{ cert_dir }}"
services:
- name: system
type: journald
dest: "{{ install_dir }}/config.yml" dest: "{{ install_dir }}/config.yml"
mode: '0644' mode: '0644'
- name: Create systemd service file - name: Start HellreigN Agent
copy: shell: |
content: | nohup {{ install_dir }}/{{ bin_name }} > /dev/null 2>&1 &
[Unit] echo $!
Description=HellreigN Agent args:
After=network.target executable: /bin/bash
environment:
[Service] CONFIG_FILE: "{{ install_dir }}/config.yml"
Type=simple register: agent_pid
ExecStart={{ install_dir }}/{{ bin_name }} changed_when: true
Restart=always
RestartSec=5
Environment=CONFIG_FILE={{ install_dir }}/config.yml
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
dest: /etc/systemd/system/{{ service_name }}.service
mode: '0644'
- name: Reload systemd daemon
systemd:
daemon_reload: yes
- name: Enable and start HellreigN Agent service
systemd:
name: "{{ service_name }}"
enabled: yes
state: started
` `
// DockerDeployPlaybook returns the Ansible playbook for Docker deployment // DockerDeployPlaybook returns the Ansible playbook for Docker deployment.
const DockerDeployPlaybook = `--- const DockerDeployPlaybook = `---
- name: Deploy HellreigN Agent (Docker) - name: Deploy HellreigN Agent (Docker)
hosts: all hosts: all
@@ -84,6 +69,7 @@ const DockerDeployPlaybook = `---
agent_label: "{{ agent_label }}" agent_label: "{{ agent_label }}"
agent_token: "{{ agent_token }}" agent_token: "{{ agent_token }}"
backend_url: "{{ backend_url }}" backend_url: "{{ backend_url }}"
grpc_url: "{{ grpc_url | default('localhost:9001') }}"
container_name: hellreign-agent-{{ agent_label }} container_name: hellreign-agent-{{ agent_label }}
image: "gitea.d3m0k1d.ru/d3m0k1d/hellreign-agent:latest" image: "gitea.d3m0k1d.ru/d3m0k1d/hellreign-agent:latest"
cert_dir: /etc/hellreign-agent/certs cert_dir: /etc/hellreign-agent/certs
@@ -117,9 +103,13 @@ const DockerDeployPlaybook = `---
copy: copy:
content: | content: |
backend_url: "{{ backend_url }}" backend_url: "{{ backend_url }}"
grpc_url: "{{ grpc_url }}"
label: "{{ agent_label }}" label: "{{ agent_label }}"
registration_token: "{{ agent_token }}" registration_token: "{{ agent_token }}"
cert_dir: "{{ cert_dir }}" cert_dir: "{{ cert_dir }}"
services:
- name: system
type: journald
dest: "{{ cert_dir }}/config.yml" dest: "{{ cert_dir }}/config.yml"
mode: '0644' mode: '0644'
+22 -43
View File
@@ -4,7 +4,6 @@ import (
"fmt" "fmt"
"io" "io"
"log" "log"
"sync"
"time" "time"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/repository" "gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/repository"
@@ -13,26 +12,19 @@ import (
"google.golang.org/grpc/metadata" "google.golang.org/grpc/metadata"
) )
// Collector handles log streaming from connected agents.
type Collector struct { type Collector struct {
proto.UnimplementedCollectorServer proto.UnimplementedCollectorServer
logRepo *repository.LogRepository logRepo *repository.LogRepository
agents map[string]*Agent tracker *ConnTracker
mu sync.RWMutex
batchSize int batchSize int
flushInterval time.Duration flushInterval time.Duration
} }
type Agent struct { func New(logRepo *repository.LogRepository, tracker *ConnTracker) *Collector {
ID string
Label string
Services []string
ConnectedAt time.Time
}
func New(logRepo *repository.LogRepository) *Collector {
return &Collector{ return &Collector{
logRepo: logRepo, logRepo: logRepo,
agents: make(map[string]*Agent), tracker: tracker,
batchSize: 100, batchSize: 100,
flushInterval: 2 * time.Second, flushInterval: 2 * time.Second,
} }
@@ -56,33 +48,24 @@ func (c *Collector) Stream(stream proto.Collector_StreamServer) error {
} }
service := serviceVals[0] service := serviceVals[0]
servicesVals := md["services"] agent := &Agent{
var services []string
if len(servicesVals) > 0 {
services = servicesVals
}
// Register agent
c.mu.Lock()
c.agents[agentName] = &Agent{
ID: agentName, ID: agentName,
Label: agentName, Label: agentName,
Services: services, Services: make([]Service, 0),
ConnectedAt: time.Now(), ConnectedAt: time.Now(),
} }
c.mu.Unlock()
defer func() { c.tracker.Register(agent)
c.mu.Lock() defer c.tracker.Unregister(agent.ID)
delete(c.agents, agentName)
c.mu.Unlock()
}()
log.Printf("Agent %s connected, streaming logs for service: %s", agentName, service) log.Printf("Agent %s connected, streaming logs for service: %s", agentName, service)
// If no ClickHouse, just consume the stream without storing // If no ClickHouse, just consume the stream without storing
if !c.logRepo.IsConnected() { if !c.logRepo.IsConnected() {
log.Printf("Warning: ClickHouse not connected yet, consuming logs without storing for agent %s", agentName) log.Printf(
"Warning: ClickHouse not connected yet, consuming logs without storing for agent %s",
agentName,
)
for { for {
_, err := stream.Recv() _, err := stream.Recv()
if err == io.EOF { if err == io.EOF {
@@ -120,7 +103,12 @@ func (c *Collector) Stream(stream proto.Collector_StreamServer) error {
return nil return nil
} }
if err := c.logRepo.InsertBatch(stream.Context(), batch); err != nil { if err := c.logRepo.InsertBatch(stream.Context(), batch); err != nil {
log.Printf("Failed to insert batch for agent %s, service %s: %v", agentName, service, err) log.Printf(
"Failed to insert batch for agent %s, service %s: %v",
agentName,
service,
err,
)
return err return err
} }
log.Printf("Flushed %d logs for agent %s, service %s", len(batch), agentName, service) log.Printf("Flushed %d logs for agent %s, service %s", len(batch), agentName, service)
@@ -131,7 +119,6 @@ func (c *Collector) Stream(stream proto.Collector_StreamServer) error {
for { for {
select { select {
case <-stream.Context().Done(): case <-stream.Context().Done():
// Context cancelled, flush remaining
_ = flush() _ = flush()
return stream.Context().Err() return stream.Context().Err()
case <-ticker.C: case <-ticker.C:
@@ -154,7 +141,6 @@ func (c *Collector) Stream(stream proto.Collector_StreamServer) error {
} }
case err := <-errCh: case err := <-errCh:
if err == io.EOF { if err == io.EOF {
// Client closed stream
return flush() return flush()
} }
return fmt.Errorf("failed to receive: %w", err) return fmt.Errorf("failed to receive: %w", err)
@@ -162,19 +148,12 @@ func (c *Collector) Stream(stream proto.Collector_StreamServer) error {
} }
} }
// GetAgent delegates to the tracker.
func (c *Collector) GetAgent(name string) (*Agent, bool) { func (c *Collector) GetAgent(name string) (*Agent, bool) {
c.mu.RLock() return c.tracker.GetAgent(name)
defer c.mu.RUnlock()
a, ok := c.agents[name]
return a, ok
} }
// Agents delegates to the tracker.
func (c *Collector) Agents() []*Agent { func (c *Collector) Agents() []*Agent {
c.mu.RLock() return c.tracker.Agents()
defer c.mu.RUnlock()
result := make([]*Agent, 0, len(c.agents))
for _, a := range c.agents {
result = append(result, a)
}
return result
} }
@@ -0,0 +1,38 @@
package collector
import (
"context"
"fmt"
"log"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/proto/proto"
"google.golang.org/grpc/metadata"
)
// ReportServices handles a unary service status update from an agent.
// Agents send their current services list, which is stored in the collector.
func (c *Collector) ReportServices(ctx context.Context, req *proto.ServicesUpdate) (*proto.ServicesUpdateResp, error) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, fmt.Errorf("no metadata in context")
}
whoamiVals := md["whoami"]
if len(whoamiVals) == 0 {
return nil, fmt.Errorf("whoami metadata missing")
}
agentName := whoamiVals[0]
services := make([]Service, 0, len(req.Services))
for _, s := range req.Services {
services = append(services, Service{s.Name, s.Status})
}
if ok := c.tracker.UpdateServices(agentName, services); ok {
log.Printf("Updated services for agent %s: %v", agentName, services)
} else {
log.Printf("Warning: received services update for unknown agent %s", agentName)
}
return &proto.ServicesUpdateResp{}, nil
}
@@ -0,0 +1,111 @@
package collector
import (
"context"
"log"
"sync"
"time"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/stats"
)
// ConnTracker tracks connected Collector agents and handles cleanup on disconnect.
// It implements grpc.StatsHandler for disconnect detection.
type ConnTracker struct {
mu sync.RWMutex
agents map[string]*Agent
}
func NewConnTracker() *ConnTracker {
return &ConnTracker{
agents: make(map[string]*Agent),
}
}
// Register adds an agent to the tracker. Called by Collector.Stream().
func (t *ConnTracker) Register(agent *Agent) {
t.mu.Lock()
t.agents[agent.ID] = agent
t.mu.Unlock()
log.Printf("[collector] agent registered: %s", agent.ID)
}
// Unregister removes an agent from the tracker.
func (t *ConnTracker) Unregister(id string) {
t.mu.Lock()
delete(t.agents, id)
t.mu.Unlock()
log.Printf("[collector] agent unregistered: %s", id)
}
// GetAgent returns the agent for the given ID.
func (t *ConnTracker) GetAgent(id string) (*Agent, bool) {
t.mu.RLock()
defer t.mu.RUnlock()
a, ok := t.agents[id]
return a, ok
}
// Agents returns all connected agents.
func (t *ConnTracker) Agents() []*Agent {
t.mu.RLock()
defer t.mu.RUnlock()
result := make([]*Agent, 0, len(t.agents))
for _, a := range t.agents {
result = append(result, a)
}
return result
}
// grpc.StatsHandler implementation.
func (t *ConnTracker) TagRPC(ctx context.Context, _ *stats.RPCTagInfo) context.Context {
return ctx
}
func (t *ConnTracker) HandleRPC(ctx context.Context, _ stats.RPCStats) {}
func (t *ConnTracker) TagConn(ctx context.Context, _ *stats.ConnTagInfo) context.Context {
return ctx
}
func (t *ConnTracker) HandleConn(ctx context.Context, s stats.ConnStats) {
switch s.(type) {
case *stats.ConnEnd:
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return
}
whoamiVals := md["whoami"]
if len(whoamiVals) == 0 {
return
}
t.Unregister(whoamiVals[0])
}
}
// UpdateServices updates the services list for the given agent.
func (t *ConnTracker) UpdateServices(id string, services []Service) bool {
t.mu.Lock()
defer t.mu.Unlock()
agent, ok := t.agents[id]
if !ok {
return false
}
agent.Services = services
return true
}
// Service represents a named service with its current status.
type Service struct {
Name, Status string
}
// Agent represents a connected agent streaming logs to the collector.
type Agent struct {
ID string
Label string
Services []Service
ConnectedAt time.Time
}
+134 -60
View File
@@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"io" "io"
"log"
"sync" "sync"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/models" "gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/models"
@@ -11,27 +12,30 @@ import (
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/metadata" "google.golang.org/grpc/metadata"
"google.golang.org/grpc/stats"
) )
// Commander handles command execution on connected agents.
type Commander struct { type Commander struct {
proto.UnimplementedCommanderServer proto.UnimplementedCommanderServer
agents map[string]Agent tracker *ConnTracker
mu sync.RWMutex jobber Jobber
jobber Jobber
} }
// Jobber persists job state.
type Jobber interface { type Jobber interface {
InitJob(ctx context.Context, agentID string, job models.JobForInsert) (int64, error) InitJob(ctx context.Context, agentID string, job models.JobForInsert) (int64, error)
UpdateJobInDB(ctx context.Context, jid int64, msg models.JobForUpdate) (models.Job, error) UpdateJobInDB(ctx context.Context, jid int64, msg models.JobForUpdate) (models.Job, error)
} }
func New(jobber Jobber) *Commander { func New(jobber Jobber, tracker *ConnTracker) *Commander {
return &Commander{ return &Commander{
agents: make(map[string]Agent), jobber: jobber,
jobber: jobber, tracker: tracker,
} }
} }
// Agent represents a connected agent with an active bidirectional stream.
type Agent struct { type Agent struct {
bidi grpc.BidiStreamingServer[proto.FinishedCommand, proto.Command] bidi grpc.BidiStreamingServer[proto.FinishedCommand, proto.Command]
in chan *proto.Command in chan *proto.Command
@@ -40,10 +44,11 @@ type Agent struct {
ctx context.Context ctx context.Context
aid string aid string
Token string // agent id Token string
Label string Label string
Services []string Services []string
} }
type JobOut struct { type JobOut struct {
fc models.Job fc models.Job
err error err error
@@ -53,48 +58,93 @@ type Job struct {
out chan JobOut out chan JobOut
} }
func (self *Commander) GetAgent(aid string) (agent Agent, ok bool) { // ConnTracker tracks connected agents and handles cleanup on disconnect.
// It implements grpc.StatsHandler for disconnect detection.
type ConnTracker struct {
mu sync.RWMutex
agents map[string]*Agent
}
// GetAgentByLabel searches for an agent by its human-readable label.
func (self *ConnTracker) GetAgentByLabel(label string) (agent Agent, ok bool) {
self.mu.RLock() self.mu.RLock()
defer self.mu.RUnlock() defer self.mu.RUnlock()
agent, ok = self.agents[aid] for _, a := range self.agents {
if a.Label == label {
return *a, true
}
}
return return
} }
func (self *Commander) Agents() []Agent { func NewConnTracker() *ConnTracker {
self.mu.RLock() return &ConnTracker{
defer self.mu.RUnlock() agents: make(map[string]*Agent),
result := make([]Agent, 0, len(self.agents)) }
for _, a := range self.agents { }
func (t *ConnTracker) Register(aid string, agent *Agent) {
t.mu.Lock()
t.agents[aid] = agent
t.mu.Unlock()
log.Printf("[conntracker] agent registered: %s", aid)
}
func (t *ConnTracker) Unregister(aid string) {
t.mu.Lock()
delete(t.agents, aid)
t.mu.Unlock()
log.Printf("[conntracker] agent unregistered: %s", aid)
}
func (t *ConnTracker) GetAgent(aid string) (*Agent, bool) {
t.mu.RLock()
defer t.mu.RUnlock()
a, ok := t.agents[aid]
return a, ok
}
func (t *ConnTracker) Agents() []*Agent {
t.mu.RLock()
defer t.mu.RUnlock()
result := make([]*Agent, 0, len(t.agents))
for _, a := range t.agents {
result = append(result, a) result = append(result, a)
} }
return result return result
} }
func (self *Commander) removeAgent(aid string) { // grpc.StatsHandler implementation.
self.mu.Lock()
defer self.mu.Unlock() func (t *ConnTracker) TagRPC(ctx context.Context, _ *stats.RPCTagInfo) context.Context {
delete(self.agents, aid) return ctx
} }
func (self *Agent) AddJob(job models.JobForInsert) (int64, error) { func (t *ConnTracker) HandleRPC(ctx context.Context, _ stats.RPCStats) {}
jid, err := self.jobber.InitJob(self.ctx, self.aid, job)
if err != nil { func (t *ConnTracker) TagConn(ctx context.Context, _ *stats.ConnTagInfo) context.Context {
return 0, err return ctx
}
func (t *ConnTracker) HandleConn(ctx context.Context, s stats.ConnStats) {
switch s.(type) {
case *stats.ConnEnd:
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return
}
aidVals := md["agentid"]
if len(aidVals) == 0 {
return
}
t.Unregister(aidVals[0])
} }
self.in <- &proto.Command{
Id: jid,
Command: job.Command,
Stdin: job.Stdin,
}
return jid, err
} }
func (self *Agent) WaitJob(jid int64) (*models.Job, error) { // Stream handles a new agent connection and runs the send/recv loops.
result := <-self.jobs[jid].out func (c *Commander) Stream(
return &result.fc, result.err bidi grpc.BidiStreamingServer[proto.FinishedCommand, proto.Command],
} ) error {
func (self *Commander) Stream(bidi grpc.BidiStreamingServer[proto.FinishedCommand, proto.Command]) error {
md, ok := metadata.FromIncomingContext(bidi.Context()) md, ok := metadata.FromIncomingContext(bidi.Context())
if !ok { if !ok {
return fmt.Errorf("no metadata in context") return fmt.Errorf("no metadata in context")
@@ -106,35 +156,58 @@ func (self *Commander) Stream(bidi grpc.BidiStreamingServer[proto.FinishedComman
aid := aidVals[0] aid := aidVals[0]
var label string var label string
labelVals := md["label"] if vals := md["label"]; len(vals) > 0 {
if len(labelVals) > 0 { label = vals[0]
label = labelVals[0]
} }
agent := newAgent(bidi, self.jobber, aid, label) agent := NewAgent(bidi.Context(), c.jobber, aid, label)
self.mu.Lock() agent.bidi = bidi
self.agents[aid] = agent
self.mu.Unlock() c.tracker.Register(aid, agent)
defer c.tracker.Unregister(aid)
defer self.removeAgent(aid)
return agent.run() return agent.run()
} }
func (self *Agent) run() error { // GetAgent returns the agent by ID. Delegates to the tracker.
func (c *Commander) GetAgent(aid string) (*Agent, bool) {
return c.tracker.GetAgent(aid)
}
func (a *Agent) AddJob(job models.JobForInsert) (int64, error) {
jid, err := a.jobber.InitJob(a.ctx, a.aid, job)
if err != nil {
return 0, err
}
a.jobs[jid] = newJob()
a.in <- &proto.Command{
Id: jid,
Command: job.Command,
Stdin: job.Stdin,
}
return jid, nil
}
func (a *Agent) WaitJob(jid int64) (*models.Job, error) {
result := <-a.jobs[jid].out
return &result.fc, result.err
}
func (a *Agent) run() error {
wg := new(errgroup.Group) wg := new(errgroup.Group)
wg.Go(self.recv) wg.Go(a.recv)
wg.Go(self.send) wg.Go(a.send)
return wg.Wait() return wg.Wait()
} }
func (self *Agent) recv() error { func (a *Agent) recv() error {
for { for {
job, err := func() (job models.Job, err error) { job, err := func() (job models.Job, err error) {
msg, err := self.bidi.Recv() msg, err := a.bidi.Recv()
if err != nil { if err != nil {
return return
} }
return self.jobber.UpdateJobInDB(self.ctx, msg.Id, models.JobForUpdate{ return a.jobber.UpdateJobInDB(a.ctx, msg.Id, models.JobForUpdate{
Stdout: msg.Stdout, Stdout: msg.Stdout,
Stderr: msg.Stderr, Stderr: msg.Stderr,
Status: msg.Status, Status: msg.Status,
@@ -143,8 +216,7 @@ func (self *Agent) recv() error {
if err == io.EOF { if err == io.EOF {
return nil return nil
} }
// TODO: that would blow up at some point out := a.jobs[job.ID].out
out := self.jobs[job.ID].out
out <- JobOut{ out <- JobOut{
fc: job, fc: job,
err: err, err: err,
@@ -153,24 +225,26 @@ func (self *Agent) recv() error {
} }
} }
func (self *Agent) send() error { func (a *Agent) send() error {
for job := range self.in { for job := range a.in {
self.jobs[job.Id] = newJob() if err := a.bidi.Send(job); err != nil {
if err := self.bidi.Send(job); err != nil {
return err return err
} }
} }
return io.EOF return io.EOF
// self.jobs[]
} }
func newAgent(bidi grpc.BidiStreamingServer[proto.FinishedCommand, proto.Command], jobber Jobber, aid string, label string) Agent { func NewAgent(
return Agent{ ctx context.Context,
bidi: bidi, jobber Jobber,
in: make(chan *proto.Command), aid string,
label string,
) *Agent {
return &Agent{
in: make(chan *proto.Command, 10),
jobs: make(map[int64]Job), jobs: make(map[int64]Job),
jobber: jobber, jobber: jobber,
ctx: bidi.Context(), ctx: ctx,
aid: aid, aid: aid,
Label: label, Label: label,
Token: aid, Token: aid,
+2 -1
View File
@@ -38,7 +38,7 @@ func NewAgentDeployGroup(h *Handlers) *AgentDeployGroup {
WorkDir: workDir, WorkDir: workDir,
GRPCServerHost: "0.0.0.0", // TODO: make configurable GRPCServerHost: "0.0.0.0", // TODO: make configurable
GRPCServerPort: grpcPort, GRPCServerPort: grpcPort,
BackendURL: backendURL, BackendURL: backendURL,
}) })
// Write playbooks on init // Write playbooks on init
@@ -117,6 +117,7 @@ func (adg *AgentDeployGroup) DeployAgents(c *gin.Context) {
Password: server.Password, Password: server.Password,
DeployType: string(server.DeployType), DeployType: string(server.DeployType),
Token: token, Token: token,
GRPCURL: adg.executor.GRPCURL(),
}, },
} }
+1 -1
View File
@@ -104,7 +104,7 @@ func (arg *AgentRegistrationGroup) Register(c *gin.Context) {
} }
type RegisterRequest struct { type RegisterRequest struct {
CSR string `json:"csr" binding:"required"` CSR string `json:"csr" binding:"required"`
Token string `json:"token" binding:"required"` Token string `json:"token" binding:"required"`
} }
+14 -6
View File
@@ -1,9 +1,11 @@
package handlers package handlers
import ( import (
"fmt"
"net/http"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/grpcsrv/collector" "gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/grpcsrv/collector"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"net/http"
) )
type AgentsGroup struct { type AgentsGroup struct {
@@ -15,17 +17,19 @@ func NewAgentsGroup(h *Handlers, coll *collector.Collector) AgentsGroup {
return AgentsGroup{Handlers: h, collector: coll} return AgentsGroup{Handlers: h, collector: coll}
} }
// AgentInfo represents a connected agent's current status.
type AgentInfo struct { type AgentInfo struct {
Token string `json:"token"` Token string `json:"token" example:"agent-001"` // Unique agent identifier
Label string `json:"label"` Label string `json:"label" example:"web-server-1"` // Human-readable label
Services []string `json:"services"` Services []string `json:"services" example:"nginx:running,redis:up"` // List of services with status (format: "name:status")
ConnectedAt string `json:"connected_at"` ConnectedAt string `json:"connected_at" example:"2026-04-04 10:30:00"` // Time when agent connected (RFC3339-like)
} }
// @Summary Get connected agents // @Summary Get connected agents
// @Description Returns a list of all agents currently connected via Collector (log streaming) // @Description Returns a list of all agents currently connected via Collector (log streaming)
// @Tags agents // @Tags agents
// @Security Bearer // @Security Bearer
// @Accept json
// @Produce json // @Produce json
// @Success 200 {array} AgentInfo // @Success 200 {array} AgentInfo
// @Router /agents [get] // @Router /agents [get]
@@ -33,10 +37,14 @@ func (ag *AgentsGroup) List(c *gin.Context) {
agents := make([]AgentInfo, 0) agents := make([]AgentInfo, 0)
for _, agent := range ag.collector.Agents() { for _, agent := range ag.collector.Agents() {
services := make([]string, 0, len(agent.Services))
for _, s := range agent.Services {
services = append(services, fmt.Sprintf("%s:%s", s.Name, s.Status))
}
agents = append(agents, AgentInfo{ agents = append(agents, AgentInfo{
Token: agent.ID, Token: agent.ID,
Label: agent.Label, Label: agent.Label,
Services: agent.Services, Services: services,
ConnectedAt: agent.ConnectedAt.Format("2006-01-02 15:04:05"), ConnectedAt: agent.ConnectedAt.Format("2006-01-02 15:04:05"),
}) })
} }
+47
View File
@@ -2,6 +2,8 @@ package handlers
import ( import (
"errors" "errors"
"fmt"
"log"
"net/http" "net/http"
"strings" "strings"
@@ -49,6 +51,39 @@ func (ag *AuthGroup) Login(c *gin.Context) {
c.JSON(http.StatusOK, resp) c.JSON(http.StatusOK, resp)
} }
// RegisterUser registers a new user with all permissions set to false.
// @Summary Register user
// @Description Registers a new user with login, password, name, last name. All permissions are set to false.
// @Tags auth
// @Accept json
// @Param request body repository.UserRegister true "Registration data"
// @Success 200 {object} map[string]string
// @Failure 400 {object} map[string]string
// @Failure 409 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /auth/register [post]
func (ag *AuthGroup) RegisterUser(c *gin.Context) {
var req repository.UserRegister
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
id, err := ag.Repo.RegisterUser(req)
if err != nil {
if strings.Contains(err.Error(), "UNIQUE constraint") {
c.JSON(http.StatusConflict, gin.H{"error": "login already exists"})
return
}
log.Printf("[register] failed: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to register user: %v", err)})
return
}
log.Printf("[register] user registered: id=%s login=%s", id, req.Login)
c.JSON(http.StatusOK, gin.H{"message": "user registered"})
}
// CreateToken creates a new user. // CreateToken creates a new user.
// @Summary Create user // @Summary Create user
// @Description Creates a new user with permissions // @Description Creates a new user with permissions
@@ -59,6 +94,7 @@ func (ag *AuthGroup) Login(c *gin.Context) {
// @Failure 400 {object} map[string]string // @Failure 400 {object} map[string]string
// @Failure 401 {object} map[string]string // @Failure 401 {object} map[string]string
// @Failure 500 {object} map[string]string // @Failure 500 {object} map[string]string
// @Security Bearer
// @Router /auth/token [post] // @Router /auth/token [post]
func (ag *AuthGroup) CreateToken(c *gin.Context) { func (ag *AuthGroup) CreateToken(c *gin.Context) {
var tc repository.TokenCreate var tc repository.TokenCreate
@@ -82,6 +118,7 @@ func (ag *AuthGroup) CreateToken(c *gin.Context) {
// @Produce json // @Produce json
// @Success 200 {object} repository.Tokens // @Success 200 {object} repository.Tokens
// @Failure 401 {object} map[string]string // @Failure 401 {object} map[string]string
// @Security Bearer
// @Router /auth/validate [get] // @Router /auth/validate [get]
func (ag *AuthGroup) ValidateToken(c *gin.Context) { func (ag *AuthGroup) ValidateToken(c *gin.Context) {
tokenVal, exists := c.Get(string(tokenContextKey)) tokenVal, exists := c.Get(string(tokenContextKey))
@@ -106,6 +143,7 @@ func (ag *AuthGroup) ValidateToken(c *gin.Context) {
// @Produce json // @Produce json
// @Success 200 {array} repository.Tokens // @Success 200 {array} repository.Tokens
// @Failure 500 {object} map[string]string // @Failure 500 {object} map[string]string
// @Security Bearer
// @Router /auth/tokens [get] // @Router /auth/tokens [get]
func (ag *AuthGroup) ListTokens(c *gin.Context) { func (ag *AuthGroup) ListTokens(c *gin.Context) {
tokens, err := ag.Repo.ListTokens() tokens, err := ag.Repo.ListTokens()
@@ -124,6 +162,7 @@ func (ag *AuthGroup) ListTokens(c *gin.Context) {
// @Success 200 {object} map[string]string // @Success 200 {object} map[string]string
// @Failure 400 {object} map[string]string // @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string // @Failure 500 {object} map[string]string
// @Security Bearer
// @Router /auth/tokens/:login [delete] // @Router /auth/tokens/:login [delete]
func (ag *AuthGroup) DeleteToken(c *gin.Context) { func (ag *AuthGroup) DeleteToken(c *gin.Context) {
login := c.Param("login") login := c.Param("login")
@@ -151,6 +190,7 @@ func (ag *AuthGroup) DeleteToken(c *gin.Context) {
// @Success 200 {object} map[string]string // @Success 200 {object} map[string]string
// @Failure 401 {object} map[string]string // @Failure 401 {object} map[string]string
// @Failure 500 {object} map[string]string // @Failure 500 {object} map[string]string
// @Security Bearer
// @Router /auth/token [delete] // @Router /auth/token [delete]
func (ag *AuthGroup) DeleteMyToken(c *gin.Context) { func (ag *AuthGroup) DeleteMyToken(c *gin.Context) {
tokenVal, exists := c.Get(string(tokenContextKey)) tokenVal, exists := c.Get(string(tokenContextKey))
@@ -182,6 +222,7 @@ func (ag *AuthGroup) DeleteMyToken(c *gin.Context) {
// @Failure 400 {object} map[string]string // @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string // @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string // @Failure 500 {object} map[string]string
// @Security Bearer
// @Router /auth/users/:login/activate [post] // @Router /auth/users/:login/activate [post]
func (ag *AuthGroup) ActivateUser(c *gin.Context) { func (ag *AuthGroup) ActivateUser(c *gin.Context) {
login := c.Param("login") login := c.Param("login")
@@ -211,6 +252,7 @@ func (ag *AuthGroup) ActivateUser(c *gin.Context) {
// @Failure 400 {object} map[string]string // @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string // @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string // @Failure 500 {object} map[string]string
// @Security Bearer
// @Router /auth/users/:login/deactivate [post] // @Router /auth/users/:login/deactivate [post]
func (ag *AuthGroup) DeactivateUser(c *gin.Context) { func (ag *AuthGroup) DeactivateUser(c *gin.Context) {
login := c.Param("login") login := c.Param("login")
@@ -238,6 +280,7 @@ func (ag *AuthGroup) DeactivateUser(c *gin.Context) {
// @Produce json // @Produce json
// @Success 200 {array} repository.Tokens // @Success 200 {array} repository.Tokens
// @Failure 500 {object} map[string]string // @Failure 500 {object} map[string]string
// @Security Bearer
// @Router /auth/users/inactive [get] // @Router /auth/users/inactive [get]
func (ag *AuthGroup) ListInactiveUsers(c *gin.Context) { func (ag *AuthGroup) ListInactiveUsers(c *gin.Context) {
tokens, err := ag.Repo.ListInactiveTokens() tokens, err := ag.Repo.ListInactiveTokens()
@@ -258,6 +301,7 @@ func (ag *AuthGroup) ListInactiveUsers(c *gin.Context) {
// @Failure 400 {object} map[string]string // @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string // @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string // @Failure 500 {object} map[string]string
// @Security Bearer
// @Router /auth/users/:login [get] // @Router /auth/users/:login [get]
func (ag *AuthGroup) GetUser(c *gin.Context) { func (ag *AuthGroup) GetUser(c *gin.Context) {
login := c.Param("login") login := c.Param("login")
@@ -290,6 +334,7 @@ func (ag *AuthGroup) GetUser(c *gin.Context) {
// @Failure 400 {object} map[string]string // @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string // @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string // @Failure 500 {object} map[string]string
// @Security Bearer
// @Router /auth/users/:login [put] // @Router /auth/users/:login [put]
func (ag *AuthGroup) UpdateUser(c *gin.Context) { func (ag *AuthGroup) UpdateUser(c *gin.Context) {
login := c.Param("login") login := c.Param("login")
@@ -327,6 +372,7 @@ func (ag *AuthGroup) UpdateUser(c *gin.Context) {
// @Failure 400 {object} map[string]string // @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string // @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string // @Failure 500 {object} map[string]string
// @Security Bearer
// @Router /auth/users/:login/permissions [put] // @Router /auth/users/:login/permissions [put]
func (ag *AuthGroup) UpdateUserPermissions(c *gin.Context) { func (ag *AuthGroup) UpdateUserPermissions(c *gin.Context) {
login := c.Param("login") login := c.Param("login")
@@ -364,6 +410,7 @@ func (ag *AuthGroup) UpdateUserPermissions(c *gin.Context) {
// @Failure 400 {object} map[string]string // @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string // @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string // @Failure 500 {object} map[string]string
// @Security Bearer
// @Router /auth/users/:login/password [put] // @Router /auth/users/:login/password [put]
func (ag *AuthGroup) ResetUserPassword(c *gin.Context) { func (ag *AuthGroup) ResetUserPassword(c *gin.Context) {
login := c.Param("login") login := c.Param("login")
+35
View File
@@ -0,0 +1,35 @@
package handlers
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/samber/lo"
)
func CorsMiddleware(origincfg string) gin.HandlerFunc {
origins := strings.Split(origincfg, ";")
if origins[0] == "" {
panic("zero cors origins wtf is your config")
}
return func(c *gin.Context) {
origin := c.GetHeader("Origin")
if !lo.Contains(origins, origin) {
origin = origins[0]
}
c.Writer.Header().Set("Access-Control-Allow-Origin", origin)
// c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
c.Writer.Header().
Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, Authorization")
c.Writer.Header().
Set("Access-Control-Allow-Methods", "OPTIONS, GET, POST, PATCH, DELETE, PUT")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(http.StatusNoContent)
return
}
c.Next()
}
}
+203 -51
View File
@@ -1,31 +1,48 @@
package handlers package handlers
import ( import (
"errors"
"fmt" "fmt"
"net/http" "net/http"
"os/exec"
"strconv"
"time"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/grpcsrv/commander" "gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/grpcsrv/commander"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/models" "gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/models"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/repository"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/service" "gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/service"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
type JobsHandlers struct { type JobsHandlers struct {
cmder *commander.Commander tracker *commander.ConnTracker
svc *service.ScriptService svc *service.ScriptService
whereami string
jobRepo *repository.JobRepository
} }
func NewJobsHandlers(cmder *commander.Commander, svc *service.ScriptService) JobsHandlers { func NewJobsHandlers(tracker *commander.ConnTracker, svc *service.ScriptService, whereami string, jobRepo *repository.JobRepository) JobsHandlers {
return JobsHandlers{cmder: cmder, svc: svc} return JobsHandlers{tracker: tracker, svc: svc, whereami: whereami, jobRepo: jobRepo}
} }
// AddJobIn is the request body for creating a job.
type AddJobIn struct { type AddJobIn struct {
Command string `json:"command" binding:"required"` Command string `json:"command" binding:"required"`
InterpreterID int64 `json:"interpreter_id"` InterpreterID int64 `json:"interpreter_id"`
Stdin *string `json:"stdin"` Stdin *string `json:"stdin"`
AgentID string `json:"agent_id" binding:"required"` AgentID string `json:"agent_id" binding:"required"`
} }
// AddJobOut is the response body for a submitted job.
type AddJobOut struct { type AddJobOut struct {
ID int64 `json:"id"`
Command []string `json:"command"`
WaitURL string `json:"wait_url"`
}
// JobResult is the response body for a completed job.
type JobResult struct {
ID int64 `json:"id"` ID int64 `json:"id"`
Command []string `json:"command"` Command []string `json:"command"`
Stdin *string `json:"stdin"` Stdin *string `json:"stdin"`
@@ -34,60 +51,195 @@ type AddJobOut struct {
Status int32 `json:"status"` Status int32 `json:"status"`
} }
// AddJob creates and executes a job on a target agent. // WaitJobIn is the request body for waiting on a job.
// @Summary Create and run a job on an agent type WaitJobIn struct {
// @Description Sends a command to the specified agent, waits for execution, and returns the result AgentID string `json:"agent_id" binding:"required"`
}
// AddJob submits a job to an agent and returns a wait_url for the result.
// @Summary Submit a job to an agent
// @Description Sends a command to the specified agent and returns a URL to wait for the result
// @Tags jobs // @Tags jobs
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param body body AddJobIn true "Job request" // @Param body body AddJobIn true "Job request"
// @Success 201 {object} AddJobOut // @Success 201 {object} AddJobOut
// @Router /jobs [post] // @Router /jobs [post]
func (self *JobsHandlers) AddJob(c *gin.Context) { func (h *JobsHandlers) AddJob(c *gin.Context) {
err := func() error { var in AddJobIn
var in AddJobIn if err := c.Bind(&in); err != nil {
if err := c.Bind(&in); err != nil { c.Error(err)
return err return
} }
agent, ok := self.cmder.GetAgent(in.AgentID)
if !ok {
c.Status(http.StatusNotFound)
return fmt.Errorf("agent not found")
}
var command []string agent, ok := h.tracker.GetAgent(in.AgentID)
if in.InterpreterID == 0 { if !ok {
command = []string{"sh", "-c", in.Command} c.Status(http.StatusNotFound)
} else { c.Error(fmt.Errorf("agent not found"))
var err error return
command, err = self.svc.ResolveCommand(c.Request.Context(), in.InterpreterID, in.Command) }
if err != nil {
return err
}
}
jid, err := agent.AddJob(models.JobForInsert{ command, err := resolveCommand(c, h.svc, in.InterpreterID, in.Command)
Command: command,
Stdin: in.Stdin,
})
if err != nil {
return err
}
job, err := agent.WaitJob(jid)
if err != nil {
return err
}
c.JSON(http.StatusCreated, AddJobOut{
ID: job.ID,
Command: job.Command,
Stdin: job.Stdin,
Stdout: job.Stdout,
Stderr: job.Stderr,
Status: job.Status,
})
return nil
}()
if err != nil { if err != nil {
c.Error(err) c.Error(err)
return
} }
jid, err := agent.AddJob(models.JobForInsert{
Command: command,
Stdin: in.Stdin,
})
if err != nil {
c.Error(err)
return
}
waitURL := fmt.Sprintf("%s/api/v1/jobs/%d/wait", h.whereami, jid)
c.JSON(http.StatusCreated, AddJobOut{
ID: jid,
Command: command,
WaitURL: waitURL,
})
}
// WaitJob waits for a submitted job to complete (long-poll).
// If the job is already done, returns immediately.
// @Summary Wait for job result
// @Description Long-polls for a job result. Returns immediately if the job is already finished.
// @Tags jobs
// @Accept json
// @Produce json
// @Param id path int true "Job ID"
// @Param body body WaitJobIn true "Agent reference"
// @Success 200 {object} JobResult
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Router /jobs/{id}/wait [post]
func (h *JobsHandlers) WaitJob(c *gin.Context) {
jid, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid job id"})
return
}
var in WaitJobIn
if err := c.Bind(&in); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
agent, ok := h.tracker.GetAgent(in.AgentID)
if !ok {
c.Status(http.StatusNotFound)
c.Error(fmt.Errorf("agent not found"))
return
}
job, err := agent.WaitJob(jid)
if err != nil {
c.Error(err)
return
}
c.JSON(http.StatusOK, JobResult{
ID: job.ID,
Command: job.Command,
Stdin: job.Stdin,
Stdout: job.Stdout,
Stderr: job.Stderr,
Status: job.Status,
})
}
func resolveCommand(c *gin.Context, svc *service.ScriptService, interpID int64, cmd string) ([]string, error) {
if interpID == 0 {
return []string{"sh", "-c", cmd}, nil
}
command, err := svc.ResolveCommand(c.Request.Context(), interpID, cmd)
if err != nil {
return nil, err
}
return command, nil
}
// @Summary Check command path
// @Description Validates that a command binary exists on the system
// @Tags jobs
// @Accept json
// @Param body body CheckCmdIn true "Command to check"
// @Success 200 {object} CheckCmdOut
// @Failure 404 {object} map[string]string
// @Router /jobs/check_cmd [post]
func (h *JobsHandlers) CheckCmd(c *gin.Context) {
var in struct {
Command string `json:"command" binding:"required"`
}
if err := c.Bind(&in); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"})
return
}
if _, err := exec.LookPath(in.Command); err != nil {
if errors.Is(err, exec.ErrNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "command not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, CheckCmdOut{Exists: true})
}
type CheckCmdIn struct {
Command string `json:"command" binding:"required" example:"bash"`
}
type CheckCmdOut struct {
Exists bool `json:"exists"`
}
// JobMetricsOut is the response body for the job metrics endpoint.
type JobMetricsOut struct {
Total int `json:"total"`
Success int `json:"success"`
Failed int `json:"failed"`
Pending int `json:"pending"`
Period string `json:"period"`
}
// GetJobMetrics returns job success metrics over a parameterized period.
// @Summary Get job metrics
// @Description Returns total, successful, failed, and pending job counts over the given period
// @Tags jobs
// @Produce json
// @Param period query string false "Time period (e.g. 1h, 24h, 7d)" default(24h)
// @Success 200 {object} JobMetricsOut
// @Failure 400 {object} map[string]string
// @Security Bearer
// @Router /jobs/metrics [get]
func (h *JobsHandlers) GetJobMetrics(c *gin.Context) {
periodStr := c.DefaultQuery("period", "24h")
period, err := time.ParseDuration(periodStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid period, use Go duration format (e.g. 1h, 24h, 7d)"})
return
}
since := time.Now().Add(-period)
metrics, err := h.jobRepo.GetJobMetrics(c.Request.Context(), since)
if err != nil {
c.Error(err)
return
}
c.JSON(http.StatusOK, JobMetricsOut{
Total: metrics.Total,
Success: metrics.Success,
Failed: metrics.Failed,
Pending: metrics.Pending,
Period: periodStr,
})
} }
+9 -9
View File
@@ -20,10 +20,10 @@ func NewLogHandlers(logRepo *repository.LogRepository) *LogHandlers {
type InsertLogRequest struct { type InsertLogRequest struct {
Timestamp time.Time `json:"timestamp"` Timestamp time.Time `json:"timestamp"`
Level string `json:"level" binding:"required"` Level string `json:"level" binding:"required"`
Service string `json:"service" binding:"required"` Service string `json:"service" binding:"required"`
Agent string `json:"agent" binding:"required"` Agent string `json:"agent" binding:"required"`
Message string `json:"message" binding:"required"` Message string `json:"message" binding:"required"`
} }
// @Summary Insert log entry // @Summary Insert log entry
@@ -105,13 +105,13 @@ func (lh *LogHandlers) InsertBatch(c *gin.Context) {
} }
type SearchLogsRequest struct { type SearchLogsRequest struct {
Level string `form:"level"` Level string `form:"level"`
Service string `form:"service"` Service string `form:"service"`
Agent string `form:"agent"` Agent string `form:"agent"`
DateFrom string `form:"date_from"` DateFrom string `form:"date_from"`
DateTo string `form:"date_to"` DateTo string `form:"date_to"`
Limit int `form:"limit"` Limit int `form:"limit"`
Offset int `form:"offset"` Offset int `form:"offset"`
} }
// @Summary Search logs // @Summary Search logs
+43 -31
View File
@@ -13,12 +13,28 @@ import (
) )
type ScriptHandlers struct { type ScriptHandlers struct {
svc *service.ScriptService svc *service.ScriptService
cmder *commander.Commander tracker *commander.ConnTracker
} }
func NewScriptHandlers(svc *service.ScriptService, cmder *commander.Commander) ScriptHandlers { func NewScriptHandlers(svc *service.ScriptService, tracker *commander.ConnTracker) ScriptHandlers {
return ScriptHandlers{svc: svc, cmder: cmder} return ScriptHandlers{svc: svc, tracker: tracker}
}
type RunScriptIn struct {
AgentID string `json:"agent_id" binding:"required"`
InterpreterID int64 `json:"interpreter_id" binding:"required"`
ScriptText string `json:"script_text" binding:"required"`
Stdin *string `json:"stdin"`
}
type RunScriptOut struct {
ID int64 `json:"id"`
Command []string `json:"command"`
Stdin *string `json:"stdin"`
Stdout string `json:"stdout"`
Stderr string `json:"stderr"`
Status int32 `json:"status"`
} }
// RunScript executes a script on a target agent. // RunScript executes a script on a target agent.
@@ -29,26 +45,25 @@ func NewScriptHandlers(svc *service.ScriptService, cmder *commander.Commander) S
// @Produce json // @Produce json
// @Param body body RunScriptIn true "Script request" // @Param body body RunScriptIn true "Script request"
// @Success 201 {object} RunScriptOut // @Success 201 {object} RunScriptOut
// @Security Bearer
// @Router /scripts/run [post] // @Router /scripts/run [post]
func (self *ScriptHandlers) RunScript(c *gin.Context) { func (h *ScriptHandlers) RunScript(c *gin.Context) {
err := func() error { err := func() error {
type RunScriptIn struct {
AgentID string `json:"agent_id" binding:"required"`
InterpreterID int64 `json:"interpreter_id" binding:"required"`
ScriptText string `json:"script_text" binding:"required"`
Stdin *string `json:"stdin"`
}
var in RunScriptIn var in RunScriptIn
if err := c.Bind(&in); err != nil { if err := c.Bind(&in); err != nil {
return err return err
} }
command, err := self.svc.ResolveCommand(c.Request.Context(), in.InterpreterID, in.ScriptText) command, err := h.svc.ResolveCommand(
c.Request.Context(),
in.InterpreterID,
in.ScriptText,
)
if err != nil { if err != nil {
return err return err
} }
agent, ok := self.cmder.GetAgent(in.AgentID) agent, ok := h.tracker.GetAgent(in.AgentID)
if !ok { if !ok {
c.Status(http.StatusNotFound) c.Status(http.StatusNotFound)
return fmt.Errorf("agent not found") return fmt.Errorf("agent not found")
@@ -67,14 +82,6 @@ func (self *ScriptHandlers) RunScript(c *gin.Context) {
return err return err
} }
type RunScriptOut struct {
ID int64 `json:"id"`
Command []string `json:"command"`
Stdin *string `json:"stdin"`
Stdout string `json:"stdout"`
Stderr string `json:"stderr"`
Status int32 `json:"status"`
}
c.JSON(http.StatusCreated, RunScriptOut{ c.JSON(http.StatusCreated, RunScriptOut{
ID: job.ID, ID: job.ID,
Command: job.Command, Command: job.Command,
@@ -96,9 +103,10 @@ func (self *ScriptHandlers) RunScript(c *gin.Context) {
// @Tags scripts // @Tags scripts
// @Produce json // @Produce json
// @Success 200 {array} repository.ScriptInterpreter // @Success 200 {array} repository.ScriptInterpreter
// @Security Bearer
// @Router /scripts/interpreters [get] // @Router /scripts/interpreters [get]
func (self *ScriptHandlers) ListInterpreters(c *gin.Context) { func (h *ScriptHandlers) ListInterpreters(c *gin.Context) {
interpreters, err := self.svc.List(c.Request.Context()) interpreters, err := h.svc.List(c.Request.Context())
if err != nil { if err != nil {
c.Error(err) c.Error(err)
return return
@@ -114,15 +122,16 @@ func (self *ScriptHandlers) ListInterpreters(c *gin.Context) {
// @Produce json // @Produce json
// @Param body body repository.ScriptInterpreterCreate true "Interpreter definition" // @Param body body repository.ScriptInterpreterCreate true "Interpreter definition"
// @Success 201 {object} repository.ScriptInterpreter // @Success 201 {object} repository.ScriptInterpreter
// @Security Bearer
// @Router /scripts/interpreters [post] // @Router /scripts/interpreters [post]
func (self *ScriptHandlers) CreateInterpreter(c *gin.Context) { func (h *ScriptHandlers) CreateInterpreter(c *gin.Context) {
var in repository.ScriptInterpreterCreate var in repository.ScriptInterpreterCreate
if err := c.BindJSON(&in); err != nil { if err := c.BindJSON(&in); err != nil {
c.Error(err) c.Error(err)
return return
} }
si, err := self.svc.Create(c.Request.Context(), in) si, err := h.svc.Create(c.Request.Context(), in)
if err != nil { if err != nil {
c.Error(err) c.Error(err)
return return
@@ -137,15 +146,16 @@ func (self *ScriptHandlers) CreateInterpreter(c *gin.Context) {
// @Produce json // @Produce json
// @Param id path int true "Interpreter ID" // @Param id path int true "Interpreter ID"
// @Success 200 {object} repository.ScriptInterpreter // @Success 200 {object} repository.ScriptInterpreter
// @Security Bearer
// @Router /scripts/interpreters/:id [get] // @Router /scripts/interpreters/:id [get]
func (self *ScriptHandlers) GetInterpreter(c *gin.Context) { func (h *ScriptHandlers) GetInterpreter(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64) id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil { if err != nil {
c.Error(err) c.Error(err)
return return
} }
si, err := self.svc.GetByID(c.Request.Context(), id) si, err := h.svc.GetByID(c.Request.Context(), id)
if err != nil { if err != nil {
c.Error(err) c.Error(err)
return return
@@ -162,8 +172,9 @@ func (self *ScriptHandlers) GetInterpreter(c *gin.Context) {
// @Param id path int true "Interpreter ID" // @Param id path int true "Interpreter ID"
// @Param body body repository.ScriptInterpreterUpdate true "Interpreter fields" // @Param body body repository.ScriptInterpreterUpdate true "Interpreter fields"
// @Success 200 {object} repository.ScriptInterpreter // @Success 200 {object} repository.ScriptInterpreter
// @Security Bearer
// @Router /scripts/interpreters/:id [put] // @Router /scripts/interpreters/:id [put]
func (self *ScriptHandlers) UpdateInterpreter(c *gin.Context) { func (h *ScriptHandlers) UpdateInterpreter(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64) id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil { if err != nil {
c.Error(err) c.Error(err)
@@ -176,7 +187,7 @@ func (self *ScriptHandlers) UpdateInterpreter(c *gin.Context) {
return return
} }
si, err := self.svc.Update(c.Request.Context(), id, in) si, err := h.svc.Update(c.Request.Context(), id, in)
if err != nil { if err != nil {
c.Error(err) c.Error(err)
return return
@@ -190,15 +201,16 @@ func (self *ScriptHandlers) UpdateInterpreter(c *gin.Context) {
// @Tags scripts // @Tags scripts
// @Param id path int true "Interpreter ID" // @Param id path int true "Interpreter ID"
// @Success 204 // @Success 204
// @Security Bearer
// @Router /scripts/interpreters/:id [delete] // @Router /scripts/interpreters/:id [delete]
func (self *ScriptHandlers) DeleteInterpreter(c *gin.Context) { func (h *ScriptHandlers) DeleteInterpreter(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64) id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil { if err != nil {
c.Error(err) c.Error(err)
return return
} }
if err := self.svc.Delete(c.Request.Context(), id); err != nil { if err := h.svc.Delete(c.Request.Context(), id); err != nil {
c.Error(err) c.Error(err)
return return
} }
+274
View File
@@ -0,0 +1,274 @@
package handlers
import (
"errors"
"fmt"
"net/http"
"strconv"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/grpcsrv/commander"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/models"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/repository"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/service"
"github.com/gin-gonic/gin"
)
// ScriptHandlersGroup handles script management routes.
type ScriptHandlersGroup struct {
svc *service.ScriptService
cmder *commander.Commander
}
// NewScriptHandlersGroup creates a new ScriptHandlersGroup.
func NewScriptHandlersGroup(svc *service.ScriptService, cmder *commander.Commander) *ScriptHandlersGroup {
return &ScriptHandlersGroup{svc: svc, cmder: cmder}
}
// GetTree returns the script directory tree.
// @Summary Get script directory tree
// @Description Returns a hierarchical tree of all scripts organized by their paths
// @Tags scripts
// @Produce json
// @Success 200 {array} repository.ScriptTreeNode
// @Security Bearer
// @Router /scripts/tree [get]
func (sh *ScriptHandlersGroup) GetTree(c *gin.Context) {
tree, err := sh.svc.BuildTree()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to build script tree"})
return
}
if tree == nil {
tree = []repository.ScriptTreeNode{}
}
c.JSON(http.StatusOK, tree)
}
// CreateScript creates a new script.
// @Summary Create script
// @Description Creates a new script with path, content, and interpreter binding
// @Tags scripts
// @Accept json
// @Produce json
// @Param body body repository.ScriptCreate true "Script data"
// @Success 201 {object} repository.Script
// @Security Bearer
// @Router /scripts [post]
func (sh *ScriptHandlersGroup) CreateScript(c *gin.Context) {
var req repository.ScriptCreate
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
script, err := sh.svc.Repo.CreateScript(req)
if err != nil {
if isUniqueConstraint(err) {
c.JSON(http.StatusConflict, gin.H{"error": "script with this path already exists"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create script"})
return
}
c.JSON(http.StatusCreated, script)
}
// GetScript returns a script by ID.
// @Summary Get script
// @Description Returns a script by its ID
// @Tags scripts
// @Produce json
// @Param id path int true "Script ID"
// @Success 200 {object} repository.Script
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Security Bearer
// @Router /scripts/:id [get]
func (sh *ScriptHandlersGroup) GetScript(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
script, err := sh.svc.Repo.GetScript(id)
if err != nil {
if errors.Is(err, repository.ErrNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "script not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get script"})
return
}
c.JSON(http.StatusOK, script)
}
// UpdateScript updates a script.
// @Summary Update script
// @Description Updates a script's path, content, or interpreter
// @Tags scripts
// @Accept json
// @Produce json
// @Param id path int true "Script ID"
// @Param body body repository.ScriptUpdate true "Script data"
// @Success 200 {object} repository.Script
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Security Bearer
// @Router /scripts/:id [put]
func (sh *ScriptHandlersGroup) UpdateScript(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
var req repository.ScriptUpdate
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
script, err := sh.svc.Repo.UpdateScript(id, req)
if err != nil {
if errors.Is(err, repository.ErrNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "script not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update script"})
return
}
c.JSON(http.StatusOK, script)
}
// DeleteScript deletes a script.
// @Summary Delete script
// @Description Deletes a script by its ID
// @Tags scripts
// @Param id path int true "Script ID"
// @Success 200 {object} map[string]string
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Security Bearer
// @Router /scripts/:id [delete]
func (sh *ScriptHandlersGroup) DeleteScript(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
if err := sh.svc.Repo.DeleteScript(id); err != nil {
if errors.Is(err, repository.ErrNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "script not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete script"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "script deleted"})
}
// RunScriptByID executes a stored script on a target agent.
// @Summary Run script by ID
// @Description Loads a script from storage, resolves interpreter command, and executes on the specified agent
// @Tags scripts
// @Accept json
// @Produce json
// @Param id path int true "Script ID"
// @Param body body RunStoredScriptIn true "Agent token and optional stdin"
// @Success 201 {object} RunScriptOut
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Security Bearer
// @Router /scripts/:id/run [post]
func (sh *ScriptHandlersGroup) RunScriptByID(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
var in RunStoredScriptIn
if err := c.ShouldBindJSON(&in); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
script, err := sh.svc.Repo.GetScript(id)
if err != nil {
if errors.Is(err, repository.ErrNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "script not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get script"})
return
}
command, err := sh.svc.ResolveCommand(c.Request.Context(), script.InterpreterID, script.Content)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to resolve command: %v", err)})
return
}
agent, ok := sh.cmder.GetAgent(in.Token)
if !ok {
c.JSON(http.StatusNotFound, gin.H{"error": "agent not found"})
return
}
jid, err := agent.AddJob(models.JobForInsert{
Command: command,
Stdin: in.Stdin,
})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to add job: %v", err)})
return
}
job, err := agent.WaitJob(jid)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("job execution failed: %v", err)})
return
}
c.JSON(http.StatusCreated, RunScriptOut{
ID: job.ID,
Command: job.Command,
Stdin: job.Stdin,
Stdout: job.Stdout,
Stderr: job.Stderr,
Status: job.Status,
})
}
// RunStoredScriptIn is the request body for running a stored script on an agent.
type RunStoredScriptIn struct {
Token string `json:"token" binding:"required"`
Stdin *string `json:"stdin"`
}
// isUniqueConstraint checks if the error is a SQLite UNIQUE constraint violation.
func isUniqueConstraint(err error) bool {
return err != nil && (err.Error() != "" && contains(err.Error(), "UNIQUE constraint"))
}
func contains(s, substr string) bool {
return len(s) >= len(substr) && searchSubstring(s, substr)
}
func searchSubstring(s, substr string) bool {
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return true
}
}
return false
}
+50 -8
View File
@@ -5,6 +5,7 @@ import (
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"fmt" "fmt"
"time"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/models" "gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/models"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/storage" "gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/storage"
@@ -23,7 +24,11 @@ func (r *JobRepository) Init(ctx context.Context) error {
return err return err
} }
func (r *JobRepository) InitJob(ctx context.Context, agentID string, job models.JobForInsert) (int64, error) { func (r *JobRepository) InitJob(
ctx context.Context,
agentID string,
job models.JobForInsert,
) (int64, error) {
commandJSON, err := json.Marshal(job.Command) commandJSON, err := json.Marshal(job.Command)
if err != nil { if err != nil {
return 0, fmt.Errorf("marshal command: %w", err) return 0, fmt.Errorf("marshal command: %w", err)
@@ -34,9 +39,12 @@ func (r *JobRepository) InitJob(ctx context.Context, agentID string, job models.
stdinVal = job.Stdin stdinVal = job.Stdin
} }
result, err := r.DB.ExecContext(ctx, result, err := r.DB.ExecContext(
ctx,
`INSERT INTO jobs (agent_id, command, stdin, stdout, stderr, status) VALUES (?, ?, ?, '', '', 0)`, `INSERT INTO jobs (agent_id, command, stdin, stdout, stderr, status) VALUES (?, ?, ?, '', '', 0)`,
agentID, string(commandJSON), stdinVal, agentID,
string(commandJSON),
stdinVal,
) )
if err != nil { if err != nil {
return 0, err return 0, err
@@ -45,10 +53,18 @@ func (r *JobRepository) InitJob(ctx context.Context, agentID string, job models.
return result.LastInsertId() return result.LastInsertId()
} }
func (r *JobRepository) UpdateJobInDB(ctx context.Context, jid int64, msg models.JobForUpdate) (models.Job, error) { func (r *JobRepository) UpdateJobInDB(
result, err := r.DB.ExecContext(ctx, ctx context.Context,
jid int64,
msg models.JobForUpdate,
) (models.Job, error) {
result, err := r.DB.ExecContext(
ctx,
`UPDATE jobs SET stdout = ?, stderr = ?, status = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, `UPDATE jobs SET stdout = ?, stderr = ?, status = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`,
msg.Stdout, msg.Stderr, msg.Status, jid, msg.Stdout,
msg.Stderr,
msg.Status,
jid,
) )
if err != nil { if err != nil {
return models.Job{}, err return models.Job{}, err
@@ -81,10 +97,36 @@ func (r *JobRepository) GetJobByID(ctx context.Context, jid int64) (models.Job,
return models.Job{}, err return models.Job{}, err
} }
if err := json.Unmarshal([]byte(commandJSON), &job.JobForInsert.Command); err != nil { if err := json.Unmarshal([]byte(commandJSON), &job.Command); err != nil {
return models.Job{}, fmt.Errorf("unmarshal command: %w", err) return models.Job{}, fmt.Errorf("unmarshal command: %w", err)
} }
job.JobForInsert.Stdin = stdinVal job.Stdin = stdinVal
return job, nil return job, nil
} }
type JobMetrics struct {
Total int
Success int
Failed int
Pending int
}
// GetJobMetrics returns job success metrics for jobs updated since the given time.
// A successful job has status == 0, failed has status != 0, pending has status == 0 with empty stdout/stderr.
func (r *JobRepository) GetJobMetrics(ctx context.Context, since time.Time) (JobMetrics, error) {
var m JobMetrics
err := r.DB.QueryRowContext(ctx,
`SELECT
COUNT(*),
SUM(CASE WHEN status = 0 AND (stdout != '' OR stderr != '') THEN 1 ELSE 0 END),
SUM(CASE WHEN status != 0 THEN 1 ELSE 0 END),
SUM(CASE WHEN status = 0 AND stdout = '' AND stderr = '' THEN 1 ELSE 0 END)
FROM jobs WHERE updated_at >= ?`,
since,
).Scan(&m.Total, &m.Success, &m.Failed, &m.Pending)
if err != nil {
return JobMetrics{}, err
}
return m, nil
}
+12 -6
View File
@@ -84,13 +84,13 @@ func (r *LogRepository) InsertBatch(ctx context.Context, logs []storage.LogEntry
} }
type LogFilter struct { type LogFilter struct {
Level string Level string
Service string Service string
Agent string Agent string
DateFrom time.Time DateFrom time.Time
DateTo time.Time DateTo time.Time
Limit int Limit int
Offset int Offset int
} }
func (r *LogRepository) Search(ctx context.Context, filter LogFilter) ([]storage.LogEntry, error) { func (r *LogRepository) Search(ctx context.Context, filter LogFilter) ([]storage.LogEntry, error) {
@@ -157,7 +157,13 @@ func (r *LogRepository) Search(ctx context.Context, filter LogFilter) ([]storage
logs := make([]storage.LogEntry, 0) logs := make([]storage.LogEntry, 0)
for rows.Next() { for rows.Next() {
var log storage.LogEntry var log storage.LogEntry
if err := rows.Scan(&log.Timestamp, &log.Level, &log.Service, &log.Agent, &log.Message); err != nil { if err := rows.Scan(
&log.Timestamp,
&log.Level,
&log.Service,
&log.Agent,
&log.Message,
); err != nil {
return nil, err return nil, err
} }
logs = append(logs, log) logs = append(logs, log)
+74 -32
View File
@@ -2,29 +2,37 @@ package repository
// Tokens represents a user record with info and permissions. // Tokens represents a user record with info and permissions.
type Tokens struct { type Tokens struct {
ID int64 `json:"id"` ID int64 `json:"id"`
Name string `json:"name"` Name string `json:"name"`
LastName string `json:"last_name"` LastName string `json:"last_name"`
Login string `json:"login"` Login string `json:"login"`
Token string `json:"token"` Token string `json:"token"`
PermissionView bool `json:"permission_view"`
PermissionManage bool `json:"permission_manage_agent"`
PermissionAdmin bool `json:"permission_admin"`
IsActive bool `json:"is_active"`
}
// TokenCreate is the request body for creating a new user.
type TokenCreate struct {
Name string `json:"name" binding:"required"`
LastName string `json:"last_name" binding:"required"`
Login string `json:"login" binding:"required"`
Password string `json:"password" binding:"required"`
PermissionView bool `json:"permission_view"` PermissionView bool `json:"permission_view"`
PermissionManage bool `json:"permission_manage_agent"` PermissionManage bool `json:"permission_manage_agent"`
PermissionAdmin bool `json:"permission_admin"` PermissionAdmin bool `json:"permission_admin"`
IsActive bool `json:"is_active"` IsActive bool `json:"is_active"`
} }
// TokenCreate is the request body for creating a new user.
type TokenCreate struct {
Name string `json:"name" binding:"required"`
LastName string `json:"last_name" binding:"required"`
Login string `json:"login" binding:"required"`
Password string `json:"password" binding:"required"`
PermissionView bool `json:"permission_view"`
PermissionManage bool `json:"permission_manage_agent"`
PermissionAdmin bool `json:"permission_admin"`
IsActive bool `json:"is_active"`
}
// UserRegister is the request body for public user registration (all permissions false).
type UserRegister struct {
Name string `json:"name" binding:"required"`
LastName string `json:"last_name" binding:"required"`
Login string `json:"login" binding:"required"`
Password string `json:"password" binding:"required"`
}
// TokenUpdate is the request body for updating an existing user. // TokenUpdate is the request body for updating an existing user.
type TokenUpdate struct { type TokenUpdate struct {
Name string `json:"name"` Name string `json:"name"`
@@ -51,7 +59,7 @@ type BatchActionRequest struct {
// LoginRequest is the request body for login. // LoginRequest is the request body for login.
type LoginRequest struct { type LoginRequest struct {
Login string `json:"login" binding:"required"` Login string `json:"login" binding:"required"`
Password string `json:"password" binding:"required"` Password string `json:"password" binding:"required"`
} }
@@ -109,14 +117,14 @@ const (
// AgentDeployConfig represents the configuration for deploying an agent to a server // AgentDeployConfig represents the configuration for deploying an agent to a server
// @Description Configuration for deploying HellreigN agent to a single server // @Description Configuration for deploying HellreigN agent to a single server
type AgentDeployConfig struct { type AgentDeployConfig struct {
User string `json:"user" binding:"required" example:"admin" description:"SSH username"` User string `json:"user" binding:"required" example:"admin" description:"SSH username"`
IP string `json:"ip" binding:"required" example:"192.168.1.100" description:"Server IP address"` IP string `json:"ip" binding:"required" example:"192.168.1.100" description:"Server IP address"`
Port int `json:"port" example:"22" description:"SSH port (default: 22)"` Port int `json:"port" example:"22" description:"SSH port (default: 22)"`
AuthMethod AuthMethod `json:"authMethod" binding:"required" example:"key" description:"SSH auth method: key or password"` AuthMethod AuthMethod `json:"authMethod" binding:"required" example:"key" description:"SSH auth method: key or password"`
SSHKey string `json:"sshKey,omitempty" example:"-----BEGIN OPENSSH PRIVATE KEY-----" description:"SSH private key (required if authMethod=key)"` SSHKey string `json:"sshKey,omitempty" example:"-----BEGIN OPENSSH PRIVATE KEY-----" description:"SSH private key (required if authMethod=key)"`
Password string `json:"password,omitempty" example:"secret" description:"SSH password (required if authMethod=password)"` Password string `json:"password,omitempty" example:"secret" description:"SSH password (required if authMethod=password)"`
DeployType DeployType `json:"deployType" binding:"required" example:"docker" description:"Deployment type: docker or binary"` DeployType DeployType `json:"deployType" binding:"required" example:"docker" description:"Deployment type: docker or binary"`
AgentLabel string `json:"agentLabel" binding:"required" example:"production-server-1" description:"Unique label for the agent"` AgentLabel string `json:"agentLabel" binding:"required" example:"production-server-1" description:"Unique label for the agent"`
} }
// DeployAgentsRequest represents the request body for deploying agents to multiple servers // DeployAgentsRequest represents the request body for deploying agents to multiple servers
@@ -129,15 +137,49 @@ type DeployAgentsRequest struct {
// @Description Response containing deployment results and registration tokens // @Description Response containing deployment results and registration tokens
type DeployResponse struct { type DeployResponse struct {
Message string `json:"message" example:"Deployment completed"` Message string `json:"message" example:"Deployment completed"`
Results []DeployResult `json:"results" description:"Deployment results for each server"` Results []DeployResult `json:"results" description:"Deployment results for each server"`
} }
// DeployResult represents the result of deploying to a single server // DeployResult represents the result of deploying to a single server
// @Description Result of deploying to a single server // @Description Result of deploying to a single server
type DeployResult struct { type DeployResult struct {
IP string `json:"ip" example:"192.168.1.100" description:"Server IP address"` IP string `json:"ip" example:"192.168.1.100" description:"Server IP address"`
AgentLabel string `json:"agent_label" example:"production-server-1" description:"Agent label"` AgentLabel string `json:"agent_label" example:"production-server-1" description:"Agent label"`
Token string `json:"token" example:"abc123..." description:"Registration token for agent registration"` Token string `json:"token" example:"abc123..." description:"Registration token for agent registration"`
Success bool `json:"success" example:"true" description:"Whether deployment succeeded"` Success bool `json:"success" example:"true" description:"Whether deployment succeeded"`
Error string `json:"error,omitempty" example:"" description:"Error message if deployment failed"` Error string `json:"error,omitempty" example:"" description:"Error message if deployment failed"`
}
// Script represents a stored script with path and interpreter binding.
type Script struct {
ID int64 `json:"id"`
Path string `json:"path"`
Content string `json:"content"`
InterpreterID int64 `json:"interpreter_id"`
CreatedAt *string `json:"created_at"`
UpdatedAt *string `json:"updated_at"`
}
// ScriptCreate is the request body for creating a script.
type ScriptCreate struct {
Path string `json:"path" binding:"required"`
Content string `json:"content"`
InterpreterID int64 `json:"interpreter_id" binding:"required"`
}
// ScriptUpdate is the request body for updating a script.
type ScriptUpdate struct {
Path *string `json:"path"`
Content *string `json:"content"`
InterpreterID *int64 `json:"interpreter_id"`
}
// ScriptTreeNode represents a node in the script directory tree.
type ScriptTreeNode struct {
Name string `json:"name"`
Type string `json:"type"` // "folder" or "file"
Children []ScriptTreeNode `json:"children,omitempty"`
ID *int64 `json:"id,omitempty"`
Content *string `json:"content,omitempty"`
InterpreterID *int64 `json:"interpreter_id,omitempty"`
} }
+187 -9
View File
@@ -3,6 +3,8 @@ package repository
import ( import (
"database/sql" "database/sql"
"errors" "errors"
"fmt"
"log"
"strconv" "strconv"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/storage" "gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/storage"
@@ -50,8 +52,15 @@ func (r *Repository) CreateToken(tc TokenCreate) (string, error) {
result, err := r.DB.Exec( result, err := r.DB.Exec(
`INSERT INTO tokens (name, last_name, login, password, token, permission_view, permission_manage_agent, permission_admin, is_active) `INSERT INTO tokens (name, last_name, login, password, token, permission_view, permission_manage_agent, permission_admin, is_active)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
tc.Name, tc.LastName, tc.Login, string(hashed), token, tc.Name,
tc.PermissionView, tc.PermissionManage, tc.PermissionAdmin, tc.IsActive, tc.LastName,
tc.Login,
string(hashed),
token,
tc.PermissionView,
tc.PermissionManage,
tc.PermissionAdmin,
tc.IsActive,
) )
if err != nil { if err != nil {
return "", err return "", err
@@ -64,6 +73,39 @@ func (r *Repository) CreateToken(tc TokenCreate) (string, error) {
return strconv.FormatInt(id, 10), nil return strconv.FormatInt(id, 10), nil
} }
// RegisterUser inserts a new user with all permissions set to false and is_active=false.
func (r *Repository) RegisterUser(ur UserRegister) (string, error) {
hashed, err := bcrypt.GenerateFromPassword([]byte(ur.Password), bcrypt.DefaultCost)
if err != nil {
return "", fmt.Errorf("hash password: %w", err)
}
token, err := utils.RandomToken()
if err != nil {
return "", fmt.Errorf("generate token: %w", err)
}
result, err := r.DB.Exec(
`INSERT INTO tokens (name, last_name, login, password, token, permission_view, permission_manage_agent, permission_admin, is_active)
VALUES (?, ?, ?, ?, ?, 0, 0, 0, 0)`,
ur.Name,
ur.LastName,
ur.Login,
string(hashed),
token,
)
if err != nil {
return "", fmt.Errorf("insert user: %w", err)
}
id, err := result.LastInsertId()
if err != nil {
return "", fmt.Errorf("get last insert id: %w", err)
}
log.Printf("[register] user created: id=%s login=%s", strconv.FormatInt(id, 10), ur.Login)
return strconv.FormatInt(id, 10), nil
}
// Login authenticates by login/password, generates a new token, and returns LoginResponse. // Login authenticates by login/password, generates a new token, and returns LoginResponse.
func (r *Repository) Login(login, password string) (*LoginResponse, error) { func (r *Repository) Login(login, password string) (*LoginResponse, error) {
var t Tokens var t Tokens
@@ -118,11 +160,11 @@ func (r *Repository) Login(login, password string) (*LoginResponse, error) {
func (r *Repository) GetToken(token string) (*Tokens, error) { func (r *Repository) GetToken(token string) (*Tokens, error) {
var t Tokens var t Tokens
err := r.DB.QueryRow( err := r.DB.QueryRow(
`SELECT id, name, last_name, login, token, permission_view, permission_manage_agent, permission_admin `SELECT id, name, last_name, login, token, permission_view, permission_manage_agent, permission_admin, is_active
FROM tokens WHERE token = ?`, FROM tokens WHERE token = ?`,
token, token,
).Scan(&t.ID, &t.Name, &t.LastName, &t.Login, &t.Token, ).Scan(&t.ID, &t.Name, &t.LastName, &t.Login, &t.Token,
&t.PermissionView, &t.PermissionManage, &t.PermissionAdmin) &t.PermissionView, &t.PermissionManage, &t.PermissionAdmin, &t.IsActive)
if err != nil { if err != nil {
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
@@ -136,7 +178,7 @@ func (r *Repository) GetToken(token string) (*Tokens, error) {
// ListTokens returns all users without password and token. // ListTokens returns all users without password and token.
func (r *Repository) ListTokens() ([]Tokens, error) { func (r *Repository) ListTokens() ([]Tokens, error) {
rows, err := r.DB.Query( rows, err := r.DB.Query(
`SELECT id, name, last_name, login, permission_view, permission_manage_agent, permission_admin `SELECT id, name, last_name, login, permission_view, permission_manage_agent, permission_admin, is_active
FROM tokens`, FROM tokens`,
) )
if err != nil { if err != nil {
@@ -148,7 +190,7 @@ func (r *Repository) ListTokens() ([]Tokens, error) {
for rows.Next() { for rows.Next() {
var t Tokens var t Tokens
if err := rows.Scan(&t.ID, &t.Name, &t.LastName, &t.Login, if err := rows.Scan(&t.ID, &t.Name, &t.LastName, &t.Login,
&t.PermissionView, &t.PermissionManage, &t.PermissionAdmin); err != nil { &t.PermissionView, &t.PermissionManage, &t.PermissionAdmin, &t.IsActive); err != nil {
return nil, err return nil, err
} }
tokens = append(tokens, t) tokens = append(tokens, t)
@@ -302,12 +344,13 @@ func (r *Repository) ActivateUserByLogin(login string) error {
login, login,
) )
if err != nil { if err != nil {
return err return fmt.Errorf("activate exec: %w", err)
} }
affected, err := result.RowsAffected() affected, err := result.RowsAffected()
if err != nil { if err != nil {
return err return fmt.Errorf("rows affected: %w", err)
} }
log.Printf("[activate] login=%s affected=%d", login, affected)
if affected == 0 { if affected == 0 {
return ErrNotFound return ErrNotFound
} }
@@ -422,7 +465,11 @@ func (r *Repository) UpdatePermissions(login string, update TokenUpdatePermissio
result, err := r.DB.Exec( result, err := r.DB.Exec(
`UPDATE tokens SET permission_view = ?, permission_manage_agent = ?, permission_admin = ?, is_active = ? WHERE login = ?`, `UPDATE tokens SET permission_view = ?, permission_manage_agent = ?, permission_admin = ?, is_active = ? WHERE login = ?`,
newView, newManage, newAdmin, newActive, login, newView,
newManage,
newAdmin,
newActive,
login,
) )
if err != nil { if err != nil {
return err return err
@@ -460,3 +507,134 @@ func (r *Repository) UpdatePassword(login string, newPassword string) error {
} }
return nil return nil
} }
// CreateScript inserts a new script into the database.
func (r *Repository) CreateScript(sc ScriptCreate) (*Script, error) {
result, err := r.DB.Exec(
`INSERT INTO scripts (path, content, interpreter_id) VALUES (?, ?, ?)`,
sc.Path, sc.Content, sc.InterpreterID,
)
if err != nil {
return nil, fmt.Errorf("insert script: %w", err)
}
id, err := result.LastInsertId()
if err != nil {
return nil, fmt.Errorf("get last insert id: %w", err)
}
return &Script{
ID: id,
Path: sc.Path,
Content: sc.Content,
InterpreterID: sc.InterpreterID,
}, nil
}
// GetScript retrieves a script by ID.
func (r *Repository) GetScript(id int64) (*Script, error) {
var s Script
err := r.DB.QueryRow(
`SELECT id, path, content, interpreter_id, created_at, updated_at FROM scripts WHERE id = ?`,
id,
).Scan(&s.ID, &s.Path, &s.Content, &s.InterpreterID, &s.CreatedAt, &s.UpdatedAt)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
}
return nil, err
}
return &s, nil
}
// GetScriptByPath retrieves a script by its path.
func (r *Repository) GetScriptByPath(path string) (*Script, error) {
var s Script
err := r.DB.QueryRow(
`SELECT id, path, content, interpreter_id, created_at, updated_at FROM scripts WHERE path = ?`,
path,
).Scan(&s.ID, &s.Path, &s.Content, &s.InterpreterID, &s.CreatedAt, &s.UpdatedAt)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
}
return nil, err
}
return &s, nil
}
// ListScripts returns all scripts.
func (r *Repository) ListScripts() ([]Script, error) {
rows, err := r.DB.Query(
`SELECT id, path, content, interpreter_id, created_at, updated_at FROM scripts`,
)
if err != nil {
return nil, err
}
defer rows.Close()
var scripts []Script
for rows.Next() {
var s Script
if err := rows.Scan(&s.ID, &s.Path, &s.Content, &s.InterpreterID, &s.CreatedAt, &s.UpdatedAt); err != nil {
return nil, err
}
scripts = append(scripts, s)
}
return scripts, rows.Err()
}
// UpdateScript updates a script by ID.
func (r *Repository) UpdateScript(id int64, update ScriptUpdate) (*Script, error) {
existing, err := r.GetScript(id)
if err != nil {
return nil, err
}
newPath := existing.Path
newContent := existing.Content
newInterpreterID := existing.InterpreterID
if update.Path != nil {
newPath = *update.Path
}
if update.Content != nil {
newContent = *update.Content
}
if update.InterpreterID != nil {
newInterpreterID = *update.InterpreterID
}
_, err = r.DB.Exec(
`UPDATE scripts SET path = ?, content = ?, interpreter_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`,
newPath, newContent, newInterpreterID, id,
)
if err != nil {
return nil, fmt.Errorf("update script: %w", err)
}
return &Script{
ID: id,
Path: newPath,
Content: newContent,
InterpreterID: newInterpreterID,
}, nil
}
// DeleteScript deletes a script by ID.
func (r *Repository) DeleteScript(id int64) error {
result, err := r.DB.Exec(`DELETE FROM scripts WHERE id = ?`, id)
if err != nil {
return err
}
affected, err := result.RowsAffected()
if err != nil {
return err
}
if affected == 0 {
return ErrNotFound
}
return nil
}
@@ -20,9 +20,9 @@ type ScriptInterpreter struct {
} }
type ScriptInterpreterCreate struct { type ScriptInterpreterCreate struct {
Name string `json:"name" binding:"required"` Name string `json:"name" binding:"required"`
Label string `json:"label" binding:"required"` Label string `json:"label" binding:"required"`
Argv []string `json:"argv" binding:"required"` Argv []string `json:"argv" binding:"required"`
} }
type ScriptInterpreterUpdate struct { type ScriptInterpreterUpdate struct {
@@ -44,7 +44,10 @@ func (r *ScriptInterpreterRepo) Init(ctx context.Context) error {
return err return err
} }
func (r *ScriptInterpreterRepo) Create(ctx context.Context, in ScriptInterpreterCreate) (*ScriptInterpreter, error) { func (r *ScriptInterpreterRepo) Create(
ctx context.Context,
in ScriptInterpreterCreate,
) (*ScriptInterpreter, error) {
argvJSON, err := json.Marshal(in.Argv) argvJSON, err := json.Marshal(in.Argv)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -71,7 +74,8 @@ func (r *ScriptInterpreterRepo) GetByID(ctx context.Context, id int64) (*ScriptI
var argvJSON string var argvJSON string
var createdAt, updatedAt string var createdAt, updatedAt string
err := r.DB.QueryRowContext(ctx, err := r.DB.QueryRowContext(
ctx,
`SELECT id, name, label, argv, created_at, updated_at FROM script_interpreters WHERE id = ?`, `SELECT id, name, label, argv, created_at, updated_at FROM script_interpreters WHERE id = ?`,
id, id,
).Scan(&si.ID, &si.Name, &si.Label, &argvJSON, &createdAt, &updatedAt) ).Scan(&si.ID, &si.Name, &si.Label, &argvJSON, &createdAt, &updatedAt)
@@ -103,7 +107,14 @@ func (r *ScriptInterpreterRepo) List(ctx context.Context) ([]ScriptInterpreter,
for rows.Next() { for rows.Next() {
var si ScriptInterpreter var si ScriptInterpreter
var argvJSON, createdAt, updatedAt string var argvJSON, createdAt, updatedAt string
if err := rows.Scan(&si.ID, &si.Name, &si.Label, &argvJSON, &createdAt, &updatedAt); err != nil { if err := rows.Scan(
&si.ID,
&si.Name,
&si.Label,
&argvJSON,
&createdAt,
&updatedAt,
); err != nil {
return nil, err return nil, err
} }
if err := json.Unmarshal([]byte(argvJSON), &si.Argv); err != nil { if err := json.Unmarshal([]byte(argvJSON), &si.Argv); err != nil {
@@ -116,7 +127,11 @@ func (r *ScriptInterpreterRepo) List(ctx context.Context) ([]ScriptInterpreter,
return interpreters, rows.Err() return interpreters, rows.Err()
} }
func (r *ScriptInterpreterRepo) Update(ctx context.Context, id int64, in ScriptInterpreterUpdate) (*ScriptInterpreter, error) { func (r *ScriptInterpreterRepo) Update(
ctx context.Context,
id int64,
in ScriptInterpreterUpdate,
) (*ScriptInterpreter, error) {
si, err := r.GetByID(ctx, id) si, err := r.GetByID(ctx, id)
if err != nil { if err != nil {
return nil, err return nil, err
+141 -23
View File
@@ -3,52 +3,170 @@ package service
import ( import (
"context" "context"
"fmt" "fmt"
"sort"
"strings"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/repository" "gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/repository"
) )
// ScriptService handles script CRUD, tree building, and interpreter resolution.
type ScriptService struct { type ScriptService struct {
repo *repository.ScriptInterpreterRepo Repo *repository.Repository
InterpreterRepo *repository.ScriptInterpreterRepo
} }
func NewScriptService(repo *repository.ScriptInterpreterRepo) *ScriptService { // NewScriptService creates a new ScriptService with both script and interpreter repos.
return &ScriptService{repo: repo} func NewScriptService(repo *repository.Repository) *ScriptService {
return &ScriptService{Repo: repo}
} }
// ResolveCommand builds the full argv[] by prepending the interpreter's argv // NewScriptServiceWithInterpreters creates a ScriptService with interpreter support.
// to the script text (as the last argument). func NewScriptServiceWithInterpreters(repo *repository.Repository, interpRepo *repository.ScriptInterpreterRepo) *ScriptService {
func (self *ScriptService) ResolveCommand(ctx context.Context, interpreterID int64, scriptText string) ([]string, error) { return &ScriptService{Repo: repo, InterpreterRepo: interpRepo}
interpreter, err := self.repo.GetByID(ctx, interpreterID) }
// treeNode is an internal representation for building the tree.
type treeNode struct {
name string
typ string // "folder" or "file"
children map[string]*treeNode
// File-specific fields
id *int64
content *string
interpreterID *int64
}
// BuildTree builds a directory tree from all scripts in the database.
// Each script path is treated as a file path (e.g. "deploy/nginx/restart.sh").
func (s *ScriptService) BuildTree() ([]repository.ScriptTreeNode, error) {
scripts, err := s.Repo.ListScripts()
if err != nil { if err != nil {
return nil, err return nil, err
} }
if len(interpreter.Argv) == 0 { root := make(map[string]*treeNode)
return nil, fmt.Errorf("interpreter %q has empty argv", interpreter.Name)
for _, sc := range scripts {
parts := strings.Split(sc.Path, "/")
// Walk through path parts, creating folders as needed
currentMap := root
for i, part := range parts {
isFile := i == len(parts)-1
if _, exists := currentMap[part]; !exists {
node := &treeNode{
name: part,
children: make(map[string]*treeNode),
}
if isFile {
node.typ = "file"
id := sc.ID
content := sc.Content
interpreterID := sc.InterpreterID
node.id = &id
node.content = &content
node.interpreterID = &interpreterID
} else {
node.typ = "folder"
}
currentMap[part] = node
}
currentMap = currentMap[part].children
}
} }
argv := make([]string, len(interpreter.Argv)+1) return buildTreeSlice(root), nil
copy(argv, interpreter.Argv)
argv[len(argv)-1] = scriptText
return argv, nil
} }
func (self *ScriptService) Create(ctx context.Context, in repository.ScriptInterpreterCreate) (*repository.ScriptInterpreter, error) { // buildTreeSlice converts a map of treeNodes to a sorted slice of ScriptTreeNode.
return self.repo.Create(ctx, in) func buildTreeSlice(m map[string]*treeNode) []repository.ScriptTreeNode {
result := make([]repository.ScriptTreeNode, 0, len(m))
for _, node := range m {
result = append(result, toScriptTreeNode(node))
}
// Sort: folders first, then files, alphabetically within each group
sort.Slice(result, func(i, j int) bool {
if result[i].Type != result[j].Type {
return result[i].Type == "folder"
}
return result[i].Name < result[j].Name
})
return result
} }
func (self *ScriptService) GetByID(ctx context.Context, id int64) (*repository.ScriptInterpreter, error) { // toScriptTreeNode converts a treeNode to a ScriptTreeNode with recursively converted children.
return self.repo.GetByID(ctx, id) func toScriptTreeNode(node *treeNode) repository.ScriptTreeNode {
result := repository.ScriptTreeNode{
Name: node.name,
Type: node.typ,
Children: []repository.ScriptTreeNode{},
}
if node.typ == "file" {
result.ID = node.id
result.Content = node.content
result.InterpreterID = node.interpreterID
} else {
result.Children = buildTreeSlice(node.children)
}
return result
} }
func (self *ScriptService) List(ctx context.Context) ([]repository.ScriptInterpreter, error) { // ResolveCommand resolves the full command for a script using its interpreter.
return self.repo.List(ctx) func (s *ScriptService) ResolveCommand(ctx context.Context, interpreterID int64, scriptText string) ([]string, error) {
if s.InterpreterRepo == nil {
return nil, fmt.Errorf("interpreter repo not configured")
}
interpreter, err := s.InterpreterRepo.GetByID(ctx, interpreterID)
if err != nil {
return nil, fmt.Errorf("get interpreter: %w", err)
}
// Build command: argv[0] argv[1] ... -c scriptText
cmd := append(interpreter.Argv, "-c", scriptText)
return cmd, nil
} }
func (self *ScriptService) Update(ctx context.Context, id int64, in repository.ScriptInterpreterUpdate) (*repository.ScriptInterpreter, error) { // List returns all interpreters.
return self.repo.Update(ctx, id, in) func (s *ScriptService) List(ctx context.Context) ([]repository.ScriptInterpreter, error) {
if s.InterpreterRepo == nil {
return nil, fmt.Errorf("interpreter repo not configured")
}
return s.InterpreterRepo.List(ctx)
} }
func (self *ScriptService) Delete(ctx context.Context, id int64) error { // Create creates a new interpreter.
return self.repo.Delete(ctx, id) func (s *ScriptService) Create(ctx context.Context, in repository.ScriptInterpreterCreate) (*repository.ScriptInterpreter, error) {
if s.InterpreterRepo == nil {
return nil, fmt.Errorf("interpreter repo not configured")
}
return s.InterpreterRepo.Create(ctx, in)
}
// GetByID returns an interpreter by ID.
func (s *ScriptService) GetByID(ctx context.Context, id int64) (*repository.ScriptInterpreter, error) {
if s.InterpreterRepo == nil {
return nil, fmt.Errorf("interpreter repo not configured")
}
return s.InterpreterRepo.GetByID(ctx, id)
}
// Update updates an interpreter.
func (s *ScriptService) Update(ctx context.Context, id int64, in repository.ScriptInterpreterUpdate) (*repository.ScriptInterpreter, error) {
if s.InterpreterRepo == nil {
return nil, fmt.Errorf("interpreter repo not configured")
}
return s.InterpreterRepo.Update(ctx, id, in)
}
// Delete deletes an interpreter.
func (s *ScriptService) Delete(ctx context.Context, id int64) error {
if s.InterpreterRepo == nil {
return fmt.Errorf("interpreter repo not configured")
}
return s.InterpreterRepo.Delete(ctx, id)
} }
+17 -3
View File
@@ -43,7 +43,11 @@ func OpenClickHouse(cfg ClickHouseConfig) (*sql.DB, error) {
} }
// OpenClickHouseWithRetry attempts to connect to ClickHouse with retries and backoff. // OpenClickHouseWithRetry attempts to connect to ClickHouse with retries and backoff.
func OpenClickHouseWithRetry(cfg ClickHouseConfig, maxRetries int, initialDelay time.Duration) (*sql.DB, error) { func OpenClickHouseWithRetry(
cfg ClickHouseConfig,
maxRetries int,
initialDelay time.Duration,
) (*sql.DB, error) {
var lastErr error var lastErr error
delay := initialDelay delay := initialDelay
@@ -53,10 +57,20 @@ func OpenClickHouseWithRetry(cfg ClickHouseConfig, maxRetries int, initialDelay
return db, nil return db, nil
} }
lastErr = err lastErr = err
log.Printf("ClickHouse connection attempt %d/%d failed: %v, retrying in %v...", i+1, maxRetries, err, delay) log.Printf(
"ClickHouse connection attempt %d/%d failed: %v, retrying in %v...",
i+1,
maxRetries,
err,
delay,
)
time.Sleep(delay) time.Sleep(delay)
delay *= 2 delay *= 2
} }
return nil, fmt.Errorf("clickhouse connection failed after %d attempts: %w", maxRetries, lastErr) return nil, fmt.Errorf(
"clickhouse connection failed after %d attempts: %w",
maxRetries,
lastErr,
)
} }
+12
View File
@@ -57,6 +57,18 @@ CREATE TABLE IF NOT EXISTS script_interpreters (
); );
` `
const CreateScriptsTable = `
CREATE TABLE IF NOT EXISTS scripts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
path TEXT NOT NULL UNIQUE,
content TEXT NOT NULL DEFAULT '',
interpreter_id INTEGER NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (interpreter_id) REFERENCES script_interpreters(id)
);
`
const CreateLogsTable = ` const CreateLogsTable = `
CREATE TABLE IF NOT EXISTS logs ( CREATE TABLE IF NOT EXISTS logs (
timestamp DateTime64(3) DEFAULT now(), timestamp DateTime64(3) DEFAULT now(),
+11 -1
View File
@@ -3,6 +3,7 @@ package storage
import ( import (
"database/sql" "database/sql"
"fmt" "fmt"
"log"
"strings" "strings"
_ "modernc.org/sqlite" _ "modernc.org/sqlite"
@@ -37,7 +38,16 @@ func Open(path string) (*sql.DB, error) {
} }
// Migration: add is_active column if it doesn't exist // Migration: add is_active column if it doesn't exist
_, _ = db.Exec(AddIsActiveColumn) if _, err := db.Exec(AddIsActiveColumn); err != nil {
log.Printf("[sqlite] WARNING: failed to add is_active column: %v", err)
} else {
log.Println("[sqlite] is_active column migration applied")
}
// Create scripts table if not exists
if _, err := db.Exec(CreateScriptsTable); err != nil {
return nil, fmt.Errorf("migrate scripts: %w", err)
}
return db, nil return db, nil
} }
+1
View File
@@ -5,6 +5,7 @@ import (
"encoding/hex" "encoding/hex"
) )
// TOOD: fuck
func RandomToken() (string, error) { func RandomToken() (string, error) {
token := make([]byte, 32) token := make([]byte, 32)
if _, err := rand.Read(token); err != nil { if _, err := rand.Read(token); err != nil {
+1 -1
View File
@@ -1,7 +1,7 @@
backend_url: http://backend:8080 backend_url: http://backend:8080
grpc_url: backend:9001 grpc_url: backend:9001
label: test-agent-1 label: test-agent-1
registration_token: "156616b56774d59ba53f1eb4b096488bb5f755bbf5b737d93a42bb1b583ad7fb" registration_token: "58b1cd3857774f690e4534ec222af4ec08eaae8cd5577614365f2b19c78d03d6"
cert_dir: /etc/hellreign-agent/certs cert_dir: /etc/hellreign-agent/certs
services: services:
- name: system - name: system
+15
View File
@@ -0,0 +1,15 @@
CREATE TABLE [jobs_new_17f2f1dd010f] (
[id] INTEGER PRIMARY KEY,
[agent_id] TEXT NOT NULL,
[command] TEXT NOT NULL,
[stdin] TEXT,
[stdout] TEXT,
[stderr] TEXT,
[status] INTEGER,
[created_at] FLOAT DEFAULT CURRENT_TIMESTAMP,
[updated_at] FLOAT DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO [jobs_new_17f2f1dd010f] ([rowid], [id], [agent_id], [command], [stdin], [stdout], [stderr], [status], [created_at], [updated_at])
SELECT [rowid], [id], [agent_id], [command], [stdin], [stdout], [stderr], [status], [created_at], [updated_at] FROM [jobs];
DROP TABLE [jobs];
ALTER TABLE [jobs_new_17f2f1dd010f] RENAME TO [jobs];
+11
View File
@@ -6,6 +6,16 @@ option go_package="gitea.d3m0k1d.ru/d3m0k1d/HellreigN/proto/proto";
service Collector { service Collector {
rpc Stream(stream CollectorRequest) returns (CollectorResponse); rpc Stream(stream CollectorRequest) returns (CollectorResponse);
rpc ReportServices(ServicesUpdate) returns (ServicesUpdateResp);
}
message ServicesUpdateResp {
}
message ServicesUpdate {
message ServiceUpdate {
string name = 1;
string status = 2;
}
repeated ServiceUpdate services = 1;
} }
message CollectorRequest { message CollectorRequest {
@@ -31,3 +41,4 @@ message FinishedCommand {
string stdout = 3; string stdout = 3;
string stderr = 4; string stderr = 4;
} }
+176 -31
View File
@@ -21,6 +21,86 @@ const (
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
) )
type ServicesUpdateResp struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ServicesUpdateResp) Reset() {
*x = ServicesUpdateResp{}
mi := &file_hellreign_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ServicesUpdateResp) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ServicesUpdateResp) ProtoMessage() {}
func (x *ServicesUpdateResp) ProtoReflect() protoreflect.Message {
mi := &file_hellreign_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ServicesUpdateResp.ProtoReflect.Descriptor instead.
func (*ServicesUpdateResp) Descriptor() ([]byte, []int) {
return file_hellreign_proto_rawDescGZIP(), []int{0}
}
type ServicesUpdate struct {
state protoimpl.MessageState `protogen:"open.v1"`
Services []*ServicesUpdate_ServiceUpdate `protobuf:"bytes,1,rep,name=services,proto3" json:"services,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ServicesUpdate) Reset() {
*x = ServicesUpdate{}
mi := &file_hellreign_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ServicesUpdate) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ServicesUpdate) ProtoMessage() {}
func (x *ServicesUpdate) ProtoReflect() protoreflect.Message {
mi := &file_hellreign_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ServicesUpdate.ProtoReflect.Descriptor instead.
func (*ServicesUpdate) Descriptor() ([]byte, []int) {
return file_hellreign_proto_rawDescGZIP(), []int{1}
}
func (x *ServicesUpdate) GetServices() []*ServicesUpdate_ServiceUpdate {
if x != nil {
return x.Services
}
return nil
}
type CollectorRequest struct { type CollectorRequest struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
@@ -30,7 +110,7 @@ type CollectorRequest struct {
func (x *CollectorRequest) Reset() { func (x *CollectorRequest) Reset() {
*x = CollectorRequest{} *x = CollectorRequest{}
mi := &file_hellreign_proto_msgTypes[0] mi := &file_hellreign_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -42,7 +122,7 @@ func (x *CollectorRequest) String() string {
func (*CollectorRequest) ProtoMessage() {} func (*CollectorRequest) ProtoMessage() {}
func (x *CollectorRequest) ProtoReflect() protoreflect.Message { func (x *CollectorRequest) ProtoReflect() protoreflect.Message {
mi := &file_hellreign_proto_msgTypes[0] mi := &file_hellreign_proto_msgTypes[2]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -55,7 +135,7 @@ func (x *CollectorRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use CollectorRequest.ProtoReflect.Descriptor instead. // Deprecated: Use CollectorRequest.ProtoReflect.Descriptor instead.
func (*CollectorRequest) Descriptor() ([]byte, []int) { func (*CollectorRequest) Descriptor() ([]byte, []int) {
return file_hellreign_proto_rawDescGZIP(), []int{0} return file_hellreign_proto_rawDescGZIP(), []int{2}
} }
func (x *CollectorRequest) GetMessage() string { func (x *CollectorRequest) GetMessage() string {
@@ -73,7 +153,7 @@ type CollectorResponse struct {
func (x *CollectorResponse) Reset() { func (x *CollectorResponse) Reset() {
*x = CollectorResponse{} *x = CollectorResponse{}
mi := &file_hellreign_proto_msgTypes[1] mi := &file_hellreign_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -85,7 +165,7 @@ func (x *CollectorResponse) String() string {
func (*CollectorResponse) ProtoMessage() {} func (*CollectorResponse) ProtoMessage() {}
func (x *CollectorResponse) ProtoReflect() protoreflect.Message { func (x *CollectorResponse) ProtoReflect() protoreflect.Message {
mi := &file_hellreign_proto_msgTypes[1] mi := &file_hellreign_proto_msgTypes[3]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -98,7 +178,7 @@ func (x *CollectorResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use CollectorResponse.ProtoReflect.Descriptor instead. // Deprecated: Use CollectorResponse.ProtoReflect.Descriptor instead.
func (*CollectorResponse) Descriptor() ([]byte, []int) { func (*CollectorResponse) Descriptor() ([]byte, []int) {
return file_hellreign_proto_rawDescGZIP(), []int{1} return file_hellreign_proto_rawDescGZIP(), []int{3}
} }
type Command struct { type Command struct {
@@ -112,7 +192,7 @@ type Command struct {
func (x *Command) Reset() { func (x *Command) Reset() {
*x = Command{} *x = Command{}
mi := &file_hellreign_proto_msgTypes[2] mi := &file_hellreign_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -124,7 +204,7 @@ func (x *Command) String() string {
func (*Command) ProtoMessage() {} func (*Command) ProtoMessage() {}
func (x *Command) ProtoReflect() protoreflect.Message { func (x *Command) ProtoReflect() protoreflect.Message {
mi := &file_hellreign_proto_msgTypes[2] mi := &file_hellreign_proto_msgTypes[4]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -137,7 +217,7 @@ func (x *Command) ProtoReflect() protoreflect.Message {
// Deprecated: Use Command.ProtoReflect.Descriptor instead. // Deprecated: Use Command.ProtoReflect.Descriptor instead.
func (*Command) Descriptor() ([]byte, []int) { func (*Command) Descriptor() ([]byte, []int) {
return file_hellreign_proto_rawDescGZIP(), []int{2} return file_hellreign_proto_rawDescGZIP(), []int{4}
} }
func (x *Command) GetId() int64 { func (x *Command) GetId() int64 {
@@ -173,7 +253,7 @@ type FinishedCommand struct {
func (x *FinishedCommand) Reset() { func (x *FinishedCommand) Reset() {
*x = FinishedCommand{} *x = FinishedCommand{}
mi := &file_hellreign_proto_msgTypes[3] mi := &file_hellreign_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -185,7 +265,7 @@ func (x *FinishedCommand) String() string {
func (*FinishedCommand) ProtoMessage() {} func (*FinishedCommand) ProtoMessage() {}
func (x *FinishedCommand) ProtoReflect() protoreflect.Message { func (x *FinishedCommand) ProtoReflect() protoreflect.Message {
mi := &file_hellreign_proto_msgTypes[3] mi := &file_hellreign_proto_msgTypes[5]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -198,7 +278,7 @@ func (x *FinishedCommand) ProtoReflect() protoreflect.Message {
// Deprecated: Use FinishedCommand.ProtoReflect.Descriptor instead. // Deprecated: Use FinishedCommand.ProtoReflect.Descriptor instead.
func (*FinishedCommand) Descriptor() ([]byte, []int) { func (*FinishedCommand) Descriptor() ([]byte, []int) {
return file_hellreign_proto_rawDescGZIP(), []int{3} return file_hellreign_proto_rawDescGZIP(), []int{5}
} }
func (x *FinishedCommand) GetId() int64 { func (x *FinishedCommand) GetId() int64 {
@@ -229,11 +309,69 @@ func (x *FinishedCommand) GetStderr() string {
return "" return ""
} }
type ServicesUpdate_ServiceUpdate struct {
state protoimpl.MessageState `protogen:"open.v1"`
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
Status string `protobuf:"bytes,2,opt,name=status,proto3" json:"status,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ServicesUpdate_ServiceUpdate) Reset() {
*x = ServicesUpdate_ServiceUpdate{}
mi := &file_hellreign_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ServicesUpdate_ServiceUpdate) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ServicesUpdate_ServiceUpdate) ProtoMessage() {}
func (x *ServicesUpdate_ServiceUpdate) ProtoReflect() protoreflect.Message {
mi := &file_hellreign_proto_msgTypes[6]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ServicesUpdate_ServiceUpdate.ProtoReflect.Descriptor instead.
func (*ServicesUpdate_ServiceUpdate) Descriptor() ([]byte, []int) {
return file_hellreign_proto_rawDescGZIP(), []int{1, 0}
}
func (x *ServicesUpdate_ServiceUpdate) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *ServicesUpdate_ServiceUpdate) GetStatus() string {
if x != nil {
return x.Status
}
return ""
}
var File_hellreign_proto protoreflect.FileDescriptor var File_hellreign_proto protoreflect.FileDescriptor
const file_hellreign_proto_rawDesc = "" + const file_hellreign_proto_rawDesc = "" +
"\n" + "\n" +
"\x0fhellreign.proto\x12\x04chat\",\n" + "\x0fhellreign.proto\x12\x04chat\"\x14\n" +
"\x12ServicesUpdateResp\"\x8d\x01\n" +
"\x0eServicesUpdate\x12>\n" +
"\bservices\x18\x01 \x03(\v2\".chat.ServicesUpdate.ServiceUpdateR\bservices\x1a;\n" +
"\rServiceUpdate\x12\x12\n" +
"\x04name\x18\x01 \x01(\tR\x04name\x12\x16\n" +
"\x06status\x18\x02 \x01(\tR\x06status\",\n" +
"\x10CollectorRequest\x12\x18\n" + "\x10CollectorRequest\x12\x18\n" +
"\amessage\x18\x01 \x01(\tR\amessage\"\x13\n" + "\amessage\x18\x01 \x01(\tR\amessage\"\x13\n" +
"\x11CollectorResponse\"X\n" + "\x11CollectorResponse\"X\n" +
@@ -246,9 +384,10 @@ const file_hellreign_proto_rawDesc = "" +
"\x02id\x18\x01 \x01(\x03R\x02id\x12\x16\n" + "\x02id\x18\x01 \x01(\x03R\x02id\x12\x16\n" +
"\x06status\x18\x02 \x01(\x05R\x06status\x12\x16\n" + "\x06status\x18\x02 \x01(\x05R\x06status\x12\x16\n" +
"\x06stdout\x18\x03 \x01(\tR\x06stdout\x12\x16\n" + "\x06stdout\x18\x03 \x01(\tR\x06stdout\x12\x16\n" +
"\x06stderr\x18\x04 \x01(\tR\x06stderr2H\n" + "\x06stderr\x18\x04 \x01(\tR\x06stderr2\x8a\x01\n" +
"\tCollector\x12;\n" + "\tCollector\x12;\n" +
"\x06Stream\x12\x16.chat.CollectorRequest\x1a\x17.chat.CollectorResponse(\x012?\n" + "\x06Stream\x12\x16.chat.CollectorRequest\x1a\x17.chat.CollectorResponse(\x01\x12@\n" +
"\x0eReportServices\x12\x14.chat.ServicesUpdate\x1a\x18.chat.ServicesUpdateResp2?\n" +
"\tCommander\x122\n" + "\tCommander\x122\n" +
"\x06Stream\x12\x15.chat.FinishedCommand\x1a\r.chat.Command(\x010\x01B0Z.gitea.d3m0k1d.ru/d3m0k1d/HellreigN/proto/protob\x06proto3" "\x06Stream\x12\x15.chat.FinishedCommand\x1a\r.chat.Command(\x010\x01B0Z.gitea.d3m0k1d.ru/d3m0k1d/HellreigN/proto/protob\x06proto3"
@@ -264,23 +403,29 @@ func file_hellreign_proto_rawDescGZIP() []byte {
return file_hellreign_proto_rawDescData return file_hellreign_proto_rawDescData
} }
var file_hellreign_proto_msgTypes = make([]protoimpl.MessageInfo, 4) var file_hellreign_proto_msgTypes = make([]protoimpl.MessageInfo, 7)
var file_hellreign_proto_goTypes = []any{ var file_hellreign_proto_goTypes = []any{
(*CollectorRequest)(nil), // 0: chat.CollectorRequest (*ServicesUpdateResp)(nil), // 0: chat.ServicesUpdateResp
(*CollectorResponse)(nil), // 1: chat.CollectorResponse (*ServicesUpdate)(nil), // 1: chat.ServicesUpdate
(*Command)(nil), // 2: chat.Command (*CollectorRequest)(nil), // 2: chat.CollectorRequest
(*FinishedCommand)(nil), // 3: chat.FinishedCommand (*CollectorResponse)(nil), // 3: chat.CollectorResponse
(*Command)(nil), // 4: chat.Command
(*FinishedCommand)(nil), // 5: chat.FinishedCommand
(*ServicesUpdate_ServiceUpdate)(nil), // 6: chat.ServicesUpdate.ServiceUpdate
} }
var file_hellreign_proto_depIdxs = []int32{ var file_hellreign_proto_depIdxs = []int32{
0, // 0: chat.Collector.Stream:input_type -> chat.CollectorRequest 6, // 0: chat.ServicesUpdate.services:type_name -> chat.ServicesUpdate.ServiceUpdate
3, // 1: chat.Commander.Stream:input_type -> chat.FinishedCommand 2, // 1: chat.Collector.Stream:input_type -> chat.CollectorRequest
1, // 2: chat.Collector.Stream:output_type -> chat.CollectorResponse 1, // 2: chat.Collector.ReportServices:input_type -> chat.ServicesUpdate
2, // 3: chat.Commander.Stream:output_type -> chat.Command 5, // 3: chat.Commander.Stream:input_type -> chat.FinishedCommand
2, // [2:4] is the sub-list for method output_type 3, // 4: chat.Collector.Stream:output_type -> chat.CollectorResponse
0, // [0:2] is the sub-list for method input_type 0, // 5: chat.Collector.ReportServices:output_type -> chat.ServicesUpdateResp
0, // [0:0] is the sub-list for extension type_name 4, // 6: chat.Commander.Stream:output_type -> chat.Command
0, // [0:0] is the sub-list for extension extendee 4, // [4:7] is the sub-list for method output_type
0, // [0:0] is the sub-list for field type_name 1, // [1:4] is the sub-list for method input_type
1, // [1:1] is the sub-list for extension type_name
1, // [1:1] is the sub-list for extension extendee
0, // [0:1] is the sub-list for field type_name
} }
func init() { file_hellreign_proto_init() } func init() { file_hellreign_proto_init() }
@@ -288,14 +433,14 @@ func file_hellreign_proto_init() {
if File_hellreign_proto != nil { if File_hellreign_proto != nil {
return return
} }
file_hellreign_proto_msgTypes[2].OneofWrappers = []any{} file_hellreign_proto_msgTypes[4].OneofWrappers = []any{}
type x struct{} type x struct{}
out := protoimpl.TypeBuilder{ out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{ File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_hellreign_proto_rawDesc), len(file_hellreign_proto_rawDesc)), RawDescriptor: unsafe.Slice(unsafe.StringData(file_hellreign_proto_rawDesc), len(file_hellreign_proto_rawDesc)),
NumEnums: 0, NumEnums: 0,
NumMessages: 4, NumMessages: 7,
NumExtensions: 0, NumExtensions: 0,
NumServices: 2, NumServices: 2,
}, },
+41 -2
View File
@@ -19,7 +19,8 @@ import (
const _ = grpc.SupportPackageIsVersion9 const _ = grpc.SupportPackageIsVersion9
const ( const (
Collector_Stream_FullMethodName = "/chat.Collector/Stream" Collector_Stream_FullMethodName = "/chat.Collector/Stream"
Collector_ReportServices_FullMethodName = "/chat.Collector/ReportServices"
) )
// CollectorClient is the client API for Collector service. // CollectorClient is the client API for Collector service.
@@ -27,6 +28,7 @@ const (
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type CollectorClient interface { type CollectorClient interface {
Stream(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[CollectorRequest, CollectorResponse], error) Stream(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[CollectorRequest, CollectorResponse], error)
ReportServices(ctx context.Context, in *ServicesUpdate, opts ...grpc.CallOption) (*ServicesUpdateResp, error)
} }
type collectorClient struct { type collectorClient struct {
@@ -50,11 +52,22 @@ func (c *collectorClient) Stream(ctx context.Context, opts ...grpc.CallOption) (
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type Collector_StreamClient = grpc.ClientStreamingClient[CollectorRequest, CollectorResponse] type Collector_StreamClient = grpc.ClientStreamingClient[CollectorRequest, CollectorResponse]
func (c *collectorClient) ReportServices(ctx context.Context, in *ServicesUpdate, opts ...grpc.CallOption) (*ServicesUpdateResp, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ServicesUpdateResp)
err := c.cc.Invoke(ctx, Collector_ReportServices_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// CollectorServer is the server API for Collector service. // CollectorServer is the server API for Collector service.
// All implementations must embed UnimplementedCollectorServer // All implementations must embed UnimplementedCollectorServer
// for forward compatibility. // for forward compatibility.
type CollectorServer interface { type CollectorServer interface {
Stream(grpc.ClientStreamingServer[CollectorRequest, CollectorResponse]) error Stream(grpc.ClientStreamingServer[CollectorRequest, CollectorResponse]) error
ReportServices(context.Context, *ServicesUpdate) (*ServicesUpdateResp, error)
mustEmbedUnimplementedCollectorServer() mustEmbedUnimplementedCollectorServer()
} }
@@ -68,6 +81,9 @@ type UnimplementedCollectorServer struct{}
func (UnimplementedCollectorServer) Stream(grpc.ClientStreamingServer[CollectorRequest, CollectorResponse]) error { func (UnimplementedCollectorServer) Stream(grpc.ClientStreamingServer[CollectorRequest, CollectorResponse]) error {
return status.Error(codes.Unimplemented, "method Stream not implemented") return status.Error(codes.Unimplemented, "method Stream not implemented")
} }
func (UnimplementedCollectorServer) ReportServices(context.Context, *ServicesUpdate) (*ServicesUpdateResp, error) {
return nil, status.Error(codes.Unimplemented, "method ReportServices not implemented")
}
func (UnimplementedCollectorServer) mustEmbedUnimplementedCollectorServer() {} func (UnimplementedCollectorServer) mustEmbedUnimplementedCollectorServer() {}
func (UnimplementedCollectorServer) testEmbeddedByValue() {} func (UnimplementedCollectorServer) testEmbeddedByValue() {}
@@ -96,13 +112,36 @@ func _Collector_Stream_Handler(srv interface{}, stream grpc.ServerStream) error
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type Collector_StreamServer = grpc.ClientStreamingServer[CollectorRequest, CollectorResponse] type Collector_StreamServer = grpc.ClientStreamingServer[CollectorRequest, CollectorResponse]
func _Collector_ReportServices_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ServicesUpdate)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(CollectorServer).ReportServices(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Collector_ReportServices_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(CollectorServer).ReportServices(ctx, req.(*ServicesUpdate))
}
return interceptor(ctx, in, info, handler)
}
// Collector_ServiceDesc is the grpc.ServiceDesc for Collector service. // Collector_ServiceDesc is the grpc.ServiceDesc for Collector service.
// It's only intended for direct use with grpc.RegisterService, // It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy) // and not to be introspected or modified (even as a copy)
var Collector_ServiceDesc = grpc.ServiceDesc{ var Collector_ServiceDesc = grpc.ServiceDesc{
ServiceName: "chat.Collector", ServiceName: "chat.Collector",
HandlerType: (*CollectorServer)(nil), HandlerType: (*CollectorServer)(nil),
Methods: []grpc.MethodDesc{}, Methods: []grpc.MethodDesc{
{
MethodName: "ReportServices",
Handler: _Collector_ReportServices_Handler,
},
},
Streams: []grpc.StreamDesc{ Streams: []grpc.StreamDesc{
{ {
StreamName: "Stream", StreamName: "Stream",