Compare commits
18 Commits
ce73e915ca
...
debug
| Author | SHA1 | Date | |
|---|---|---|---|
| abc6cb4e46 | |||
| 0660117c07 | |||
| 9ede6257f8 | |||
| f5b9b32a9f | |||
| e721cff3f8 | |||
| 7e54d62170 | |||
| 477dd94227 | |||
| c59d122e04 | |||
| ad92439770 | |||
| f1fc52bd6b | |||
| 24cc11bc8d | |||
| 10d899b50f | |||
| 2a8faaa9fe | |||
| c5e35b4c12 | |||
| f578b6eb51 | |||
| a2c71da3a0 | |||
| 28631865c8 | |||
| edb1458806 |
+6
-8
@@ -2,16 +2,14 @@ FROM golang:1.26.1 as builder
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY go.mod go.sum ./
|
COPY proto/ proto/
|
||||||
RUN --mount=type=cache,target=/go/pkg/mod \
|
COPY agent/ agent/
|
||||||
--mount=type=cache,target=/root/.cache/go-build \
|
|
||||||
go mod download
|
|
||||||
|
|
||||||
COPY . .
|
WORKDIR /app/agent
|
||||||
ENV CGO_ENABLED=0
|
|
||||||
RUN --mount=type=cache,target=/go/pkg/mod \
|
RUN --mount=type=cache,target=/go/pkg/mod \
|
||||||
--mount=type=cache,target=/root/.cache/go-build \
|
--mount=type=cache,target=/root/.cache/go-build \
|
||||||
go build -ldflags "-s -w" -o agent ./main.go
|
go mod download && \
|
||||||
|
CGO_ENABLED=0 go build -ldflags "-s -w" -o /agent .
|
||||||
|
|
||||||
FROM debian:bookworm-slim
|
FROM debian:bookworm-slim
|
||||||
|
|
||||||
@@ -21,6 +19,6 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY --from=builder /app/agent .
|
COPY --from=builder /agent .
|
||||||
|
|
||||||
CMD ["./agent"]
|
CMD ["./agent"]
|
||||||
|
|||||||
@@ -4,11 +4,21 @@ go 1.26.1
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
gitea.d3m0k1d.ru/d3m0k1d/HellreigN/proto v0.0.0-20260403214837-94be9799f47d
|
gitea.d3m0k1d.ru/d3m0k1d/HellreigN/proto v0.0.0-20260403214837-94be9799f47d
|
||||||
|
github.com/hpcloud/tail v1.0.0
|
||||||
|
github.com/samber/lo v1.53.0
|
||||||
golang.org/x/sync v0.20.0
|
golang.org/x/sync v0.20.0
|
||||||
google.golang.org/grpc v1.80.0
|
google.golang.org/grpc v1.80.0
|
||||||
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
|
modernc.org/sqlite v1.34.5
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.41.0 // indirect
|
go.opentelemetry.io/otel/metric v1.41.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.41.0 // indirect
|
go.opentelemetry.io/otel/trace v1.41.0 // indirect
|
||||||
golang.org/x/net v0.52.0 // indirect
|
golang.org/x/net v0.52.0 // indirect
|
||||||
@@ -16,4 +26,11 @@ require (
|
|||||||
golang.org/x/text v0.35.0 // indirect
|
golang.org/x/text v0.35.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 // indirect
|
||||||
google.golang.org/protobuf v1.36.11 // indirect
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
|
gopkg.in/fsnotify.v1 v1.4.7 // indirect
|
||||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
||||||
|
modernc.org/libc v1.55.3 // indirect
|
||||||
|
modernc.org/mathutil v1.6.0 // indirect
|
||||||
|
modernc.org/memory v1.8.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
|
replace gitea.d3m0k1d.ru/d3m0k1d/HellreigN/proto => ../proto
|
||||||
|
|||||||
+53
-2
@@ -1,7 +1,9 @@
|
|||||||
gitea.d3m0k1d.ru/d3m0k1d/HellreigN/proto v0.0.0-20260403214837-94be9799f47d h1:oBBLU8/nhXgOr0Z/M/t4pYj3KjuRj8AI15J0RJCiRt8=
|
|
||||||
gitea.d3m0k1d.ru/d3m0k1d/HellreigN/proto v0.0.0-20260403214837-94be9799f47d/go.mod h1:FEPB3qn+wXkes/eArIMdq1/3CbHnSDUxsUtXhC8mgOg=
|
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
@@ -10,8 +12,20 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek
|
|||||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
|
||||||
|
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||||
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||||
|
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
|
github.com/samber/lo v1.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM=
|
||||||
|
github.com/samber/lo v1.53.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||||
go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c=
|
go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c=
|
||||||
@@ -24,14 +38,19 @@ go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2W
|
|||||||
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
||||||
go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0=
|
go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0=
|
||||||
go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis=
|
go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis=
|
||||||
|
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
||||||
|
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
||||||
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||||
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||||
|
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
||||||
|
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
||||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU=
|
||||||
@@ -40,3 +59,35 @@ google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
|
|||||||
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
|
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
|
||||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||||
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
|
||||||
|
modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
|
||||||
|
modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y=
|
||||||
|
modernc.org/ccgo/v4 v4.19.2/go.mod h1:ysS3mxiMV38XGRTTcgo0DQTeTmAO4oCmJl1nX9VFI3s=
|
||||||
|
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
|
||||||
|
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
|
||||||
|
modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw=
|
||||||
|
modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
|
||||||
|
modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U=
|
||||||
|
modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w=
|
||||||
|
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
|
||||||
|
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
|
||||||
|
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
|
||||||
|
modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
|
||||||
|
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
|
||||||
|
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||||
|
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
|
||||||
|
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
|
||||||
|
modernc.org/sqlite v1.34.5 h1:Bb6SR13/fjp15jt70CL4f18JIN7p7dnMExd+UFnF15g=
|
||||||
|
modernc.org/sqlite v1.34.5/go.mod h1:YLuNmX9NKs8wRNK2ko1LW1NGYcc9FkBO69JOt1AR9JE=
|
||||||
|
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
|
||||||
|
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
|
||||||
|
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
|
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||||
|
|||||||
@@ -0,0 +1,161 @@
|
|||||||
|
package buffer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
_ "modernc.org/sqlite"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BufferedLog represents a log entry stored for later delivery
|
||||||
|
type BufferedLog struct {
|
||||||
|
ID int64
|
||||||
|
Service string
|
||||||
|
Message string
|
||||||
|
CreatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogBuffer provides SQLite-backed log buffering
|
||||||
|
type LogBuffer struct {
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLogBuffer creates a new log buffer with the given database path
|
||||||
|
func NewLogBuffer(dbPath string) (*LogBuffer, error) {
|
||||||
|
db, err := sql.Open("sqlite", dbPath+"?_journal_mode=WAL&_busy_timeout=5000")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to open database: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create table if not exists
|
||||||
|
_, err = db.Exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS buffered_logs (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
service TEXT NOT NULL,
|
||||||
|
message TEXT NOT NULL,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
_ = db.Close()
|
||||||
|
return nil, fmt.Errorf("failed to create table: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create index for efficient ordering
|
||||||
|
_, _ = db.Exec(`CREATE INDEX IF NOT EXISTS idx_created_at ON buffered_logs(created_at ASC)`)
|
||||||
|
|
||||||
|
return &LogBuffer{db: db}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the database connection
|
||||||
|
func (b *LogBuffer) Close() error {
|
||||||
|
return b.db.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store stores a log entry in the buffer
|
||||||
|
func (b *LogBuffer) Store(service, message string) error {
|
||||||
|
_, err := b.db.Exec(
|
||||||
|
"INSERT INTO buffered_logs (service, message) VALUES (?, ?)",
|
||||||
|
service, message,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// StoreBatch stores multiple log entries in a single transaction
|
||||||
|
func (b *LogBuffer) StoreBatch(entries []BufferedLog) error {
|
||||||
|
tx, err := b.db.Begin()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer tx.Rollback()
|
||||||
|
|
||||||
|
stmt, err := tx.Prepare("INSERT INTO buffered_logs (service, message) VALUES (?, ?)")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer stmt.Close()
|
||||||
|
|
||||||
|
for _, entry := range entries {
|
||||||
|
if _, err := stmt.Exec(entry.Service, entry.Message); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx.Commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPending retrieves pending logs in order of arrival, limited to batchSize
|
||||||
|
func (b *LogBuffer) GetPending(batchSize int) ([]BufferedLog, error) {
|
||||||
|
rows, err := b.db.Query(
|
||||||
|
"SELECT id, service, message, created_at FROM buffered_logs ORDER BY created_at ASC LIMIT ?",
|
||||||
|
batchSize,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var logs []BufferedLog
|
||||||
|
for rows.Next() {
|
||||||
|
var log BufferedLog
|
||||||
|
var createdAt string
|
||||||
|
if err := rows.Scan(&log.ID, &log.Service, &log.Message, &createdAt); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
log.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", createdAt)
|
||||||
|
logs = append(logs, log)
|
||||||
|
}
|
||||||
|
|
||||||
|
return logs, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete removes a log entry from the buffer after successful delivery
|
||||||
|
func (b *LogBuffer) Delete(id int64) error {
|
||||||
|
_, err := b.db.Exec("DELETE FROM buffered_logs WHERE id = ?", id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteBatch removes multiple log entries after successful delivery
|
||||||
|
func (b *LogBuffer) DeleteBatch(ids []int64) error {
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tx, err := b.db.Begin()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer tx.Rollback()
|
||||||
|
|
||||||
|
for _, id := range ids {
|
||||||
|
if _, err := tx.Exec("DELETE FROM buffered_logs WHERE id = ?", id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx.Commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count returns the number of buffered logs
|
||||||
|
func (b *LogBuffer) Count() (int, error) {
|
||||||
|
var count int
|
||||||
|
err := b.db.QueryRow("SELECT COUNT(*) FROM buffered_logs").Scan(&count)
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear removes all buffered logs
|
||||||
|
func (b *LogBuffer) Clear() error {
|
||||||
|
_, err := b.db.Exec("DELETE FROM buffered_logs")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// FlushToJSON exports buffered logs to JSON format for debugging
|
||||||
|
func (b *LogBuffer) FlushToJSON() ([]byte, error) {
|
||||||
|
logs, err := b.GetPending(1000)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return json.MarshalIndent(logs, "", " ")
|
||||||
|
}
|
||||||
@@ -13,26 +13,29 @@ import (
|
|||||||
"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/credentials"
|
||||||
|
"google.golang.org/grpc/metadata"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CommanderClient struct {
|
type CommanderClient struct {
|
||||||
cmder *commander.Commander
|
cmder *commander.CommandExecutor
|
||||||
wg *sync.WaitGroup
|
wg *sync.WaitGroup
|
||||||
|
id, label string
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(
|
func New(
|
||||||
cmder *commander.Commander,
|
cmder *commander.CommandExecutor,
|
||||||
wg *sync.WaitGroup,
|
id, label string,
|
||||||
) CommanderClient {
|
) CommanderClient {
|
||||||
return CommanderClient{cmder, wg}
|
return CommanderClient{cmder, new(sync.WaitGroup), id, label}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *CommanderClient) HandleCommands(ctx context.Context, srvAddr string, tc credentials.TransportCredentials) error {
|
func (self *CommanderClient) HandleCommands(ctx context.Context, srvAddr string, tc credentials.TransportCredentials) error {
|
||||||
cli, err := grpc.NewClient(srvAddr, grpc.WithTransportCredentials(tc))
|
cli, err := grpc.NewClient(srvAddr, grpc.WithTransportCredentials(tc))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Failed to connect to gRPC: %w", err)
|
return fmt.Errorf("Failed to connect to gRPC: %w", err)
|
||||||
}
|
}
|
||||||
ccli := proto.NewCommanderClient(cli)
|
ccli := proto.NewCommanderClient(cli)
|
||||||
bidi, err := ccli.Stream(ctx)
|
bidi, err := ccli.Stream(metadata.NewOutgoingContext(ctx, metadata.MD{"agentid": []string{self.id}, "label": []string{self.label}}))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -56,7 +59,6 @@ func (self *CommanderClient) recv(bidi grpc.BidiStreamingClient[proto.FinishedCo
|
|||||||
}
|
}
|
||||||
self.wg.Go(func() {
|
self.wg.Go(func() {
|
||||||
func() error {
|
func() error {
|
||||||
|
|
||||||
fc, err := self.cmder.Execute(msg)
|
fc, err := self.cmder.Execute(msg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -10,10 +10,10 @@ import (
|
|||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Commander struct {
|
type CommandExecutor struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*Commander) Execute(command *proto.Command) (*proto.FinishedCommand, error) {
|
func (*CommandExecutor) Execute(command *proto.Command) (*proto.FinishedCommand, error) {
|
||||||
cmd := exec.Command(command.Command[0], command.Command[1:]...)
|
cmd := exec.Command(command.Command[0], command.Command[1:]...)
|
||||||
var (
|
var (
|
||||||
stdin io.WriteCloser
|
stdin io.WriteCloser
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ServiceConfig struct {
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
Type string `yaml:"type"`
|
||||||
|
Path *string `yaml:"path"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AgentConfig struct {
|
||||||
|
BackendURL string `yaml:"backend_url"`
|
||||||
|
GRPCURL string `yaml:"grpc_url"`
|
||||||
|
RegistrationToken string `yaml:"registration_token"`
|
||||||
|
Label string `yaml:"label"`
|
||||||
|
CertDir string `yaml:"cert_dir"`
|
||||||
|
Services []ServiceConfig `yaml:"services"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func Load(path string) (*AgentConfig, error) {
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var cfg AgentConfig
|
||||||
|
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.CertDir == "" {
|
||||||
|
cfg.CertDir = "/etc/hellreign-agent/certs"
|
||||||
|
}
|
||||||
|
|
||||||
|
return &cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadFromString(data string) (*AgentConfig, error) {
|
||||||
|
var cfg AgentConfig
|
||||||
|
if err := yaml.Unmarshal([]byte(data), &cfg); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateConfigPath(path string) error {
|
||||||
|
if _, err := os.Stat(path); err != nil {
|
||||||
|
return fmt.Errorf("config file not found: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Logger struct {
|
||||||
|
*slog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(debug bool) *Logger {
|
||||||
|
var level slog.Level
|
||||||
|
if debug {
|
||||||
|
level = slog.LevelDebug
|
||||||
|
} else {
|
||||||
|
level = slog.LevelInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
handler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
|
||||||
|
Level: level,
|
||||||
|
})
|
||||||
|
|
||||||
|
return &Logger{
|
||||||
|
Logger: slog.New(handler),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package file
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/agent/internal/logsource"
|
||||||
|
"github.com/hpcloud/tail"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ logsource.LogSource = new(FileLogSource)
|
||||||
|
|
||||||
|
type FileLogSource struct {
|
||||||
|
*tail.Tail
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(filepath string) (fls *FileLogSource, err error) {
|
||||||
|
if _, err := os.Stat(filepath); os.IsNotExist(err) {
|
||||||
|
if err := os.WriteFile(filepath, []byte{}, 0600); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t, err := tail.TailFile(filepath, tail.Config{
|
||||||
|
Follow: true,
|
||||||
|
Location: &tail.SeekInfo{
|
||||||
|
Offset: 100,
|
||||||
|
Whence: 2,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return &FileLogSource{t}, nil
|
||||||
|
}
|
||||||
|
func (f *FileLogSource) ReadLine() (string, error) {
|
||||||
|
select {
|
||||||
|
case <-f.Dead():
|
||||||
|
return "", errors.Join(logsource.ErrDead, f.Err())
|
||||||
|
case line := <-f.Lines:
|
||||||
|
return line.Text, line.Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (f *FileLogSource) Close() error {
|
||||||
|
return f.Stop()
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package logsource
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
type LogSource interface {
|
||||||
|
ReadLine() (string, error)
|
||||||
|
Close() error
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrDead = errors.New("shouldn't continue to read that")
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package journald
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os/exec"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/agent/internal/config"
|
||||||
|
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/agent/internal/logsource"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ logsource.LogSource = new(JournaldLogSource)
|
||||||
|
|
||||||
|
type JournaldLogSource struct {
|
||||||
|
cmd *exec.Cmd
|
||||||
|
stdout io.ReadCloser
|
||||||
|
stdoutscanner *bufio.Scanner
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadLine implements logsource.LogSource.
|
||||||
|
func (j *JournaldLogSource) ReadLine() (string, error) {
|
||||||
|
if j.stdoutscanner.Scan() {
|
||||||
|
return j.stdoutscanner.Text(), nil
|
||||||
|
} else {
|
||||||
|
if j.stdoutscanner.Err() == nil {
|
||||||
|
return "", fmt.Errorf("%w: %s", logsource.ErrDead, io.EOF)
|
||||||
|
}
|
||||||
|
return "", j.stdoutscanner.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (j *JournaldLogSource) Close() error {
|
||||||
|
_ = j.cmd.Process.Signal(syscall.SIGTERM)
|
||||||
|
return j.cmd.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(cfg config.ServiceConfig, logdir string) (*JournaldLogSource, error) {
|
||||||
|
args := make([]string, 0)
|
||||||
|
if cfg.Path != nil {
|
||||||
|
args = append(args, "-u", *cfg.Path)
|
||||||
|
}
|
||||||
|
args = append(args, "-f", "-n", "0", "-o", "short", "--no-pager", "--directory", logdir)
|
||||||
|
cmd := exec.Command("journalctl", args...) //nolint:gosec
|
||||||
|
stdout, err := cmd.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = cmd.Start()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
stdoutscanner := bufio.NewScanner(stdout)
|
||||||
|
return &JournaldLogSource{cmd, stdout, stdoutscanner}, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package mtls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"google.golang.org/grpc/credentials"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoadMTLSCredentials loads client certificate and CA certificate for mTLS.
|
||||||
|
func LoadMTLSCredentials(caCertPEM, clientCertPEM, clientKeyPEM []byte) (credentials.TransportCredentials, error) {
|
||||||
|
cert, err := tls.X509KeyPair(clientCertPEM, clientKeyPEM)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("load client key pair: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
caCertPool := x509.NewCertPool()
|
||||||
|
if !caCertPool.AppendCertsFromPEM(caCertPEM) {
|
||||||
|
return nil, fmt.Errorf("failed to append CA certificate")
|
||||||
|
}
|
||||||
|
|
||||||
|
tlsConfig := &tls.Config{
|
||||||
|
Certificates: []tls.Certificate{cert},
|
||||||
|
RootCAs: caCertPool,
|
||||||
|
MinVersion: tls.VersionTLS12,
|
||||||
|
}
|
||||||
|
|
||||||
|
return credentials.NewTLS(tlsConfig), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadMTLSCredentialsFromFiles loads mTLS credentials from file paths.
|
||||||
|
func LoadMTLSCredentialsFromFiles(caCertPath, clientCertPath, clientKeyPath string) (credentials.TransportCredentials, error) {
|
||||||
|
caCert, err := os.ReadFile(caCertPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read CA cert: %w", err)
|
||||||
|
}
|
||||||
|
clientCert, err := os.ReadFile(clientCertPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read client cert: %w", err)
|
||||||
|
}
|
||||||
|
clientKey, err := os.ReadFile(clientKeyPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read client key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return LoadMTLSCredentials(caCert, clientCert, clientKey)
|
||||||
|
}
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
package registration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/elliptic"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
|
"encoding/json"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Certs struct {
|
||||||
|
CACertPEM []byte
|
||||||
|
ClientCertPEM []byte
|
||||||
|
ClientKeyPEM []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type RegisterRequest struct {
|
||||||
|
CSR string `json:"csr"`
|
||||||
|
Token string `json:"token"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RegisterResponse struct {
|
||||||
|
CACert string `json:"ca_cert"`
|
||||||
|
ClientCert string `json:"client_cert"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TokenResponse struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ErrorResponse struct {
|
||||||
|
Error string `json:"error"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateKeyAndCSR generates a new ECDSA private key and CSR for the agent.
|
||||||
|
func GenerateKeyAndCSR(label string) (*ecdsa.PrivateKey, []byte, error) {
|
||||||
|
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("generate key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
template := x509.CertificateRequest{
|
||||||
|
Subject: pkix.Name{
|
||||||
|
CommonName: label,
|
||||||
|
Organization: []string{"HellreigN Agent"},
|
||||||
|
},
|
||||||
|
SignatureAlgorithm: x509.ECDSAWithSHA256,
|
||||||
|
}
|
||||||
|
|
||||||
|
csrDER, err := x509.CreateCertificateRequest(rand.Reader, &template, key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("create csr: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
csrPEM := pem.EncodeToMemory(&pem.Block{
|
||||||
|
Type: "CERTIFICATE REQUEST",
|
||||||
|
Bytes: csrDER,
|
||||||
|
})
|
||||||
|
|
||||||
|
return key, csrPEM, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register sends CSR to backend and receives signed certificates.
|
||||||
|
func Register(backendURL, token string, csrPEM []byte) (*Certs, error) {
|
||||||
|
reqBody := RegisterRequest{CSR: string(csrPEM), Token: token}
|
||||||
|
body, err := json.Marshal(reqBody)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
url := fmt.Sprintf("%s/api/v1/agents/register", backendURL)
|
||||||
|
resp, err := http.Post(url, "application/json", bytes.NewReader(body))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("register request: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
var errResp ErrorResponse
|
||||||
|
json.NewDecoder(resp.Body).Decode(&errResp)
|
||||||
|
return nil, fmt.Errorf("registration failed (status %d): %s", resp.StatusCode, errResp.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
var regResp RegisterResponse
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(®Resp); err != nil {
|
||||||
|
return nil, fmt.Errorf("decode response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Certs{
|
||||||
|
CACertPEM: []byte(regResp.CACert),
|
||||||
|
ClientCertPEM: []byte(regResp.ClientCert),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveCerts saves CA cert, client cert, and client key to the given directory.
|
||||||
|
func SaveCerts(certDir string, certs *Certs, key *ecdsa.PrivateKey) error {
|
||||||
|
if err := os.MkdirAll(certDir, 0700); err != nil {
|
||||||
|
return fmt.Errorf("create cert dir: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(filepath.Join(certDir, "ca.crt"), certs.CACertPEM, 0644); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(certDir, "client.crt"), certs.ClientCertPEM, 0644); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
keyDER, err := x509.MarshalECPrivateKey(key)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
keyPEM := pem.EncodeToMemory(&pem.Block{
|
||||||
|
Type: "EC PRIVATE KEY",
|
||||||
|
Bytes: keyDER,
|
||||||
|
})
|
||||||
|
if err := os.WriteFile(filepath.Join(certDir, "client.key"), keyPEM, 0600); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadCerts loads existing certificates and key from disk.
|
||||||
|
func LoadCerts(certDir string) (*Certs, *ecdsa.PrivateKey, error) {
|
||||||
|
caCert, err := os.ReadFile(filepath.Join(certDir, "ca.crt"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
clientCert, err := os.ReadFile(filepath.Join(certDir, "client.crt"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
clientKeyPEM, err := os.ReadFile(filepath.Join(certDir, "client.key"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
block, _ := pem.Decode(clientKeyPEM)
|
||||||
|
if block == nil {
|
||||||
|
return nil, nil, fmt.Errorf("decode client key")
|
||||||
|
}
|
||||||
|
key, err := x509.ParseECPrivateKey(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("parse client key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Certs{
|
||||||
|
CACertPEM: caCert,
|
||||||
|
ClientCertPEM: clientCert,
|
||||||
|
}, key, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CertsExist checks if all certificate files exist in the directory.
|
||||||
|
func CertsExist(certDir string) bool {
|
||||||
|
files := []string{"ca.crt", "client.crt", "client.key"}
|
||||||
|
for _, f := range files {
|
||||||
|
if _, err := os.Stat(filepath.Join(certDir, f)); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
+302
@@ -1 +1,303 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/agent/internal/buffer"
|
||||||
|
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/agent/internal/client"
|
||||||
|
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/agent/internal/commander"
|
||||||
|
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/agent/internal/config"
|
||||||
|
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/agent/internal/logger"
|
||||||
|
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/agent/internal/logsource"
|
||||||
|
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/agent/internal/logsource/file"
|
||||||
|
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/agent/internal/logsource/journald"
|
||||||
|
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/agent/internal/mtls"
|
||||||
|
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/agent/internal/registration"
|
||||||
|
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/proto/proto"
|
||||||
|
"github.com/samber/lo"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/metadata"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cfgPath := os.Getenv("CONFIG_FILE")
|
||||||
|
if cfgPath == "" {
|
||||||
|
cfgPath = "/etc/hellreign-agent/config.yml"
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := config.Load(cfgPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to load config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
lgr := logger.New(os.Getenv("IS_DEBUG") == "1")
|
||||||
|
lgr.Debug("Config parsed", "cfg", cfg)
|
||||||
|
|
||||||
|
// Check if certificates already exist (agent was previously registered)
|
||||||
|
if registration.CertsExist(cfg.CertDir) {
|
||||||
|
lgr.Info("Certificates found, skipping registration")
|
||||||
|
} else {
|
||||||
|
if cfg.RegistrationToken == "" {
|
||||||
|
lgr.Error("No registration token provided")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate key and CSR
|
||||||
|
k, csrPEM, err := registration.GenerateKeyAndCSR(cfg.Label)
|
||||||
|
if err != nil {
|
||||||
|
lgr.Error("Failed to generate key and CSR", "err", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
lgr.Info("Generated ECDSA key pair and CSR")
|
||||||
|
|
||||||
|
// Register with backend
|
||||||
|
certs, err := registration.Register(cfg.BackendURL, cfg.RegistrationToken, csrPEM)
|
||||||
|
if err != nil {
|
||||||
|
lgr.Error("Failed to register", "err", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
lgr.Info("Successfully registered, received certificates")
|
||||||
|
|
||||||
|
// Save certificates
|
||||||
|
if err := registration.SaveCerts(cfg.CertDir, certs, k); err != nil {
|
||||||
|
lgr.Error("Failed to save certificates", "err", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
lgr.Info("Certificates saved", "cert_dir", cfg.CertDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
creds, err := mtls.LoadMTLSCredentialsFromFiles(
|
||||||
|
cfg.CertDir+"/ca.crt",
|
||||||
|
cfg.CertDir+"/client.crt",
|
||||||
|
cfg.CertDir+"/client.key",
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
lgr.Error("Failed to load TLS credentials", "err", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize log buffer for offline storage
|
||||||
|
dbPath := getEnvOrDefault("BUFFER_DB", "/var/lib/hellreign-agent/agent_buffer.db")
|
||||||
|
logBuf, err := buffer.NewLogBuffer(dbPath)
|
||||||
|
if err != nil {
|
||||||
|
lgr.Error("Failed to create log buffer", "err", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defer func() { _ = logBuf.Close() }()
|
||||||
|
lgr.Info("Log buffer initialized", "path", dbPath)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
wg := &errgroup.Group{}
|
||||||
|
|
||||||
|
grpcAddr := cfg.GRPCURL
|
||||||
|
if grpcAddr == "" {
|
||||||
|
grpcAddr = cfg.BackendURL
|
||||||
|
}
|
||||||
|
grpcAddr = strings.TrimPrefix(grpcAddr, "http://")
|
||||||
|
grpcAddr = strings.TrimPrefix(grpcAddr, "https://")
|
||||||
|
// Start command executor
|
||||||
|
wg.Go(func() error {
|
||||||
|
cmdexe := new(commander.CommandExecutor)
|
||||||
|
ccli := client.New(cmdexe, cfg.Label, cfg.Label)
|
||||||
|
return ccli.HandleCommands(ctx, grpcAddr, creds)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Start log collectors
|
||||||
|
if len(cfg.Services) > 0 {
|
||||||
|
wg.Go(func() error {
|
||||||
|
conn, err := grpc.NewClient(grpcAddr, grpc.WithTransportCredentials(creds))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to connect to gRPC: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = conn.Close() }()
|
||||||
|
|
||||||
|
ccli := proto.NewCollectorClient(conn)
|
||||||
|
|
||||||
|
svcWg := new(errgroup.Group)
|
||||||
|
for _, svc := range cfg.Services {
|
||||||
|
svc := svc
|
||||||
|
var src logsource.LogSource
|
||||||
|
switch svc.Type {
|
||||||
|
case "journald":
|
||||||
|
src, err = journald.New(svc, os.Getenv("JOURNALD_LOGDIR"))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create journald source %q: %w", svc.Name, err)
|
||||||
|
}
|
||||||
|
case "file":
|
||||||
|
if svc.Path == nil {
|
||||||
|
return fmt.Errorf("path is required for file log source %q", svc.Name)
|
||||||
|
}
|
||||||
|
src, err = file.New(*svc.Path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create file source %q: %w", svc.Name, err)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unknown log source type %q for service %q", svc.Type, svc.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
svcWg.Go(func() error {
|
||||||
|
lgr.Info("Starting log stream", "service", svc.Name)
|
||||||
|
|
||||||
|
// First, flush any buffered logs from offline period
|
||||||
|
if err := flushBufferedLogs(ctx, ccli, logBuf, svc.Name, cfg.Label, cfg.RegistrationToken, lgr); err != nil {
|
||||||
|
lgr.Error("Failed to flush buffered logs", "service", svc.Name, "err", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
scli, err := ccli.Stream(
|
||||||
|
metadata.NewOutgoingContext(ctx, metadata.MD{
|
||||||
|
"whoami": []string{cfg.Label},
|
||||||
|
"service": []string{svc.Name},
|
||||||
|
"token": []string{cfg.RegistrationToken},
|
||||||
|
"services": lo.Map(cfg.Services, func(item config.ServiceConfig, _ int) string {
|
||||||
|
return item.Name
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create stream: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
line, err := src.ReadLine()
|
||||||
|
if err != nil {
|
||||||
|
lgr.Error("ReadLine error", "service", svc.Name, "err", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := scli.Send(&proto.CollectorRequest{
|
||||||
|
Message: line,
|
||||||
|
}); err != nil {
|
||||||
|
// Connection failed, buffer the log
|
||||||
|
lgr.Warn("Send failed, buffering log", "service", svc.Name, "err", err)
|
||||||
|
if storeErr := logBuf.Store(svc.Name, line); storeErr != nil {
|
||||||
|
lgr.Error("Failed to buffer log", "service", svc.Name, "err", storeErr)
|
||||||
|
}
|
||||||
|
// Try to reconnect
|
||||||
|
if reconnectErr := reconnectStream(ctx, &scli, ccli, svc.Name, cfg.Label, cfg.RegistrationToken, logBuf, lgr); reconnectErr != nil {
|
||||||
|
return reconnectErr
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return svcWg.Wait()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := wg.Wait(); err != nil {
|
||||||
|
lgr.Error("Agent dead", "err", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEnvOrDefault(key, defaultValue string) string {
|
||||||
|
if value := os.Getenv(key); value != "" {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
// flushBufferedLogs sends any buffered logs to the server
|
||||||
|
func flushBufferedLogs(
|
||||||
|
ctx context.Context,
|
||||||
|
ccli proto.CollectorClient,
|
||||||
|
logBuf *buffer.LogBuffer,
|
||||||
|
service, agentName, token string,
|
||||||
|
lgr *logger.Logger,
|
||||||
|
) error {
|
||||||
|
count, err := logBuf.Count()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if count == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
lgr.Info("Flushing buffered logs", "service", service, "count", count)
|
||||||
|
|
||||||
|
scli, err := ccli.Stream(
|
||||||
|
metadata.NewOutgoingContext(ctx, metadata.MD{
|
||||||
|
"whoami": []string{agentName},
|
||||||
|
"service": []string{service},
|
||||||
|
"token": []string{token},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create stream for flush: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
const batchSize = 100
|
||||||
|
var deletedIDs []int64
|
||||||
|
|
||||||
|
for {
|
||||||
|
logs, err := logBuf.GetPending(batchSize)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(logs) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, logEntry := range logs {
|
||||||
|
if err := scli.Send(&proto.CollectorRequest{Message: logEntry.Message}); err != nil {
|
||||||
|
lgr.Error("Failed to send buffered log", "service", service, "err", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
deletedIDs = append(deletedIDs, logEntry.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete successfully sent logs
|
||||||
|
if err := logBuf.DeleteBatch(deletedIDs); err != nil {
|
||||||
|
lgr.Error("Failed to delete sent logs from buffer", "service", service, "err", err)
|
||||||
|
}
|
||||||
|
deletedIDs = deletedIDs[:0]
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = scli.CloseAndRecv()
|
||||||
|
lgr.Info("Buffer flush complete", "service", service)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// reconnectStream attempts to recreate a gRPC stream connection
|
||||||
|
func reconnectStream(
|
||||||
|
ctx context.Context,
|
||||||
|
scli *grpc.ClientStreamingClient[proto.CollectorRequest, proto.CollectorResponse],
|
||||||
|
ccli proto.CollectorClient,
|
||||||
|
service, agentName, token string,
|
||||||
|
buf *buffer.LogBuffer,
|
||||||
|
lgr *logger.Logger,
|
||||||
|
) error {
|
||||||
|
lgr.Info("Attempting to reconnect stream...", "service", service)
|
||||||
|
|
||||||
|
// Try up to 5 times with exponential backoff
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
time.Sleep(time.Duration(i+1) * time.Second)
|
||||||
|
|
||||||
|
newCli, err := ccli.Stream(
|
||||||
|
metadata.NewOutgoingContext(ctx, metadata.MD{
|
||||||
|
"whoami": []string{agentName},
|
||||||
|
"service": []string{service},
|
||||||
|
"token": []string{token},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
lgr.Warn("Reconnect attempt failed", "service", service, "attempt", i+1, "err", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
*scli = newCli
|
||||||
|
lgr.Info("Stream reconnected successfully", "service", service)
|
||||||
|
return flushBufferedLogs(ctx, ccli, buf, service, agentName, token, lgr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("failed to reconnect after 5 attempts for service %s", service)
|
||||||
|
}
|
||||||
|
|||||||
+191
-19
@@ -2,17 +2,28 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
"log"
|
"log"
|
||||||
|
"net"
|
||||||
"os"
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/docs"
|
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/docs"
|
||||||
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/config"
|
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/config"
|
||||||
|
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/grpcsrv/collector"
|
||||||
|
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/grpcsrv/commander"
|
||||||
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/handlers"
|
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/handlers"
|
||||||
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/repository"
|
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/repository"
|
||||||
|
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/service"
|
||||||
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/storage"
|
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/storage"
|
||||||
|
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/proto/proto"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
swaggerFiles "github.com/swaggo/files"
|
swaggerFiles "github.com/swaggo/files"
|
||||||
ginSwagger "github.com/swaggo/gin-swagger"
|
ginSwagger "github.com/swaggo/gin-swagger"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/credentials"
|
||||||
)
|
)
|
||||||
|
|
||||||
// @securityDefinitions.apikey Bearer
|
// @securityDefinitions.apikey Bearer
|
||||||
@@ -37,8 +48,58 @@ func main() {
|
|||||||
defer db.Close()
|
defer db.Close()
|
||||||
|
|
||||||
h := handlers.New(db)
|
h := handlers.New(db)
|
||||||
agents := handlers.AgentsGroup{Handlers: h}
|
|
||||||
|
// Initialize registration tokens table
|
||||||
|
if err := h.Repo.InitRegistrationTokens(); err != nil {
|
||||||
|
log.Printf("Warning: failed to initialize registration tokens table: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize jobs table
|
||||||
|
jobRepo := repository.NewJobRepository(db)
|
||||||
|
if err := jobRepo.Init(context.Background()); err != nil {
|
||||||
|
log.Printf("Warning: failed to initialize jobs table: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize ClickHouse and log repository
|
||||||
|
logRepo := repository.NewLogRepository()
|
||||||
|
if cfg.Database.Clickhouse_host != "" {
|
||||||
|
go func() {
|
||||||
|
db, err := storage.OpenClickHouseWithRetry(storage.ClickHouseConfig{
|
||||||
|
Host: cfg.Database.Clickhouse_host,
|
||||||
|
User: cfg.Database.Clickhouse_user,
|
||||||
|
Password: cfg.Database.Clickhouse_password,
|
||||||
|
Database: cfg.Database.Clickhouse_database,
|
||||||
|
}, 10, 5*time.Second)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Warning: ClickHouse connection failed: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Println("ClickHouse connected successfully")
|
||||||
|
logRepo.SetDB(db)
|
||||||
|
if err := logRepo.Init(context.Background()); err != nil {
|
||||||
|
log.Printf("Warning: Failed to initialize logs table: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize Collector gRPC service
|
||||||
|
coll := collector.New(logRepo)
|
||||||
|
|
||||||
|
cmdr := commander.New(jobRepo)
|
||||||
|
|
||||||
|
// Initialize script interpreter repository and service
|
||||||
|
scriptRepo := repository.NewScriptInterpreterRepo(db)
|
||||||
|
if err := scriptRepo.Init(context.Background()); err != nil {
|
||||||
|
log.Printf("Warning: failed to initialize script interpreters table: %v", err)
|
||||||
|
}
|
||||||
|
scriptSvc := service.NewScriptService(scriptRepo)
|
||||||
|
scriptHandlers := handlers.NewScriptHandlers(scriptSvc, cmdr)
|
||||||
|
jobsHandlers := handlers.NewJobsHandlers(cmdr, scriptSvc)
|
||||||
|
|
||||||
|
agents := handlers.NewAgentsGroup(h, coll)
|
||||||
auth := handlers.AuthGroup{Handlers: h}
|
auth := handlers.AuthGroup{Handlers: h}
|
||||||
|
agentReg := handlers.NewAgentRegistrationGroup(h)
|
||||||
|
agentDeploy := handlers.NewAgentDeployGroup(h)
|
||||||
|
|
||||||
// Create admin user from config if not exists
|
// Create admin user from config if not exists
|
||||||
if cfg.Admin.Admin_login != "" && cfg.Admin.Admin_password != "" {
|
if cfg.Admin.Admin_login != "" && cfg.Admin.Admin_password != "" {
|
||||||
@@ -49,13 +110,22 @@ func main() {
|
|||||||
Login: cfg.Admin.Admin_login,
|
Login: cfg.Admin.Admin_login,
|
||||||
Password: cfg.Admin.Admin_password,
|
Password: cfg.Admin.Admin_password,
|
||||||
PermissionView: true,
|
PermissionView: true,
|
||||||
|
PermissionManage: true,
|
||||||
PermissionAdmin: true,
|
PermissionAdmin: true,
|
||||||
|
IsActive: true,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Warning: failed to create admin user: %v", err)
|
log.Printf("Warning: failed to create admin user: %v", err)
|
||||||
} else {
|
} else {
|
||||||
log.Println("Admin user created from config")
|
log.Println("Admin user created from config")
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Ensure existing admin is activated
|
||||||
|
if err := h.Repo.ActivateUserByLogin(cfg.Admin.Admin_login); err != nil {
|
||||||
|
log.Printf("Warning: failed to activate admin user: %v", err)
|
||||||
|
} else {
|
||||||
|
log.Println("Admin user activated")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,6 +154,17 @@ func main() {
|
|||||||
authTokenGroup.GET("/tokens", handlers.RequireAdmin(), auth.ListTokens)
|
authTokenGroup.GET("/tokens", handlers.RequireAdmin(), auth.ListTokens)
|
||||||
authTokenGroup.DELETE("/token", auth.DeleteMyToken)
|
authTokenGroup.DELETE("/token", auth.DeleteMyToken)
|
||||||
authTokenGroup.DELETE("/tokens/:login", handlers.RequireAdmin(), auth.DeleteToken)
|
authTokenGroup.DELETE("/tokens/:login", handlers.RequireAdmin(), auth.DeleteToken)
|
||||||
|
|
||||||
|
// User management (admin only) - Full CRUD
|
||||||
|
authTokenGroup.GET("/users/:login", handlers.RequireAdmin(), auth.GetUser)
|
||||||
|
authTokenGroup.PUT("/users/:login", handlers.RequireAdmin(), auth.UpdateUser)
|
||||||
|
authTokenGroup.PUT("/users/:login/permissions", handlers.RequireAdmin(), auth.UpdateUserPermissions)
|
||||||
|
authTokenGroup.PUT("/users/:login/password", handlers.RequireAdmin(), auth.ResetUserPassword)
|
||||||
|
|
||||||
|
// User activation management (admin only)
|
||||||
|
authTokenGroup.POST("/users/:login/activate", handlers.RequireAdmin(), auth.ActivateUser)
|
||||||
|
authTokenGroup.POST("/users/:login/deactivate", handlers.RequireAdmin(), auth.DeactivateUser)
|
||||||
|
authTokenGroup.GET("/users/inactive", handlers.RequireAdmin(), auth.ListInactiveUsers)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Agents (requires manage_agent permission)
|
// Agents (requires manage_agent permission)
|
||||||
@@ -93,27 +174,34 @@ func main() {
|
|||||||
agentsGroup.GET("", agents.List)
|
agentsGroup.GET("", agents.List)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Jobs (requires admin permission)
|
||||||
|
jobsGroup := v1.Group("/jobs")
|
||||||
|
jobsGroup.Use(auth.AuthMiddleware(), handlers.RequireAdmin())
|
||||||
|
{
|
||||||
|
jobsGroup.POST("", jobsHandlers.AddJob)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Agent registration
|
||||||
|
agentRegGroup := v1.Group("/agents")
|
||||||
|
{
|
||||||
|
agentRegGroup.POST("/register", agentReg.Register)
|
||||||
|
}
|
||||||
|
agentRegTokenGroup := v1.Group("/agents")
|
||||||
|
agentRegTokenGroup.Use(auth.AuthMiddleware(), handlers.RequireManageAgent())
|
||||||
|
{
|
||||||
|
agentRegTokenGroup.POST("/register-token", agentReg.CreateRegistrationToken)
|
||||||
|
agentRegTokenGroup.POST("/deploy", agentDeploy.DeployAgents)
|
||||||
|
}
|
||||||
|
|
||||||
// Logs (requires view permission)
|
// Logs (requires view permission)
|
||||||
logsGroup := v1.Group("/logs")
|
logsGroup := v1.Group("/logs")
|
||||||
logsGroup.Use(auth.AuthMiddleware(), handlers.RequireView())
|
logsGroup.Use(auth.AuthMiddleware(), handlers.RequireView())
|
||||||
{
|
{
|
||||||
if cfg.Database.Clickhouse_host != "" {
|
// Mock logs endpoint (always available, no ClickHouse required)
|
||||||
chConn, err := storage.OpenClickHouse(storage.ClickHouseConfig{
|
mockLogHandlers := handlers.NewLogHandlers(nil)
|
||||||
Host: cfg.Database.Clickhouse_host,
|
logsGroup.GET("/mock", mockLogHandlers.GetMockLogs)
|
||||||
User: cfg.Database.Clickhouse_user,
|
|
||||||
Password: cfg.Database.Clickhouse_password,
|
|
||||||
Database: cfg.Database.Clickhouse_database,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Warning: ClickHouse connection failed: %v", err)
|
|
||||||
} else {
|
|
||||||
defer chConn.Close()
|
|
||||||
|
|
||||||
logRepo := repository.NewLogRepository(chConn)
|
|
||||||
if err := logRepo.Init(context.Background()); err != nil {
|
|
||||||
log.Printf("Warning: Failed to initialize logs table: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// ClickHouse log handlers (always registered, work when ClickHouse connects)
|
||||||
logHandlers := handlers.NewLogHandlers(logRepo)
|
logHandlers := handlers.NewLogHandlers(logRepo)
|
||||||
logsGroup.POST("", logHandlers.Insert)
|
logsGroup.POST("", logHandlers.Insert)
|
||||||
logsGroup.POST("/batch", logHandlers.InsertBatch)
|
logsGroup.POST("/batch", logHandlers.InsertBatch)
|
||||||
@@ -122,9 +210,93 @@ func main() {
|
|||||||
logsGroup.GET("/agents", logHandlers.GetAgents)
|
logsGroup.GET("/agents", logHandlers.GetAgents)
|
||||||
logsGroup.GET("/levels", logHandlers.GetLevels)
|
logsGroup.GET("/levels", logHandlers.GetLevels)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// Scripts (requires admin permission)
|
||||||
|
scriptsGroup := v1.Group("/scripts")
|
||||||
|
scriptsGroup.Use(auth.AuthMiddleware(), handlers.RequireAdmin())
|
||||||
|
{
|
||||||
|
scriptsGroup.POST("/run", scriptHandlers.RunScript)
|
||||||
|
scriptsGroup.GET("/interpreters", scriptHandlers.ListInterpreters)
|
||||||
|
scriptsGroup.POST("/interpreters", scriptHandlers.CreateInterpreter)
|
||||||
|
scriptsGroup.GET("/interpreters/:id", scriptHandlers.GetInterpreter)
|
||||||
|
scriptsGroup.PUT("/interpreters/:id", scriptHandlers.UpdateInterpreter)
|
||||||
|
scriptsGroup.DELETE("/interpreters/:id", scriptHandlers.DeleteInterpreter)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Fatal(router.Run(":8080"))
|
// Start gRPC server with mTLS in background
|
||||||
|
grpcPort := os.Getenv("GRPC_PORT")
|
||||||
|
if grpcPort == "" {
|
||||||
|
grpcPort = "9001"
|
||||||
|
}
|
||||||
|
|
||||||
|
certDir := os.Getenv("SSL_CERT_DIR")
|
||||||
|
if certDir == "" {
|
||||||
|
certDir = "/var/lib/hellreign/ssl"
|
||||||
|
}
|
||||||
|
|
||||||
|
certFile := certDir + "/server.crt"
|
||||||
|
keyFile := certDir + "/server.key"
|
||||||
|
caFile := certDir + "/ca.crt"
|
||||||
|
|
||||||
|
// Load server cert
|
||||||
|
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to load server cert: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load CA cert for client verification
|
||||||
|
caCert, err := os.ReadFile(caFile)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to load CA cert: %v", err)
|
||||||
|
}
|
||||||
|
caCertPool := x509.NewCertPool()
|
||||||
|
caCertPool.AppendCertsFromPEM(caCert)
|
||||||
|
|
||||||
|
tlsConfig := &tls.Config{
|
||||||
|
Certificates: []tls.Certificate{cert},
|
||||||
|
ClientCAs: caCertPool,
|
||||||
|
ClientAuth: tls.RequireAndVerifyClientCert,
|
||||||
|
MinVersion: tls.VersionTLS12,
|
||||||
|
}
|
||||||
|
|
||||||
|
grpcServer := grpc.NewServer(grpc.Creds(credentials.NewTLS(tlsConfig)))
|
||||||
|
proto.RegisterCommanderServer(grpcServer, cmdr)
|
||||||
|
proto.RegisterCollectorServer(grpcServer, coll)
|
||||||
|
|
||||||
|
lis, err := net.Listen("tcp", ":"+grpcPort)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to listen on gRPC port %s: %v", grpcPort, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
g, ctx := errgroup.WithContext(context.Background())
|
||||||
|
|
||||||
|
g.Go(func() error {
|
||||||
|
log.Printf("gRPC server starting on port %s with mTLS", grpcPort)
|
||||||
|
errCh := make(chan error, 1)
|
||||||
|
go func() { errCh <- grpcServer.Serve(lis) }()
|
||||||
|
select {
|
||||||
|
case err := <-errCh:
|
||||||
|
return err
|
||||||
|
case <-ctx.Done():
|
||||||
|
grpcServer.GracefulStop()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
g.Go(func() error {
|
||||||
|
log.Printf("HTTP server starting on :8080")
|
||||||
|
errCh := make(chan error, 1)
|
||||||
|
go func() { errCh <- router.Run(":8080") }()
|
||||||
|
select {
|
||||||
|
case err := <-errCh:
|
||||||
|
return err
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := g.Wait(); err != nil {
|
||||||
|
log.Fatalf("Server error: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-9
@@ -2,7 +2,9 @@ FROM golang:1.26.1 as builder
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY . .
|
COPY backend/ backend/
|
||||||
|
COPY proto/ proto/
|
||||||
|
WORKDIR /app/backend
|
||||||
ENV CGO_ENABLED=0
|
ENV CGO_ENABLED=0
|
||||||
ENV GIN_MODE=release
|
ENV GIN_MODE=release
|
||||||
RUN --mount=type=cache,target=/go/pkg/mod \
|
RUN --mount=type=cache,target=/go/pkg/mod \
|
||||||
@@ -12,13 +14,11 @@ 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
|
RUN apk add --no-cache curl openssl bash ansible
|
||||||
|
|
||||||
COPY --from=builder /app/backend .
|
COPY --from=builder /app/backend/backend .
|
||||||
#COPY --from=builder /app/scripts /etc/mnemosyne/scripts
|
COPY --from=builder /app/backend/scripts /etc/hellreign/scripts
|
||||||
#RUN chmod +x /etc/mnemosyne/scripts/generate-certs.sh
|
RUN chmod +x /etc/hellreign/scripts/generate-certs.sh
|
||||||
|
|
||||||
EXPOSE 8080
|
# Generate certificates on container start
|
||||||
HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 CMD [ "curl --fail http://localhost:8080/health" ]
|
ENTRYPOINT ["/bin/sh", "-c", "/etc/hellreign/scripts/generate-certs.sh ${SSL_CERT_DIR:-/var/lib/hellreign/ssl} && exec ./backend"]
|
||||||
|
|
||||||
CMD ["./backend"]
|
|
||||||
|
|||||||
+996
-134
File diff suppressed because it is too large
Load Diff
+996
-134
File diff suppressed because it is too large
Load Diff
+616
-53
@@ -1,5 +1,157 @@
|
|||||||
definitions:
|
definitions:
|
||||||
gitea_d3m0k1d_ru_d3m0k1d_HellreigN_backend_internal_repository.LoginRequest:
|
handlers.AgentInfo:
|
||||||
|
properties:
|
||||||
|
connected_at:
|
||||||
|
type: string
|
||||||
|
label:
|
||||||
|
type: string
|
||||||
|
services:
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
token:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
handlers.InsertLogRequest:
|
||||||
|
properties:
|
||||||
|
agent:
|
||||||
|
type: string
|
||||||
|
level:
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
service:
|
||||||
|
type: string
|
||||||
|
timestamp:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- agent
|
||||||
|
- level
|
||||||
|
- message
|
||||||
|
- service
|
||||||
|
type: object
|
||||||
|
handlers.InsertLogsRequest:
|
||||||
|
properties:
|
||||||
|
logs:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/handlers.InsertLogRequest'
|
||||||
|
type: array
|
||||||
|
required:
|
||||||
|
- logs
|
||||||
|
type: object
|
||||||
|
handlers.RegisterRequest:
|
||||||
|
properties:
|
||||||
|
csr:
|
||||||
|
type: string
|
||||||
|
token:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- csr
|
||||||
|
- token
|
||||||
|
type: object
|
||||||
|
handlers.RegisterResponse:
|
||||||
|
properties:
|
||||||
|
ca_cert:
|
||||||
|
type: string
|
||||||
|
client_cert:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
repository.AgentDeployConfig:
|
||||||
|
description: Configuration for deploying HellreigN agent to a single server
|
||||||
|
properties:
|
||||||
|
agentLabel:
|
||||||
|
example: production-server-1
|
||||||
|
type: string
|
||||||
|
authMethod:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/definitions/repository.AuthMethod'
|
||||||
|
example: key
|
||||||
|
deployType:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/definitions/repository.DeployType'
|
||||||
|
example: docker
|
||||||
|
ip:
|
||||||
|
example: 192.168.1.100
|
||||||
|
type: string
|
||||||
|
password:
|
||||||
|
example: secret
|
||||||
|
type: string
|
||||||
|
port:
|
||||||
|
example: 22
|
||||||
|
type: integer
|
||||||
|
sshKey:
|
||||||
|
example: '-----BEGIN OPENSSH PRIVATE KEY-----'
|
||||||
|
type: string
|
||||||
|
user:
|
||||||
|
example: admin
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- agentLabel
|
||||||
|
- authMethod
|
||||||
|
- deployType
|
||||||
|
- ip
|
||||||
|
- user
|
||||||
|
type: object
|
||||||
|
repository.AuthMethod:
|
||||||
|
description: 'SSH authentication method: key or password'
|
||||||
|
enum:
|
||||||
|
- key
|
||||||
|
- password
|
||||||
|
type: string
|
||||||
|
x-enum-varnames:
|
||||||
|
- AuthMethodKey
|
||||||
|
- AuthMethodPassword
|
||||||
|
repository.DeployAgentsRequest:
|
||||||
|
description: Request to deploy HellreigN agents to multiple servers
|
||||||
|
properties:
|
||||||
|
servers:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/repository.AgentDeployConfig'
|
||||||
|
minItems: 1
|
||||||
|
type: array
|
||||||
|
required:
|
||||||
|
- servers
|
||||||
|
type: object
|
||||||
|
repository.DeployResponse:
|
||||||
|
description: Response containing deployment results and registration tokens
|
||||||
|
properties:
|
||||||
|
message:
|
||||||
|
example: Deployment completed
|
||||||
|
type: string
|
||||||
|
results:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/repository.DeployResult'
|
||||||
|
type: array
|
||||||
|
type: object
|
||||||
|
repository.DeployResult:
|
||||||
|
description: Result of deploying to a single server
|
||||||
|
properties:
|
||||||
|
agent_label:
|
||||||
|
example: production-server-1
|
||||||
|
type: string
|
||||||
|
error:
|
||||||
|
example: ""
|
||||||
|
type: string
|
||||||
|
ip:
|
||||||
|
example: 192.168.1.100
|
||||||
|
type: string
|
||||||
|
success:
|
||||||
|
example: true
|
||||||
|
type: boolean
|
||||||
|
token:
|
||||||
|
example: abc123...
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
repository.DeployType:
|
||||||
|
description: 'Type of deployment: docker or binary'
|
||||||
|
enum:
|
||||||
|
- docker
|
||||||
|
- binary
|
||||||
|
type: string
|
||||||
|
x-enum-varnames:
|
||||||
|
- DeployTypeDocker
|
||||||
|
- DeployTypeBinary
|
||||||
|
repository.LoginRequest:
|
||||||
properties:
|
properties:
|
||||||
login:
|
login:
|
||||||
type: string
|
type: string
|
||||||
@@ -9,8 +161,10 @@ definitions:
|
|||||||
- login
|
- login
|
||||||
- password
|
- password
|
||||||
type: object
|
type: object
|
||||||
gitea_d3m0k1d_ru_d3m0k1d_HellreigN_backend_internal_repository.LoginResponse:
|
repository.LoginResponse:
|
||||||
properties:
|
properties:
|
||||||
|
is_active:
|
||||||
|
type: boolean
|
||||||
last_name:
|
last_name:
|
||||||
type: string
|
type: string
|
||||||
login:
|
login:
|
||||||
@@ -26,8 +180,17 @@ definitions:
|
|||||||
token:
|
token:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
gitea_d3m0k1d_ru_d3m0k1d_HellreigN_backend_internal_repository.TokenCreate:
|
repository.RegistrationRequest:
|
||||||
properties:
|
properties:
|
||||||
|
label:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- label
|
||||||
|
type: object
|
||||||
|
repository.TokenCreate:
|
||||||
|
properties:
|
||||||
|
is_active:
|
||||||
|
type: boolean
|
||||||
last_name:
|
last_name:
|
||||||
type: string
|
type: string
|
||||||
login:
|
login:
|
||||||
@@ -48,10 +211,37 @@ definitions:
|
|||||||
- name
|
- name
|
||||||
- password
|
- password
|
||||||
type: object
|
type: object
|
||||||
gitea_d3m0k1d_ru_d3m0k1d_HellreigN_backend_internal_repository.Tokens:
|
repository.TokenPasswordReset:
|
||||||
|
properties:
|
||||||
|
new_password:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- new_password
|
||||||
|
type: object
|
||||||
|
repository.TokenUpdate:
|
||||||
|
properties:
|
||||||
|
last_name:
|
||||||
|
type: string
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
repository.TokenUpdatePermissions:
|
||||||
|
properties:
|
||||||
|
is_active:
|
||||||
|
type: boolean
|
||||||
|
permission_admin:
|
||||||
|
type: boolean
|
||||||
|
permission_manage_agent:
|
||||||
|
type: boolean
|
||||||
|
permission_view:
|
||||||
|
type: boolean
|
||||||
|
type: object
|
||||||
|
repository.Tokens:
|
||||||
properties:
|
properties:
|
||||||
id:
|
id:
|
||||||
type: integer
|
type: integer
|
||||||
|
is_active:
|
||||||
|
type: boolean
|
||||||
last_name:
|
last_name:
|
||||||
type: string
|
type: string
|
||||||
login:
|
login:
|
||||||
@@ -67,7 +257,7 @@ definitions:
|
|||||||
token:
|
token:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
gitea_d3m0k1d_ru_d3m0k1d_HellreigN_backend_internal_storage.LogEntry:
|
storage.LogEntry:
|
||||||
properties:
|
properties:
|
||||||
agent:
|
agent:
|
||||||
type: string
|
type: string
|
||||||
@@ -80,50 +270,13 @@ definitions:
|
|||||||
timestamp:
|
timestamp:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
internal_handlers.AgentInfo:
|
|
||||||
properties:
|
|
||||||
label:
|
|
||||||
type: string
|
|
||||||
services:
|
|
||||||
items:
|
|
||||||
type: string
|
|
||||||
type: array
|
|
||||||
token:
|
|
||||||
type: string
|
|
||||||
type: object
|
|
||||||
internal_handlers.InsertLogRequest:
|
|
||||||
properties:
|
|
||||||
agent:
|
|
||||||
type: string
|
|
||||||
level:
|
|
||||||
type: string
|
|
||||||
message:
|
|
||||||
type: string
|
|
||||||
service:
|
|
||||||
type: string
|
|
||||||
timestamp:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- agent
|
|
||||||
- level
|
|
||||||
- message
|
|
||||||
- service
|
|
||||||
type: object
|
|
||||||
internal_handlers.InsertLogsRequest:
|
|
||||||
properties:
|
|
||||||
logs:
|
|
||||||
items:
|
|
||||||
$ref: '#/definitions/internal_handlers.InsertLogRequest'
|
|
||||||
type: array
|
|
||||||
required:
|
|
||||||
- logs
|
|
||||||
type: object
|
|
||||||
info:
|
info:
|
||||||
contact: {}
|
contact: {}
|
||||||
paths:
|
paths:
|
||||||
/agents:
|
/agents:
|
||||||
get:
|
get:
|
||||||
description: Returns a list of all agents currently connected via gRPC streaming
|
description: Returns a list of all agents currently connected via Collector
|
||||||
|
(log streaming)
|
||||||
produces:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
responses:
|
responses:
|
||||||
@@ -131,11 +284,96 @@ paths:
|
|||||||
description: OK
|
description: OK
|
||||||
schema:
|
schema:
|
||||||
items:
|
items:
|
||||||
$ref: '#/definitions/internal_handlers.AgentInfo'
|
$ref: '#/definitions/handlers.AgentInfo'
|
||||||
type: array
|
type: array
|
||||||
|
security:
|
||||||
|
- Bearer: []
|
||||||
summary: Get connected agents
|
summary: Get connected agents
|
||||||
tags:
|
tags:
|
||||||
- agents
|
- agents
|
||||||
|
/agents/deploy:
|
||||||
|
post:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Deploy HellreigN agents to multiple servers using Ansible playbooks.
|
||||||
|
Supports Docker and Binary deployment types.
|
||||||
|
parameters:
|
||||||
|
- description: Deployment configuration for servers
|
||||||
|
in: body
|
||||||
|
name: request
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/repository.DeployAgentsRequest'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Deployment results with tokens for each server
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/repository.DeployResponse'
|
||||||
|
"400":
|
||||||
|
description: Invalid request
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
"500":
|
||||||
|
description: Internal server error
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
security:
|
||||||
|
- Bearer: []
|
||||||
|
summary: Deploy agents to multiple servers via Ansible
|
||||||
|
tags:
|
||||||
|
- agents
|
||||||
|
/agents/register:
|
||||||
|
post:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
parameters:
|
||||||
|
- description: CSR + token
|
||||||
|
in: body
|
||||||
|
name: request
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/handlers.RegisterRequest'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/handlers.RegisterResponse'
|
||||||
|
summary: Register agent
|
||||||
|
tags:
|
||||||
|
- agents
|
||||||
|
/agents/register-token:
|
||||||
|
post:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
parameters:
|
||||||
|
- description: Label
|
||||||
|
in: body
|
||||||
|
name: request
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/repository.RegistrationRequest'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
security:
|
||||||
|
- Bearer: []
|
||||||
|
summary: Create registration token
|
||||||
|
tags:
|
||||||
|
- agents
|
||||||
/auth/login:
|
/auth/login:
|
||||||
post:
|
post:
|
||||||
consumes:
|
consumes:
|
||||||
@@ -147,12 +385,12 @@ paths:
|
|||||||
name: request
|
name: request
|
||||||
required: true
|
required: true
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_HellreigN_backend_internal_repository.LoginRequest'
|
$ref: '#/definitions/repository.LoginRequest'
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: OK
|
description: OK
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_HellreigN_backend_internal_repository.LoginResponse'
|
$ref: '#/definitions/repository.LoginResponse'
|
||||||
"400":
|
"400":
|
||||||
description: Bad Request
|
description: Bad Request
|
||||||
schema:
|
schema:
|
||||||
@@ -165,6 +403,12 @@ paths:
|
|||||||
additionalProperties:
|
additionalProperties:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
|
"403":
|
||||||
|
description: Forbidden
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
summary: Login
|
summary: Login
|
||||||
tags:
|
tags:
|
||||||
- auth
|
- auth
|
||||||
@@ -203,7 +447,7 @@ paths:
|
|||||||
name: request
|
name: request
|
||||||
required: true
|
required: true
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_HellreigN_backend_internal_repository.TokenCreate'
|
$ref: '#/definitions/repository.TokenCreate'
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: OK
|
description: OK
|
||||||
@@ -242,7 +486,7 @@ paths:
|
|||||||
description: OK
|
description: OK
|
||||||
schema:
|
schema:
|
||||||
items:
|
items:
|
||||||
$ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_HellreigN_backend_internal_repository.Tokens'
|
$ref: '#/definitions/repository.Tokens'
|
||||||
type: array
|
type: array
|
||||||
"500":
|
"500":
|
||||||
description: Internal Server Error
|
description: Internal Server Error
|
||||||
@@ -284,6 +528,272 @@ paths:
|
|||||||
summary: Delete user
|
summary: Delete user
|
||||||
tags:
|
tags:
|
||||||
- auth
|
- auth
|
||||||
|
/auth/users/:login:
|
||||||
|
get:
|
||||||
|
description: Returns a user by their login (admin only)
|
||||||
|
parameters:
|
||||||
|
- description: Login of the user
|
||||||
|
in: path
|
||||||
|
name: login
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/repository.Tokens'
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
"404":
|
||||||
|
description: Not Found
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
summary: Get user by login
|
||||||
|
tags:
|
||||||
|
- auth
|
||||||
|
put:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Updates a user's name and last name (admin only)
|
||||||
|
parameters:
|
||||||
|
- description: Login of the user
|
||||||
|
in: path
|
||||||
|
name: login
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
- description: User data to update
|
||||||
|
in: body
|
||||||
|
name: request
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/repository.TokenUpdate'
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
"404":
|
||||||
|
description: Not Found
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
summary: Update user
|
||||||
|
tags:
|
||||||
|
- auth
|
||||||
|
/auth/users/:login/activate:
|
||||||
|
post:
|
||||||
|
description: Activates a user account by login (admin only)
|
||||||
|
parameters:
|
||||||
|
- description: Login of the user to activate
|
||||||
|
in: path
|
||||||
|
name: login
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
"404":
|
||||||
|
description: Not Found
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
summary: Activate user
|
||||||
|
tags:
|
||||||
|
- auth
|
||||||
|
/auth/users/:login/deactivate:
|
||||||
|
post:
|
||||||
|
description: Deactivates a user account by login (admin only)
|
||||||
|
parameters:
|
||||||
|
- description: Login of the user to deactivate
|
||||||
|
in: path
|
||||||
|
name: login
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
"404":
|
||||||
|
description: Not Found
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
summary: Deactivate user
|
||||||
|
tags:
|
||||||
|
- auth
|
||||||
|
/auth/users/:login/password:
|
||||||
|
put:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Resets a user's password to a new value (admin only)
|
||||||
|
parameters:
|
||||||
|
- description: Login of the user
|
||||||
|
in: path
|
||||||
|
name: login
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
- description: New password
|
||||||
|
in: body
|
||||||
|
name: request
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/repository.TokenPasswordReset'
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
"404":
|
||||||
|
description: Not Found
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
summary: Reset user password
|
||||||
|
tags:
|
||||||
|
- auth
|
||||||
|
/auth/users/:login/permissions:
|
||||||
|
put:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Updates a user's permissions and activation status (admin only)
|
||||||
|
parameters:
|
||||||
|
- description: Login of the user
|
||||||
|
in: path
|
||||||
|
name: login
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
- description: Permissions to update
|
||||||
|
in: body
|
||||||
|
name: request
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/repository.TokenUpdatePermissions'
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
"404":
|
||||||
|
description: Not Found
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
summary: Update user permissions
|
||||||
|
tags:
|
||||||
|
- auth
|
||||||
|
/auth/users/inactive:
|
||||||
|
get:
|
||||||
|
description: Returns list of all users waiting for activation
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/repository.Tokens'
|
||||||
|
type: array
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
summary: List inactive users
|
||||||
|
tags:
|
||||||
|
- auth
|
||||||
/auth/validate:
|
/auth/validate:
|
||||||
get:
|
get:
|
||||||
description: Check if the provided Bearer token is valid and return its permissions
|
description: Check if the provided Bearer token is valid and return its permissions
|
||||||
@@ -293,7 +803,7 @@ paths:
|
|||||||
"200":
|
"200":
|
||||||
description: OK
|
description: OK
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_HellreigN_backend_internal_repository.Tokens'
|
$ref: '#/definitions/repository.Tokens'
|
||||||
"401":
|
"401":
|
||||||
description: Unauthorized
|
description: Unauthorized
|
||||||
schema:
|
schema:
|
||||||
@@ -344,8 +854,10 @@ paths:
|
|||||||
description: OK
|
description: OK
|
||||||
schema:
|
schema:
|
||||||
items:
|
items:
|
||||||
$ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_HellreigN_backend_internal_storage.LogEntry'
|
$ref: '#/definitions/storage.LogEntry'
|
||||||
type: array
|
type: array
|
||||||
|
security:
|
||||||
|
- Bearer: []
|
||||||
summary: Search logs
|
summary: Search logs
|
||||||
tags:
|
tags:
|
||||||
- logs
|
- logs
|
||||||
@@ -359,7 +871,7 @@ paths:
|
|||||||
name: body
|
name: body
|
||||||
required: true
|
required: true
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/internal_handlers.InsertLogRequest'
|
$ref: '#/definitions/handlers.InsertLogRequest'
|
||||||
produces:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
responses:
|
responses:
|
||||||
@@ -369,6 +881,8 @@ paths:
|
|||||||
additionalProperties:
|
additionalProperties:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
|
security:
|
||||||
|
- Bearer: []
|
||||||
summary: Insert log entry
|
summary: Insert log entry
|
||||||
tags:
|
tags:
|
||||||
- logs
|
- logs
|
||||||
@@ -384,6 +898,8 @@ paths:
|
|||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
type: array
|
type: array
|
||||||
|
security:
|
||||||
|
- Bearer: []
|
||||||
summary: Get distinct agents
|
summary: Get distinct agents
|
||||||
tags:
|
tags:
|
||||||
- logs
|
- logs
|
||||||
@@ -398,7 +914,7 @@ paths:
|
|||||||
name: body
|
name: body
|
||||||
required: true
|
required: true
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/internal_handlers.InsertLogsRequest'
|
$ref: '#/definitions/handlers.InsertLogsRequest'
|
||||||
produces:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
responses:
|
responses:
|
||||||
@@ -408,6 +924,8 @@ paths:
|
|||||||
additionalProperties:
|
additionalProperties:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
|
security:
|
||||||
|
- Bearer: []
|
||||||
summary: Insert log entries (batch)
|
summary: Insert log entries (batch)
|
||||||
tags:
|
tags:
|
||||||
- logs
|
- logs
|
||||||
@@ -423,9 +941,52 @@ paths:
|
|||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
type: array
|
type: array
|
||||||
|
security:
|
||||||
|
- Bearer: []
|
||||||
summary: Get distinct log levels
|
summary: Get distinct log levels
|
||||||
tags:
|
tags:
|
||||||
- logs
|
- logs
|
||||||
|
/logs/mock:
|
||||||
|
get:
|
||||||
|
description: Returns 100 mock log entries for frontend development (no ClickHouse
|
||||||
|
required)
|
||||||
|
parameters:
|
||||||
|
- description: Filter by level
|
||||||
|
in: query
|
||||||
|
name: level
|
||||||
|
type: string
|
||||||
|
- description: Filter by service
|
||||||
|
in: query
|
||||||
|
name: service
|
||||||
|
type: string
|
||||||
|
- description: Filter by agent
|
||||||
|
in: query
|
||||||
|
name: agent
|
||||||
|
type: string
|
||||||
|
- default: 100
|
||||||
|
description: Limit results
|
||||||
|
in: query
|
||||||
|
name: limit
|
||||||
|
type: integer
|
||||||
|
- default: 0
|
||||||
|
description: Offset results
|
||||||
|
in: query
|
||||||
|
name: offset
|
||||||
|
type: integer
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/storage.LogEntry'
|
||||||
|
type: array
|
||||||
|
security:
|
||||||
|
- Bearer: []
|
||||||
|
summary: Get mock logs
|
||||||
|
tags:
|
||||||
|
- logs
|
||||||
/logs/services:
|
/logs/services:
|
||||||
get:
|
get:
|
||||||
description: Returns list of all unique service names in logs
|
description: Returns list of all unique service names in logs
|
||||||
@@ -438,6 +999,8 @@ paths:
|
|||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
type: array
|
type: array
|
||||||
|
security:
|
||||||
|
- Bearer: []
|
||||||
summary: Get distinct services
|
summary: Get distinct services
|
||||||
tags:
|
tags:
|
||||||
- logs
|
- logs
|
||||||
|
|||||||
+3
-1
@@ -9,6 +9,7 @@ require (
|
|||||||
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
|
||||||
|
golang.org/x/crypto v0.49.0
|
||||||
golang.org/x/sync v0.20.0
|
golang.org/x/sync v0.20.0
|
||||||
google.golang.org/grpc v1.80.0
|
google.golang.org/grpc v1.80.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
@@ -68,7 +69,6 @@ require (
|
|||||||
go.opentelemetry.io/otel/trace v1.41.0 // indirect
|
go.opentelemetry.io/otel/trace v1.41.0 // indirect
|
||||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
golang.org/x/arch v0.25.0 // indirect
|
golang.org/x/arch v0.25.0 // indirect
|
||||||
golang.org/x/crypto v0.49.0 // indirect
|
|
||||||
golang.org/x/mod v0.34.0 // indirect
|
golang.org/x/mod v0.34.0 // indirect
|
||||||
golang.org/x/net v0.52.0 // indirect
|
golang.org/x/net v0.52.0 // indirect
|
||||||
golang.org/x/sys v0.42.0 // indirect
|
golang.org/x/sys v0.42.0 // indirect
|
||||||
@@ -80,3 +80,5 @@ require (
|
|||||||
modernc.org/mathutil v1.7.1 // indirect
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
modernc.org/memory v1.11.0 // indirect
|
modernc.org/memory v1.11.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
|
replace gitea.d3m0k1d.ru/d3m0k1d/HellreigN/proto => ../proto
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
gitea.d3m0k1d.ru/d3m0k1d/HellreigN/proto v0.0.0-20260403210401-a6212c89fc0e h1:Z/2Mjc9NU0CWIduj8Wd9ClnZt4dqmQUUXl1VlyGQe6U=
|
|
||||||
gitea.d3m0k1d.ru/d3m0k1d/HellreigN/proto v0.0.0-20260403210401-a6212c89fc0e/go.mod h1:1DByetpOnW2+AjM8ZWbJ1Xfzprus8fBie2AMUP/YHHA=
|
|
||||||
github.com/ClickHouse/ch-go v0.71.0 h1:bUdZ/EZj/LcVHsMqaRUP2holqygrPWQKeMjc6nZoyRM=
|
github.com/ClickHouse/ch-go v0.71.0 h1:bUdZ/EZj/LcVHsMqaRUP2holqygrPWQKeMjc6nZoyRM=
|
||||||
github.com/ClickHouse/ch-go v0.71.0/go.mod h1:NwbNc+7jaqfY58dmdDUbG4Jl22vThgx1cYjBw0vtgXw=
|
github.com/ClickHouse/ch-go v0.71.0/go.mod h1:NwbNc+7jaqfY58dmdDUbG4Jl22vThgx1cYjBw0vtgXw=
|
||||||
github.com/ClickHouse/clickhouse-go/v2 v2.44.0 h1:9pxs5pRwIvhni5BDRPn/n5A8DeUod5TnBaeulFBX8EQ=
|
github.com/ClickHouse/clickhouse-go/v2 v2.44.0 h1:9pxs5pRwIvhni5BDRPn/n5A8DeUod5TnBaeulFBX8EQ=
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
package ansible
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Executor handles running Ansible playbooks
|
||||||
|
type Executor struct {
|
||||||
|
workDir string
|
||||||
|
grpcServerHost string
|
||||||
|
grpcServerPort string
|
||||||
|
backendURL string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecutorConfig holds configuration for the Executor
|
||||||
|
type ExecutorConfig struct {
|
||||||
|
WorkDir string
|
||||||
|
GRPCServerHost string
|
||||||
|
GRPCServerPort string
|
||||||
|
BackendURL string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewExecutor creates a new Ansible executor
|
||||||
|
func NewExecutor(cfg ExecutorConfig) *Executor {
|
||||||
|
return &Executor{
|
||||||
|
workDir: cfg.WorkDir,
|
||||||
|
grpcServerHost: cfg.GRPCServerHost,
|
||||||
|
grpcServerPort: cfg.GRPCServerPort,
|
||||||
|
backendURL: cfg.BackendURL,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeployResult holds the result of a deployment
|
||||||
|
type DeployResult struct {
|
||||||
|
Host string
|
||||||
|
Success bool
|
||||||
|
Stdout string
|
||||||
|
Stderr string
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
// WorkDir returns the work directory path
|
||||||
|
func (e *Executor) WorkDir() string {
|
||||||
|
return e.workDir
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deploy runs Ansible playbook for the given inventory
|
||||||
|
func (e *Executor) Deploy(ctx context.Context, inventoryPath string, deployType string) ([]DeployResult, error) {
|
||||||
|
playbookName := "binary_deploy.yml"
|
||||||
|
if deployType == "docker" {
|
||||||
|
playbookName = "docker_deploy.yml"
|
||||||
|
}
|
||||||
|
|
||||||
|
playbookPath := filepath.Join(e.workDir, playbookName)
|
||||||
|
|
||||||
|
cmd := exec.CommandContext(ctx, "ansible-playbook",
|
||||||
|
"-i", inventoryPath,
|
||||||
|
"-e", fmt.Sprintf("backend_url=%s", e.backendURL),
|
||||||
|
playbookPath,
|
||||||
|
)
|
||||||
|
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
cmd.Stdout = &stdout
|
||||||
|
cmd.Stderr = &stderr
|
||||||
|
|
||||||
|
runErr := cmd.Run()
|
||||||
|
|
||||||
|
// Parse results per host (simplified - returns single result for all)
|
||||||
|
return []DeployResult{
|
||||||
|
{
|
||||||
|
Host: "all",
|
||||||
|
Success: runErr == nil,
|
||||||
|
Stdout: stdout.String(),
|
||||||
|
Stderr: stderr.String(),
|
||||||
|
Err: runErr,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeployParallel runs Ansible playbook for multiple inventories in parallel
|
||||||
|
func (e *Executor) DeployParallel(ctx context.Context, inventoryPaths []string, deployType string) (map[string][]DeployResult, error) {
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
results := make(map[string][]DeployResult)
|
||||||
|
errCh := make(chan error, len(inventoryPaths))
|
||||||
|
|
||||||
|
for _, path := range inventoryPaths {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(p string) {
|
||||||
|
defer wg.Done()
|
||||||
|
res, err := e.Deploy(ctx, p, deployType)
|
||||||
|
if err != nil {
|
||||||
|
errCh <- err
|
||||||
|
}
|
||||||
|
results[p] = res
|
||||||
|
}(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
close(errCh)
|
||||||
|
|
||||||
|
// Collect errors
|
||||||
|
var errs []error
|
||||||
|
for err := range errCh {
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errs) > 0 {
|
||||||
|
return results, fmt.Errorf("some deployments failed: %v", errs)
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WritePlaybook writes a playbook to the work directory
|
||||||
|
func (e *Executor) WritePlaybook(name string, content string) error {
|
||||||
|
path := filepath.Join(e.workDir, name)
|
||||||
|
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.WriteFile(path, []byte(content), 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteAllPlaybooks writes all playbooks to the work directory
|
||||||
|
func (e *Executor) WriteAllPlaybooks() error {
|
||||||
|
if err := e.WritePlaybook("binary_deploy.yml", BinaryDeployPlaybook); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return e.WritePlaybook("docker_deploy.yml", DockerDeployPlaybook)
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package ansible
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"text/template"
|
||||||
|
)
|
||||||
|
|
||||||
|
// InventoryHost represents a single host in the inventory
|
||||||
|
type InventoryHost struct {
|
||||||
|
Name string
|
||||||
|
IP string
|
||||||
|
Port int
|
||||||
|
User string
|
||||||
|
AuthMethod string
|
||||||
|
SSHKey string
|
||||||
|
Password string
|
||||||
|
DeployType string
|
||||||
|
Token string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inventory represents an Ansible inventory file
|
||||||
|
type Inventory struct {
|
||||||
|
Hosts []InventoryHost
|
||||||
|
}
|
||||||
|
|
||||||
|
const inventoryTemplateText = `{{ range .Hosts }}
|
||||||
|
{{ .Name }} ansible_host={{ .IP }} ansible_port={{ .Port }} ansible_user={{ .User }} ansible_connection=ssh
|
||||||
|
{{ if eq .AuthMethod "key" }}ansible_ssh_private_key_file={{ .SSHKey }}{{ end }}
|
||||||
|
{{ if eq .AuthMethod "password" }}ansible_ssh_pass={{ .Password }}{{ end }}
|
||||||
|
deploy_type={{ .DeployType }}
|
||||||
|
agent_token={{ .Token }}
|
||||||
|
agent_label={{ .Name }}
|
||||||
|
|
||||||
|
{{ end }}`
|
||||||
|
|
||||||
|
// GenerateInventory generates an Ansible inventory file from the given hosts
|
||||||
|
func GenerateInventory(hosts []InventoryHost, outputPath string) error {
|
||||||
|
tmpl, err := template.New("inventory").Parse(inventoryTemplateText)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to parse inventory template: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure directory exists
|
||||||
|
dir := filepath.Dir(outputPath)
|
||||||
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create inventory directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.Create(outputPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create inventory file: %w", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
if err := tmpl.Execute(file, Inventory{Hosts: hosts}); err != nil {
|
||||||
|
return fmt.Errorf("failed to execute inventory template: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
package ansible
|
||||||
|
|
||||||
|
// BinaryDeployPlaybook returns the Ansible playbook for binary deployment
|
||||||
|
const BinaryDeployPlaybook = `---
|
||||||
|
- name: Deploy HellreigN Agent (Binary)
|
||||||
|
hosts: all
|
||||||
|
become: yes
|
||||||
|
vars:
|
||||||
|
agent_label: "{{ agent_label }}"
|
||||||
|
agent_token: "{{ agent_token }}"
|
||||||
|
backend_url: "{{ backend_url }}"
|
||||||
|
install_dir: /opt/hellreign
|
||||||
|
bin_name: hellreign-agent
|
||||||
|
service_name: hellreign-agent
|
||||||
|
cert_dir: "{{ install_dir }}/certs"
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- name: Create installation directory
|
||||||
|
file:
|
||||||
|
path: "{{ install_dir }}"
|
||||||
|
state: directory
|
||||||
|
mode: '0755'
|
||||||
|
|
||||||
|
- name: Create certificates directory
|
||||||
|
file:
|
||||||
|
path: "{{ cert_dir }}"
|
||||||
|
state: directory
|
||||||
|
mode: '0755'
|
||||||
|
|
||||||
|
- name: Download HellreigN Agent binary
|
||||||
|
get_url:
|
||||||
|
url: "https://gitea.d3m0k1d.ru/d3m0k1d/HellreigN/releases/latest/download/{{ bin_name }}"
|
||||||
|
dest: "{{ install_dir }}/{{ bin_name }}"
|
||||||
|
mode: '0755'
|
||||||
|
|
||||||
|
- name: Create agent configuration
|
||||||
|
copy:
|
||||||
|
content: |
|
||||||
|
backend_url: "{{ backend_url }}"
|
||||||
|
label: "{{ agent_label }}"
|
||||||
|
registration_token: "{{ agent_token }}"
|
||||||
|
cert_dir: "{{ cert_dir }}"
|
||||||
|
dest: "{{ install_dir }}/config.yml"
|
||||||
|
mode: '0644'
|
||||||
|
|
||||||
|
- name: Create systemd service file
|
||||||
|
copy:
|
||||||
|
content: |
|
||||||
|
[Unit]
|
||||||
|
Description=HellreigN Agent
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
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
|
||||||
|
const DockerDeployPlaybook = `---
|
||||||
|
- name: Deploy HellreigN Agent (Docker)
|
||||||
|
hosts: all
|
||||||
|
become: yes
|
||||||
|
vars:
|
||||||
|
agent_label: "{{ agent_label }}"
|
||||||
|
agent_token: "{{ agent_token }}"
|
||||||
|
backend_url: "{{ backend_url }}"
|
||||||
|
container_name: hellreign-agent-{{ agent_label }}
|
||||||
|
image: "gitea.d3m0k1d.ru/d3m0k1d/hellreign-agent:latest"
|
||||||
|
cert_dir: /etc/hellreign-agent/certs
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- name: Install Docker (if not present)
|
||||||
|
block:
|
||||||
|
- name: Check if Docker is installed
|
||||||
|
command: docker --version
|
||||||
|
register: docker_check
|
||||||
|
ignore_errors: yes
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Install Docker
|
||||||
|
shell: |
|
||||||
|
curl -fsSL https://get.docker.com | sh
|
||||||
|
when: docker_check.rc != 0
|
||||||
|
|
||||||
|
- name: Create certificates directory
|
||||||
|
file:
|
||||||
|
path: "{{ cert_dir }}"
|
||||||
|
state: directory
|
||||||
|
mode: '0755'
|
||||||
|
|
||||||
|
- name: Pull HellreigN Agent image
|
||||||
|
community.docker.docker_image:
|
||||||
|
name: "{{ image }}"
|
||||||
|
source: pull
|
||||||
|
|
||||||
|
- name: Create agent configuration
|
||||||
|
copy:
|
||||||
|
content: |
|
||||||
|
backend_url: "{{ backend_url }}"
|
||||||
|
label: "{{ agent_label }}"
|
||||||
|
registration_token: "{{ agent_token }}"
|
||||||
|
cert_dir: "{{ cert_dir }}"
|
||||||
|
dest: "{{ cert_dir }}/config.yml"
|
||||||
|
mode: '0644'
|
||||||
|
|
||||||
|
- name: Create and run HellreigN Agent container
|
||||||
|
community.docker.docker_container:
|
||||||
|
name: "{{ container_name }}"
|
||||||
|
image: "{{ image }}"
|
||||||
|
state: started
|
||||||
|
restart_policy: always
|
||||||
|
volumes:
|
||||||
|
- "{{ cert_dir }}:/etc/hellreign-agent/certs"
|
||||||
|
env:
|
||||||
|
CONFIG_FILE: /etc/hellreign-agent/certs/config.yml
|
||||||
|
`
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package ansible
|
||||||
|
|
||||||
|
const BaseInvTemplate = `
|
||||||
|
|
||||||
|
`
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
package collector
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/repository"
|
||||||
|
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/storage"
|
||||||
|
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/proto/proto"
|
||||||
|
"google.golang.org/grpc/metadata"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Collector struct {
|
||||||
|
proto.UnimplementedCollectorServer
|
||||||
|
logRepo *repository.LogRepository
|
||||||
|
agents map[string]*Agent
|
||||||
|
mu sync.RWMutex
|
||||||
|
batchSize int
|
||||||
|
flushInterval time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
type Agent struct {
|
||||||
|
ID string
|
||||||
|
Label string
|
||||||
|
Services []string
|
||||||
|
ConnectedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(logRepo *repository.LogRepository) *Collector {
|
||||||
|
return &Collector{
|
||||||
|
logRepo: logRepo,
|
||||||
|
agents: make(map[string]*Agent),
|
||||||
|
batchSize: 100,
|
||||||
|
flushInterval: 2 * time.Second,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Collector) Stream(stream proto.Collector_StreamServer) error {
|
||||||
|
md, ok := metadata.FromIncomingContext(stream.Context())
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("no metadata in context")
|
||||||
|
}
|
||||||
|
|
||||||
|
whoamiVals := md["whoami"]
|
||||||
|
if len(whoamiVals) == 0 {
|
||||||
|
return fmt.Errorf("whoami metadata missing")
|
||||||
|
}
|
||||||
|
agentName := whoamiVals[0]
|
||||||
|
|
||||||
|
serviceVals := md["service"]
|
||||||
|
if len(serviceVals) == 0 {
|
||||||
|
return fmt.Errorf("service metadata missing")
|
||||||
|
}
|
||||||
|
service := serviceVals[0]
|
||||||
|
|
||||||
|
servicesVals := md["services"]
|
||||||
|
var services []string
|
||||||
|
if len(servicesVals) > 0 {
|
||||||
|
services = servicesVals
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register agent
|
||||||
|
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)
|
||||||
|
|
||||||
|
// If no ClickHouse, just consume the stream without storing
|
||||||
|
if !c.logRepo.IsConnected() {
|
||||||
|
log.Printf("Warning: ClickHouse not connected yet, consuming logs without storing for agent %s", agentName)
|
||||||
|
for {
|
||||||
|
_, err := stream.Recv()
|
||||||
|
if err == io.EOF {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to receive: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Channels for communication with recv goroutine
|
||||||
|
recvCh := make(chan *proto.CollectorRequest, 1)
|
||||||
|
errCh := make(chan error, 1)
|
||||||
|
|
||||||
|
// Goroutine that blocks on Recv
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
req, err := stream.Recv()
|
||||||
|
if err != nil {
|
||||||
|
errCh <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
recvCh <- req
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Buffer for batch inserts
|
||||||
|
var batch []storage.LogEntry
|
||||||
|
ticker := time.NewTicker(c.flushInterval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
flush := func() error {
|
||||||
|
if len(batch) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := c.logRepo.InsertBatch(stream.Context(), batch); err != nil {
|
||||||
|
log.Printf("Failed to insert batch for agent %s, service %s: %v", agentName, service, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Printf("Flushed %d logs for agent %s, service %s", len(batch), agentName, service)
|
||||||
|
batch = batch[:0]
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-stream.Context().Done():
|
||||||
|
// Context cancelled, flush remaining
|
||||||
|
_ = flush()
|
||||||
|
return stream.Context().Err()
|
||||||
|
case <-ticker.C:
|
||||||
|
if err := flush(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case req := <-recvCh:
|
||||||
|
batch = append(batch, storage.LogEntry{
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
Level: "info",
|
||||||
|
Service: service,
|
||||||
|
Agent: agentName,
|
||||||
|
Message: req.Message,
|
||||||
|
})
|
||||||
|
|
||||||
|
if len(batch) >= c.batchSize {
|
||||||
|
if err := flush(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case err := <-errCh:
|
||||||
|
if err == io.EOF {
|
||||||
|
// Client closed stream
|
||||||
|
return flush()
|
||||||
|
}
|
||||||
|
return fmt.Errorf("failed to receive: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Collector) GetAgent(name string) (*Agent, bool) {
|
||||||
|
c.mu.RLock()
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
a, ok := c.agents[name]
|
||||||
|
return a, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Collector) Agents() []*Agent {
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -4,6 +4,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"log"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/models"
|
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/models"
|
||||||
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/proto/proto"
|
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/proto/proto"
|
||||||
@@ -15,17 +17,33 @@ import (
|
|||||||
type Commander struct {
|
type Commander struct {
|
||||||
proto.UnimplementedCommanderServer
|
proto.UnimplementedCommanderServer
|
||||||
agents map[string]Agent
|
agents map[string]Agent
|
||||||
|
mu sync.RWMutex
|
||||||
|
jobber Jobber
|
||||||
|
}
|
||||||
|
|
||||||
|
type Jobber interface {
|
||||||
|
InitJob(ctx context.Context, agentID string, job models.JobForInsert) (int64, error)
|
||||||
|
UpdateJobInDB(ctx context.Context, jid int64, msg models.JobForUpdate) (models.Job, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(jobber Jobber) *Commander {
|
||||||
|
return &Commander{
|
||||||
|
agents: make(map[string]Agent),
|
||||||
|
jobber: jobber,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
jobs map[int64]Job
|
jobs map[int64]Job
|
||||||
jobber interface {
|
jobber Jobber
|
||||||
InitJob(ctx context.Context) (int64, error)
|
|
||||||
UpdateJobInDB(ctx context.Context, jid int64, msg models.JobForUpdate) (models.Job, error)
|
|
||||||
}
|
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
|
aid string
|
||||||
|
|
||||||
|
Token string // agent id
|
||||||
|
Label string
|
||||||
|
Services []string
|
||||||
}
|
}
|
||||||
type JobOut struct {
|
type JobOut struct {
|
||||||
fc models.Job
|
fc models.Job
|
||||||
@@ -37,25 +55,49 @@ type Job struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (self *Commander) GetAgent(aid string) (agent Agent, ok bool) {
|
func (self *Commander) GetAgent(aid string) (agent Agent, ok bool) {
|
||||||
|
self.mu.RLock()
|
||||||
|
defer self.mu.RUnlock()
|
||||||
agent, ok = self.agents[aid]
|
agent, ok = self.agents[aid]
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (self *Commander) Agents() []Agent {
|
||||||
|
self.mu.RLock()
|
||||||
|
defer self.mu.RUnlock()
|
||||||
|
result := make([]Agent, 0, len(self.agents))
|
||||||
|
for _, a := range self.agents {
|
||||||
|
result = append(result, a)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Commander) removeAgent(aid string) {
|
||||||
|
self.mu.Lock()
|
||||||
|
defer self.mu.Unlock()
|
||||||
|
delete(self.agents, aid)
|
||||||
|
}
|
||||||
|
|
||||||
func (self *Agent) AddJob(job models.JobForInsert) (int64, error) {
|
func (self *Agent) AddJob(job models.JobForInsert) (int64, error) {
|
||||||
jid, err := self.jobber.InitJob(self.ctx)
|
log.Printf("[DEBUG] AddJob: agent=%s, command=%v", self.aid, job.Command)
|
||||||
|
jid, err := self.jobber.InitJob(self.ctx, self.aid, job)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Printf("[DEBUG] AddJob: InitJob failed: %v", err)
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
log.Printf("[DEBUG] AddJob: InitJob returned jid=%d, sending to self.in channel", jid)
|
||||||
self.in <- &proto.Command{
|
self.in <- &proto.Command{
|
||||||
Id: 0,
|
Id: jid,
|
||||||
Command: []string{},
|
Command: job.Command,
|
||||||
Stdin: new(string),
|
Stdin: job.Stdin,
|
||||||
}
|
}
|
||||||
|
log.Printf("[DEBUG] AddJob: sent to self.in channel successfully")
|
||||||
return jid, err
|
return jid, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *Agent) WaitJob(jid int64) (*models.Job, error) {
|
func (self *Agent) WaitJob(jid int64) (*models.Job, error) {
|
||||||
|
log.Printf("[DEBUG] WaitJob: agent=%s, jid=%d, waiting on self.jobs[%d].out", self.aid, jid, jid)
|
||||||
result := <-self.jobs[jid].out
|
result := <-self.jobs[jid].out
|
||||||
|
log.Printf("[DEBUG] WaitJob: agent=%s, jid=%d, received result", self.aid, jid)
|
||||||
return &result.fc, result.err
|
return &result.fc, result.err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,8 +111,19 @@ func (self *Commander) Stream(bidi grpc.BidiStreamingServer[proto.FinishedComman
|
|||||||
return fmt.Errorf("agentid metadata missing")
|
return fmt.Errorf("agentid metadata missing")
|
||||||
}
|
}
|
||||||
aid := aidVals[0]
|
aid := aidVals[0]
|
||||||
agent := newAgent(bidi)
|
|
||||||
|
var label string
|
||||||
|
labelVals := md["label"]
|
||||||
|
if len(labelVals) > 0 {
|
||||||
|
label = labelVals[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
agent := newAgent(bidi, self.jobber, aid, label)
|
||||||
|
self.mu.Lock()
|
||||||
self.agents[aid] = agent
|
self.agents[aid] = agent
|
||||||
|
self.mu.Unlock()
|
||||||
|
|
||||||
|
defer self.removeAgent(aid)
|
||||||
return agent.run()
|
return agent.run()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,40 +141,55 @@ func (self *Agent) recv() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
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{
|
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,
|
||||||
})
|
})
|
||||||
}()
|
}()
|
||||||
// TODO: that would blow up at some point
|
if err == io.EOF {
|
||||||
|
log.Printf("[DEBUG] recv: agent=%s, EOF received", self.aid)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[DEBUG] recv: agent=%s, error: %v", self.aid, err)
|
||||||
|
}
|
||||||
out := self.jobs[job.ID].out
|
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 (self *Agent) send() error {
|
func (self *Agent) send() error {
|
||||||
for job := range self.in {
|
for job := range self.in {
|
||||||
|
log.Printf("[DEBUG] send: agent=%s, job id=%d, command=%v", self.aid, job.Id, job.Command)
|
||||||
self.jobs[job.Id] = newJob()
|
self.jobs[job.Id] = newJob()
|
||||||
if err := self.bidi.Send(job); err != nil {
|
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[]
|
// self.jobs[]
|
||||||
}
|
}
|
||||||
|
|
||||||
func newAgent(bidi grpc.BidiStreamingServer[proto.FinishedCommand, proto.Command]) Agent {
|
func newAgent(bidi grpc.BidiStreamingServer[proto.FinishedCommand, proto.Command], jobber Jobber, aid string, label string) Agent {
|
||||||
return Agent{
|
return Agent{
|
||||||
bidi,
|
bidi: bidi,
|
||||||
make(chan *proto.Command),
|
in: make(chan *proto.Command),
|
||||||
make(map[int64]Job),
|
jobs: make(map[int64]Job),
|
||||||
nil,
|
jobber: jobber,
|
||||||
bidi.Context(),
|
ctx: bidi.Context(),
|
||||||
|
aid: aid,
|
||||||
|
Label: label,
|
||||||
|
Token: aid,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/ansible"
|
||||||
|
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/repository"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AgentDeployGroup struct {
|
||||||
|
*Handlers
|
||||||
|
executor *ansible.Executor
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAgentDeployGroup(h *Handlers) *AgentDeployGroup {
|
||||||
|
workDir := os.Getenv("ANSIBLE_WORK_DIR")
|
||||||
|
if workDir == "" {
|
||||||
|
workDir = "/tmp/hellreign/ansible"
|
||||||
|
}
|
||||||
|
|
||||||
|
grpcPort := os.Getenv("GRPC_PORT")
|
||||||
|
if grpcPort == "" {
|
||||||
|
grpcPort = "9001"
|
||||||
|
}
|
||||||
|
|
||||||
|
backendURL := os.Getenv("BACKEND_URL")
|
||||||
|
if backendURL == "" {
|
||||||
|
backendURL = "http://localhost:8080"
|
||||||
|
}
|
||||||
|
|
||||||
|
exec := ansible.NewExecutor(ansible.ExecutorConfig{
|
||||||
|
WorkDir: workDir,
|
||||||
|
GRPCServerHost: "0.0.0.0", // TODO: make configurable
|
||||||
|
GRPCServerPort: grpcPort,
|
||||||
|
BackendURL: backendURL,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Write playbooks on init
|
||||||
|
if err := exec.WriteAllPlaybooks(); err != nil {
|
||||||
|
// Log but don't fail - playbooks can be written later
|
||||||
|
_ = err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &AgentDeployGroup{
|
||||||
|
Handlers: h,
|
||||||
|
executor: exec,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeployAgents deploys agents to multiple servers
|
||||||
|
// @Summary Deploy agents to multiple servers via Ansible
|
||||||
|
// @Description Deploy HellreigN agents to multiple servers using Ansible playbooks. Supports Docker and Binary deployment types.
|
||||||
|
// @Tags agents
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param request body repository.DeployAgentsRequest true "Deployment configuration for servers"
|
||||||
|
// @Success 200 {object} repository.DeployResponse "Deployment results with tokens for each server"
|
||||||
|
// @Failure 400 {object} map[string]string "Invalid request"
|
||||||
|
// @Failure 500 {object} map[string]string "Internal server error"
|
||||||
|
// @Security Bearer
|
||||||
|
// @Router /agents/deploy [post]
|
||||||
|
func (adg *AgentDeployGroup) DeployAgents(c *gin.Context) {
|
||||||
|
var req repository.DeployAgentsRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create work directory
|
||||||
|
workDir := adg.executor.WorkDir()
|
||||||
|
if err := os.MkdirAll(workDir, 0755); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create work directory"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate registration tokens for each server
|
||||||
|
results := make([]repository.DeployResult, 0, len(req.Servers))
|
||||||
|
timestamp := time.Now().UnixMilli()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(c.Request.Context(), 10*time.Minute)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
for i, server := range req.Servers {
|
||||||
|
// Create registration token
|
||||||
|
token, err := adg.Repo.CreateRegistrationToken(server.AgentLabel)
|
||||||
|
if err != nil {
|
||||||
|
results = append(results, repository.DeployResult{
|
||||||
|
IP: server.IP,
|
||||||
|
AgentLabel: server.AgentLabel,
|
||||||
|
Success: false,
|
||||||
|
Error: fmt.Sprintf("failed to create token: %v", err),
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set default port
|
||||||
|
port := server.Port
|
||||||
|
if port == 0 {
|
||||||
|
port = 22
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate inventory for this single server
|
||||||
|
inventoryHosts := []ansible.InventoryHost{
|
||||||
|
{
|
||||||
|
Name: server.AgentLabel,
|
||||||
|
IP: server.IP,
|
||||||
|
Port: port,
|
||||||
|
User: server.User,
|
||||||
|
AuthMethod: string(server.AuthMethod),
|
||||||
|
SSHKey: server.SSHKey,
|
||||||
|
Password: server.Password,
|
||||||
|
DeployType: string(server.DeployType),
|
||||||
|
Token: token,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
inventoryPath := filepath.Join(workDir, fmt.Sprintf("inventory_%d_%d", timestamp, i))
|
||||||
|
if err := ansible.GenerateInventory(inventoryHosts, inventoryPath); err != nil {
|
||||||
|
results = append(results, repository.DeployResult{
|
||||||
|
IP: server.IP,
|
||||||
|
AgentLabel: server.AgentLabel,
|
||||||
|
Token: token,
|
||||||
|
Success: false,
|
||||||
|
Error: fmt.Sprintf("failed to generate inventory: %v", err),
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run Ansible playbook for this server
|
||||||
|
deployResults, err := adg.executor.Deploy(ctx, inventoryPath, string(server.DeployType))
|
||||||
|
|
||||||
|
// Clean up inventory file
|
||||||
|
os.Remove(inventoryPath)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
results = append(results, repository.DeployResult{
|
||||||
|
IP: server.IP,
|
||||||
|
AgentLabel: server.AgentLabel,
|
||||||
|
Token: token,
|
||||||
|
Success: false,
|
||||||
|
Error: fmt.Sprintf("deployment failed: %v", err),
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
success := true
|
||||||
|
errMsg := ""
|
||||||
|
if len(deployResults) > 0 && !deployResults[0].Success {
|
||||||
|
success = false
|
||||||
|
errMsg = deployResults[0].Stderr
|
||||||
|
}
|
||||||
|
|
||||||
|
results = append(results, repository.DeployResult{
|
||||||
|
IP: server.IP,
|
||||||
|
AgentLabel: server.AgentLabel,
|
||||||
|
Token: token,
|
||||||
|
Success: success,
|
||||||
|
Error: errMsg,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, repository.DeployResponse{
|
||||||
|
Message: "Deployment completed",
|
||||||
|
Results: results,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/repository"
|
||||||
|
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/utils"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AgentRegistrationGroup struct {
|
||||||
|
*Handlers
|
||||||
|
certBundle *utils.CertBundle
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAgentRegistrationGroup(h *Handlers) *AgentRegistrationGroup {
|
||||||
|
certDir := getCertDir()
|
||||||
|
bundle, err := utils.LoadCertBundle(certDir)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[agent-reg] WARNING: cert bundle load failed: %v", err)
|
||||||
|
}
|
||||||
|
return &AgentRegistrationGroup{
|
||||||
|
Handlers: h,
|
||||||
|
certBundle: bundle,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateRegistrationToken — админ создаёт токен для агента
|
||||||
|
// @Summary Create registration token
|
||||||
|
// @Tags agents
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param request body repository.RegistrationRequest true "Label"
|
||||||
|
// @Success 200 {object} map[string]string
|
||||||
|
// @Security Bearer
|
||||||
|
// @Router /agents/register-token [post]
|
||||||
|
func (arg *AgentRegistrationGroup) CreateRegistrationToken(c *gin.Context) {
|
||||||
|
var req repository.RegistrationRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := arg.Repo.CreateRegistrationToken(req.Label)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create token"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"token": token})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register — агент шлёт CSR + token, получает сертификаты
|
||||||
|
// @Summary Register agent
|
||||||
|
// @Tags agents
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param request body RegisterRequest true "CSR + token"
|
||||||
|
// @Success 200 {object} RegisterResponse
|
||||||
|
// @Router /agents/register [post]
|
||||||
|
func (arg *AgentRegistrationGroup) Register(c *gin.Context) {
|
||||||
|
var req RegisterRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if arg.certBundle == nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "certificate bundle not available"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
regToken, err := arg.Repo.GetRegistrationToken(req.Token)
|
||||||
|
if err != nil {
|
||||||
|
if err == repository.ErrNotFound {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid registration token"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to verify token"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if regToken.Used {
|
||||||
|
c.JSON(http.StatusGone, gin.H{"error": "registration token already used"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
clientCertPEM, err := arg.certBundle.SignCSR([]byte(req.CSR), regToken.Label)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "failed to sign CSR: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := arg.Repo.MarkRegistrationTokenUsed(req.Token); err != nil {
|
||||||
|
log.Printf("[agent-reg] WARNING: failed to mark token used: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, RegisterResponse{
|
||||||
|
CACert: string(arg.certBundle.GetCACertPEM()),
|
||||||
|
ClientCert: string(clientCertPEM),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type RegisterRequest struct {
|
||||||
|
CSR string `json:"csr" binding:"required"`
|
||||||
|
Token string `json:"token" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RegisterResponse struct {
|
||||||
|
CACert string `json:"ca_cert"`
|
||||||
|
ClientCert string `json:"client_cert"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCertDir() string {
|
||||||
|
if d := os.Getenv("SSL_CERT_DIR"); d != "" {
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
return "/var/lib/hellreign/ssl"
|
||||||
|
}
|
||||||
@@ -1,26 +1,45 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/grpcsrv/collector"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AgentsGroup struct {
|
type AgentsGroup struct {
|
||||||
*Handlers
|
*Handlers
|
||||||
|
collector *collector.Collector
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAgentsGroup(h *Handlers, coll *collector.Collector) AgentsGroup {
|
||||||
|
return AgentsGroup{Handlers: h, collector: coll}
|
||||||
}
|
}
|
||||||
|
|
||||||
type AgentInfo struct {
|
type AgentInfo struct {
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
Label string `json:"label"`
|
Label string `json:"label"`
|
||||||
Services []string `json:"services"`
|
Services []string `json:"services"`
|
||||||
|
ConnectedAt string `json:"connected_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Summary Get connected agents
|
// @Summary Get connected agents
|
||||||
// @Description Returns a list of all agents currently connected via gRPC streaming
|
// @Description Returns a list of all agents currently connected via Collector (log streaming)
|
||||||
// @Tags agents
|
// @Tags agents
|
||||||
|
// @Security Bearer
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {array} AgentInfo
|
// @Success 200 {array} AgentInfo
|
||||||
// @Router /agents [get]
|
// @Router /agents [get]
|
||||||
func (ag *AgentsGroup) List(c *gin.Context) {
|
func (ag *AgentsGroup) List(c *gin.Context) {
|
||||||
c.JSON(http.StatusOK, gin.H{"message": "Agents list"})
|
agents := make([]AgentInfo, 0)
|
||||||
|
|
||||||
|
for _, agent := range ag.collector.Agents() {
|
||||||
|
agents = append(agents, AgentInfo{
|
||||||
|
Token: agent.ID,
|
||||||
|
Label: agent.Label,
|
||||||
|
Services: agent.Services,
|
||||||
|
ConnectedAt: agent.ConnectedAt.Format("2006-01-02 15:04:05"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, agents)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ type AuthGroup struct {
|
|||||||
// @Success 200 {object} repository.LoginResponse
|
// @Success 200 {object} repository.LoginResponse
|
||||||
// @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 403 {object} map[string]string
|
||||||
// @Router /auth/login [post]
|
// @Router /auth/login [post]
|
||||||
func (ag *AuthGroup) Login(c *gin.Context) {
|
func (ag *AuthGroup) Login(c *gin.Context) {
|
||||||
var req repository.LoginRequest
|
var req repository.LoginRequest
|
||||||
@@ -37,6 +38,10 @@ func (ag *AuthGroup) Login(c *gin.Context) {
|
|||||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid credentials"})
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid credentials"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if errors.Is(err, repository.ErrAccountInactive) {
|
||||||
|
c.JSON(http.StatusForbidden, gin.H{"error": "account is not activated by admin"})
|
||||||
|
return
|
||||||
|
}
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to authenticate"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to authenticate"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -168,6 +173,223 @@ func (ag *AuthGroup) DeleteMyToken(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, gin.H{"message": "account deleted"})
|
c.JSON(http.StatusOK, gin.H{"message": "account deleted"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ActivateUser activates a user by login.
|
||||||
|
// @Summary Activate user
|
||||||
|
// @Description Activates a user account by login (admin only)
|
||||||
|
// @Tags auth
|
||||||
|
// @Param login path string true "Login of the user to activate"
|
||||||
|
// @Success 200 {object} map[string]string
|
||||||
|
// @Failure 400 {object} map[string]string
|
||||||
|
// @Failure 404 {object} map[string]string
|
||||||
|
// @Failure 500 {object} map[string]string
|
||||||
|
// @Router /auth/users/:login/activate [post]
|
||||||
|
func (ag *AuthGroup) ActivateUser(c *gin.Context) {
|
||||||
|
login := c.Param("login")
|
||||||
|
if login == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "login required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ag.Repo.ActivateUserByLogin(login); err != nil {
|
||||||
|
if errors.Is(err, repository.ErrNotFound) {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to activate user"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "user activated"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeactivateUser deactivates a user by login.
|
||||||
|
// @Summary Deactivate user
|
||||||
|
// @Description Deactivates a user account by login (admin only)
|
||||||
|
// @Tags auth
|
||||||
|
// @Param login path string true "Login of the user to deactivate"
|
||||||
|
// @Success 200 {object} map[string]string
|
||||||
|
// @Failure 400 {object} map[string]string
|
||||||
|
// @Failure 404 {object} map[string]string
|
||||||
|
// @Failure 500 {object} map[string]string
|
||||||
|
// @Router /auth/users/:login/deactivate [post]
|
||||||
|
func (ag *AuthGroup) DeactivateUser(c *gin.Context) {
|
||||||
|
login := c.Param("login")
|
||||||
|
if login == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "login required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ag.Repo.DeactivateUserByLogin(login); err != nil {
|
||||||
|
if errors.Is(err, repository.ErrNotFound) {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to deactivate user"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "user deactivated"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListInactiveUsers returns all users that are not activated.
|
||||||
|
// @Summary List inactive users
|
||||||
|
// @Description Returns list of all users waiting for activation
|
||||||
|
// @Tags auth
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {array} repository.Tokens
|
||||||
|
// @Failure 500 {object} map[string]string
|
||||||
|
// @Router /auth/users/inactive [get]
|
||||||
|
func (ag *AuthGroup) ListInactiveUsers(c *gin.Context) {
|
||||||
|
tokens, err := ag.Repo.ListInactiveTokens()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list inactive users"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, tokens)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUser returns a user by login.
|
||||||
|
// @Summary Get user by login
|
||||||
|
// @Description Returns a user by their login (admin only)
|
||||||
|
// @Tags auth
|
||||||
|
// @Produce json
|
||||||
|
// @Param login path string true "Login of the user"
|
||||||
|
// @Success 200 {object} repository.Tokens
|
||||||
|
// @Failure 400 {object} map[string]string
|
||||||
|
// @Failure 404 {object} map[string]string
|
||||||
|
// @Failure 500 {object} map[string]string
|
||||||
|
// @Router /auth/users/:login [get]
|
||||||
|
func (ag *AuthGroup) GetUser(c *gin.Context) {
|
||||||
|
login := c.Param("login")
|
||||||
|
if login == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "login required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := ag.Repo.GetTokenByLogin(login)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, repository.ErrNotFound) {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get user"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, user)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateUser updates user's name and last name.
|
||||||
|
// @Summary Update user
|
||||||
|
// @Description Updates a user's name and last name (admin only)
|
||||||
|
// @Tags auth
|
||||||
|
// @Accept json
|
||||||
|
// @Param login path string true "Login of the user"
|
||||||
|
// @Param request body repository.TokenUpdate true "User data to update"
|
||||||
|
// @Success 200 {object} map[string]string
|
||||||
|
// @Failure 400 {object} map[string]string
|
||||||
|
// @Failure 404 {object} map[string]string
|
||||||
|
// @Failure 500 {object} map[string]string
|
||||||
|
// @Router /auth/users/:login [put]
|
||||||
|
func (ag *AuthGroup) UpdateUser(c *gin.Context) {
|
||||||
|
login := c.Param("login")
|
||||||
|
if login == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "login required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var update repository.TokenUpdate
|
||||||
|
if err := c.ShouldBindJSON(&update); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ag.Repo.UpdateToken(login, update); err != nil {
|
||||||
|
if errors.Is(err, repository.ErrNotFound) {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update user"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "user updated"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateUserPermissions updates user's permissions and activation status.
|
||||||
|
// @Summary Update user permissions
|
||||||
|
// @Description Updates a user's permissions and activation status (admin only)
|
||||||
|
// @Tags auth
|
||||||
|
// @Accept json
|
||||||
|
// @Param login path string true "Login of the user"
|
||||||
|
// @Param request body repository.TokenUpdatePermissions true "Permissions to update"
|
||||||
|
// @Success 200 {object} map[string]string
|
||||||
|
// @Failure 400 {object} map[string]string
|
||||||
|
// @Failure 404 {object} map[string]string
|
||||||
|
// @Failure 500 {object} map[string]string
|
||||||
|
// @Router /auth/users/:login/permissions [put]
|
||||||
|
func (ag *AuthGroup) UpdateUserPermissions(c *gin.Context) {
|
||||||
|
login := c.Param("login")
|
||||||
|
if login == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "login required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var update repository.TokenUpdatePermissions
|
||||||
|
if err := c.ShouldBindJSON(&update); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ag.Repo.UpdatePermissions(login, update); err != nil {
|
||||||
|
if errors.Is(err, repository.ErrNotFound) {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update permissions"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "permissions updated"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetUserPassword resets a user's password.
|
||||||
|
// @Summary Reset user password
|
||||||
|
// @Description Resets a user's password to a new value (admin only)
|
||||||
|
// @Tags auth
|
||||||
|
// @Accept json
|
||||||
|
// @Param login path string true "Login of the user"
|
||||||
|
// @Param request body repository.TokenPasswordReset true "New password"
|
||||||
|
// @Success 200 {object} map[string]string
|
||||||
|
// @Failure 400 {object} map[string]string
|
||||||
|
// @Failure 404 {object} map[string]string
|
||||||
|
// @Failure 500 {object} map[string]string
|
||||||
|
// @Router /auth/users/:login/password [put]
|
||||||
|
func (ag *AuthGroup) ResetUserPassword(c *gin.Context) {
|
||||||
|
login := c.Param("login")
|
||||||
|
if login == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "login required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req repository.TokenPasswordReset
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ag.Repo.UpdatePassword(login, req.NewPassword); err != nil {
|
||||||
|
if errors.Is(err, repository.ErrNotFound) {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to reset password"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "password reset"})
|
||||||
|
}
|
||||||
|
|
||||||
// getTokenFromHeader extracts the Bearer token from the Authorization header.
|
// getTokenFromHeader extracts the Bearer token from the Authorization header.
|
||||||
func getTokenFromHeader(c *gin.Context) string {
|
func getTokenFromHeader(c *gin.Context) string {
|
||||||
auth := c.GetHeader("Authorization")
|
auth := c.GetHeader("Authorization")
|
||||||
|
|||||||
@@ -2,48 +2,31 @@ package handlers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
"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/service"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
type JobsHandlers struct {
|
type JobsHandlers struct {
|
||||||
cmder *commander.Commander
|
cmder *commander.Commander
|
||||||
|
svc *service.ScriptService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewJobsHandlers(cmder *commander.Commander) JobsHandlers {
|
func NewJobsHandlers(cmder *commander.Commander, svc *service.ScriptService) JobsHandlers {
|
||||||
return JobsHandlers{cmder}
|
return JobsHandlers{cmder: cmder, svc: svc}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *JobsHandlers) AddJob(c *gin.Context) {
|
type AddJobIn struct {
|
||||||
err := func() error {
|
Command string `json:"command" binding:"required"`
|
||||||
type In struct {
|
InterpreterID int64 `json:"interpreter_id"`
|
||||||
Command []string `json:"command"`
|
|
||||||
Stdin *string `json:"stdin"`
|
Stdin *string `json:"stdin"`
|
||||||
AID string `json:"agent_id"`
|
AgentID string `json:"agent_id" binding:"required"`
|
||||||
}
|
}
|
||||||
var in In
|
type AddJobOut struct {
|
||||||
if err := c.Bind(&in); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
agent, ok := self.cmder.GetAgent(in.AID)
|
|
||||||
if !ok {
|
|
||||||
c.Status(404)
|
|
||||||
return fmt.Errorf("Agent not found")
|
|
||||||
}
|
|
||||||
jid, err := agent.AddJob(models.JobForInsert{
|
|
||||||
Command: in.Command,
|
|
||||||
Stdin: in.Stdin,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
job, err := agent.WaitJob(jid)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
type Out 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,7 +34,62 @@ func (self *JobsHandlers) AddJob(c *gin.Context) {
|
|||||||
Stderr string `json:"stderr"`
|
Stderr string `json:"stderr"`
|
||||||
Status int32 `json:"status"`
|
Status int32 `json:"status"`
|
||||||
}
|
}
|
||||||
c.JSON(201, Out{
|
|
||||||
|
// AddJob creates and executes a job on a target agent.
|
||||||
|
// @Summary Create and run a job on an agent
|
||||||
|
// @Description Sends a command to the specified agent, waits for execution, and returns the result
|
||||||
|
// @Tags jobs
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param body body AddJobIn true "Job request"
|
||||||
|
// @Success 201 {object} AddJobOut
|
||||||
|
// @Router /jobs [post]
|
||||||
|
func (self *JobsHandlers) AddJob(c *gin.Context) {
|
||||||
|
log.Printf("[DEBUG] AddJob handler: request received")
|
||||||
|
err := func() error {
|
||||||
|
var in AddJobIn
|
||||||
|
if err := c.Bind(&in); err != nil {
|
||||||
|
log.Printf("[DEBUG] AddJob handler: bind failed: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Printf("[DEBUG] AddJob handler: agent_id=%s, command=%s, interpreter_id=%d", in.AgentID, in.Command, in.InterpreterID)
|
||||||
|
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")
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[DEBUG] AddJob handler: calling agent.AddJob with command=%v", command)
|
||||||
|
jid, err := agent.AddJob(models.JobForInsert{
|
||||||
|
Command: command,
|
||||||
|
Stdin: in.Stdin,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[DEBUG] AddJob handler: agent.AddJob failed: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Printf("[DEBUG] AddJob handler: agent.AddJob returned jid=%d, calling WaitJob", jid)
|
||||||
|
job, err := agent.WaitJob(jid)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[DEBUG] AddJob handler: agent.WaitJob failed: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Printf("[DEBUG] AddJob handler: agent.WaitJob returned job id=%d, status=%d", job.ID, job.Status)
|
||||||
|
c.JSON(http.StatusCreated, AddJobOut{
|
||||||
ID: job.ID,
|
ID: job.ID,
|
||||||
Command: job.Command,
|
Command: job.Command,
|
||||||
Stdin: job.Stdin,
|
Stdin: job.Stdin,
|
||||||
@@ -59,6 +97,7 @@ func (self *JobsHandlers) AddJob(c *gin.Context) {
|
|||||||
Stderr: job.Stderr,
|
Stderr: job.Stderr,
|
||||||
Status: job.Status,
|
Status: job.Status,
|
||||||
})
|
})
|
||||||
|
log.Printf("[DEBUG] AddJob handler: response sent")
|
||||||
return nil
|
return nil
|
||||||
}()
|
}()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ type InsertLogRequest struct {
|
|||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param body body InsertLogRequest true "Log entry"
|
// @Param body body InsertLogRequest true "Log entry"
|
||||||
// @Success 201 {object} map[string]string
|
// @Success 201 {object} map[string]string
|
||||||
|
// @Security Bearer
|
||||||
// @Router /logs [post]
|
// @Router /logs [post]
|
||||||
func (lh *LogHandlers) Insert(c *gin.Context) {
|
func (lh *LogHandlers) Insert(c *gin.Context) {
|
||||||
var req InsertLogRequest
|
var req InsertLogRequest
|
||||||
@@ -72,6 +73,7 @@ type InsertLogsRequest struct {
|
|||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param body body InsertLogsRequest true "Log entries"
|
// @Param body body InsertLogsRequest true "Log entries"
|
||||||
// @Success 201 {object} map[string]string
|
// @Success 201 {object} map[string]string
|
||||||
|
// @Security Bearer
|
||||||
// @Router /logs/batch [post]
|
// @Router /logs/batch [post]
|
||||||
func (lh *LogHandlers) InsertBatch(c *gin.Context) {
|
func (lh *LogHandlers) InsertBatch(c *gin.Context) {
|
||||||
var req InsertLogsRequest
|
var req InsertLogsRequest
|
||||||
@@ -124,6 +126,7 @@ type SearchLogsRequest struct {
|
|||||||
// @Param limit query int false "Limit results" default(100)
|
// @Param limit query int false "Limit results" default(100)
|
||||||
// @Param offset query int false "Offset results" default(0)
|
// @Param offset query int false "Offset results" default(0)
|
||||||
// @Success 200 {array} storage.LogEntry
|
// @Success 200 {array} storage.LogEntry
|
||||||
|
// @Security Bearer
|
||||||
// @Router /logs [get]
|
// @Router /logs [get]
|
||||||
func (lh *LogHandlers) Search(c *gin.Context) {
|
func (lh *LogHandlers) Search(c *gin.Context) {
|
||||||
var req SearchLogsRequest
|
var req SearchLogsRequest
|
||||||
@@ -170,6 +173,7 @@ func (lh *LogHandlers) Search(c *gin.Context) {
|
|||||||
// @Tags logs
|
// @Tags logs
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {array} string
|
// @Success 200 {array} string
|
||||||
|
// @Security Bearer
|
||||||
// @Router /logs/services [get]
|
// @Router /logs/services [get]
|
||||||
func (lh *LogHandlers) GetServices(c *gin.Context) {
|
func (lh *LogHandlers) GetServices(c *gin.Context) {
|
||||||
services, err := lh.LogRepo.GetDistinctServices(c.Request.Context())
|
services, err := lh.LogRepo.GetDistinctServices(c.Request.Context())
|
||||||
@@ -190,6 +194,7 @@ func (lh *LogHandlers) GetServices(c *gin.Context) {
|
|||||||
// @Tags logs
|
// @Tags logs
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {array} string
|
// @Success 200 {array} string
|
||||||
|
// @Security Bearer
|
||||||
// @Router /logs/agents [get]
|
// @Router /logs/agents [get]
|
||||||
func (lh *LogHandlers) GetAgents(c *gin.Context) {
|
func (lh *LogHandlers) GetAgents(c *gin.Context) {
|
||||||
agents, err := lh.LogRepo.GetDistinctAgents(c.Request.Context())
|
agents, err := lh.LogRepo.GetDistinctAgents(c.Request.Context())
|
||||||
@@ -210,6 +215,7 @@ func (lh *LogHandlers) GetAgents(c *gin.Context) {
|
|||||||
// @Tags logs
|
// @Tags logs
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {array} string
|
// @Success 200 {array} string
|
||||||
|
// @Security Bearer
|
||||||
// @Router /logs/levels [get]
|
// @Router /logs/levels [get]
|
||||||
func (lh *LogHandlers) GetLevels(c *gin.Context) {
|
func (lh *LogHandlers) GetLevels(c *gin.Context) {
|
||||||
levels, err := lh.LogRepo.GetDistinctLevels(c.Request.Context())
|
levels, err := lh.LogRepo.GetDistinctLevels(c.Request.Context())
|
||||||
|
|||||||
@@ -0,0 +1,203 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/storage"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetMockLogs returns 100 mock log entries for frontend development
|
||||||
|
// @Summary Get mock logs
|
||||||
|
// @Description Returns 100 mock log entries for frontend development (no ClickHouse required)
|
||||||
|
// @Tags logs
|
||||||
|
// @Produce json
|
||||||
|
// @Param level query string false "Filter by level"
|
||||||
|
// @Param service query string false "Filter by service"
|
||||||
|
// @Param agent query string false "Filter by agent"
|
||||||
|
// @Param limit query int false "Limit results" default(100)
|
||||||
|
// @Param offset query int false "Offset results" default(0)
|
||||||
|
// @Success 200 {array} storage.LogEntry
|
||||||
|
// @Security Bearer
|
||||||
|
// @Router /logs/mock [get]
|
||||||
|
func (lh *LogHandlers) GetMockLogs(c *gin.Context) {
|
||||||
|
levelFilter := c.Query("level")
|
||||||
|
serviceFilter := c.Query("service")
|
||||||
|
agentFilter := c.Query("agent")
|
||||||
|
|
||||||
|
limit := 100
|
||||||
|
offset := 0
|
||||||
|
|
||||||
|
if l := c.Query("limit"); l != "" {
|
||||||
|
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 {
|
||||||
|
limit = parsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if o := c.Query("offset"); o != "" {
|
||||||
|
if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 {
|
||||||
|
offset = parsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logs := generateMockLogs(100)
|
||||||
|
|
||||||
|
// Apply filters
|
||||||
|
var filtered []storage.LogEntry
|
||||||
|
for _, log := range logs {
|
||||||
|
if levelFilter != "" && log.Level != levelFilter {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if serviceFilter != "" && log.Service != serviceFilter {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if agentFilter != "" && log.Agent != agentFilter {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
filtered = append(filtered, log)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply pagination
|
||||||
|
end := offset + limit
|
||||||
|
if end > len(filtered) {
|
||||||
|
end = len(filtered)
|
||||||
|
}
|
||||||
|
if offset > len(filtered) {
|
||||||
|
filtered = []storage.LogEntry{}
|
||||||
|
} else {
|
||||||
|
filtered = filtered[offset:end]
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, filtered)
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateMockLogs(count int) []storage.LogEntry {
|
||||||
|
services := []string{
|
||||||
|
"auth-service",
|
||||||
|
"user-service",
|
||||||
|
"agent-service",
|
||||||
|
"gateway",
|
||||||
|
"scheduler",
|
||||||
|
"notification-service",
|
||||||
|
"metrics-collector",
|
||||||
|
"deployment-service",
|
||||||
|
}
|
||||||
|
|
||||||
|
agents := []string{
|
||||||
|
"agent-prod-01",
|
||||||
|
"agent-prod-02",
|
||||||
|
"agent-staging-01",
|
||||||
|
"agent-dev-01",
|
||||||
|
"agent-dev-02",
|
||||||
|
"agent-monitoring-01",
|
||||||
|
"agent-backup-01",
|
||||||
|
"agent-ci-runner-01",
|
||||||
|
}
|
||||||
|
|
||||||
|
levels := []string{"INFO", "WARNING", "ERROR", "FATAL", "DEBUG"}
|
||||||
|
levelWeights := []int{50, 20, 15, 5, 10} // weighted distribution
|
||||||
|
|
||||||
|
messages := map[string][]string{
|
||||||
|
"INFO": {
|
||||||
|
"Service started successfully",
|
||||||
|
"Health check passed",
|
||||||
|
"Configuration loaded",
|
||||||
|
"Connection established to database",
|
||||||
|
"Cache refreshed successfully",
|
||||||
|
"Request processed in 45ms",
|
||||||
|
"User login successful",
|
||||||
|
"Agent registered successfully",
|
||||||
|
"Deployment completed for 3 servers",
|
||||||
|
"Metrics exported to storage",
|
||||||
|
"Backup completed successfully",
|
||||||
|
"SSL certificate valid for 89 days",
|
||||||
|
"Task scheduled: cleanup-temp-files",
|
||||||
|
"Webhook delivered successfully",
|
||||||
|
"Session created for user admin",
|
||||||
|
},
|
||||||
|
"WARNING": {
|
||||||
|
"High memory usage detected: 85%",
|
||||||
|
"Slow query detected: 2.3s",
|
||||||
|
"Rate limit approaching for client 192.168.1.50",
|
||||||
|
"Certificate expires in 7 days",
|
||||||
|
"Retry attempt 2/3 for request",
|
||||||
|
"Disk usage above threshold: 78%",
|
||||||
|
"Connection pool nearly exhausted: 45/50",
|
||||||
|
"Deprecated API endpoint called: /api/v1/legacy",
|
||||||
|
"Response time exceeded SLA: 1.2s > 1s",
|
||||||
|
"Agent heartbeat delayed by 5s",
|
||||||
|
},
|
||||||
|
"ERROR": {
|
||||||
|
"Failed to connect to database: timeout after 30s",
|
||||||
|
"Authentication failed for user test_user",
|
||||||
|
"Agent deployment failed: SSH connection refused",
|
||||||
|
"Failed to send notification: SMTP server unavailable",
|
||||||
|
"Request failed with status 500",
|
||||||
|
"File not found: /etc/hellreign/config.yml",
|
||||||
|
"Invalid token provided",
|
||||||
|
"Permission denied for user viewer",
|
||||||
|
"Failed to parse configuration: invalid YAML",
|
||||||
|
"Agent unreachable: connection timeout",
|
||||||
|
},
|
||||||
|
"FATAL": {
|
||||||
|
"Out of memory: cannot allocate 512MB",
|
||||||
|
"Database connection lost, all retries exhausted",
|
||||||
|
"Critical: SSL certificate expired",
|
||||||
|
"Unrecoverable error: data corruption detected",
|
||||||
|
"Service crashed: segmentation fault",
|
||||||
|
},
|
||||||
|
"DEBUG": {
|
||||||
|
"Processing request payload: 2.3KB",
|
||||||
|
"Cache hit ratio: 78%",
|
||||||
|
"Executing query: SELECT * FROM logs WHERE...",
|
||||||
|
"HTTP request headers: {Content-Type: application/json}",
|
||||||
|
"Agent status check: 8 agents online",
|
||||||
|
"Memory allocation: 256MB used of 1024MB",
|
||||||
|
"Thread pool size: 12 active, 4 idle",
|
||||||
|
"GC pause: 15ms",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
r := rand.New(rand.NewSource(42)) // fixed seed for reproducibility
|
||||||
|
|
||||||
|
var logs []storage.LogEntry
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
for i := 0; i < count; i++ {
|
||||||
|
level := weightedRandom(r, levels, levelWeights)
|
||||||
|
service := services[r.Intn(len(services))]
|
||||||
|
agent := agents[r.Intn(len(agents))]
|
||||||
|
msgs := messages[level]
|
||||||
|
message := msgs[r.Intn(len(msgs))]
|
||||||
|
|
||||||
|
// Spread logs over the last 24 hours
|
||||||
|
timestamp := now.Add(-time.Duration(count-i) * time.Minute * 15)
|
||||||
|
|
||||||
|
logs = append(logs, storage.LogEntry{
|
||||||
|
Timestamp: timestamp,
|
||||||
|
Level: level,
|
||||||
|
Service: service,
|
||||||
|
Agent: agent,
|
||||||
|
Message: message,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return logs
|
||||||
|
}
|
||||||
|
|
||||||
|
func weightedRandom(r *rand.Rand, items []string, weights []int) string {
|
||||||
|
total := 0
|
||||||
|
for _, w := range weights {
|
||||||
|
total += w
|
||||||
|
}
|
||||||
|
n := r.Intn(total)
|
||||||
|
for i, w := range weights {
|
||||||
|
n -= w
|
||||||
|
if n < 0 {
|
||||||
|
return items[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return items[len(items)-1]
|
||||||
|
}
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ScriptHandlers struct {
|
||||||
|
svc *service.ScriptService
|
||||||
|
cmder *commander.Commander
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewScriptHandlers(svc *service.ScriptService, cmder *commander.Commander) ScriptHandlers {
|
||||||
|
return ScriptHandlers{svc: svc, cmder: cmder}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunScript executes a script on a target agent.
|
||||||
|
// @Summary Run a script on an agent
|
||||||
|
// @Description Resolves interpreter argv[] and sends the full command to the agent
|
||||||
|
// @Tags scripts
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param body body RunScriptIn true "Script request"
|
||||||
|
// @Success 201 {object} RunScriptOut
|
||||||
|
// @Router /scripts/run [post]
|
||||||
|
func (self *ScriptHandlers) RunScript(c *gin.Context) {
|
||||||
|
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
|
||||||
|
if err := c.Bind(&in); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
command, err := self.svc.ResolveCommand(c.Request.Context(), in.InterpreterID, in.ScriptText)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
agent, ok := self.cmder.GetAgent(in.AgentID)
|
||||||
|
if !ok {
|
||||||
|
c.Status(http.StatusNotFound)
|
||||||
|
return fmt.Errorf("agent not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
jid, err := agent.AddJob(models.JobForInsert{
|
||||||
|
Command: command,
|
||||||
|
Stdin: in.Stdin,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
job, err := agent.WaitJob(jid)
|
||||||
|
if err != nil {
|
||||||
|
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{
|
||||||
|
ID: job.ID,
|
||||||
|
Command: job.Command,
|
||||||
|
Stdin: job.Stdin,
|
||||||
|
Stdout: job.Stdout,
|
||||||
|
Stderr: job.Stderr,
|
||||||
|
Status: job.Status,
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}()
|
||||||
|
if err != nil {
|
||||||
|
c.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListInterpreters returns all registered script interpreters.
|
||||||
|
// @Summary List interpreters
|
||||||
|
// @Description Returns all script interpreters available in the system
|
||||||
|
// @Tags scripts
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {array} repository.ScriptInterpreter
|
||||||
|
// @Router /scripts/interpreters [get]
|
||||||
|
func (self *ScriptHandlers) ListInterpreters(c *gin.Context) {
|
||||||
|
interpreters, err := self.svc.List(c.Request.Context())
|
||||||
|
if err != nil {
|
||||||
|
c.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, interpreters)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateInterpreter registers a new script interpreter.
|
||||||
|
// @Summary Create interpreter
|
||||||
|
// @Description Registers a new script interpreter with name, label, and argv
|
||||||
|
// @Tags scripts
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param body body repository.ScriptInterpreterCreate true "Interpreter definition"
|
||||||
|
// @Success 201 {object} repository.ScriptInterpreter
|
||||||
|
// @Router /scripts/interpreters [post]
|
||||||
|
func (self *ScriptHandlers) CreateInterpreter(c *gin.Context) {
|
||||||
|
var in repository.ScriptInterpreterCreate
|
||||||
|
if err := c.BindJSON(&in); err != nil {
|
||||||
|
c.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
si, err := self.svc.Create(c.Request.Context(), in)
|
||||||
|
if err != nil {
|
||||||
|
c.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusCreated, si)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetInterpreter returns a single interpreter by ID.
|
||||||
|
// @Summary Get interpreter
|
||||||
|
// @Description Returns a script interpreter by ID
|
||||||
|
// @Tags scripts
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path int true "Interpreter ID"
|
||||||
|
// @Success 200 {object} repository.ScriptInterpreter
|
||||||
|
// @Router /scripts/interpreters/:id [get]
|
||||||
|
func (self *ScriptHandlers) GetInterpreter(c *gin.Context) {
|
||||||
|
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
c.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
si, err := self.svc.GetByID(c.Request.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
c.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, si)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateInterpreter updates an interpreter.
|
||||||
|
// @Summary Update interpreter
|
||||||
|
// @Description Updates fields of a script interpreter
|
||||||
|
// @Tags scripts
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path int true "Interpreter ID"
|
||||||
|
// @Param body body repository.ScriptInterpreterUpdate true "Interpreter fields"
|
||||||
|
// @Success 200 {object} repository.ScriptInterpreter
|
||||||
|
// @Router /scripts/interpreters/:id [put]
|
||||||
|
func (self *ScriptHandlers) UpdateInterpreter(c *gin.Context) {
|
||||||
|
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
c.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var in repository.ScriptInterpreterUpdate
|
||||||
|
if err := c.BindJSON(&in); err != nil {
|
||||||
|
c.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
si, err := self.svc.Update(c.Request.Context(), id, in)
|
||||||
|
if err != nil {
|
||||||
|
c.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, si)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteInterpreter removes an interpreter.
|
||||||
|
// @Summary Delete interpreter
|
||||||
|
// @Description Removes a script interpreter by ID
|
||||||
|
// @Tags scripts
|
||||||
|
// @Param id path int true "Interpreter ID"
|
||||||
|
// @Success 204
|
||||||
|
// @Router /scripts/interpreters/:id [delete]
|
||||||
|
func (self *ScriptHandlers) DeleteInterpreter(c *gin.Context) {
|
||||||
|
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
c.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := self.svc.Delete(c.Request.Context(), id); err != nil {
|
||||||
|
c.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Status(http.StatusNoContent)
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/models"
|
||||||
|
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
type JobRepository struct {
|
||||||
|
DB *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewJobRepository(db *sql.DB) *JobRepository {
|
||||||
|
return &JobRepository{DB: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *JobRepository) Init(ctx context.Context) error {
|
||||||
|
_, err := r.DB.ExecContext(ctx, storage.CreateJobsTable)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *JobRepository) InitJob(ctx context.Context, agentID string, job models.JobForInsert) (int64, error) {
|
||||||
|
commandJSON, err := json.Marshal(job.Command)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("marshal command: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var stdinVal *string
|
||||||
|
if job.Stdin != nil {
|
||||||
|
stdinVal = job.Stdin
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := r.DB.ExecContext(ctx,
|
||||||
|
`INSERT INTO jobs (agent_id, command, stdin, stdout, stderr, status) VALUES (?, ?, ?, '', '', 0)`,
|
||||||
|
agentID, string(commandJSON), stdinVal,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.LastInsertId()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *JobRepository) UpdateJobInDB(ctx context.Context, jid int64, msg models.JobForUpdate) (models.Job, error) {
|
||||||
|
result, err := r.DB.ExecContext(ctx,
|
||||||
|
`UPDATE jobs SET stdout = ?, stderr = ?, status = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`,
|
||||||
|
msg.Stdout, msg.Stderr, msg.Status, jid,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return models.Job{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
affected, err := result.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return models.Job{}, err
|
||||||
|
}
|
||||||
|
if affected == 0 {
|
||||||
|
return models.Job{}, ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.GetJobByID(ctx, jid)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *JobRepository) GetJobByID(ctx context.Context, jid int64) (models.Job, error) {
|
||||||
|
var job models.Job
|
||||||
|
var commandJSON string
|
||||||
|
var stdinVal *string
|
||||||
|
|
||||||
|
err := r.DB.QueryRowContext(ctx,
|
||||||
|
`SELECT id, command, stdin, stdout, stderr, status FROM jobs WHERE id = ?`,
|
||||||
|
jid,
|
||||||
|
).Scan(&job.ID, &commandJSON, &stdinVal, &job.Stdout, &job.Stderr, &job.Status)
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return models.Job{}, ErrNotFound
|
||||||
|
}
|
||||||
|
return models.Job{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal([]byte(commandJSON), &job.JobForInsert.Command); err != nil {
|
||||||
|
return models.Job{}, fmt.Errorf("unmarshal command: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
job.JobForInsert.Stdin = stdinVal
|
||||||
|
return job, nil
|
||||||
|
}
|
||||||
@@ -2,44 +2,85 @@ package repository
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/storage"
|
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/storage"
|
||||||
"github.com/ClickHouse/clickhouse-go/v2/lib/driver"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type LogRepository struct {
|
type LogRepository struct {
|
||||||
Conn driver.Conn
|
mu sync.RWMutex
|
||||||
|
DB *sql.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewLogRepository(conn driver.Conn) *LogRepository {
|
func NewLogRepository() *LogRepository {
|
||||||
return &LogRepository{Conn: conn}
|
return &LogRepository{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LogRepository) SetDB(db *sql.DB) {
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
r.DB = db
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LogRepository) IsConnected() bool {
|
||||||
|
r.mu.RLock()
|
||||||
|
defer r.mu.RUnlock()
|
||||||
|
return r.DB != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LogRepository) getDB() *sql.DB {
|
||||||
|
r.mu.RLock()
|
||||||
|
defer r.mu.RUnlock()
|
||||||
|
return r.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *LogRepository) Init(ctx context.Context) error {
|
func (r *LogRepository) Init(ctx context.Context) error {
|
||||||
return r.Conn.Exec(ctx, storage.CreateLogsTable)
|
db := r.getDB()
|
||||||
|
if db == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
_, err := db.ExecContext(ctx, storage.CreateLogsTable)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *LogRepository) Insert(ctx context.Context, log storage.LogEntry) error {
|
func (r *LogRepository) Insert(ctx context.Context, log storage.LogEntry) error {
|
||||||
return r.Conn.Exec(ctx, `
|
db := r.getDB()
|
||||||
|
if db == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
_, err := db.ExecContext(ctx, `
|
||||||
INSERT INTO logs (timestamp, level, service, agent, message)
|
INSERT INTO logs (timestamp, level, service, agent, message)
|
||||||
VALUES ($1, $2, $3, $4, $5)
|
VALUES ($1, $2, $3, $4, $5)
|
||||||
`, log.Timestamp, log.Level, log.Service, log.Agent, log.Message)
|
`, log.Timestamp, log.Level, log.Service, log.Agent, log.Message)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *LogRepository) InsertBatch(ctx context.Context, logs []storage.LogEntry) error {
|
func (r *LogRepository) InsertBatch(ctx context.Context, logs []storage.LogEntry) error {
|
||||||
batch, err := r.Conn.PrepareBatch(ctx, "INSERT INTO logs (timestamp, level, service, agent, message)")
|
db := r.getDB()
|
||||||
if err != nil {
|
if db == nil {
|
||||||
return err
|
return nil
|
||||||
|
}
|
||||||
|
if len(logs) == 0 {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, log := range logs {
|
// Build multi-row INSERT statement
|
||||||
if err := batch.Append(log.Timestamp, log.Level, log.Service, log.Agent, log.Message); err != nil {
|
query := "INSERT INTO logs (timestamp, level, service, agent, message) VALUES "
|
||||||
return err
|
args := make([]interface{}, 0, len(logs)*5)
|
||||||
|
for i, log := range logs {
|
||||||
|
if i > 0 {
|
||||||
|
query += ", "
|
||||||
}
|
}
|
||||||
|
query += fmt.Sprintf("($%d, $%d, $%d, $%d, $%d)",
|
||||||
|
i*5+1, i*5+2, i*5+3, i*5+4, i*5+5)
|
||||||
|
args = append(args, log.Timestamp, log.Level, log.Service, log.Agent, log.Message)
|
||||||
}
|
}
|
||||||
|
|
||||||
return batch.Send()
|
_, err := db.ExecContext(ctx, query, args...)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
type LogFilter struct {
|
type LogFilter struct {
|
||||||
@@ -53,6 +94,11 @@ type LogFilter struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *LogRepository) Search(ctx context.Context, filter LogFilter) ([]storage.LogEntry, error) {
|
func (r *LogRepository) Search(ctx context.Context, filter LogFilter) ([]storage.LogEntry, error) {
|
||||||
|
db := r.getDB()
|
||||||
|
if db == nil {
|
||||||
|
return []storage.LogEntry{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
query := "SELECT timestamp, level, service, agent, message FROM logs WHERE 1=1"
|
query := "SELECT timestamp, level, service, agent, message FROM logs WHERE 1=1"
|
||||||
args := make([]interface{}, 0)
|
args := make([]interface{}, 0)
|
||||||
argIdx := 1
|
argIdx := 1
|
||||||
@@ -102,13 +148,13 @@ func (r *LogRepository) Search(ctx context.Context, filter LogFilter) ([]storage
|
|||||||
args = append(args, filter.Offset)
|
args = append(args, filter.Offset)
|
||||||
}
|
}
|
||||||
|
|
||||||
rows, err := r.Conn.Query(ctx, query, args...)
|
rows, err := db.QueryContext(ctx, query, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
var logs []storage.LogEntry
|
logs := make([]storage.LogEntry, 0)
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var log storage.LogEntry
|
var log storage.LogEntry
|
||||||
if err := rows.Scan(&log.Timestamp, &log.Level, &log.Service, &log.Agent, &log.Message); err != nil {
|
if err := rows.Scan(&log.Timestamp, &log.Level, &log.Service, &log.Agent, &log.Message); err != nil {
|
||||||
@@ -121,13 +167,17 @@ func (r *LogRepository) Search(ctx context.Context, filter LogFilter) ([]storage
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *LogRepository) GetDistinctServices(ctx context.Context) ([]string, error) {
|
func (r *LogRepository) GetDistinctServices(ctx context.Context) ([]string, error) {
|
||||||
rows, err := r.Conn.Query(ctx, "SELECT DISTINCT service FROM logs ORDER BY service")
|
db := r.getDB()
|
||||||
|
if db == nil {
|
||||||
|
return []string{}, nil
|
||||||
|
}
|
||||||
|
rows, err := db.QueryContext(ctx, "SELECT DISTINCT service FROM logs ORDER BY service")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
var services []string
|
services := make([]string, 0)
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var service string
|
var service string
|
||||||
if err := rows.Scan(&service); err != nil {
|
if err := rows.Scan(&service); err != nil {
|
||||||
@@ -140,13 +190,17 @@ func (r *LogRepository) GetDistinctServices(ctx context.Context) ([]string, erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *LogRepository) GetDistinctAgents(ctx context.Context) ([]string, error) {
|
func (r *LogRepository) GetDistinctAgents(ctx context.Context) ([]string, error) {
|
||||||
rows, err := r.Conn.Query(ctx, "SELECT DISTINCT agent FROM logs ORDER BY agent")
|
db := r.getDB()
|
||||||
|
if db == nil {
|
||||||
|
return []string{}, nil
|
||||||
|
}
|
||||||
|
rows, err := db.QueryContext(ctx, "SELECT DISTINCT agent FROM logs ORDER BY agent")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
var agents []string
|
agents := make([]string, 0)
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var agent string
|
var agent string
|
||||||
if err := rows.Scan(&agent); err != nil {
|
if err := rows.Scan(&agent); err != nil {
|
||||||
@@ -159,13 +213,17 @@ func (r *LogRepository) GetDistinctAgents(ctx context.Context) ([]string, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *LogRepository) GetDistinctLevels(ctx context.Context) ([]string, error) {
|
func (r *LogRepository) GetDistinctLevels(ctx context.Context) ([]string, error) {
|
||||||
rows, err := r.Conn.Query(ctx, "SELECT DISTINCT level FROM logs ORDER BY level")
|
db := r.getDB()
|
||||||
|
if db == nil {
|
||||||
|
return []string{}, nil
|
||||||
|
}
|
||||||
|
rows, err := db.QueryContext(ctx, "SELECT DISTINCT level FROM logs ORDER BY level")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
var levels []string
|
levels := make([]string, 0)
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var level string
|
var level string
|
||||||
if err := rows.Scan(&level); err != nil {
|
if err := rows.Scan(&level); err != nil {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ type Tokens struct {
|
|||||||
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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TokenCreate is the request body for creating a new user.
|
// TokenCreate is the request body for creating a new user.
|
||||||
@@ -21,6 +22,31 @@ type TokenCreate struct {
|
|||||||
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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TokenUpdate is the request body for updating an existing user.
|
||||||
|
type TokenUpdate struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
LastName string `json:"last_name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TokenUpdatePermissions is the request body for updating user permissions.
|
||||||
|
type TokenUpdatePermissions struct {
|
||||||
|
PermissionView *bool `json:"permission_view"`
|
||||||
|
PermissionManage *bool `json:"permission_manage_agent"`
|
||||||
|
PermissionAdmin *bool `json:"permission_admin"`
|
||||||
|
IsActive *bool `json:"is_active"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TokenPasswordReset is the request body for resetting a user's password.
|
||||||
|
type TokenPasswordReset struct {
|
||||||
|
NewPassword string `json:"new_password" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchActionRequest is the request body for batch activate/deactivate users.
|
||||||
|
type BatchActionRequest struct {
|
||||||
|
Logins []string `json:"logins" binding:"required,min=1"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoginRequest is the request body for login.
|
// LoginRequest is the request body for login.
|
||||||
@@ -38,4 +64,80 @@ type LoginResponse struct {
|
|||||||
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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegistrationToken represents a one-time agent registration token.
|
||||||
|
type RegistrationToken struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Token string `json:"token"`
|
||||||
|
Label string `json:"label"`
|
||||||
|
Used bool `json:"used"`
|
||||||
|
CreatedAt *string `json:"created_at"`
|
||||||
|
UsedAt *string `json:"used_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegistrationRequest is the request body for creating a registration token.
|
||||||
|
type RegistrationRequest struct {
|
||||||
|
Label string `json:"label" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegistrationResponse is returned when an agent registers.
|
||||||
|
type RegistrationResponse struct {
|
||||||
|
CACert string `json:"ca_cert"`
|
||||||
|
ClientCert string `json:"client_cert"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeployType represents the type of agent deployment
|
||||||
|
// @Description Type of deployment: docker or binary
|
||||||
|
type DeployType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
DeployTypeDocker DeployType = "docker"
|
||||||
|
DeployTypeBinary DeployType = "binary"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AuthMethod represents the SSH authentication method
|
||||||
|
// @Description SSH authentication method: key or password
|
||||||
|
type AuthMethod string
|
||||||
|
|
||||||
|
const (
|
||||||
|
AuthMethodKey AuthMethod = "key"
|
||||||
|
AuthMethodPassword AuthMethod = "password"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AgentDeployConfig represents the configuration for deploying an agent to a server
|
||||||
|
// @Description Configuration for deploying HellreigN agent to a single server
|
||||||
|
type AgentDeployConfig struct {
|
||||||
|
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"`
|
||||||
|
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"`
|
||||||
|
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)"`
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeployAgentsRequest represents the request body for deploying agents to multiple servers
|
||||||
|
// @Description Request to deploy HellreigN agents to multiple servers
|
||||||
|
type DeployAgentsRequest struct {
|
||||||
|
Servers []AgentDeployConfig `json:"servers" binding:"required,min=1,dive" description:"List of server configurations"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeployResponse represents the response after deploying agents
|
||||||
|
// @Description Response containing deployment results and registration tokens
|
||||||
|
type DeployResponse struct {
|
||||||
|
Message string `json:"message" example:"Deployment completed"`
|
||||||
|
Results []DeployResult `json:"results" description:"Deployment results for each server"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeployResult represents the result of deploying to a single server
|
||||||
|
// @Description Result of deploying to a single server
|
||||||
|
type DeployResult struct {
|
||||||
|
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"`
|
||||||
|
Token string `json:"token" example:"abc123..." description:"Registration token for agent registration"`
|
||||||
|
Success bool `json:"success" example:"true" description:"Whether deployment succeeded"`
|
||||||
|
Error string `json:"error,omitempty" example:"" description:"Error message if deployment failed"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,14 +21,21 @@ func New(db *sql.DB) *Repository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var ErrNotFound = errors.New("not found")
|
var ErrNotFound = errors.New("not found")
|
||||||
|
var ErrAccountInactive = errors.New("account is not activated")
|
||||||
|
|
||||||
// Init creates the tokens table if it does not exist.
|
// Init creates the tokens table if it does not exist.
|
||||||
func (r *Repository) Init() error {
|
func (r *Repository) Init() error {
|
||||||
_, err := r.DB.Exec(storage.CreateSqlite)
|
_, err := r.DB.Exec(storage.CreateSqlite)
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
// Migration: add is_active column if it doesn't exist (SQLite ignores errors for duplicate column)
|
||||||
|
_, _ = r.DB.Exec(storage.AddIsActiveColumn)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// CreateToken inserts a new user record with hashed password and generated token.
|
// CreateToken inserts a new user record with hashed password and generated token.
|
||||||
|
// New users are created with is_active=false by default.
|
||||||
func (r *Repository) CreateToken(tc TokenCreate) (string, error) {
|
func (r *Repository) CreateToken(tc TokenCreate) (string, error) {
|
||||||
hashed, err := bcrypt.GenerateFromPassword([]byte(tc.Password), bcrypt.DefaultCost)
|
hashed, err := bcrypt.GenerateFromPassword([]byte(tc.Password), bcrypt.DefaultCost)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -41,10 +48,10 @@ 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)
|
`INSERT INTO tokens (name, last_name, login, password, token, permission_view, permission_manage_agent, permission_admin, is_active)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
tc.Name, tc.LastName, tc.Login, string(hashed), token,
|
tc.Name, tc.LastName, tc.Login, string(hashed), token,
|
||||||
tc.PermissionView, tc.PermissionManage, tc.PermissionAdmin,
|
tc.PermissionView, tc.PermissionManage, tc.PermissionAdmin, tc.IsActive,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@@ -63,11 +70,11 @@ func (r *Repository) Login(login, password string) (*LoginResponse, error) {
|
|||||||
var hashedPassword string
|
var hashedPassword string
|
||||||
|
|
||||||
err := r.DB.QueryRow(
|
err := r.DB.QueryRow(
|
||||||
`SELECT id, name, last_name, login, password, token, permission_view, permission_manage_agent, permission_admin
|
`SELECT id, name, last_name, login, password, token, permission_view, permission_manage_agent, permission_admin, is_active
|
||||||
FROM tokens WHERE login = ?`,
|
FROM tokens WHERE login = ?`,
|
||||||
login,
|
login,
|
||||||
).Scan(&t.ID, &t.Name, &t.LastName, &t.Login, &hashedPassword, &t.Token,
|
).Scan(&t.ID, &t.Name, &t.LastName, &t.Login, &hashedPassword, &t.Token,
|
||||||
&t.PermissionView, &t.PermissionManage, &t.PermissionAdmin)
|
&t.PermissionView, &t.PermissionManage, &t.PermissionAdmin, &t.IsActive)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
@@ -80,6 +87,10 @@ func (r *Repository) Login(login, password string) (*LoginResponse, error) {
|
|||||||
return nil, ErrNotFound
|
return nil, ErrNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !t.IsActive {
|
||||||
|
return nil, ErrAccountInactive
|
||||||
|
}
|
||||||
|
|
||||||
// Generate new token on each login
|
// Generate new token on each login
|
||||||
newToken, err := utils.RandomToken()
|
newToken, err := utils.RandomToken()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -99,6 +110,7 @@ func (r *Repository) Login(login, password string) (*LoginResponse, error) {
|
|||||||
PermissionView: t.PermissionView,
|
PermissionView: t.PermissionView,
|
||||||
PermissionManage: t.PermissionManage,
|
PermissionManage: t.PermissionManage,
|
||||||
PermissionAdmin: t.PermissionAdmin,
|
PermissionAdmin: t.PermissionAdmin,
|
||||||
|
IsActive: t.IsActive,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,3 +197,266 @@ func (r *Repository) ExistsByLogin(login string) bool {
|
|||||||
}
|
}
|
||||||
return count > 0
|
return count > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InitRegistrationTokens creates the registration_tokens table if it does not exist.
|
||||||
|
func (r *Repository) InitRegistrationTokens() error {
|
||||||
|
_, err := r.DB.Exec(storage.CreateRegistrationTokensTable)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateRegistrationToken inserts a new one-time registration token.
|
||||||
|
func (r *Repository) CreateRegistrationToken(label string) (string, error) {
|
||||||
|
token, err := utils.RandomToken()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = r.DB.Exec(
|
||||||
|
`INSERT INTO registration_tokens (token, label, used) VALUES (?, ?, 0)`,
|
||||||
|
token, label,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRegistrationToken retrieves a registration token if it exists and is not used.
|
||||||
|
func (r *Repository) GetRegistrationToken(token string) (*RegistrationToken, error) {
|
||||||
|
var rt RegistrationToken
|
||||||
|
err := r.DB.QueryRow(
|
||||||
|
`SELECT id, token, label, used, created_at, used_at FROM registration_tokens WHERE token = ?`,
|
||||||
|
token,
|
||||||
|
).Scan(&rt.ID, &rt.Token, &rt.Label, &rt.Used, &rt.CreatedAt, &rt.UsedAt)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return nil, ErrNotFound
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &rt, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarkRegistrationTokenUsed marks a registration token as used.
|
||||||
|
func (r *Repository) MarkRegistrationTokenUsed(token string) error {
|
||||||
|
result, err := r.DB.Exec(
|
||||||
|
`UPDATE registration_tokens SET used = 1, used_at = CURRENT_TIMESTAMP WHERE token = ? AND used = 0`,
|
||||||
|
token,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
affected, err := result.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if affected == 0 {
|
||||||
|
return ErrNotFound
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ActivateToken activates a user by token value.
|
||||||
|
func (r *Repository) ActivateToken(token string) error {
|
||||||
|
result, err := r.DB.Exec(
|
||||||
|
`UPDATE tokens SET is_active = 1 WHERE token = ?`,
|
||||||
|
token,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
affected, err := result.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if affected == 0 {
|
||||||
|
return ErrNotFound
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeactivateToken deactivates a user by token value.
|
||||||
|
func (r *Repository) DeactivateToken(token string) error {
|
||||||
|
result, err := r.DB.Exec(
|
||||||
|
`UPDATE tokens SET is_active = 0 WHERE token = ?`,
|
||||||
|
token,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
affected, err := result.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if affected == 0 {
|
||||||
|
return ErrNotFound
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ActivateUserByLogin activates a user by login.
|
||||||
|
func (r *Repository) ActivateUserByLogin(login string) error {
|
||||||
|
result, err := r.DB.Exec(
|
||||||
|
`UPDATE tokens SET is_active = 1 WHERE login = ?`,
|
||||||
|
login,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
affected, err := result.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if affected == 0 {
|
||||||
|
return ErrNotFound
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeactivateUserByLogin deactivates a user by login.
|
||||||
|
func (r *Repository) DeactivateUserByLogin(login string) error {
|
||||||
|
result, err := r.DB.Exec(
|
||||||
|
`UPDATE tokens SET is_active = 0 WHERE login = ?`,
|
||||||
|
login,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
affected, err := result.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if affected == 0 {
|
||||||
|
return ErrNotFound
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListInactiveTokens returns all users that are not activated.
|
||||||
|
func (r *Repository) ListInactiveTokens() ([]Tokens, error) {
|
||||||
|
rows, err := r.DB.Query(
|
||||||
|
`SELECT id, name, last_name, login, token, permission_view, permission_manage_agent, permission_admin, is_active
|
||||||
|
FROM tokens WHERE is_active = 0`,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var tokens []Tokens
|
||||||
|
for rows.Next() {
|
||||||
|
var t Tokens
|
||||||
|
if err := rows.Scan(&t.ID, &t.Name, &t.LastName, &t.Login, &t.Token,
|
||||||
|
&t.PermissionView, &t.PermissionManage, &t.PermissionAdmin, &t.IsActive); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tokens = append(tokens, t)
|
||||||
|
}
|
||||||
|
return tokens, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTokenByLogin retrieves a user by login.
|
||||||
|
func (r *Repository) GetTokenByLogin(login string) (*Tokens, error) {
|
||||||
|
var t Tokens
|
||||||
|
err := r.DB.QueryRow(
|
||||||
|
`SELECT id, name, last_name, login, token, permission_view, permission_manage_agent, permission_admin, is_active
|
||||||
|
FROM tokens WHERE login = ?`,
|
||||||
|
login,
|
||||||
|
).Scan(&t.ID, &t.Name, &t.LastName, &t.Login, &t.Token,
|
||||||
|
&t.PermissionView, &t.PermissionManage, &t.PermissionAdmin, &t.IsActive)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return nil, ErrNotFound
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &t, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateToken updates name and last_name for a user by login.
|
||||||
|
func (r *Repository) UpdateToken(login string, update TokenUpdate) error {
|
||||||
|
result, err := r.DB.Exec(
|
||||||
|
`UPDATE tokens SET name = ?, last_name = ? WHERE login = ?`,
|
||||||
|
update.Name, update.LastName, login,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
affected, err := result.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if affected == 0 {
|
||||||
|
return ErrNotFound
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdatePermissions updates permissions and is_active for a user by login.
|
||||||
|
func (r *Repository) UpdatePermissions(login string, update TokenUpdatePermissions) error {
|
||||||
|
user, err := r.GetTokenByLogin(login)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use existing values if not provided
|
||||||
|
newView := user.PermissionView
|
||||||
|
newManage := user.PermissionManage
|
||||||
|
newAdmin := user.PermissionAdmin
|
||||||
|
newActive := user.IsActive
|
||||||
|
|
||||||
|
if update.PermissionView != nil {
|
||||||
|
newView = *update.PermissionView
|
||||||
|
}
|
||||||
|
if update.PermissionManage != nil {
|
||||||
|
newManage = *update.PermissionManage
|
||||||
|
}
|
||||||
|
if update.PermissionAdmin != nil {
|
||||||
|
newAdmin = *update.PermissionAdmin
|
||||||
|
}
|
||||||
|
if update.IsActive != nil {
|
||||||
|
newActive = *update.IsActive
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := r.DB.Exec(
|
||||||
|
`UPDATE tokens SET permission_view = ?, permission_manage_agent = ?, permission_admin = ?, is_active = ? WHERE login = ?`,
|
||||||
|
newView, newManage, newAdmin, newActive, login,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
affected, err := result.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if affected == 0 {
|
||||||
|
return ErrNotFound
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdatePassword updates the password for a user by login.
|
||||||
|
func (r *Repository) UpdatePassword(login string, newPassword string) error {
|
||||||
|
hashed, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := r.DB.Exec(
|
||||||
|
`UPDATE tokens SET password = ? WHERE login = ?`,
|
||||||
|
string(hashed), login,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
affected, err := result.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if affected == 0 {
|
||||||
|
return ErrNotFound
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,189 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ScriptInterpreter struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Label string `json:"label"`
|
||||||
|
Argv []string `json:"argv"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ScriptInterpreterCreate struct {
|
||||||
|
Name string `json:"name" binding:"required"`
|
||||||
|
Label string `json:"label" binding:"required"`
|
||||||
|
Argv []string `json:"argv" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ScriptInterpreterUpdate struct {
|
||||||
|
Name *string `json:"name"`
|
||||||
|
Label *string `json:"label"`
|
||||||
|
Argv []string `json:"argv"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ScriptInterpreterRepo struct {
|
||||||
|
DB *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewScriptInterpreterRepo(db *sql.DB) *ScriptInterpreterRepo {
|
||||||
|
return &ScriptInterpreterRepo{DB: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ScriptInterpreterRepo) Init(ctx context.Context) error {
|
||||||
|
_, err := r.DB.ExecContext(ctx, storage.CreateScriptInterpretersTable)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ScriptInterpreterRepo) Create(ctx context.Context, in ScriptInterpreterCreate) (*ScriptInterpreter, error) {
|
||||||
|
argvJSON, err := json.Marshal(in.Argv)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := r.DB.ExecContext(ctx,
|
||||||
|
`INSERT INTO script_interpreters (name, label, argv) VALUES (?, ?, ?)`,
|
||||||
|
in.Name, in.Label, string(argvJSON),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := result.LastInsertId()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.GetByID(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ScriptInterpreterRepo) GetByID(ctx context.Context, id int64) (*ScriptInterpreter, error) {
|
||||||
|
var si ScriptInterpreter
|
||||||
|
var argvJSON string
|
||||||
|
var createdAt, updatedAt string
|
||||||
|
|
||||||
|
err := r.DB.QueryRowContext(ctx,
|
||||||
|
`SELECT id, name, label, argv, created_at, updated_at FROM script_interpreters WHERE id = ?`,
|
||||||
|
id,
|
||||||
|
).Scan(&si.ID, &si.Name, &si.Label, &argvJSON, &createdAt, &updatedAt)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return nil, ErrNotFound
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal([]byte(argvJSON), &si.Argv); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
si.CreatedAt, _ = time.Parse(time.RFC3339, createdAt)
|
||||||
|
si.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt)
|
||||||
|
return &si, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ScriptInterpreterRepo) List(ctx context.Context) ([]ScriptInterpreter, error) {
|
||||||
|
rows, err := r.DB.QueryContext(ctx,
|
||||||
|
`SELECT id, name, label, argv, created_at, updated_at FROM script_interpreters`,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var interpreters []ScriptInterpreter
|
||||||
|
for rows.Next() {
|
||||||
|
var si ScriptInterpreter
|
||||||
|
var argvJSON, createdAt, updatedAt string
|
||||||
|
if err := rows.Scan(&si.ID, &si.Name, &si.Label, &argvJSON, &createdAt, &updatedAt); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal([]byte(argvJSON), &si.Argv); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
si.CreatedAt, _ = time.Parse(time.RFC3339, createdAt)
|
||||||
|
si.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt)
|
||||||
|
interpreters = append(interpreters, si)
|
||||||
|
}
|
||||||
|
return interpreters, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ScriptInterpreterRepo) Update(ctx context.Context, id int64, in ScriptInterpreterUpdate) (*ScriptInterpreter, error) {
|
||||||
|
si, err := r.GetByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
set := ""
|
||||||
|
args := make([]interface{}, 0)
|
||||||
|
idx := 1
|
||||||
|
|
||||||
|
if in.Name != nil {
|
||||||
|
set += "name = ?"
|
||||||
|
args = append(args, *in.Name)
|
||||||
|
idx++
|
||||||
|
}
|
||||||
|
if in.Label != nil {
|
||||||
|
if idx > 1 {
|
||||||
|
set += ", "
|
||||||
|
}
|
||||||
|
set += "label = ?"
|
||||||
|
args = append(args, *in.Label)
|
||||||
|
idx++
|
||||||
|
}
|
||||||
|
if in.Argv != nil {
|
||||||
|
if idx > 1 {
|
||||||
|
set += ", "
|
||||||
|
}
|
||||||
|
argvJSON, err := json.Marshal(in.Argv)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
set += "argv = ?"
|
||||||
|
args = append(args, string(argvJSON))
|
||||||
|
idx++
|
||||||
|
}
|
||||||
|
|
||||||
|
if idx == 1 {
|
||||||
|
return si, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
set += ", updated_at = CURRENT_TIMESTAMP"
|
||||||
|
args = append(args, id)
|
||||||
|
|
||||||
|
_, err = r.DB.ExecContext(ctx,
|
||||||
|
`UPDATE script_interpreters SET `+set+` WHERE id = ?`,
|
||||||
|
args...,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.GetByID(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ScriptInterpreterRepo) Delete(ctx context.Context, id int64) error {
|
||||||
|
result, err := r.DB.ExecContext(ctx,
|
||||||
|
`DELETE FROM script_interpreters WHERE id = ?`,
|
||||||
|
id,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
affected, err := result.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if affected == 0 {
|
||||||
|
return ErrNotFound
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/repository"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ScriptService struct {
|
||||||
|
repo *repository.ScriptInterpreterRepo
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewScriptService(repo *repository.ScriptInterpreterRepo) *ScriptService {
|
||||||
|
return &ScriptService{repo: repo}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResolveCommand builds the full argv[] by prepending the interpreter's argv
|
||||||
|
// to the script text (as the last argument).
|
||||||
|
func (self *ScriptService) ResolveCommand(ctx context.Context, interpreterID int64, scriptText string) ([]string, error) {
|
||||||
|
interpreter, err := self.repo.GetByID(ctx, interpreterID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(interpreter.Argv) == 0 {
|
||||||
|
return nil, fmt.Errorf("interpreter %q has empty argv", interpreter.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
argv := make([]string, len(interpreter.Argv)+1)
|
||||||
|
copy(argv, interpreter.Argv)
|
||||||
|
argv[len(argv)-1] = scriptText
|
||||||
|
return argv, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *ScriptService) Create(ctx context.Context, in repository.ScriptInterpreterCreate) (*repository.ScriptInterpreter, error) {
|
||||||
|
return self.repo.Create(ctx, in)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *ScriptService) GetByID(ctx context.Context, id int64) (*repository.ScriptInterpreter, error) {
|
||||||
|
return self.repo.GetByID(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *ScriptService) List(ctx context.Context) ([]repository.ScriptInterpreter, error) {
|
||||||
|
return self.repo.List(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *ScriptService) Update(ctx context.Context, id int64, in repository.ScriptInterpreterUpdate) (*repository.ScriptInterpreter, error) {
|
||||||
|
return self.repo.Update(ctx, id, in)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *ScriptService) Delete(ctx context.Context, id int64) error {
|
||||||
|
return self.repo.Delete(ctx, id)
|
||||||
|
}
|
||||||
@@ -2,10 +2,12 @@ package storage
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/ClickHouse/clickhouse-go/v2"
|
_ "github.com/ClickHouse/clickhouse-go/v2"
|
||||||
"github.com/ClickHouse/clickhouse-go/v2/lib/driver"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ClickHouseConfig struct {
|
type ClickHouseConfig struct {
|
||||||
@@ -15,33 +17,46 @@ type ClickHouseConfig struct {
|
|||||||
Database string
|
Database string
|
||||||
}
|
}
|
||||||
|
|
||||||
func OpenClickHouse(cfg ClickHouseConfig) (driver.Conn, error) {
|
func OpenClickHouse(cfg ClickHouseConfig) (*sql.DB, error) {
|
||||||
conn, err := clickhouse.Open(&clickhouse.Options{
|
dsn := fmt.Sprintf("clickhouse://%s:%s@%s/%s",
|
||||||
Addr: []string{cfg.Host},
|
cfg.User, cfg.Password, cfg.Host, cfg.Database)
|
||||||
Auth: clickhouse.Auth{
|
|
||||||
Database: cfg.Database,
|
db, err := sql.Open("clickhouse", dsn)
|
||||||
Username: cfg.User,
|
|
||||||
Password: cfg.Password,
|
|
||||||
},
|
|
||||||
Settings: clickhouse.Settings{
|
|
||||||
"max_execution_time": 60,
|
|
||||||
},
|
|
||||||
Compression: &clickhouse.Compression{
|
|
||||||
Method: clickhouse.CompressionLZ4,
|
|
||||||
},
|
|
||||||
DialTimeout: 30,
|
|
||||||
MaxOpenConns: 10,
|
|
||||||
MaxIdleConns: 5,
|
|
||||||
ConnMaxLifetime: 3600,
|
|
||||||
ConnOpenStrategy: clickhouse.ConnOpenInOrder,
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("clickhouse connect: %w", err)
|
return nil, fmt.Errorf("clickhouse open: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := conn.Ping(context.Background()); err != nil {
|
db.SetMaxOpenConns(5)
|
||||||
|
db.SetMaxIdleConns(2)
|
||||||
|
db.SetConnMaxLifetime(10 * time.Minute)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := db.PingContext(ctx); err != nil {
|
||||||
|
db.Close()
|
||||||
return nil, fmt.Errorf("clickhouse ping: %w", err)
|
return nil, fmt.Errorf("clickhouse ping: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return conn, nil
|
log.Printf("ClickHouse connected via database/sql: %s", cfg.Host)
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenClickHouseWithRetry attempts to connect to ClickHouse with retries and backoff.
|
||||||
|
func OpenClickHouseWithRetry(cfg ClickHouseConfig, maxRetries int, initialDelay time.Duration) (*sql.DB, error) {
|
||||||
|
var lastErr error
|
||||||
|
delay := initialDelay
|
||||||
|
|
||||||
|
for i := 0; i < maxRetries; i++ {
|
||||||
|
db, err := OpenClickHouse(cfg)
|
||||||
|
if err == nil {
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
|
lastErr = err
|
||||||
|
log.Printf("ClickHouse connection attempt %d/%d failed: %v, retrying in %v...", i+1, maxRetries, err, delay)
|
||||||
|
time.Sleep(delay)
|
||||||
|
delay *= 2
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("clickhouse connection failed after %d attempts: %w", maxRetries, lastErr)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,50 @@ const CreateSqlite = `
|
|||||||
token TEXT NOT NULL UNIQUE,
|
token TEXT NOT NULL UNIQUE,
|
||||||
permission_view BOOL NOT NULL,
|
permission_view BOOL NOT NULL,
|
||||||
permission_manage_agent BOOL NOT NULL,
|
permission_manage_agent BOOL NOT NULL,
|
||||||
permission_admin BOOL NOT NULL
|
permission_admin BOOL NOT NULL,
|
||||||
|
is_active BOOL NOT NULL DEFAULT 0
|
||||||
|
);
|
||||||
|
`
|
||||||
|
|
||||||
|
// AddIsActiveColumn adds is_active column to tokens table if it doesn't exist.
|
||||||
|
// This is a migration for existing databases that don't have this column.
|
||||||
|
const AddIsActiveColumn = `
|
||||||
|
ALTER TABLE tokens ADD COLUMN is_active BOOL NOT NULL DEFAULT 0
|
||||||
|
`
|
||||||
|
|
||||||
|
const CreateRegistrationTokensTable = `
|
||||||
|
CREATE TABLE IF NOT EXISTS registration_tokens (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
token TEXT NOT NULL UNIQUE,
|
||||||
|
label TEXT NOT NULL,
|
||||||
|
used BOOL NOT NULL DEFAULT 0,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
used_at DATETIME
|
||||||
|
);
|
||||||
|
`
|
||||||
|
|
||||||
|
const CreateJobsTable = `
|
||||||
|
CREATE TABLE IF NOT EXISTS jobs (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
agent_id TEXT NOT NULL,
|
||||||
|
command TEXT NOT NULL,
|
||||||
|
stdin TEXT,
|
||||||
|
stdout TEXT DEFAULT '',
|
||||||
|
stderr TEXT DEFAULT '',
|
||||||
|
status INTEGER DEFAULT 0,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
`
|
||||||
|
|
||||||
|
const CreateScriptInterpretersTable = `
|
||||||
|
CREATE TABLE IF NOT EXISTS script_interpreters (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL UNIQUE,
|
||||||
|
label TEXT NOT NULL,
|
||||||
|
argv TEXT NOT NULL,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|||||||
@@ -36,5 +36,8 @@ func Open(path string) (*sql.DB, error) {
|
|||||||
return nil, fmt.Errorf("migrate: %w", err)
|
return nil, fmt.Errorf("migrate: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Migration: add is_active column if it doesn't exist
|
||||||
|
_, _ = db.Exec(AddIsActiveColumn)
|
||||||
|
|
||||||
return db, nil
|
return db, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,157 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
"math/big"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CertBundle holds CA and server certificates loaded from disk.
|
||||||
|
type CertBundle struct {
|
||||||
|
CACert *x509.Certificate
|
||||||
|
CAKey *rsa.PrivateKey
|
||||||
|
ServerCert *x509.Certificate
|
||||||
|
ServerKey *rsa.PrivateKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadCertBundle loads CA and server certificates from the given directory.
|
||||||
|
func LoadCertBundle(certDir string) (*CertBundle, error) {
|
||||||
|
caCertPEM, err := os.ReadFile(filepath.Join(certDir, "ca.crt"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read ca.crt: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
caKeyPEM, err := os.ReadFile(filepath.Join(certDir, "ca.key"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read ca.key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
serverCertPEM, err := os.ReadFile(filepath.Join(certDir, "server.crt"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read server.crt: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
serverKeyPEM, err := os.ReadFile(filepath.Join(certDir, "server.key"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read server.key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
caCert := decodeCert(caCertPEM)
|
||||||
|
caKey, err := decodeRSAPrivateKey(caKeyPEM)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parse ca.key: %w", err)
|
||||||
|
}
|
||||||
|
serverCert := decodeCert(serverCertPEM)
|
||||||
|
serverKey, err := decodeRSAPrivateKey(serverKeyPEM)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parse server.key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &CertBundle{
|
||||||
|
CACert: caCert,
|
||||||
|
CAKey: caKey,
|
||||||
|
ServerCert: serverCert,
|
||||||
|
ServerKey: serverKey,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignCSR signs a client CSR with the CA and returns the client certificate PEM.
|
||||||
|
func (b *CertBundle) SignCSR(csrPEM []byte, label string) ([]byte, error) {
|
||||||
|
csr := decodeCSR(csrPEM)
|
||||||
|
|
||||||
|
// Verify CSR signature
|
||||||
|
if err := csr.CheckSignature(); err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid CSR signature: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("generate serial: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
template := x509.Certificate{
|
||||||
|
SerialNumber: serialNumber,
|
||||||
|
Subject: pkix.Name{
|
||||||
|
CommonName: label,
|
||||||
|
Organization: csr.Subject.Organization,
|
||||||
|
},
|
||||||
|
NotBefore: now,
|
||||||
|
NotAfter: now.Add(365 * 24 * time.Hour),
|
||||||
|
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
|
||||||
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
|
||||||
|
BasicConstraintsValid: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
certDER, err := x509.CreateCertificate(rand.Reader, &template, b.CACert, csr.PublicKey, b.CAKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create certificate: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
certPEM := pem.EncodeToMemory(&pem.Block{
|
||||||
|
Type: "CERTIFICATE",
|
||||||
|
Bytes: certDER,
|
||||||
|
})
|
||||||
|
|
||||||
|
return certPEM, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCACertPEM returns the CA certificate as PEM bytes.
|
||||||
|
func (b *CertBundle) GetCACertPEM() []byte {
|
||||||
|
return pem.EncodeToMemory(&pem.Block{
|
||||||
|
Type: "CERTIFICATE",
|
||||||
|
Bytes: b.CACert.Raw,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeCert(pemData []byte) *x509.Certificate {
|
||||||
|
block, _ := pem.Decode(pemData)
|
||||||
|
if block == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
cert, err := x509.ParseCertificate(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return cert
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeRSAPrivateKey(pemData []byte) (*rsa.PrivateKey, error) {
|
||||||
|
block, _ := pem.Decode(pemData)
|
||||||
|
if block == nil {
|
||||||
|
return nil, fmt.Errorf("no PEM block found")
|
||||||
|
}
|
||||||
|
key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
// Try PKCS1 fallback
|
||||||
|
key, err = x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parse PKCS1: %w", err)
|
||||||
|
}
|
||||||
|
return key.(*rsa.PrivateKey), nil
|
||||||
|
}
|
||||||
|
rsaKey, ok := key.(*rsa.PrivateKey)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("key is not RSA, got %T", key)
|
||||||
|
}
|
||||||
|
return rsaKey, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeCSR(pemData []byte) *x509.CertificateRequest {
|
||||||
|
block, _ := pem.Decode(pemData)
|
||||||
|
if block == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
csr, err := x509.ParseCertificateRequest(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return csr
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
CERT_DIR="${1:-/etc/mnemosyne/ssl}"
|
CERT_DIR="${1:-/etc/HellreigN/ssl}"
|
||||||
DAYS_VALID=365
|
DAYS_VALID=365
|
||||||
|
|
||||||
echo "Generating CA and server certificates in ${CERT_DIR}..."
|
echo "Generating CA and server certificates in ${CERT_DIR}..."
|
||||||
@@ -26,7 +26,7 @@ openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:4096 -out "${CERT_DIR}/c
|
|||||||
openssl req -x509 -new -nodes -sha256 -days ${DAYS_VALID} \
|
openssl req -x509 -new -nodes -sha256 -days ${DAYS_VALID} \
|
||||||
-key "${CERT_DIR}/ca.key" \
|
-key "${CERT_DIR}/ca.key" \
|
||||||
-out "${CERT_DIR}/ca.crt" \
|
-out "${CERT_DIR}/ca.crt" \
|
||||||
-subj "/CN=Mnemosyne Root CA"
|
-subj "/CN=HellreigN Root CA"
|
||||||
|
|
||||||
# Генерация серверного сертификата
|
# Генерация серверного сертификата
|
||||||
echo "Generating server certificate..."
|
echo "Generating server certificate..."
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
backend_url: http://backend:8080
|
backend_url: http://backend:8080
|
||||||
|
grpc_url: backend:9001
|
||||||
label: test-agent-1
|
label: test-agent-1
|
||||||
|
registration_token: "156616b56774d59ba53f1eb4b096488bb5f755bbf5b737d93a42bb1b583ad7fb"
|
||||||
|
cert_dir: /etc/hellreign-agent/certs
|
||||||
services:
|
services:
|
||||||
- service1
|
- name: system
|
||||||
- service2
|
type: journald
|
||||||
|
|||||||
@@ -3,3 +3,9 @@ database:
|
|||||||
clickhouse_host: clickhouse:9000
|
clickhouse_host: clickhouse:9000
|
||||||
clickhouse_user: default
|
clickhouse_user: default
|
||||||
clickhouse_password: testpassword
|
clickhouse_password: testpassword
|
||||||
|
clickhouse_database: hellreign
|
||||||
|
admin:
|
||||||
|
admin_name: Admin
|
||||||
|
admin_last_name: User
|
||||||
|
admin_login: admin
|
||||||
|
admin_password: admin123
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ clickhouse-client --query "CREATE DATABASE IF NOT EXISTS hellreign;"
|
|||||||
clickhouse-client --query "
|
clickhouse-client --query "
|
||||||
CREATE TABLE IF NOT EXISTS hellreign.logs (
|
CREATE TABLE IF NOT EXISTS hellreign.logs (
|
||||||
timestamp DateTime64(3) DEFAULT now(),
|
timestamp DateTime64(3) DEFAULT now(),
|
||||||
level String,
|
level LowCardinality(String),
|
||||||
service String,
|
service LowCardinality(String),
|
||||||
message String,
|
agent LowCardinality(String),
|
||||||
host String,
|
message String
|
||||||
trace_id String
|
|
||||||
) ENGINE = MergeTree()
|
) ENGINE = MergeTree()
|
||||||
ORDER BY (timestamp, service, level);
|
ORDER BY (timestamp, level, service, agent)
|
||||||
|
SETTINGS index_granularity = 8192;
|
||||||
"
|
"
|
||||||
|
|||||||
@@ -13,23 +13,34 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- clickhouse_data:/var/lib/clickhouse
|
- clickhouse_data:/var/lib/clickhouse
|
||||||
- ./clickhouse/init:/docker-entrypoint-initdb.d
|
- ./clickhouse/init:/docker-entrypoint-initdb.d
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8123/ping"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 20
|
||||||
|
start_period: 30s
|
||||||
networks:
|
networks:
|
||||||
- hellreign
|
- hellreign
|
||||||
|
|
||||||
backend:
|
backend:
|
||||||
build:
|
build:
|
||||||
context: ../backend
|
context: ..
|
||||||
dockerfile: dockerfile
|
dockerfile: backend/dockerfile
|
||||||
container_name: hellreign-backend
|
container_name: hellreign-backend
|
||||||
environment:
|
environment:
|
||||||
CONFIG_FILE: /etc/hellreign/config.yml
|
CONFIG_FILE: /etc/hellreign/config.yml
|
||||||
|
SSL_CERT_DIR: /var/lib/hellreign/ssl
|
||||||
|
SERVER_SAN_DNS: localhost,backend
|
||||||
|
SERVER_SAN_IP: 127.0.0.1
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
|
- "9001:9001"
|
||||||
volumes:
|
volumes:
|
||||||
- ./backend/config.yml:/etc/hellreign/config.yml:ro
|
- ./backend/config.yml:/etc/hellreign/config.yml:ro
|
||||||
- backend_data:/var/lib/hellreign
|
- backend_data:/var/lib/hellreign
|
||||||
depends_on:
|
depends_on:
|
||||||
- clickhouse
|
clickhouse:
|
||||||
|
condition: service_healthy
|
||||||
networks:
|
networks:
|
||||||
- hellreign
|
- hellreign
|
||||||
|
|
||||||
@@ -47,13 +58,18 @@ services:
|
|||||||
|
|
||||||
agent:
|
agent:
|
||||||
build:
|
build:
|
||||||
context: ../agent
|
context: ..
|
||||||
dockerfile: dockerfile
|
dockerfile: agent/dockerfile
|
||||||
container_name: hellreign-agent
|
container_name: hellreign-agent
|
||||||
environment:
|
environment:
|
||||||
CONFIG_FILE: /etc/hellreign-agent/config.yml
|
CONFIG_FILE: /etc/hellreign-agent/config.yml
|
||||||
|
JOURNALD_LOGDIR: /var/log/journal
|
||||||
|
BUFFER_DB: /var/lib/hellreign-agent/agent_buffer.db
|
||||||
volumes:
|
volumes:
|
||||||
- ./agent/config.yml:/etc/hellreign-agent/config.yml:ro
|
- ./agent/config.yml:/etc/hellreign-agent/config.yml:ro
|
||||||
|
- agent_certs:/etc/hellreign-agent/certs
|
||||||
|
- agent_data:/var/lib/hellreign-agent
|
||||||
|
- /var/log/journal:/var/log/journal:ro
|
||||||
depends_on:
|
depends_on:
|
||||||
- backend
|
- backend
|
||||||
networks:
|
networks:
|
||||||
@@ -64,6 +80,10 @@ volumes:
|
|||||||
driver: local
|
driver: local
|
||||||
backend_data:
|
backend_data:
|
||||||
driver: local
|
driver: local
|
||||||
|
agent_certs:
|
||||||
|
driver: local
|
||||||
|
agent_data:
|
||||||
|
driver: local
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
hellreign:
|
hellreign:
|
||||||
|
|||||||
Reference in New Issue
Block a user