Compare commits
1 Commits
428140ff15
..
debug
| Author | SHA1 | Date | |
|---|---|---|---|
| abc6cb4e46 |
@@ -1,81 +0,0 @@
|
|||||||
# 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
@@ -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-20260404174628-3389df740c20
|
gitea.d3m0k1d.ru/d3m0k1d/HellreigN/proto v0.0.0-20260403214837-94be9799f47d
|
||||||
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
|
||||||
|
|||||||
@@ -3,19 +3,22 @@ package commander
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/proto/proto"
|
|
||||||
"golang.org/x/sync/errgroup"
|
|
||||||
"io"
|
"io"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
|
||||||
|
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/proto/proto"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CommandExecutor struct{}
|
type CommandExecutor struct {
|
||||||
|
}
|
||||||
|
|
||||||
func (*CommandExecutor) Execute(command *proto.Command) (fc *proto.FinishedCommand, err error) {
|
func (*CommandExecutor) Execute(command *proto.Command) (*proto.FinishedCommand, 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 stdin io.WriteCloser
|
var (
|
||||||
|
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 {
|
||||||
@@ -47,20 +50,16 @@ func (*CommandExecutor) Execute(command *proto.Command) (fc *proto.FinishedComma
|
|||||||
_, err := io.Copy(stderrbuf, stderr)
|
_, err := io.Copy(stderrbuf, stderr)
|
||||||
return err
|
return err
|
||||||
})
|
})
|
||||||
if waitErr := cmd.Wait(); waitErr != nil {
|
if err := cmd.Wait(); err != nil {
|
||||||
var exitErr *exec.ExitError
|
return nil, err
|
||||||
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
|
||||||
}
|
}
|
||||||
fc.Status = int32(cmd.ProcessState.ExitCode())
|
return &proto.FinishedCommand{
|
||||||
fc.Stdout = stdoutbuf.String()
|
Id: command.Id,
|
||||||
fc.Stderr = stderrbuf.String()
|
Status: int32(cmd.ProcessState.ExitCode()),
|
||||||
return
|
Stdout: stdoutbuf.String(),
|
||||||
|
Stderr: stderrbuf.String(),
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ 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"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -111,13 +110,6 @@ 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 {
|
||||||
@@ -309,52 +301,3 @@ 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:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+12
-55
@@ -82,29 +82,19 @@ func main() {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize Collector (log streaming) with its own ConnTracker
|
// Initialize Collector gRPC service
|
||||||
collTracker := collector.NewConnTracker()
|
coll := collector.New(logRepo)
|
||||||
coll := collector.New(logRepo, collTracker)
|
|
||||||
|
|
||||||
// Initialize ConnTracker for Commander agent lifecycle
|
cmdr := commander.New(jobRepo)
|
||||||
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.NewScriptServiceWithInterpreters(h.Repo, scriptRepo)
|
scriptSvc := service.NewScriptService(scriptRepo)
|
||||||
scriptHandlers := handlers.NewScriptHandlers(scriptSvc, cmdTracker)
|
scriptHandlers := handlers.NewScriptHandlers(scriptSvc, cmdr)
|
||||||
jobsHandlers := handlers.NewJobsHandlers(cmdTracker, scriptSvc,
|
jobsHandlers := handlers.NewJobsHandlers(cmdr, 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}
|
||||||
@@ -140,7 +130,6 @@ 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"
|
||||||
@@ -154,14 +143,13 @@ 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", auth.CreateToken)
|
authTokenGroup.POST("/token", handlers.RequireAdmin(), 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)
|
||||||
@@ -170,28 +158,12 @@ 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(
|
authTokenGroup.PUT("/users/:login/permissions", handlers.RequireAdmin(), auth.UpdateUserPermissions)
|
||||||
"/users/:login/permissions",
|
authTokenGroup.PUT("/users/:login/password", handlers.RequireAdmin(), auth.ResetUserPassword)
|
||||||
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(
|
authTokenGroup.POST("/users/:login/activate", handlers.RequireAdmin(), auth.ActivateUser)
|
||||||
"/users/:login/activate",
|
authTokenGroup.POST("/users/:login/deactivate", handlers.RequireAdmin(), auth.DeactivateUser)
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,9 +179,6 @@ 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
|
||||||
@@ -252,14 +221,6 @@ 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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,11 +260,7 @@ func main() {
|
|||||||
MinVersion: tls.VersionTLS12,
|
MinVersion: tls.VersionTLS12,
|
||||||
}
|
}
|
||||||
|
|
||||||
grpcServer := grpc.NewServer(
|
grpcServer := grpc.NewServer(grpc.Creds(credentials.NewTLS(tlsConfig)))
|
||||||
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
@@ -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 sqlite
|
RUN apk add --no-cache curl openssl bash ansible
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
+125
-1090
File diff suppressed because it is too large
Load Diff
+125
-1090
File diff suppressed because it is too large
Load Diff
+96
-706
File diff suppressed because it is too large
Load Diff
+1
-2
@@ -3,10 +3,9 @@ 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-20260404174628-3389df740c20
|
gitea.d3m0k1d.ru/d3m0k1d/HellreigN/proto v0.0.0-20260403210401-a6212c89fc0e
|
||||||
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
|
||||||
|
|||||||
@@ -138,8 +138,6 @@ 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=
|
||||||
|
|||||||
@@ -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,17 +50,8 @@ 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(
|
func (e *Executor) Deploy(ctx context.Context, inventoryPath string, deployType string) ([]DeployResult, error) {
|
||||||
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"
|
||||||
@@ -71,7 +62,6 @@ func (e *Executor) Deploy(
|
|||||||
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,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -94,11 +84,7 @@ func (e *Executor) Deploy(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DeployParallel runs Ansible playbook for multiple inventories in parallel
|
// DeployParallel runs Ansible playbook for multiple inventories in parallel
|
||||||
func (e *Executor) DeployParallel(
|
func (e *Executor) DeployParallel(ctx context.Context, inventoryPaths []string, deployType string) (map[string][]DeployResult, error) {
|
||||||
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))
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ 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
|
||||||
@@ -33,7 +32,6 @@ 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 }}`
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
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
|
||||||
@@ -13,6 +11,7 @@ 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:
|
||||||
@@ -38,29 +37,45 @@ 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: Start HellreigN Agent
|
- name: Create systemd service file
|
||||||
shell: |
|
copy:
|
||||||
nohup {{ install_dir }}/{{ bin_name }} > /dev/null 2>&1 &
|
content: |
|
||||||
echo $!
|
[Unit]
|
||||||
args:
|
Description=HellreigN Agent
|
||||||
executable: /bin/bash
|
After=network.target
|
||||||
environment:
|
|
||||||
CONFIG_FILE: "{{ install_dir }}/config.yml"
|
[Service]
|
||||||
register: agent_pid
|
Type=simple
|
||||||
changed_when: true
|
ExecStart={{ install_dir }}/{{ bin_name }}
|
||||||
|
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
|
||||||
@@ -69,7 +84,6 @@ 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
|
||||||
@@ -103,13 +117,9 @@ 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'
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ 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"
|
||||||
@@ -12,19 +13,26 @@ 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
|
||||||
tracker *ConnTracker
|
agents map[string]*Agent
|
||||||
|
mu sync.RWMutex
|
||||||
batchSize int
|
batchSize int
|
||||||
flushInterval time.Duration
|
flushInterval time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(logRepo *repository.LogRepository, tracker *ConnTracker) *Collector {
|
type Agent struct {
|
||||||
|
ID string
|
||||||
|
Label string
|
||||||
|
Services []string
|
||||||
|
ConnectedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(logRepo *repository.LogRepository) *Collector {
|
||||||
return &Collector{
|
return &Collector{
|
||||||
logRepo: logRepo,
|
logRepo: logRepo,
|
||||||
tracker: tracker,
|
agents: make(map[string]*Agent),
|
||||||
batchSize: 100,
|
batchSize: 100,
|
||||||
flushInterval: 2 * time.Second,
|
flushInterval: 2 * time.Second,
|
||||||
}
|
}
|
||||||
@@ -48,24 +56,33 @@ func (c *Collector) Stream(stream proto.Collector_StreamServer) error {
|
|||||||
}
|
}
|
||||||
service := serviceVals[0]
|
service := serviceVals[0]
|
||||||
|
|
||||||
agent := &Agent{
|
servicesVals := md["services"]
|
||||||
ID: agentName,
|
var services []string
|
||||||
Label: agentName,
|
if len(servicesVals) > 0 {
|
||||||
Services: make([]Service, 0),
|
services = servicesVals
|
||||||
ConnectedAt: time.Now(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.tracker.Register(agent)
|
// Register agent
|
||||||
defer c.tracker.Unregister(agent.ID)
|
c.mu.Lock()
|
||||||
|
c.agents[agentName] = &Agent{
|
||||||
|
ID: agentName,
|
||||||
|
Label: agentName,
|
||||||
|
Services: services,
|
||||||
|
ConnectedAt: time.Now(),
|
||||||
|
}
|
||||||
|
c.mu.Unlock()
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
c.mu.Lock()
|
||||||
|
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(
|
log.Printf("Warning: ClickHouse not connected yet, consuming logs without storing for agent %s", agentName)
|
||||||
"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 {
|
||||||
@@ -103,12 +120,7 @@ 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(
|
log.Printf("Failed to insert batch for agent %s, service %s: %v", agentName, service, err)
|
||||||
"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)
|
||||||
@@ -119,6 +131,7 @@ 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:
|
||||||
@@ -141,6 +154,7 @@ 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)
|
||||||
@@ -148,12 +162,19 @@ 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) {
|
||||||
return c.tracker.GetAgent(name)
|
c.mu.RLock()
|
||||||
|
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 {
|
||||||
return c.tracker.Agents()
|
c.mu.RLock()
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
result := make([]*Agent, 0, len(c.agents))
|
||||||
|
for _, a := range c.agents {
|
||||||
|
result = append(result, a)
|
||||||
|
}
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -12,30 +12,27 @@ 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
|
||||||
tracker *ConnTracker
|
agents map[string]Agent
|
||||||
jobber Jobber
|
mu sync.RWMutex
|
||||||
|
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, tracker *ConnTracker) *Commander {
|
func New(jobber Jobber) *Commander {
|
||||||
return &Commander{
|
return &Commander{
|
||||||
jobber: jobber,
|
agents: make(map[string]Agent),
|
||||||
tracker: tracker,
|
jobber: jobber,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
@@ -44,11 +41,10 @@ type Agent struct {
|
|||||||
ctx context.Context
|
ctx context.Context
|
||||||
aid string
|
aid string
|
||||||
|
|
||||||
Token string
|
Token string // agent id
|
||||||
Label string
|
Label string
|
||||||
Services []string
|
Services []string
|
||||||
}
|
}
|
||||||
|
|
||||||
type JobOut struct {
|
type JobOut struct {
|
||||||
fc models.Job
|
fc models.Job
|
||||||
err error
|
err error
|
||||||
@@ -58,93 +54,54 @@ type Job struct {
|
|||||||
out chan JobOut
|
out chan JobOut
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConnTracker tracks connected agents and handles cleanup on disconnect.
|
func (self *Commander) GetAgent(aid string) (agent Agent, ok bool) {
|
||||||
// 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()
|
||||||
for _, a := range self.agents {
|
agent, ok = self.agents[aid]
|
||||||
if a.Label == label {
|
|
||||||
return *a, true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewConnTracker() *ConnTracker {
|
func (self *Commander) Agents() []Agent {
|
||||||
return &ConnTracker{
|
self.mu.RLock()
|
||||||
agents: make(map[string]*Agent),
|
defer self.mu.RUnlock()
|
||||||
}
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
// grpc.StatsHandler implementation.
|
func (self *Commander) removeAgent(aid string) {
|
||||||
|
self.mu.Lock()
|
||||||
func (t *ConnTracker) TagRPC(ctx context.Context, _ *stats.RPCTagInfo) context.Context {
|
defer self.mu.Unlock()
|
||||||
return ctx
|
delete(self.agents, aid)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *ConnTracker) HandleRPC(ctx context.Context, _ stats.RPCStats) {}
|
func (self *Agent) AddJob(job models.JobForInsert) (int64, error) {
|
||||||
|
log.Printf("[DEBUG] AddJob: agent=%s, command=%v", self.aid, job.Command)
|
||||||
func (t *ConnTracker) TagConn(ctx context.Context, _ *stats.ConnTagInfo) context.Context {
|
jid, err := self.jobber.InitJob(self.ctx, self.aid, job)
|
||||||
return ctx
|
if err != nil {
|
||||||
}
|
log.Printf("[DEBUG] AddJob: InitJob failed: %v", err)
|
||||||
|
return 0, err
|
||||||
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])
|
|
||||||
}
|
}
|
||||||
|
log.Printf("[DEBUG] AddJob: InitJob returned jid=%d, sending to self.in channel", jid)
|
||||||
|
self.in <- &proto.Command{
|
||||||
|
Id: jid,
|
||||||
|
Command: job.Command,
|
||||||
|
Stdin: job.Stdin,
|
||||||
|
}
|
||||||
|
log.Printf("[DEBUG] AddJob: sent to self.in channel successfully")
|
||||||
|
return jid, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stream handles a new agent connection and runs the send/recv loops.
|
func (self *Agent) WaitJob(jid int64) (*models.Job, error) {
|
||||||
func (c *Commander) Stream(
|
log.Printf("[DEBUG] WaitJob: agent=%s, jid=%d, waiting on self.jobs[%d].out", self.aid, jid, jid)
|
||||||
bidi grpc.BidiStreamingServer[proto.FinishedCommand, proto.Command],
|
result := <-self.jobs[jid].out
|
||||||
) error {
|
log.Printf("[DEBUG] WaitJob: agent=%s, jid=%d, received result", self.aid, jid)
|
||||||
|
return &result.fc, result.err
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
@@ -156,95 +113,80 @@ func (c *Commander) Stream(
|
|||||||
aid := aidVals[0]
|
aid := aidVals[0]
|
||||||
|
|
||||||
var label string
|
var label string
|
||||||
if vals := md["label"]; len(vals) > 0 {
|
labelVals := md["label"]
|
||||||
label = vals[0]
|
if len(labelVals) > 0 {
|
||||||
|
label = labelVals[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
agent := NewAgent(bidi.Context(), c.jobber, aid, label)
|
agent := newAgent(bidi, self.jobber, aid, label)
|
||||||
agent.bidi = bidi
|
self.mu.Lock()
|
||||||
|
self.agents[aid] = agent
|
||||||
c.tracker.Register(aid, agent)
|
self.mu.Unlock()
|
||||||
defer c.tracker.Unregister(aid)
|
|
||||||
|
|
||||||
|
defer self.removeAgent(aid)
|
||||||
return agent.run()
|
return agent.run()
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAgent returns the agent by ID. Delegates to the tracker.
|
func (self *Agent) run() error {
|
||||||
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(a.recv)
|
wg.Go(self.recv)
|
||||||
wg.Go(a.send)
|
wg.Go(self.send)
|
||||||
return wg.Wait()
|
return wg.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Agent) recv() error {
|
func (self *Agent) recv() error {
|
||||||
for {
|
for {
|
||||||
job, err := func() (job models.Job, err error) {
|
job, err := func() (job models.Job, err error) {
|
||||||
msg, err := a.bidi.Recv()
|
msg, err := self.bidi.Recv()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
return a.jobber.UpdateJobInDB(a.ctx, msg.Id, models.JobForUpdate{
|
log.Printf("[DEBUG] recv: agent=%s, received finished job id=%d", self.aid, msg.Id)
|
||||||
|
return self.jobber.UpdateJobInDB(self.ctx, msg.Id, models.JobForUpdate{
|
||||||
Stdout: msg.Stdout,
|
Stdout: msg.Stdout,
|
||||||
Stderr: msg.Stderr,
|
Stderr: msg.Stderr,
|
||||||
Status: msg.Status,
|
Status: msg.Status,
|
||||||
})
|
})
|
||||||
}()
|
}()
|
||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
|
log.Printf("[DEBUG] recv: agent=%s, EOF received", self.aid)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
out := a.jobs[job.ID].out
|
if err != nil {
|
||||||
|
log.Printf("[DEBUG] recv: agent=%s, error: %v", self.aid, err)
|
||||||
|
}
|
||||||
|
out := self.jobs[job.ID].out
|
||||||
out <- JobOut{
|
out <- JobOut{
|
||||||
fc: job,
|
fc: job,
|
||||||
err: err,
|
err: err,
|
||||||
}
|
}
|
||||||
close(out)
|
close(out)
|
||||||
|
log.Printf("[DEBUG] recv: agent=%s, sent result for job id=%d", self.aid, job.ID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Agent) send() error {
|
func (self *Agent) send() error {
|
||||||
for job := range a.in {
|
for job := range self.in {
|
||||||
if err := a.bidi.Send(job); err != nil {
|
log.Printf("[DEBUG] send: agent=%s, job id=%d, command=%v", self.aid, job.Id, job.Command)
|
||||||
|
self.jobs[job.Id] = newJob()
|
||||||
|
if err := self.bidi.Send(job); err != nil {
|
||||||
|
log.Printf("[DEBUG] send: agent=%s, failed to send job id=%d: %v", self.aid, job.Id, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
log.Printf("[DEBUG] send: agent=%s, sent job id=%d to agent", self.aid, job.Id)
|
||||||
}
|
}
|
||||||
|
log.Printf("[DEBUG] send: agent=%s, self.in channel closed", self.aid)
|
||||||
return io.EOF
|
return io.EOF
|
||||||
|
// self.jobs[]
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAgent(
|
func newAgent(bidi grpc.BidiStreamingServer[proto.FinishedCommand, proto.Command], jobber Jobber, aid string, label string) Agent {
|
||||||
ctx context.Context,
|
return Agent{
|
||||||
jobber Jobber,
|
bidi: bidi,
|
||||||
aid string,
|
in: make(chan *proto.Command),
|
||||||
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: ctx,
|
ctx: bidi.Context(),
|
||||||
aid: aid,
|
aid: aid,
|
||||||
Label: label,
|
Label: label,
|
||||||
Token: aid,
|
Token: aid,
|
||||||
|
|||||||
@@ -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,7 +117,6 @@ 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(),
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
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 {
|
||||||
@@ -17,19 +15,17 @@ 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" example:"agent-001"` // Unique agent identifier
|
Token string `json:"token"`
|
||||||
Label string `json:"label" example:"web-server-1"` // Human-readable label
|
Label string `json:"label"`
|
||||||
Services []string `json:"services" example:"nginx:running,redis:up"` // List of services with status (format: "name:status")
|
Services []string `json:"services"`
|
||||||
ConnectedAt string `json:"connected_at" example:"2026-04-04 10:30:00"` // Time when agent connected (RFC3339-like)
|
ConnectedAt string `json:"connected_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// @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]
|
||||||
@@ -37,14 +33,10 @@ 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: services,
|
Services: agent.Services,
|
||||||
ConnectedAt: agent.ConnectedAt.Format("2006-01-02 15:04:05"),
|
ConnectedAt: agent.ConnectedAt.Format("2006-01-02 15:04:05"),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ package handlers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -51,39 +49,6 @@ 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
|
||||||
@@ -94,7 +59,6 @@ func (ag *AuthGroup) RegisterUser(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
|
||||||
@@ -118,7 +82,6 @@ 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))
|
||||||
@@ -143,7 +106,6 @@ 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()
|
||||||
@@ -162,7 +124,6 @@ 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")
|
||||||
@@ -190,7 +151,6 @@ 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))
|
||||||
@@ -222,7 +182,6 @@ 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")
|
||||||
@@ -252,7 +211,6 @@ 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")
|
||||||
@@ -280,7 +238,6 @@ 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()
|
||||||
@@ -301,7 +258,6 @@ 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")
|
||||||
@@ -334,7 +290,6 @@ 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")
|
||||||
@@ -372,7 +327,6 @@ 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")
|
||||||
@@ -410,7 +364,6 @@ 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")
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,48 +1,32 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"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 {
|
||||||
tracker *commander.ConnTracker
|
cmder *commander.Commander
|
||||||
svc *service.ScriptService
|
svc *service.ScriptService
|
||||||
whereami string
|
|
||||||
jobRepo *repository.JobRepository
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewJobsHandlers(tracker *commander.ConnTracker, svc *service.ScriptService, whereami string, jobRepo *repository.JobRepository) JobsHandlers {
|
func NewJobsHandlers(cmder *commander.Commander, svc *service.ScriptService) JobsHandlers {
|
||||||
return JobsHandlers{tracker: tracker, svc: svc, whereami: whereami, jobRepo: jobRepo}
|
return JobsHandlers{cmder: cmder, svc: svc}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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"`
|
||||||
@@ -51,195 +35,72 @@ type JobResult struct {
|
|||||||
Status int32 `json:"status"`
|
Status int32 `json:"status"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// WaitJobIn is the request body for waiting on a job.
|
// AddJob creates and executes a job on a target agent.
|
||||||
type WaitJobIn struct {
|
// @Summary Create and run a job on an agent
|
||||||
AgentID string `json:"agent_id" binding:"required"`
|
// @Description Sends a command to the specified agent, waits for execution, and returns the result
|
||||||
}
|
|
||||||
|
|
||||||
// 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 (h *JobsHandlers) AddJob(c *gin.Context) {
|
func (self *JobsHandlers) AddJob(c *gin.Context) {
|
||||||
var in AddJobIn
|
log.Printf("[DEBUG] AddJob handler: request received")
|
||||||
if err := c.Bind(&in); err != nil {
|
err := func() error {
|
||||||
c.Error(err)
|
var in AddJobIn
|
||||||
return
|
if err := c.Bind(&in); err != nil {
|
||||||
}
|
log.Printf("[DEBUG] AddJob handler: bind failed: %v", err)
|
||||||
|
return err
|
||||||
agent, ok := h.tracker.GetAgent(in.AgentID)
|
|
||||||
if !ok {
|
|
||||||
c.Status(http.StatusNotFound)
|
|
||||||
c.Error(fmt.Errorf("agent not found"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
command, err := resolveCommand(c, h.svc, in.InterpreterID, in.Command)
|
|
||||||
if err != nil {
|
|
||||||
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()})
|
log.Printf("[DEBUG] AddJob handler: agent_id=%s, command=%s, interpreter_id=%d", in.AgentID, in.Command, in.InterpreterID)
|
||||||
return
|
agent, ok := self.cmder.GetAgent(in.AgentID)
|
||||||
}
|
if !ok {
|
||||||
|
log.Printf("[DEBUG] AddJob handler: agent %s not found", in.AgentID)
|
||||||
|
c.Status(http.StatusNotFound)
|
||||||
|
return fmt.Errorf("agent not found")
|
||||||
|
}
|
||||||
|
log.Printf("[DEBUG] AddJob handler: agent found, resolving command")
|
||||||
|
|
||||||
c.JSON(http.StatusOK, CheckCmdOut{Exists: true})
|
var command []string
|
||||||
}
|
if in.InterpreterID == 0 {
|
||||||
|
command = []string{"sh", "-c", in.Command}
|
||||||
|
} else {
|
||||||
|
var err error
|
||||||
|
command, err = self.svc.ResolveCommand(c.Request.Context(), in.InterpreterID, in.Command)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[DEBUG] AddJob handler: ResolveCommand failed: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type CheckCmdIn struct {
|
log.Printf("[DEBUG] AddJob handler: calling agent.AddJob with command=%v", command)
|
||||||
Command string `json:"command" binding:"required" example:"bash"`
|
jid, err := agent.AddJob(models.JobForInsert{
|
||||||
}
|
Command: command,
|
||||||
type CheckCmdOut struct {
|
Stdin: in.Stdin,
|
||||||
Exists bool `json:"exists"`
|
})
|
||||||
}
|
if err != nil {
|
||||||
|
log.Printf("[DEBUG] AddJob handler: agent.AddJob failed: %v", err)
|
||||||
// JobMetricsOut is the response body for the job metrics endpoint.
|
return err
|
||||||
type JobMetricsOut struct {
|
}
|
||||||
Total int `json:"total"`
|
log.Printf("[DEBUG] AddJob handler: agent.AddJob returned jid=%d, calling WaitJob", jid)
|
||||||
Success int `json:"success"`
|
job, err := agent.WaitJob(jid)
|
||||||
Failed int `json:"failed"`
|
if err != nil {
|
||||||
Pending int `json:"pending"`
|
log.Printf("[DEBUG] AddJob handler: agent.WaitJob failed: %v", err)
|
||||||
Period string `json:"period"`
|
return err
|
||||||
}
|
}
|
||||||
|
log.Printf("[DEBUG] AddJob handler: agent.WaitJob returned job id=%d, status=%d", job.ID, job.Status)
|
||||||
// GetJobMetrics returns job success metrics over a parameterized period.
|
c.JSON(http.StatusCreated, AddJobOut{
|
||||||
// @Summary Get job metrics
|
ID: job.ID,
|
||||||
// @Description Returns total, successful, failed, and pending job counts over the given period
|
Command: job.Command,
|
||||||
// @Tags jobs
|
Stdin: job.Stdin,
|
||||||
// @Produce json
|
Stdout: job.Stdout,
|
||||||
// @Param period query string false "Time period (e.g. 1h, 24h, 7d)" default(24h)
|
Stderr: job.Stderr,
|
||||||
// @Success 200 {object} JobMetricsOut
|
Status: job.Status,
|
||||||
// @Failure 400 {object} map[string]string
|
})
|
||||||
// @Security Bearer
|
log.Printf("[DEBUG] AddJob handler: response sent")
|
||||||
// @Router /jobs/metrics [get]
|
return nil
|
||||||
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 {
|
if err != nil {
|
||||||
c.Error(err)
|
c.Error(err)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, JobMetricsOut{
|
|
||||||
Total: metrics.Total,
|
|
||||||
Success: metrics.Success,
|
|
||||||
Failed: metrics.Failed,
|
|
||||||
Pending: metrics.Pending,
|
|
||||||
Period: periodStr,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -13,28 +13,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ScriptHandlers struct {
|
type ScriptHandlers struct {
|
||||||
svc *service.ScriptService
|
svc *service.ScriptService
|
||||||
tracker *commander.ConnTracker
|
cmder *commander.Commander
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewScriptHandlers(svc *service.ScriptService, tracker *commander.ConnTracker) ScriptHandlers {
|
func NewScriptHandlers(svc *service.ScriptService, cmder *commander.Commander) ScriptHandlers {
|
||||||
return ScriptHandlers{svc: svc, tracker: tracker}
|
return ScriptHandlers{svc: svc, cmder: cmder}
|
||||||
}
|
|
||||||
|
|
||||||
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.
|
||||||
@@ -45,25 +29,26 @@ type RunScriptOut struct {
|
|||||||
// @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 (h *ScriptHandlers) RunScript(c *gin.Context) {
|
func (self *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 := h.svc.ResolveCommand(
|
command, err := self.svc.ResolveCommand(c.Request.Context(), in.InterpreterID, in.ScriptText)
|
||||||
c.Request.Context(),
|
|
||||||
in.InterpreterID,
|
|
||||||
in.ScriptText,
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
agent, ok := h.tracker.GetAgent(in.AgentID)
|
agent, ok := self.cmder.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")
|
||||||
@@ -82,6 +67,14 @@ func (h *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,
|
||||||
@@ -103,10 +96,9 @@ func (h *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 (h *ScriptHandlers) ListInterpreters(c *gin.Context) {
|
func (self *ScriptHandlers) ListInterpreters(c *gin.Context) {
|
||||||
interpreters, err := h.svc.List(c.Request.Context())
|
interpreters, err := self.svc.List(c.Request.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Error(err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
@@ -122,16 +114,15 @@ func (h *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 (h *ScriptHandlers) CreateInterpreter(c *gin.Context) {
|
func (self *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 := h.svc.Create(c.Request.Context(), in)
|
si, err := self.svc.Create(c.Request.Context(), in)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Error(err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
@@ -146,16 +137,15 @@ func (h *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 (h *ScriptHandlers) GetInterpreter(c *gin.Context) {
|
func (self *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 := h.svc.GetByID(c.Request.Context(), id)
|
si, err := self.svc.GetByID(c.Request.Context(), id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Error(err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
@@ -172,9 +162,8 @@ func (h *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 (h *ScriptHandlers) UpdateInterpreter(c *gin.Context) {
|
func (self *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)
|
||||||
@@ -187,7 +176,7 @@ func (h *ScriptHandlers) UpdateInterpreter(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
si, err := h.svc.Update(c.Request.Context(), id, in)
|
si, err := self.svc.Update(c.Request.Context(), id, in)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Error(err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
@@ -201,16 +190,15 @@ func (h *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 (h *ScriptHandlers) DeleteInterpreter(c *gin.Context) {
|
func (self *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 := h.svc.Delete(c.Request.Context(), id); err != nil {
|
if err := self.svc.Delete(c.Request.Context(), id); err != nil {
|
||||||
c.Error(err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,274 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -5,7 +5,6 @@ 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"
|
||||||
@@ -24,11 +23,7 @@ func (r *JobRepository) Init(ctx context.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *JobRepository) InitJob(
|
func (r *JobRepository) InitJob(ctx context.Context, agentID string, job models.JobForInsert) (int64, error) {
|
||||||
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)
|
||||||
@@ -39,12 +34,9 @@ func (r *JobRepository) InitJob(
|
|||||||
stdinVal = job.Stdin
|
stdinVal = job.Stdin
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := r.DB.ExecContext(
|
result, err := r.DB.ExecContext(ctx,
|
||||||
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,
|
agentID, string(commandJSON), stdinVal,
|
||||||
string(commandJSON),
|
|
||||||
stdinVal,
|
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
@@ -53,18 +45,10 @@ func (r *JobRepository) InitJob(
|
|||||||
return result.LastInsertId()
|
return result.LastInsertId()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *JobRepository) UpdateJobInDB(
|
func (r *JobRepository) UpdateJobInDB(ctx context.Context, jid int64, msg models.JobForUpdate) (models.Job, error) {
|
||||||
ctx context.Context,
|
result, err := r.DB.ExecContext(ctx,
|
||||||
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.Stdout, msg.Stderr, msg.Status, jid,
|
||||||
msg.Stderr,
|
|
||||||
msg.Status,
|
|
||||||
jid,
|
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return models.Job{}, err
|
return models.Job{}, err
|
||||||
@@ -97,36 +81,10 @@ 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.Command); err != nil {
|
if err := json.Unmarshal([]byte(commandJSON), &job.JobForInsert.Command); err != nil {
|
||||||
return models.Job{}, fmt.Errorf("unmarshal command: %w", err)
|
return models.Job{}, fmt.Errorf("unmarshal command: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
job.Stdin = stdinVal
|
job.JobForInsert.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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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,13 +157,7 @@ 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(
|
if err := rows.Scan(&log.Timestamp, &log.Level, &log.Service, &log.Agent, &log.Message); err != nil {
|
||||||
&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)
|
||||||
|
|||||||
@@ -2,37 +2,29 @@ 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"`
|
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.
|
// TokenCreate is the request body for creating a new user.
|
||||||
type TokenCreate struct {
|
type TokenCreate struct {
|
||||||
Name string `json:"name" binding:"required"`
|
Name string `json:"name" binding:"required"`
|
||||||
LastName string `json:"last_name" binding:"required"`
|
LastName string `json:"last_name" binding:"required"`
|
||||||
Login string `json:"login" binding:"required"`
|
Login string `json:"login" binding:"required"`
|
||||||
Password string `json:"password" 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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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"`
|
||||||
@@ -59,7 +51,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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,14 +109,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
|
||||||
@@ -137,49 +129,15 @@ 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"`
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ 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"
|
||||||
@@ -52,15 +50,8 @@ 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.Name, tc.LastName, tc.Login, string(hashed), token,
|
||||||
tc.LastName,
|
tc.PermissionView, tc.PermissionManage, tc.PermissionAdmin, tc.IsActive,
|
||||||
tc.Login,
|
|
||||||
string(hashed),
|
|
||||||
token,
|
|
||||||
tc.PermissionView,
|
|
||||||
tc.PermissionManage,
|
|
||||||
tc.PermissionAdmin,
|
|
||||||
tc.IsActive,
|
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@@ -73,39 +64,6 @@ 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
|
||||||
@@ -160,11 +118,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, is_active
|
`SELECT id, name, last_name, login, token, permission_view, permission_manage_agent, permission_admin
|
||||||
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.IsActive)
|
&t.PermissionView, &t.PermissionManage, &t.PermissionAdmin)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
@@ -178,7 +136,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, is_active
|
`SELECT id, name, last_name, login, permission_view, permission_manage_agent, permission_admin
|
||||||
FROM tokens`,
|
FROM tokens`,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -190,7 +148,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, &t.IsActive); err != nil {
|
&t.PermissionView, &t.PermissionManage, &t.PermissionAdmin); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
tokens = append(tokens, t)
|
tokens = append(tokens, t)
|
||||||
@@ -344,13 +302,12 @@ func (r *Repository) ActivateUserByLogin(login string) error {
|
|||||||
login,
|
login,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("activate exec: %w", err)
|
return err
|
||||||
}
|
}
|
||||||
affected, err := result.RowsAffected()
|
affected, err := result.RowsAffected()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("rows affected: %w", err)
|
return err
|
||||||
}
|
}
|
||||||
log.Printf("[activate] login=%s affected=%d", login, affected)
|
|
||||||
if affected == 0 {
|
if affected == 0 {
|
||||||
return ErrNotFound
|
return ErrNotFound
|
||||||
}
|
}
|
||||||
@@ -465,11 +422,7 @@ 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,
|
newView, newManage, newAdmin, newActive, login,
|
||||||
newManage,
|
|
||||||
newAdmin,
|
|
||||||
newActive,
|
|
||||||
login,
|
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -507,134 +460,3 @@ 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,10 +44,7 @@ func (r *ScriptInterpreterRepo) Init(ctx context.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ScriptInterpreterRepo) Create(
|
func (r *ScriptInterpreterRepo) Create(ctx context.Context, in ScriptInterpreterCreate) (*ScriptInterpreter, error) {
|
||||||
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
|
||||||
@@ -74,8 +71,7 @@ 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(
|
err := r.DB.QueryRowContext(ctx,
|
||||||
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)
|
||||||
@@ -107,14 +103,7 @@ 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(
|
if err := rows.Scan(&si.ID, &si.Name, &si.Label, &argvJSON, &createdAt, &updatedAt); err != nil {
|
||||||
&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 {
|
||||||
@@ -127,11 +116,7 @@ func (r *ScriptInterpreterRepo) List(ctx context.Context) ([]ScriptInterpreter,
|
|||||||
return interpreters, rows.Err()
|
return interpreters, rows.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ScriptInterpreterRepo) Update(
|
func (r *ScriptInterpreterRepo) Update(ctx context.Context, id int64, in ScriptInterpreterUpdate) (*ScriptInterpreter, error) {
|
||||||
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
|
||||||
|
|||||||
@@ -3,170 +3,52 @@ 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.Repository
|
repo *repository.ScriptInterpreterRepo
|
||||||
InterpreterRepo *repository.ScriptInterpreterRepo
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewScriptService creates a new ScriptService with both script and interpreter repos.
|
func NewScriptService(repo *repository.ScriptInterpreterRepo) *ScriptService {
|
||||||
func NewScriptService(repo *repository.Repository) *ScriptService {
|
return &ScriptService{repo: repo}
|
||||||
return &ScriptService{Repo: repo}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewScriptServiceWithInterpreters creates a ScriptService with interpreter support.
|
// ResolveCommand builds the full argv[] by prepending the interpreter's argv
|
||||||
func NewScriptServiceWithInterpreters(repo *repository.Repository, interpRepo *repository.ScriptInterpreterRepo) *ScriptService {
|
// to the script text (as the last argument).
|
||||||
return &ScriptService{Repo: repo, InterpreterRepo: interpRepo}
|
func (self *ScriptService) ResolveCommand(ctx context.Context, interpreterID int64, scriptText string) ([]string, error) {
|
||||||
}
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
root := make(map[string]*treeNode)
|
if len(interpreter.Argv) == 0 {
|
||||||
|
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return buildTreeSlice(root), nil
|
argv := make([]string, len(interpreter.Argv)+1)
|
||||||
|
copy(argv, interpreter.Argv)
|
||||||
|
argv[len(argv)-1] = scriptText
|
||||||
|
return argv, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildTreeSlice converts a map of treeNodes to a sorted slice of ScriptTreeNode.
|
func (self *ScriptService) Create(ctx context.Context, in repository.ScriptInterpreterCreate) (*repository.ScriptInterpreter, error) {
|
||||||
func buildTreeSlice(m map[string]*treeNode) []repository.ScriptTreeNode {
|
return self.repo.Create(ctx, in)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// toScriptTreeNode converts a treeNode to a ScriptTreeNode with recursively converted children.
|
func (self *ScriptService) GetByID(ctx context.Context, id int64) (*repository.ScriptInterpreter, error) {
|
||||||
func toScriptTreeNode(node *treeNode) repository.ScriptTreeNode {
|
return self.repo.GetByID(ctx, id)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResolveCommand resolves the full command for a script using its interpreter.
|
func (self *ScriptService) List(ctx context.Context) ([]repository.ScriptInterpreter, error) {
|
||||||
func (s *ScriptService) ResolveCommand(ctx context.Context, interpreterID int64, scriptText string) ([]string, error) {
|
return self.repo.List(ctx)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// List returns all interpreters.
|
func (self *ScriptService) Update(ctx context.Context, id int64, in repository.ScriptInterpreterUpdate) (*repository.ScriptInterpreter, error) {
|
||||||
func (s *ScriptService) List(ctx context.Context) ([]repository.ScriptInterpreter, error) {
|
return self.repo.Update(ctx, id, in)
|
||||||
if s.InterpreterRepo == nil {
|
|
||||||
return nil, fmt.Errorf("interpreter repo not configured")
|
|
||||||
}
|
|
||||||
return s.InterpreterRepo.List(ctx)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create creates a new interpreter.
|
func (self *ScriptService) Delete(ctx context.Context, id int64) error {
|
||||||
func (s *ScriptService) Create(ctx context.Context, in repository.ScriptInterpreterCreate) (*repository.ScriptInterpreter, error) {
|
return self.repo.Delete(ctx, id)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,11 +43,7 @@ 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(
|
func OpenClickHouseWithRetry(cfg ClickHouseConfig, maxRetries int, initialDelay time.Duration) (*sql.DB, error) {
|
||||||
cfg ClickHouseConfig,
|
|
||||||
maxRetries int,
|
|
||||||
initialDelay time.Duration,
|
|
||||||
) (*sql.DB, error) {
|
|
||||||
var lastErr error
|
var lastErr error
|
||||||
delay := initialDelay
|
delay := initialDelay
|
||||||
|
|
||||||
@@ -57,20 +53,10 @@ func OpenClickHouseWithRetry(
|
|||||||
return db, nil
|
return db, nil
|
||||||
}
|
}
|
||||||
lastErr = err
|
lastErr = err
|
||||||
log.Printf(
|
log.Printf("ClickHouse connection attempt %d/%d failed: %v, retrying in %v...", i+1, maxRetries, err, delay)
|
||||||
"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(
|
return nil, fmt.Errorf("clickhouse connection failed after %d attempts: %w", maxRetries, lastErr)
|
||||||
"clickhouse connection failed after %d attempts: %w",
|
|
||||||
maxRetries,
|
|
||||||
lastErr,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,18 +57,6 @@ 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(),
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package storage
|
|||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
_ "modernc.org/sqlite"
|
_ "modernc.org/sqlite"
|
||||||
@@ -38,16 +37,7 @@ 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
|
||||||
if _, err := db.Exec(AddIsActiveColumn); err != nil {
|
_, _ = db.Exec(AddIsActiveColumn)
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ 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,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: "58b1cd3857774f690e4534ec222af4ec08eaae8cd5577614365f2b19c78d03d6"
|
registration_token: "156616b56774d59ba53f1eb4b096488bb5f755bbf5b737d93a42bb1b583ad7fb"
|
||||||
cert_dir: /etc/hellreign-agent/certs
|
cert_dir: /etc/hellreign-agent/certs
|
||||||
services:
|
services:
|
||||||
- name: system
|
- name: system
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
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];
|
|
||||||
@@ -6,16 +6,6 @@ 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 {
|
||||||
@@ -41,4 +31,3 @@ message FinishedCommand {
|
|||||||
string stdout = 3;
|
string stdout = 3;
|
||||||
string stderr = 4;
|
string stderr = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+31
-176
@@ -21,86 +21,6 @@ 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"`
|
||||||
@@ -110,7 +30,7 @@ type CollectorRequest struct {
|
|||||||
|
|
||||||
func (x *CollectorRequest) Reset() {
|
func (x *CollectorRequest) Reset() {
|
||||||
*x = CollectorRequest{}
|
*x = CollectorRequest{}
|
||||||
mi := &file_hellreign_proto_msgTypes[2]
|
mi := &file_hellreign_proto_msgTypes[0]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -122,7 +42,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[2]
|
mi := &file_hellreign_proto_msgTypes[0]
|
||||||
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 {
|
||||||
@@ -135,7 +55,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{2}
|
return file_hellreign_proto_rawDescGZIP(), []int{0}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *CollectorRequest) GetMessage() string {
|
func (x *CollectorRequest) GetMessage() string {
|
||||||
@@ -153,7 +73,7 @@ type CollectorResponse struct {
|
|||||||
|
|
||||||
func (x *CollectorResponse) Reset() {
|
func (x *CollectorResponse) Reset() {
|
||||||
*x = CollectorResponse{}
|
*x = CollectorResponse{}
|
||||||
mi := &file_hellreign_proto_msgTypes[3]
|
mi := &file_hellreign_proto_msgTypes[1]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -165,7 +85,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[3]
|
mi := &file_hellreign_proto_msgTypes[1]
|
||||||
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 {
|
||||||
@@ -178,7 +98,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{3}
|
return file_hellreign_proto_rawDescGZIP(), []int{1}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Command struct {
|
type Command struct {
|
||||||
@@ -192,7 +112,7 @@ type Command struct {
|
|||||||
|
|
||||||
func (x *Command) Reset() {
|
func (x *Command) Reset() {
|
||||||
*x = Command{}
|
*x = Command{}
|
||||||
mi := &file_hellreign_proto_msgTypes[4]
|
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)
|
||||||
}
|
}
|
||||||
@@ -204,7 +124,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[4]
|
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 {
|
||||||
@@ -217,7 +137,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{4}
|
return file_hellreign_proto_rawDescGZIP(), []int{2}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *Command) GetId() int64 {
|
func (x *Command) GetId() int64 {
|
||||||
@@ -253,7 +173,7 @@ type FinishedCommand struct {
|
|||||||
|
|
||||||
func (x *FinishedCommand) Reset() {
|
func (x *FinishedCommand) Reset() {
|
||||||
*x = FinishedCommand{}
|
*x = FinishedCommand{}
|
||||||
mi := &file_hellreign_proto_msgTypes[5]
|
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)
|
||||||
}
|
}
|
||||||
@@ -265,7 +185,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[5]
|
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 {
|
||||||
@@ -278,7 +198,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{5}
|
return file_hellreign_proto_rawDescGZIP(), []int{3}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *FinishedCommand) GetId() int64 {
|
func (x *FinishedCommand) GetId() int64 {
|
||||||
@@ -309,69 +229,11 @@ 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\"\x14\n" +
|
"\x0fhellreign.proto\x12\x04chat\",\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" +
|
||||||
@@ -384,10 +246,9 @@ 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\x06stderr2\x8a\x01\n" +
|
"\x06stderr\x18\x04 \x01(\tR\x06stderr2H\n" +
|
||||||
"\tCollector\x12;\n" +
|
"\tCollector\x12;\n" +
|
||||||
"\x06Stream\x12\x16.chat.CollectorRequest\x1a\x17.chat.CollectorResponse(\x01\x12@\n" +
|
"\x06Stream\x12\x16.chat.CollectorRequest\x1a\x17.chat.CollectorResponse(\x012?\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"
|
||||||
|
|
||||||
@@ -403,29 +264,23 @@ func file_hellreign_proto_rawDescGZIP() []byte {
|
|||||||
return file_hellreign_proto_rawDescData
|
return file_hellreign_proto_rawDescData
|
||||||
}
|
}
|
||||||
|
|
||||||
var file_hellreign_proto_msgTypes = make([]protoimpl.MessageInfo, 7)
|
var file_hellreign_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
|
||||||
var file_hellreign_proto_goTypes = []any{
|
var file_hellreign_proto_goTypes = []any{
|
||||||
(*ServicesUpdateResp)(nil), // 0: chat.ServicesUpdateResp
|
(*CollectorRequest)(nil), // 0: chat.CollectorRequest
|
||||||
(*ServicesUpdate)(nil), // 1: chat.ServicesUpdate
|
(*CollectorResponse)(nil), // 1: chat.CollectorResponse
|
||||||
(*CollectorRequest)(nil), // 2: chat.CollectorRequest
|
(*Command)(nil), // 2: chat.Command
|
||||||
(*CollectorResponse)(nil), // 3: chat.CollectorResponse
|
(*FinishedCommand)(nil), // 3: chat.FinishedCommand
|
||||||
(*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{
|
||||||
6, // 0: chat.ServicesUpdate.services:type_name -> chat.ServicesUpdate.ServiceUpdate
|
0, // 0: chat.Collector.Stream:input_type -> chat.CollectorRequest
|
||||||
2, // 1: chat.Collector.Stream:input_type -> chat.CollectorRequest
|
3, // 1: chat.Commander.Stream:input_type -> chat.FinishedCommand
|
||||||
1, // 2: chat.Collector.ReportServices:input_type -> chat.ServicesUpdate
|
1, // 2: chat.Collector.Stream:output_type -> chat.CollectorResponse
|
||||||
5, // 3: chat.Commander.Stream:input_type -> chat.FinishedCommand
|
2, // 3: chat.Commander.Stream:output_type -> chat.Command
|
||||||
3, // 4: chat.Collector.Stream:output_type -> chat.CollectorResponse
|
2, // [2:4] is the sub-list for method output_type
|
||||||
0, // 5: chat.Collector.ReportServices:output_type -> chat.ServicesUpdateResp
|
0, // [0:2] is the sub-list for method input_type
|
||||||
4, // 6: chat.Commander.Stream:output_type -> chat.Command
|
0, // [0:0] is the sub-list for extension type_name
|
||||||
4, // [4:7] is the sub-list for method output_type
|
0, // [0:0] is the sub-list for extension extendee
|
||||||
1, // [1:4] is the sub-list for method input_type
|
0, // [0:0] is the sub-list for field type_name
|
||||||
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() }
|
||||||
@@ -433,14 +288,14 @@ func file_hellreign_proto_init() {
|
|||||||
if File_hellreign_proto != nil {
|
if File_hellreign_proto != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
file_hellreign_proto_msgTypes[4].OneofWrappers = []any{}
|
file_hellreign_proto_msgTypes[2].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: 7,
|
NumMessages: 4,
|
||||||
NumExtensions: 0,
|
NumExtensions: 0,
|
||||||
NumServices: 2,
|
NumServices: 2,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -19,8 +19,7 @@ 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.
|
||||||
@@ -28,7 +27,6 @@ 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 {
|
||||||
@@ -52,22 +50,11 @@ 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()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,9 +68,6 @@ 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() {}
|
||||||
|
|
||||||
@@ -112,36 +96,13 @@ 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",
|
||||||
|
|||||||
Reference in New Issue
Block a user