1 Commits

Author SHA1 Message Date
zero@thinky abc6cb4e46 debug logs 2026-04-04 17:33:36 +03:00
51 changed files with 818 additions and 7002 deletions
-81
View File
@@ -1,81 +0,0 @@
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
version: 2
project_name: BanForge
gitea_urls:
api: https://gitea.d3m0k1d.ru/api/v1
download: https://gitea.d3m0k1d.ru/d3m0k1d/HellreigN/releases/download
skip_tls_verify: false
builds:
- id: banforge
main: ./cmd/banforge/main.go
binary: banforge
ignore:
- goos: windows
- goos: darwin
- goos: freebsd
goos:
- linux
goarch:
- amd64
- arm64
ldflags:
- "-s -w"
env:
- CGO_ENABLED=0
archives:
- formats: [tar.gz]
name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
nfpms:
- id: banforge
package_name: banforge
file_name_template: "{{ .PackageName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
homepage: https://gitea.d3m0k1d.ru/d3m0k1d/HellreigN
description: HellreigN agent
maintainer: d3m0k1d <contact@d3m0k1d.ru>
license: GPLv3.0
formats:
- apk
- deb
- rpm
- archlinux
bindir: /usr/bin
scripts:
postinstall: build/postinstall.sh
postremove: build/postremove.sh
contents:
- src: docs/man/banforge.1
dst: /usr/share/man/man1/banforge.1
file_info:
mode: 0644
- src: docs/man/banforge.5
dst: /usr/share/man/man5/banforge.5
file_info:
mode: 0644
release:
gitea:
owner: d3m0k1d
name: BanForge
mode: keep-existing
changelog:
sort: asc
filters:
exclude:
- "^docs:"
- "^test:"
checksum:
name_template: "{{ .ProjectName }}_{{ .Version }}_checksums.txt"
algorithm: sha256
sboms:
- artifacts: archive
documents:
- "{{ .ArtifactName }}.spdx.json"
cmd: syft
args: ["$artifact", "--output", "spdx-json=$document"]
+1 -54
View File
@@ -3,67 +3,15 @@ module gitea.d3m0k1d.ru/d3m0k1d/HellreigN/agent
go 1.26.1
require (
gitea.d3m0k1d.ru/d3m0k1d/HellreigN/proto v0.0.0-20260404174628-3389df740c20
gitea.d3m0k1d.ru/d3m0k1d/HellreigN/proto v0.0.0-20260403214837-94be9799f47d
github.com/hpcloud/tail v1.0.0
github.com/moby/moby/api v1.54.1
github.com/moby/moby/client v0.4.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
k8s.io/api v0.35.3
k8s.io/apimachinery v0.35.3
modernc.org/sqlite v1.34.5
)
require (
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/go-connections v0.6.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/emicklei/go-restful/v3 v3.12.2 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/google/gnostic-models v0.7.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/spf13/pflag v1.0.9 // indirect
github.com/x448/float16 v0.8.4 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
go.opentelemetry.io/otel v1.41.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/oauth2 v0.34.0 // indirect
golang.org/x/term v0.41.0 // indirect
golang.org/x/time v0.11.0 // indirect
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
)
require (
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
@@ -80,7 +28,6 @@ require (
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
k8s.io/client-go v0.35.3
modernc.org/libc v1.55.3 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.8.0 // indirect
+3 -137
View File
@@ -1,127 +1,33 @@
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
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/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
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/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
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/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU=
github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
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/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
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.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
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-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
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/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
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/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
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/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
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/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/moby/api v1.54.1 h1:TqVzuJkOLsgLDDwNLmYqACUuTehOHRGKiPhvH8V3Nn4=
github.com/moby/moby/api v1.54.1/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
github.com/moby/moby/client v0.4.0 h1:S+2XegzHQrrvTCvF6s5HFzcrywWQmuVnhOXe2kiWjIw=
github.com/moby/moby/client v0.4.0/go.mod h1:QWPbvWchQbxBNdaLSpoKpCdf5E+WxFAgNHogCWDoa7g=
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/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
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/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns=
github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
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/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.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/samber/lo v1.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM=
github.com/samber/lo v1.53.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
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 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
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.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.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
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/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
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/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=
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=
@@ -132,27 +38,17 @@ go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2W
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0=
go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
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/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/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
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/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
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/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
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=
@@ -163,34 +59,14 @@ 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/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/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo=
gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
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.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=
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
k8s.io/api v0.35.3 h1:pA2fiBc6+N9PDf7SAiluKGEBuScsTzd2uYBkA5RzNWQ=
k8s.io/api v0.35.3/go.mod h1:9Y9tkBcFwKNq2sxwZTQh1Njh9qHl81D0As56tu42GA4=
k8s.io/apimachinery v0.35.3 h1:MeaUwQCV3tjKP4bcwWGgZ/cp/vpsRnQzqO6J6tJyoF8=
k8s.io/apimachinery v0.35.3/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=
k8s.io/client-go v0.35.3 h1:s1lZbpN4uI6IxeTM2cpdtrwHcSOBML1ODNTCCfsP1pg=
k8s.io/client-go v0.35.3/go.mod h1:RzoXkc0mzpWIDvBrRnD+VlfXP+lRzqQjCmKtiwZ8Q9c=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE=
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
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=
@@ -215,13 +91,3 @@ 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=
pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=
pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=
sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
+18 -19
View File
@@ -3,19 +3,22 @@ package commander
import (
"bytes"
"errors"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/proto/proto"
"golang.org/x/sync/errgroup"
"io"
"os/exec"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/proto/proto"
"golang.org/x/sync/errgroup"
)
type CommandExecutor struct{}
type CommandExecutor struct {
}
func (*CommandExecutor) Execute(command *proto.Command) (fc *proto.FinishedCommand, err error) {
fc = new(proto.FinishedCommand)
fc.Id = command.Id
func (*CommandExecutor) Execute(command *proto.Command) (*proto.FinishedCommand, error) {
cmd := exec.Command(command.Command[0], command.Command[1:]...)
var stdin io.WriteCloser
var (
stdin io.WriteCloser
err error
)
if command.Stdin != nil {
stdin, err = cmd.StdinPipe()
if err != nil {
@@ -47,20 +50,16 @@ func (*CommandExecutor) Execute(command *proto.Command) (fc *proto.FinishedComma
_, err := io.Copy(stderrbuf, stderr)
return err
})
if waitErr := cmd.Wait(); waitErr != nil {
var exitErr *exec.ExitError
if !errors.As(waitErr, &exitErr) {
return nil, waitErr
}
fc.Status = int32(exitErr.ExitCode())
} else {
fc.Status = int32(cmd.ProcessState.ExitCode())
if err := cmd.Wait(); err != nil {
return nil, err
}
if err := eg.Wait(); err != nil {
return nil, err
}
fc.Status = int32(cmd.ProcessState.ExitCode())
fc.Stdout = stdoutbuf.String()
fc.Stderr = stderrbuf.String()
return
return &proto.FinishedCommand{
Id: command.Id,
Status: int32(cmd.ProcessState.ExitCode()),
Stdout: stdoutbuf.String(),
Stderr: stderrbuf.String(),
}, nil
}
-5
View File
@@ -20,11 +20,6 @@ type AgentConfig struct {
Label string `yaml:"label"`
CertDir string `yaml:"cert_dir"`
Services []ServiceConfig `yaml:"services"`
MonitorDocker bool `yaml:"monitor_docker"`
MonitorKubernetes bool `yaml:"monitor_kubes"`
KubernetesNamespace *string `yaml:"kubernetes_namespace"`
}
func Load(path string) (*AgentConfig, error) {
-89
View File
@@ -1,89 +0,0 @@
package docker
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(DockerLogSource)
// DockerLogSource reads logs from a Docker container via `docker logs -f`.
type DockerLogSource struct {
cmd *exec.Cmd
stdout io.ReadCloser
stdoutscanner *bufio.Scanner
}
// ReadLine implements logsource.LogSource.
func (d *DockerLogSource) ReadLine() (string, error) {
if d.stdoutscanner.Scan() {
return d.stdoutscanner.Text(), nil
} else {
if d.stdoutscanner.Err() == nil {
return "", fmt.Errorf("%w: %s", logsource.ErrDead, io.EOF)
}
return "", d.stdoutscanner.Err()
}
}
// Close implements logsource.LogSource.
func (d *DockerLogSource) Close() error {
_ = d.cmd.Process.Signal(syscall.SIGTERM)
return d.cmd.Wait()
}
// New creates a Docker log source for the given container.
// The container name is taken from cfg.Path (if set) or cfg.Name.
func New(cfg config.ServiceConfig) (*DockerLogSource, error) {
containerName := cfg.Name
if cfg.Path != nil && *cfg.Path != "" {
containerName = *cfg.Path
}
// docker logs -f --tail=0 --no-color <container_name>
// -f : follow new logs
// --tail=0 : skip existing logs
// --no-color: strip color codes for clean output
cmd := exec.Command("docker", "logs", "-f", "--tail=0", "--no-color", containerName) //nolint:gosec
stdout, err := cmd.StdoutPipe()
if err != nil {
return nil, fmt.Errorf("failed to create stdout pipe for docker logs: %w", err)
}
// Also capture stderr since docker logs merges stdout and stderr from the container
stderr, err := cmd.StderrPipe()
if err != nil {
return nil, fmt.Errorf("failed to create stderr pipe for docker logs: %w", err)
}
err = cmd.Start()
if err != nil {
return nil, fmt.Errorf("failed to start docker logs for container %q: %w", containerName, err)
}
// Use MultiReader to merge stdout and stderr
// Docker logs outputs container stdout+stderr to its own stdout, but we also
// capture the docker CLI's stderr separately in case of errors (e.g. container not found)
stdoutscanner := bufio.NewScanner(stdout)
// Start a goroutine to consume stderr (we don't send docker CLI stderr as logs,
// but we need to prevent the pipe from filling up)
go func() {
buf := make([]byte, 4096)
for {
_, err := stderr.Read(buf)
if err != nil {
return
}
}
}()
return &DockerLogSource{cmd, stdout, stdoutscanner}, nil
}
@@ -1,95 +0,0 @@
package kubernetes
import (
"bufio"
"fmt"
"io"
"os/exec"
"strings"
"syscall"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/agent/internal/config"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/agent/internal/logsource"
)
var _ logsource.LogSource = new(KubernetesLogSource)
// KubernetesLogSource reads logs from a Kubernetes pod via `kubectl logs -f`.
type KubernetesLogSource struct {
cmd *exec.Cmd
stdout io.ReadCloser
stdoutscanner *bufio.Scanner
}
// ReadLine implements logsource.LogSource.
func (k *KubernetesLogSource) ReadLine() (string, error) {
if k.stdoutscanner.Scan() {
return k.stdoutscanner.Text(), nil
} else {
if k.stdoutscanner.Err() == nil {
return "", fmt.Errorf("%w: %s", logsource.ErrDead, io.EOF)
}
return "", k.stdoutscanner.Err()
}
}
// Close implements logsource.LogSource.
func (k *KubernetesLogSource) Close() error {
_ = k.cmd.Process.Signal(syscall.SIGTERM)
return k.cmd.Wait()
}
// New creates a Kubernetes log source for the given pod.
// The pod identifier is taken from cfg.Path in the format "namespace/podname".
// If no namespace is specified (just "podname"), "default" namespace is used.
// If cfg.Path is nil or empty, cfg.Name is used as the pod name with "default" namespace.
func New(cfg config.ServiceConfig) (*KubernetesLogSource, error) {
podName := cfg.Name
namespace := "default"
if cfg.Path != nil && *cfg.Path != "" {
parts := strings.SplitN(*cfg.Path, "/", 2)
if len(parts) == 2 {
namespace = parts[0]
podName = parts[1]
} else {
podName = parts[0]
}
}
// kubectl logs -f <pod> -n <namespace> --tail=0 --no-color
// -f : follow new logs
// --tail=0 : skip existing logs
// --no-color: strip color codes for clean output
cmd := exec.Command("kubectl", "logs", "-f", podName, "-n", namespace, "--tail=0", "--no-color") //nolint:gosec
stdout, err := cmd.StdoutPipe()
if err != nil {
return nil, fmt.Errorf("failed to create stdout pipe for kubectl logs: %w", err)
}
stderr, err := cmd.StderrPipe()
if err != nil {
return nil, fmt.Errorf("failed to create stderr pipe for kubectl logs: %w", err)
}
err = cmd.Start()
if err != nil {
return nil, fmt.Errorf("failed to start kubectl logs for pod %q (ns: %q): %w", podName, namespace, err)
}
stdoutscanner := bufio.NewScanner(stdout)
// Consume stderr to prevent pipe from filling up
go func() {
buf := make([]byte, 4096)
for {
_, err := stderr.Read(buf)
if err != nil {
return
}
}
}()
return &KubernetesLogSource{cmd, stdout, stdoutscanner}, nil
}
-6
View File
@@ -1,6 +0,0 @@
package models
type Service struct {
Name string
Status string
}
-42
View File
@@ -1,42 +0,0 @@
package docker
import (
"context"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/agent/internal/models"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/agent/internal/monitor"
"github.com/moby/moby/api/types/container"
moby "github.com/moby/moby/client"
"github.com/samber/lo"
)
var _ monitor.ServiceMonitor = new(DockerMonitor)
type DockerMonitor struct{}
func New() *DockerMonitor {
return &DockerMonitor{}
}
func (self *DockerMonitor) CheckServices(ctx context.Context) ([]models.Service, error) {
client, err := moby.New(moby.FromEnv)
if err != nil {
return nil, err
}
ctrs, err := client.ContainerList(ctx, moby.ContainerListOptions{
Size: false,
All: false,
Limit: 0,
Filters: moby.Filters{},
Latest: false,
})
if err != nil {
return nil, err
}
return lo.Map(ctrs.Items, func(item container.Summary, _ int) models.Service {
return models.Service{
Name: lo.If(len(item.Names) > 0, item.Names[0]).Else(item.ID),
Status: string(item.State), // TODO: map to standartized states enum
}
}), nil
}
-11
View File
@@ -1,11 +0,0 @@
package monitor
import (
"context"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/agent/internal/models"
)
type ServiceMonitor interface {
CheckServices(ctx context.Context) ([]models.Service, error)
}
-50
View File
@@ -1,50 +0,0 @@
package kubes
import (
"context"
"os"
"path/filepath"
"github.com/samber/lo"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/agent/internal/models"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/agent/internal/monitor"
)
var _ monitor.ServiceMonitor = new(KubesMonitor)
type KubesMonitor struct{ namespace string }
func New(namespace string) *KubesMonitor {
return &KubesMonitor{namespace}
}
func (self *KubesMonitor) CheckServices(ctx context.Context) ([]models.Service, error) {
home, err := os.UserHomeDir()
if err != nil {
return nil, err
}
config, err := clientcmd.BuildConfigFromFlags("", filepath.Join(home, ".kube", "config"))
if err != nil {
return nil, err
}
clientset, err := kubernetes.NewForConfig(config)
if err != nil {
return nil, err
}
// TODO: consider moving all the shit above into constructor
pods, err := clientset.CoreV1().Pods(self.namespace).List(context.TODO(), metav1.ListOptions{})
if err != nil {
return nil, err
}
return lo.Map(pods.Items, func(item corev1.Pod, _ int) models.Service {
return models.Service{
Name: item.Name,
Status: string(item.Status.Phase), // TODO: map to standartized states enum
}
}), nil
}
-69
View File
@@ -14,17 +14,14 @@ import (
"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/docker"
"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/logsource/kubernetes"
"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/credentials"
"google.golang.org/grpc/metadata"
)
@@ -113,13 +110,6 @@ func main() {
return ccli.HandleCommands(ctx, grpcAddr, creds)
})
// Start services update stream
if len(cfg.Services) > 0 {
wg.Go(func() error {
return reportServices(ctx, grpcAddr, creds, cfg.Label, cfg.Services, lgr)
})
}
// Start log collectors
if len(cfg.Services) > 0 {
wg.Go(func() error {
@@ -149,16 +139,6 @@ func main() {
if err != nil {
return fmt.Errorf("failed to create file source %q: %w", svc.Name, err)
}
case "docker":
src, err = docker.New(svc)
if err != nil {
return fmt.Errorf("failed to create docker source for container %q: %w", svc.Name, err)
}
case "kubernetes":
src, err = kubernetes.New(svc)
if err != nil {
return fmt.Errorf("failed to create kubernetes source for pod %q: %w", svc.Name, err)
}
default:
return fmt.Errorf("unknown log source type %q for service %q", svc.Type, svc.Name)
}
@@ -321,52 +301,3 @@ func reconnectStream(
return fmt.Errorf("failed to reconnect after 5 attempts for service %s", service)
}
// reportServices periodically sends service status updates to the backend via gRPC.
// For now, all configured services are reported as "up" every 5 seconds.
func reportServices(
ctx context.Context,
grpcAddr string,
creds credentials.TransportCredentials,
label string,
services []config.ServiceConfig,
lgr *logger.Logger,
) error {
conn, err := grpc.NewClient(grpcAddr, grpc.WithTransportCredentials(creds))
if err != nil {
return fmt.Errorf("failed to connect for services report: %w", err)
}
defer conn.Close()
ccli := proto.NewCollectorClient(conn)
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
// Send immediately on start, then every 5 seconds
for {
svcUpdates := make([]*proto.ServicesUpdate_ServiceUpdate, 0, len(services))
for _, svc := range services {
svcUpdates = append(svcUpdates, &proto.ServicesUpdate_ServiceUpdate{
Name: svc.Name,
Status: "up",
})
}
md := metadata.New(map[string]string{"whoami": label})
_, err := ccli.ReportServices(
metadata.NewOutgoingContext(ctx, md),
&proto.ServicesUpdate{Services: svcUpdates},
)
if err != nil {
lgr.Warn("Failed to report services", "err", err)
} else {
lgr.Debug("Services reported successfully", "count", len(services))
}
select {
case <-ctx.Done():
return ctx.Err()
case <-ticker.C:
}
}
}
+12 -66
View File
@@ -82,30 +82,19 @@ func main() {
}()
}
// Initialize Collector (log streaming) with its own ConnTracker
collTracker := collector.NewConnTracker()
coll := collector.New(logRepo, collTracker)
// Initialize Collector gRPC service
coll := collector.New(logRepo)
// Initialize ConnTracker for Commander agent lifecycle
cmdTracker := commander.NewConnTracker()
cmdr := commander.New(jobRepo, cmdTracker)
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.NewScriptServiceWithInterpreters(h.Repo, scriptRepo)
scriptHandlers := handlers.NewScriptHandlers(scriptSvc, cmdTracker,
os.Getenv("WHEREAMI"))
jobsHandlers := handlers.NewJobsHandlers(cmdTracker, scriptSvc,
os.Getenv("WHEREAMI"), /* our address for redirects */
jobRepo,
)
// Initialize script management service and handlers
scriptManageSvc := service.NewScriptService(h.Repo)
scriptManageHandlers := handlers.NewScriptHandlersGroup(scriptManageSvc, cmdr)
scriptSvc := service.NewScriptService(scriptRepo)
scriptHandlers := handlers.NewScriptHandlers(scriptSvc, cmdr)
jobsHandlers := handlers.NewJobsHandlers(cmdr, scriptSvc)
agents := handlers.NewAgentsGroup(h, coll)
auth := handlers.AuthGroup{Handlers: h}
@@ -141,7 +130,6 @@ func main() {
}
router := gin.Default()
router.Use(handlers.CorsMiddleware("http://127.0.0.1:5173;http://localhost:5173"))
docs.SwaggerInfo.BasePath = "/api/v1"
docs.SwaggerInfo.Title = "HellreigN"
docs.SwaggerInfo.Version = "1.0"
@@ -155,14 +143,13 @@ func main() {
authGroup := v1.Group("/auth")
{
authGroup.POST("/login", auth.Login)
authGroup.POST("/register", auth.RegisterUser)
}
// Auth token management (requires auth)
authTokenGroup := v1.Group("/auth")
authTokenGroup.Use(auth.AuthMiddleware())
{
authTokenGroup.POST("/token", auth.CreateToken)
authTokenGroup.POST("/token", handlers.RequireAdmin(), auth.CreateToken)
authTokenGroup.GET("/validate", auth.ValidateToken)
authTokenGroup.GET("/tokens", handlers.RequireAdmin(), auth.ListTokens)
authTokenGroup.DELETE("/token", auth.DeleteMyToken)
@@ -171,28 +158,12 @@ func main() {
// 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,
)
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.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)
}
@@ -208,9 +179,6 @@ func main() {
jobsGroup.Use(auth.AuthMiddleware(), handlers.RequireAdmin())
{
jobsGroup.POST("", jobsHandlers.AddJob)
jobsGroup.POST("/:id/wait", jobsHandlers.WaitJob)
jobsGroup.GET("/metrics", jobsHandlers.GetJobMetrics)
jobsGroup.POST("/check_cmd", jobsHandlers.CheckCmd)
}
// Agent registration
@@ -253,24 +221,6 @@ func main() {
scriptsGroup.GET("/interpreters/:id", scriptHandlers.GetInterpreter)
scriptsGroup.PUT("/interpreters/:id", scriptHandlers.UpdateInterpreter)
scriptsGroup.DELETE("/interpreters/:id", scriptHandlers.DeleteInterpreter)
// Script management (tree, CRUD)
scriptsGroup.GET("/tree", scriptManageHandlers.GetTree)
scriptsGroup.POST("", scriptManageHandlers.CreateScript)
scriptsGroup.GET("/:id", scriptManageHandlers.GetScript)
scriptsGroup.PUT("/:id", scriptManageHandlers.UpdateScript)
scriptsGroup.DELETE("/:id", scriptManageHandlers.DeleteScript)
scriptsGroup.POST("/:id/run", scriptManageHandlers.RunScriptByID)
// Folder management
scriptsGroup.POST("/folder", scriptManageHandlers.CreateFolder)
scriptsGroup.DELETE("/folder", scriptManageHandlers.DeleteFolder)
// Rename script or folder
scriptsGroup.POST("/rename", scriptManageHandlers.Rename)
// Get script by path
scriptsGroup.GET("/by-path", scriptManageHandlers.GetScriptByPath)
}
}
@@ -310,11 +260,7 @@ func main() {
MinVersion: tls.VersionTLS12,
}
grpcServer := grpc.NewServer(
grpc.Creds(credentials.NewTLS(tlsConfig)),
grpc.StatsHandler(collTracker),
grpc.StatsHandler(cmdTracker),
)
grpcServer := grpc.NewServer(grpc.Creds(credentials.NewTLS(tlsConfig)))
proto.RegisterCommanderServer(grpcServer, cmdr)
proto.RegisterCollectorServer(grpcServer, coll)
+1 -1
View File
@@ -14,7 +14,7 @@ RUN --mount=type=cache,target=/go/pkg/mod \
FROM alpine:3.23.0
RUN apk add --no-cache curl openssl bash ansible sqlite
RUN apk add --no-cache curl openssl bash ansible
COPY --from=builder /app/backend/backend .
COPY --from=builder /app/backend/scripts /etc/hellreign/scripts
+125 -1535
View File
File diff suppressed because it is too large Load Diff
+125 -1535
View File
File diff suppressed because it is too large Load Diff
+96 -997
View File
File diff suppressed because it is too large Load Diff
+1 -2
View File
@@ -3,10 +3,9 @@ module gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend
go 1.26.1
require (
gitea.d3m0k1d.ru/d3m0k1d/HellreigN/proto v0.0.0-20260404174628-3389df740c20
gitea.d3m0k1d.ru/d3m0k1d/HellreigN/proto v0.0.0-20260403210401-a6212c89fc0e
github.com/ClickHouse/clickhouse-go/v2 v2.44.0
github.com/gin-gonic/gin v1.12.0
github.com/samber/lo v1.53.0
github.com/swaggo/files v1.0.1
github.com/swaggo/gin-swagger v1.6.1
github.com/swaggo/swag v1.16.6
-2
View File
@@ -138,8 +138,6 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/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/samber/lo v1.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM=
github.com/samber/lo v1.53.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
+19 -74
View File
@@ -7,49 +7,42 @@ import (
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
)
// ErrUnknownDeployType is returned when an unsupported deployment type is specified
var ErrUnknownDeployType = fmt.Errorf("unknown deploy type, expected 'docker' or 'binary'")
// Executor handles running Ansible playbooks
type Executor struct {
workDir string
grpcServerHost string
grpcServerPort string
backendURL string
giteaReleasesURL string
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
GiteaReleasesURL string
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,
giteaReleasesURL: cfg.GiteaReleasesURL,
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
Host string
Success bool
Stdout string
Stderr string
Err error
}
// WorkDir returns the work directory path
@@ -57,47 +50,8 @@ func (e *Executor) WorkDir() string {
return e.workDir
}
// GRPCURL returns the gRPC server URL (host:port)
func (e *Executor) GRPCURL() string {
return e.grpcServerHost + ":" + e.grpcServerPort
}
// CheckDockerCollection verifies that the community.docker Ansible collection is installed.
// Returns an error if the collection is not found.
func (e *Executor) CheckDockerCollection() error {
cmd := exec.Command("ansible-galaxy", "collection", "list", "community.docker")
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("community.docker collection not found: %s", stderr.String())
}
// ansible-galaxy collection list returns output like:
// # /usr/share/ansible/collections/ansible_collections
// Collection Version
// ---------------- -------
// community.docker 3.10.0
//
// If the collection is not installed, it won't appear in the output.
if !strings.Contains(stdout.String(), "community.docker") {
return fmt.Errorf("community.docker collection is not installed. Run: ansible-galaxy collection install community.docker")
}
return nil
}
// Deploy runs Ansible playbook for the given inventory
func (e *Executor) Deploy(
ctx context.Context,
inventoryPath string,
deployType string,
) ([]DeployResult, error) {
if deployType != "docker" && deployType != "binary" {
return nil, fmt.Errorf("invalid deploy type %q: %w", deployType, ErrUnknownDeployType)
}
func (e *Executor) Deploy(ctx context.Context, inventoryPath string, deployType string) ([]DeployResult, error) {
playbookName := "binary_deploy.yml"
if deployType == "docker" {
playbookName = "docker_deploy.yml"
@@ -108,8 +62,6 @@ func (e *Executor) Deploy(
cmd := exec.CommandContext(ctx, "ansible-playbook",
"-i", inventoryPath,
"-e", fmt.Sprintf("backend_url=%s", e.backendURL),
"-e", fmt.Sprintf("grpc_url=%s", e.grpcServerHost+":"+e.grpcServerPort),
"-e", fmt.Sprintf("gitea_releases_url=%s", e.giteaReleasesURL),
playbookPath,
)
@@ -132,13 +84,8 @@ func (e *Executor) Deploy(
}
// DeployParallel runs Ansible playbook for multiple inventories in parallel
func (e *Executor) DeployParallel(
ctx context.Context,
inventoryPaths []string,
deployType string,
) (map[string][]DeployResult, error) {
func (e *Executor) DeployParallel(ctx context.Context, inventoryPaths []string, deployType string) (map[string][]DeployResult, error) {
var wg sync.WaitGroup
var mu sync.Mutex
results := make(map[string][]DeployResult)
errCh := make(chan error, len(inventoryPaths))
@@ -150,9 +97,7 @@ func (e *Executor) DeployParallel(
if err != nil {
errCh <- err
}
mu.Lock()
results[p] = res
mu.Unlock()
}(path)
}
+9 -8
View File
@@ -18,7 +18,6 @@ type InventoryHost struct {
Password string
DeployType string
Token string
GRPCURL string
}
// Inventory represents an Ansible inventory file
@@ -26,13 +25,15 @@ type Inventory struct {
Hosts []InventoryHost
}
const inventoryTemplateText = `{{- range $i, $host := .Hosts }}
{{ $host.Name }} ansible_host={{ $host.IP }} ansible_port={{ $host.Port }} ansible_user={{ $host.User }} ansible_connection=ssh{{ if eq $host.AuthMethod "key" }} ansible_ssh_private_key_file={{ $host.SSHKey }}{{ end }}{{ if eq $host.AuthMethod "password" }} ansible_ssh_pass={{ $host.Password }}{{ end }}
deploy_type={{ $host.DeployType }}
agent_token={{ $host.Token }}
agent_label={{ $host.Name }}
grpc_url={{ $host.GRPCURL }}
{{ end -}}`
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 {
+12 -39
View File
@@ -1,7 +1,6 @@
package ansible
// BinaryDeployPlaybook returns the Ansible playbook for binary deployment.
// Downloads the agent binary, writes config, and installs a systemd unit for automatic restart.
// BinaryDeployPlaybook returns the Ansible playbook for binary deployment
const BinaryDeployPlaybook = `---
- name: Deploy HellreigN Agent (Binary)
hosts: all
@@ -12,8 +11,8 @@ const BinaryDeployPlaybook = `---
backend_url: "{{ backend_url }}"
install_dir: /opt/hellreign
bin_name: hellreign-agent
service_name: hellreign-agent
cert_dir: "{{ install_dir }}/certs"
gitea_releases_url: "{{ gitea_releases_url | default('https://gitea.d3m0k1d.ru/d3m0k1d/HellreigN/releases/latest/download') }}"
tasks:
- name: Create installation directory
@@ -30,7 +29,7 @@ const BinaryDeployPlaybook = `---
- name: Download HellreigN Agent binary
get_url:
url: "{{ gitea_releases_url }}/{{ bin_name }}"
url: "https://gitea.d3m0k1d.ru/d3m0k1d/HellreigN/releases/latest/download/{{ bin_name }}"
dest: "{{ install_dir }}/{{ bin_name }}"
mode: '0755'
@@ -38,23 +37,18 @@ const BinaryDeployPlaybook = `---
copy:
content: |
backend_url: "{{ backend_url }}"
grpc_url: "{{ grpc_url | default('localhost:9001') }}"
label: "{{ agent_label }}"
registration_token: "{{ agent_token }}"
cert_dir: "{{ cert_dir }}"
services:
- name: system
type: journald
dest: "{{ install_dir }}/config.yml"
mode: '0644'
- name: Create systemd unit file
- name: Create systemd service file
copy:
content: |
[Unit]
Description=HellreigN Agent
After=network-online.target
Wants=network-online.target
After=network.target
[Service]
Type=simple
@@ -62,10 +56,12 @@ const BinaryDeployPlaybook = `---
Restart=always
RestartSec=5
Environment=CONFIG_FILE={{ install_dir }}/config.yml
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
dest: /etc/systemd/system/hellreign-agent.service
dest: /etc/systemd/system/{{ service_name }}.service
mode: '0644'
- name: Reload systemd daemon
@@ -74,20 +70,12 @@ const BinaryDeployPlaybook = `---
- name: Enable and start HellreigN Agent service
systemd:
name: hellreign-agent
name: "{{ service_name }}"
enabled: yes
state: started
- name: Wait for agent to start
pause:
seconds: 3
- name: Verify HellreigN Agent is running
command: systemctl is-active --quiet hellreign-agent
changed_when: false
`
// DockerDeployPlaybook returns the Ansible playbook for Docker deployment.
// DockerDeployPlaybook returns the Ansible playbook for Docker deployment
const DockerDeployPlaybook = `---
- name: Deploy HellreigN Agent (Docker)
hosts: all
@@ -96,12 +84,9 @@ const DockerDeployPlaybook = `---
agent_label: "{{ agent_label }}"
agent_token: "{{ agent_token }}"
backend_url: "{{ backend_url }}"
grpc_url: "{{ grpc_url | default('localhost:9001') }}"
container_name: hellreign-agent-{{ agent_label }}
image: "gitea.d3m0k1d.ru/d3m0k1d/hellreign-agent:latest"
install_dir: /opt/hellreign
cert_dir: /etc/hellreign-agent/certs
config_dir: /etc/hellreign-agent
tasks:
- name: Install Docker (if not present)
@@ -123,12 +108,6 @@ const DockerDeployPlaybook = `---
state: directory
mode: '0755'
- name: Create configuration directory
file:
path: "{{ config_dir }}"
state: directory
mode: '0755'
- name: Pull HellreigN Agent image
community.docker.docker_image:
name: "{{ image }}"
@@ -138,15 +117,10 @@ const DockerDeployPlaybook = `---
copy:
content: |
backend_url: "{{ backend_url }}"
grpc_url: "{{ grpc_url | default('localhost:9001') }}"
label: "{{ agent_label }}"
registration_token: "{{ agent_token }}"
cert_dir: "{{ cert_dir }}"
services:
- name: "{{ agent_label }}"
type: docker
path: "{{ container_name }}"
dest: "{{ config_dir }}/config.yml"
dest: "{{ cert_dir }}/config.yml"
mode: '0644'
- name: Create and run HellreigN Agent container
@@ -157,7 +131,6 @@ const DockerDeployPlaybook = `---
restart_policy: always
volumes:
- "{{ cert_dir }}:/etc/hellreign-agent/certs"
- "{{ config_dir }}/config.yml:/etc/hellreign-agent/config.yml:ro"
env:
CONFIG_FILE: /etc/hellreign-agent/config.yml
CONFIG_FILE: /etc/hellreign-agent/certs/config.yml
`
+3 -2
View File
@@ -1,4 +1,5 @@
package ansible
// This package contains embedded Ansible templates for playbooks and inventory generation.
// All templates are defined in playbooks.go and inventory.go.
const BaseInvTemplate = `
`
+46 -25
View File
@@ -4,6 +4,7 @@ import (
"fmt"
"io"
"log"
"sync"
"time"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/repository"
@@ -12,19 +13,26 @@ import (
"google.golang.org/grpc/metadata"
)
// Collector handles log streaming from connected agents.
type Collector struct {
proto.UnimplementedCollectorServer
logRepo *repository.LogRepository
tracker *ConnTracker
agents map[string]*Agent
mu sync.RWMutex
batchSize int
flushInterval time.Duration
}
func New(logRepo *repository.LogRepository, tracker *ConnTracker) *Collector {
type Agent struct {
ID string
Label string
Services []string
ConnectedAt time.Time
}
func New(logRepo *repository.LogRepository) *Collector {
return &Collector{
logRepo: logRepo,
tracker: tracker,
agents: make(map[string]*Agent),
batchSize: 100,
flushInterval: 2 * time.Second,
}
@@ -48,24 +56,33 @@ func (c *Collector) Stream(stream proto.Collector_StreamServer) error {
}
service := serviceVals[0]
agent := &Agent{
ID: agentName,
Label: agentName,
Services: make([]Service, 0),
ConnectedAt: time.Now(),
servicesVals := md["services"]
var services []string
if len(servicesVals) > 0 {
services = servicesVals
}
c.tracker.Register(agent)
defer c.tracker.Unregister(agent.ID)
// 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,
)
log.Printf("Warning: ClickHouse not connected yet, consuming logs without storing for agent %s", agentName)
for {
_, err := stream.Recv()
if err == io.EOF {
@@ -103,12 +120,7 @@ func (c *Collector) Stream(stream proto.Collector_StreamServer) error {
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,
)
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)
@@ -119,6 +131,7 @@ func (c *Collector) Stream(stream proto.Collector_StreamServer) error {
for {
select {
case <-stream.Context().Done():
// Context cancelled, flush remaining
_ = flush()
return stream.Context().Err()
case <-ticker.C:
@@ -141,6 +154,7 @@ func (c *Collector) Stream(stream proto.Collector_StreamServer) error {
}
case err := <-errCh:
if err == io.EOF {
// Client closed stream
return flush()
}
return fmt.Errorf("failed to receive: %w", err)
@@ -148,12 +162,19 @@ func (c *Collector) Stream(stream proto.Collector_StreamServer) error {
}
}
// GetAgent delegates to the tracker.
func (c *Collector) GetAgent(name string) (*Agent, bool) {
return c.tracker.GetAgent(name)
c.mu.RLock()
defer c.mu.RUnlock()
a, ok := c.agents[name]
return a, ok
}
// Agents delegates to the tracker.
func (c *Collector) Agents() []*Agent {
return c.tracker.Agents()
c.mu.RLock()
defer c.mu.RUnlock()
result := make([]*Agent, 0, len(c.agents))
for _, a := range c.agents {
result = append(result, a)
}
return result
}
@@ -1,38 +0,0 @@
package collector
import (
"context"
"fmt"
"log"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/proto/proto"
"google.golang.org/grpc/metadata"
)
// ReportServices handles a unary service status update from an agent.
// Agents send their current services list, which is stored in the collector.
func (c *Collector) ReportServices(ctx context.Context, req *proto.ServicesUpdate) (*proto.ServicesUpdateResp, error) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, fmt.Errorf("no metadata in context")
}
whoamiVals := md["whoami"]
if len(whoamiVals) == 0 {
return nil, fmt.Errorf("whoami metadata missing")
}
agentName := whoamiVals[0]
services := make([]Service, 0, len(req.Services))
for _, s := range req.Services {
services = append(services, Service{s.Name, s.Status})
}
if ok := c.tracker.UpdateServices(agentName, services); ok {
log.Printf("Updated services for agent %s: %v", agentName, services)
} else {
log.Printf("Warning: received services update for unknown agent %s", agentName)
}
return &proto.ServicesUpdateResp{}, nil
}
@@ -1,111 +0,0 @@
package collector
import (
"context"
"log"
"sync"
"time"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/stats"
)
// ConnTracker tracks connected Collector agents and handles cleanup on disconnect.
// It implements grpc.StatsHandler for disconnect detection.
type ConnTracker struct {
mu sync.RWMutex
agents map[string]*Agent
}
func NewConnTracker() *ConnTracker {
return &ConnTracker{
agents: make(map[string]*Agent),
}
}
// Register adds an agent to the tracker. Called by Collector.Stream().
func (t *ConnTracker) Register(agent *Agent) {
t.mu.Lock()
t.agents[agent.ID] = agent
t.mu.Unlock()
log.Printf("[collector] agent registered: %s", agent.ID)
}
// Unregister removes an agent from the tracker.
func (t *ConnTracker) Unregister(id string) {
t.mu.Lock()
delete(t.agents, id)
t.mu.Unlock()
log.Printf("[collector] agent unregistered: %s", id)
}
// GetAgent returns the agent for the given ID.
func (t *ConnTracker) GetAgent(id string) (*Agent, bool) {
t.mu.RLock()
defer t.mu.RUnlock()
a, ok := t.agents[id]
return a, ok
}
// Agents returns all connected agents.
func (t *ConnTracker) Agents() []*Agent {
t.mu.RLock()
defer t.mu.RUnlock()
result := make([]*Agent, 0, len(t.agents))
for _, a := range t.agents {
result = append(result, a)
}
return result
}
// grpc.StatsHandler implementation.
func (t *ConnTracker) TagRPC(ctx context.Context, _ *stats.RPCTagInfo) context.Context {
return ctx
}
func (t *ConnTracker) HandleRPC(ctx context.Context, _ stats.RPCStats) {}
func (t *ConnTracker) TagConn(ctx context.Context, _ *stats.ConnTagInfo) context.Context {
return ctx
}
func (t *ConnTracker) HandleConn(ctx context.Context, s stats.ConnStats) {
switch s.(type) {
case *stats.ConnEnd:
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return
}
whoamiVals := md["whoami"]
if len(whoamiVals) == 0 {
return
}
t.Unregister(whoamiVals[0])
}
}
// UpdateServices updates the services list for the given agent.
func (t *ConnTracker) UpdateServices(id string, services []Service) bool {
t.mu.Lock()
defer t.mu.Unlock()
agent, ok := t.agents[id]
if !ok {
return false
}
agent.Services = services
return true
}
// Service represents a named service with its current status.
type Service struct {
Name, Status string
}
// Agent represents a connected agent streaming logs to the collector.
type Agent struct {
ID string
Label string
Services []Service
ConnectedAt time.Time
}
+75 -133
View File
@@ -12,30 +12,27 @@ import (
"golang.org/x/sync/errgroup"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/stats"
)
// Commander handles command execution on connected agents.
type Commander struct {
proto.UnimplementedCommanderServer
tracker *ConnTracker
jobber Jobber
agents map[string]Agent
mu sync.RWMutex
jobber Jobber
}
// Jobber persists job state.
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, tracker *ConnTracker) *Commander {
func New(jobber Jobber) *Commander {
return &Commander{
jobber: jobber,
tracker: tracker,
agents: make(map[string]Agent),
jobber: jobber,
}
}
// Agent represents a connected agent with an active bidirectional stream.
type Agent struct {
bidi grpc.BidiStreamingServer[proto.FinishedCommand, proto.Command]
in chan *proto.Command
@@ -44,11 +41,10 @@ type Agent struct {
ctx context.Context
aid string
Token string
Token string // agent id
Label string
Services []string
}
type JobOut struct {
fc models.Job
err error
@@ -58,93 +54,54 @@ type Job struct {
out chan JobOut
}
// ConnTracker tracks connected agents and handles cleanup on disconnect.
// It implements grpc.StatsHandler for disconnect detection.
type ConnTracker struct {
mu sync.RWMutex
agents map[string]*Agent
}
// GetAgentByLabel searches for an agent by its human-readable label.
func (self *ConnTracker) GetAgentByLabel(label string) (agent Agent, ok bool) {
func (self *Commander) GetAgent(aid string) (agent Agent, ok bool) {
self.mu.RLock()
defer self.mu.RUnlock()
for _, a := range self.agents {
if a.Label == label {
return *a, true
}
}
agent, ok = self.agents[aid]
return
}
func NewConnTracker() *ConnTracker {
return &ConnTracker{
agents: make(map[string]*Agent),
}
}
func (t *ConnTracker) Register(aid string, agent *Agent) {
t.mu.Lock()
t.agents[aid] = agent
t.mu.Unlock()
log.Printf("[conntracker] agent registered: %s", aid)
}
func (t *ConnTracker) Unregister(aid string) {
t.mu.Lock()
delete(t.agents, aid)
t.mu.Unlock()
log.Printf("[conntracker] agent unregistered: %s", aid)
}
func (t *ConnTracker) GetAgent(aid string) (*Agent, bool) {
t.mu.RLock()
defer t.mu.RUnlock()
a, ok := t.agents[aid]
return a, ok
}
func (t *ConnTracker) Agents() []*Agent {
t.mu.RLock()
defer t.mu.RUnlock()
result := make([]*Agent, 0, len(t.agents))
for _, a := range t.agents {
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
}
// grpc.StatsHandler implementation.
func (t *ConnTracker) TagRPC(ctx context.Context, _ *stats.RPCTagInfo) context.Context {
return ctx
func (self *Commander) removeAgent(aid string) {
self.mu.Lock()
defer self.mu.Unlock()
delete(self.agents, aid)
}
func (t *ConnTracker) HandleRPC(ctx context.Context, _ stats.RPCStats) {}
func (t *ConnTracker) TagConn(ctx context.Context, _ *stats.ConnTagInfo) context.Context {
return ctx
}
func (t *ConnTracker) HandleConn(ctx context.Context, s stats.ConnStats) {
switch s.(type) {
case *stats.ConnEnd:
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return
}
aidVals := md["agentid"]
if len(aidVals) == 0 {
return
}
t.Unregister(aidVals[0])
func (self *Agent) AddJob(job models.JobForInsert) (int64, error) {
log.Printf("[DEBUG] AddJob: agent=%s, command=%v", self.aid, job.Command)
jid, err := self.jobber.InitJob(self.ctx, self.aid, job)
if err != nil {
log.Printf("[DEBUG] AddJob: InitJob failed: %v", err)
return 0, err
}
log.Printf("[DEBUG] AddJob: InitJob returned jid=%d, sending to self.in channel", jid)
self.in <- &proto.Command{
Id: jid,
Command: job.Command,
Stdin: job.Stdin,
}
log.Printf("[DEBUG] AddJob: sent to self.in channel successfully")
return jid, err
}
// Stream handles a new agent connection and runs the send/recv loops.
func (c *Commander) Stream(
bidi grpc.BidiStreamingServer[proto.FinishedCommand, proto.Command],
) error {
func (self *Agent) WaitJob(jid int64) (*models.Job, error) {
log.Printf("[DEBUG] WaitJob: agent=%s, jid=%d, waiting on self.jobs[%d].out", self.aid, jid, jid)
result := <-self.jobs[jid].out
log.Printf("[DEBUG] WaitJob: agent=%s, jid=%d, received result", self.aid, jid)
return &result.fc, result.err
}
func (self *Commander) Stream(bidi grpc.BidiStreamingServer[proto.FinishedCommand, proto.Command]) error {
md, ok := metadata.FromIncomingContext(bidi.Context())
if !ok {
return fmt.Errorf("no metadata in context")
@@ -156,95 +113,80 @@ func (c *Commander) Stream(
aid := aidVals[0]
var label string
if vals := md["label"]; len(vals) > 0 {
label = vals[0]
labelVals := md["label"]
if len(labelVals) > 0 {
label = labelVals[0]
}
agent := NewAgent(bidi.Context(), c.jobber, aid, label)
agent.bidi = bidi
c.tracker.Register(aid, agent)
defer c.tracker.Unregister(aid)
agent := newAgent(bidi, self.jobber, aid, label)
self.mu.Lock()
self.agents[aid] = agent
self.mu.Unlock()
defer self.removeAgent(aid)
return agent.run()
}
// GetAgent returns the agent by ID. Delegates to the tracker.
func (c *Commander) GetAgent(aid string) (*Agent, bool) {
return c.tracker.GetAgent(aid)
}
func (a *Agent) AddJob(job models.JobForInsert) (int64, error) {
jid, err := a.jobber.InitJob(a.ctx, a.aid, job)
if err != nil {
return 0, err
}
a.jobs[jid] = newJob()
a.in <- &proto.Command{
Id: jid,
Command: job.Command,
Stdin: job.Stdin,
}
return jid, nil
}
func (a *Agent) WaitJob(jid int64) (*models.Job, error) {
result := <-a.jobs[jid].out
return &result.fc, result.err
}
func (a *Agent) run() error {
func (self *Agent) run() error {
wg := new(errgroup.Group)
wg.Go(a.recv)
wg.Go(a.send)
wg.Go(self.recv)
wg.Go(self.send)
return wg.Wait()
}
func (a *Agent) recv() error {
func (self *Agent) recv() error {
for {
job, err := func() (job models.Job, err error) {
msg, err := a.bidi.Recv()
msg, err := self.bidi.Recv()
if err != nil {
return
}
return a.jobber.UpdateJobInDB(a.ctx, msg.Id, models.JobForUpdate{
log.Printf("[DEBUG] recv: agent=%s, received finished job id=%d", self.aid, msg.Id)
return self.jobber.UpdateJobInDB(self.ctx, msg.Id, models.JobForUpdate{
Stdout: msg.Stdout,
Stderr: msg.Stderr,
Status: msg.Status,
})
}()
if err == io.EOF {
log.Printf("[DEBUG] recv: agent=%s, EOF received", self.aid)
return nil
}
out := a.jobs[job.ID].out
if err != nil {
log.Printf("[DEBUG] recv: agent=%s, error: %v", self.aid, err)
}
out := self.jobs[job.ID].out
out <- JobOut{
fc: job,
err: err,
}
close(out)
log.Printf("[DEBUG] recv: agent=%s, sent result for job id=%d", self.aid, job.ID)
}
}
func (a *Agent) send() error {
for job := range a.in {
if err := a.bidi.Send(job); err != nil {
func (self *Agent) send() error {
for job := range self.in {
log.Printf("[DEBUG] send: agent=%s, job id=%d, command=%v", self.aid, job.Id, job.Command)
self.jobs[job.Id] = newJob()
if err := self.bidi.Send(job); err != nil {
log.Printf("[DEBUG] send: agent=%s, failed to send job id=%d: %v", self.aid, job.Id, err)
return err
}
log.Printf("[DEBUG] send: agent=%s, sent job id=%d to agent", self.aid, job.Id)
}
log.Printf("[DEBUG] send: agent=%s, self.in channel closed", self.aid)
return io.EOF
// self.jobs[]
}
func NewAgent(
ctx context.Context,
jobber Jobber,
aid string,
label string,
) *Agent {
return &Agent{
in: make(chan *proto.Command, 10),
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: ctx,
ctx: bidi.Context(),
aid: aid,
Label: label,
Token: aid,
+6 -68
View File
@@ -29,33 +29,22 @@ func NewAgentDeployGroup(h *Handlers) *AgentDeployGroup {
grpcPort = "9001"
}
grpcHost := os.Getenv("GRPC_SERVER_HOST")
if grpcHost == "" {
grpcHost = "0.0.0.0"
}
backendURL := os.Getenv("BACKEND_URL")
if backendURL == "" {
backendURL = "http://localhost:8080"
}
giteaURL := os.Getenv("GITEA_RELEASES_URL")
if giteaURL == "" {
giteaURL = "https://gitea.d3m0k1d.ru/d3m0k1d/HellreigN/releases/latest/download"
}
exec := ansible.NewExecutor(ansible.ExecutorConfig{
WorkDir: workDir,
GRPCServerHost: grpcHost,
GRPCServerHost: "0.0.0.0", // TODO: make configurable
GRPCServerPort: grpcPort,
BackendURL: backendURL,
GiteaReleasesURL: giteaURL,
BackendURL: backendURL,
})
// Write playbooks on init
if err := exec.WriteAllPlaybooks(); err != nil {
// Log the error - deployment will fail later if playbooks can't be written
fmt.Fprintf(os.Stderr, "WARNING: failed to write Ansible playbooks: %v\n", err)
// Log but don't fail - playbooks can be written later
_ = err
}
return &AgentDeployGroup{
@@ -83,48 +72,6 @@ func (adg *AgentDeployGroup) DeployAgents(c *gin.Context) {
return
}
// Validate auth credentials for each server
for i, server := range req.Servers {
switch server.AuthMethod {
case repository.AuthMethodKey:
if server.SSHKey == "" {
c.JSON(http.StatusBadRequest, gin.H{
"error": fmt.Sprintf("server %d (%s): sshKey is required when authMethod is 'key'", i, server.IP),
})
return
}
case repository.AuthMethodPassword:
if server.Password == "" {
c.JSON(http.StatusBadRequest, gin.H{
"error": fmt.Sprintf("server %d (%s): password is required when authMethod is 'password'", i, server.IP),
})
return
}
default:
c.JSON(http.StatusBadRequest, gin.H{
"error": fmt.Sprintf("server %d (%s): invalid authMethod %q, expected 'key' or 'password'", i, server.IP, server.AuthMethod),
})
return
}
}
// Pre-flight check: verify community.docker collection is available for docker deployments
needsDockerCollection := false
for _, server := range req.Servers {
if server.DeployType == repository.DeployTypeDocker {
needsDockerCollection = true
break
}
}
if needsDockerCollection {
if err := adg.executor.CheckDockerCollection(); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": fmt.Sprintf("Docker deployment requires 'community.docker' Ansible collection: %v", err),
})
return
}
}
// Create work directory
workDir := adg.executor.WorkDir()
if err := os.MkdirAll(workDir, 0755); err != nil {
@@ -170,14 +117,11 @@ func (adg *AgentDeployGroup) DeployAgents(c *gin.Context) {
Password: server.Password,
DeployType: string(server.DeployType),
Token: token,
GRPCURL: adg.executor.GRPCURL(),
},
}
inventoryPath := filepath.Join(workDir, fmt.Sprintf("inventory_%d_%d", timestamp, i))
if err := ansible.GenerateInventory(inventoryHosts, inventoryPath); err != nil {
// Rollback: delete the token we just created
_ = adg.Repo.DeleteRegistrationToken(token)
results = append(results, repository.DeployResult{
IP: server.IP,
AgentLabel: server.AgentLabel,
@@ -191,14 +135,10 @@ func (adg *AgentDeployGroup) DeployAgents(c *gin.Context) {
// Run Ansible playbook for this server
deployResults, err := adg.executor.Deploy(ctx, inventoryPath, string(server.DeployType))
// Clean up inventory file (log error but don't fail deployment)
if cleanupErr := os.Remove(inventoryPath); cleanupErr != nil {
fmt.Fprintf(os.Stderr, "WARNING: failed to remove inventory file %s: %v\n", inventoryPath, cleanupErr)
}
// Clean up inventory file
os.Remove(inventoryPath)
if err != nil {
// Rollback: delete the token since deployment failed
_ = adg.Repo.DeleteRegistrationToken(token)
results = append(results, repository.DeployResult{
IP: server.IP,
AgentLabel: server.AgentLabel,
@@ -214,8 +154,6 @@ func (adg *AgentDeployGroup) DeployAgents(c *gin.Context) {
if len(deployResults) > 0 && !deployResults[0].Success {
success = false
errMsg = deployResults[0].Stderr
// Rollback: delete the token since ansible playbook reported failure
_ = adg.Repo.DeleteRegistrationToken(token)
}
results = append(results, repository.DeployResult{
+1 -1
View File
@@ -104,7 +104,7 @@ func (arg *AgentRegistrationGroup) Register(c *gin.Context) {
}
type RegisterRequest struct {
CSR string `json:"csr" binding:"required"`
CSR string `json:"csr" binding:"required"`
Token string `json:"token" binding:"required"`
}
+6 -14
View File
@@ -1,11 +1,9 @@
package handlers
import (
"fmt"
"net/http"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/grpcsrv/collector"
"github.com/gin-gonic/gin"
"net/http"
)
type AgentsGroup struct {
@@ -17,19 +15,17 @@ func NewAgentsGroup(h *Handlers, coll *collector.Collector) AgentsGroup {
return AgentsGroup{Handlers: h, collector: coll}
}
// AgentInfo represents a connected agent's current status.
type AgentInfo struct {
Token string `json:"token" example:"agent-001"` // Unique agent identifier
Label string `json:"label" example:"web-server-1"` // Human-readable label
Services []string `json:"services" example:"nginx:running,redis:up"` // List of services with status (format: "name:status")
ConnectedAt string `json:"connected_at" example:"2026-04-04 10:30:00"` // Time when agent connected (RFC3339-like)
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
// @Accept json
// @Produce json
// @Success 200 {array} AgentInfo
// @Router /agents [get]
@@ -37,14 +33,10 @@ func (ag *AgentsGroup) List(c *gin.Context) {
agents := make([]AgentInfo, 0)
for _, agent := range ag.collector.Agents() {
services := make([]string, 0, len(agent.Services))
for _, s := range agent.Services {
services = append(services, fmt.Sprintf("%s:%s", s.Name, s.Status))
}
agents = append(agents, AgentInfo{
Token: agent.ID,
Label: agent.Label,
Services: services,
Services: agent.Services,
ConnectedAt: agent.ConnectedAt.Format("2006-01-02 15:04:05"),
})
}
-47
View File
@@ -2,8 +2,6 @@ package handlers
import (
"errors"
"fmt"
"log"
"net/http"
"strings"
@@ -51,39 +49,6 @@ func (ag *AuthGroup) Login(c *gin.Context) {
c.JSON(http.StatusOK, resp)
}
// RegisterUser registers a new user with all permissions set to false.
// @Summary Register user
// @Description Registers a new user with login, password, name, last name. All permissions are set to false.
// @Tags auth
// @Accept json
// @Param request body repository.UserRegister true "Registration data"
// @Success 200 {object} map[string]string
// @Failure 400 {object} map[string]string
// @Failure 409 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /auth/register [post]
func (ag *AuthGroup) RegisterUser(c *gin.Context) {
var req repository.UserRegister
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
id, err := ag.Repo.RegisterUser(req)
if err != nil {
if strings.Contains(err.Error(), "UNIQUE constraint") {
c.JSON(http.StatusConflict, gin.H{"error": "login already exists"})
return
}
log.Printf("[register] failed: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to register user: %v", err)})
return
}
log.Printf("[register] user registered: id=%s login=%s", id, req.Login)
c.JSON(http.StatusOK, gin.H{"message": "user registered"})
}
// CreateToken creates a new user.
// @Summary Create user
// @Description Creates a new user with permissions
@@ -94,7 +59,6 @@ func (ag *AuthGroup) RegisterUser(c *gin.Context) {
// @Failure 400 {object} map[string]string
// @Failure 401 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Security Bearer
// @Router /auth/token [post]
func (ag *AuthGroup) CreateToken(c *gin.Context) {
var tc repository.TokenCreate
@@ -118,7 +82,6 @@ func (ag *AuthGroup) CreateToken(c *gin.Context) {
// @Produce json
// @Success 200 {object} repository.Tokens
// @Failure 401 {object} map[string]string
// @Security Bearer
// @Router /auth/validate [get]
func (ag *AuthGroup) ValidateToken(c *gin.Context) {
tokenVal, exists := c.Get(string(tokenContextKey))
@@ -143,7 +106,6 @@ func (ag *AuthGroup) ValidateToken(c *gin.Context) {
// @Produce json
// @Success 200 {array} repository.Tokens
// @Failure 500 {object} map[string]string
// @Security Bearer
// @Router /auth/tokens [get]
func (ag *AuthGroup) ListTokens(c *gin.Context) {
tokens, err := ag.Repo.ListTokens()
@@ -162,7 +124,6 @@ func (ag *AuthGroup) ListTokens(c *gin.Context) {
// @Success 200 {object} map[string]string
// @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Security Bearer
// @Router /auth/tokens/:login [delete]
func (ag *AuthGroup) DeleteToken(c *gin.Context) {
login := c.Param("login")
@@ -190,7 +151,6 @@ func (ag *AuthGroup) DeleteToken(c *gin.Context) {
// @Success 200 {object} map[string]string
// @Failure 401 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Security Bearer
// @Router /auth/token [delete]
func (ag *AuthGroup) DeleteMyToken(c *gin.Context) {
tokenVal, exists := c.Get(string(tokenContextKey))
@@ -222,7 +182,6 @@ func (ag *AuthGroup) DeleteMyToken(c *gin.Context) {
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Security Bearer
// @Router /auth/users/:login/activate [post]
func (ag *AuthGroup) ActivateUser(c *gin.Context) {
login := c.Param("login")
@@ -252,7 +211,6 @@ func (ag *AuthGroup) ActivateUser(c *gin.Context) {
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Security Bearer
// @Router /auth/users/:login/deactivate [post]
func (ag *AuthGroup) DeactivateUser(c *gin.Context) {
login := c.Param("login")
@@ -280,7 +238,6 @@ func (ag *AuthGroup) DeactivateUser(c *gin.Context) {
// @Produce json
// @Success 200 {array} repository.Tokens
// @Failure 500 {object} map[string]string
// @Security Bearer
// @Router /auth/users/inactive [get]
func (ag *AuthGroup) ListInactiveUsers(c *gin.Context) {
tokens, err := ag.Repo.ListInactiveTokens()
@@ -301,7 +258,6 @@ func (ag *AuthGroup) ListInactiveUsers(c *gin.Context) {
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Security Bearer
// @Router /auth/users/:login [get]
func (ag *AuthGroup) GetUser(c *gin.Context) {
login := c.Param("login")
@@ -334,7 +290,6 @@ func (ag *AuthGroup) GetUser(c *gin.Context) {
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Security Bearer
// @Router /auth/users/:login [put]
func (ag *AuthGroup) UpdateUser(c *gin.Context) {
login := c.Param("login")
@@ -372,7 +327,6 @@ func (ag *AuthGroup) UpdateUser(c *gin.Context) {
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Security Bearer
// @Router /auth/users/:login/permissions [put]
func (ag *AuthGroup) UpdateUserPermissions(c *gin.Context) {
login := c.Param("login")
@@ -410,7 +364,6 @@ func (ag *AuthGroup) UpdateUserPermissions(c *gin.Context) {
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Security Bearer
// @Router /auth/users/:login/password [put]
func (ag *AuthGroup) ResetUserPassword(c *gin.Context) {
login := c.Param("login")
-35
View File
@@ -1,35 +0,0 @@
package handlers
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/samber/lo"
)
func CorsMiddleware(origincfg string) gin.HandlerFunc {
origins := strings.Split(origincfg, ";")
if origins[0] == "" {
panic("zero cors origins wtf is your config")
}
return func(c *gin.Context) {
origin := c.GetHeader("Origin")
if !lo.Contains(origins, origin) {
origin = origins[0]
}
c.Writer.Header().Set("Access-Control-Allow-Origin", origin)
// c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
c.Writer.Header().
Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, Authorization")
c.Writer.Header().
Set("Access-Control-Allow-Methods", "OPTIONS, GET, POST, PATCH, DELETE, PUT")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(http.StatusNoContent)
return
}
c.Next()
}
}
+63 -216
View File
@@ -1,48 +1,32 @@
package handlers
import (
"errors"
"fmt"
"log"
"net/http"
"os/exec"
"strconv"
"time"
"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 JobsHandlers struct {
tracker *commander.ConnTracker
svc *service.ScriptService
whereami string
jobRepo *repository.JobRepository
cmder *commander.Commander
svc *service.ScriptService
}
func NewJobsHandlers(tracker *commander.ConnTracker, svc *service.ScriptService, whereami string, jobRepo *repository.JobRepository) JobsHandlers {
return JobsHandlers{tracker: tracker, svc: svc, whereami: whereami, jobRepo: jobRepo}
func NewJobsHandlers(cmder *commander.Commander, svc *service.ScriptService) JobsHandlers {
return JobsHandlers{cmder: cmder, svc: svc}
}
// AddJobIn is the request body for creating a job.
type AddJobIn struct {
Command string `json:"command" binding:"required"`
Command string `json:"command" binding:"required"`
InterpreterID int64 `json:"interpreter_id"`
Stdin *string `json:"stdin"`
AgentID string `json:"agent_id" binding:"required"`
AgentID string `json:"agent_id" binding:"required"`
}
// AddJobOut is the response body for a submitted job.
type AddJobOut struct {
ID int64 `json:"id"`
Command []string `json:"command"`
WaitURL string `json:"wait_url"`
}
// JobResult is the response body for a completed job.
type JobResult struct {
ID int64 `json:"id"`
Command []string `json:"command"`
Stdin *string `json:"stdin"`
@@ -51,209 +35,72 @@ type JobResult struct {
Status int32 `json:"status"`
}
// WaitJobIn is the request body for waiting on a job.
type WaitJobIn struct {
AgentID string `json:"agent_id" binding:"required"`
}
// AddJob submits a job to an agent and returns a wait_url for the result.
// @Summary Submit a job to an agent
// @Description Sends a command to the specified agent and returns a URL to wait for the result
// 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 (h *JobsHandlers) AddJob(c *gin.Context) {
var in AddJobIn
if err := c.Bind(&in); err != nil {
c.Error(err)
return
}
result, err := h.runCommand(c, in.AgentID, in.InterpreterID, in.Command, in.Stdin)
if err != nil {
c.Error(err)
return
}
c.JSON(http.StatusCreated, result)
}
// runCommand resolves command, submits a job to the agent, and returns AddJobOut.
// Shared between jobs and scripts handlers.
func (h *JobsHandlers) runCommand(
c *gin.Context,
agentID string,
interpID int64,
command string,
stdin *string,
) (*AddJobOut, error) {
agent, ok := h.tracker.GetAgent(agentID)
if !ok {
return nil, fmt.Errorf("agent not found")
}
cmd, err := resolveCommand(c, h.svc, interpID, command)
if err != nil {
return nil, err
}
jid, err := agent.AddJob(models.JobForInsert{
Command: cmd,
Stdin: stdin,
})
if err != nil {
return nil, err
}
waitURL := fmt.Sprintf("%s/api/v1/jobs/%d/wait", h.whereami, jid)
return &AddJobOut{
ID: jid,
Command: cmd,
WaitURL: waitURL,
}, nil
}
// WaitJob waits for a submitted job to complete (long-poll).
// If the job is already done, returns immediately.
// @Summary Wait for job result
// @Description Long-polls for a job result. Returns immediately if the job is already finished.
// @Tags jobs
// @Accept json
// @Produce json
// @Param id path int true "Job ID"
// @Param body body WaitJobIn true "Agent reference"
// @Success 200 {object} JobResult
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Router /jobs/{id}/wait [post]
func (h *JobsHandlers) WaitJob(c *gin.Context) {
jid, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid job id"})
return
}
var in WaitJobIn
if err := c.Bind(&in); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
agent, ok := h.tracker.GetAgent(in.AgentID)
if !ok {
c.Status(http.StatusNotFound)
c.Error(fmt.Errorf("agent not found"))
return
}
job, err := agent.WaitJob(jid)
if err != nil {
c.Error(err)
return
}
c.JSON(http.StatusOK, JobResult{
ID: job.ID,
Command: job.Command,
Stdin: job.Stdin,
Stdout: job.Stdout,
Stderr: job.Stderr,
Status: job.Status,
})
}
func resolveCommand(c *gin.Context, svc *service.ScriptService, interpID int64, cmd string) ([]string, error) {
if interpID == 0 {
return []string{"sh", "-c", cmd}, nil
}
command, err := svc.ResolveCommand(c.Request.Context(), interpID, cmd)
if err != nil {
return nil, err
}
return command, nil
}
// @Summary Check command path
// @Description Validates that a command binary exists on the system
// @Tags jobs
// @Accept json
// @Param body body CheckCmdIn true "Command to check"
// @Success 200 {object} CheckCmdOut
// @Failure 404 {object} map[string]string
// @Router /jobs/check_cmd [post]
func (h *JobsHandlers) CheckCmd(c *gin.Context) {
var in struct {
Command string `json:"command" binding:"required"`
}
if err := c.Bind(&in); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"})
return
}
if _, err := exec.LookPath(in.Command); err != nil {
if errors.Is(err, exec.ErrNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "command not found"})
return
func (self *JobsHandlers) AddJob(c *gin.Context) {
log.Printf("[DEBUG] AddJob handler: request received")
err := func() error {
var in AddJobIn
if err := c.Bind(&in); err != nil {
log.Printf("[DEBUG] AddJob handler: bind failed: %v", err)
return err
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
log.Printf("[DEBUG] AddJob handler: agent_id=%s, command=%s, interpreter_id=%d", in.AgentID, in.Command, in.InterpreterID)
agent, ok := self.cmder.GetAgent(in.AgentID)
if !ok {
log.Printf("[DEBUG] AddJob handler: agent %s not found", in.AgentID)
c.Status(http.StatusNotFound)
return fmt.Errorf("agent not found")
}
log.Printf("[DEBUG] AddJob handler: agent found, resolving command")
c.JSON(http.StatusOK, CheckCmdOut{Exists: true})
}
var command []string
if in.InterpreterID == 0 {
command = []string{"sh", "-c", in.Command}
} else {
var err error
command, err = self.svc.ResolveCommand(c.Request.Context(), in.InterpreterID, in.Command)
if err != nil {
log.Printf("[DEBUG] AddJob handler: ResolveCommand failed: %v", err)
return err
}
}
type CheckCmdIn struct {
Command string `json:"command" binding:"required" example:"bash"`
}
type CheckCmdOut struct {
Exists bool `json:"exists"`
}
// JobMetricsOut is the response body for the job metrics endpoint.
type JobMetricsOut struct {
Total int `json:"total"`
Success int `json:"success"`
Failed int `json:"failed"`
Pending int `json:"pending"`
Period string `json:"period"`
}
// GetJobMetrics returns job success metrics over a parameterized period.
// @Summary Get job metrics
// @Description Returns total, successful, failed, and pending job counts over the given period
// @Tags jobs
// @Produce json
// @Param period query string false "Time period (e.g. 1h, 24h, 7d)" default(24h)
// @Success 200 {object} JobMetricsOut
// @Failure 400 {object} map[string]string
// @Security Bearer
// @Router /jobs/metrics [get]
func (h *JobsHandlers) GetJobMetrics(c *gin.Context) {
periodStr := c.DefaultQuery("period", "24h")
period, err := time.ParseDuration(periodStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid period, use Go duration format (e.g. 1h, 24h, 7d)"})
return
}
since := time.Now().Add(-period)
metrics, err := h.jobRepo.GetJobMetrics(c.Request.Context(), since)
log.Printf("[DEBUG] AddJob handler: calling agent.AddJob with command=%v", command)
jid, err := agent.AddJob(models.JobForInsert{
Command: command,
Stdin: in.Stdin,
})
if err != nil {
log.Printf("[DEBUG] AddJob handler: agent.AddJob failed: %v", err)
return err
}
log.Printf("[DEBUG] AddJob handler: agent.AddJob returned jid=%d, calling WaitJob", jid)
job, err := agent.WaitJob(jid)
if err != nil {
log.Printf("[DEBUG] AddJob handler: agent.WaitJob failed: %v", err)
return err
}
log.Printf("[DEBUG] AddJob handler: agent.WaitJob returned job id=%d, status=%d", job.ID, job.Status)
c.JSON(http.StatusCreated, AddJobOut{
ID: job.ID,
Command: job.Command,
Stdin: job.Stdin,
Stdout: job.Stdout,
Stderr: job.Stderr,
Status: job.Status,
})
log.Printf("[DEBUG] AddJob handler: response sent")
return nil
}()
if err != nil {
c.Error(err)
return
}
c.JSON(http.StatusOK, JobMetricsOut{
Total: metrics.Total,
Success: metrics.Success,
Failed: metrics.Failed,
Pending: metrics.Pending,
Period: periodStr,
})
}
+9 -9
View File
@@ -20,10 +20,10 @@ func NewLogHandlers(logRepo *repository.LogRepository) *LogHandlers {
type InsertLogRequest struct {
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"`
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
@@ -105,13 +105,13 @@ func (lh *LogHandlers) InsertBatch(c *gin.Context) {
}
type SearchLogsRequest struct {
Level string `form:"level"`
Service string `form:"service"`
Agent string `form:"agent"`
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"`
Limit int `form:"limit"`
Offset int `form:"offset"`
}
// @Summary Search logs
+69 -61
View File
@@ -13,68 +13,81 @@ import (
)
type ScriptHandlers struct {
svc *service.ScriptService
tracker *commander.ConnTracker
whereami string
svc *service.ScriptService
cmder *commander.Commander
}
func NewScriptHandlers(svc *service.ScriptService, tracker *commander.ConnTracker, whereami string) ScriptHandlers {
return ScriptHandlers{svc: svc, tracker: tracker, whereami: whereami}
func NewScriptHandlers(svc *service.ScriptService, cmder *commander.Commander) ScriptHandlers {
return ScriptHandlers{svc: svc, cmder: cmder}
}
type RunScriptIn struct {
AgentID string `json:"agent_id" binding:"required"`
InterpreterID int64 `json:"interpreter_id" binding:"required"`
ScriptText string `json:"script_text" binding:"required"`
Stdin *string `json:"stdin"`
}
// RunScript submits a script as a job and returns a wait_url for the result.
// 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} AddJobOut
// @Security Bearer
// @Success 201 {object} RunScriptOut
// @Router /scripts/run [post]
func (h *ScriptHandlers) RunScript(c *gin.Context) {
var in RunScriptIn
if err := c.Bind(&in); err != nil {
c.Error(err)
return
}
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
}
agent, ok := h.tracker.GetAgent(in.AgentID)
if !ok {
c.Status(http.StatusNotFound)
c.Error(fmt.Errorf("agent not found"))
return
}
command, err := self.svc.ResolveCommand(c.Request.Context(), in.InterpreterID, in.ScriptText)
if err != nil {
return err
}
command, err := h.svc.ResolveCommand(c.Request.Context(), in.InterpreterID, in.ScriptText)
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)
return
}
jid, err := agent.AddJob(models.JobForInsert{
Command: command,
Stdin: in.Stdin,
})
if err != nil {
c.Error(err)
return
}
waitURL := fmt.Sprintf("%s/api/v1/jobs/%d/wait", h.whereami, jid)
c.JSON(http.StatusCreated, AddJobOut{
ID: jid,
Command: command,
WaitURL: waitURL,
})
}
// ListInterpreters returns all registered script interpreters.
@@ -83,10 +96,9 @@ func (h *ScriptHandlers) RunScript(c *gin.Context) {
// @Tags scripts
// @Produce json
// @Success 200 {array} repository.ScriptInterpreter
// @Security Bearer
// @Router /scripts/interpreters [get]
func (h *ScriptHandlers) ListInterpreters(c *gin.Context) {
interpreters, err := h.svc.List(c.Request.Context())
func (self *ScriptHandlers) ListInterpreters(c *gin.Context) {
interpreters, err := self.svc.List(c.Request.Context())
if err != nil {
c.Error(err)
return
@@ -102,16 +114,15 @@ func (h *ScriptHandlers) ListInterpreters(c *gin.Context) {
// @Produce json
// @Param body body repository.ScriptInterpreterCreate true "Interpreter definition"
// @Success 201 {object} repository.ScriptInterpreter
// @Security Bearer
// @Router /scripts/interpreters [post]
func (h *ScriptHandlers) CreateInterpreter(c *gin.Context) {
func (self *ScriptHandlers) CreateInterpreter(c *gin.Context) {
var in repository.ScriptInterpreterCreate
if err := c.BindJSON(&in); err != nil {
c.Error(err)
return
}
si, err := h.svc.Create(c.Request.Context(), in)
si, err := self.svc.Create(c.Request.Context(), in)
if err != nil {
c.Error(err)
return
@@ -126,16 +137,15 @@ func (h *ScriptHandlers) CreateInterpreter(c *gin.Context) {
// @Produce json
// @Param id path int true "Interpreter ID"
// @Success 200 {object} repository.ScriptInterpreter
// @Security Bearer
// @Router /scripts/interpreters/:id [get]
func (h *ScriptHandlers) GetInterpreter(c *gin.Context) {
func (self *ScriptHandlers) GetInterpreter(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.Error(err)
return
}
si, err := h.svc.GetByID(c.Request.Context(), id)
si, err := self.svc.GetByID(c.Request.Context(), id)
if err != nil {
c.Error(err)
return
@@ -152,9 +162,8 @@ func (h *ScriptHandlers) GetInterpreter(c *gin.Context) {
// @Param id path int true "Interpreter ID"
// @Param body body repository.ScriptInterpreterUpdate true "Interpreter fields"
// @Success 200 {object} repository.ScriptInterpreter
// @Security Bearer
// @Router /scripts/interpreters/:id [put]
func (h *ScriptHandlers) UpdateInterpreter(c *gin.Context) {
func (self *ScriptHandlers) UpdateInterpreter(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.Error(err)
@@ -167,7 +176,7 @@ func (h *ScriptHandlers) UpdateInterpreter(c *gin.Context) {
return
}
si, err := h.svc.Update(c.Request.Context(), id, in)
si, err := self.svc.Update(c.Request.Context(), id, in)
if err != nil {
c.Error(err)
return
@@ -181,16 +190,15 @@ func (h *ScriptHandlers) UpdateInterpreter(c *gin.Context) {
// @Tags scripts
// @Param id path int true "Interpreter ID"
// @Success 204
// @Security Bearer
// @Router /scripts/interpreters/:id [delete]
func (h *ScriptHandlers) DeleteInterpreter(c *gin.Context) {
func (self *ScriptHandlers) DeleteInterpreter(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.Error(err)
return
}
if err := h.svc.Delete(c.Request.Context(), id); err != nil {
if err := self.svc.Delete(c.Request.Context(), id); err != nil {
c.Error(err)
return
}
-534
View File
@@ -1,534 +0,0 @@
package handlers
import (
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/grpcsrv/commander"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/models"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/repository"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/service"
"github.com/gin-gonic/gin"
)
// ScriptHandlersGroup handles script management routes.
type ScriptHandlersGroup struct {
svc *service.ScriptService
cmder *commander.Commander
}
// NewScriptHandlersGroup creates a new ScriptHandlersGroup.
func NewScriptHandlersGroup(svc *service.ScriptService, cmder *commander.Commander) *ScriptHandlersGroup {
return &ScriptHandlersGroup{svc: svc, cmder: cmder}
}
// GetTree returns the script directory tree.
// @Summary Get script directory tree
// @Description Returns a hierarchical tree of all scripts organized by their paths
// @Tags scripts
// @Produce json
// @Success 200 {array} repository.ScriptTreeNode
// @Security Bearer
// @Router /scripts/tree [get]
func (sh *ScriptHandlersGroup) GetTree(c *gin.Context) {
tree, err := sh.svc.BuildTree()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to build script tree"})
return
}
if tree == nil {
tree = []repository.ScriptTreeNode{}
}
c.JSON(http.StatusOK, tree)
}
// CreateScript creates a new script.
// @Summary Create script
// @Description Creates a new script with path, content, and interpreter binding
// @Tags scripts
// @Accept json
// @Produce json
// @Param body body repository.ScriptCreate true "Script data"
// @Success 201 {object} repository.Script
// @Security Bearer
// @Router /scripts [post]
func (sh *ScriptHandlersGroup) CreateScript(c *gin.Context) {
var req repository.ScriptCreate
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
// Validate path
if err := validateScriptPath(req.Path); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
script, err := sh.svc.Repo.CreateScript(req)
if err != nil {
if isUniqueConstraint(err) {
c.JSON(http.StatusConflict, gin.H{"error": "script with this path already exists"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create script"})
return
}
c.JSON(http.StatusCreated, script)
}
// GetScript returns a script by ID.
// @Summary Get script
// @Description Returns a script by its ID
// @Tags scripts
// @Produce json
// @Param id path int true "Script ID"
// @Success 200 {object} repository.Script
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Security Bearer
// @Router /scripts/:id [get]
func (sh *ScriptHandlersGroup) GetScript(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
script, err := sh.svc.Repo.GetScript(id)
if err != nil {
if errors.Is(err, repository.ErrNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "script not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get script"})
return
}
c.JSON(http.StatusOK, script)
}
// UpdateScript updates a script.
// @Summary Update script
// @Description Updates a script's path, content, or interpreter
// @Tags scripts
// @Accept json
// @Produce json
// @Param id path int true "Script ID"
// @Param body body repository.ScriptUpdate true "Script data"
// @Success 200 {object} repository.Script
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Security Bearer
// @Router /scripts/:id [put]
func (sh *ScriptHandlersGroup) UpdateScript(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
var req repository.ScriptUpdate
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
// Validate path if it's being updated
if req.Path != nil {
if err := validateScriptPath(*req.Path); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
}
script, err := sh.svc.Repo.UpdateScript(id, req)
if err != nil {
if errors.Is(err, repository.ErrNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "script not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update script"})
return
}
c.JSON(http.StatusOK, script)
}
// DeleteScript deletes a script.
// @Summary Delete script
// @Description Deletes a script by its ID
// @Tags scripts
// @Param id path int true "Script ID"
// @Success 200 {object} map[string]string
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Security Bearer
// @Router /scripts/:id [delete]
func (sh *ScriptHandlersGroup) DeleteScript(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
if err := sh.svc.Repo.DeleteScript(id); err != nil {
if errors.Is(err, repository.ErrNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "script not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete script"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "script deleted"})
}
// RunScriptByID executes a stored script on a target agent.
// @Summary Run script by ID
// @Description Loads a script from storage, resolves interpreter command, and executes on the specified agent
// @Tags scripts
// @Accept json
// @Produce json
// @Param id path int true "Script ID"
// @Param body body RunStoredScriptIn true "Agent token and optional stdin"
// @Success 201 {object} RunScriptOut
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Security Bearer
// @Router /scripts/:id/run [post]
func (sh *ScriptHandlersGroup) RunScriptByID(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
var in RunStoredScriptIn
if err := c.ShouldBindJSON(&in); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
script, err := sh.svc.Repo.GetScript(id)
if err != nil {
if errors.Is(err, repository.ErrNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "script not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get script"})
return
}
command, err := sh.svc.ResolveCommand(c.Request.Context(), script.InterpreterID, script.Content)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to resolve command: %v", err)})
return
}
agent, ok := sh.cmder.GetAgent(in.Token)
if !ok {
c.JSON(http.StatusNotFound, gin.H{"error": "agent not found"})
return
}
jid, err := agent.AddJob(models.JobForInsert{
Command: command,
Stdin: in.Stdin,
})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to add job: %v", err)})
return
}
job, err := agent.WaitJob(jid)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("job execution failed: %v", err)})
return
}
c.JSON(http.StatusCreated, JobResult{
ID: job.ID,
Command: job.Command,
Stdin: job.Stdin,
Stdout: job.Stdout,
Stderr: job.Stderr,
Status: job.Status,
})
}
// RunStoredScriptIn is the request body for running a stored script on an agent.
type RunStoredScriptIn struct {
Token string `json:"token" binding:"required"`
Stdin *string `json:"stdin"`
}
// CreateFolderRequest is the request body for creating a script folder.
type CreateFolderRequest struct {
Path string `json:"path" binding:"required" example:"deploy/nginx" description:"Folder path (e.g. 'deploy/nginx')"`
}
// DeleteFolderRequest is the request body for deleting a script folder.
type DeleteFolderRequest struct {
Path string `json:"path" binding:"required" example:"deploy/nginx" description:"Folder path to delete"`
}
// RenameRequest is the request body for renaming a script or folder.
type RenameRequest struct {
OldPath string `json:"old_path" binding:"required" example:"deploy/nginx" description:"Current path"`
NewPath string `json:"new_path" binding:"required" example:"deploy/nginx-v2" description:"New path"`
}
// Rename renames a script or all scripts under a folder path.
// @Summary Rename script or folder
// @Description Renames a single script or all scripts under a folder prefix
// @Tags scripts
// @Accept json
// @Produce json
// @Param body body RenameRequest true "Rename request"
// @Success 200 {object} map[string]interface{} "Rename result with count of renamed scripts"
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Failure 409 {object} map[string]string
// @Security Bearer
// @Router /scripts/rename [post]
func (sh *ScriptHandlersGroup) Rename(c *gin.Context) {
var req RenameRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
// Validate new path
if err := validateScriptPath(req.NewPath); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("invalid new path: %v", err)})
return
}
// Validate old path
if err := validateScriptPath(req.OldPath); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("invalid old path: %v", err)})
return
}
// Get all scripts
allScripts, err := sh.svc.Repo.ListScripts()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list scripts"})
return
}
// Find scripts to rename: exact match or folder prefix
prefix := req.OldPath + "/"
var toRename []repository.Script
for _, script := range allScripts {
if script.Path == req.OldPath || strings.HasPrefix(script.Path, prefix) {
toRename = append(toRename, script)
}
}
if len(toRename) == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "no scripts found with this path"})
return
}
// Rename each script
renamedCount := 0
for _, script := range toRename {
newPath := req.NewPath + strings.TrimPrefix(script.Path, req.OldPath)
// Check if new path already exists (excluding the scripts we're renaming)
for _, existing := range allScripts {
if existing.ID != script.ID && existing.Path == newPath {
c.JSON(http.StatusConflict, gin.H{"error": fmt.Sprintf("path '%s' already exists", newPath)})
return
}
}
_, err := sh.svc.Repo.UpdateScript(script.ID, repository.ScriptUpdate{
Path: &newPath,
})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to rename %s: %v", script.Path, err)})
return
}
renamedCount++
}
c.JSON(http.StatusOK, gin.H{
"message": "renamed",
"old_path": req.OldPath,
"new_path": req.NewPath,
"renamed_count": renamedCount,
})
}
// CreateFolder creates a virtual folder in the script tree.
// @Summary Create folder
// @Description Creates a virtual folder by creating a placeholder script with the folder path
// @Tags scripts
// @Accept json
// @Produce json
// @Param body body CreateFolderRequest true "Folder path"
// @Success 201 {object} map[string]string "Folder created"
// @Security Bearer
// @Router /scripts/folder [post]
func (sh *ScriptHandlersGroup) CreateFolder(c *gin.Context) {
var req CreateFolderRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
// Validate folder path
if err := validateScriptPath(req.Path); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("invalid folder path: %v", err)})
return
}
// Create a placeholder script with the folder path to ensure the folder exists in the tree
// The placeholder uses ".folder" as content and interpreter_id 0 (will be resolved at runtime)
_, err := sh.svc.Repo.CreateScript(repository.ScriptCreate{
Path: req.Path,
Content: "",
InterpreterID: 0,
})
if err != nil {
if isUniqueConstraint(err) {
c.JSON(http.StatusConflict, gin.H{"error": "folder with this path already exists"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create folder"})
return
}
c.JSON(http.StatusCreated, gin.H{"message": "folder created", "path": req.Path})
}
// DeleteFolder deletes all scripts under a given path prefix.
// @Summary Delete folder
// @Description Deletes all scripts that start with the given folder path
// @Tags scripts
// @Accept json
// @Produce json
// @Param body body DeleteFolderRequest true "Folder path"
// @Success 200 {object} map[string]interface{} "Folder deleted with count of deleted scripts"
// @Security Bearer
// @Router /scripts/folder [delete]
func (sh *ScriptHandlersGroup) DeleteFolder(c *gin.Context) {
var req DeleteFolderRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
// Validate folder path
if err := validateScriptPath(req.Path); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("invalid folder path: %v", err)})
return
}
// Get all scripts and filter by path prefix
allScripts, err := sh.svc.Repo.ListScripts()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list scripts"})
return
}
prefix := req.Path + "/"
deletedCount := 0
for _, script := range allScripts {
// Delete scripts that are in this folder (path starts with prefix)
// or the folder placeholder itself (exact match)
if script.Path == req.Path || strings.HasPrefix(script.Path, prefix) {
if err := sh.svc.Repo.DeleteScript(script.ID); err != nil && !errors.Is(err, repository.ErrNotFound) {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to delete script %s: %v", script.Path, err)})
return
}
deletedCount++
}
}
c.JSON(http.StatusOK, gin.H{"message": "folder deleted", "path": req.Path, "deleted_count": deletedCount})
}
// GetScriptByPath returns a script by its path.
// @Summary Get script by path
// @Description Returns a script by its full path (e.g. 'deploy/nginx/restart.sh')
// @Tags scripts
// @Produce json
// @Param path query string true "Script path"
// @Success 200 {object} repository.Script
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Security Bearer
// @Router /scripts/by-path [get]
func (sh *ScriptHandlersGroup) GetScriptByPath(c *gin.Context) {
path := c.Query("path")
if path == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "path query parameter is required"})
return
}
script, err := sh.svc.Repo.GetScriptByPath(path)
if err != nil {
if errors.Is(err, repository.ErrNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "script not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get script"})
return
}
c.JSON(http.StatusOK, script)
}
// isUniqueConstraint checks if the error is a SQLite UNIQUE constraint violation.
func isUniqueConstraint(err error) bool {
return err != nil && (err.Error() != "" && contains(err.Error(), "UNIQUE constraint"))
}
func contains(s, substr string) bool {
return len(s) >= len(substr) && searchSubstring(s, substr)
}
func searchSubstring(s, substr string) bool {
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return true
}
}
return false
}
// validateScriptPath validates that a script path is well-formed.
// Rules: non-empty, no leading slash, no double slashes, no trailing slash, no empty segments.
func validateScriptPath(path string) error {
if path == "" {
return fmt.Errorf("path cannot be empty")
}
if strings.HasPrefix(path, "/") {
return fmt.Errorf("path cannot start with '/'")
}
if strings.HasSuffix(path, "/") {
return fmt.Errorf("path cannot end with '/'")
}
if strings.Contains(path, "//") {
return fmt.Errorf("path cannot contain '//'")
}
// Check for empty segments (e.g. "a//b" already caught, but "a/ /b" should be allowed)
segments := strings.Split(path, "/")
for i, seg := range segments {
if strings.TrimSpace(seg) == "" {
return fmt.Errorf("path segment %d cannot be empty or whitespace", i+1)
}
}
return nil
}
+8 -50
View File
@@ -5,7 +5,6 @@ import (
"database/sql"
"encoding/json"
"fmt"
"time"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/models"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/storage"
@@ -24,11 +23,7 @@ func (r *JobRepository) Init(ctx context.Context) error {
return err
}
func (r *JobRepository) InitJob(
ctx context.Context,
agentID string,
job models.JobForInsert,
) (int64, error) {
func (r *JobRepository) InitJob(ctx context.Context, agentID string, job models.JobForInsert) (int64, error) {
commandJSON, err := json.Marshal(job.Command)
if err != nil {
return 0, fmt.Errorf("marshal command: %w", err)
@@ -39,12 +34,9 @@ func (r *JobRepository) InitJob(
stdinVal = job.Stdin
}
result, err := r.DB.ExecContext(
ctx,
result, err := r.DB.ExecContext(ctx,
`INSERT INTO jobs (agent_id, command, stdin, stdout, stderr, status) VALUES (?, ?, ?, '', '', 0)`,
agentID,
string(commandJSON),
stdinVal,
agentID, string(commandJSON), stdinVal,
)
if err != nil {
return 0, err
@@ -53,18 +45,10 @@ func (r *JobRepository) InitJob(
return result.LastInsertId()
}
func (r *JobRepository) UpdateJobInDB(
ctx context.Context,
jid int64,
msg models.JobForUpdate,
) (models.Job, error) {
result, err := r.DB.ExecContext(
ctx,
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,
msg.Stdout, msg.Stderr, msg.Status, jid,
)
if err != nil {
return models.Job{}, err
@@ -97,36 +81,10 @@ func (r *JobRepository) GetJobByID(ctx context.Context, jid int64) (models.Job,
return models.Job{}, err
}
if err := json.Unmarshal([]byte(commandJSON), &job.Command); err != nil {
if err := json.Unmarshal([]byte(commandJSON), &job.JobForInsert.Command); err != nil {
return models.Job{}, fmt.Errorf("unmarshal command: %w", err)
}
job.Stdin = stdinVal
job.JobForInsert.Stdin = stdinVal
return job, nil
}
type JobMetrics struct {
Total int
Success int
Failed int
Pending int
}
// GetJobMetrics returns job success metrics for jobs updated since the given time.
// A successful job has status == 0, failed has status != 0, pending has status == 0 with empty stdout/stderr.
func (r *JobRepository) GetJobMetrics(ctx context.Context, since time.Time) (JobMetrics, error) {
var m JobMetrics
err := r.DB.QueryRowContext(ctx,
`SELECT
COUNT(*),
SUM(CASE WHEN status = 0 AND (stdout != '' OR stderr != '') THEN 1 ELSE 0 END),
SUM(CASE WHEN status != 0 THEN 1 ELSE 0 END),
SUM(CASE WHEN status = 0 AND stdout = '' AND stderr = '' THEN 1 ELSE 0 END)
FROM jobs WHERE updated_at >= ?`,
since,
).Scan(&m.Total, &m.Success, &m.Failed, &m.Pending)
if err != nil {
return JobMetrics{}, err
}
return m, nil
}
+6 -12
View File
@@ -84,13 +84,13 @@ func (r *LogRepository) InsertBatch(ctx context.Context, logs []storage.LogEntry
}
type LogFilter struct {
Level string
Service string
Agent string
Level string
Service string
Agent string
DateFrom time.Time
DateTo time.Time
Limit int
Offset int
Limit int
Offset int
}
func (r *LogRepository) Search(ctx context.Context, filter LogFilter) ([]storage.LogEntry, error) {
@@ -157,13 +157,7 @@ func (r *LogRepository) Search(ctx context.Context, filter LogFilter) ([]storage
logs := make([]storage.LogEntry, 0)
for rows.Next() {
var log storage.LogEntry
if err := rows.Scan(
&log.Timestamp,
&log.Level,
&log.Service,
&log.Agent,
&log.Message,
); err != nil {
if err := rows.Scan(&log.Timestamp, &log.Level, &log.Service, &log.Agent, &log.Message); err != nil {
return nil, err
}
logs = append(logs, log)
+28 -70
View File
@@ -2,37 +2,29 @@ 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"`
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"`
Name string `json:"name" binding:"required"`
LastName string `json:"last_name" binding:"required"`
Login string `json:"login" binding:"required"`
Password string `json:"password" binding:"required"`
PermissionView bool `json:"permission_view"`
PermissionManage bool `json:"permission_manage_agent"`
PermissionAdmin bool `json:"permission_admin"`
IsActive bool `json:"is_active"`
}
// UserRegister is the request body for public user registration (all permissions false).
type UserRegister struct {
Name string `json:"name" binding:"required"`
LastName string `json:"last_name" binding:"required"`
Login string `json:"login" binding:"required"`
Password string `json:"password" binding:"required"`
}
// TokenUpdate is the request body for updating an existing user.
type TokenUpdate struct {
Name string `json:"name"`
@@ -59,7 +51,7 @@ type BatchActionRequest struct {
// LoginRequest is the request body for login.
type LoginRequest struct {
Login string `json:"login" binding:"required"`
Login string `json:"login" binding:"required"`
Password string `json:"password" binding:"required"`
}
@@ -117,14 +109,14 @@ const (
// 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"`
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
@@ -137,49 +129,15 @@ type DeployAgentsRequest struct {
// @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"`
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"`
}
// Script represents a stored script with path and interpreter binding.
type Script struct {
ID int64 `json:"id"`
Path string `json:"path"`
Content string `json:"content"`
InterpreterID int64 `json:"interpreter_id"`
CreatedAt *string `json:"created_at"`
UpdatedAt *string `json:"updated_at"`
}
// ScriptCreate is the request body for creating a script.
type ScriptCreate struct {
Path string `json:"path" binding:"required"`
Content string `json:"content"`
InterpreterID int64 `json:"interpreter_id" binding:"required"`
}
// ScriptUpdate is the request body for updating a script.
type ScriptUpdate struct {
Path *string `json:"path"`
Content *string `json:"content"`
InterpreterID *int64 `json:"interpreter_id"`
}
// ScriptTreeNode represents a node in the script directory tree.
type ScriptTreeNode struct {
Name string `json:"name"`
Type string `json:"type"` // "folder" or "file"
Children []ScriptTreeNode `json:"children,omitempty"`
ID *int64 `json:"id,omitempty"`
Content *string `json:"content,omitempty"`
InterpreterID *int64 `json:"interpreter_id,omitempty"`
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"`
}
+9 -193
View File
@@ -3,8 +3,6 @@ package repository
import (
"database/sql"
"errors"
"fmt"
"log"
"strconv"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/storage"
@@ -52,15 +50,8 @@ func (r *Repository) CreateToken(tc TokenCreate) (string, error) {
result, err := r.DB.Exec(
`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,
tc.Name, tc.LastName, tc.Login, string(hashed), token,
tc.PermissionView, tc.PermissionManage, tc.PermissionAdmin, tc.IsActive,
)
if err != nil {
return "", err
@@ -73,39 +64,6 @@ func (r *Repository) CreateToken(tc TokenCreate) (string, error) {
return strconv.FormatInt(id, 10), nil
}
// RegisterUser inserts a new user with all permissions set to false and is_active=false.
func (r *Repository) RegisterUser(ur UserRegister) (string, error) {
hashed, err := bcrypt.GenerateFromPassword([]byte(ur.Password), bcrypt.DefaultCost)
if err != nil {
return "", fmt.Errorf("hash password: %w", err)
}
token, err := utils.RandomToken()
if err != nil {
return "", fmt.Errorf("generate token: %w", err)
}
result, err := r.DB.Exec(
`INSERT INTO tokens (name, last_name, login, password, token, permission_view, permission_manage_agent, permission_admin, is_active)
VALUES (?, ?, ?, ?, ?, 0, 0, 0, 0)`,
ur.Name,
ur.LastName,
ur.Login,
string(hashed),
token,
)
if err != nil {
return "", fmt.Errorf("insert user: %w", err)
}
id, err := result.LastInsertId()
if err != nil {
return "", fmt.Errorf("get last insert id: %w", err)
}
log.Printf("[register] user created: id=%s login=%s", strconv.FormatInt(id, 10), ur.Login)
return strconv.FormatInt(id, 10), nil
}
// Login authenticates by login/password, generates a new token, and returns LoginResponse.
func (r *Repository) Login(login, password string) (*LoginResponse, error) {
var t Tokens
@@ -160,11 +118,11 @@ func (r *Repository) Login(login, password string) (*LoginResponse, error) {
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, is_active
`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, &t.IsActive)
&t.PermissionView, &t.PermissionManage, &t.PermissionAdmin)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
@@ -178,7 +136,7 @@ func (r *Repository) GetToken(token string) (*Tokens, error) {
// ListTokens returns all users without password and token.
func (r *Repository) ListTokens() ([]Tokens, error) {
rows, err := r.DB.Query(
`SELECT id, name, last_name, login, permission_view, permission_manage_agent, permission_admin, is_active
`SELECT id, name, last_name, login, permission_view, permission_manage_agent, permission_admin
FROM tokens`,
)
if err != nil {
@@ -190,7 +148,7 @@ func (r *Repository) ListTokens() ([]Tokens, error) {
for rows.Next() {
var t Tokens
if err := rows.Scan(&t.ID, &t.Name, &t.LastName, &t.Login,
&t.PermissionView, &t.PermissionManage, &t.PermissionAdmin, &t.IsActive); err != nil {
&t.PermissionView, &t.PermissionManage, &t.PermissionAdmin); err != nil {
return nil, err
}
tokens = append(tokens, t)
@@ -299,12 +257,6 @@ func (r *Repository) MarkRegistrationTokenUsed(token string) error {
return nil
}
// DeleteRegistrationToken deletes a registration token (used for rollback on deployment failure).
func (r *Repository) DeleteRegistrationToken(token string) error {
_, err := r.DB.Exec(`DELETE FROM registration_tokens WHERE token = ?`, token)
return err
}
// ActivateToken activates a user by token value.
func (r *Repository) ActivateToken(token string) error {
result, err := r.DB.Exec(
@@ -350,13 +302,12 @@ func (r *Repository) ActivateUserByLogin(login string) error {
login,
)
if err != nil {
return fmt.Errorf("activate exec: %w", err)
return err
}
affected, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("rows affected: %w", err)
return err
}
log.Printf("[activate] login=%s affected=%d", login, affected)
if affected == 0 {
return ErrNotFound
}
@@ -471,11 +422,7 @@ func (r *Repository) UpdatePermissions(login string, update TokenUpdatePermissio
result, err := r.DB.Exec(
`UPDATE tokens SET permission_view = ?, permission_manage_agent = ?, permission_admin = ?, is_active = ? WHERE login = ?`,
newView,
newManage,
newAdmin,
newActive,
login,
newView, newManage, newAdmin, newActive, login,
)
if err != nil {
return err
@@ -513,134 +460,3 @@ func (r *Repository) UpdatePassword(login string, newPassword string) error {
}
return nil
}
// CreateScript inserts a new script into the database.
func (r *Repository) CreateScript(sc ScriptCreate) (*Script, error) {
result, err := r.DB.Exec(
`INSERT INTO scripts (path, content, interpreter_id) VALUES (?, ?, ?)`,
sc.Path, sc.Content, sc.InterpreterID,
)
if err != nil {
return nil, fmt.Errorf("insert script: %w", err)
}
id, err := result.LastInsertId()
if err != nil {
return nil, fmt.Errorf("get last insert id: %w", err)
}
return &Script{
ID: id,
Path: sc.Path,
Content: sc.Content,
InterpreterID: sc.InterpreterID,
}, nil
}
// GetScript retrieves a script by ID.
func (r *Repository) GetScript(id int64) (*Script, error) {
var s Script
err := r.DB.QueryRow(
`SELECT id, path, content, interpreter_id, created_at, updated_at FROM scripts WHERE id = ?`,
id,
).Scan(&s.ID, &s.Path, &s.Content, &s.InterpreterID, &s.CreatedAt, &s.UpdatedAt)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
}
return nil, err
}
return &s, nil
}
// GetScriptByPath retrieves a script by its path.
func (r *Repository) GetScriptByPath(path string) (*Script, error) {
var s Script
err := r.DB.QueryRow(
`SELECT id, path, content, interpreter_id, created_at, updated_at FROM scripts WHERE path = ?`,
path,
).Scan(&s.ID, &s.Path, &s.Content, &s.InterpreterID, &s.CreatedAt, &s.UpdatedAt)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
}
return nil, err
}
return &s, nil
}
// ListScripts returns all scripts.
func (r *Repository) ListScripts() ([]Script, error) {
rows, err := r.DB.Query(
`SELECT id, path, content, interpreter_id, created_at, updated_at FROM scripts`,
)
if err != nil {
return nil, err
}
defer rows.Close()
var scripts []Script
for rows.Next() {
var s Script
if err := rows.Scan(&s.ID, &s.Path, &s.Content, &s.InterpreterID, &s.CreatedAt, &s.UpdatedAt); err != nil {
return nil, err
}
scripts = append(scripts, s)
}
return scripts, rows.Err()
}
// UpdateScript updates a script by ID.
func (r *Repository) UpdateScript(id int64, update ScriptUpdate) (*Script, error) {
existing, err := r.GetScript(id)
if err != nil {
return nil, err
}
newPath := existing.Path
newContent := existing.Content
newInterpreterID := existing.InterpreterID
if update.Path != nil {
newPath = *update.Path
}
if update.Content != nil {
newContent = *update.Content
}
if update.InterpreterID != nil {
newInterpreterID = *update.InterpreterID
}
_, err = r.DB.Exec(
`UPDATE scripts SET path = ?, content = ?, interpreter_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`,
newPath, newContent, newInterpreterID, id,
)
if err != nil {
return nil, fmt.Errorf("update script: %w", err)
}
return &Script{
ID: id,
Path: newPath,
Content: newContent,
InterpreterID: newInterpreterID,
}, nil
}
// DeleteScript deletes a script by ID.
func (r *Repository) DeleteScript(id int64) error {
result, err := r.DB.Exec(`DELETE FROM scripts WHERE id = ?`, id)
if err != nil {
return err
}
affected, err := result.RowsAffected()
if err != nil {
return err
}
if affected == 0 {
return ErrNotFound
}
return nil
}
@@ -20,9 +20,9 @@ type ScriptInterpreter struct {
}
type ScriptInterpreterCreate struct {
Name string `json:"name" binding:"required"`
Name string `json:"name" binding:"required"`
Label string `json:"label" binding:"required"`
Argv []string `json:"argv" binding:"required"`
Argv []string `json:"argv" binding:"required"`
}
type ScriptInterpreterUpdate struct {
@@ -44,10 +44,7 @@ func (r *ScriptInterpreterRepo) Init(ctx context.Context) error {
return err
}
func (r *ScriptInterpreterRepo) Create(
ctx context.Context,
in ScriptInterpreterCreate,
) (*ScriptInterpreter, error) {
func (r *ScriptInterpreterRepo) Create(ctx context.Context, in ScriptInterpreterCreate) (*ScriptInterpreter, error) {
argvJSON, err := json.Marshal(in.Argv)
if err != nil {
return nil, err
@@ -74,8 +71,7 @@ func (r *ScriptInterpreterRepo) GetByID(ctx context.Context, id int64) (*ScriptI
var argvJSON string
var createdAt, updatedAt string
err := r.DB.QueryRowContext(
ctx,
err := r.DB.QueryRowContext(ctx,
`SELECT id, name, label, argv, created_at, updated_at FROM script_interpreters WHERE id = ?`,
id,
).Scan(&si.ID, &si.Name, &si.Label, &argvJSON, &createdAt, &updatedAt)
@@ -107,14 +103,7 @@ func (r *ScriptInterpreterRepo) List(ctx context.Context) ([]ScriptInterpreter,
for rows.Next() {
var si ScriptInterpreter
var argvJSON, createdAt, updatedAt string
if err := rows.Scan(
&si.ID,
&si.Name,
&si.Label,
&argvJSON,
&createdAt,
&updatedAt,
); err != nil {
if err := rows.Scan(&si.ID, &si.Name, &si.Label, &argvJSON, &createdAt, &updatedAt); err != nil {
return nil, err
}
if err := json.Unmarshal([]byte(argvJSON), &si.Argv); err != nil {
@@ -127,11 +116,7 @@ func (r *ScriptInterpreterRepo) List(ctx context.Context) ([]ScriptInterpreter,
return interpreters, rows.Err()
}
func (r *ScriptInterpreterRepo) Update(
ctx context.Context,
id int64,
in ScriptInterpreterUpdate,
) (*ScriptInterpreter, error) {
func (r *ScriptInterpreterRepo) Update(ctx context.Context, id int64, in ScriptInterpreterUpdate) (*ScriptInterpreter, error) {
si, err := r.GetByID(ctx, id)
if err != nil {
return nil, err
+23 -160
View File
@@ -3,189 +3,52 @@ package service
import (
"context"
"fmt"
"sort"
"strings"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/repository"
)
// ScriptService handles script CRUD, tree building, and interpreter resolution.
type ScriptService struct {
Repo *repository.Repository
InterpreterRepo *repository.ScriptInterpreterRepo
repo *repository.ScriptInterpreterRepo
}
// NewScriptService creates a new ScriptService with both script and interpreter repos.
func NewScriptService(repo *repository.Repository) *ScriptService {
return &ScriptService{Repo: repo}
func NewScriptService(repo *repository.ScriptInterpreterRepo) *ScriptService {
return &ScriptService{repo: repo}
}
// NewScriptServiceWithInterpreters creates a ScriptService with interpreter support.
func NewScriptServiceWithInterpreters(repo *repository.Repository, interpRepo *repository.ScriptInterpreterRepo) *ScriptService {
return &ScriptService{Repo: repo, InterpreterRepo: interpRepo}
}
// treeNode is an internal representation for building the tree.
type treeNode struct {
name string
typ string // "folder" or "file"
children map[string]*treeNode
// File-specific fields
id *int64
content *string
interpreterID *int64
}
// BuildTree builds a directory tree from all scripts in the database.
// Each script path is treated as a file path (e.g. "deploy/nginx/restart.sh").
// Scripts with empty content and interpreter_id=0 are treated as folder placeholders.
func (s *ScriptService) BuildTree() ([]repository.ScriptTreeNode, error) {
scripts, err := s.Repo.ListScripts()
// 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
}
root := make(map[string]*treeNode)
for _, sc := range scripts {
parts := strings.Split(sc.Path, "/")
// A script with empty content and interpreter_id=0 is a folder placeholder
isPlaceholder := sc.Content == "" && sc.InterpreterID == 0
// Walk through path parts, creating folders as needed
currentMap := root
for i, part := range parts {
isLastPart := i == len(parts)-1
isFile := isLastPart && !isPlaceholder
if _, exists := currentMap[part]; !exists {
node := &treeNode{
name: part,
children: make(map[string]*treeNode),
}
if isFile {
node.typ = "file"
id := sc.ID
content := sc.Content
interpreterID := sc.InterpreterID
node.id = &id
node.content = &content
node.interpreterID = &interpreterID
} else {
node.typ = "folder"
}
currentMap[part] = node
} else if isFile {
// Node already exists but was created as a folder (e.g. by another script's path).
// Convert it to a file if it was a folder placeholder.
existing := currentMap[part]
if existing.typ == "folder" {
id := sc.ID
content := sc.Content
interpreterID := sc.InterpreterID
existing.typ = "file"
existing.id = &id
existing.content = &content
existing.interpreterID = &interpreterID
}
}
currentMap = currentMap[part].children
}
if len(interpreter.Argv) == 0 {
return nil, fmt.Errorf("interpreter %q has empty argv", interpreter.Name)
}
return buildTreeSlice(root), nil
argv := make([]string, len(interpreter.Argv)+1)
copy(argv, interpreter.Argv)
argv[len(argv)-1] = scriptText
return argv, nil
}
// buildTreeSlice converts a map of treeNodes to a sorted slice of ScriptTreeNode.
func buildTreeSlice(m map[string]*treeNode) []repository.ScriptTreeNode {
result := make([]repository.ScriptTreeNode, 0, len(m))
for _, node := range m {
result = append(result, toScriptTreeNode(node))
}
// Sort: folders first, then files, alphabetically within each group
sort.Slice(result, func(i, j int) bool {
if result[i].Type != result[j].Type {
return result[i].Type == "folder"
}
return result[i].Name < result[j].Name
})
return result
func (self *ScriptService) Create(ctx context.Context, in repository.ScriptInterpreterCreate) (*repository.ScriptInterpreter, error) {
return self.repo.Create(ctx, in)
}
// toScriptTreeNode converts a treeNode to a ScriptTreeNode with recursively converted children.
func toScriptTreeNode(node *treeNode) repository.ScriptTreeNode {
result := repository.ScriptTreeNode{
Name: node.name,
Type: node.typ,
Children: []repository.ScriptTreeNode{},
}
if node.typ == "file" {
result.ID = node.id
result.Content = node.content
result.InterpreterID = node.interpreterID
} else {
result.Children = buildTreeSlice(node.children)
}
return result
func (self *ScriptService) GetByID(ctx context.Context, id int64) (*repository.ScriptInterpreter, error) {
return self.repo.GetByID(ctx, id)
}
// ResolveCommand resolves the full command for a script using its interpreter.
func (s *ScriptService) ResolveCommand(ctx context.Context, interpreterID int64, scriptText string) ([]string, error) {
if s.InterpreterRepo == nil {
return nil, fmt.Errorf("interpreter repo not configured")
}
interpreter, err := s.InterpreterRepo.GetByID(ctx, interpreterID)
if err != nil {
return nil, fmt.Errorf("get interpreter: %w", err)
}
// Build command: argv[0] argv[1] ... -c scriptText
cmd := append(interpreter.Argv, "-c", scriptText)
return cmd, nil
func (self *ScriptService) List(ctx context.Context) ([]repository.ScriptInterpreter, error) {
return self.repo.List(ctx)
}
// List returns all interpreters.
func (s *ScriptService) List(ctx context.Context) ([]repository.ScriptInterpreter, error) {
if s.InterpreterRepo == nil {
return nil, fmt.Errorf("interpreter repo not configured")
}
return s.InterpreterRepo.List(ctx)
func (self *ScriptService) Update(ctx context.Context, id int64, in repository.ScriptInterpreterUpdate) (*repository.ScriptInterpreter, error) {
return self.repo.Update(ctx, id, in)
}
// Create creates a new interpreter.
func (s *ScriptService) Create(ctx context.Context, in repository.ScriptInterpreterCreate) (*repository.ScriptInterpreter, error) {
if s.InterpreterRepo == nil {
return nil, fmt.Errorf("interpreter repo not configured")
}
return s.InterpreterRepo.Create(ctx, in)
}
// GetByID returns an interpreter by ID.
func (s *ScriptService) GetByID(ctx context.Context, id int64) (*repository.ScriptInterpreter, error) {
if s.InterpreterRepo == nil {
return nil, fmt.Errorf("interpreter repo not configured")
}
return s.InterpreterRepo.GetByID(ctx, id)
}
// Update updates an interpreter.
func (s *ScriptService) Update(ctx context.Context, id int64, in repository.ScriptInterpreterUpdate) (*repository.ScriptInterpreter, error) {
if s.InterpreterRepo == nil {
return nil, fmt.Errorf("interpreter repo not configured")
}
return s.InterpreterRepo.Update(ctx, id, in)
}
// Delete deletes an interpreter.
func (s *ScriptService) Delete(ctx context.Context, id int64) error {
if s.InterpreterRepo == nil {
return fmt.Errorf("interpreter repo not configured")
}
return s.InterpreterRepo.Delete(ctx, id)
func (self *ScriptService) Delete(ctx context.Context, id int64) error {
return self.repo.Delete(ctx, id)
}
+3 -17
View File
@@ -43,11 +43,7 @@ func OpenClickHouse(cfg ClickHouseConfig) (*sql.DB, error) {
}
// OpenClickHouseWithRetry attempts to connect to ClickHouse with retries and backoff.
func OpenClickHouseWithRetry(
cfg ClickHouseConfig,
maxRetries int,
initialDelay time.Duration,
) (*sql.DB, error) {
func OpenClickHouseWithRetry(cfg ClickHouseConfig, maxRetries int, initialDelay time.Duration) (*sql.DB, error) {
var lastErr error
delay := initialDelay
@@ -57,20 +53,10 @@ func OpenClickHouseWithRetry(
return db, nil
}
lastErr = err
log.Printf(
"ClickHouse connection attempt %d/%d failed: %v, retrying in %v...",
i+1,
maxRetries,
err,
delay,
)
log.Printf("ClickHouse connection attempt %d/%d failed: %v, retrying in %v...", i+1, maxRetries, err, delay)
time.Sleep(delay)
delay *= 2
}
return nil, fmt.Errorf(
"clickhouse connection failed after %d attempts: %w",
maxRetries,
lastErr,
)
return nil, fmt.Errorf("clickhouse connection failed after %d attempts: %w", maxRetries, lastErr)
}
-12
View File
@@ -57,18 +57,6 @@ CREATE TABLE IF NOT EXISTS script_interpreters (
);
`
const CreateScriptsTable = `
CREATE TABLE IF NOT EXISTS scripts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
path TEXT NOT NULL UNIQUE,
content TEXT NOT NULL DEFAULT '',
interpreter_id INTEGER NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (interpreter_id) REFERENCES script_interpreters(id)
);
`
const CreateLogsTable = `
CREATE TABLE IF NOT EXISTS logs (
timestamp DateTime64(3) DEFAULT now(),
+1 -11
View File
@@ -3,7 +3,6 @@ package storage
import (
"database/sql"
"fmt"
"log"
"strings"
_ "modernc.org/sqlite"
@@ -38,16 +37,7 @@ func Open(path string) (*sql.DB, error) {
}
// Migration: add is_active column if it doesn't exist
if _, err := db.Exec(AddIsActiveColumn); err != nil {
log.Printf("[sqlite] WARNING: failed to add is_active column: %v", err)
} else {
log.Println("[sqlite] is_active column migration applied")
}
// Create scripts table if not exists
if _, err := db.Exec(CreateScriptsTable); err != nil {
return nil, fmt.Errorf("migrate scripts: %w", err)
}
_, _ = db.Exec(AddIsActiveColumn)
return db, nil
}
-1
View File
@@ -5,7 +5,6 @@ import (
"encoding/hex"
)
// TOOD: fuck
func RandomToken() (string, error) {
token := make([]byte, 32)
if _, err := rand.Read(token); err != nil {
+1 -1
View File
@@ -1,7 +1,7 @@
backend_url: http://backend:8080
grpc_url: backend:9001
label: test-agent-1
registration_token: "58b1cd3857774f690e4534ec222af4ec08eaae8cd5577614365f2b19c78d03d6"
registration_token: "156616b56774d59ba53f1eb4b096488bb5f755bbf5b737d93a42bb1b583ad7fb"
cert_dir: /etc/hellreign-agent/certs
services:
- name: system
-15
View File
@@ -1,15 +0,0 @@
CREATE TABLE [jobs_new_17f2f1dd010f] (
[id] INTEGER PRIMARY KEY,
[agent_id] TEXT NOT NULL,
[command] TEXT NOT NULL,
[stdin] TEXT,
[stdout] TEXT,
[stderr] TEXT,
[status] INTEGER,
[created_at] FLOAT DEFAULT CURRENT_TIMESTAMP,
[updated_at] FLOAT DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO [jobs_new_17f2f1dd010f] ([rowid], [id], [agent_id], [command], [stdin], [stdout], [stderr], [status], [created_at], [updated_at])
SELECT [rowid], [id], [agent_id], [command], [stdin], [stdout], [stderr], [status], [created_at], [updated_at] FROM [jobs];
DROP TABLE [jobs];
ALTER TABLE [jobs_new_17f2f1dd010f] RENAME TO [jobs];
-11
View File
@@ -6,16 +6,6 @@ option go_package="gitea.d3m0k1d.ru/d3m0k1d/HellreigN/proto/proto";
service Collector {
rpc Stream(stream CollectorRequest) returns (CollectorResponse);
rpc ReportServices(ServicesUpdate) returns (ServicesUpdateResp);
}
message ServicesUpdateResp {
}
message ServicesUpdate {
message ServiceUpdate {
string name = 1;
string status = 2;
}
repeated ServiceUpdate services = 1;
}
message CollectorRequest {
@@ -41,4 +31,3 @@ message FinishedCommand {
string stdout = 3;
string stderr = 4;
}
+31 -176
View File
@@ -21,86 +21,6 @@ const (
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type ServicesUpdateResp struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ServicesUpdateResp) Reset() {
*x = ServicesUpdateResp{}
mi := &file_hellreign_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ServicesUpdateResp) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ServicesUpdateResp) ProtoMessage() {}
func (x *ServicesUpdateResp) ProtoReflect() protoreflect.Message {
mi := &file_hellreign_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ServicesUpdateResp.ProtoReflect.Descriptor instead.
func (*ServicesUpdateResp) Descriptor() ([]byte, []int) {
return file_hellreign_proto_rawDescGZIP(), []int{0}
}
type ServicesUpdate struct {
state protoimpl.MessageState `protogen:"open.v1"`
Services []*ServicesUpdate_ServiceUpdate `protobuf:"bytes,1,rep,name=services,proto3" json:"services,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ServicesUpdate) Reset() {
*x = ServicesUpdate{}
mi := &file_hellreign_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ServicesUpdate) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ServicesUpdate) ProtoMessage() {}
func (x *ServicesUpdate) ProtoReflect() protoreflect.Message {
mi := &file_hellreign_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ServicesUpdate.ProtoReflect.Descriptor instead.
func (*ServicesUpdate) Descriptor() ([]byte, []int) {
return file_hellreign_proto_rawDescGZIP(), []int{1}
}
func (x *ServicesUpdate) GetServices() []*ServicesUpdate_ServiceUpdate {
if x != nil {
return x.Services
}
return nil
}
type CollectorRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
@@ -110,7 +30,7 @@ type CollectorRequest struct {
func (x *CollectorRequest) Reset() {
*x = CollectorRequest{}
mi := &file_hellreign_proto_msgTypes[2]
mi := &file_hellreign_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -122,7 +42,7 @@ func (x *CollectorRequest) String() string {
func (*CollectorRequest) ProtoMessage() {}
func (x *CollectorRequest) ProtoReflect() protoreflect.Message {
mi := &file_hellreign_proto_msgTypes[2]
mi := &file_hellreign_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -135,7 +55,7 @@ func (x *CollectorRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use CollectorRequest.ProtoReflect.Descriptor instead.
func (*CollectorRequest) Descriptor() ([]byte, []int) {
return file_hellreign_proto_rawDescGZIP(), []int{2}
return file_hellreign_proto_rawDescGZIP(), []int{0}
}
func (x *CollectorRequest) GetMessage() string {
@@ -153,7 +73,7 @@ type CollectorResponse struct {
func (x *CollectorResponse) Reset() {
*x = CollectorResponse{}
mi := &file_hellreign_proto_msgTypes[3]
mi := &file_hellreign_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -165,7 +85,7 @@ func (x *CollectorResponse) String() string {
func (*CollectorResponse) ProtoMessage() {}
func (x *CollectorResponse) ProtoReflect() protoreflect.Message {
mi := &file_hellreign_proto_msgTypes[3]
mi := &file_hellreign_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -178,7 +98,7 @@ func (x *CollectorResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use CollectorResponse.ProtoReflect.Descriptor instead.
func (*CollectorResponse) Descriptor() ([]byte, []int) {
return file_hellreign_proto_rawDescGZIP(), []int{3}
return file_hellreign_proto_rawDescGZIP(), []int{1}
}
type Command struct {
@@ -192,7 +112,7 @@ type Command struct {
func (x *Command) Reset() {
*x = Command{}
mi := &file_hellreign_proto_msgTypes[4]
mi := &file_hellreign_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -204,7 +124,7 @@ func (x *Command) String() string {
func (*Command) ProtoMessage() {}
func (x *Command) ProtoReflect() protoreflect.Message {
mi := &file_hellreign_proto_msgTypes[4]
mi := &file_hellreign_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -217,7 +137,7 @@ func (x *Command) ProtoReflect() protoreflect.Message {
// Deprecated: Use Command.ProtoReflect.Descriptor instead.
func (*Command) Descriptor() ([]byte, []int) {
return file_hellreign_proto_rawDescGZIP(), []int{4}
return file_hellreign_proto_rawDescGZIP(), []int{2}
}
func (x *Command) GetId() int64 {
@@ -253,7 +173,7 @@ type FinishedCommand struct {
func (x *FinishedCommand) Reset() {
*x = FinishedCommand{}
mi := &file_hellreign_proto_msgTypes[5]
mi := &file_hellreign_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -265,7 +185,7 @@ func (x *FinishedCommand) String() string {
func (*FinishedCommand) ProtoMessage() {}
func (x *FinishedCommand) ProtoReflect() protoreflect.Message {
mi := &file_hellreign_proto_msgTypes[5]
mi := &file_hellreign_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -278,7 +198,7 @@ func (x *FinishedCommand) ProtoReflect() protoreflect.Message {
// Deprecated: Use FinishedCommand.ProtoReflect.Descriptor instead.
func (*FinishedCommand) Descriptor() ([]byte, []int) {
return file_hellreign_proto_rawDescGZIP(), []int{5}
return file_hellreign_proto_rawDescGZIP(), []int{3}
}
func (x *FinishedCommand) GetId() int64 {
@@ -309,69 +229,11 @@ func (x *FinishedCommand) GetStderr() string {
return ""
}
type ServicesUpdate_ServiceUpdate struct {
state protoimpl.MessageState `protogen:"open.v1"`
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
Status string `protobuf:"bytes,2,opt,name=status,proto3" json:"status,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ServicesUpdate_ServiceUpdate) Reset() {
*x = ServicesUpdate_ServiceUpdate{}
mi := &file_hellreign_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ServicesUpdate_ServiceUpdate) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ServicesUpdate_ServiceUpdate) ProtoMessage() {}
func (x *ServicesUpdate_ServiceUpdate) ProtoReflect() protoreflect.Message {
mi := &file_hellreign_proto_msgTypes[6]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ServicesUpdate_ServiceUpdate.ProtoReflect.Descriptor instead.
func (*ServicesUpdate_ServiceUpdate) Descriptor() ([]byte, []int) {
return file_hellreign_proto_rawDescGZIP(), []int{1, 0}
}
func (x *ServicesUpdate_ServiceUpdate) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *ServicesUpdate_ServiceUpdate) GetStatus() string {
if x != nil {
return x.Status
}
return ""
}
var File_hellreign_proto protoreflect.FileDescriptor
const file_hellreign_proto_rawDesc = "" +
"\n" +
"\x0fhellreign.proto\x12\x04chat\"\x14\n" +
"\x12ServicesUpdateResp\"\x8d\x01\n" +
"\x0eServicesUpdate\x12>\n" +
"\bservices\x18\x01 \x03(\v2\".chat.ServicesUpdate.ServiceUpdateR\bservices\x1a;\n" +
"\rServiceUpdate\x12\x12\n" +
"\x04name\x18\x01 \x01(\tR\x04name\x12\x16\n" +
"\x06status\x18\x02 \x01(\tR\x06status\",\n" +
"\x0fhellreign.proto\x12\x04chat\",\n" +
"\x10CollectorRequest\x12\x18\n" +
"\amessage\x18\x01 \x01(\tR\amessage\"\x13\n" +
"\x11CollectorResponse\"X\n" +
@@ -384,10 +246,9 @@ const file_hellreign_proto_rawDesc = "" +
"\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\x06stderr2\x8a\x01\n" +
"\x06stderr\x18\x04 \x01(\tR\x06stderr2H\n" +
"\tCollector\x12;\n" +
"\x06Stream\x12\x16.chat.CollectorRequest\x1a\x17.chat.CollectorResponse(\x01\x12@\n" +
"\x0eReportServices\x12\x14.chat.ServicesUpdate\x1a\x18.chat.ServicesUpdateResp2?\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"
@@ -403,29 +264,23 @@ func file_hellreign_proto_rawDescGZIP() []byte {
return file_hellreign_proto_rawDescData
}
var file_hellreign_proto_msgTypes = make([]protoimpl.MessageInfo, 7)
var file_hellreign_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
var file_hellreign_proto_goTypes = []any{
(*ServicesUpdateResp)(nil), // 0: chat.ServicesUpdateResp
(*ServicesUpdate)(nil), // 1: chat.ServicesUpdate
(*CollectorRequest)(nil), // 2: chat.CollectorRequest
(*CollectorResponse)(nil), // 3: chat.CollectorResponse
(*Command)(nil), // 4: chat.Command
(*FinishedCommand)(nil), // 5: chat.FinishedCommand
(*ServicesUpdate_ServiceUpdate)(nil), // 6: chat.ServicesUpdate.ServiceUpdate
(*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{
6, // 0: chat.ServicesUpdate.services:type_name -> chat.ServicesUpdate.ServiceUpdate
2, // 1: chat.Collector.Stream:input_type -> chat.CollectorRequest
1, // 2: chat.Collector.ReportServices:input_type -> chat.ServicesUpdate
5, // 3: chat.Commander.Stream:input_type -> chat.FinishedCommand
3, // 4: chat.Collector.Stream:output_type -> chat.CollectorResponse
0, // 5: chat.Collector.ReportServices:output_type -> chat.ServicesUpdateResp
4, // 6: chat.Commander.Stream:output_type -> chat.Command
4, // [4:7] is the sub-list for method output_type
1, // [1:4] is the sub-list for method input_type
1, // [1:1] is the sub-list for extension type_name
1, // [1:1] is the sub-list for extension extendee
0, // [0:1] is the sub-list for field type_name
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() }
@@ -433,14 +288,14 @@ func file_hellreign_proto_init() {
if File_hellreign_proto != nil {
return
}
file_hellreign_proto_msgTypes[4].OneofWrappers = []any{}
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: 7,
NumMessages: 4,
NumExtensions: 0,
NumServices: 2,
},
+2 -41
View File
@@ -19,8 +19,7 @@ import (
const _ = grpc.SupportPackageIsVersion9
const (
Collector_Stream_FullMethodName = "/chat.Collector/Stream"
Collector_ReportServices_FullMethodName = "/chat.Collector/ReportServices"
Collector_Stream_FullMethodName = "/chat.Collector/Stream"
)
// CollectorClient is the client API for Collector service.
@@ -28,7 +27,6 @@ const (
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type CollectorClient interface {
Stream(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[CollectorRequest, CollectorResponse], error)
ReportServices(ctx context.Context, in *ServicesUpdate, opts ...grpc.CallOption) (*ServicesUpdateResp, error)
}
type collectorClient struct {
@@ -52,22 +50,11 @@ func (c *collectorClient) Stream(ctx context.Context, opts ...grpc.CallOption) (
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type Collector_StreamClient = grpc.ClientStreamingClient[CollectorRequest, CollectorResponse]
func (c *collectorClient) ReportServices(ctx context.Context, in *ServicesUpdate, opts ...grpc.CallOption) (*ServicesUpdateResp, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ServicesUpdateResp)
err := c.cc.Invoke(ctx, Collector_ReportServices_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// CollectorServer is the server API for Collector service.
// All implementations must embed UnimplementedCollectorServer
// for forward compatibility.
type CollectorServer interface {
Stream(grpc.ClientStreamingServer[CollectorRequest, CollectorResponse]) error
ReportServices(context.Context, *ServicesUpdate) (*ServicesUpdateResp, error)
mustEmbedUnimplementedCollectorServer()
}
@@ -81,9 +68,6 @@ type UnimplementedCollectorServer struct{}
func (UnimplementedCollectorServer) Stream(grpc.ClientStreamingServer[CollectorRequest, CollectorResponse]) error {
return status.Error(codes.Unimplemented, "method Stream not implemented")
}
func (UnimplementedCollectorServer) ReportServices(context.Context, *ServicesUpdate) (*ServicesUpdateResp, error) {
return nil, status.Error(codes.Unimplemented, "method ReportServices not implemented")
}
func (UnimplementedCollectorServer) mustEmbedUnimplementedCollectorServer() {}
func (UnimplementedCollectorServer) testEmbeddedByValue() {}
@@ -112,36 +96,13 @@ func _Collector_Stream_Handler(srv interface{}, stream grpc.ServerStream) error
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type Collector_StreamServer = grpc.ClientStreamingServer[CollectorRequest, CollectorResponse]
func _Collector_ReportServices_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ServicesUpdate)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(CollectorServer).ReportServices(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Collector_ReportServices_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(CollectorServer).ReportServices(ctx, req.(*ServicesUpdate))
}
return interceptor(ctx, in, info, handler)
}
// Collector_ServiceDesc is the grpc.ServiceDesc for Collector service.
// 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{
{
MethodName: "ReportServices",
Handler: _Collector_ReportServices_Handler,
},
},
Methods: []grpc.MethodDesc{},
Streams: []grpc.StreamDesc{
{
StreamName: "Stream",