46 Commits

Author SHA1 Message Date
zero@thinky 0660117c07 docs(backend): add swag to jobs
ci-agent / build (push) Failing after 2m56s
2026-04-04 16:51:18 +03:00
zero@thinky 9ede6257f8 feat(backend): use interpreters service for jobs handlers 2026-04-04 16:51:18 +03:00
zero@thinky f5b9b32a9f feat(backend): add script interpreters 2026-04-04 16:51:18 +03:00
zero@thinky e721cff3f8 refactor(agent): error handling 2026-04-04 16:51:18 +03:00
d3m0k1d 7e54d62170 fix: clickhouse logs and reg agents
ci-agent / build (push) Failing after 9m40s
2026-04-04 15:30:05 +03:00
d3m0k1d 477dd94227 chore: add logparser logic for agent and add parsed log to clickhouse
ci-agent / build (push) Failing after 3m30s
2026-04-04 06:29:07 +03:00
d3m0k1d c59d122e04 fix: conflict
ci-agent / build (push) Failing after 1m45s
2026-04-04 05:46:42 +03:00
d3m0k1d ad92439770 fix: mtls for agent, problems with auth 2026-04-04 05:45:37 +03:00
zero@thinky f1fc52bd6b fix(backend/commander): synchronize; add cleanup
ci-agent / build (push) Failing after 1m33s
2026-04-04 05:33:49 +03:00
zero@thinky 24cc11bc8d fix(agent): forgor argument
ci-agent / build (push) Failing after 1m22s
2026-04-04 05:24:44 +03:00
d3m0k1d 10d899b50f chore: add ansible deploy simple logic, upgrade admin auth logic and docs
ci-agent / build (push) Failing after 1m55s
2026-04-04 05:19:40 +03:00
d3m0k1d 2a8faaa9fe fix: dockerfile 2026-04-04 05:15:10 +03:00
zero@thinky c5e35b4c12 feat(backend): implement job storage; tie everything up
ci-agent / build (push) Failing after 1m55s
2026-04-04 05:11:09 +03:00
zero@thinky f578b6eb51 feat(agent): tie up 2026-04-04 05:11:09 +03:00
d3m0k1d a2c71da3a0 chore: grpc + mtls working
ci-agent / build (push) Failing after 1m19s
2026-04-04 03:55:37 +03:00
zero@thinky 28631865c8 refactor(agent): rename commander
ci-agent / build (push) Failing after 1m28s
2026-04-04 02:52:54 +03:00
zero@thinky edb1458806 feat(agent): add metadata to stream
ci-agent / build (push) Failing after 1m31s
2026-04-04 02:47:50 +03:00
zero@thinky ce73e915ca chore(agent): go mod tidy
ci-agent / build (push) Failing after 1m25s
2026-04-04 02:43:58 +03:00
zero@thinky baaa27005e feat(backend): add jobs http handler 2026-04-04 02:43:42 +03:00
zero@thinky 84807b9ba9 feat(backend): implement grpc commander, add job dispatcher 2026-04-04 02:43:42 +03:00
d3m0k1d b99f60c7e5 fix: dockerfiles and add generate certs script
ci-agent / build (push) Failing after 1m18s
2026-04-04 02:39:46 +03:00
d3m0k1d 6740dbb1b7 Merge pull request 'frontend' (#1) from frontend into backend
ci-agent / build (push) Failing after 1m27s
Reviewed-on: #1
2026-04-03 22:47:29 +00:00
d3m0k1d 5c67c0287e chore: add docker compose for local tests
ci-agent / build (push) Failing after 1m24s
2026-04-04 01:37:33 +03:00
d3m0k1d 8ab7fbc6b2 chore: add auth logic
ci-agent / build (push) Failing after 7m51s
2026-04-04 01:12:49 +03:00
zero@thinky d917a9e465 fix(proto): forgor to commit source
ci-agent / build (push) Failing after 1m21s
2026-04-04 01:05:44 +03:00
zero@thinky 82c6e1bb15 feat(agent): add client for commander server 2026-04-04 01:03:15 +03:00
zero@thinky 68f3174f08 chore(agent): update proto 2026-04-04 00:51:32 +03:00
zero@thinky 94be9799f4 fix(proto): oopsie
ci-agent / build (push) Failing after 1m23s
2026-04-04 00:48:37 +03:00
zero@thinky 3541fbdaae fix: modules
ci-agent / build (push) Failing after 1m55s
2026-04-04 00:34:28 +03:00
zero@thinky 81f3ba52cc chore: update go.work.sum 2026-04-04 00:34:27 +03:00
zero@thinky 8dee5ac823 fix(agent): fix import path, go.mod and go.sum 2026-04-04 00:34:13 +03:00
zero@thinky 980526c630 fix(proto): package path 2026-04-04 00:34:13 +03:00
zero@thinky eb193b1b95 feat(agent): init; add commander 2026-04-04 00:34:13 +03:00
d3m0k1d 514e3e30b6 chore: add admin to config
ci-agent / build (push) Failing after 25s
2026-04-03 23:51:03 +03:00
zero@thinky 94ff261c9a chore: add go work
ci-agent / build (push) Failing after 24s
2026-04-03 23:36:40 +03:00
zero@thinky a44630cfea feat(proto): init 2026-04-03 23:36:32 +03:00
d3m0k1d b69f2e4c9a chore: add migrations for sqlite
ci-agent / build (push) Failing after 22s
2026-04-03 23:30:42 +03:00
d3m0k1d d96f952d73 chore: add clickhouse as db for logs on agent and search
ci-agent / build (push) Failing after 22s
2026-04-03 23:23:43 +03:00
d3m0k1d 27e82f80f1 docs: add docs for agents list
ci-agent / build (push) Failing after 22s
2026-04-03 23:01:59 +03:00
d3m0k1d 83427193bc fix: code style
ci-agent / build (push) Failing after 24s
2026-04-03 22:50:07 +03:00
d3m0k1d 28ef2dc1fd chore: add sqlite init and config, add repository for sql
ci-agent / build (push) Failing after 26s
2026-04-03 22:48:31 +03:00
d3m0k1d 2ebf374413 chore: add linter
ci-agent / build (push) Failing after 22s
2026-04-03 21:07:32 +03:00
d3m0k1d 3293915062 chore: proj struct and swagger docs for backend
ci-agent / build (push) Failing after 28s
2026-04-03 21:04:49 +03:00
d3m0k1d 8913353e64 chore: add dockerfile and nginx conf for frontend
ci-agent / build (push) Failing after 25s
2026-04-03 19:43:27 +03:00
d3m0k1d 9992e254d5 chore: write dockerfiles for agent and backend 2026-04-03 19:43:13 +03:00
d3m0k1d b75d95f9a7 chore: init go mod files 2026-04-03 19:39:25 +03:00
69 changed files with 10821 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
go.work.sum
+24
View File
@@ -0,0 +1,24 @@
FROM golang:1.26.1 as builder
WORKDIR /app
COPY proto/ proto/
COPY agent/ agent/
WORKDIR /app/agent
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
go mod download && \
CGO_ENABLED=0 go build -ldflags "-s -w" -o /agent .
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
systemd \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY --from=builder /agent .
CMD ["./agent"]
+36
View File
@@ -0,0 +1,36 @@
module gitea.d3m0k1d.ru/d3m0k1d/HellreigN/agent
go 1.26.1
require (
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
google.golang.org/grpc v1.80.0
gopkg.in/yaml.v3 v3.0.1
modernc.org/sqlite v1.34.5
)
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/trace v1.41.0 // indirect
golang.org/x/net v0.52.0 // indirect
golang.org/x/sys v0.42.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/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
+93
View File
@@ -0,0 +1,93 @@
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/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/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
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/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/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/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c=
go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE=
go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ=
go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
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/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/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
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/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/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
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/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/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/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
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/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
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=
+161
View File
@@ -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, "", " ")
}
+81
View File
@@ -0,0 +1,81 @@
package client
import (
"context"
"errors"
"fmt"
"io"
"log"
"sync"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/agent/internal/commander"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/proto/proto"
"golang.org/x/sync/errgroup"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/metadata"
)
type CommanderClient struct {
cmder *commander.CommandExecutor
wg *sync.WaitGroup
id, label string
}
func New(
cmder *commander.CommandExecutor,
id, label string,
) CommanderClient {
return CommanderClient{cmder, new(sync.WaitGroup), id, label}
}
func (self *CommanderClient) HandleCommands(ctx context.Context, srvAddr string, tc credentials.TransportCredentials) error {
cli, err := grpc.NewClient(srvAddr, grpc.WithTransportCredentials(tc))
if err != nil {
return fmt.Errorf("Failed to connect to gRPC: %w", err)
}
ccli := proto.NewCommanderClient(cli)
bidi, err := ccli.Stream(metadata.NewOutgoingContext(ctx, metadata.MD{"agentid": []string{self.id}, "label": []string{self.label}}))
if err != nil {
return err
}
wg := new(errgroup.Group)
wg.Go(self.recv(bidi))
// wg.Go(self.send(bidi))
err = wg.Wait()
self.wg.Wait()
return err
}
func (self *CommanderClient) recv(bidi grpc.BidiStreamingClient[proto.FinishedCommand, proto.Command]) func() error {
return func() error {
for {
msg, err := bidi.Recv()
if err != nil {
if errors.Is(err, io.EOF) {
return nil
}
return err
}
self.wg.Go(func() {
func() error {
fc, err := self.cmder.Execute(msg)
if err != nil {
return err
}
return bidi.Send(fc)
}()
if err != nil {
log.Println(err)
}
})
}
}
}
// func (self *God) send(bidi grpc.BidiStreamingClient[proto.FinishedCommand, proto.Command]) func() error {
// return func() error {
// return nil
// }
// }
+65
View File
@@ -0,0 +1,65 @@
package commander
import (
"bytes"
"errors"
"io"
"os/exec"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/proto/proto"
"golang.org/x/sync/errgroup"
)
type CommandExecutor struct {
}
func (*CommandExecutor) Execute(command *proto.Command) (*proto.FinishedCommand, error) {
cmd := exec.Command(command.Command[0], command.Command[1:]...)
var (
stdin io.WriteCloser
err error
)
if command.Stdin != nil {
stdin, err = cmd.StdinPipe()
if err != nil {
return nil, err
}
}
stdout, err1 := cmd.StdoutPipe()
stderr, err2 := cmd.StderrPipe()
if err := errors.Join(err1, err2); err != nil {
return nil, err
}
if err := cmd.Start(); err != nil {
return nil, err
}
if command.Stdin != nil {
io.WriteString(stdin, *command.Stdin)
if err := stdin.Close(); err != nil {
return nil, err
}
}
eg := new(errgroup.Group)
stdoutbuf := new(bytes.Buffer)
stderrbuf := new(bytes.Buffer)
eg.Go(func() error {
_, err := io.Copy(stdoutbuf, stdout)
return err
})
eg.Go(func() error {
_, err := io.Copy(stderrbuf, stderr)
return err
})
if err := cmd.Wait(); err != nil {
return nil, err
}
if err := eg.Wait(); err != nil {
return nil, err
}
return &proto.FinishedCommand{
Id: command.Id,
Status: int32(cmd.ProcessState.ExitCode()),
Stdout: stdoutbuf.String(),
Stderr: stderrbuf.String(),
}, nil
}
+57
View File
@@ -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
}
+27
View File
@@ -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),
}
}
+45
View File
@@ -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()
}
+10
View File
@@ -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")
+55
View File
@@ -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
}
+49
View File
@@ -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)
}
+169
View File
@@ -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(&regResp); 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
}
+303
View File
@@ -0,0 +1,303 @@
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)
}
+21
View File
@@ -0,0 +1,21 @@
version: "2"
run:
timeout: 5m
tests: false
build-tags:
- integration
linters:
enable:
- errcheck
- errname
- govet
- staticcheck
- gosec
- nilerr
formatters:
enable:
- gofmt
- goimports
- golines
+302
View File
@@ -0,0 +1,302 @@
package main
import (
"context"
"crypto/tls"
"crypto/x509"
"log"
"net"
"os"
"time"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/docs"
"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/repository"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/service"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/storage"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/proto/proto"
"github.com/gin-gonic/gin"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
"golang.org/x/sync/errgroup"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)
// @securityDefinitions.apikey Bearer
// @in header
// @name Authorization
// @description Type "Bearer" followed by a space and the JWT token.
func main() {
cfg_path, ok := os.LookupEnv("CONFIG_FILE")
if !ok {
cfg_path = "/etc/hellreign/config.yml"
}
cfg, err := config.ImportSettings(cfg_path)
if err != nil {
log.Fatalf("Err loading config: %v", err)
}
db, err := storage.Open(cfg.Database.Token_db)
if err != nil {
log.Fatalf("Err opening database: %v", err)
}
defer db.Close()
h := handlers.New(db)
// 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}
agentReg := handlers.NewAgentRegistrationGroup(h)
agentDeploy := handlers.NewAgentDeployGroup(h)
// Create admin user from config if not exists
if cfg.Admin.Admin_login != "" && cfg.Admin.Admin_password != "" {
if !h.Repo.ExistsByLogin(cfg.Admin.Admin_login) {
_, err := h.Repo.CreateToken(repository.TokenCreate{
Name: cfg.Admin.Admin_name,
LastName: cfg.Admin.Admin_last_name,
Login: cfg.Admin.Admin_login,
Password: cfg.Admin.Admin_password,
PermissionView: true,
PermissionManage: true,
PermissionAdmin: true,
IsActive: true,
})
if err != nil {
log.Printf("Warning: failed to create admin user: %v", err)
} else {
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")
}
}
}
router := gin.Default()
docs.SwaggerInfo.BasePath = "/api/v1"
docs.SwaggerInfo.Title = "HellreigN"
docs.SwaggerInfo.Version = "1.0"
docs.SwaggerInfo.Description = "API for HellreigN"
docs.SwaggerInfo.Schemes = []string{"http"}
router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
v1 := router.Group("/api/v1")
{
// Auth routes (public)
authGroup := v1.Group("/auth")
{
authGroup.POST("/login", auth.Login)
}
// Auth token management (requires auth)
authTokenGroup := v1.Group("/auth")
authTokenGroup.Use(auth.AuthMiddleware())
{
authTokenGroup.POST("/token", handlers.RequireAdmin(), auth.CreateToken)
authTokenGroup.GET("/validate", auth.ValidateToken)
authTokenGroup.GET("/tokens", handlers.RequireAdmin(), auth.ListTokens)
authTokenGroup.DELETE("/token", auth.DeleteMyToken)
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)
agentsGroup := v1.Group("/agents")
agentsGroup.Use(auth.AuthMiddleware(), handlers.RequireManageAgent())
{
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)
logsGroup := v1.Group("/logs")
logsGroup.Use(auth.AuthMiddleware(), handlers.RequireView())
{
// Mock logs endpoint (always available, no ClickHouse required)
mockLogHandlers := handlers.NewLogHandlers(nil)
logsGroup.GET("/mock", mockLogHandlers.GetMockLogs)
// ClickHouse log handlers (always registered, work when ClickHouse connects)
logHandlers := handlers.NewLogHandlers(logRepo)
logsGroup.POST("", logHandlers.Insert)
logsGroup.POST("/batch", logHandlers.InsertBatch)
logsGroup.GET("", logHandlers.Search)
logsGroup.GET("/services", logHandlers.GetServices)
logsGroup.GET("/agents", logHandlers.GetAgents)
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)
}
}
// 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)
}
}
+24
View File
@@ -0,0 +1,24 @@
FROM golang:1.26.1 as builder
WORKDIR /app
COPY backend/ backend/
COPY proto/ proto/
WORKDIR /app/backend
ENV CGO_ENABLED=0
ENV GIN_MODE=release
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
go mod download && \
go build -ldflags "-s -w" -o backend ./cmd/main.go
FROM alpine:3.23.0
RUN apk add --no-cache curl openssl bash ansible
COPY --from=builder /app/backend/backend .
COPY --from=builder /app/backend/scripts /etc/hellreign/scripts
RUN chmod +x /etc/hellreign/scripts/generate-certs.sh
# Generate certificates on container start
ENTRYPOINT ["/bin/sh", "-c", "/etc/hellreign/scripts/generate-certs.sh ${SSL_CERT_DIR:-/var/lib/hellreign/ssl} && exec ./backend"]
+1575
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+84
View File
@@ -0,0 +1,84 @@
module gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend
go 1.26.1
require (
gitea.d3m0k1d.ru/d3m0k1d/HellreigN/proto v0.0.0-20260403210401-a6212c89fc0e
github.com/ClickHouse/clickhouse-go/v2 v2.44.0
github.com/gin-gonic/gin v1.12.0
github.com/swaggo/files v1.0.1
github.com/swaggo/gin-swagger v1.6.1
github.com/swaggo/swag v1.16.6
golang.org/x/crypto v0.49.0
golang.org/x/sync v0.20.0
google.golang.org/grpc v1.80.0
gopkg.in/yaml.v3 v3.0.1
modernc.org/sqlite v1.48.1
)
require (
github.com/ClickHouse/ch-go v0.71.0 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/bytedance/gopkg v0.1.4 // indirect
github.com/bytedance/sonic v1.15.0 // indirect
github.com/bytedance/sonic/loader v0.5.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
github.com/gin-contrib/sse v1.1.1 // indirect
github.com/go-faster/city v1.0.1 // indirect
github.com/go-faster/errors v0.7.1 // indirect
github.com/go-openapi/jsonpointer v0.22.5 // indirect
github.com/go-openapi/jsonreference v0.21.5 // indirect
github.com/go-openapi/spec v0.22.4 // indirect
github.com/go-openapi/swag/conv v0.25.5 // indirect
github.com/go-openapi/swag/jsonname v0.25.5 // indirect
github.com/go-openapi/swag/jsonutils v0.25.5 // indirect
github.com/go-openapi/swag/loading v0.25.5 // indirect
github.com/go-openapi/swag/stringutils v0.25.5 // indirect
github.com/go-openapi/swag/typeutils v0.25.5 // indirect
github.com/go-openapi/swag/yamlutils v0.25.5 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.30.2 // indirect
github.com/goccy/go-json v0.10.6 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.18.3 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/paulmach/orb v0.12.0 // indirect
github.com/pelletier/go-toml/v2 v2.3.0 // indirect
github.com/pierrec/lz4/v4 v4.1.25 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.59.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/segmentio/asm v1.2.1 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.1 // indirect
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
go.opentelemetry.io/otel v1.41.0 // indirect
go.opentelemetry.io/otel/trace v1.41.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/arch v0.25.0 // indirect
golang.org/x/mod v0.34.0 // indirect
golang.org/x/net v0.52.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.35.0 // indirect
golang.org/x/tools v0.43.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 // indirect
google.golang.org/protobuf v1.36.11 // indirect
modernc.org/libc v1.70.0 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
)
replace gitea.d3m0k1d.ru/d3m0k1d/HellreigN/proto => ../proto
+304
View File
@@ -0,0 +1,304 @@
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/clickhouse-go/v2 v2.44.0 h1:9pxs5pRwIvhni5BDRPn/n5A8DeUod5TnBaeulFBX8EQ=
github.com/ClickHouse/clickhouse-go/v2 v2.44.0/go.mod h1:giJfUVlMkcfUEPVfRpt51zZaGEx9i17gCos8gBl392c=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/bytedance/gopkg v0.1.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM=
github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4=
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
github.com/bytedance/sonic/loader v0.5.1 h1:Ygpfa9zwRCCKSlrp5bBP/b/Xzc3VxsAW+5NIYXrOOpI=
github.com/bytedance/sonic/loader v0.5.1/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
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/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
github.com/gin-contrib/sse v1.1.1 h1:uGYpNwTacv5R68bSGMapo62iLTRa9l5zxGCps4hK6ko=
github.com/gin-contrib/sse v1.1.1/go.mod h1:QXzuVkA0YO7o/gun03UI1Q+FTI8ZV/n5t03kIQAI89s=
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw=
github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw=
github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg=
github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo=
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/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA=
github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0=
github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE=
github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw=
github.com/go-openapi/spec v0.22.4 h1:4pxGjipMKu0FzFiu/DPwN3CTBRlVM2yLf/YTWorYfDQ=
github.com/go-openapi/spec v0.22.4/go.mod h1:WQ6Ai0VPWMZgMT4XySjlRIE6GP1bGQOtEThn3gcWLtQ=
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
github.com/go-openapi/swag/conv v0.25.5 h1:wAXBYEXJjoKwE5+vc9YHhpQOFj2JYBMF2DUi+tGu97g=
github.com/go-openapi/swag/conv v0.25.5/go.mod h1:CuJ1eWvh1c4ORKx7unQnFGyvBbNlRKbnRyAvDvzWA4k=
github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo=
github.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU=
github.com/go-openapi/swag/jsonutils v0.25.5 h1:XUZF8awQr75MXeC+/iaw5usY/iM7nXPDwdG3Jbl9vYo=
github.com/go-openapi/swag/jsonutils v0.25.5/go.mod h1:48FXUaz8YsDAA9s5AnaUvAmry1UcLcNVWUjY42XkrN4=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5 h1:SX6sE4FrGb4sEnnxbFL/25yZBb5Hcg1inLeErd86Y1U=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5/go.mod h1:/2KvOTrKWjVA5Xli3DZWdMCZDzz3uV/T7bXwrKWPquo=
github.com/go-openapi/swag/loading v0.25.5 h1:odQ/umlIZ1ZVRteI6ckSrvP6e2w9UTF5qgNdemJHjuU=
github.com/go-openapi/swag/loading v0.25.5/go.mod h1:I8A8RaaQ4DApxhPSWLNYWh9NvmX2YKMoB9nwvv6oW6g=
github.com/go-openapi/swag/stringutils v0.25.5 h1:NVkoDOA8YBgtAR/zvCx5rhJKtZF3IzXcDdwOsYzrB6M=
github.com/go-openapi/swag/stringutils v0.25.5/go.mod h1:PKK8EZdu4QJq8iezt17HM8RXnLAzY7gW0O1KKarrZII=
github.com/go-openapi/swag/typeutils v0.25.5 h1:EFJ+PCga2HfHGdo8s8VJXEVbeXRCYwzzr9u4rJk7L7E=
github.com/go-openapi/swag/typeutils v0.25.5/go.mod h1:itmFmScAYE1bSD8C4rS0W+0InZUBrB2xSPbWt6DLGuc=
github.com/go-openapi/swag/yamlutils v0.25.5 h1:kASCIS+oIeoc55j28T4o8KwlV2S4ZLPT6G0iq2SSbVQ=
github.com/go-openapi/swag/yamlutils v0.25.5/go.mod h1:Gek1/SjjfbYvM+Iq4QGwa/2lEXde9n2j4a3wI3pNuOQ=
github.com/go-openapi/testify/enable/yaml/v2 v2.4.0 h1:7SgOMTvJkM8yWrQlU8Jm18VeDPuAvB/xWrdxFJkoFag=
github.com/go-openapi/testify/enable/yaml/v2 v2.4.0/go.mod h1:14iV8jyyQlinc9StD7w1xVPW3CO3q1Gj04Jy//Kw4VM=
github.com/go-openapi/testify/v2 v2.4.0 h1:8nsPrHVCWkQ4p8h1EsRVymA2XABB4OT40gcvAu+voFM=
github.com/go-openapi/testify/v2 v2.4.0/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.30.2 h1:JiFIMtSSHb2/XBUbWM4i/MpeQm9ZK2xqPNk8vgvu5JQ=
github.com/go-playground/validator/v10 v10.30.2/go.mod h1:mAf2pIOVXjTEBrwUMGKkCWKKPs9NheYGabeB04txQSc=
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
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/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
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/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/paulmach/orb v0.12.0 h1:z+zOwjmG3MyEEqzv92UN49Lg1JFYx0L9GpGKNVDKk1s=
github.com/paulmach/orb v0.12.0/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU=
github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY=
github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM=
github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pierrec/lz4/v4 v4.1.25 h1:kocOqRffaIbU5djlIBr7Wh+cx82C0vtFb0fOurZHqD0=
github.com/pierrec/lz4/v4 v4.1.25/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
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/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
github.com/swaggo/gin-swagger v1.6.1 h1:Ri06G4gc9N4t4k8hekMigJ9zKTFSlqj/9paAQCQs7cY=
github.com/swaggo/gin-swagger v1.6.1/go.mod h1:LQ+hJStHakCWRiK/YNYtJOu4mR2FP+pxLnILT/qNiTw=
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g=
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
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/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c=
go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE=
go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ=
go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
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/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/arch v0.25.0 h1:qnk6Ksugpi5Bz32947rkUgDt9/s5qvqDPl/gBKdMJLE=
golang.org/x/arch v0.25.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
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/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
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/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
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/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
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/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
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/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
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.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.32.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw=
modernc.org/ccgo/v4 v4.32.0/go.mod h1:6F08EBCx5uQc38kMGl+0Nm0oWczoo1c7cgpzEry7Uc0=
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.70.0 h1:U58NawXqXbgpZ/dcdS9kMshu08aiA6b7gusEusqzNkw=
modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.48.1 h1:S85iToyU6cgeojybE2XJlSbcsvcWkQ6qqNXJHtW5hWA=
modernc.org/sqlite v1.48.1/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
+135
View File
@@ -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)
}
+62
View File
@@ -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
}
+136
View File
@@ -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
`
+5
View File
@@ -0,0 +1,5 @@
package ansible
const BaseInvTemplate = `
`
+22
View File
@@ -0,0 +1,22 @@
package config
import (
"fmt"
"os"
"gopkg.in/yaml.v3"
)
func ImportSettings(path string) (*HellreigN, error) {
data, err := os.ReadFile(path)
if err != nil {
fmt.Println(err)
}
var cfg HellreigN
if err := yaml.Unmarshal(data, &cfg); err != nil {
return nil, err
}
return &cfg, nil
}
+20
View File
@@ -0,0 +1,20 @@
package config
type HellreigN struct {
Database Databases `yaml:"database"`
Admin Admin `yaml:"admin"`
}
type Databases struct {
Token_db string `yaml:"token_db"`
Clickhouse_host string `yaml:"clickhouse_host"`
Clickhouse_user string `yaml:"clickhouse_user"`
Clickhouse_password string `yaml:"clickhouse_password"`
Clickhouse_database string `yaml:"clickhouse_database"`
}
type Admin struct {
Admin_name string `yaml:"admin_name"`
Admin_last_name string `yaml:"admin_last_name"`
Admin_login string `yaml:"admin_login"`
Admin_password string `yaml:"admin_password"`
}
@@ -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
}
@@ -0,0 +1,182 @@
package commander
import (
"context"
"fmt"
"io"
"sync"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/models"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/proto/proto"
"golang.org/x/sync/errgroup"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
)
type Commander struct {
proto.UnimplementedCommanderServer
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 {
bidi grpc.BidiStreamingServer[proto.FinishedCommand, proto.Command]
in chan *proto.Command
jobs map[int64]Job
jobber Jobber
ctx context.Context
aid string
Token string // agent id
Label string
Services []string
}
type JobOut struct {
fc models.Job
err error
}
type Job struct {
out chan JobOut
}
func (self *Commander) GetAgent(aid string) (agent Agent, ok bool) {
self.mu.RLock()
defer self.mu.RUnlock()
agent, ok = self.agents[aid]
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) {
jid, err := self.jobber.InitJob(self.ctx, self.aid, job)
if err != nil {
return 0, err
}
self.in <- &proto.Command{
Id: jid,
Command: job.Command,
Stdin: job.Stdin,
}
return jid, err
}
func (self *Agent) WaitJob(jid int64) (*models.Job, error) {
result := <-self.jobs[jid].out
return &result.fc, result.err
}
func (self *Commander) Stream(bidi grpc.BidiStreamingServer[proto.FinishedCommand, proto.Command]) error {
md, ok := metadata.FromIncomingContext(bidi.Context())
if !ok {
return fmt.Errorf("no metadata in context")
}
aidVals := md["agentid"]
if len(aidVals) == 0 {
return fmt.Errorf("agentid metadata missing")
}
aid := aidVals[0]
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.mu.Unlock()
defer self.removeAgent(aid)
return agent.run()
}
func (self *Agent) run() error {
wg := new(errgroup.Group)
wg.Go(self.recv)
wg.Go(self.send)
return wg.Wait()
}
func (self *Agent) recv() error {
for {
job, err := func() (job models.Job, err error) {
msg, err := self.bidi.Recv()
if err != nil {
return
}
return self.jobber.UpdateJobInDB(self.ctx, msg.Id, models.JobForUpdate{
Stdout: msg.Stdout,
Stderr: msg.Stderr,
Status: msg.Status,
})
}()
if err == io.EOF {
return nil
}
// TODO: that would blow up at some point
out := self.jobs[job.ID].out
out <- JobOut{
fc: job,
err: err,
}
close(out)
}
}
func (self *Agent) send() error {
for job := range self.in {
self.jobs[job.Id] = newJob()
if err := self.bidi.Send(job); err != nil {
return err
}
}
return io.EOF
// self.jobs[]
}
func newAgent(bidi grpc.BidiStreamingServer[proto.FinishedCommand, proto.Command], jobber Jobber, aid string, label string) Agent {
return Agent{
bidi: bidi,
in: make(chan *proto.Command),
jobs: make(map[int64]Job),
jobber: jobber,
ctx: bidi.Context(),
aid: aid,
Label: label,
Token: aid,
}
}
func newJob() Job {
return Job{make(chan JobOut, 1)}
}
+172
View File
@@ -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,
})
}
+121
View File
@@ -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"
}
+45
View File
@@ -0,0 +1,45 @@
package handlers
import (
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/grpcsrv/collector"
"github.com/gin-gonic/gin"
"net/http"
)
type AgentsGroup struct {
*Handlers
collector *collector.Collector
}
func NewAgentsGroup(h *Handlers, coll *collector.Collector) AgentsGroup {
return AgentsGroup{Handlers: h, collector: coll}
}
type AgentInfo struct {
Token string `json:"token"`
Label string `json:"label"`
Services []string `json:"services"`
ConnectedAt string `json:"connected_at"`
}
// @Summary Get connected agents
// @Description Returns a list of all agents currently connected via Collector (log streaming)
// @Tags agents
// @Security Bearer
// @Produce json
// @Success 200 {array} AgentInfo
// @Router /agents [get]
func (ag *AgentsGroup) List(c *gin.Context) {
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)
}
+404
View File
@@ -0,0 +1,404 @@
package handlers
import (
"errors"
"net/http"
"strings"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/repository"
"github.com/gin-gonic/gin"
)
// AuthGroup handles authentication routes.
type AuthGroup struct {
*Handlers
}
// Login authenticates a user by login and password, returns a token.
// @Summary Login
// @Description Authenticate with login and password, returns a token and permissions
// @Tags auth
// @Accept json
// @Param request body repository.LoginRequest true "Login credentials"
// @Success 200 {object} repository.LoginResponse
// @Failure 400 {object} map[string]string
// @Failure 401 {object} map[string]string
// @Failure 403 {object} map[string]string
// @Router /auth/login [post]
func (ag *AuthGroup) Login(c *gin.Context) {
var req repository.LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
resp, err := ag.Repo.Login(req.Login, req.Password)
if err != nil {
if errors.Is(err, repository.ErrNotFound) {
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid credentials"})
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"})
return
}
c.JSON(http.StatusOK, resp)
}
// CreateToken creates a new user.
// @Summary Create user
// @Description Creates a new user with permissions
// @Tags auth
// @Accept json
// @Param request body repository.TokenCreate true "User data"
// @Success 200 {object} map[string]string
// @Failure 400 {object} map[string]string
// @Failure 401 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /auth/token [post]
func (ag *AuthGroup) CreateToken(c *gin.Context) {
var tc repository.TokenCreate
if err := c.ShouldBindJSON(&tc); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
if _, err := ag.Repo.CreateToken(tc); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create user"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "user created"})
}
// ValidateToken validates the current Bearer token and returns user info.
// @Summary Validate token
// @Description Check if the provided Bearer token is valid and return its permissions
// @Tags auth
// @Produce json
// @Success 200 {object} repository.Tokens
// @Failure 401 {object} map[string]string
// @Router /auth/validate [get]
func (ag *AuthGroup) ValidateToken(c *gin.Context) {
tokenVal, exists := c.Get(string(tokenContextKey))
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "not authenticated"})
return
}
token, ok := tokenVal.(*repository.Tokens)
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid token context"})
return
}
c.JSON(http.StatusOK, token)
}
// ListTokens returns all users.
// @Summary List users
// @Description Returns list of all users with their permissions
// @Tags auth
// @Produce json
// @Success 200 {array} repository.Tokens
// @Failure 500 {object} map[string]string
// @Router /auth/tokens [get]
func (ag *AuthGroup) ListTokens(c *gin.Context) {
tokens, err := ag.Repo.ListTokens()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list users"})
return
}
c.JSON(http.StatusOK, tokens)
}
// DeleteToken deletes a user by login from URL path.
// @Summary Delete user
// @Description Deletes a user by their login
// @Tags auth
// @Param login path string true "Login of the user to delete"
// @Success 200 {object} map[string]string
// @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /auth/tokens/:login [delete]
func (ag *AuthGroup) DeleteToken(c *gin.Context) {
login := c.Param("login")
if login == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "login required"})
return
}
if err := ag.Repo.DeleteTokenByLogin(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 delete user"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "user deleted"})
}
// DeleteMyToken deletes the current user's account.
// @Summary Delete my account
// @Description Deletes the current authenticated user
// @Tags auth
// @Success 200 {object} map[string]string
// @Failure 401 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /auth/token [delete]
func (ag *AuthGroup) DeleteMyToken(c *gin.Context) {
tokenVal, exists := c.Get(string(tokenContextKey))
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "not authenticated"})
return
}
token, ok := tokenVal.(*repository.Tokens)
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid token context"})
return
}
if err := ag.Repo.DeleteToken(token.Token); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete account"})
return
}
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.
func getTokenFromHeader(c *gin.Context) string {
auth := c.GetHeader("Authorization")
if auth == "" {
return ""
}
parts := strings.SplitN(auth, " ", 2)
if len(parts) != 2 || !strings.EqualFold(parts[0], "bearer") {
return ""
}
return parts[1]
}
+19
View File
@@ -0,0 +1,19 @@
package handlers
import (
"database/sql"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/repository"
)
type Handlers struct {
DB *sql.DB
Repo *repository.Repository
}
func New(db *sql.DB) *Handlers {
return &Handlers{
DB: db,
Repo: repository.New(db),
}
}
+93
View File
@@ -0,0 +1,93 @@
package handlers
import (
"fmt"
"net/http"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/grpcsrv/commander"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/models"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/service"
"github.com/gin-gonic/gin"
)
type JobsHandlers struct {
cmder *commander.Commander
svc *service.ScriptService
}
func NewJobsHandlers(cmder *commander.Commander, svc *service.ScriptService) JobsHandlers {
return JobsHandlers{cmder: cmder, svc: svc}
}
type AddJobIn struct {
Command string `json:"command" binding:"required"`
InterpreterID int64 `json:"interpreter_id"`
Stdin *string `json:"stdin"`
AgentID string `json:"agent_id" binding:"required"`
}
type AddJobOut 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"`
}
// 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) {
err := func() error {
var in AddJobIn
if err := c.Bind(&in); err != nil {
return err
}
agent, ok := self.cmder.GetAgent(in.AgentID)
if !ok {
c.Status(http.StatusNotFound)
return fmt.Errorf("agent not found")
}
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 {
return err
}
}
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
}
c.JSON(http.StatusCreated, AddJobOut{
ID: job.ID,
Command: job.Command,
Stdin: job.Stdin,
Stdout: job.Stdout,
Stderr: job.Stderr,
Status: job.Status,
})
return nil
}()
if err != nil {
c.Error(err)
}
}
+235
View File
@@ -0,0 +1,235 @@
package handlers
import (
"context"
"net/http"
"time"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/repository"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/storage"
"github.com/gin-gonic/gin"
)
type LogHandlers struct {
LogRepo *repository.LogRepository
}
func NewLogHandlers(logRepo *repository.LogRepository) *LogHandlers {
return &LogHandlers{LogRepo: logRepo}
}
type InsertLogRequest struct {
Timestamp time.Time `json:"timestamp"`
Level string `json:"level" binding:"required"`
Service string `json:"service" binding:"required"`
Agent string `json:"agent" binding:"required"`
Message string `json:"message" binding:"required"`
}
// @Summary Insert log entry
// @Description Inserts a single log entry into ClickHouse
// @Tags logs
// @Accept json
// @Produce json
// @Param body body InsertLogRequest true "Log entry"
// @Success 201 {object} map[string]string
// @Security Bearer
// @Router /logs [post]
func (lh *LogHandlers) Insert(c *gin.Context) {
var req InsertLogRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if req.Timestamp.IsZero() {
req.Timestamp = time.Now()
}
log := storage.LogEntry{
Timestamp: req.Timestamp,
Level: req.Level,
Service: req.Service,
Agent: req.Agent,
Message: req.Message,
}
if err := lh.LogRepo.Insert(c.Request.Context(), log); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to insert log"})
return
}
c.JSON(http.StatusCreated, gin.H{"status": "ok"})
}
type InsertLogsRequest struct {
Logs []InsertLogRequest `json:"logs" binding:"required"`
}
// @Summary Insert log entries (batch)
// @Description Inserts multiple log entries into ClickHouse
// @Tags logs
// @Accept json
// @Produce json
// @Param body body InsertLogsRequest true "Log entries"
// @Success 201 {object} map[string]string
// @Security Bearer
// @Router /logs/batch [post]
func (lh *LogHandlers) InsertBatch(c *gin.Context) {
var req InsertLogsRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
logs := make([]storage.LogEntry, len(req.Logs))
for i, l := range req.Logs {
if l.Timestamp.IsZero() {
l.Timestamp = time.Now()
}
logs[i] = storage.LogEntry{
Timestamp: l.Timestamp,
Level: l.Level,
Service: l.Service,
Agent: l.Agent,
Message: l.Message,
}
}
if err := lh.LogRepo.InsertBatch(c.Request.Context(), logs); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to insert logs"})
return
}
c.JSON(http.StatusCreated, gin.H{"status": "ok", "count": len(logs)})
}
type SearchLogsRequest struct {
Level string `form:"level"`
Service string `form:"service"`
Agent string `form:"agent"`
DateFrom string `form:"date_from"`
DateTo string `form:"date_to"`
Limit int `form:"limit"`
Offset int `form:"offset"`
}
// @Summary Search logs
// @Description Searches logs with various filters
// @Tags logs
// @Produce json
// @Param level query string false "Log level (INFO, WARNING, ERROR, FATAL)"
// @Param service query string false "Service name"
// @Param agent query string false "Agent name"
// @Param date_from query string false "Date from (RFC3339)"
// @Param date_to query string false "Date to (RFC3339)"
// @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 [get]
func (lh *LogHandlers) Search(c *gin.Context) {
var req SearchLogsRequest
if err := c.ShouldBindQuery(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
filter := repository.LogFilter{
Level: req.Level,
Service: req.Service,
Agent: req.Agent,
Limit: req.Limit,
Offset: req.Offset,
}
if req.DateFrom != "" {
if t, err := time.Parse(time.RFC3339, req.DateFrom); err == nil {
filter.DateFrom = t
}
}
if req.DateTo != "" {
if t, err := time.Parse(time.RFC3339, req.DateTo); err == nil {
filter.DateTo = t
}
}
if filter.Limit <= 0 {
filter.Limit = 100
}
logs, err := lh.LogRepo.Search(c.Request.Context(), filter)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to search logs"})
return
}
c.JSON(http.StatusOK, logs)
}
// @Summary Get distinct services
// @Description Returns list of all unique service names in logs
// @Tags logs
// @Produce json
// @Success 200 {array} string
// @Security Bearer
// @Router /logs/services [get]
func (lh *LogHandlers) GetServices(c *gin.Context) {
services, err := lh.LogRepo.GetDistinctServices(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get services"})
return
}
if services == nil {
services = []string{}
}
c.JSON(http.StatusOK, services)
}
// @Summary Get distinct agents
// @Description Returns list of all unique agent names in logs
// @Tags logs
// @Produce json
// @Success 200 {array} string
// @Security Bearer
// @Router /logs/agents [get]
func (lh *LogHandlers) GetAgents(c *gin.Context) {
agents, err := lh.LogRepo.GetDistinctAgents(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get agents"})
return
}
if agents == nil {
agents = []string{}
}
c.JSON(http.StatusOK, agents)
}
// @Summary Get distinct log levels
// @Description Returns list of all unique log levels in logs
// @Tags logs
// @Produce json
// @Success 200 {array} string
// @Security Bearer
// @Router /logs/levels [get]
func (lh *LogHandlers) GetLevels(c *gin.Context) {
levels, err := lh.LogRepo.GetDistinctLevels(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get levels"})
return
}
if levels == nil {
levels = []string{}
}
c.JSON(http.StatusOK, levels)
}
// Ensure context is used
var _ = context.Background
+203
View File
@@ -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]
}
+86
View File
@@ -0,0 +1,86 @@
package handlers
import (
"net/http"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/repository"
"github.com/gin-gonic/gin"
)
// TokenContextKey is the context key for storing authenticated token info.
type TokenContextKey string
const tokenContextKey TokenContextKey = "token"
// AuthMiddleware validates that a Bearer token exists and is valid.
// It stores the token info in the context for later use.
// Returns 401 if token is missing or invalid.
func (ag *AuthGroup) AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
token := getTokenFromHeader(c)
if token == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "missing authorization header"})
c.Abort()
return
}
// Look up user by token value
tokens, err := ag.Repo.GetToken(token)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
c.Abort()
return
}
c.Set(string(tokenContextKey), tokens)
c.Next()
}
}
// RequirePermission is a generic permission checker.
func RequirePermission(check func(*repository.Tokens) bool) gin.HandlerFunc {
return func(c *gin.Context) {
tokenVal, exists := c.Get(string(tokenContextKey))
if !exists {
c.JSON(http.StatusForbidden, gin.H{"error": "authentication required"})
c.Abort()
return
}
token, ok := tokenVal.(*repository.Tokens)
if !ok {
c.JSON(http.StatusForbidden, gin.H{"error": "invalid token context"})
c.Abort()
return
}
if !check(token) {
c.JSON(http.StatusForbidden, gin.H{"error": "insufficient permissions"})
c.Abort()
return
}
c.Next()
}
}
// RequireView requires permission_view.
func RequireView() gin.HandlerFunc {
return RequirePermission(func(t *repository.Tokens) bool {
return t.PermissionView
})
}
// RequireManageAgent requires permission_manage_agent.
func RequireManageAgent() gin.HandlerFunc {
return RequirePermission(func(t *repository.Tokens) bool {
return t.PermissionManage
})
}
// RequireAdmin requires permission_admin.
func RequireAdmin() gin.HandlerFunc {
return RequirePermission(func(t *repository.Tokens) bool {
return t.PermissionAdmin
})
}
+206
View File
@@ -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)
}
+17
View File
@@ -0,0 +1,17 @@
package models
type Job struct {
ID int64
JobForInsert
JobForUpdate
}
type JobForInsert struct {
Command []string
Stdin *string
}
type JobForUpdate struct {
Stdout string
Stderr string
Status int32
}
@@ -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
}
@@ -0,0 +1,236 @@
package repository
import (
"context"
"database/sql"
"fmt"
"sync"
"time"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/storage"
)
type LogRepository struct {
mu sync.RWMutex
DB *sql.DB
}
func NewLogRepository() *LogRepository {
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 {
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 {
db := r.getDB()
if db == nil {
return nil
}
_, err := db.ExecContext(ctx, `
INSERT INTO logs (timestamp, level, service, agent, message)
VALUES ($1, $2, $3, $4, $5)
`, log.Timestamp, log.Level, log.Service, log.Agent, log.Message)
return err
}
func (r *LogRepository) InsertBatch(ctx context.Context, logs []storage.LogEntry) error {
db := r.getDB()
if db == nil {
return nil
}
if len(logs) == 0 {
return nil
}
// Build multi-row INSERT statement
query := "INSERT INTO logs (timestamp, level, service, agent, message) VALUES "
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)
}
_, err := db.ExecContext(ctx, query, args...)
return err
}
type LogFilter struct {
Level string
Service string
Agent string
DateFrom time.Time
DateTo time.Time
Limit int
Offset int
}
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"
args := make([]interface{}, 0)
argIdx := 1
if filter.Level != "" {
query += " AND level = $" + string(rune('0'+argIdx))
args = append(args, filter.Level)
argIdx++
}
if filter.Service != "" {
query += " AND service = $" + string(rune('0'+argIdx))
args = append(args, filter.Service)
argIdx++
}
if filter.Agent != "" {
query += " AND agent = $" + string(rune('0'+argIdx))
args = append(args, filter.Agent)
argIdx++
}
if !filter.DateFrom.IsZero() {
query += " AND timestamp >= $" + string(rune('0'+argIdx))
args = append(args, filter.DateFrom)
argIdx++
}
if !filter.DateTo.IsZero() {
query += " AND timestamp <= $" + string(rune('0'+argIdx))
args = append(args, filter.DateTo)
argIdx++
}
query += " ORDER BY timestamp DESC"
if filter.Limit > 0 {
query += " LIMIT $" + string(rune('0'+argIdx))
args = append(args, filter.Limit)
argIdx++
} else {
query += " LIMIT 100"
}
if filter.Offset > 0 {
query += " OFFSET $" + string(rune('0'+argIdx))
args = append(args, filter.Offset)
}
rows, err := db.QueryContext(ctx, query, args...)
if err != nil {
return nil, err
}
defer rows.Close()
logs := make([]storage.LogEntry, 0)
for rows.Next() {
var log storage.LogEntry
if err := rows.Scan(&log.Timestamp, &log.Level, &log.Service, &log.Agent, &log.Message); err != nil {
return nil, err
}
logs = append(logs, log)
}
return logs, rows.Err()
}
func (r *LogRepository) GetDistinctServices(ctx context.Context) ([]string, error) {
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 {
return nil, err
}
defer rows.Close()
services := make([]string, 0)
for rows.Next() {
var service string
if err := rows.Scan(&service); err != nil {
return nil, err
}
services = append(services, service)
}
return services, rows.Err()
}
func (r *LogRepository) GetDistinctAgents(ctx context.Context) ([]string, error) {
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 {
return nil, err
}
defer rows.Close()
agents := make([]string, 0)
for rows.Next() {
var agent string
if err := rows.Scan(&agent); err != nil {
return nil, err
}
agents = append(agents, agent)
}
return agents, rows.Err()
}
func (r *LogRepository) GetDistinctLevels(ctx context.Context) ([]string, error) {
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 {
return nil, err
}
defer rows.Close()
levels := make([]string, 0)
for rows.Next() {
var level string
if err := rows.Scan(&level); err != nil {
return nil, err
}
levels = append(levels, level)
}
return levels, rows.Err()
}
+143
View File
@@ -0,0 +1,143 @@
package repository
// Tokens represents a user record with info and permissions.
type Tokens struct {
ID int64 `json:"id"`
Name string `json:"name"`
LastName string `json:"last_name"`
Login string `json:"login"`
Token string `json:"token"`
PermissionView bool `json:"permission_view"`
PermissionManage bool `json:"permission_manage_agent"`
PermissionAdmin bool `json:"permission_admin"`
IsActive bool `json:"is_active"`
}
// TokenCreate is the request body for creating a new user.
type TokenCreate struct {
Name string `json:"name" binding:"required"`
LastName string `json:"last_name" binding:"required"`
Login string `json:"login" binding:"required"`
Password string `json:"password" binding:"required"`
PermissionView bool `json:"permission_view"`
PermissionManage bool `json:"permission_manage_agent"`
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.
type LoginRequest struct {
Login string `json:"login" binding:"required"`
Password string `json:"password" binding:"required"`
}
// LoginResponse is returned after successful login.
type LoginResponse struct {
Token string `json:"token"`
Name string `json:"name"`
LastName string `json:"last_name"`
Login string `json:"login"`
PermissionView bool `json:"permission_view"`
PermissionManage bool `json:"permission_manage_agent"`
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"`
}
+462
View File
@@ -0,0 +1,462 @@
package repository
import (
"database/sql"
"errors"
"strconv"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/storage"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/utils"
"golang.org/x/crypto/bcrypt"
)
// Repository wraps a SQLite database connection.
type Repository struct {
DB *sql.DB
}
// New creates a new Repository.
func New(db *sql.DB) *Repository {
return &Repository{DB: db}
}
var ErrNotFound = errors.New("not found")
var ErrAccountInactive = errors.New("account is not activated")
// Init creates the tokens table if it does not exist.
func (r *Repository) Init() error {
_, err := r.DB.Exec(storage.CreateSqlite)
if err != nil {
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.
// New users are created with is_active=false by default.
func (r *Repository) CreateToken(tc TokenCreate) (string, error) {
hashed, err := bcrypt.GenerateFromPassword([]byte(tc.Password), bcrypt.DefaultCost)
if err != nil {
return "", err
}
token, err := utils.RandomToken()
if err != nil {
return "", err
}
result, err := r.DB.Exec(
`INSERT INTO tokens (name, last_name, login, password, token, permission_view, permission_manage_agent, permission_admin, is_active)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
tc.Name, tc.LastName, tc.Login, string(hashed), token,
tc.PermissionView, tc.PermissionManage, tc.PermissionAdmin, tc.IsActive,
)
if err != nil {
return "", err
}
id, err := result.LastInsertId()
if err != nil {
return "", err
}
return strconv.FormatInt(id, 10), nil
}
// Login authenticates by login/password, generates a new token, and returns LoginResponse.
func (r *Repository) Login(login, password string) (*LoginResponse, error) {
var t Tokens
var hashedPassword string
err := r.DB.QueryRow(
`SELECT id, name, last_name, login, password, token, permission_view, permission_manage_agent, permission_admin, is_active
FROM tokens WHERE login = ?`,
login,
).Scan(&t.ID, &t.Name, &t.LastName, &t.Login, &hashedPassword, &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
}
if err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)); err != nil {
return nil, ErrNotFound
}
if !t.IsActive {
return nil, ErrAccountInactive
}
// Generate new token on each login
newToken, err := utils.RandomToken()
if err != nil {
return nil, err
}
_, err = r.DB.Exec(`UPDATE tokens SET token = ? WHERE id = ?`, newToken, t.ID)
if err != nil {
return nil, err
}
return &LoginResponse{
Token: newToken,
Name: t.Name,
LastName: t.LastName,
Login: t.Login,
PermissionView: t.PermissionView,
PermissionManage: t.PermissionManage,
PermissionAdmin: t.PermissionAdmin,
IsActive: t.IsActive,
}, nil
}
// GetTokenByToken retrieves a user record by token value.
func (r *Repository) GetToken(token string) (*Tokens, error) {
var t Tokens
err := r.DB.QueryRow(
`SELECT id, name, last_name, login, token, permission_view, permission_manage_agent, permission_admin
FROM tokens WHERE token = ?`,
token,
).Scan(&t.ID, &t.Name, &t.LastName, &t.Login, &t.Token,
&t.PermissionView, &t.PermissionManage, &t.PermissionAdmin)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
}
return nil, err
}
return &t, nil
}
// ListTokens returns all users without password and token.
func (r *Repository) ListTokens() ([]Tokens, error) {
rows, err := r.DB.Query(
`SELECT id, name, last_name, login, permission_view, permission_manage_agent, permission_admin
FROM tokens`,
)
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.PermissionView, &t.PermissionManage, &t.PermissionAdmin); err != nil {
return nil, err
}
tokens = append(tokens, t)
}
return tokens, rows.Err()
}
// DeleteToken deletes a user by token value.
func (r *Repository) DeleteToken(token string) error {
result, err := r.DB.Exec(`DELETE FROM tokens WHERE token = ?`, token)
if err != nil {
return err
}
affected, err := result.RowsAffected()
if err != nil {
return err
}
if affected == 0 {
return ErrNotFound
}
return nil
}
// DeleteTokenByLogin deletes a user by login.
func (r *Repository) DeleteTokenByLogin(login string) error {
result, err := r.DB.Exec(`DELETE FROM tokens WHERE login = ?`, login)
if err != nil {
return err
}
affected, err := result.RowsAffected()
if err != nil {
return err
}
if affected == 0 {
return ErrNotFound
}
return nil
}
// ExistsByLogin checks if a user with given login exists.
func (r *Repository) ExistsByLogin(login string) bool {
var count int
err := r.DB.QueryRow(`SELECT COUNT(*) FROM tokens WHERE login = ?`, login).Scan(&count)
if err != nil {
return false
}
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)
}
+62
View File
@@ -0,0 +1,62 @@
package storage
import (
"context"
"database/sql"
"fmt"
"log"
"time"
_ "github.com/ClickHouse/clickhouse-go/v2"
)
type ClickHouseConfig struct {
Host string
User string
Password string
Database string
}
func OpenClickHouse(cfg ClickHouseConfig) (*sql.DB, error) {
dsn := fmt.Sprintf("clickhouse://%s:%s@%s/%s",
cfg.User, cfg.Password, cfg.Host, cfg.Database)
db, err := sql.Open("clickhouse", dsn)
if err != nil {
return nil, fmt.Errorf("clickhouse open: %w", err)
}
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)
}
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)
}
+11
View File
@@ -0,0 +1,11 @@
package storage
import "time"
type LogEntry struct {
Timestamp time.Time `ch:"timestamp"`
Level string `ch:"level"`
Service string `ch:"service"`
Agent string `ch:"agent"`
Message string `ch:"message"`
}
+71
View File
@@ -0,0 +1,71 @@
package storage
const CreateSqlite = `
CREATE TABLE IF NOT EXISTS tokens (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
last_name TEXT NOT NULL,
login TEXT NOT NULL UNIQUE,
password TEXT NOT NULL,
token TEXT NOT NULL UNIQUE,
permission_view BOOL NOT NULL,
permission_manage_agent 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
);
`
const CreateLogsTable = `
CREATE TABLE IF NOT EXISTS logs (
timestamp DateTime64(3) DEFAULT now(),
level LowCardinality(String),
service LowCardinality(String),
agent LowCardinality(String),
message String
) ENGINE = MergeTree()
ORDER BY (timestamp, level, service, agent)
TTL timestamp + INTERVAL 30 DAY
SETTINGS index_granularity = 8192
`
+43
View File
@@ -0,0 +1,43 @@
package storage
import (
"database/sql"
"fmt"
"strings"
_ "modernc.org/sqlite"
)
var pragmas = map[string]string{
`journal_mode`: `wal`,
`synchronous`: `normal`,
`busy_timeout`: `30000`,
}
func buildSqliteDsn(path string, pragmas map[string]string) string {
pragmastrs := make([]string, len(pragmas))
i := 0
for k, v := range pragmas {
pragmastrs[i] = (fmt.Sprintf(`pragma=%s(%s)`, k, v))
i++
}
return path + "?" + "mode=rwc&" + strings.Join(pragmastrs, "&")
}
func Open(path string) (*sql.DB, error) {
dsn := buildSqliteDsn(path, pragmas)
db, err := sql.Open("sqlite", dsn)
if err != nil {
return nil, err
}
// Run migrations
if _, err := db.Exec(CreateSqlite); err != nil {
return nil, fmt.Errorf("migrate: %w", err)
}
// Migration: add is_active column if it doesn't exist
_, _ = db.Exec(AddIsActiveColumn)
return db, nil
}
+157
View File
@@ -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
}
+14
View File
@@ -0,0 +1,14 @@
package utils
import (
"crypto/rand"
"encoding/hex"
)
func RandomToken() (string, error) {
token := make([]byte, 32)
if _, err := rand.Read(token); err != nil {
return "", err
}
return hex.EncodeToString(token), nil
}
+6
View File
@@ -0,0 +1,6 @@
.PHONY: docs lint
docs:
swag init -g ./cmd/main.go --parseDependency --parseInternal
lint:
golangci-lint run --fix
+98
View File
@@ -0,0 +1,98 @@
#!/bin/bash
# Скрипт генерации SSL сертификатов для mTLS gRPC
set -e
CERT_DIR="${1:-/etc/HellreigN/ssl}"
DAYS_VALID=365
echo "Generating CA and server certificates in ${CERT_DIR}..."
# Создаём директорию
mkdir -p "${CERT_DIR}"
# Если сертификаты уже есть и не пустые - не перегенерируем
if [ -s "${CERT_DIR}/ca.crt" ] && [ -s "${CERT_DIR}/server.crt" ] && [ -s "${CERT_DIR}/server.key" ]; then
echo "Certificates already exist, skipping generation."
exit 0
fi
# Если файлы существуют но пустые - удаляем их для перегенерации
rm -f "${CERT_DIR}/ca.crt" "${CERT_DIR}/ca.key" "${CERT_DIR}/server.crt" "${CERT_DIR}/server.key" "${CERT_DIR}/server.csr"
# Генерация CA
echo "Generating CA..."
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:4096 -out "${CERT_DIR}/ca.key"
openssl req -x509 -new -nodes -sha256 -days ${DAYS_VALID} \
-key "${CERT_DIR}/ca.key" \
-out "${CERT_DIR}/ca.crt" \
-subj "/CN=HellreigN Root CA"
# Генерация серверного сертификата
echo "Generating server certificate..."
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:4096 -out "${CERT_DIR}/server.key"
openssl req -new -sha256 \
-key "${CERT_DIR}/server.key" \
-out "${CERT_DIR}/server.csr" \
-subj "/CN=${SERVER_CN:-localhost}"
# Создаём конфиг для server SAN
# Поддержка переменных окружения:
# SERVER_SAN_DNS - список DNS имен через запятую (например: localhost,backend,myserver.example.com)
# SERVER_SAN_IP - список IP адресов через запятую (например: 127.0.0.1,192.168.1.100)
cat > "${CERT_DIR}/server.ext" <<EOF
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names
[alt_names]
EOF
# Добавляем DNS SAN
dns_idx=1
IFS=',' read -ra DNS_NAMES <<< "${SERVER_SAN_DNS:-localhost,backend}"
for dns_name in "${DNS_NAMES[@]}"; do
dns_name=$(echo "$dns_name" | xargs) # trim whitespace
if [ -n "$dns_name" ]; then
echo "DNS.${dns_idx} = ${dns_name}" >> "${CERT_DIR}/server.ext"
((dns_idx++))
fi
done
# Добавляем wildcard для localhost если есть
echo "DNS.${dns_idx} = *.localhost" >> "${CERT_DIR}/server.ext"
((dns_idx++))
# Добавляем IP SAN
ip_idx=1
IFS=',' read -ra IP_ADDRS <<< "${SERVER_SAN_IP:-127.0.0.1}"
for ip_addr in "${IP_ADDRS[@]}"; do
ip_addr=$(echo "$ip_addr" | xargs) # trim whitespace
if [ -n "$ip_addr" ]; then
echo "IP.${ip_idx} = ${ip_addr}" >> "${CERT_DIR}/server.ext"
((ip_idx++))
fi
done
openssl x509 -req -sha256 -days ${DAYS_VALID} \
-in "${CERT_DIR}/server.csr" \
-CA "${CERT_DIR}/ca.crt" \
-CAkey "${CERT_DIR}/ca.key" \
-CAcreateserial \
-out "${CERT_DIR}/server.crt" \
-extfile "${CERT_DIR}/server.ext"
# Очистка лишних файлов
rm -f "${CERT_DIR}/server.ext"
# Установка прав
chmod 600 "${CERT_DIR}"/*.key
chmod 644 "${CERT_DIR}"/*.crt
echo "Certificates generated successfully!"
echo " CA: ${CERT_DIR}/ca.crt"
echo " Server: ${CERT_DIR}/server.crt + ${CERT_DIR}/server.key"
echo " SAN DNS: ${SERVER_SAN_DNS:-localhost,backend}"
echo " SAN IP: ${SERVER_SAN_IP:-127.0.0.1}"
+16
View File
@@ -0,0 +1,16 @@
FROM node:25-alpine3.23 AS builder
WORKDIR /app
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile
COPY . .
RUN yarn build
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
+32
View File
@@ -0,0 +1,32 @@
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Gzip сжатие
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
}
+7
View File
@@ -0,0 +1,7 @@
go 1.26.1
use (
./agent
./backend
./proto
)
+8
View File
@@ -0,0 +1,8 @@
backend_url: http://backend:8080
grpc_url: backend:9001
label: test-agent-1
registration_token: "156616b56774d59ba53f1eb4b096488bb5f755bbf5b737d93a42bb1b583ad7fb"
cert_dir: /etc/hellreign-agent/certs
services:
- name: system
type: journald
+11
View File
@@ -0,0 +1,11 @@
database:
token_db: /var/lib/hellreign/tokens.db
clickhouse_host: clickhouse:9000
clickhouse_user: default
clickhouse_password: testpassword
clickhouse_database: hellreign
admin:
admin_name: Admin
admin_last_name: User
admin_login: admin
admin_password: admin123
@@ -0,0 +1,16 @@
#!/bin/bash
set -e
clickhouse-client --query "CREATE DATABASE IF NOT EXISTS hellreign;"
clickhouse-client --query "
CREATE TABLE IF NOT EXISTS hellreign.logs (
timestamp DateTime64(3) DEFAULT now(),
level LowCardinality(String),
service LowCardinality(String),
agent LowCardinality(String),
message String
) ENGINE = MergeTree()
ORDER BY (timestamp, level, service, agent)
SETTINGS index_granularity = 8192;
"
+90
View File
@@ -0,0 +1,90 @@
services:
clickhouse:
image: clickhouse/clickhouse-server:24.8
container_name: hellreign-clickhouse
environment:
CLICKHOUSE_DB: hellreign
CLICKHOUSE_USER: default
CLICKHOUSE_PASSWORD: testpassword
CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT: 1
ports:
- "8123:8123"
- "9000:9000"
volumes:
- clickhouse_data:/var/lib/clickhouse
- ./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:
- hellreign
backend:
build:
context: ..
dockerfile: backend/dockerfile
container_name: hellreign-backend
environment:
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:
- "8080:8080"
- "9001:9001"
volumes:
- ./backend/config.yml:/etc/hellreign/config.yml:ro
- backend_data:/var/lib/hellreign
depends_on:
clickhouse:
condition: service_healthy
networks:
- hellreign
frontend:
build:
context: ../frontend
dockerfile: dockerfile
container_name: hellreign-frontend
ports:
- "3000:80"
depends_on:
- backend
networks:
- hellreign
agent:
build:
context: ..
dockerfile: agent/dockerfile
container_name: hellreign-agent
environment:
CONFIG_FILE: /etc/hellreign-agent/config.yml
JOURNALD_LOGDIR: /var/log/journal
BUFFER_DB: /var/lib/hellreign-agent/agent_buffer.db
volumes:
- ./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:
- backend
networks:
- hellreign
volumes:
clickhouse_data:
driver: local
backend_data:
driver: local
agent_certs:
driver: local
agent_data:
driver: local
networks:
hellreign:
driver: bridge
Executable
+11
View File
@@ -0,0 +1,11 @@
#!/usr/bin/env bash
set -exuo pipefail
protogen() {
in=./hellreign.proto
protoc --go_out="." --go_opt=module="gitea.d3m0k1d.ru/d3m0k1d/HellreigN/proto" \
--go-grpc_out="." --go-grpc_opt=module="gitea.d3m0k1d.ru/d3m0k1d/HellreigN/proto" "$in"
}
"$@"
+16
View File
@@ -0,0 +1,16 @@
module gitea.d3m0k1d.ru/d3m0k1d/HellreigN/proto
go 1.25.0
require (
google.golang.org/grpc v1.80.0
google.golang.org/protobuf v1.36.11
)
require (
go.opentelemetry.io/otel v1.41.0 // indirect
golang.org/x/net v0.52.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.35.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 // indirect
)
+38
View File
@@ -0,0 +1,38 @@
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/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
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/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
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/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
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=
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/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c=
go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE=
go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ=
go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
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/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis=
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/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
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/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
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/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
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/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
+33
View File
@@ -0,0 +1,33 @@
syntax = "proto3";
package chat;
option go_package="gitea.d3m0k1d.ru/d3m0k1d/HellreigN/proto/proto";
service Collector {
rpc Stream(stream CollectorRequest) returns (CollectorResponse);
}
message CollectorRequest {
string message = 1;
}
message CollectorResponse {
}
service Commander {
rpc Stream(stream FinishedCommand) returns (stream Command);
}
message Command {
int64 id = 1;
repeated string command = 2;
optional string stdin = 3;
}
message FinishedCommand {
int64 id = 1;
int32 status = 2;
string stdout = 3;
string stderr = 4;
}
+309
View File
@@ -0,0 +1,309 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc v3.21.9
// source: hellreign.proto
package proto
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type CollectorRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *CollectorRequest) Reset() {
*x = CollectorRequest{}
mi := &file_hellreign_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *CollectorRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CollectorRequest) ProtoMessage() {}
func (x *CollectorRequest) ProtoReflect() protoreflect.Message {
mi := &file_hellreign_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use CollectorRequest.ProtoReflect.Descriptor instead.
func (*CollectorRequest) Descriptor() ([]byte, []int) {
return file_hellreign_proto_rawDescGZIP(), []int{0}
}
func (x *CollectorRequest) GetMessage() string {
if x != nil {
return x.Message
}
return ""
}
type CollectorResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *CollectorResponse) Reset() {
*x = CollectorResponse{}
mi := &file_hellreign_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *CollectorResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CollectorResponse) ProtoMessage() {}
func (x *CollectorResponse) ProtoReflect() protoreflect.Message {
mi := &file_hellreign_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use CollectorResponse.ProtoReflect.Descriptor instead.
func (*CollectorResponse) Descriptor() ([]byte, []int) {
return file_hellreign_proto_rawDescGZIP(), []int{1}
}
type Command struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
Command []string `protobuf:"bytes,2,rep,name=command,proto3" json:"command,omitempty"`
Stdin *string `protobuf:"bytes,3,opt,name=stdin,proto3,oneof" json:"stdin,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Command) Reset() {
*x = Command{}
mi := &file_hellreign_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Command) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Command) ProtoMessage() {}
func (x *Command) ProtoReflect() protoreflect.Message {
mi := &file_hellreign_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Command.ProtoReflect.Descriptor instead.
func (*Command) Descriptor() ([]byte, []int) {
return file_hellreign_proto_rawDescGZIP(), []int{2}
}
func (x *Command) GetId() int64 {
if x != nil {
return x.Id
}
return 0
}
func (x *Command) GetCommand() []string {
if x != nil {
return x.Command
}
return nil
}
func (x *Command) GetStdin() string {
if x != nil && x.Stdin != nil {
return *x.Stdin
}
return ""
}
type FinishedCommand struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
Status int32 `protobuf:"varint,2,opt,name=status,proto3" json:"status,omitempty"`
Stdout string `protobuf:"bytes,3,opt,name=stdout,proto3" json:"stdout,omitempty"`
Stderr string `protobuf:"bytes,4,opt,name=stderr,proto3" json:"stderr,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *FinishedCommand) Reset() {
*x = FinishedCommand{}
mi := &file_hellreign_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *FinishedCommand) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*FinishedCommand) ProtoMessage() {}
func (x *FinishedCommand) ProtoReflect() protoreflect.Message {
mi := &file_hellreign_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use FinishedCommand.ProtoReflect.Descriptor instead.
func (*FinishedCommand) Descriptor() ([]byte, []int) {
return file_hellreign_proto_rawDescGZIP(), []int{3}
}
func (x *FinishedCommand) GetId() int64 {
if x != nil {
return x.Id
}
return 0
}
func (x *FinishedCommand) GetStatus() int32 {
if x != nil {
return x.Status
}
return 0
}
func (x *FinishedCommand) GetStdout() string {
if x != nil {
return x.Stdout
}
return ""
}
func (x *FinishedCommand) GetStderr() string {
if x != nil {
return x.Stderr
}
return ""
}
var File_hellreign_proto protoreflect.FileDescriptor
const file_hellreign_proto_rawDesc = "" +
"\n" +
"\x0fhellreign.proto\x12\x04chat\",\n" +
"\x10CollectorRequest\x12\x18\n" +
"\amessage\x18\x01 \x01(\tR\amessage\"\x13\n" +
"\x11CollectorResponse\"X\n" +
"\aCommand\x12\x0e\n" +
"\x02id\x18\x01 \x01(\x03R\x02id\x12\x18\n" +
"\acommand\x18\x02 \x03(\tR\acommand\x12\x19\n" +
"\x05stdin\x18\x03 \x01(\tH\x00R\x05stdin\x88\x01\x01B\b\n" +
"\x06_stdin\"i\n" +
"\x0fFinishedCommand\x12\x0e\n" +
"\x02id\x18\x01 \x01(\x03R\x02id\x12\x16\n" +
"\x06status\x18\x02 \x01(\x05R\x06status\x12\x16\n" +
"\x06stdout\x18\x03 \x01(\tR\x06stdout\x12\x16\n" +
"\x06stderr\x18\x04 \x01(\tR\x06stderr2H\n" +
"\tCollector\x12;\n" +
"\x06Stream\x12\x16.chat.CollectorRequest\x1a\x17.chat.CollectorResponse(\x012?\n" +
"\tCommander\x122\n" +
"\x06Stream\x12\x15.chat.FinishedCommand\x1a\r.chat.Command(\x010\x01B0Z.gitea.d3m0k1d.ru/d3m0k1d/HellreigN/proto/protob\x06proto3"
var (
file_hellreign_proto_rawDescOnce sync.Once
file_hellreign_proto_rawDescData []byte
)
func file_hellreign_proto_rawDescGZIP() []byte {
file_hellreign_proto_rawDescOnce.Do(func() {
file_hellreign_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_hellreign_proto_rawDesc), len(file_hellreign_proto_rawDesc)))
})
return file_hellreign_proto_rawDescData
}
var file_hellreign_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
var file_hellreign_proto_goTypes = []any{
(*CollectorRequest)(nil), // 0: chat.CollectorRequest
(*CollectorResponse)(nil), // 1: chat.CollectorResponse
(*Command)(nil), // 2: chat.Command
(*FinishedCommand)(nil), // 3: chat.FinishedCommand
}
var file_hellreign_proto_depIdxs = []int32{
0, // 0: chat.Collector.Stream:input_type -> chat.CollectorRequest
3, // 1: chat.Commander.Stream:input_type -> chat.FinishedCommand
1, // 2: chat.Collector.Stream:output_type -> chat.CollectorResponse
2, // 3: chat.Commander.Stream:output_type -> chat.Command
2, // [2:4] is the sub-list for method output_type
0, // [0:2] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_hellreign_proto_init() }
func file_hellreign_proto_init() {
if File_hellreign_proto != nil {
return
}
file_hellreign_proto_msgTypes[2].OneofWrappers = []any{}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_hellreign_proto_rawDesc), len(file_hellreign_proto_rawDesc)),
NumEnums: 0,
NumMessages: 4,
NumExtensions: 0,
NumServices: 2,
},
GoTypes: file_hellreign_proto_goTypes,
DependencyIndexes: file_hellreign_proto_depIdxs,
MessageInfos: file_hellreign_proto_msgTypes,
}.Build()
File_hellreign_proto = out.File
file_hellreign_proto_goTypes = nil
file_hellreign_proto_depIdxs = nil
}
+210
View File
@@ -0,0 +1,210 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.6.1
// - protoc v3.21.9
// source: hellreign.proto
package proto
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.64.0 or later.
const _ = grpc.SupportPackageIsVersion9
const (
Collector_Stream_FullMethodName = "/chat.Collector/Stream"
)
// CollectorClient is the client API for Collector service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type CollectorClient interface {
Stream(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[CollectorRequest, CollectorResponse], error)
}
type collectorClient struct {
cc grpc.ClientConnInterface
}
func NewCollectorClient(cc grpc.ClientConnInterface) CollectorClient {
return &collectorClient{cc}
}
func (c *collectorClient) Stream(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[CollectorRequest, CollectorResponse], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &Collector_ServiceDesc.Streams[0], Collector_Stream_FullMethodName, cOpts...)
if err != nil {
return nil, err
}
x := &grpc.GenericClientStream[CollectorRequest, CollectorResponse]{ClientStream: stream}
return x, nil
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type Collector_StreamClient = grpc.ClientStreamingClient[CollectorRequest, CollectorResponse]
// CollectorServer is the server API for Collector service.
// All implementations must embed UnimplementedCollectorServer
// for forward compatibility.
type CollectorServer interface {
Stream(grpc.ClientStreamingServer[CollectorRequest, CollectorResponse]) error
mustEmbedUnimplementedCollectorServer()
}
// UnimplementedCollectorServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedCollectorServer struct{}
func (UnimplementedCollectorServer) Stream(grpc.ClientStreamingServer[CollectorRequest, CollectorResponse]) error {
return status.Error(codes.Unimplemented, "method Stream not implemented")
}
func (UnimplementedCollectorServer) mustEmbedUnimplementedCollectorServer() {}
func (UnimplementedCollectorServer) testEmbeddedByValue() {}
// UnsafeCollectorServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to CollectorServer will
// result in compilation errors.
type UnsafeCollectorServer interface {
mustEmbedUnimplementedCollectorServer()
}
func RegisterCollectorServer(s grpc.ServiceRegistrar, srv CollectorServer) {
// If the following call panics, it indicates UnimplementedCollectorServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&Collector_ServiceDesc, srv)
}
func _Collector_Stream_Handler(srv interface{}, stream grpc.ServerStream) error {
return srv.(CollectorServer).Stream(&grpc.GenericServerStream[CollectorRequest, CollectorResponse]{ServerStream: stream})
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type Collector_StreamServer = grpc.ClientStreamingServer[CollectorRequest, CollectorResponse]
// Collector_ServiceDesc is the grpc.ServiceDesc for Collector service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var Collector_ServiceDesc = grpc.ServiceDesc{
ServiceName: "chat.Collector",
HandlerType: (*CollectorServer)(nil),
Methods: []grpc.MethodDesc{},
Streams: []grpc.StreamDesc{
{
StreamName: "Stream",
Handler: _Collector_Stream_Handler,
ClientStreams: true,
},
},
Metadata: "hellreign.proto",
}
const (
Commander_Stream_FullMethodName = "/chat.Commander/Stream"
)
// CommanderClient is the client API for Commander service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type CommanderClient interface {
Stream(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[FinishedCommand, Command], error)
}
type commanderClient struct {
cc grpc.ClientConnInterface
}
func NewCommanderClient(cc grpc.ClientConnInterface) CommanderClient {
return &commanderClient{cc}
}
func (c *commanderClient) Stream(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[FinishedCommand, Command], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &Commander_ServiceDesc.Streams[0], Commander_Stream_FullMethodName, cOpts...)
if err != nil {
return nil, err
}
x := &grpc.GenericClientStream[FinishedCommand, Command]{ClientStream: stream}
return x, nil
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type Commander_StreamClient = grpc.BidiStreamingClient[FinishedCommand, Command]
// CommanderServer is the server API for Commander service.
// All implementations must embed UnimplementedCommanderServer
// for forward compatibility.
type CommanderServer interface {
Stream(grpc.BidiStreamingServer[FinishedCommand, Command]) error
mustEmbedUnimplementedCommanderServer()
}
// UnimplementedCommanderServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedCommanderServer struct{}
func (UnimplementedCommanderServer) Stream(grpc.BidiStreamingServer[FinishedCommand, Command]) error {
return status.Error(codes.Unimplemented, "method Stream not implemented")
}
func (UnimplementedCommanderServer) mustEmbedUnimplementedCommanderServer() {}
func (UnimplementedCommanderServer) testEmbeddedByValue() {}
// UnsafeCommanderServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to CommanderServer will
// result in compilation errors.
type UnsafeCommanderServer interface {
mustEmbedUnimplementedCommanderServer()
}
func RegisterCommanderServer(s grpc.ServiceRegistrar, srv CommanderServer) {
// If the following call panics, it indicates UnimplementedCommanderServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&Commander_ServiceDesc, srv)
}
func _Commander_Stream_Handler(srv interface{}, stream grpc.ServerStream) error {
return srv.(CommanderServer).Stream(&grpc.GenericServerStream[FinishedCommand, Command]{ServerStream: stream})
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type Commander_StreamServer = grpc.BidiStreamingServer[FinishedCommand, Command]
// Commander_ServiceDesc is the grpc.ServiceDesc for Commander service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var Commander_ServiceDesc = grpc.ServiceDesc{
ServiceName: "chat.Commander",
HandlerType: (*CommanderServer)(nil),
Methods: []grpc.MethodDesc{},
Streams: []grpc.StreamDesc{
{
StreamName: "Stream",
Handler: _Commander_Stream_Handler,
ServerStreams: true,
ClientStreams: true,
},
},
Metadata: "hellreign.proto",
}