mirror of
https://github.com/dragonheim/gagent.git
synced 2025-04-10 22:42:40 -07:00
Compare commits
79 commits
Author | SHA1 | Date | |
---|---|---|---|
48c0bc8537 | |||
e1c359639b | |||
6b8f83bd27 | |||
ed8a3a27e3 | |||
8c664dd007 | |||
4717b49701 | |||
07215828b8 | |||
01e3c42ac4 | |||
8cb31d2fbc | |||
91c591e22f | |||
df07f6be28 | |||
9a89745878 | |||
9e167cdefe | |||
f2b2cc705c | |||
44033c273a | |||
9727822b8a | |||
823a16a97b | |||
21a439b105 | |||
685a3eb16a | |||
97c8af8816 | |||
82a964fc50 | |||
81acc798e9 | |||
d13a309593 | |||
3bb7096625 | |||
426a360f1e | |||
8f77f2258c | |||
bdeaf2ec92 | |||
7f9a5777bd | |||
ac8c225752 | |||
7d5c02d16b | |||
c5f2cb534b | |||
8077c66fc9 | |||
2b0975b30a | |||
13d18d7714 | |||
db351f3892 | |||
7a9225ce86 | |||
d5fcd11592 | |||
1bcc682b7c | |||
8640d42132 | |||
b95d3950c5 | |||
0de851e0e5 | |||
42417afc22 | |||
b1759291b4 | |||
28dd864350 | |||
4a9d4ceca8 | |||
77c9399942 | |||
316ea1afa6 | |||
c677d30cf5 | |||
89f1dce976 | |||
134f33cd6a | |||
42623f63c6 | |||
d5ac24bb26 | |||
8fea3b2527 | |||
31c6551cce | |||
06147db215 | |||
af5407f2e6 | |||
1a0455152a | |||
66970dcd25 | |||
458214aaa9 | |||
749bd6557e | |||
27045c94d6 | |||
abba1972d1 | |||
de41740bac | |||
7e51584832 | |||
8515707300 | |||
52f8d521cd | |||
7a2ca771f2 | |||
5e69931ed7 | |||
0d0695d195 | |||
f47b6846db | |||
8a7842e6bc | |||
16d40fd93a | |||
f543276b82 | |||
00cd60ebec | |||
ae2cee7be4 | |||
f7cba2689c | |||
e61e6d2994 | |||
a7747040ce | |||
0557021282 |
69 changed files with 3108 additions and 1250 deletions
22
.devcontainer/Dockerfile
Normal file
22
.devcontainer/Dockerfile
Normal file
|
@ -0,0 +1,22 @@
|
|||
# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.194.3/containers/go/.devcontainer/base.Dockerfile
|
||||
|
||||
# [Choice] Go version: 1, 1.16, 1.17
|
||||
ARG VARIANT="1.22"
|
||||
FROM mcr.microsoft.com/vscode/devcontainers/go:0-${VARIANT}
|
||||
|
||||
COPY --from=aquasec/trivy:0.38.3 /usr/local/bin/trivy /usr/bin/trivy
|
||||
COPY --from=securego/gosec:2.15.0 /bin/gosec /usr/bin/gosec
|
||||
|
||||
# [Choice] Node.js version: none, lts/*, 16, 14, 12, 10
|
||||
ARG NODE_VERSION="none"
|
||||
RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi
|
||||
|
||||
# [Optional] Uncomment this section to install additional OS packages.
|
||||
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
||||
&& apt-get -y install --no-install-recommends libzmq3-dev libzmq5
|
||||
|
||||
# [Optional] Uncomment the next line to use go get to install anything else you need
|
||||
# RUN go get -x <your-dependency-or-tool>
|
||||
|
||||
# [Optional] Uncomment this line to install global node packages.
|
||||
# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g <your-package-here>" 2>&1
|
|
@ -1,42 +1,74 @@
|
|||
// For format details, see https://aka.ms/vscode-remote/devcontainer.json or this file's README at:
|
||||
// https://github.com/microsoft/vscode-dev-containers/tree/v0.195.0/containers/go
|
||||
{
|
||||
"name": "GitHub Codespaces (Default)",
|
||||
"build": {
|
||||
"dockerfile": "Dockerfile"
|
||||
},
|
||||
"settings": {
|
||||
"go.toolsManagement.checkForUpdates": "local",
|
||||
"go.useLanguageServer": true,
|
||||
"go.gopath": "/go",
|
||||
"go.goroot": "/usr/local/go",
|
||||
"go.linting.enabled": true,
|
||||
"lldb.executable": "/usr/bin/lldb",
|
||||
"files.watcherExclude": {
|
||||
"**/target/**": true
|
||||
}
|
||||
},
|
||||
"remoteUser": "codespace",
|
||||
"overrideCommand": false,
|
||||
"mounts": ["source=codespaces-linux-var-lib-docker,target=/var/lib/docker,type=volume"],
|
||||
"runArgs": [
|
||||
"--cap-add=SYS_PTRACE",
|
||||
"--security-opt",
|
||||
"seccomp=unconfined",
|
||||
"--privileged",
|
||||
"--init"
|
||||
],
|
||||
|
||||
// Add the IDs of extensions you want installed when the container is created.
|
||||
"extensions": [
|
||||
"GitHub.vscode-pull-request-github",
|
||||
"golang.go",
|
||||
"hashicorp.terraform",
|
||||
"wholroyd.hcl",
|
||||
"github.copilot"
|
||||
],
|
||||
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
// "forwardPorts": [],
|
||||
"name": "Go",
|
||||
"build": {
|
||||
"dockerfile": "Dockerfile",
|
||||
"args": {
|
||||
// Update the VARIANT arg to pick a version of Go: 1, 1.16, 1.17
|
||||
// Append -bullseye or -buster to pin to an OS version.
|
||||
// Use -bullseye variants on local arm64/Apple Silicon.
|
||||
"VARIANT": "1.22-bookworm",
|
||||
// Options
|
||||
"NODE_VERSION": "none"
|
||||
}
|
||||
},
|
||||
"runArgs": [ "--cap-add=SYS_PTRACE", "--security-opt", "seccomp=unconfined" ],
|
||||
|
||||
// "oryx build" will automatically install your dependencies and attempt to build your project
|
||||
// "postCreateCommand": "oryx build -p virtualenv_name=.venv --log-file /tmp/oryx-build.log --manifest-dir /tmp || echo 'Could not auto-build. Skipping.'"
|
||||
// Set *default* container specific settings.json values on container create.
|
||||
"settings": {
|
||||
"go.toolsManagement.checkForUpdates": "local",
|
||||
"go.useLanguageServer": true,
|
||||
"go.gopath": "/go",
|
||||
"go.goroot": "/usr/local/go"
|
||||
},
|
||||
|
||||
// Use the same extensions we had on Gitpod plus HCL support.
|
||||
"extensions": [
|
||||
"editorconfig.editorconfig",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"golang.Go",
|
||||
"stylelint.vscode-stylelint",
|
||||
"DavidAnson.vscode-markdownlint",
|
||||
"Vue.volar",
|
||||
"ms-azuretools.vscode-docker",
|
||||
"vitest.explorer",
|
||||
"GitHub.vscode-pull-request-github",
|
||||
"wholroyd.hcl"
|
||||
],
|
||||
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
// "forwardPorts": [9000],
|
||||
"forwardPorts": [9101, 35570, 35571, 35572],
|
||||
|
||||
// Use 'portsAttributes' to set default properties for specific forwarded ports. More info: https://code.visualstudio.com/docs/remote/devcontainerjson-reference.
|
||||
"portsAttributes": {
|
||||
"9101": {
|
||||
"label": "Prometheus",
|
||||
"onAutoForward": "notify"
|
||||
},
|
||||
"35570": {
|
||||
"label": "Router To Router",
|
||||
"onAutoForward": "notify"
|
||||
},
|
||||
"35571": {
|
||||
"label": "Worker To Router",
|
||||
"onAutoForward": "notify"
|
||||
},
|
||||
"35572": {
|
||||
"label": "Client To Router",
|
||||
"onAutoForward": "notify"
|
||||
}
|
||||
},
|
||||
|
||||
// Use 'otherPortsAttributes' to configure any ports that aren't configured using 'portsAttributes'.
|
||||
// "otherPortsAttributes": {
|
||||
// "onAutoForward": "silent"
|
||||
// },
|
||||
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
// "postCreateCommand": "go version",
|
||||
|
||||
// Uncomment to connect as a non-root user. More info: https://aka.ms/vscode-remote/containers/non-root.
|
||||
"remoteUser": "vscode"
|
||||
}
|
||||
|
|
78
.drone.yml
78
.drone.yml
|
@ -1,78 +0,0 @@
|
|||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: validation
|
||||
|
||||
platform:
|
||||
arch: amd64
|
||||
|
||||
clone:
|
||||
depth: 1
|
||||
|
||||
volumes:
|
||||
- name: dockersock
|
||||
host:
|
||||
path: /run/docker.sock
|
||||
|
||||
steps:
|
||||
- name: Notify Datadog That We Are Starting
|
||||
image: masci/drone-datadog
|
||||
settings:
|
||||
api_key:
|
||||
from_secret: Datadog
|
||||
events:
|
||||
- title: "Begin Build: ${DRONE_REPO}"
|
||||
text: "Build ${DRONE_BUILD_NUMBER}(${DRONE_COMMIT_LINK})"
|
||||
alert_type: "info"
|
||||
|
||||
- name: Validate code base and dependencies
|
||||
image: dragonheim/golang:1.17.0
|
||||
volumes:
|
||||
- name: dockersock
|
||||
path: /var/run/docker.sock
|
||||
environment:
|
||||
TRIVY_QUIET: true
|
||||
TRIVY_LIGHT: true
|
||||
TRIVY_FORMAT: table
|
||||
TRIVY_IGNORE_UNFIXED: true
|
||||
TRIVY_NO_PROGRESS: true
|
||||
commands:
|
||||
# Populate temporary container with tools / files we will need for building and testing
|
||||
- apk add --no-cache zeromq-dev zeromq
|
||||
|
||||
# Format Golang code. Golang does not really care about formatting, but this standardizes things
|
||||
- go fmt ./...
|
||||
|
||||
# Perform basic linting of the Golang code. Ideally this should never be needed, but merges can introduce imcompatabilities.
|
||||
- go vet ./...
|
||||
|
||||
# Perform code security check of lower level vulnerabilities. This will not break the build, we just want this information, just in case.
|
||||
- trivy fs --exit-code 0 --severity UNKNOWN,LOW,MEDIUM .
|
||||
|
||||
# Perform code security check of higher level vulnerabilities. This can break the build.
|
||||
- trivy fs --skip-update --exit-code 1 --severity CRITICAL,HIGH .
|
||||
|
||||
# Build new container image.
|
||||
- docker buildx build --push --platform linux/amd64 --progress plain --build-arg SEMVER="${DRONE_SEMVER}" -t ${DRONE_REPO}:latest -t ${DRONE_REPO}:${DRONE_SEMVER} -f docker/Dockerfile .
|
||||
|
||||
# Perform image security check of lower level vulnerabilities. This will not break the build, we just want this information, just in case.
|
||||
- trivy image --skip-update --exit-code 0 --severity UNKNOWN,LOW,MEDIUM,HIGH ${DRONE_REPO}:${DRONE_SEMVER}
|
||||
|
||||
# Perform image security check of higher level vulnerabilities. This can break the build.
|
||||
- trivy image --skip-update --exit-code 1 --severity CRITICAL ${DRONE_REPO}:${DRONE_SEMVER}
|
||||
|
||||
# name: Create Test Environment
|
||||
# image: dragonheim/terraform:latest
|
||||
|
||||
- name: Notify Datadog That We Have Completed
|
||||
image: masci/drone-datadog
|
||||
settings:
|
||||
api_key:
|
||||
from_secret: Datadog
|
||||
events:
|
||||
- title: "Build failure on amd64"
|
||||
text: "Build ${DRONE_BUILD_NUMBER}"
|
||||
alert_type: "error"
|
||||
when:
|
||||
status:
|
||||
- failure
|
72
.forgejo/workflows/build.yaml
Normal file
72
.forgejo/workflows/build.yaml
Normal file
|
@ -0,0 +1,72 @@
|
|||
name: G'Agent Scan, Build, and Test
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- releases/**
|
||||
tags:
|
||||
- v*
|
||||
|
||||
env:
|
||||
SEMVAR: 0.0.13
|
||||
jobs:
|
||||
scan:
|
||||
runs-on: docker
|
||||
container:
|
||||
image: dragonheim/golang:latest
|
||||
steps:
|
||||
- run: apk add --no-cache zeromq-dev nodejs npm
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: .cache/trivy
|
||||
key: tmp.woDBBj4Baw
|
||||
|
||||
- run: trivy fs --no-progress --severity CRITICAL --cache-dir .cache/trivy --exit-code 1 .
|
||||
- run: gosec -exclude=G114 -quiet ./...
|
||||
|
||||
build_simple:
|
||||
needs: scan
|
||||
runs-on: docker
|
||||
container:
|
||||
image: dragonheim/golang:latest
|
||||
steps:
|
||||
- run: apk add --no-cache zeromq-dev nodejs npm
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- run: go build -o test_artifact -ldflags="-X main.Version=${{ env.SEMVAR }}" cmd/gagent/main.go
|
||||
- run: ./test_artifact --version
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: test_artifact
|
||||
path: test_artifact
|
||||
|
||||
test_simple:
|
||||
needs: build_simple
|
||||
runs-on: docker
|
||||
container:
|
||||
image: dragonheim/golang:latest
|
||||
steps:
|
||||
- run: apk add --no-cache zeromq-dev nodejs npm
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: test_artifact
|
||||
|
||||
- run: chmod a+x test_artifact
|
||||
- run: ./test_artifact --version
|
||||
|
||||
build_image:
|
||||
needs: test_simple
|
||||
runs-on: docker
|
||||
container:
|
||||
image: dragonheim/golang:latest
|
||||
steps:
|
||||
- run: apk add --no-cache zeromq-dev nodejs npm
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- run: echo "${{ secrets.DOCKER_LOGIN }}" | docker login -u dragonheim2024 --password-stdin
|
||||
- run: docker buildx build --push -t dragonheim/gagent:test -f assets/docker/Dockerfile --build-arg SEMVER=${{ env.SEMVAR }} .
|
8
.gitattributes
vendored
Normal file
8
.gitattributes
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
* text=auto eol=lf
|
||||
*.{cmd,[cC][mM][dD]} text eol=crlf
|
||||
*.{bat,[bB][aA][tT]} text eol=crlf
|
||||
*.gif binary
|
||||
*.jpeg binary
|
||||
*.png binary
|
||||
*.gz binary
|
||||
*.jar binary
|
|
@ -1,3 +0,0 @@
|
|||
# No impact in our project
|
||||
CVE-2020-29652
|
||||
CVE-2020-9283
|
|
@ -1,4 +0,0 @@
|
|||
### [0.0.0](https://git.dragonheim.net/dragonheim/gagent/commit/5863999) (20210425)
|
||||
---
|
||||
#### Features
|
||||
* [Initial Commit](https://git.dragonheim.net/dragonheim/gagent/commit/5863999) -- James Wells
|
19
LICENSE
19
LICENSE
|
@ -1,19 +0,0 @@
|
|||
MIT License
|
||||
Copyright (c) 2017-2021 James Wells <jwells@dragonheim.net>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
9
LICENSE.md
Normal file
9
LICENSE.md
Normal file
|
@ -0,0 +1,9 @@
|
|||
# Released under MIT License
|
||||
|
||||
Copyright (c) 2017-2023 [James Wells](mailto:jwells@dragonheim.net?subject=G%27Agent)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
24
README.md
24
README.md
|
@ -1,24 +1,30 @@
|
|||
# G'Agent
|
||||
[](https://git.dragonheim.net/dragonheim/gagent)
|
||||
[](https://git.dragonheim.net/dragonheim/gagent/src/branch/main/LICENSE)
|
||||
[](https://github.com/dragonheim/gagent)
|
||||
[](https://github.com/dragonheim/gagent/src/branch/main/LICENSE)
|
||||
|
||||
[](https://drone.dragonheim.net/dragonheim/gagent)
|
||||
[](https://goreportcard.com/report/git.dragonheim.net/dragonheim/gagent)
|
||||
[](https://goreportcard.com/report/github.com/dragonheim/gagent)
|
||||
[](https://hub.docker.com/r/dragonheim/gagent/tags?page=1&ordering=last_updated)
|
||||
|
||||
A Golang based mobile agent system loosely inspired by the [Agent Tcl / D'Agents](http://www.cs.dartmouth.edu/~dfk/agents/) system created by Robert S. Gray of Dartmouth college.
|
||||
A Golang based mobile agent system loosely inspired by the [Agent Tcl / D'Agents](https://digitalcommons.dartmouth.edu/dissertations/62/) system created by Robert S. Gray of Dartmouth college.
|
||||
|
||||
Please note that this program is mostly meant to be used as a tool for me to learn software development in Go, and is not meant to be used in production environments.
|
||||
|
||||
GPT-4 has been used in various parts of this project to debug, improve functionality and provide examples of how to get over various hurdles as I learn Go.
|
||||
|
||||
## Sumary
|
||||
G'Agent streamlines interplanetary data searches using TCL scripts, allowing efficient communication despite time delays between Earth and Mars.
|
||||
|
||||
## Purpose
|
||||
As we move close and closer to a true space-age, we need to start thinking about solutions for various space-age issues such as the bi-directional time delay between the surface of Mars and the surface of Earth. At present it takes between 6 and 44 minutes for single round-trip, making most online data services unuseable. G'Agent is a potential solution for data services given the time delay.
|
||||
As we move closer and closer to a true space age, we need to start thinking about solutions for various space-age issues, such as the bi-directional time delay between the surface of Mars and the surface of Earth. At present, it takes between 6 and 44 minutes for a single round-trip, making most online data services unusable. G'Agent is a potential solution for data services given the time delay.
|
||||
|
||||
Imagine, for a moment, that you are on Mars and need to perform a data search in a specific domain space. You would have to explain it to someone on Earth, and hope they understand enough of the domain space to know where to search and understand you well enough to perform the actual search and then send you the results. With G'Agent, instead you would write a basic script (TCL), hereafter called an agent, providing various hints about the domain space and the search as TCL code. Your client would then send it on to a server, hereafter called a router, on Earth. The router may or may not know anything about the domain space of your search, so the router will use the hints that you provide to attempt to route the agent to known workers or other routers closer to the desired domain space. Eventually your agent will reach a router whose workers can handle your search. The workers, will take the agent, run the script portion and collect the response(s), returning the reponse(s) to the router(s) for return to your client.
|
||||
|
||||
Imagine, for a moment, that you are on Mars and need to perform a data search in a specific domain space. You would have to explain it to someone on Earth, and hope they understand enough of the domain space to know where to search and understand you well enough to perform the actual search and then send you the results. With G'Agent, instead, you would write a basic script (TCL), hereafter called an agent, providing various hints about the domain space and the search as TCL code. Your client would then send it to a server, hereafter called a router, on Earth. The router may or may not know anything about the domain space of your search, so the router will use the hints that you provide to attempt to route the agent to known workers or other routers closer to the desired domain space. Eventually, your agent will reach a router whose workers can handle your search. The workers will take the agent, run the script portion, and collect the response(s), returning the response(s) to the router(s) for return to your client.
|
||||
## Example Agent
|
||||
```tcl
|
||||
1 : ###################
|
||||
2 : ### Hello Earth ###
|
||||
3 : ###################
|
||||
4 : array set GHINT ["thermal measurements" "gravity measurements" "gravity fluctuations"]
|
||||
4 : set GHINT [split "thermal measurements, gravity measurements, gravity fluctuations" ,]
|
||||
5 : proc hello_earth {} {
|
||||
6 : puts "Hello Earth, does localized tempurature variations alter specific gravity?"
|
||||
7 : }
|
||||
|
@ -34,4 +40,4 @@ Line 8 executes the hello_earth procedure defined above.
|
|||
|
||||
|
||||
## History
|
||||
More information about Agent TCL / D'Agents can be found in the original [documentation](http://www.cs.dartmouth.edu/~dfk/agents/pub/agents/doc.5.1.ps.gz), and in the project's [wiki](https://git.dragonheim.net/dragonheim/gagent/wiki/_pages).
|
||||
More information about Agent TCL / D'Agents can be found in the original [documentation](http://www.cs.dartmouth.edu/~dfk/agents/pub/agents/doc.5.1.ps.gz), and in the project's [wiki](https://github.com/dragonheim/gagent/wiki/_pages).
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
### [Source Code Scan](#source)
|
||||
|
||||
IGNORED: We are not using the SSH features of golang.org/x/crypto
|
||||
```
|
||||
2021-08-30T07:10:13.085-0700 INFO Detected OS: unknown
|
||||
2021-08-30T07:10:13.085-0700 INFO Number of PL dependency files: 1
|
||||
2021-08-30T07:10:13.085-0700 INFO Detecting gomod vulnerabilities...
|
||||
|
||||
go.sum
|
||||
======
|
||||
Total: 2 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 2, CRITICAL: 0)
|
||||
|
||||
+---------------------+------------------+----------+-----------------------------------+------------------------------------+---------------------------------------+
|
||||
| LIBRARY | VULNERABILITY ID | SEVERITY | INSTALLED VERSION | FIXED VERSION | TITLE |
|
||||
+---------------------+------------------+----------+-----------------------------------+------------------------------------+---------------------------------------+
|
||||
| golang.org/x/crypto | CVE-2020-29652 | HIGH | 0.0.0-20190426145343-a29dc8fdc734 | v0.0.0-20201216223049-8b5274cf687f | golang: crypto/ssh: crafted |
|
||||
| | | | | | authentication request can |
|
||||
| | | | | | lead to nil pointer dereference |
|
||||
| | | | | | -->avd.aquasec.com/nvd/cve-2020-29652 |
|
||||
+ +------------------+ + +------------------------------------+---------------------------------------+
|
||||
| | CVE-2020-9283 | | | v0.0.0-20200220183623-bac4c82f6975 | golang.org/x/crypto: Processing |
|
||||
| | | | | | of crafted ssh-ed25519 |
|
||||
| | | | | | public keys allows for panic |
|
||||
| | | | | | -->avd.aquasec.com/nvd/cve-2020-9283 |
|
||||
+---------------------+------------------+----------+-----------------------------------+------------------------------------+---------------------------------------+
|
||||
```
|
||||
---
|
||||
### [Image Scan](#image)
|
||||
|
||||
NONE
|
|
@ -1,6 +1,5 @@
|
|||
# FROM golang:1.17-alpine3.14 as builder
|
||||
FROM dragonheim/golang:1.17.0 as builder
|
||||
ARG SEMVER
|
||||
FROM dragonheim/golang:1.23 as builder
|
||||
ARG SEMVER=${SEMVER:-0.0.11}
|
||||
|
||||
WORKDIR /gagent
|
||||
COPY . .
|
||||
|
@ -10,22 +9,22 @@ ARG GOOS=${GOOS:-linux}
|
|||
ARG CGO_ENABLED=1
|
||||
|
||||
RUN apk add --no-cache zeromq-dev build-base git
|
||||
RUN go build -o /gagent/bin/gagent -ldflags "-X main.semVER=${SEMVER}" gagent/main.go
|
||||
RUN go build -o /gagent/bin/gagent -ldflags "-X main.Version=${SEMVER}" cmd/gagent/main.go
|
||||
RUN strip /gagent/bin/gagent
|
||||
|
||||
FROM alpine:3.14
|
||||
ARG vernum
|
||||
FROM alpine:3.20
|
||||
ARG SEMVER
|
||||
LABEL Name="G'Agent"
|
||||
LABEL Maintainer="jwells@dragonheim.net"
|
||||
LABEL License="MIT License"
|
||||
LABEL version="${SEMVER}"
|
||||
RUN apk add --no-cache zeromq && mkdir -p -m 0700 /etc/gagent
|
||||
|
||||
COPY --from=builder /gagent/examples/gagent.hcl /etc/gagent/gagent.hcl
|
||||
COPY --from=builder /gagent/assets/examples/gagent.hcl /etc/gagent/gagent.hcl
|
||||
COPY --from=builder /gagent/bin/gagent /usr/bin/
|
||||
|
||||
# Router Client Worker
|
||||
EXPOSE 35570/tcp 35571/tcp 35572/tcp
|
||||
# Router Client Worker Prometheus
|
||||
EXPOSE 35570/tcp 35572/tcp 35571/tcp 9101/tcp
|
||||
VOLUME /etc/gagent
|
||||
|
||||
CMD ["/usr/bin/gagent"]
|
13
assets/examples/agents/fib.tcl
Normal file
13
assets/examples/agents/fib.tcl
Normal file
|
@ -0,0 +1,13 @@
|
|||
##########################################
|
||||
### Perform Fibanaci sequence up to 10 ###
|
||||
##########################################
|
||||
set GHINT [split "math, fib" ,]
|
||||
proc fib {x} {
|
||||
if {<= $x 1} {
|
||||
return 1
|
||||
} else {
|
||||
+ [fib [- $x 1]] [fib [- $x 2]]
|
||||
}
|
||||
}
|
||||
|
||||
puts [fib 20]
|
|
@ -1,7 +1,7 @@
|
|||
###################
|
||||
### Hello Earth ###
|
||||
###################
|
||||
array set GHINT ["thermal measurements" "gravity measurements" "gravity fluctuations"]
|
||||
set GHINT [split "thermal measurements, gravity measurements, gravity fluctuations" ,]
|
||||
proc hello_earth {} {
|
||||
puts "Hello Earth, does localized tempurature variations alter specific gravity?"
|
||||
}
|
18
assets/examples/agents/t2.tcl
Normal file
18
assets/examples/agents/t2.tcl
Normal file
|
@ -0,0 +1,18 @@
|
|||
###########################################
|
||||
### Square numbers in sequence up to 10 ###
|
||||
###########################################
|
||||
set GHINT [split "math, square" ,]
|
||||
proc square {x} {
|
||||
* $x $x
|
||||
}
|
||||
|
||||
set a 1
|
||||
while {<= $a 10} {
|
||||
if {== $a 5} {
|
||||
puts {Missing five!}
|
||||
set a [+ $a 1]
|
||||
continue
|
||||
}
|
||||
puts "I can compute that $a*$a = [square $a]"
|
||||
set a [+ $a 1]
|
||||
}
|
|
@ -27,7 +27,7 @@
|
|||
*
|
||||
* Required.
|
||||
*/
|
||||
mode = "router"
|
||||
mode = "client"
|
||||
|
||||
/*
|
||||
* @TODO: Add authentication based on UUID
|
||||
|
@ -48,12 +48,21 @@ mode = "router"
|
|||
// listenaddr = 0.0.0.0
|
||||
|
||||
/*
|
||||
* This is the port to the router will listen for on
|
||||
* for clients. It defaults to 35570.
|
||||
* This is the port to G'Agent will listen for on
|
||||
* for Prometheus queries. Can be overriden by MONITOR_PORT environment variable
|
||||
* Monitoring will be disabled if this is set to 0.
|
||||
*
|
||||
* Optional.
|
||||
*/
|
||||
// clientport = 35571
|
||||
// monitorport = 9101
|
||||
|
||||
/*
|
||||
* This is the port to the router will listen for on
|
||||
* for clients. It defaults to 35572.
|
||||
*
|
||||
* Optional.
|
||||
*/
|
||||
// clientport = 35572
|
||||
|
||||
/*
|
||||
* This is the port to the router will listen for on
|
||||
|
@ -69,7 +78,7 @@ mode = "router"
|
|||
*
|
||||
* Optional.
|
||||
*/
|
||||
// workerport = 35572
|
||||
// workerport = 35571
|
||||
|
||||
/*
|
||||
* @TODO
|
11
assets/examples/genesis.hcl
Normal file
11
assets/examples/genesis.hcl
Normal file
|
@ -0,0 +1,11 @@
|
|||
"genesis_time" = "2021-10-25:00:00.000000000Z"
|
||||
"chainid" = "gagent_ledger"
|
||||
|
||||
"agent" = {
|
||||
"client" = "7e9d13fe-5151-5876-66c0-20ca03e8fca4"
|
||||
"shasum" = "a76f7c3c7bc0f94b4f8aa63c605f8534db5675bb05d761f4461127fcadbf32d4"
|
||||
"status" = "complete"
|
||||
}
|
||||
"clients" = {
|
||||
"client" = "7e9d13fe-5151-5876-66c0-20ca03e8fca4"
|
||||
}
|
16
assets/examples/genesis.json
Normal file
16
assets/examples/genesis.json
Normal file
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"timestamp": "2021-10-25:00:00.000000000Z",
|
||||
"chainid": "gagent_ledger",
|
||||
"agent": {
|
||||
"status": "complete",
|
||||
"client": "7e9d13fe-5151-5876-66c0-20ca03e8fca4",
|
||||
"shasum": "a76f7c3c7bc0f94b4f8aa63c605f8534db5675bb05d761f4461127fcadbf32d4",
|
||||
"hints": {},
|
||||
"script": "",
|
||||
"answer": ""
|
||||
},
|
||||
"clients": {
|
||||
"clientID": "7e9d13fe-5151-5876-66c0-20ca03e8fca4"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 776 B After Width: | Height: | Size: 776 B |
|
@ -1,4 +1,4 @@
|
|||
# variable "vpc_id" {}
|
||||
variable "vpc_id" {}
|
||||
|
||||
data "aws_vpc" "selected" {
|
||||
id = var.vpc_id
|
9
assets/tfenv/terraform.tf
Normal file
9
assets/tfenv/terraform.tf
Normal file
|
@ -0,0 +1,9 @@
|
|||
terraform {
|
||||
required_version = ">= 1.3.1"
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 4.56.0"
|
||||
}
|
||||
}
|
||||
}
|
310
cmd/gagent/main.go
Normal file
310
cmd/gagent/main.go
Normal file
|
@ -0,0 +1,310 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
log "log"
|
||||
http "net/http"
|
||||
os "os"
|
||||
strconv "strconv"
|
||||
sync "sync"
|
||||
|
||||
autorestart "github.com/slayer/autorestart"
|
||||
|
||||
env "github.com/caarlos0/env/v6"
|
||||
|
||||
fqdn "github.com/Showmax/go-fqdn"
|
||||
|
||||
gstructs "github.com/dragonheim/gagent/internal/gstructs"
|
||||
|
||||
gc "github.com/dragonheim/gagent/internal/client"
|
||||
gr "github.com/dragonheim/gagent/internal/router"
|
||||
gs "github.com/dragonheim/gagent/internal/setup"
|
||||
gw "github.com/dragonheim/gagent/internal/worker"
|
||||
|
||||
docopt "github.com/aviddiviner/docopt-go"
|
||||
|
||||
hclsimple "github.com/hashicorp/hcl/v2/hclsimple"
|
||||
|
||||
logutils "github.com/hashicorp/logutils"
|
||||
|
||||
promhttp "github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
|
||||
uuid "github.com/jakehl/goid"
|
||||
)
|
||||
|
||||
/*
|
||||
* Exit Codes
|
||||
* 0 Success
|
||||
* 1 Environment variable parsing failed
|
||||
* 2 Configuration file is missing or unreadable
|
||||
* 3 Setup failed
|
||||
* 4 Invalid mode of operation
|
||||
* 5 Agent file is missing or unreadable
|
||||
* 6 Agent is missing tags
|
||||
* 7 No routers defined
|
||||
* 8 No workers defined
|
||||
* 9 Agent not defined
|
||||
* 10 Agent hints / tags not defined
|
||||
* 11 Router not connected
|
||||
*/
|
||||
|
||||
var environment struct {
|
||||
ConfigFile string `env:"GAGENT_CONFIG" envDefault:"/etc/gagent/gagent.hcl"`
|
||||
LogLevel string `env:"GAGENT_LOGLEVEL" envDefault:"WARN"`
|
||||
Mode string `env:"GAGENT_MODE" envDefault:"setup"`
|
||||
MonitorPort int `env:"MONITOR_PORT" envDefault:"0"`
|
||||
}
|
||||
|
||||
/*
|
||||
* This is the application configuration. It is populated from the configuration
|
||||
* file and then used throughout the application.
|
||||
*/
|
||||
var config gstructs.GagentConfig
|
||||
|
||||
/*
|
||||
* We use a WaitGroup to wait for all goroutines to finish before exiting.
|
||||
*/
|
||||
var wg sync.WaitGroup
|
||||
|
||||
/*
|
||||
* Set version
|
||||
*/
|
||||
var Version = "0.0.1"
|
||||
|
||||
/*
|
||||
* This is the main function, and it assumes that the configuration file has
|
||||
* already been read and parsed by the init() function.
|
||||
*/
|
||||
func main() {
|
||||
log.Printf("[DEBUG] Configuration is %v\n", config)
|
||||
|
||||
switch config.Mode {
|
||||
case "client":
|
||||
log.Printf("[INFO] Running in client mode\n")
|
||||
|
||||
if len(config.Routers) == 0 {
|
||||
log.Printf("[ERROR] No routers defined.\n")
|
||||
os.Exit(7)
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
go gc.Main(&wg, config)
|
||||
|
||||
case "router":
|
||||
log.Printf("[INFO] Running in router mode\n")
|
||||
|
||||
if len(config.Workers) == 0 {
|
||||
log.Printf("[ERROR] No workers defined.\n")
|
||||
os.Exit(7)
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
go gr.Main(&wg, config)
|
||||
|
||||
case "worker":
|
||||
log.Printf("[INFO] Running in worker mode\n")
|
||||
|
||||
if len(config.Routers) == 0 {
|
||||
log.Printf("[ERROR] No routers defined.\n")
|
||||
os.Exit(6)
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
go gw.Main(&wg, config)
|
||||
|
||||
case "setup":
|
||||
log.Printf("[INFO] Running in setup mode\n")
|
||||
|
||||
wg.Add(1)
|
||||
go gs.Main(&wg, config)
|
||||
|
||||
default:
|
||||
log.Printf("[ERROR] Unknown operating mode, exiting.\n")
|
||||
os.Exit(4)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
/*
|
||||
* This is the init() function. It is called before the main() function, and
|
||||
* it reads the configuration file, parses the command line arguments, and
|
||||
* reads the environment variables. It also sets up the logging.
|
||||
*/
|
||||
func init() {
|
||||
cfg := environment
|
||||
err := env.Parse(&cfg)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Failed to parse environment variables: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
filter := &logutils.LevelFilter{
|
||||
Levels: []logutils.LogLevel{"DEBUG", "INFO", "WARN", "ERROR"},
|
||||
MinLevel: logutils.LogLevel(cfg.LogLevel),
|
||||
Writer: os.Stderr,
|
||||
}
|
||||
log.SetOutput(filter)
|
||||
|
||||
http.Handle("/metrics", promhttp.Handler())
|
||||
|
||||
/*
|
||||
* Initialize the configuration
|
||||
*/
|
||||
config.Version = Version
|
||||
|
||||
config.File = cfg.ConfigFile
|
||||
|
||||
config.Mode = "setup"
|
||||
|
||||
config.Name, _ = fqdn.FqdnHostname()
|
||||
|
||||
/*
|
||||
* Set a default UUID for this node.
|
||||
* This is used throughout the G'Agent system to uniquely identify this node.
|
||||
* It can be overridden in the configuration file by setting uuid
|
||||
*/
|
||||
config.UUID = uuid.NewV4UUID().String()
|
||||
|
||||
/*
|
||||
* By default, we want to listen on all IP addresses.
|
||||
*/
|
||||
config.ListenAddr = "0.0.0.0"
|
||||
|
||||
/*
|
||||
* G'Agent will use this port for monitoring via prometheus., If set
|
||||
* is set to 0, G'Agent will not listen for prometheus metrics.
|
||||
*/
|
||||
config.MonitorPort = cfg.MonitorPort
|
||||
|
||||
/*
|
||||
* G'Agent client will use this port to communicate with the routers.
|
||||
*/
|
||||
config.ClientPort = 35572
|
||||
|
||||
/*
|
||||
* G'Agent router will use this port to communicate with other routers.
|
||||
*/
|
||||
config.RouterPort = 35570
|
||||
|
||||
/*
|
||||
* G'Agent worker will use this port to communicate with the routers.
|
||||
*/
|
||||
config.WorkerPort = 35571
|
||||
|
||||
config.Clients = make([]*gstructs.ClientDetails, 0)
|
||||
config.Routers = make([]*gstructs.RouterDetails, 0)
|
||||
config.Workers = make([]*gstructs.WorkerDetails, 0)
|
||||
|
||||
/*
|
||||
* Create a usage variable and then use that to declare the arguments and
|
||||
* options. This allows us to use DocOpt for consistency between usage help
|
||||
* and available arguments / options. Documentation is available at;
|
||||
* http://docopt.org/
|
||||
*/
|
||||
usage := "G'Agents\n"
|
||||
usage += "\n"
|
||||
usage += " Go based mobile agent system, loosely inspired by the Agent Tcl / D'Agents\n"
|
||||
usage += " system created by Robert S. Gray of Dartmouth college.\n"
|
||||
usage += "\n"
|
||||
|
||||
usage += "Usage:\n"
|
||||
usage += " gagent client (pull|push) [--config=<config>] [--agent=<file>]\n"
|
||||
usage += " gagent router [--config=<config>]\n"
|
||||
usage += " gagent worker [--config=<config>]\n"
|
||||
usage += " gagent setup [--config=<config>]\n"
|
||||
usage += " gagent --version\n"
|
||||
usage += "\n"
|
||||
|
||||
usage += "Arguments:\n"
|
||||
usage += " client pull -- Start as a G'Agent client to pull agent results\n"
|
||||
usage += " client push -- Start as a G'Agent client to push agent\n"
|
||||
usage += " router -- Start as a G'Agent router\n"
|
||||
usage += " worker -- Start as a G'Agent worker\n"
|
||||
usage += " setup -- Write initial configuration file\n"
|
||||
usage += "\n"
|
||||
|
||||
usage += "Options:\n"
|
||||
usage += " -h, --help -- Show this help screen and exit\n"
|
||||
usage += " -v, --version -- Show version and exit\n"
|
||||
usage += " --config=<config> -- [default: /etc/gagent/gagent.hcl]\n"
|
||||
usage += " --agent=<file> -- filename of the agent to be uploaded to the G'Agent network. Required in push mode\n"
|
||||
usage += "\n"
|
||||
|
||||
usage += "Environment Variables:\n"
|
||||
usage += " GAGENT_CONFIG -- [default: /etc/gagent/gagent.hcl]\n"
|
||||
usage += " GAGENT_MONITOR -- [default: 0]\n"
|
||||
usage += " MODE -- [default: setup]\n"
|
||||
usage += "\n"
|
||||
|
||||
usage += "Examples:\n"
|
||||
usage += " gagent client pull --config=/etc/gagent/gagent.hcl\n"
|
||||
usage += " gagent client push --config=/etc/gagent/gagent.hcl --agent=/tmp/agent.tcl\n"
|
||||
usage += " gagent router --config=/etc/gagent/gagent.hcl\n"
|
||||
usage += " gagent worker --config=/etc/gagent/gagent.hcl\n"
|
||||
usage += " gagent setup --config=/etc/gagent/gagent.hcl\n"
|
||||
usage += "\n"
|
||||
|
||||
/*
|
||||
* Consume the usage variable and the command line arguments to create a
|
||||
* dictionary / map.
|
||||
*/
|
||||
opts, _ := docopt.ParseArgs(usage, nil, config.Version)
|
||||
log.Printf("[DEBUG] Arguments are %v\n", opts)
|
||||
|
||||
if opts["--config"] != nil {
|
||||
config.File = opts["--config"].(string)
|
||||
}
|
||||
|
||||
err = hclsimple.DecodeFile(config.File, nil, &config)
|
||||
if err != nil && opts["setup"] == false {
|
||||
log.Printf("[ERROR] Failed to load configuration file: %s.\n", config.File)
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
/*
|
||||
* Let the command line mode override the configuration.
|
||||
*/
|
||||
if opts["setup"] == true {
|
||||
config.Mode = "setup"
|
||||
} else {
|
||||
if opts["client"] == true {
|
||||
config.Mode = "client"
|
||||
if opts["--agent"] == nil {
|
||||
log.Printf("[ERROR] Agent file not specified")
|
||||
os.Exit(8)
|
||||
} else {
|
||||
config.Agent = opts["--agent"].(string)
|
||||
}
|
||||
|
||||
if opts["pull"] == true {
|
||||
config.CMode = false
|
||||
}
|
||||
|
||||
if opts["push"] == true {
|
||||
config.CMode = true
|
||||
}
|
||||
}
|
||||
|
||||
if opts["router"] == true {
|
||||
config.Mode = "router"
|
||||
}
|
||||
|
||||
if opts["worker"] == true {
|
||||
config.Mode = "worker"
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Start Prometheus metrics exporter
|
||||
*/
|
||||
if config.MonitorPort != 0 {
|
||||
go func() {
|
||||
log.Printf("[INFO] Starting Prometheus metrics exporter on port %d\n", config.MonitorPort)
|
||||
err := http.ListenAndServe(string(config.ListenAddr)+":"+strconv.Itoa(config.MonitorPort), nil)
|
||||
log.Printf("[ERROR] Prometheus metrics exporter returned: %s\n", err)
|
||||
}()
|
||||
}
|
||||
autorestart.WatchFilename = config.File
|
||||
autorestart.StartWatcher()
|
||||
}
|
120
cmd/gagent/main_test.go
Normal file
120
cmd/gagent/main_test.go
Normal file
|
@ -0,0 +1,120 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
env "github.com/caarlos0/env/v6"
|
||||
gstructs "github.com/dragonheim/gagent/internal/gstructs"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// Mocking the WaitGroup to avoid actually waiting in tests
|
||||
type MockWaitGroup struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *MockWaitGroup) Add(delta int) {
|
||||
m.Called(delta)
|
||||
}
|
||||
|
||||
func (m *MockWaitGroup) Done() {
|
||||
m.Called()
|
||||
}
|
||||
|
||||
func (m *MockWaitGroup) Wait() {
|
||||
m.Called()
|
||||
}
|
||||
|
||||
// Mocking the config loader function to inject test configurations
|
||||
func mockInitConfig() {
|
||||
config = gstructs.GagentConfig{
|
||||
Mode: "client",
|
||||
MonitorPort: 8080,
|
||||
// Populate other required fields as needed
|
||||
}
|
||||
}
|
||||
|
||||
func TestMainFunction(t *testing.T) {
|
||||
var wg MockWaitGroup
|
||||
wg.On("Add", 1).Return()
|
||||
wg.On("Wait").Return()
|
||||
wg.On("Done").Return()
|
||||
|
||||
mockInitConfig()
|
||||
|
||||
// Test the client mode
|
||||
config.Mode = "client"
|
||||
main()
|
||||
wg.AssertCalled(t, "Add", 1)
|
||||
|
||||
// Test the router mode
|
||||
config.Mode = "router"
|
||||
main()
|
||||
wg.AssertCalled(t, "Add", 1)
|
||||
|
||||
// Test the worker mode
|
||||
config.Mode = "worker"
|
||||
main()
|
||||
wg.AssertCalled(t, "Add", 1)
|
||||
|
||||
// Test the setup mode
|
||||
config.Mode = "setup"
|
||||
main()
|
||||
wg.AssertCalled(t, "Add", 1)
|
||||
|
||||
// Test an invalid mode
|
||||
config.Mode = "invalid"
|
||||
assert.Panics(t, func() { main() }, "Expected main() to panic with invalid mode")
|
||||
}
|
||||
|
||||
func TestInitFunction(t *testing.T) {
|
||||
// Backup original stdout and defer restoration
|
||||
origStdout := os.Stdout
|
||||
defer func() { os.Stdout = origStdout }()
|
||||
|
||||
// Capture stdout output to test log output
|
||||
var logOutput bytes.Buffer
|
||||
log.SetOutput(&logOutput)
|
||||
|
||||
// Test init
|
||||
// init() #@TODO Write a useful test for init
|
||||
|
||||
// Assertions
|
||||
assert.Contains(t, logOutput.String(), "[DEBUG] Arguments are")
|
||||
assert.NotEmpty(t, config.Version, "Config version should not be empty")
|
||||
assert.NotEmpty(t, config.UUID, "Config UUID should not be empty")
|
||||
}
|
||||
|
||||
func TestPrometheusMetricsExporter(t *testing.T) {
|
||||
mockInitConfig()
|
||||
config.MonitorPort = 8080
|
||||
|
||||
req, err := http.NewRequest("GET", "/metrics", nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.DefaultServeMux.ServeHTTP(w, r)
|
||||
})
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rr.Code, "Handler returned wrong status code")
|
||||
assert.Contains(t, rr.Body.String(), "go_gc_duration_seconds", "Expected metrics output")
|
||||
}
|
||||
|
||||
func TestEnvironmentParsing(t *testing.T) {
|
||||
cfg := environment
|
||||
err := env.Parse(&cfg)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "/etc/gagent/gagent.hcl", cfg.ConfigFile, "Expected default config file path")
|
||||
assert.Equal(t, "WARN", cfg.LogLevel, "Expected default log level")
|
||||
assert.Equal(t, "setup", cfg.Mode, "Expected default mode")
|
||||
assert.Equal(t, 0, cfg.MonitorPort, "Expected default monitor port")
|
||||
}
|
4
docs/CHANGELOG.md
Normal file
4
docs/CHANGELOG.md
Normal file
|
@ -0,0 +1,4 @@
|
|||
### [0.0.0](https://github.com/dragonheim/gagent/commit/5863999) (20210425)
|
||||
---
|
||||
#### Features
|
||||
* [Initial Commit](https://github.com/dragonheim/gagent/commit/5863999) -- James Wells
|
|
@ -6,7 +6,7 @@ Document the G'Agent TCL language extension.
|
|||
|
||||
We are using [Lain Dono](mailto:lain.dono@gmail.com)'s [Picol](https://github.com/lain-dono/picol.go) package though we will be modifying it specific to support a subset of the [Agent Tcl / D'Agents](http://www.cs.dartmouth.edu/~dfk/agents/pub/agents/doc.5.1.ps.gz) language extensions and modern versions of Go.
|
||||
|
||||
The language extensions can be found in the [LANGUAGE.md](https://git.dragonheim.net/dragonheim/gagent/src/branch/main/LANGUAGE.md) file.
|
||||
The language extensions can be found in the [LANGUAGE.md](https://github.com/dragonheim/gagent/src/branch/main/LANGUAGE.md) file.
|
||||
|
||||
|
||||
## AgentTCL Language Extension
|
32
docs/VULNERABILITIES.md
Normal file
32
docs/VULNERABILITIES.md
Normal file
|
@ -0,0 +1,32 @@
|
|||
### [Source Code Scan](#source)
|
||||
```
|
||||
2023-03-15T06:59:15.989-0700 INFO Need to update DB
|
||||
2023-03-15T06:59:15.989-0700 INFO DB Repository: ghcr.io/aquasecurity/trivy-db
|
||||
2023-03-15T06:59:15.989-0700 INFO Downloading DB...
|
||||
36.01 MiB / 36.01 MiB [-----------------------------------------------------------------------------------------------------------------------------------------------------------------] 100.00% 7.93 MiB p/s 4.7s
|
||||
2023-03-15T06:59:21.791-0700 INFO Vulnerability scanning is enabled
|
||||
2023-03-15T06:59:21.791-0700 INFO Secret scanning is enabled
|
||||
2023-03-15T06:59:21.791-0700 INFO If your scanning is slow, please try '--scanners vuln' to disable secret scanning
|
||||
2023-03-15T06:59:21.791-0700 INFO Please see also https://aquasecurity.github.io/trivy/v0.37/docs/secret/scanning/#recommendation for faster secret detection
|
||||
2023-03-15T06:59:25.698-0700 INFO Number of language-specific files: 1
|
||||
2023-03-15T06:59:25.699-0700 INFO Detecting gomod vulnerabilities...
|
||||
|
||||
Total: 0 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0)
|
||||
```
|
||||
|
||||
---
|
||||
### [Image Scan](#image)
|
||||
```
|
||||
2023-03-15T07:05:44.377-0700 INFO Vulnerability scanning is enabled
|
||||
2023-03-15T07:05:44.377-0700 INFO Secret scanning is enabled
|
||||
2023-03-15T07:05:44.377-0700 INFO If your scanning is slow, please try '--scanners vuln' to disable secret scanning
|
||||
2023-03-15T07:05:44.377-0700 INFO Please see also https://aquasecurity.github.io/trivy/v0.37/docs/secret/scanning/#recommendation for faster secret detection
|
||||
2023-03-15T07:05:44.731-0700 INFO Detected OS: alpine
|
||||
2023-03-15T07:05:44.731-0700 INFO Detecting Alpine vulnerabilities...
|
||||
2023-03-15T07:05:44.732-0700 INFO Number of language-specific files: 1
|
||||
2023-03-15T07:05:44.732-0700 INFO Detecting gobinary vulnerabilities...
|
||||
|
||||
dragonheim/gagent:0.0.7 (alpine 3.17.2)
|
||||
|
||||
Total: 0 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0)
|
||||
```
|
251
gagent/main.go
251
gagent/main.go
|
@ -1,251 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
gs "git.dragonheim.net/dragonheim/gagent/src/gstructs"
|
||||
|
||||
gc "git.dragonheim.net/dragonheim/gagent/src/client"
|
||||
gr "git.dragonheim.net/dragonheim/gagent/src/router"
|
||||
gw "git.dragonheim.net/dragonheim/gagent/src/worker"
|
||||
|
||||
docopt "github.com/aviddiviner/docopt-go"
|
||||
hclsimple "github.com/hashicorp/hcl/v2/hclsimple"
|
||||
hclwrite "github.com/hashicorp/hcl/v2/hclwrite"
|
||||
logutils "github.com/hashicorp/logutils"
|
||||
uuid "github.com/nu7hatch/gouuid"
|
||||
cty "github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
var (
|
||||
semVER = "0.0.2"
|
||||
)
|
||||
|
||||
var exitCodes = struct {
|
||||
m map[string]int
|
||||
}{m: map[string]int{
|
||||
"SUCCESS": 0,
|
||||
"CONFIG_FILE_MISSING": 1,
|
||||
"SETUP_FAILED": 2,
|
||||
"INVALID_MODE": 3,
|
||||
"AGENT_LOAD_FAILED": 4,
|
||||
"AGENT_MISSING_TAGS": 5,
|
||||
"NO_ROUTERS_DEFINED": 6,
|
||||
"NO_WORKERS_DEFINED": 7,
|
||||
"AGENT_NOT_DEFINED": 8,
|
||||
}}
|
||||
|
||||
func main() {
|
||||
filter := &logutils.LevelFilter{
|
||||
Levels: []logutils.LogLevel{"DEBUG", "INFO", "WARN", "ERROR"},
|
||||
MinLevel: logutils.LogLevel("WARN"),
|
||||
Writer: os.Stderr,
|
||||
}
|
||||
log.SetOutput(filter)
|
||||
|
||||
var config gs.GagentConfig
|
||||
var configFile string = "/etc/gagent/gagent.hcl"
|
||||
|
||||
config.Name, _ = os.Hostname()
|
||||
config.Mode = "setup"
|
||||
|
||||
/*
|
||||
* Set a default UUID for this node.
|
||||
* This is used throughout the G'Agent system to uniquely identify this node.
|
||||
* It can be overridden in the configuration file by setting uuid
|
||||
*/
|
||||
identity, _ := uuid.NewV5(uuid.NamespaceURL, []byte("gagent"+config.Name))
|
||||
config.UUID = identity.String()
|
||||
|
||||
/*
|
||||
* By default, we want to listen on all IP addresses. It can be overridden
|
||||
* in the configuration file by setting listenaddr
|
||||
*/
|
||||
config.ListenAddr = "0.0.0.0"
|
||||
|
||||
/*
|
||||
* By default, G'Agent client will use port 35571 to communicate with the
|
||||
* routers, but you can override it by setting the clientport in the
|
||||
* configuration file
|
||||
*/
|
||||
config.ClientPort = 35571
|
||||
|
||||
/*
|
||||
* By default, G'Agent router will use port 35572 to communicate with
|
||||
* other routers, but you can override it by setting the routerport in
|
||||
* the configuration file
|
||||
*/
|
||||
config.RouterPort = 35570
|
||||
|
||||
/*
|
||||
* By default, G'Agent worker will use port 35570 to communicate with the
|
||||
* routers, but you can override it by setting the workerport in the
|
||||
* configuration file
|
||||
*/
|
||||
config.WorkerPort = 35572
|
||||
|
||||
/*
|
||||
* Create a usage variable and then use that to declare the arguments and
|
||||
* options. This allows us to use DocOpt for consistency between usage help
|
||||
* and available arguments / options. Documentation is available at;
|
||||
* http://docopt.org/
|
||||
*/
|
||||
usage := "G'Agents \n"
|
||||
usage += "\n"
|
||||
usage += " Go based mobile agent system, loosely inspired by the Agent Tcl / D'Agents \n"
|
||||
usage += " system created by Robert S. Gray of Dartmouth college. \n"
|
||||
usage += "\n"
|
||||
|
||||
usage += "Usage: \n"
|
||||
usage += " gagent client [--config=<config>] [--agent=<file>] \n"
|
||||
usage += " gagent router [--config=<config>] \n"
|
||||
usage += " gagent worker [--config=<config>] \n"
|
||||
usage += " gagent setup [--config=<config>] \n"
|
||||
usage += " gagent --version \n"
|
||||
usage += "\n"
|
||||
|
||||
usage += "Arguments: \n"
|
||||
usage += " client -- Start as a G'Agent client \n"
|
||||
usage += " router -- Start as a G'Agent router \n"
|
||||
usage += " worker -- Start as a G'Agent worker \n"
|
||||
usage += " setup -- Write initial configuration file \n"
|
||||
usage += "\n"
|
||||
|
||||
usage += "Options:\n"
|
||||
usage += " -h --help -- Show this help screen and exit \n"
|
||||
usage += " --version -- Show version and exit \n"
|
||||
usage += " --config=<config> -- [default: /etc/gagent/gagent.hcl] \n"
|
||||
usage += " --agent=<file> -- filename of the agent to be uploaded to the G'Agent network \n"
|
||||
|
||||
/*
|
||||
* Consume the usage variable and the command line arguments to create a
|
||||
* dictionary / map.
|
||||
*/
|
||||
opts, _ := docopt.ParseArgs(usage, nil, semVER)
|
||||
log.Printf("[DEBUG] Arguments are %v\n", opts)
|
||||
|
||||
if opts["--config"] != nil {
|
||||
configFile = opts["--config"].(string)
|
||||
}
|
||||
|
||||
/*
|
||||
* Let the command line mode override the configuration.
|
||||
*/
|
||||
if opts["setup"] == true {
|
||||
config.Mode = "setup"
|
||||
} else {
|
||||
err := hclsimple.DecodeFile(configFile, nil, &config)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Failed to load configuration file: %s.\n", configFile)
|
||||
log.Printf("[ERROR] %s\n", err)
|
||||
os.Exit(exitCodes.m["CONFIG_FILE_MISSING"])
|
||||
}
|
||||
if opts["client"] == true {
|
||||
config.Mode = "client"
|
||||
}
|
||||
if opts["router"] == true {
|
||||
config.Mode = "router"
|
||||
}
|
||||
if opts["worker"] == true {
|
||||
config.Mode = "worker"
|
||||
}
|
||||
}
|
||||
log.Printf("[DEBUG] Configuration is %v\n", config)
|
||||
|
||||
switch config.Mode {
|
||||
case "client":
|
||||
/*
|
||||
* Client mode will send an agent file to a router for processing
|
||||
* Clients do not process the agent files, only send them as
|
||||
* requests to a router. If started without arguments, the client
|
||||
* will contact the router and attempt to retrieve the results
|
||||
* of it's most recent request.
|
||||
*/
|
||||
log.Printf("[INFO] Running in client mode\n")
|
||||
|
||||
if len(config.Routers) == 0 {
|
||||
log.Printf("[ERROR] No routers defined.\n")
|
||||
os.Exit(exitCodes.m["NO_ROUTERS_DEFINED"])
|
||||
}
|
||||
|
||||
if opts["--agent"] == nil {
|
||||
log.Printf("[ERROR] Agent file not specified")
|
||||
os.Exit(exitCodes.m["AGENT_NOT_DEFINED"])
|
||||
}
|
||||
agent, err := ioutil.ReadFile(opts["--agent"].(string))
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Failed to load Agent file: %s", opts["--agent"])
|
||||
os.Exit(exitCodes.m["AGENT_LOAD_FAILED"])
|
||||
}
|
||||
for key := range config.Routers {
|
||||
go gc.Main(config, key, string(agent))
|
||||
time.Sleep(10 * time.Second)
|
||||
}
|
||||
|
||||
case "router":
|
||||
/*
|
||||
* The 'router' processes routing requests from the agent. The router does
|
||||
* not handle any of the agent activities beyond processing the agent's
|
||||
* list of tags and passing the agent and it's storage to either a member
|
||||
* or client node. Tags are used by the agent to give hints as to where
|
||||
* it should be routed.
|
||||
*/
|
||||
log.Printf("[INFO] Running in router mode\n")
|
||||
|
||||
if len(config.Workers) == 0 {
|
||||
log.Printf("[ERROR] No workers defined.\n")
|
||||
os.Exit(exitCodes.m["NO_WORKERS_DEFINED"])
|
||||
}
|
||||
|
||||
go gr.Main(config)
|
||||
select {}
|
||||
|
||||
case "worker":
|
||||
/*
|
||||
* The 'worker' processes the agent code. The worker nodes do not know
|
||||
* anything about the network structure. Instead they know only to which
|
||||
* router(s) they are connected. The worker will execute the agent code and
|
||||
* pass the agent and it's results to a router.
|
||||
*/
|
||||
log.Printf("[INFO] Running in worker mode\n")
|
||||
|
||||
if len(config.Routers) == 0 {
|
||||
log.Printf("[ERROR] No routers defined.\n")
|
||||
os.Exit(exitCodes.m["NO_ROUTERS_DEFINED"])
|
||||
}
|
||||
|
||||
for key := range config.Routers {
|
||||
go gw.Main(config, key)
|
||||
time.Sleep(10 * time.Second)
|
||||
}
|
||||
|
||||
select {}
|
||||
|
||||
case "setup":
|
||||
log.Printf("[INFO] Running in setup mode\n")
|
||||
f := hclwrite.NewEmptyFile()
|
||||
rootBody := f.Body()
|
||||
rootBody.SetAttributeValue("name", cty.StringVal(config.Name))
|
||||
rootBody.SetAttributeValue("mode", cty.StringVal("client"))
|
||||
rootBody.SetAttributeValue("uuid", cty.StringVal(config.UUID))
|
||||
rootBody.AppendNewline()
|
||||
|
||||
routerBlock1 := rootBody.AppendNewBlock("router", []string{config.Name})
|
||||
routerBody1 := routerBlock1.Body()
|
||||
routerBody1.SetAttributeValue("routerid", cty.StringVal(config.UUID))
|
||||
routerBody1.SetAttributeValue("address", cty.StringVal("127.0.0.1"))
|
||||
rootBody.AppendNewline()
|
||||
|
||||
log.Printf("\n%s", f.Bytes())
|
||||
os.Exit(exitCodes.m["SUCCESS"])
|
||||
|
||||
default:
|
||||
log.Printf("[ERROR] Unknown operating mode, exiting.\n")
|
||||
os.Exit(exitCodes.m["INVALID_MODE"])
|
||||
}
|
||||
|
||||
os.Exit(exitCodes.m["SUCCESS"])
|
||||
}
|
46
go.mod
46
go.mod
|
@ -1,15 +1,43 @@
|
|||
module git.dragonheim.net/dragonheim/gagent
|
||||
module github.com/dragonheim/gagent
|
||||
|
||||
go 1.16
|
||||
go 1.23
|
||||
|
||||
require (
|
||||
github.com/Showmax/go-fqdn v1.0.0
|
||||
github.com/aviddiviner/docopt-go v0.0.0-20170807220726-d8a1d67efc6a
|
||||
github.com/caarlos0/env/v6 v6.10.1
|
||||
github.com/carlmjohnson/versioninfo v0.22.5
|
||||
github.com/hashicorp/hcl/v2 v2.22.0
|
||||
github.com/hashicorp/logutils v1.0.0
|
||||
github.com/jakehl/goid v1.1.0
|
||||
github.com/pebbe/zmq4 v1.2.11
|
||||
github.com/prometheus/client_golang v1.20.3
|
||||
github.com/slayer/autorestart v0.0.0-20170706172547-5ebd91f955ae
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/zclconf/go-cty v1.15.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/agext/levenshtein v1.2.3 // indirect
|
||||
github.com/aviddiviner/docopt-go v0.0.0-20170807220726-d8a1d67efc6a
|
||||
github.com/hashicorp/hcl/v2 v2.10.0
|
||||
github.com/hashicorp/logutils v1.0.0
|
||||
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/klauspost/compress v1.17.9 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
|
||||
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d
|
||||
github.com/pebbe/zmq4 v1.2.6
|
||||
github.com/zclconf/go-cty v1.8.3
|
||||
golang.org/x/text v0.3.6 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.59.1 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
golang.org/x/mod v0.21.0 // indirect
|
||||
golang.org/x/sync v0.8.0 // indirect
|
||||
golang.org/x/sys v0.25.0 // indirect
|
||||
golang.org/x/text v0.18.0 // indirect
|
||||
golang.org/x/tools v0.24.0 // indirect
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
|
125
go.sum
125
go.sum
|
@ -1,69 +1,82 @@
|
|||
github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
|
||||
github.com/Showmax/go-fqdn v1.0.0 h1:0rG5IbmVliNT5O19Mfuvna9LL7zlHyRfsSvBPZmF9tM=
|
||||
github.com/Showmax/go-fqdn v1.0.0/go.mod h1:SfrFBzmDCtCGrnHhoDjuvFnKsWjEQX/Q9ARZvOrJAko=
|
||||
github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
|
||||
github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
|
||||
github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM=
|
||||
github.com/apparentlymart/go-textseg v1.0.0 h1:rRmlIsPEEhUTIKQb7T++Nz/A5Q6C9IuX2wFoYVvnCs0=
|
||||
github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk=
|
||||
github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw=
|
||||
github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo=
|
||||
github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
|
||||
github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
|
||||
github.com/aviddiviner/docopt-go v0.0.0-20170807220726-d8a1d67efc6a h1:YJuVATwP+Gzk7nys0U/DKjKkoYp1n/sYm0yi5vX8W8M=
|
||||
github.com/aviddiviner/docopt-go v0.0.0-20170807220726-d8a1d67efc6a/go.mod h1:pBRbUcGboHT5qBceq2Cg/WIcDbO78a8wPxTg8zmS3Hs=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/caarlos0/env/v6 v6.10.1 h1:t1mPSxNpei6M5yAeu1qtRdPAK29Nbcf/n3G7x+b3/II=
|
||||
github.com/caarlos0/env/v6 v6.10.1/go.mod h1:hvp/ryKXKipEkcuYjs9mI4bBCg+UI0Yhgm5Zu0ddvwc=
|
||||
github.com/carlmjohnson/versioninfo v0.22.5 h1:O00sjOLUAFxYQjlN/bzYTuZiS0y6fWDQjMRvwtKgwwc=
|
||||
github.com/carlmjohnson/versioninfo v0.22.5/go.mod h1:QT9mph3wcVfISUKd0i9sZfVrPviHuSF+cUtLjm2WSf8=
|
||||
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/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
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/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
|
||||
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
||||
github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/hashicorp/hcl/v2 v2.10.0 h1:1S1UnuhDGlv3gRFV4+0EdwB+znNP5HmcGbIqwnSCByg=
|
||||
github.com/hashicorp/hcl/v2 v2.10.0/go.mod h1:FwWsfWEjyV/CMj8s/gqAuiviY72rJ1/oayI9WftqcKg=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/hashicorp/hcl/v2 v2.22.0 h1:hkZ3nCtqeJsDhPRFz5EA9iwcG1hNWGePOTw6oyul12M=
|
||||
github.com/hashicorp/hcl/v2 v2.22.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA=
|
||||
github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y=
|
||||
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4=
|
||||
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
|
||||
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
|
||||
github.com/jakehl/goid v1.1.0 h1:c08GO8z16wWJtfQhyiD8BQRFkpf1oDxaPmA+uYAG+50=
|
||||
github.com/jakehl/goid v1.1.0/go.mod h1:V6bQh+tr2Oay5WHL0jmTTJWrABYIO+cs4/P6e1prV1o=
|
||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
|
||||
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
|
||||
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ=
|
||||
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U=
|
||||
github.com/pebbe/zmq4 v1.2.6 h1:zcTAfa/jYi2RxjTFIdLfnVrmxbAd9nSr02+CDMn8swg=
|
||||
github.com/pebbe/zmq4 v1.2.6/go.mod h1:nqnPueOapVhE2wItZ0uOErngczsJdLOGkebMxaO8r48=
|
||||
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/pebbe/zmq4 v1.2.11 h1:Ua5mgIaZeabUGnH7tqswkUcjkL7JYGai5e8v4hpEU9Q=
|
||||
github.com/pebbe/zmq4 v1.2.11/go.mod h1:nqnPueOapVhE2wItZ0uOErngczsJdLOGkebMxaO8r48=
|
||||
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/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
|
||||
github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4=
|
||||
github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
|
||||
github.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8=
|
||||
github.com/zclconf/go-cty v1.8.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk=
|
||||
github.com/zclconf/go-cty v1.8.3 h1:48gwZXrdSADU2UW9eZKHprxAI7APZGW9XmExpJpSjT0=
|
||||
github.com/zclconf/go-cty v1.8.3/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk=
|
||||
github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502175342-a43fa875dd82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
github.com/prometheus/client_golang v1.20.3 h1:oPksm4K8B+Vt35tUhw6GbSNSgVlVSBH0qELP/7u83l4=
|
||||
github.com/prometheus/client_golang v1.20.3/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.59.1 h1:LXb1quJHWm1P6wq/U824uxYi4Sg0oGvNeUm1z5dJoX0=
|
||||
github.com/prometheus/common v0.59.1/go.mod h1:GpWM7dewqmVYcd7SmRaiWVe9SSqjf0UrwnYnpEZNuT0=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
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/slayer/autorestart v0.0.0-20170706172547-5ebd91f955ae h1:hnJJroq/kooxO2jUKDc8KXxj8tilWvOlD0hzDDv05ss=
|
||||
github.com/slayer/autorestart v0.0.0-20170706172547-5ebd91f955ae/go.mod h1:p+QQKBy7tS+myk+y3sgnAKx4gUtD/Q9Z6KEd77cLzWY=
|
||||
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.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/zclconf/go-cty v1.15.0 h1:tTCRWxsexYUmtt/wVxgDClUe+uQusuI443uL6e+5sXQ=
|
||||
github.com/zclconf/go-cty v1.15.0/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
|
||||
github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo=
|
||||
github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM=
|
||||
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
|
||||
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
||||
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
|
||||
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
|
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||
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/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
102
internal/chaindb/chaindb.go
Normal file
102
internal/chaindb/chaindb.go
Normal file
|
@ -0,0 +1,102 @@
|
|||
package chaindb
|
||||
|
||||
import (
|
||||
sha256 "crypto/sha256"
|
||||
fmt "fmt"
|
||||
log "log"
|
||||
os "os"
|
||||
time "time"
|
||||
|
||||
gs "github.com/dragonheim/gagent/internal/gstructs"
|
||||
|
||||
cty "github.com/zclconf/go-cty/cty"
|
||||
|
||||
hclsimple "github.com/hashicorp/hcl/v2/hclsimple"
|
||||
hclwrite "github.com/hashicorp/hcl/v2/hclwrite"
|
||||
)
|
||||
|
||||
type GagentDb struct {
|
||||
ChainRow []*GagentDbRow `hcl:"timestamp,block"`
|
||||
}
|
||||
|
||||
type GagentDbRow struct {
|
||||
Timestamp time.Time `hcl:"timestamp"`
|
||||
DBName string `hcl:"chainid,optional"`
|
||||
Agent gs.AgentDetails `hcl:"agent,block"`
|
||||
DbCurrHash [32]byte `hcl:"currhash"`
|
||||
DbPrevHash [32]byte `hcl:"prevhash"`
|
||||
}
|
||||
|
||||
/*
|
||||
* Initialize the database
|
||||
*/
|
||||
func NewGagentDb() *GagentDb {
|
||||
return &GagentDb{
|
||||
ChainRow: make([]*GagentDbRow, 0),
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Load the database from disk
|
||||
*/
|
||||
func (db *GagentDb) LoadHCL(ChainDBPath string) error {
|
||||
err := hclsimple.DecodeFile(ChainDBPath, nil, db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("[DEBUG] DB values: %v\n", db)
|
||||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
* Write the database to an HCL file
|
||||
*/
|
||||
func (db *GagentDb) WriteHCL(ChainDBPath string) error {
|
||||
f := hclwrite.NewEmptyFile()
|
||||
rootBody := f.Body()
|
||||
|
||||
for _, row := range db.ChainRow {
|
||||
rowBlock := rootBody.AppendNewBlock("row", []string{})
|
||||
rowBody := rowBlock.Body()
|
||||
|
||||
rowBody.SetAttributeValue("timestamp", cty.StringVal(row.Timestamp.Format(time.RFC3339)))
|
||||
rowBody.SetAttributeValue("chainid", cty.StringVal(row.DBName))
|
||||
rowBody.SetAttributeValue("currhash", cty.StringVal(fmt.Sprintf("%x", row.DbCurrHash)))
|
||||
rowBody.SetAttributeValue("prevhash", cty.StringVal(fmt.Sprintf("%x", row.DbPrevHash)))
|
||||
|
||||
agentBlock := rowBody.AppendNewBlock("agent", []string{})
|
||||
agentBody := agentBlock.Body()
|
||||
agentBody.SetAttributeValue("name", cty.StringVal(row.Agent.Client))
|
||||
agentBody.SetAttributeValue("version", cty.StringVal(row.Agent.Shasum))
|
||||
}
|
||||
|
||||
return os.WriteFile(ChainDBPath, f.Bytes(), 0600)
|
||||
}
|
||||
|
||||
/*
|
||||
* Add a new row to the chaindb
|
||||
*/
|
||||
func (db *GagentDb) AddRow(row *GagentDbRow) {
|
||||
row.Timestamp = time.Now()
|
||||
db.ChainRow = append(db.ChainRow, row)
|
||||
db.SetCurrHash()
|
||||
db.SetPrevHash()
|
||||
}
|
||||
|
||||
/*
|
||||
* Set current hash of the database
|
||||
*/
|
||||
func (db *GagentDb) SetCurrHash() {
|
||||
row := db.ChainRow[len(db.ChainRow)-1]
|
||||
row.DbCurrHash = sha256.Sum256([]byte(fmt.Sprintf("%v", db)))
|
||||
}
|
||||
|
||||
/*
|
||||
* Set previous hash of the database
|
||||
*/
|
||||
func (db *GagentDb) SetPrevHash() {
|
||||
row := db.ChainRow[len(db.ChainRow)-1]
|
||||
if len(db.ChainRow) > 1 {
|
||||
row.DbPrevHash = db.ChainRow[len(db.ChainRow)-2].DbCurrHash
|
||||
}
|
||||
}
|
65
internal/chaindb/chaindb_test.go
Normal file
65
internal/chaindb/chaindb_test.go
Normal file
|
@ -0,0 +1,65 @@
|
|||
package chaindb
|
||||
|
||||
import (
|
||||
bytes "bytes"
|
||||
os "os"
|
||||
testing "testing"
|
||||
time "time"
|
||||
|
||||
gs "github.com/dragonheim/gagent/internal/gstructs"
|
||||
)
|
||||
|
||||
const testChainDBPath = "/tmp/test_chaindb.hcl"
|
||||
|
||||
func TestGagentDb(t *testing.T) {
|
||||
// Create a new GagentDb
|
||||
db := NewGagentDb()
|
||||
|
||||
// Add a row to the database
|
||||
row := &GagentDbRow{
|
||||
DBName: "testDB",
|
||||
Agent: gs.AgentDetails{
|
||||
Client: "testAgent",
|
||||
Shasum: "v1.0.0",
|
||||
},
|
||||
}
|
||||
db.AddRow(row)
|
||||
|
||||
// Check if the row was added correctly
|
||||
if len(db.ChainRow) != 1 {
|
||||
t.Errorf("Expected length of ChainRow to be 1, but got %d", len(db.ChainRow))
|
||||
}
|
||||
|
||||
// Check if the timestamp was set correctly
|
||||
if db.ChainRow[0].Timestamp.After(time.Now()) {
|
||||
t.Error("Timestamp is incorrectly set in the future")
|
||||
}
|
||||
|
||||
// Write the database to an HCL file
|
||||
err := db.WriteHCL(testChainDBPath)
|
||||
if err != nil {
|
||||
t.Errorf("Error writing HCL file: %v", err)
|
||||
}
|
||||
|
||||
// Load the database from the HCL file
|
||||
loadedDb := NewGagentDb()
|
||||
err = loadedDb.LoadHCL(testChainDBPath)
|
||||
if err != nil {
|
||||
t.Errorf("Error loading HCL file: %v", err)
|
||||
}
|
||||
|
||||
// Check if the loaded database is the same as the original one
|
||||
if !bytes.Equal(loadedDb.ChainRow[0].DbCurrHash[:], db.ChainRow[0].DbCurrHash[:]) {
|
||||
t.Error("Loaded database has a different current hash than the original one")
|
||||
}
|
||||
|
||||
if !bytes.Equal(loadedDb.ChainRow[0].DbPrevHash[:], db.ChainRow[0].DbPrevHash[:]) {
|
||||
t.Error("Loaded database has a different previous hash than the original one")
|
||||
}
|
||||
|
||||
// Clean up the test HCL file
|
||||
err = os.Remove(testChainDBPath)
|
||||
if err != nil {
|
||||
t.Errorf("Error cleaning up test HCL file: %v", err)
|
||||
}
|
||||
}
|
120
internal/client/client.go
Normal file
120
internal/client/client.go
Normal file
|
@ -0,0 +1,120 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
sha "crypto/sha256"
|
||||
hex "encoding/hex"
|
||||
fmt "fmt"
|
||||
log "log"
|
||||
os "os"
|
||||
regexp "regexp"
|
||||
strconv "strconv"
|
||||
strings "strings"
|
||||
sync "sync"
|
||||
time "time"
|
||||
|
||||
gs "github.com/dragonheim/gagent/internal/gstructs"
|
||||
|
||||
zmq "github.com/pebbe/zmq4"
|
||||
)
|
||||
|
||||
/*
|
||||
* Client mode will send an agent file to a router for processing
|
||||
* Clients do not process the agent files, only send them as
|
||||
* requests to a router. If started without arguments, the client
|
||||
* will contact the router and attempt to retrieve the results
|
||||
* of it's most recent request.
|
||||
* Main is the entrypoint for the client process
|
||||
*/
|
||||
func Main(wg *sync.WaitGroup, config gs.GagentConfig) {
|
||||
log.Printf("[INFO] Starting client\n")
|
||||
defer wg.Done()
|
||||
|
||||
var agent gs.AgentDetails
|
||||
var err error
|
||||
|
||||
if config.CMode {
|
||||
agent.Script, err = os.ReadFile(config.Agent)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] No such file or directory: %s", config.Agent)
|
||||
os.Exit(4)
|
||||
}
|
||||
log.Printf("[DEBUG] Agent file contents: \n----- -----\n%s\n----- -----\n", agent.Script)
|
||||
}
|
||||
agent.Client = config.UUID
|
||||
tmpsum := sha.Sum256([]byte(agent.Script))
|
||||
agent.Shasum = fmt.Sprintf("%v", hex.EncodeToString(tmpsum[:]))
|
||||
log.Printf("[INFO] SHA256 of Agent file: %s", agent.Shasum)
|
||||
agent.Status = 1
|
||||
agent.Hints = getTagsFromHints(agent)
|
||||
agent.Answer = nil
|
||||
|
||||
for key := range config.Routers {
|
||||
/*
|
||||
* Generate connect string for this router.
|
||||
*/
|
||||
rport := config.ClientPort
|
||||
if config.Routers[key].ClientPort != 0 {
|
||||
rport = config.Routers[key].ClientPort
|
||||
}
|
||||
connectString := "tcp://" + config.Routers[key].RouterAddr + ":" + strconv.Itoa(rport)
|
||||
|
||||
wg.Add(1)
|
||||
go sendAgent(wg, config.UUID, connectString, agent)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Parse Agent file for GHINT data to populate the G'Agent hints
|
||||
*/
|
||||
func getTagsFromHints(agent gs.AgentDetails) []string {
|
||||
var tags []string
|
||||
|
||||
// Use named capture groups to extract the hints
|
||||
re := regexp.MustCompile(`^*set\s+GHINT\s*\[\s*split\s*"(?P<Hints>[^"]+)"\s*,\s*\]`)
|
||||
res := re.FindStringSubmatch(string(agent.Script))
|
||||
|
||||
// If we don't have at least 2 matches, we have no hints
|
||||
if len(res) < 2 {
|
||||
log.Printf("[ERROR] Agent is missing GHINT tags")
|
||||
os.Exit(4)
|
||||
}
|
||||
|
||||
// Use named capturing group index
|
||||
hintsIndex := re.SubexpIndex("Hints")
|
||||
tags = strings.Split(res[hintsIndex], ",")
|
||||
|
||||
log.Printf("[DEBUG] G'Agent hints: %v\n", tags)
|
||||
|
||||
return tags
|
||||
}
|
||||
|
||||
func sendAgent(wg *sync.WaitGroup, uuid string, connectString string, agent gs.AgentDetails) {
|
||||
defer wg.Done()
|
||||
|
||||
var mu sync.Mutex
|
||||
mu.Lock()
|
||||
|
||||
sock, _ := zmq.NewSocket(zmq.REQ)
|
||||
defer sock.Close()
|
||||
|
||||
_ = sock.SetIdentity(uuid)
|
||||
|
||||
log.Printf("[DEBUG] Attempting to connect to %s\n", connectString)
|
||||
err := sock.Connect(connectString)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Failed to connect to %s\n", connectString)
|
||||
os.Exit(10)
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG] Start sending agent...\n")
|
||||
agent.Status = 2
|
||||
status, err := sock.SendMessage(agent)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Failed to send agent to router\n")
|
||||
return
|
||||
}
|
||||
log.Printf("[DEBUG] Agent send status: %d\n", status)
|
||||
mu.Unlock()
|
||||
time.Sleep(10 * time.Second)
|
||||
|
||||
}
|
153
internal/client/client_test.go
Normal file
153
internal/client/client_test.go
Normal file
|
@ -0,0 +1,153 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
bytes "bytes"
|
||||
errors "errors"
|
||||
io "io"
|
||||
log "log"
|
||||
os "os"
|
||||
sync "sync"
|
||||
testing "testing"
|
||||
|
||||
gs "github.com/dragonheim/gagent/internal/gstructs"
|
||||
|
||||
zmq "github.com/pebbe/zmq4"
|
||||
)
|
||||
|
||||
type mockSocket struct {
|
||||
sendMessageError error
|
||||
}
|
||||
|
||||
func (m *mockSocket) Close() error { return nil }
|
||||
func (m *mockSocket) Bind(endpoint string) error { return nil }
|
||||
func (m *mockSocket) Connect(endpoint string) error { return nil }
|
||||
func (m *mockSocket) SetIdentity(identity string) error { return nil }
|
||||
func (m *mockSocket) SendMessage(parts ...interface{}) (int, error) { return 0, m.sendMessageError }
|
||||
func (m *mockSocket) RecvMessage(flags zmq.Flag) ([]string, error) { return nil, nil }
|
||||
func (m *mockSocket) RecvMessageBytes(flags zmq.Flag) ([][]byte, error) { return nil, nil }
|
||||
func (m *mockSocket) RecvMessageString(flags zmq.Flag) ([]string, error) { return nil, nil }
|
||||
func (m *mockSocket) SetSubscribe(filter string) error { return nil }
|
||||
func (m *mockSocket) SetUnsubscribe(filter string) error { return nil }
|
||||
func (m *mockSocket) Send(msg string, flags zmq.Flag) (int, error) { return 0, nil }
|
||||
func (m *mockSocket) SendBytes(msg []byte, flags zmq.Flag) (int, error) { return 0, nil }
|
||||
func (m *mockSocket) SendFrame(msg []byte, flags zmq.Flag) (int, error) { return 0, nil }
|
||||
func (m *mockSocket) SendMultipart(parts [][]byte, flags zmq.Flag) (int, error) { return 0, nil }
|
||||
func (m *mockSocket) Recv(flags zmq.Flag) (string, error) { return "", nil }
|
||||
func (m *mockSocket) RecvBytes(flags zmq.Flag) ([]byte, error) { return nil, nil }
|
||||
func (m *mockSocket) RecvFrame(flags zmq.Flag) ([]byte, error) { return nil, nil }
|
||||
func (m *mockSocket) RecvMultipart(flags zmq.Flag) ([][]byte, error) { return nil, nil }
|
||||
func (m *mockSocket) SetOption(option zmq.SocketOption, value interface{}) error { return nil }
|
||||
func (m *mockSocket) GetOption(option zmq.SocketOption) (interface{}, error) { return nil, nil }
|
||||
func (m *mockSocket) Events() zmq.State { return 0 }
|
||||
func (m *mockSocket) String() string { return "" }
|
||||
|
||||
func TestGetTagsFromHints(t *testing.T) {
|
||||
agent := gs.AgentDetails{
|
||||
Script: []byte(`*set GHINT[split "tag1,tag2,tag3",]`),
|
||||
}
|
||||
|
||||
expectedHints := []string{"tag1", "tag2", "tag3"}
|
||||
hints := getTagsFromHints(agent)
|
||||
|
||||
if !equalStringSlices(hints, expectedHints) {
|
||||
t.Errorf("Expected hints %v, but got %v", expectedHints, hints)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendAgent(t *testing.T) {
|
||||
wg := &sync.WaitGroup{}
|
||||
config := gs.GagentConfig{
|
||||
UUID: "test-uuid",
|
||||
ClientPort: 1234,
|
||||
Routers: map[string]gs.Router{
|
||||
"test-router": {
|
||||
RouterAddr: "127.0.0.1",
|
||||
ClientPort: 1234,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
agent := gs.AgentDetails{
|
||||
Client: "test-client",
|
||||
Script: []byte(`*set GHINT[split "tag1,tag2,tag3",]`),
|
||||
}
|
||||
|
||||
// Replace zmq.NewSocket with a function that returns a mock socket
|
||||
origNewSocket := newSocket
|
||||
defer func() { newSocket = origNewSocket }()
|
||||
newSocket = func(t zmq.Type) (zmq.Socket, error) {
|
||||
return &mockSocket{}, nil
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
go sendAgent(wg, config.UUID, "tcp://127.0.0.1:1234", agent)
|
||||
wg.Wait()
|
||||
|
||||
// Test with an error in sending a message
|
||||
newSocket = func(t zmq.Type) (zmq.Socket, error) {
|
||||
return &mockSocket{sendMessageError: errors.New("send message error")}, nil
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
go sendAgent(wg, config.UUID, "tcp://127.0.0.1:1234", agent)
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func equalStringSlices(a, b []string) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i := range a {
|
||||
if a[i] != b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func TestMain(t *testing.T) {
|
||||
// Prepare a temporary agent file for testing
|
||||
tmpAgentFile, err := io.TempFile("", "agent")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.Remove(tmpAgentFile.Name())
|
||||
|
||||
content := []byte(`*set GHINT[split "tag1,tag2,tag3",]`)
|
||||
if _, err := tmpAgentFile.Write(content); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := tmpAgentFile.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
config := gs.GagentConfig{
|
||||
CMode: true,
|
||||
UUID: "test-uuid",
|
||||
ClientPort: 1234,
|
||||
Agent: tmpAgentFile.Name(),
|
||||
Routers: map[string]gs.Router{
|
||||
"test-router": {
|
||||
RouterAddr: "127.0.0.1",
|
||||
ClientPort: 1234,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Replace log output with a buffer to suppress output during testing
|
||||
origLogOutput := log.Writer()
|
||||
defer log.SetOutput(origLogOutput)
|
||||
log.SetOutput(&bytes.Buffer{})
|
||||
|
||||
// Replace zmq.NewSocket with a function that returns a mock socket
|
||||
origNewSocket := newSocket
|
||||
defer func() { newSocket = origNewSocket }()
|
||||
newSocket = func(t zmq.Type) (zmq.Socket, error) {
|
||||
return &mockSocket{}, nil
|
||||
}
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(1)
|
||||
go Main(wg, config)
|
||||
wg.Wait()
|
||||
}
|
35
internal/gstructs/agent_status.go
Normal file
35
internal/gstructs/agent_status.go
Normal file
|
@ -0,0 +1,35 @@
|
|||
package gstructs
|
||||
|
||||
import (
|
||||
fmt "fmt"
|
||||
)
|
||||
|
||||
type AgentStatus []string
|
||||
|
||||
var AgentStatuses = AgentStatus{
|
||||
"ERROR",
|
||||
"INIT",
|
||||
"SENDING",
|
||||
"RECEIVING",
|
||||
"ROUTING",
|
||||
"PROCESSING",
|
||||
"COMPLETED",
|
||||
"RETURNING",
|
||||
"ERROR",
|
||||
}
|
||||
|
||||
func (a AgentStatus) GetByIndex(index int) (string, error) {
|
||||
if index < 0 || index >= len(a) {
|
||||
return "", fmt.Errorf("invalid index: %d", index)
|
||||
}
|
||||
return a[index], nil
|
||||
}
|
||||
|
||||
func (a AgentStatus) GetByName(name string) (byte, error) {
|
||||
for i, status := range a {
|
||||
if status == name {
|
||||
return byte(i), nil
|
||||
}
|
||||
}
|
||||
return 0, fmt.Errorf("value not found: %s", name)
|
||||
}
|
59
internal/gstructs/agent_status_test.go
Normal file
59
internal/gstructs/agent_status_test.go
Normal file
|
@ -0,0 +1,59 @@
|
|||
package gstructs_test
|
||||
|
||||
import (
|
||||
testing "testing"
|
||||
|
||||
gs "github.com/dragonheim/gagent/internal/gstructs"
|
||||
assert "github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetByIndex(t *testing.T) {
|
||||
agentStatuses := gs.AgentStatuses
|
||||
|
||||
tests := []struct {
|
||||
index int
|
||||
expected string
|
||||
shouldReturn bool
|
||||
}{
|
||||
{0, "ERROR", true},
|
||||
{1, "INIT", true},
|
||||
{8, "ERROR", true},
|
||||
{9, "", false},
|
||||
{-1, "", false},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
res, err := agentStatuses.GetByIndex(test.index)
|
||||
if test.shouldReturn {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, test.expected, res)
|
||||
} else {
|
||||
assert.Error(t, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetByName(t *testing.T) {
|
||||
agentStatuses := gs.AgentStatuses
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
expected byte
|
||||
shouldReturn bool
|
||||
}{
|
||||
{"ERROR", 0, true},
|
||||
{"INIT", 1, true},
|
||||
{"COMPLETED", 6, true},
|
||||
{"INVALID", 0, false},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
res, err := agentStatuses.GetByName(test.name)
|
||||
if test.shouldReturn {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, test.expected, res)
|
||||
} else {
|
||||
assert.Error(t, err)
|
||||
}
|
||||
}
|
||||
}
|
120
internal/gstructs/gstructs.go
Normal file
120
internal/gstructs/gstructs.go
Normal file
|
@ -0,0 +1,120 @@
|
|||
package gstructs
|
||||
|
||||
/*
|
||||
* GagentConfig is the primary construct used by all modes
|
||||
*/
|
||||
type GagentConfig struct {
|
||||
Name string `hcl:"name,optional"`
|
||||
Mode string `hcl:"mode,attr"`
|
||||
UUID string `hcl:"uuid,optional"`
|
||||
ListenAddr string `hcl:"listenaddr,optional"`
|
||||
ChainDBPath string `hcl:"chaindbpath,optional"`
|
||||
MonitorPort int `hcl:"monitorport,optional"`
|
||||
ClientPort int `hcl:"clientport,optional"`
|
||||
RouterPort int `hcl:"routerport,optional"`
|
||||
WorkerPort int `hcl:"workerport,optional"`
|
||||
Clients []*ClientDetails `hcl:"client,block"`
|
||||
Routers []*RouterDetails `hcl:"router,block"`
|
||||
Workers []*WorkerDetails `hcl:"worker,block"`
|
||||
Version string
|
||||
File string
|
||||
Agent string
|
||||
CMode bool
|
||||
}
|
||||
|
||||
/*
|
||||
* ClientDetails are details about known clients
|
||||
*/
|
||||
type ClientDetails struct {
|
||||
/*
|
||||
* Client name for display purposes in logs and diagnostics.
|
||||
*/
|
||||
ClientName string `hcl:",label"`
|
||||
|
||||
/*
|
||||
* UUID String for the client node. This is used by the router to
|
||||
* determine which MQ client to send the agent's results to. This
|
||||
* attempts to keep the clients unique globally.
|
||||
*/
|
||||
ClientID string `hcl:"clientid,optional"`
|
||||
}
|
||||
|
||||
/*
|
||||
* RouterDetails is details about known routers
|
||||
*/
|
||||
type RouterDetails struct {
|
||||
/*
|
||||
* Router name for display purposes in logs and diagnostics.
|
||||
*/
|
||||
RouterName string `hcl:",label"`
|
||||
|
||||
/*
|
||||
* UUID String for the router node. This is used by the clients,
|
||||
* routers, and workers to determine which MQ router to send the
|
||||
* agent's requests to. This attempts to keep the routers unique
|
||||
* globally.
|
||||
*/
|
||||
RouterID string `hcl:"routerid,attr"`
|
||||
|
||||
/*
|
||||
* This is the IP address or hostname the router will listen on. The
|
||||
* router will start up a 0MQ service that clients and workers will
|
||||
* connect to.
|
||||
*/
|
||||
RouterAddr string `hcl:"address,attr"`
|
||||
|
||||
/*
|
||||
* These tags will be passed to the router upon connection. The router
|
||||
* will then use these tags to help determine which worker / client to
|
||||
* send the client's requests and results to.
|
||||
*/
|
||||
RouterTags []string `hcl:"tags,optional"`
|
||||
|
||||
/*
|
||||
* G'Agent client will use this port to communicate with the routers.
|
||||
*/
|
||||
ClientPort int `hcl:"clientport,optional"`
|
||||
|
||||
/*
|
||||
* G'Agent router will use this port to communicate with other routers.
|
||||
*/
|
||||
RouterPort int `hcl:"routerport,optional"`
|
||||
|
||||
/*
|
||||
* G'Agent worker will use this port to communicate with the routers.
|
||||
*/
|
||||
WorkerPort int `hcl:"workerport,optional"`
|
||||
}
|
||||
|
||||
/*
|
||||
* WorkerDetails is details about known workers
|
||||
*/
|
||||
type WorkerDetails struct {
|
||||
/*
|
||||
* Router name for display purposes in logs and diagnostics.
|
||||
*/
|
||||
WorkerName string `hcl:",label"`
|
||||
|
||||
/*
|
||||
* UUID String for the worker node. This is used by the router to
|
||||
* determine which MQ client to send agents to. This attempts to keep
|
||||
* the workers unique globally.
|
||||
*/
|
||||
WorkerID string `hcl:"workerid,attr"`
|
||||
|
||||
/*
|
||||
* These tags will be passed to the router upon connection. The router
|
||||
* will then use these tags to help determine which worker / client to
|
||||
* send the agent and it's results to.
|
||||
*/
|
||||
WorkerTags []string `hcl:"tags,optional"`
|
||||
}
|
||||
|
||||
type AgentDetails struct {
|
||||
Status byte `hcl:"status"`
|
||||
Client string `hcl:"client"`
|
||||
Shasum string `hcl:"shasum"`
|
||||
Hints []string `hcl:"hints"`
|
||||
Script []byte `hcl:"script"`
|
||||
Answer []byte `hcl:"answer"`
|
||||
}
|
76
internal/gstructs/gstructs_test.go
Normal file
76
internal/gstructs/gstructs_test.go
Normal file
|
@ -0,0 +1,76 @@
|
|||
package gstructs_test
|
||||
|
||||
import (
|
||||
testing "testing"
|
||||
|
||||
gs "github.com/dragonheim/gagent/internal/gstructs"
|
||||
)
|
||||
|
||||
func TestGagentConfig(t *testing.T) {
|
||||
config := gs.GagentConfig{
|
||||
Name: "test-config",
|
||||
Mode: "client",
|
||||
UUID: "test-uuid",
|
||||
ListenAddr: "127.0.0.1",
|
||||
ChainDBPath: "/tmp/chaindb",
|
||||
MonitorPort: 8888,
|
||||
ClientPort: 1234,
|
||||
RouterPort: 5678,
|
||||
WorkerPort: 9012,
|
||||
Clients: []*gs.ClientDetails{
|
||||
{
|
||||
ClientName: "test-client",
|
||||
ClientID: "client-id",
|
||||
},
|
||||
},
|
||||
Routers: []*gs.RouterDetails{
|
||||
{
|
||||
RouterName: "test-router",
|
||||
RouterID: "router-id",
|
||||
RouterAddr: "192.168.1.1",
|
||||
RouterTags: []string{"tag1", "tag2"},
|
||||
ClientPort: 1234,
|
||||
RouterPort: 5678,
|
||||
WorkerPort: 9012,
|
||||
},
|
||||
},
|
||||
Workers: []*gs.WorkerDetails{
|
||||
{
|
||||
WorkerName: "test-worker",
|
||||
WorkerID: "worker-id",
|
||||
WorkerTags: []string{"tag3", "tag4"},
|
||||
},
|
||||
},
|
||||
Version: "1.0.0",
|
||||
File: "config.hcl",
|
||||
Agent: "agent.gagent",
|
||||
CMode: true,
|
||||
}
|
||||
|
||||
if config.Name != "test-config" {
|
||||
t.Errorf("Expected config name to be 'test-config', got %s", config.Name)
|
||||
}
|
||||
if config.Mode != "client" {
|
||||
t.Errorf("Expected config mode to be 'client', got %s", config.Mode)
|
||||
}
|
||||
// TODO: add more assertions for other config fields
|
||||
}
|
||||
|
||||
func TestAgentDetails(t *testing.T) {
|
||||
agent := gs.AgentDetails{
|
||||
Status: 1,
|
||||
Client: "test-client",
|
||||
Shasum: "123456789abcdef",
|
||||
Hints: []string{"tag1", "tag2", "tag3"},
|
||||
Script: []byte("sample script content"),
|
||||
Answer: []byte("sample answer content"),
|
||||
}
|
||||
|
||||
if agent.Status != 1 {
|
||||
t.Errorf("Expected agent status to be 1, got %d", agent.Status)
|
||||
}
|
||||
if agent.Client != "test-client" {
|
||||
t.Errorf("Expected agent client to be 'test-client', got %s", agent.Client)
|
||||
}
|
||||
// TODO: add more assertions for other agent fields
|
||||
}
|
185
internal/router/router.go
Normal file
185
internal/router/router.go
Normal file
|
@ -0,0 +1,185 @@
|
|||
package router
|
||||
|
||||
import (
|
||||
fmt "fmt"
|
||||
log "log"
|
||||
http "net/http"
|
||||
strconv "strconv"
|
||||
sync "sync"
|
||||
|
||||
gcdb "github.com/dragonheim/gagent/internal/chaindb"
|
||||
gstructs "github.com/dragonheim/gagent/internal/gstructs"
|
||||
|
||||
zmq "github.com/pebbe/zmq4"
|
||||
)
|
||||
|
||||
var (
|
||||
db gcdb.GagentDb
|
||||
)
|
||||
|
||||
/*
|
||||
* The 'router' processes routing requests from the agent. The router does
|
||||
* not handle any of the agent activities beyond processing the agent's
|
||||
* list of tags and passing the agent and it's storage to either a member
|
||||
* or client node. Tags are used by the agent to give hints as to where
|
||||
* it should be routed.
|
||||
* Main is the entrypoint for the router
|
||||
*/
|
||||
func Main(wg *sync.WaitGroup, config gstructs.GagentConfig) {
|
||||
log.Printf("[INFO] Starting router\n")
|
||||
defer wg.Done()
|
||||
|
||||
http.HandleFunc("/hello", AnswerClient)
|
||||
clientSock, _ := zmq.NewSocket(zmq.ROUTER)
|
||||
defer clientSock.Close()
|
||||
|
||||
workerSock, _ := zmq.NewSocket(zmq.DEALER)
|
||||
defer workerSock.Close()
|
||||
|
||||
chain := gcdb.NewGagentDb()
|
||||
log.Println("[DEBUG] Loading chaindb ")
|
||||
err := chain.LoadHCL(config.ChainDBPath)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Error loading chaindb: %s", err)
|
||||
}
|
||||
|
||||
workerListener := "tcp://" + config.ListenAddr + ":" + strconv.Itoa(config.WorkerPort)
|
||||
_ = workerSock.Bind(workerListener)
|
||||
|
||||
workers := make([]string, 0)
|
||||
|
||||
poller1 := zmq.NewPoller()
|
||||
poller1.Add(workerSock, zmq.POLLIN)
|
||||
|
||||
poller2 := zmq.NewPoller()
|
||||
poller2.Add(workerSock, zmq.POLLIN)
|
||||
|
||||
wg.Add(1)
|
||||
go createClientListener(wg, config)
|
||||
|
||||
LOOP:
|
||||
for {
|
||||
/*
|
||||
* Poll frontend only if we have available workers
|
||||
*/
|
||||
var sockets []zmq.Polled
|
||||
var err error
|
||||
if len(workers) > 0 {
|
||||
sockets, err = poller2.Poll(-1)
|
||||
} else {
|
||||
sockets, err = poller1.Poll(-1)
|
||||
}
|
||||
if err != nil {
|
||||
/*
|
||||
* Interrupt
|
||||
*/
|
||||
break
|
||||
}
|
||||
for _, socket := range sockets {
|
||||
switch s := socket.Socket; s {
|
||||
case workerSock:
|
||||
/*
|
||||
* Handle worker activity on backend
|
||||
* Use worker identity for load-balancing
|
||||
*/
|
||||
msg, err := s.RecvMessage(0)
|
||||
if err != nil {
|
||||
/*
|
||||
* Interrupt
|
||||
*/
|
||||
break LOOP
|
||||
}
|
||||
var identity string
|
||||
identity, msg = unwrap(msg)
|
||||
log.Printf("[DEBUG] Worker message received: %s", msg)
|
||||
workers = append(workers, identity)
|
||||
|
||||
case clientSock:
|
||||
wg.Add(1)
|
||||
go createClientListener(wg, config)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Create listener for client requests
|
||||
*/
|
||||
func createClientListener(wg *sync.WaitGroup, config gstructs.GagentConfig) error {
|
||||
defer wg.Done()
|
||||
|
||||
clientSock, err := zmq.NewSocket(zmq.ROUTER)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Error creating client socket: %s", err)
|
||||
return err
|
||||
}
|
||||
defer clientSock.Close()
|
||||
|
||||
clientListener := "tcp://" + config.ListenAddr + ":" + strconv.Itoa(config.ClientPort)
|
||||
log.Printf("[DEBUG] Binding to: %s", clientListener)
|
||||
err = clientSock.Bind(clientListener)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Error binding client socket: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
for {
|
||||
msg, err := clientSock.RecvMessage(0)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
log.Printf("[DEBUG] Client message received: %s", msg)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func unwrap(msg []string) (head string, tail []string) {
|
||||
head = msg[0]
|
||||
if len(msg) > 1 && msg[1] == "" {
|
||||
tail = msg[2:]
|
||||
} else {
|
||||
tail = msg[1:]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func AnswerClient(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
* Common code for all requests can go here...
|
||||
*/
|
||||
switch r.Method {
|
||||
/*
|
||||
* Handle GET requests
|
||||
*/
|
||||
case http.MethodGet:
|
||||
log.Println("[DEBUG] GET method received")
|
||||
fmt.Fprintf(w, "%v\n", r)
|
||||
|
||||
/*
|
||||
* Handle POST requests
|
||||
*/
|
||||
case http.MethodPost:
|
||||
log.Println("[DEBUG] POST method received")
|
||||
fmt.Fprintf(w, "%v\n", r)
|
||||
|
||||
/*
|
||||
* Handle PUT requests
|
||||
*/
|
||||
case http.MethodOptions:
|
||||
log.Println("[DEBUG] PUT method received")
|
||||
w.Header().Set("Allow", "GET, POST, OPTIONS")
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
|
||||
/*
|
||||
* Handle everything else
|
||||
*/
|
||||
default:
|
||||
w.Header().Set("Allow", "GET, POST, OPTIONS")
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
72
internal/router/router_test.go
Normal file
72
internal/router/router_test.go
Normal file
|
@ -0,0 +1,72 @@
|
|||
package router_test
|
||||
|
||||
import (
|
||||
http "net/http"
|
||||
httptest "net/http/httptest"
|
||||
sync "sync"
|
||||
testing "testing"
|
||||
time "time"
|
||||
|
||||
gs "github.com/dragonheim/gagent/internal/gstructs"
|
||||
router "github.com/dragonheim/gagent/internal/router"
|
||||
)
|
||||
|
||||
func TestRouterMain(t *testing.T) {
|
||||
config := gs.GagentConfig{
|
||||
Name: "test-config",
|
||||
Mode: "router",
|
||||
UUID: "test-uuid",
|
||||
ListenAddr: "127.0.0.1",
|
||||
ClientPort: 1234,
|
||||
RouterPort: 5678,
|
||||
WorkerPort: 9012,
|
||||
ChainDBPath: "test-chaindb-path",
|
||||
}
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(1)
|
||||
|
||||
go router.Main(wg, config)
|
||||
|
||||
// Allow router to start before sending HTTP requests
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
|
||||
// Test GET request
|
||||
resp := makeRequest(t, "GET", "http://localhost:1234/hello")
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("Expected status code %d, got %d", http.StatusOK, resp.StatusCode)
|
||||
}
|
||||
|
||||
// Test POST request
|
||||
resp = makeRequest(t, "POST", "http://localhost:1234/hello")
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("Expected status code %d, got %d", http.StatusOK, resp.StatusCode)
|
||||
}
|
||||
|
||||
// Test OPTIONS request
|
||||
resp = makeRequest(t, "OPTIONS", "http://localhost:1234/hello")
|
||||
if resp.StatusCode != http.StatusNoContent {
|
||||
t.Errorf("Expected status code %d, got %d", http.StatusNoContent, resp.StatusCode)
|
||||
}
|
||||
|
||||
// Test unsupported method
|
||||
resp = makeRequest(t, "PUT", "http://localhost:1234/hello")
|
||||
if resp.StatusCode != http.StatusMethodNotAllowed {
|
||||
t.Errorf("Expected status code %d, got %d", http.StatusMethodNotAllowed, resp.StatusCode)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func makeRequest(t *testing.T, method, url string) *http.Response {
|
||||
req, err := http.NewRequest(method, url, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating request: %v", err)
|
||||
}
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
handler := http.HandlerFunc(router.AnswerClient)
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
return rr.Result()
|
||||
}
|
61
internal/setup/setup.go
Normal file
61
internal/setup/setup.go
Normal file
|
@ -0,0 +1,61 @@
|
|||
package setup
|
||||
|
||||
import (
|
||||
log "log"
|
||||
sync "sync"
|
||||
|
||||
cty "github.com/zclconf/go-cty/cty"
|
||||
|
||||
gs "github.com/dragonheim/gagent/internal/gstructs"
|
||||
|
||||
hclwrite "github.com/hashicorp/hcl/v2/hclwrite"
|
||||
)
|
||||
|
||||
/*
|
||||
* Main is the entrypoint for the setup process
|
||||
*/
|
||||
func Main(wg *sync.WaitGroup, config gs.GagentConfig) {
|
||||
log.Printf("[INFO] Starting setup\n")
|
||||
defer wg.Done()
|
||||
|
||||
f := hclwrite.NewEmptyFile()
|
||||
rootBody := f.Body()
|
||||
rootBody.SetAttributeValue("name", cty.StringVal(config.Name))
|
||||
rootBody.SetAttributeValue("mode", cty.StringVal(config.Mode))
|
||||
rootBody.SetAttributeValue("uuid", cty.StringVal(config.UUID))
|
||||
rootBody.SetAttributeValue("listenaddr", cty.StringVal("0.0.0.0"))
|
||||
rootBody.SetAttributeValue("clientport", cty.NumberIntVal(int64(config.ClientPort)))
|
||||
rootBody.SetAttributeValue("routerport", cty.NumberIntVal(int64(config.RouterPort)))
|
||||
rootBody.SetAttributeValue("workerport", cty.NumberIntVal(int64(config.WorkerPort)))
|
||||
rootBody.AppendNewline()
|
||||
|
||||
clientBlock1 := rootBody.AppendNewBlock("client", []string{config.Name})
|
||||
clientBody1 := clientBlock1.Body()
|
||||
/*
|
||||
* clientBody1.AppendUnstructuredTokens(
|
||||
* hclwrite.TokensForTraversal(hcl.Traversal{
|
||||
* hcl.TraverseRoot{
|
||||
* Name: hcl.CommentGenerator("comment"),
|
||||
* },
|
||||
* },
|
||||
* ))
|
||||
*/
|
||||
clientBody1.SetAttributeValue("clientid", cty.StringVal(config.UUID))
|
||||
rootBody.AppendNewline()
|
||||
|
||||
routerBlock1 := rootBody.AppendNewBlock("router", []string{config.Name})
|
||||
routerBody1 := routerBlock1.Body()
|
||||
routerBody1.SetAttributeValue("routerid", cty.StringVal(config.UUID))
|
||||
routerBody1.SetAttributeValue("address", cty.StringVal("127.0.0.1"))
|
||||
routerBody1.SetAttributeValue("clientport", cty.NumberIntVal(int64(config.ClientPort)))
|
||||
routerBody1.SetAttributeValue("routerport", cty.NumberIntVal(int64(config.RouterPort)))
|
||||
routerBody1.SetAttributeValue("workerport", cty.NumberIntVal(int64(config.WorkerPort)))
|
||||
rootBody.AppendNewline()
|
||||
|
||||
workerBlock1 := rootBody.AppendNewBlock("worker", []string{config.Name})
|
||||
workerBody1 := workerBlock1.Body()
|
||||
workerBody1.SetAttributeValue("workerid", cty.StringVal(config.UUID))
|
||||
rootBody.AppendNewline()
|
||||
|
||||
log.Printf("[DEBUG] Configuration file created;\n\n%s\n", f.Bytes())
|
||||
}
|
57
internal/setup/setup_test.go
Normal file
57
internal/setup/setup_test.go
Normal file
|
@ -0,0 +1,57 @@
|
|||
package setup_test
|
||||
|
||||
import (
|
||||
bytes "bytes"
|
||||
io "io"
|
||||
log "log"
|
||||
os "os"
|
||||
testing "testing"
|
||||
|
||||
strings "strings"
|
||||
sync "sync"
|
||||
|
||||
gs "github.com/dragonheim/gagent/internal/gstructs"
|
||||
setup "github.com/dragonheim/gagent/internal/setup"
|
||||
)
|
||||
|
||||
func TestSetupMain(t *testing.T) {
|
||||
config := gs.GagentConfig{
|
||||
Name: "test-config",
|
||||
Mode: "client",
|
||||
UUID: "test-uuid",
|
||||
ListenAddr: "127.0.0.1",
|
||||
ClientPort: 1234,
|
||||
RouterPort: 5678,
|
||||
WorkerPort: 9012,
|
||||
}
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(1)
|
||||
|
||||
capturedOutput := captureOutput(func() {
|
||||
setup.Main(wg, config)
|
||||
})
|
||||
|
||||
expectedOutput := `Configuration file created`
|
||||
if !strings.Contains(capturedOutput, expectedOutput) {
|
||||
t.Errorf("Expected output to contain '%s', got '%s'", expectedOutput, capturedOutput)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func captureOutput(f func()) string {
|
||||
original := log.Writer()
|
||||
r, w, _ := os.Pipe()
|
||||
log.SetOutput(w)
|
||||
|
||||
f()
|
||||
|
||||
w.Close()
|
||||
log.SetOutput(original)
|
||||
|
||||
var buf bytes.Buffer
|
||||
io.Copy(&buf, r)
|
||||
|
||||
return buf.String()
|
||||
}
|
71
internal/worker/worker.go
Normal file
71
internal/worker/worker.go
Normal file
|
@ -0,0 +1,71 @@
|
|||
package worker
|
||||
|
||||
import (
|
||||
log "log"
|
||||
strconv "strconv"
|
||||
sync "sync"
|
||||
|
||||
gstructs "github.com/dragonheim/gagent/internal/gstructs"
|
||||
|
||||
/*
|
||||
* picol "github.com/dragonheim/gagent/pkg/picol"
|
||||
*/
|
||||
|
||||
prometheus "github.com/prometheus/client_golang/prometheus"
|
||||
promauto "github.com/prometheus/client_golang/prometheus/promauto"
|
||||
|
||||
zmq "github.com/pebbe/zmq4"
|
||||
)
|
||||
|
||||
var (
|
||||
opsProcessed = promauto.NewCounter(prometheus.CounterOpts{
|
||||
Name: "agent_requests_collected",
|
||||
})
|
||||
)
|
||||
|
||||
/*
|
||||
* The "worker" processes the agent code. The worker nodes do not know
|
||||
* anything about the network structure. Instead they know only to which
|
||||
* router(s) they are connected. The worker will execute the agent code and
|
||||
* pass the agent and it's results to a router.
|
||||
* Main is the entrypoint for the worker process
|
||||
*/
|
||||
func Main(wg *sync.WaitGroup, config gstructs.GagentConfig) {
|
||||
log.Printf("[INFO] Starting worker\n")
|
||||
defer wg.Done()
|
||||
|
||||
for key := range config.Routers {
|
||||
rport := config.WorkerPort
|
||||
if config.Routers[key].WorkerPort != 0 {
|
||||
rport = config.Routers[key].WorkerPort
|
||||
}
|
||||
|
||||
/*
|
||||
* Generate connect string for this router.
|
||||
*/
|
||||
connectString := "tcp://" + config.Routers[key].RouterAddr + ":" + strconv.Itoa(rport)
|
||||
|
||||
wg.Add(1)
|
||||
go getAgent(wg, config.UUID, connectString)
|
||||
}
|
||||
/*
|
||||
* workerListener := "tcp://" + config.ListenAddr + ":" + strconv.Itoa(config.WorkerPort)
|
||||
*/
|
||||
|
||||
}
|
||||
|
||||
func getAgent(wg *sync.WaitGroup, uuid string, connectString string) {
|
||||
log.Printf("[DEBUG] Attempting to connect to %s\n", connectString)
|
||||
defer wg.Done()
|
||||
|
||||
subscriber, _ := zmq.NewSocket(zmq.REP)
|
||||
defer subscriber.Close()
|
||||
|
||||
_ = subscriber.Connect(connectString)
|
||||
|
||||
msg, err := subscriber.Recv(0)
|
||||
if err != nil {
|
||||
log.Printf("[DEBUG] Received error: %v", err)
|
||||
}
|
||||
log.Printf("[DEBUG] Received message: %v", msg[0])
|
||||
}
|
66
internal/worker/worker_test.go
Normal file
66
internal/worker/worker_test.go
Normal file
|
@ -0,0 +1,66 @@
|
|||
package worker_test
|
||||
|
||||
import (
|
||||
bytes "bytes"
|
||||
io "io"
|
||||
log "log"
|
||||
os "os"
|
||||
testing "testing"
|
||||
|
||||
strings "strings"
|
||||
sync "sync"
|
||||
|
||||
gs "github.com/dragonheim/gagent/internal/gstructs"
|
||||
|
||||
worker "github.com/dragonheim/gagent/internal/worker"
|
||||
)
|
||||
|
||||
func TestWorkerMain(t *testing.T) {
|
||||
config := gs.GagentConfig{
|
||||
Name: "test-config",
|
||||
Mode: "worker",
|
||||
UUID: "test-uuid",
|
||||
ListenAddr: "127.0.0.1",
|
||||
ClientPort: 1234,
|
||||
RouterPort: 5678,
|
||||
WorkerPort: 9012,
|
||||
Routers: []*gs.RouterDetails{
|
||||
{
|
||||
RouterName: "test-router",
|
||||
RouterID: "test-router-id",
|
||||
RouterAddr: "127.0.0.1",
|
||||
WorkerPort: 9012,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(1)
|
||||
|
||||
capturedOutput := captureOutput(func() {
|
||||
worker.Main(wg, config)
|
||||
})
|
||||
|
||||
expectedOutput := `Starting worker`
|
||||
if !strings.Contains(capturedOutput, expectedOutput) {
|
||||
t.Errorf("Expected output to contain '%s', got '%s'", expectedOutput, capturedOutput)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func captureOutput(f func()) string {
|
||||
original := log.Writer()
|
||||
r, w, _ := os.Pipe()
|
||||
log.SetOutput(w)
|
||||
|
||||
f()
|
||||
|
||||
w.Close()
|
||||
log.SetOutput(original)
|
||||
|
||||
var buf bytes.Buffer
|
||||
io.Copy(&buf, r)
|
||||
|
||||
return buf.String()
|
||||
}
|
|
@ -17,4 +17,4 @@ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
272
pkg/picol/commands.go
Normal file
272
pkg/picol/commands.go
Normal file
|
@ -0,0 +1,272 @@
|
|||
package picol
|
||||
|
||||
import (
|
||||
errors "errors"
|
||||
fmt "fmt"
|
||||
strconv "strconv"
|
||||
strings "strings"
|
||||
)
|
||||
|
||||
/*
|
||||
* incorrectArgCountError returns an error message indicating the incorrect
|
||||
* number of arguments provided for a given function. It takes an interpreter
|
||||
* instance 'i', the function name 'name', and a slice of argument values 'argv'.
|
||||
*/
|
||||
func incorrectArgCountError(i *Interpreter, name string, argv []string) error {
|
||||
return fmt.Errorf("wrong number of args for %s %s", name, argv)
|
||||
}
|
||||
|
||||
/*
|
||||
* NeedleInHaystack returns true if the string is in a slice
|
||||
*/
|
||||
func NeedleInHaystack(needle string, haystack []string) bool {
|
||||
for _, haystackMember := range haystack {
|
||||
if haystackMember == needle {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/*
|
||||
* CommandMath is the math command for TCL
|
||||
*/
|
||||
func CommandMath(i *Interpreter, argv []string, pd interface{}) (string, error) {
|
||||
if len(argv) != 3 {
|
||||
return "", incorrectArgCountError(i, argv[0], argv)
|
||||
}
|
||||
a, _ := strconv.Atoi(argv[1])
|
||||
b, _ := strconv.Atoi(argv[2])
|
||||
var c int
|
||||
switch {
|
||||
case argv[0] == "+":
|
||||
c = a + b
|
||||
case argv[0] == "-":
|
||||
c = a - b
|
||||
case argv[0] == "*":
|
||||
c = a * b
|
||||
case argv[0] == "/":
|
||||
c = a / b
|
||||
case argv[0] == ">":
|
||||
if a > b {
|
||||
c = 1
|
||||
}
|
||||
case argv[0] == ">=":
|
||||
if a >= b {
|
||||
c = 1
|
||||
}
|
||||
case argv[0] == "<":
|
||||
if a < b {
|
||||
c = 1
|
||||
}
|
||||
case argv[0] == "<=":
|
||||
if a <= b {
|
||||
c = 1
|
||||
}
|
||||
case argv[0] == "==":
|
||||
if a == b {
|
||||
c = 1
|
||||
}
|
||||
case argv[0] == "!=":
|
||||
if a != b {
|
||||
c = 1
|
||||
}
|
||||
default:
|
||||
return "0", errors.New("invalid operator " + argv[0])
|
||||
}
|
||||
return fmt.Sprintf("%d", c), nil
|
||||
}
|
||||
|
||||
/*
|
||||
* CommandSet is the set command for TCL
|
||||
*/
|
||||
func CommandSet(i *Interpreter, argv []string, pd interface{}) (string, error) {
|
||||
if len(argv) != 3 {
|
||||
return "", incorrectArgCountError(i, argv[0], argv)
|
||||
}
|
||||
i.SetVariable(argv[1], argv[2])
|
||||
return argv[2], nil
|
||||
}
|
||||
|
||||
/*
|
||||
* CommandUnset is the unset command for TCL
|
||||
*/
|
||||
func CommandUnset(i *Interpreter, argv []string, pd interface{}) (string, error) {
|
||||
if len(argv) != 2 {
|
||||
return "", incorrectArgCountError(i, argv[0], argv)
|
||||
}
|
||||
i.UnsetVariable(argv[1])
|
||||
return "", nil
|
||||
}
|
||||
|
||||
/*
|
||||
* CommandIf is the if command for TCL
|
||||
*/
|
||||
func CommandIf(i *Interpreter, argv []string, pd interface{}) (string, error) {
|
||||
if len(argv) != 3 && len(argv) != 5 {
|
||||
return "", incorrectArgCountError(i, argv[0], argv)
|
||||
}
|
||||
|
||||
result, err := i.Eval(argv[1])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if r, _ := strconv.Atoi(result); r != 0 {
|
||||
return i.Eval(argv[2])
|
||||
} else if len(argv) == 5 {
|
||||
return i.Eval(argv[4])
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
/*
|
||||
* CommandWhile is the while command for TCL
|
||||
*/
|
||||
func CommandWhile(i *Interpreter, argv []string, pd interface{}) (string, error) {
|
||||
if len(argv) != 3 {
|
||||
return "", incorrectArgCountError(i, argv[0], argv)
|
||||
}
|
||||
|
||||
for {
|
||||
result, err := i.Eval(argv[1])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if r, _ := strconv.Atoi(result); r != 0 {
|
||||
result, err := i.Eval(argv[2])
|
||||
switch err {
|
||||
case ErrContinue, nil:
|
||||
/*
|
||||
* pass
|
||||
*/
|
||||
case ErrBreak:
|
||||
return result, nil
|
||||
default:
|
||||
return result, err
|
||||
}
|
||||
} else {
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* CommandRetCodes is a function to get the return codes for TCL
|
||||
*/
|
||||
func CommandRetCodes(i *Interpreter, argv []string, pd interface{}) (string, error) {
|
||||
if len(argv) != 1 {
|
||||
return "", incorrectArgCountError(i, argv[0], argv)
|
||||
}
|
||||
switch argv[0] {
|
||||
case "break":
|
||||
return "", ErrBreak
|
||||
case "continue":
|
||||
return "", ErrContinue
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
/*
|
||||
* CommandCallProc is a function to call proc commands for TCL
|
||||
*/
|
||||
func CommandCallProc(i *Interpreter, argv []string, pd interface{}) (string, error) {
|
||||
var x []string
|
||||
|
||||
if pd, ok := pd.([]string); ok {
|
||||
x = pd
|
||||
} else {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
i.callframe = &CallFrame{vars: make(map[string]Variable), parent: i.callframe}
|
||||
defer func() { i.callframe = i.callframe.parent }() // remove the called proc callframe
|
||||
|
||||
arity := 0
|
||||
for _, arg := range strings.Split(x[0], " ") {
|
||||
if len(arg) == 0 {
|
||||
continue
|
||||
}
|
||||
arity++
|
||||
i.SetVariable(arg, argv[arity])
|
||||
}
|
||||
|
||||
if arity != len(argv)-1 {
|
||||
return "", fmt.Errorf("proc '%s' called with wrong arg num", argv[0])
|
||||
}
|
||||
|
||||
body := x[1]
|
||||
result, err := i.Eval(body)
|
||||
if err == ErrReturn {
|
||||
err = nil
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
/*
|
||||
* CommandProc is a function to register proc commands for TCL
|
||||
*/
|
||||
func CommandProc(i *Interpreter, argv []string, pd interface{}) (string, error) {
|
||||
if len(argv) != 4 {
|
||||
return "", incorrectArgCountError(i, argv[0], argv)
|
||||
}
|
||||
return "", i.RegisterCommand(argv[1], CommandCallProc, []string{argv[2], argv[3]})
|
||||
}
|
||||
|
||||
/*
|
||||
* CommandReturn is a function to register return codes for commands for TCL
|
||||
*/
|
||||
func CommandReturn(i *Interpreter, argv []string, pd interface{}) (string, error) {
|
||||
if len(argv) != 1 && len(argv) != 2 {
|
||||
return "", incorrectArgCountError(i, argv[0], argv)
|
||||
}
|
||||
var r string
|
||||
if len(argv) == 2 {
|
||||
r = argv[1]
|
||||
}
|
||||
return r, ErrReturn
|
||||
}
|
||||
|
||||
/*
|
||||
* CommandError is a function to return error codes for commands for TCL
|
||||
*/
|
||||
func CommandError(i *Interpreter, argv []string, pd interface{}) (string, error) {
|
||||
if len(argv) != 1 && len(argv) != 2 {
|
||||
return "", incorrectArgCountError(i, argv[0], argv)
|
||||
}
|
||||
return "", fmt.Errorf(argv[1])
|
||||
}
|
||||
|
||||
/*
|
||||
* CommandPuts is a function to print strings for TCL
|
||||
*/
|
||||
func CommandPuts(i *Interpreter, argv []string, pd interface{}) (string, error) {
|
||||
if len(argv) != 2 {
|
||||
return "", fmt.Errorf("wrong number of args for %s %s", argv[0], argv)
|
||||
}
|
||||
fmt.Println(argv[1])
|
||||
return "", nil
|
||||
}
|
||||
|
||||
/*
|
||||
* RegisterCoreCommands is a callable to register TCL commands.
|
||||
*/
|
||||
func (i *Interpreter) RegisterCoreCommands() error {
|
||||
name := [...]string{"+", "-", "*", "/", ">", ">=", "<", "<=", "==", "!="}
|
||||
for _, n := range name {
|
||||
_ = i.RegisterCommand(n, CommandMath, nil)
|
||||
}
|
||||
_ = i.RegisterCommand("set", CommandSet, nil)
|
||||
_ = i.RegisterCommand("unset", CommandUnset, nil)
|
||||
_ = i.RegisterCommand("if", CommandIf, nil)
|
||||
_ = i.RegisterCommand("while", CommandWhile, nil)
|
||||
_ = i.RegisterCommand("break", CommandRetCodes, nil)
|
||||
_ = i.RegisterCommand("continue", CommandRetCodes, nil)
|
||||
_ = i.RegisterCommand("proc", CommandProc, nil)
|
||||
_ = i.RegisterCommand("return", CommandReturn, nil)
|
||||
_ = i.RegisterCommand("error", CommandError, nil)
|
||||
_ = i.RegisterCommand("puts", CommandPuts, nil)
|
||||
|
||||
return nil
|
||||
}
|
300
pkg/picol/commands_test.go
Normal file
300
pkg/picol/commands_test.go
Normal file
|
@ -0,0 +1,300 @@
|
|||
package picol_test
|
||||
|
||||
import (
|
||||
testing "testing"
|
||||
|
||||
picol "github.com/dragonheim/gagent/pkg/picol"
|
||||
)
|
||||
|
||||
func Test_NeedleInHaystack(t *testing.T) {
|
||||
var haystack = []string{"a", "b", "c"}
|
||||
var needle = "a"
|
||||
if !picol.NeedleInHaystack(needle, haystack) {
|
||||
t.Errorf("%s not in %s", needle, haystack)
|
||||
}
|
||||
|
||||
needle = "j"
|
||||
if picol.NeedleInHaystack(needle, haystack) {
|
||||
t.Errorf("%s in %s", needle, haystack)
|
||||
}
|
||||
|
||||
needle = "ab"
|
||||
if picol.NeedleInHaystack(needle, haystack) {
|
||||
t.Errorf("%s in %s", needle, haystack)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_CommandMath(t *testing.T) {
|
||||
// You can add more test cases for various operations and edge cases
|
||||
testCases := []struct {
|
||||
argv []string
|
||||
result string
|
||||
err error
|
||||
}{
|
||||
{[]string{"+", "2", "3"}, "5", nil},
|
||||
{[]string{"-", "8", "3"}, "5", nil},
|
||||
{[]string{"*", "2", "3"}, "6", nil},
|
||||
{[]string{"/", "6", "3"}, "2", nil},
|
||||
{[]string{">", "4", "2"}, "1", nil},
|
||||
}
|
||||
|
||||
i := picol.NewInterpreter()
|
||||
for _, tc := range testCases {
|
||||
result, err := picol.CommandMath(i, tc.argv, nil)
|
||||
if result != tc.result || (err != nil && tc.err != nil && err.Error() != tc.err.Error()) {
|
||||
t.Errorf("CommandMath(%v) = (%v, %v); expected (%v, %v)", tc.argv, result, err, tc.result, tc.err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_CommandSet(t *testing.T) {
|
||||
testCases := []struct {
|
||||
argv []string
|
||||
result string
|
||||
err error
|
||||
}{
|
||||
{[]string{"set", "x", "42"}, "42", nil},
|
||||
{[]string{"set", "y", "abc"}, "abc", nil},
|
||||
}
|
||||
|
||||
i := picol.NewInterpreter()
|
||||
for _, tc := range testCases {
|
||||
result, err := picol.CommandSet(i, tc.argv, nil)
|
||||
if result != tc.result || (err != nil && tc.err != nil && err.Error() != tc.err.Error()) {
|
||||
t.Errorf("CommandSet(%v) = (%v, %v); expected (%v, %v)", tc.argv, result, err, tc.result, tc.err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_CommandUnset(t *testing.T) {
|
||||
testCases := []struct {
|
||||
argv []string
|
||||
result string
|
||||
err error
|
||||
}{
|
||||
{[]string{"unset", "x"}, "", nil},
|
||||
{[]string{"unset", "y"}, "", nil},
|
||||
}
|
||||
|
||||
i := picol.NewInterpreter()
|
||||
i.SetVariable("x", "42")
|
||||
i.SetVariable("y", "abc")
|
||||
|
||||
for _, tc := range testCases {
|
||||
result, err := picol.CommandUnset(i, tc.argv, nil)
|
||||
if result != tc.result || (err != nil && tc.err != nil && err.Error() != tc.err.Error()) {
|
||||
t.Errorf("CommandUnset(%v) = (%v, %v); expected (%v, %v)", tc.argv, result, err, tc.result, tc.err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_CommandIf(t *testing.T) {
|
||||
testCases := []struct {
|
||||
argv []string
|
||||
result string
|
||||
err error
|
||||
}{
|
||||
{[]string{"unset", "x"}, "", nil},
|
||||
{[]string{"unset", "y"}, "", nil},
|
||||
}
|
||||
|
||||
i := picol.NewInterpreter()
|
||||
i.SetVariable("x", "42")
|
||||
i.SetVariable("y", "abc")
|
||||
|
||||
for _, tc := range testCases {
|
||||
result, err := picol.CommandUnset(i, tc.argv, nil)
|
||||
if result != tc.result || (err != nil && tc.err != nil && err.Error() != tc.err.Error()) {
|
||||
t.Errorf("CommandUnset(%v) = (%v, %v); expected (%v, %v)", tc.argv, result, err, tc.result, tc.err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_CommandWhile(t *testing.T) {
|
||||
i := picol.NewInterpreter()
|
||||
|
||||
// Test simple while loop
|
||||
err := i.RegisterCoreCommands()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
script := `
|
||||
set i 0
|
||||
set result 0
|
||||
while { < $i 5 } {
|
||||
set result [+ $result $i]
|
||||
set i [+ $i 1]
|
||||
}
|
||||
`
|
||||
|
||||
res, err := i.Eval(script)
|
||||
if err != nil {
|
||||
t.Fatalf("Error during while loop evaluation: %s", err)
|
||||
}
|
||||
|
||||
expectedResult := "10"
|
||||
if res != expectedResult {
|
||||
t.Errorf("Expected %s, got %s", expectedResult, res)
|
||||
}
|
||||
|
||||
// Test nested while loops
|
||||
script = `
|
||||
set i 0
|
||||
set j 0
|
||||
set result 0
|
||||
while { < $i 3 } {
|
||||
set j 0
|
||||
while { < $j 3 } {
|
||||
set result [+ $result $j]
|
||||
set j [+ $j 1]
|
||||
}
|
||||
set i [+ $i 1]
|
||||
}
|
||||
`
|
||||
|
||||
res, err = i.Eval(script)
|
||||
if err != nil {
|
||||
t.Fatalf("Error during nested while loop evaluation: %s", err)
|
||||
}
|
||||
|
||||
expectedResult = "9"
|
||||
if res != expectedResult {
|
||||
t.Errorf("Expected %s, got %s", expectedResult, res)
|
||||
}
|
||||
}
|
||||
|
||||
// You can also add test functions for other commands like CommandProc, CommandReturn, etc.
|
||||
func Test_CommandRetCodes(t *testing.T) {
|
||||
i := picol.NewInterpreter()
|
||||
|
||||
err := i.RegisterCoreCommands()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Test break
|
||||
script := `
|
||||
while { 1 } {
|
||||
break
|
||||
}
|
||||
`
|
||||
_, err = i.Eval(script)
|
||||
if err != nil {
|
||||
t.Fatalf("Error during break evaluation: %s", err)
|
||||
}
|
||||
|
||||
// Test continue
|
||||
script = `
|
||||
set i 0
|
||||
set result 0
|
||||
while { < $i 5 } {
|
||||
if { == $i 2 } {
|
||||
continue
|
||||
}
|
||||
set result [+ $result $i]
|
||||
set i [+ $i 1]
|
||||
}
|
||||
`
|
||||
expectedResult := "7"
|
||||
res, err := i.Eval(script)
|
||||
if err != nil {
|
||||
t.Fatalf("Error during continue evaluation: %s", err)
|
||||
}
|
||||
if res != expectedResult {
|
||||
t.Errorf("Expected %s, got %s", expectedResult, res)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_CommandProc(t *testing.T) {
|
||||
i := picol.NewInterpreter()
|
||||
|
||||
err := i.RegisterCoreCommands()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
script := `
|
||||
proc sum {a b} {
|
||||
return [+ $a $b]
|
||||
}
|
||||
set res [sum 3 4]
|
||||
`
|
||||
expectedResult := "7"
|
||||
res, err := i.Eval(script)
|
||||
if err != nil {
|
||||
t.Fatalf("Error during proc evaluation: %s", err)
|
||||
}
|
||||
if res != expectedResult {
|
||||
t.Errorf("Expected %s, got %s", expectedResult, res)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_CommandReturn(t *testing.T) {
|
||||
i := picol.NewInterpreter()
|
||||
|
||||
err := i.RegisterCoreCommands()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
script := `
|
||||
proc testReturn {val} {
|
||||
return $val
|
||||
}
|
||||
set res [testReturn 42]
|
||||
`
|
||||
expectedResult := "42"
|
||||
res, err := i.Eval(script)
|
||||
if err != nil {
|
||||
t.Fatalf("Error during return evaluation: %s", err)
|
||||
}
|
||||
if res != expectedResult {
|
||||
t.Errorf("Expected %s, got %s", expectedResult, res)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_CommandError(t *testing.T) {
|
||||
i := picol.NewInterpreter()
|
||||
|
||||
err := i.RegisterCoreCommands()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
script := `
|
||||
error "An error occurred"
|
||||
`
|
||||
_, err = i.Eval(script)
|
||||
if err == nil || err.Error() != "An error occurred" {
|
||||
t.Fatalf("Error not raised or incorrect error message: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_CommandPuts(t *testing.T) {
|
||||
i := picol.NewInterpreter()
|
||||
|
||||
err := i.RegisterCoreCommands()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// The following test checks if the "puts" command runs without any error.
|
||||
// However, it doesn't check the printed output since it's not straightforward to capture stdout in tests.
|
||||
script := `
|
||||
puts "Hello, world!"
|
||||
`
|
||||
_, err = i.Eval(script)
|
||||
if err != nil {
|
||||
t.Fatalf("Error during puts evaluation: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_RegisterCoreCommands(t *testing.T) {
|
||||
i := picol.NewInterpreter()
|
||||
|
||||
err := i.RegisterCoreCommands()
|
||||
if err != nil {
|
||||
t.Fatalf("Error during core command registration: %s", err)
|
||||
}
|
||||
}
|
|
@ -5,16 +5,18 @@ import (
|
|||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// Define parser token types
|
||||
const (
|
||||
ptESC = iota
|
||||
ptSTR
|
||||
ptCMD
|
||||
ptVAR
|
||||
ptSEP
|
||||
ptEOL
|
||||
ptEOF
|
||||
ParserTokenESC = iota
|
||||
ParserTokenSTR
|
||||
ParserTokenCMD
|
||||
ParserTokenVAR
|
||||
ParserTokenSEP
|
||||
ParserTokenEOL
|
||||
ParserTokenEOF
|
||||
)
|
||||
|
||||
// parserStruct represents the parser state
|
||||
type parserStruct struct {
|
||||
text string
|
||||
p, start, end, ln int
|
||||
|
@ -22,26 +24,31 @@ type parserStruct struct {
|
|||
Type int
|
||||
}
|
||||
|
||||
func initParser(text string) *parserStruct {
|
||||
return &parserStruct{text, 0, 0, 0, len(text), 0, ptEOL}
|
||||
// initParser initializes a new parserStruct instance
|
||||
func InitParser(text string) *parserStruct {
|
||||
return &parserStruct{text: text, ln: len(text), Type: ParserTokenEOL}
|
||||
}
|
||||
|
||||
// next advances the parser position by one rune
|
||||
func (p *parserStruct) next() {
|
||||
_, w := utf8.DecodeRuneInString(p.text[p.p:])
|
||||
p.p += w
|
||||
p.ln -= w
|
||||
}
|
||||
|
||||
// current returns the current rune at the parser position
|
||||
func (p *parserStruct) current() rune {
|
||||
r, _ := utf8.DecodeRuneInString(p.text[p.p:])
|
||||
return r
|
||||
}
|
||||
|
||||
// token returns the current token text between start and end positions
|
||||
func (p *parserStruct) token() (t string) {
|
||||
defer recover()
|
||||
return p.text[p.start:p.end]
|
||||
}
|
||||
|
||||
// parseSep parses whitespace separators
|
||||
func (p *parserStruct) parseSep() string {
|
||||
p.start = p.p
|
||||
for ; p.p < len(p.text); p.next() {
|
||||
|
@ -50,10 +57,11 @@ func (p *parserStruct) parseSep() string {
|
|||
}
|
||||
}
|
||||
p.end = p.p
|
||||
p.Type = ptSEP
|
||||
p.Type = ParserTokenSEP
|
||||
return p.token()
|
||||
}
|
||||
|
||||
// parseEol parses end of line and comments
|
||||
func (p *parserStruct) parseEol() string {
|
||||
p.start = p.p
|
||||
|
||||
|
@ -66,10 +74,11 @@ func (p *parserStruct) parseEol() string {
|
|||
}
|
||||
|
||||
p.end = p.p
|
||||
p.Type = ptEOL
|
||||
p.Type = ParserTokenEOL
|
||||
return p.token()
|
||||
}
|
||||
|
||||
// parseCommand parses a command within brackets
|
||||
func (p *parserStruct) parseCommand() string {
|
||||
level, blevel := 1, 0
|
||||
p.next() // skip
|
||||
|
@ -96,19 +105,20 @@ Loop:
|
|||
p.next()
|
||||
}
|
||||
p.end = p.p
|
||||
p.Type = ptCMD
|
||||
p.Type = ParserTokenCMD
|
||||
if p.p < len(p.text) && p.current() == ']' {
|
||||
p.next()
|
||||
}
|
||||
return p.token()
|
||||
}
|
||||
|
||||
// parseVar parses a variable reference
|
||||
func (p *parserStruct) parseVar() string {
|
||||
p.next() // skip the $
|
||||
p.start = p.p
|
||||
|
||||
if p.current() == '{' {
|
||||
p.Type = ptVAR
|
||||
p.Type = ParserTokenVAR
|
||||
return p.parseBrace()
|
||||
}
|
||||
|
||||
|
@ -124,14 +134,15 @@ func (p *parserStruct) parseVar() string {
|
|||
if p.start == p.p { // It's just a single char string "$"
|
||||
p.start = p.p - 1
|
||||
p.end = p.p
|
||||
p.Type = ptSTR
|
||||
p.Type = ParserTokenSTR
|
||||
} else {
|
||||
p.end = p.p
|
||||
p.Type = ptVAR
|
||||
p.Type = ParserTokenVAR
|
||||
}
|
||||
return p.token()
|
||||
}
|
||||
|
||||
// parseBrace parses a brace-enclosed string
|
||||
func (p *parserStruct) parseBrace() string {
|
||||
level := 1
|
||||
p.next() // skip
|
||||
|
@ -160,11 +171,12 @@ Loop:
|
|||
return p.token()
|
||||
}
|
||||
|
||||
// parseString parses a string with or without quotes
|
||||
func (p *parserStruct) parseString() string {
|
||||
newword := p.Type == ptSEP || p.Type == ptEOL || p.Type == ptSTR
|
||||
newword := p.Type == ParserTokenSEP || p.Type == ParserTokenEOL || p.Type == ParserTokenSTR
|
||||
|
||||
if c := p.current(); newword && c == '{' {
|
||||
p.Type = ptSTR
|
||||
p.Type = ParserTokenSTR
|
||||
return p.parseBrace()
|
||||
} else if newword && c == '"' {
|
||||
p.insidequote = 1
|
||||
|
@ -185,7 +197,7 @@ Loop:
|
|||
case '"':
|
||||
if p.insidequote != 0 {
|
||||
p.end = p.p
|
||||
p.Type = ptESC
|
||||
p.Type = ParserTokenESC
|
||||
p.next()
|
||||
p.insidequote = 0
|
||||
return p.token()
|
||||
|
@ -199,10 +211,11 @@ Loop:
|
|||
}
|
||||
|
||||
p.end = p.p
|
||||
p.Type = ptESC
|
||||
p.Type = ParserTokenESC
|
||||
return p.token()
|
||||
}
|
||||
|
||||
// parseComment skips over comment text
|
||||
func (p *parserStruct) parseComment() string {
|
||||
for p.ln != 0 && p.current() != '\n' {
|
||||
p.next()
|
||||
|
@ -210,13 +223,14 @@ func (p *parserStruct) parseComment() string {
|
|||
return p.token()
|
||||
}
|
||||
|
||||
// GetToken returns the next token from the parser
|
||||
func (p *parserStruct) GetToken() string {
|
||||
for {
|
||||
if p.ln == 0 {
|
||||
if p.Type != ptEOL && p.Type != ptEOF {
|
||||
p.Type = ptEOL
|
||||
if p.Type != ParserTokenEOL && p.Type != ParserTokenEOF {
|
||||
p.Type = ParserTokenEOL
|
||||
} else {
|
||||
p.Type = ptEOF
|
||||
p.Type = ParserTokenEOF
|
||||
}
|
||||
return p.token()
|
||||
}
|
||||
|
@ -237,7 +251,7 @@ func (p *parserStruct) GetToken() string {
|
|||
case '$':
|
||||
return p.parseVar()
|
||||
case '#':
|
||||
if p.Type == ptEOL {
|
||||
if p.Type == ParserTokenEOL {
|
||||
p.parseComment()
|
||||
continue
|
||||
}
|
||||
|
@ -246,5 +260,4 @@ func (p *parserStruct) GetToken() string {
|
|||
return p.parseString()
|
||||
}
|
||||
}
|
||||
/* return p.token() /* unreached */
|
||||
}
|
79
pkg/picol/parser_test.go
Normal file
79
pkg/picol/parser_test.go
Normal file
|
@ -0,0 +1,79 @@
|
|||
package picol_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/dragonheim/gagent/pkg/picol"
|
||||
)
|
||||
|
||||
func TestParser(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
input string
|
||||
expected []int
|
||||
}{
|
||||
{
|
||||
"Simple test",
|
||||
"set x 10\nincr x",
|
||||
[]int{
|
||||
picol.ParserTokenSTR,
|
||||
picol.ParserTokenSEP,
|
||||
picol.ParserTokenSTR,
|
||||
picol.ParserTokenSEP,
|
||||
picol.ParserTokenSTR,
|
||||
picol.ParserTokenEOL,
|
||||
picol.ParserTokenSTR,
|
||||
picol.ParserTokenSEP,
|
||||
picol.ParserTokenSTR,
|
||||
picol.ParserTokenEOL,
|
||||
picol.ParserTokenEOF,
|
||||
},
|
||||
},
|
||||
{
|
||||
"Variable and command test",
|
||||
"set x $y\nputs [expr $x * 2]",
|
||||
[]int{
|
||||
picol.ParserTokenSTR,
|
||||
picol.ParserTokenSEP,
|
||||
picol.ParserTokenSTR,
|
||||
picol.ParserTokenSEP,
|
||||
picol.ParserTokenVAR,
|
||||
picol.ParserTokenEOL,
|
||||
picol.ParserTokenSTR,
|
||||
picol.ParserTokenSEP,
|
||||
picol.ParserTokenCMD,
|
||||
picol.ParserTokenEOL,
|
||||
picol.ParserTokenEOF,
|
||||
},
|
||||
},
|
||||
{
|
||||
"Braces and quotes test",
|
||||
`set x {"Hello World"}`,
|
||||
[]int{
|
||||
picol.ParserTokenSTR,
|
||||
picol.ParserTokenSEP,
|
||||
picol.ParserTokenSTR,
|
||||
picol.ParserTokenSEP,
|
||||
picol.ParserTokenSTR,
|
||||
picol.ParserTokenEOL,
|
||||
picol.ParserTokenEOF,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
parser := picol.InitParser(tc.input)
|
||||
|
||||
for _, expectedType := range tc.expected {
|
||||
parser.GetToken()
|
||||
if parser.Type != expectedType {
|
||||
t.Errorf("Expected token type %d, got %d", expectedType, parser.Type)
|
||||
}
|
||||
if parser.Type == picol.ParserTokenEOF {
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
184
pkg/picol/picol.go
Normal file
184
pkg/picol/picol.go
Normal file
|
@ -0,0 +1,184 @@
|
|||
package picol
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
/*
|
||||
* Error variables
|
||||
*/
|
||||
var (
|
||||
ErrReturn = errors.New("RETURN")
|
||||
ErrBreak = errors.New("BREAK")
|
||||
ErrContinue = errors.New("CONTINUE")
|
||||
)
|
||||
|
||||
/*
|
||||
* Variable type
|
||||
*/
|
||||
type Variable string
|
||||
|
||||
/*
|
||||
* CommandFunc type
|
||||
*/
|
||||
type CommandFunc func(interp *Interpreter, argv []string, privdata interface{}) (string, error)
|
||||
|
||||
/*
|
||||
* Command structure
|
||||
*/
|
||||
type Command struct {
|
||||
fn CommandFunc
|
||||
privdata interface{}
|
||||
}
|
||||
|
||||
/*
|
||||
* CallFrame structure
|
||||
*/
|
||||
type CallFrame struct {
|
||||
vars map[string]Variable
|
||||
parent *CallFrame
|
||||
}
|
||||
|
||||
/*
|
||||
* Interpreter structure
|
||||
*/
|
||||
type Interpreter struct {
|
||||
level int
|
||||
callframe *CallFrame
|
||||
commands map[string]Command
|
||||
}
|
||||
|
||||
/*
|
||||
* NewInterpreter initializes a new Interpreter
|
||||
*/
|
||||
func NewInterpreter() *Interpreter {
|
||||
return &Interpreter{
|
||||
level: 0,
|
||||
callframe: &CallFrame{vars: make(map[string]Variable)},
|
||||
commands: make(map[string]Command),
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Variable retrieves a variable's value
|
||||
*/
|
||||
func (interp *Interpreter) Variable(name string) (Variable, bool) {
|
||||
for frame := interp.callframe; frame != nil; frame = frame.parent {
|
||||
v, ok := frame.vars[name]
|
||||
if ok {
|
||||
return v, ok
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
/*
|
||||
* SetVariable sets a variable's value
|
||||
*/
|
||||
func (interp *Interpreter) SetVariable(name, val string) {
|
||||
interp.callframe.vars[name] = Variable(val)
|
||||
}
|
||||
|
||||
/*
|
||||
* UnsetVariable removes a variable
|
||||
*/
|
||||
func (interp *Interpreter) UnsetVariable(name string) {
|
||||
delete(interp.callframe.vars, name)
|
||||
}
|
||||
|
||||
/*
|
||||
* Command retrieves a command
|
||||
*/
|
||||
func (interp *Interpreter) Command(name string) *Command {
|
||||
v, ok := interp.commands[name]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return &v
|
||||
}
|
||||
|
||||
/*
|
||||
* RegisterCommand registers a new command
|
||||
*/
|
||||
func (interp *Interpreter) RegisterCommand(name string, fn CommandFunc, privdata interface{}) error {
|
||||
cmd := interp.Command(name)
|
||||
if cmd != nil {
|
||||
return fmt.Errorf("Command '%s' already defined", name)
|
||||
}
|
||||
|
||||
interp.commands[name] = Command{fn, privdata}
|
||||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
* Eval evaluates a script
|
||||
*/
|
||||
func (interp *Interpreter) Eval(script string) (string, error) {
|
||||
parser := InitParser(script)
|
||||
var result string
|
||||
var err error
|
||||
|
||||
argv := []string{}
|
||||
|
||||
for {
|
||||
prevType := parser.Type
|
||||
token := parser.GetToken()
|
||||
if parser.Type == ParserTokenEOF {
|
||||
break
|
||||
}
|
||||
|
||||
switch parser.Type {
|
||||
case ParserTokenVAR:
|
||||
v, ok := interp.Variable(token)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("no such variable '%s'", token)
|
||||
}
|
||||
token = string(v)
|
||||
case ParserTokenCMD:
|
||||
result, err = interp.Eval(token)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
token = result
|
||||
case ParserTokenESC:
|
||||
/*
|
||||
* TODO: escape handling missing!
|
||||
*/
|
||||
case ParserTokenSEP:
|
||||
// prevType = parser.Type
|
||||
continue
|
||||
}
|
||||
|
||||
if parser.Type == ParserTokenEOL {
|
||||
// prevType = parser.Type
|
||||
if len(argv) != 0 {
|
||||
cmd := interp.Command(argv[0])
|
||||
if cmd == nil {
|
||||
return "", fmt.Errorf("no such command '%s'", argv[0])
|
||||
}
|
||||
result, err = cmd.fn(interp, argv, cmd.privdata)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
}
|
||||
/*
|
||||
* Prepare for the next command
|
||||
*/
|
||||
argv = []string{}
|
||||
continue
|
||||
}
|
||||
|
||||
/*
|
||||
* We have a new token, append to the previous or as new arg?
|
||||
*/
|
||||
if prevType == ParserTokenSEP || prevType == ParserTokenEOL {
|
||||
argv = append(argv, token)
|
||||
} else { // Interpolation
|
||||
argv[len(argv)-1] = strings.Join([]string{argv[len(argv)-1], token}, "")
|
||||
}
|
||||
// prevType = parser.Type
|
||||
}
|
||||
return result, nil
|
||||
}
|
56
pkg/picol/picol_test.go
Normal file
56
pkg/picol/picol_test.go
Normal file
|
@ -0,0 +1,56 @@
|
|||
package picol_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/dragonheim/gagent/pkg/picol"
|
||||
)
|
||||
|
||||
func TestInterpreter(t *testing.T) {
|
||||
interp := picol.NewInterpreter()
|
||||
|
||||
// Register a command
|
||||
err := interp.RegisterCommand("test", testCommand, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Error registering test command: %v", err)
|
||||
}
|
||||
|
||||
// Test command execution
|
||||
script := "test hello world"
|
||||
result, err := interp.Eval(script)
|
||||
if err != nil {
|
||||
t.Fatalf("Error executing script: %v", err)
|
||||
}
|
||||
expected := "hello world"
|
||||
if result != expected {
|
||||
t.Errorf("Expected result '%s', got '%s'", expected, result)
|
||||
}
|
||||
|
||||
// Test variable setting
|
||||
interp.SetVariable("x", "42")
|
||||
|
||||
// Test variable retrieval
|
||||
val, ok := interp.Variable("x")
|
||||
if !ok {
|
||||
t.Fatalf("Variable 'x' not found")
|
||||
}
|
||||
expectedVar := "42"
|
||||
if val != picol.Variable(expectedVar) {
|
||||
t.Errorf("Expected variable value '%s', got '%s'", expectedVar, val)
|
||||
}
|
||||
|
||||
// Test variable unsetting
|
||||
interp.UnsetVariable("x")
|
||||
_, ok = interp.Variable("x")
|
||||
if ok {
|
||||
t.Fatalf("Variable 'x' should have been unset")
|
||||
}
|
||||
}
|
||||
|
||||
// testCommand is a simple custom command for testing
|
||||
func testCommand(interp *picol.Interpreter, argv []string, privdata interface{}) (string, error) {
|
||||
if len(argv) != 3 {
|
||||
return "", nil
|
||||
}
|
||||
return argv[1] + " " + argv[2], nil
|
||||
}
|
45
pkg/picol/picol_unused/main.go_unused
Normal file
45
pkg/picol/picol_unused/main.go_unused
Normal file
|
@ -0,0 +1,45 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
bufio "bufio"
|
||||
flag "flag"
|
||||
fmt "fmt"
|
||||
io "io"
|
||||
os "os"
|
||||
|
||||
picol "github.com/dragonheim/gagent/pkg/picol"
|
||||
)
|
||||
|
||||
var fname = flag.String("f", "", "file name")
|
||||
|
||||
func RunPicol(fname string) error {
|
||||
interp := picol.NewInterpreter()
|
||||
interp.RegisterCoreCommands()
|
||||
|
||||
buf, err := io.ReadFile(fname)
|
||||
if err == nil {
|
||||
result, err := interp.Eval(string(buf))
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error: %s, Result: %s", err, result)
|
||||
}
|
||||
} else {
|
||||
for {
|
||||
fmt.Print("picol> ")
|
||||
scanner := bufio.NewReader(os.Stdin)
|
||||
clibuf, _ := scanner.ReadString('\n')
|
||||
result, err := interp.Eval(clibuf[:len(clibuf)-1])
|
||||
if len(result) != 0 {
|
||||
return fmt.Errorf("Error: %s, Result: %s", err, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
err := RunPicol(*fname)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
}
|
31
pkg/picol/picol_unused/main_test.go_unused
Normal file
31
pkg/picol/picol_unused/main_test.go_unused
Normal file
|
@ -0,0 +1,31 @@
|
|||
package main_test
|
||||
|
||||
import (
|
||||
io "io"
|
||||
os "os"
|
||||
testing "testing"
|
||||
|
||||
picol "github.com/dragonheim/gagent/pkg/picol/picol_unused"
|
||||
)
|
||||
|
||||
func Test_RunPicol(t *testing.T) {
|
||||
// Create a temporary test file
|
||||
content := []byte("set a 5\nset b 7\n+ $a $b\n")
|
||||
tmpfile, err := io.TempFile("", "picol_test")
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating temporary test file: %v", err)
|
||||
}
|
||||
defer os.Remove(tmpfile.Name()) // clean up
|
||||
|
||||
if _, err := tmpfile.Write(content); err != nil {
|
||||
t.Fatalf("Error writing content to temporary test file: %v", err)
|
||||
}
|
||||
if err := tmpfile.Close(); err != nil {
|
||||
t.Fatalf("Error closing temporary test file: %v", err)
|
||||
}
|
||||
|
||||
err = picol.RunPicol(tmpfile.Name())
|
||||
if err != nil {
|
||||
t.Errorf("Error during RunPicol: %v", err)
|
||||
}
|
||||
}
|
|
@ -1,54 +0,0 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
gs "git.dragonheim.net/dragonheim/gagent/src/gstructs"
|
||||
|
||||
zmq "github.com/pebbe/zmq4"
|
||||
)
|
||||
|
||||
// Main is the initiation function for a Client
|
||||
func Main(config gs.GagentConfig, rid int, agent string) {
|
||||
log.Printf("[INFO] Starting client\n")
|
||||
|
||||
// Generate connect string for this router.
|
||||
var rport = int64(config.ClientPort)
|
||||
if config.Routers[rid].ClientPort != 0 {
|
||||
rport = config.Routers[rid].ClientPort
|
||||
}
|
||||
connectString := fmt.Sprintf("tcp://%s:%d", config.Routers[rid].RouterAddr, rport)
|
||||
log.Printf("[DEBUG] Attempting to connect to %s\n", connectString)
|
||||
|
||||
var mu sync.Mutex
|
||||
|
||||
sock, _ := zmq.NewSocket(zmq.REQ)
|
||||
defer sock.Close()
|
||||
|
||||
sock.SetIdentity(config.UUID)
|
||||
sock.Connect(connectString)
|
||||
|
||||
go func() {
|
||||
mu.Lock()
|
||||
log.Printf("[DEBUG] Start sending agent...\n")
|
||||
sock.SendMessage(agent)
|
||||
log.Printf("[DEBUG] End sending agent...\n")
|
||||
mu.Unlock()
|
||||
}()
|
||||
|
||||
// time.Sleep(10 * time.Millisecond)
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
// for {
|
||||
// time.Sleep(10 * time.Millisecond)
|
||||
// mu.Lock()
|
||||
// msg, err := sock.RecvMessage(zmq.DONTWAIT)
|
||||
// if err == nil {
|
||||
// log.Println(msg[0], config.UUID)
|
||||
// }
|
||||
// mu.Unlock()
|
||||
// }
|
||||
}
|
|
@ -1,110 +0,0 @@
|
|||
package gstructs
|
||||
|
||||
// GagentConfig is the primary construct used by all modes
|
||||
type GagentConfig struct {
|
||||
Name string `hcl:"name,optional"`
|
||||
Mode string `hcl:"mode,attr"`
|
||||
UUID string `hcl:"uuid,optional"`
|
||||
ListenAddr string `hcl:"listenaddr,optional"`
|
||||
ClientPort int64 `hcl:"clientport,optional"`
|
||||
RouterPort int64 `hcl:"routerport,optional"`
|
||||
WorkerPort int64 `hcl:"workerport,optional"`
|
||||
Clients []*ClientDetails `hcl:"client,block"`
|
||||
Routers []*RouterDetails `hcl:"router,block"`
|
||||
Workers []*WorkerDetails `hcl:"worker,block"`
|
||||
}
|
||||
|
||||
// ClientDetails is details about known clients
|
||||
type ClientDetails struct {
|
||||
/*
|
||||
* Client name for display purposes in logs and
|
||||
* diagnostics.
|
||||
*/
|
||||
ClientName string `hcl:",label"`
|
||||
|
||||
/*
|
||||
* UUID String for the client node. This is used by
|
||||
* the router to determine which MQ client to send
|
||||
* the agent's results to. This attempts to keep the
|
||||
* clients unique globally.
|
||||
*/
|
||||
ClientID string `hcl:"clientid,optional"`
|
||||
}
|
||||
|
||||
// RouterDetails is details about known routers
|
||||
type RouterDetails struct {
|
||||
/*
|
||||
* Router name for display purposes in logs and
|
||||
* diagnostics
|
||||
*/
|
||||
RouterName string `hcl:",label"`
|
||||
|
||||
/*
|
||||
* UUID String for the router node. This is used by
|
||||
* the clients, routers, and workers to determine
|
||||
* which MQ router to send the agent's requests to.
|
||||
* This attempts to keep the routers unique globally.
|
||||
*/
|
||||
RouterID string `hcl:"routerid,attr"`
|
||||
|
||||
/*
|
||||
* This is the IP address or hostname the router
|
||||
* will listen on. The router will start up a 0MQ
|
||||
* service that clients and workers will connect to.
|
||||
*/
|
||||
RouterAddr string `hcl:"address,attr"`
|
||||
|
||||
/*
|
||||
* This is the is the port that the router listens
|
||||
* on for clients. If not defined, it will default
|
||||
* to 35571.
|
||||
*/
|
||||
ClientPort int64 `hcl:"clientport,optional"`
|
||||
|
||||
/*
|
||||
* This is the is the port that the router listens
|
||||
* on for routers. If not defined, it will default
|
||||
* to 35570.
|
||||
*/
|
||||
RouterPort int64 `hcl:"workerport,optional"`
|
||||
|
||||
/*
|
||||
* This is the is the port that the router listens
|
||||
* on for clients. If not defined, it will default
|
||||
* to 35572.
|
||||
*/
|
||||
WorkerPort int64 `hcl:"workerport,optional"`
|
||||
|
||||
/*
|
||||
* These tags will be passed to the router upon
|
||||
* connection. The router will then use these
|
||||
* tags to help determine which worker / client
|
||||
* to send the client's requests and results to.
|
||||
*/
|
||||
RouterTags []string `hcl:"tags,optional"`
|
||||
}
|
||||
|
||||
// WorkerDetails is details about known workers
|
||||
type WorkerDetails struct {
|
||||
/*
|
||||
* Router name for display purposes in logs and
|
||||
* diagnostics
|
||||
*/
|
||||
WorkerName string `hcl:",label"`
|
||||
|
||||
/*
|
||||
* UUID String for the worker node. This is used
|
||||
* by the router to determine which MQ client to
|
||||
* send agents to. This attempts to keep the
|
||||
* workers unique globally.
|
||||
*/
|
||||
WorkerID string `hcl:"workerid,attr"`
|
||||
|
||||
/*
|
||||
* These tags will be passed to the router upon
|
||||
* connection. The router will then use these
|
||||
* tags to help determine which worker / client
|
||||
* to send the agent and it's results to.
|
||||
*/
|
||||
WorkerTags []string `hcl:"tags,optional"`
|
||||
}
|
|
@ -1,226 +0,0 @@
|
|||
package picol
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func ArityErr(i *Interp, name string, argv []string) error {
|
||||
return fmt.Errorf("wrong number of args for %s %s", name, argv)
|
||||
}
|
||||
|
||||
// CommandMath is the math command for TCL
|
||||
func CommandMath(i *Interp, argv []string, pd interface{}) (string, error) {
|
||||
if len(argv) != 3 {
|
||||
return "", ArityErr(i, argv[0], argv)
|
||||
}
|
||||
a, _ := strconv.Atoi(argv[1])
|
||||
b, _ := strconv.Atoi(argv[2])
|
||||
var c int
|
||||
switch {
|
||||
case argv[0] == "+":
|
||||
c = a + b
|
||||
case argv[0] == "-":
|
||||
c = a - b
|
||||
case argv[0] == "*":
|
||||
c = a * b
|
||||
case argv[0] == "/":
|
||||
c = a / b
|
||||
case argv[0] == ">":
|
||||
if a > b {
|
||||
c = 1
|
||||
}
|
||||
case argv[0] == ">=":
|
||||
if a >= b {
|
||||
c = 1
|
||||
}
|
||||
case argv[0] == "<":
|
||||
if a < b {
|
||||
c = 1
|
||||
}
|
||||
case argv[0] == "<=":
|
||||
if a <= b {
|
||||
c = 1
|
||||
}
|
||||
case argv[0] == "==":
|
||||
if a == b {
|
||||
c = 1
|
||||
}
|
||||
case argv[0] == "!=":
|
||||
if a != b {
|
||||
c = 1
|
||||
}
|
||||
default: // FIXME I hate warnings
|
||||
c = 0
|
||||
}
|
||||
return fmt.Sprintf("%d", c), nil
|
||||
}
|
||||
|
||||
// CommandSet is the set command for TCL
|
||||
func CommandSet(i *Interp, argv []string, pd interface{}) (string, error) {
|
||||
if len(argv) != 3 {
|
||||
return "", ArityErr(i, argv[0], argv)
|
||||
}
|
||||
i.SetVar(argv[1], argv[2])
|
||||
return argv[2], nil
|
||||
}
|
||||
|
||||
// CommandUnset is the unset command for TCL
|
||||
func CommandUnset(i *Interp, argv []string, pd interface{}) (string, error) {
|
||||
if len(argv) != 2 {
|
||||
return "", ArityErr(i, argv[0], argv)
|
||||
}
|
||||
i.UnsetVar(argv[1])
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// CommandIf is the if command for TCL
|
||||
func CommandIf(i *Interp, argv []string, pd interface{}) (string, error) {
|
||||
if len(argv) != 3 && len(argv) != 5 {
|
||||
return "", ArityErr(i, argv[0], argv)
|
||||
}
|
||||
|
||||
result, err := i.Eval(argv[1])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if r, _ := strconv.Atoi(result); r != 0 {
|
||||
return i.Eval(argv[2])
|
||||
} else if len(argv) == 5 {
|
||||
return i.Eval(argv[4])
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// CommandWhile is the while command for TCL
|
||||
func CommandWhile(i *Interp, argv []string, pd interface{}) (string, error) {
|
||||
if len(argv) != 3 {
|
||||
return "", ArityErr(i, argv[0], argv)
|
||||
}
|
||||
|
||||
for {
|
||||
result, err := i.Eval(argv[1])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if r, _ := strconv.Atoi(result); r != 0 {
|
||||
result, err := i.Eval(argv[2])
|
||||
switch err {
|
||||
case errContinue, nil:
|
||||
//pass
|
||||
case errBreak:
|
||||
return result, nil
|
||||
default:
|
||||
return result, err
|
||||
}
|
||||
} else {
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CommandRetCodes is a function to get the return codes for TCL
|
||||
func CommandRetCodes(i *Interp, argv []string, pd interface{}) (string, error) {
|
||||
if len(argv) != 1 {
|
||||
return "", ArityErr(i, argv[0], argv)
|
||||
}
|
||||
switch argv[0] {
|
||||
case "break":
|
||||
return "", errBreak
|
||||
case "continue":
|
||||
return "", errContinue
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// CommandCallProc is a function to call proc commands for TCL
|
||||
func CommandCallProc(i *Interp, argv []string, pd interface{}) (string, error) {
|
||||
var x []string
|
||||
|
||||
if pd, ok := pd.([]string); ok {
|
||||
x = pd
|
||||
} else {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
i.callframe = &CallFrame{vars: make(map[string]Var), parent: i.callframe}
|
||||
defer func() { i.callframe = i.callframe.parent }() // remove the called proc callframe
|
||||
|
||||
arity := 0
|
||||
for _, arg := range strings.Split(x[0], " ") {
|
||||
if len(arg) == 0 {
|
||||
continue
|
||||
}
|
||||
arity++
|
||||
i.SetVar(arg, argv[arity])
|
||||
}
|
||||
|
||||
if arity != len(argv)-1 {
|
||||
return "", fmt.Errorf("proc '%s' called with wrong arg num", argv[0])
|
||||
}
|
||||
|
||||
body := x[1]
|
||||
result, err := i.Eval(body)
|
||||
if err == errReturn {
|
||||
err = nil
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
// CommandProc is a function to register proc commands for TCL
|
||||
func CommandProc(i *Interp, argv []string, pd interface{}) (string, error) {
|
||||
if len(argv) != 4 {
|
||||
return "", ArityErr(i, argv[0], argv)
|
||||
}
|
||||
return "", i.RegisterCommand(argv[1], CommandCallProc, []string{argv[2], argv[3]})
|
||||
}
|
||||
|
||||
// CommandReturn is a function to register return codes for commands for TCL
|
||||
func CommandReturn(i *Interp, argv []string, pd interface{}) (string, error) {
|
||||
if len(argv) != 1 && len(argv) != 2 {
|
||||
return "", ArityErr(i, argv[0], argv)
|
||||
}
|
||||
var r string
|
||||
if len(argv) == 2 {
|
||||
r = argv[1]
|
||||
}
|
||||
return r, errReturn
|
||||
}
|
||||
|
||||
// CommandError is a function to return error codes for commands for TCL
|
||||
func CommandError(i *Interp, argv []string, pd interface{}) (string, error) {
|
||||
if len(argv) != 1 && len(argv) != 2 {
|
||||
return "", ArityErr(i, argv[0], argv)
|
||||
}
|
||||
return "", fmt.Errorf(argv[1])
|
||||
}
|
||||
|
||||
// CommandPuts is a function to print strings for TCL
|
||||
func CommandPuts(i *Interp, argv []string, pd interface{}) (string, error) {
|
||||
if len(argv) != 2 {
|
||||
return "", fmt.Errorf("wrong number of args for %s %s", argv[0], argv)
|
||||
}
|
||||
fmt.Println(argv[1])
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// RegisterCoreCommands is a callable to register TCL commands.
|
||||
func (i *Interp) RegisterCoreCommands() {
|
||||
name := [...]string{"+", "-", "*", "/", ">", ">=", "<", "<=", "==", "!="}
|
||||
for _, n := range name {
|
||||
i.RegisterCommand(n, CommandMath, nil)
|
||||
}
|
||||
i.RegisterCommand("set", CommandSet, nil)
|
||||
i.RegisterCommand("unset", CommandUnset, nil)
|
||||
i.RegisterCommand("if", CommandIf, nil)
|
||||
i.RegisterCommand("while", CommandWhile, nil)
|
||||
i.RegisterCommand("break", CommandRetCodes, nil)
|
||||
i.RegisterCommand("continue", CommandRetCodes, nil)
|
||||
i.RegisterCommand("proc", CommandProc, nil)
|
||||
i.RegisterCommand("return", CommandReturn, nil)
|
||||
i.RegisterCommand("error", CommandError, nil)
|
||||
i.RegisterCommand("puts", CommandPuts, nil)
|
||||
}
|
|
@ -1,138 +0,0 @@
|
|||
package picol
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
errReturn = errors.New("RETURN")
|
||||
errBreak = errors.New("BREAK")
|
||||
errContinue = errors.New("CONTINUE")
|
||||
)
|
||||
|
||||
type Var string
|
||||
type CmdFunc func(i *Interp, argv []string, privdata interface{}) (string, error)
|
||||
type Cmd struct {
|
||||
fn CmdFunc
|
||||
privdata interface{}
|
||||
}
|
||||
type CallFrame struct {
|
||||
vars map[string]Var
|
||||
parent *CallFrame
|
||||
}
|
||||
type Interp struct {
|
||||
level int
|
||||
callframe *CallFrame
|
||||
commands map[string]Cmd
|
||||
}
|
||||
|
||||
func InitInterp() *Interp {
|
||||
return &Interp{
|
||||
level: 0,
|
||||
callframe: &CallFrame{vars: make(map[string]Var)},
|
||||
commands: make(map[string]Cmd),
|
||||
}
|
||||
}
|
||||
|
||||
func (i *Interp) Var(name string) (Var, bool) {
|
||||
for frame := i.callframe; frame != nil; frame = frame.parent {
|
||||
v, ok := frame.vars[name]
|
||||
if ok {
|
||||
return v, ok
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
func (i *Interp) SetVar(name, val string) {
|
||||
i.callframe.vars[name] = Var(val)
|
||||
}
|
||||
|
||||
func (i *Interp) UnsetVar(name string) {
|
||||
delete(i.callframe.vars, name)
|
||||
}
|
||||
|
||||
func (i *Interp) Command(name string) *Cmd {
|
||||
v, ok := i.commands[name]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return &v
|
||||
}
|
||||
|
||||
func (i *Interp) RegisterCommand(name string, fn CmdFunc, privdata interface{}) error {
|
||||
c := i.Command(name)
|
||||
if c != nil {
|
||||
return fmt.Errorf("Command '%s' already defined", name)
|
||||
}
|
||||
|
||||
i.commands[name] = Cmd{fn, privdata}
|
||||
return nil
|
||||
}
|
||||
|
||||
/* EVAL! */
|
||||
func (i *Interp) Eval(t string) (string, error) {
|
||||
p := initParser(t)
|
||||
var result string
|
||||
var err error
|
||||
|
||||
argv := []string{}
|
||||
|
||||
for {
|
||||
prevtype := p.Type
|
||||
// XXX
|
||||
t = p.GetToken()
|
||||
if p.Type == ptEOF {
|
||||
break
|
||||
}
|
||||
|
||||
switch p.Type {
|
||||
case ptVAR:
|
||||
v, ok := i.Var(t)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("no such variable '%s'", t)
|
||||
}
|
||||
t = string(v)
|
||||
case ptCMD:
|
||||
result, err = i.Eval(t)
|
||||
if err != nil {
|
||||
return result, err
|
||||
} else {
|
||||
t = result
|
||||
}
|
||||
case ptESC:
|
||||
// XXX: escape handling missing!
|
||||
case ptSEP:
|
||||
prevtype = p.Type
|
||||
continue
|
||||
}
|
||||
|
||||
// We have a complete command + args. Call it!
|
||||
if p.Type == ptEOL {
|
||||
prevtype = p.Type
|
||||
if len(argv) != 0 {
|
||||
c := i.Command(argv[0])
|
||||
if c == nil {
|
||||
return "", fmt.Errorf("no such command '%s'", argv[0])
|
||||
}
|
||||
result, err = c.fn(i, argv, c.privdata)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
}
|
||||
// Prepare for the next command
|
||||
argv = []string{}
|
||||
continue
|
||||
}
|
||||
|
||||
// We have a new token, append to the previous or as new arg?
|
||||
if prevtype == ptSEP || prevtype == ptEOL {
|
||||
argv = append(argv, t)
|
||||
} else { // Interpolation
|
||||
argv[len(argv)-1] = strings.Join([]string{argv[len(argv)-1], t}, "")
|
||||
}
|
||||
prevtype = p.Type
|
||||
}
|
||||
return result, nil
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
picol "git.dragonheim.net/dragonheim/gagent/src/picol"
|
||||
)
|
||||
|
||||
var fname = flag.String("f", "", "file name")
|
||||
|
||||
// CommandPuts is a simple version of the TCL puts function.
|
||||
func CommandPuts(i *picol.Interp, argv []string, pd interface{}) (string, error) {
|
||||
if len(argv) != 2 {
|
||||
return "", fmt.Errorf("wrong number of args for %s %s", argv[0], argv)
|
||||
}
|
||||
fmt.Println(argv[1])
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
interp := picol.InitInterp()
|
||||
interp.RegisterCoreCommands()
|
||||
interp.RegisterCommand("puts", CommandPuts, nil)
|
||||
|
||||
buf, err := ioutil.ReadFile(*fname)
|
||||
if err == nil {
|
||||
result, err := interp.Eval(string(buf))
|
||||
if err != nil {
|
||||
fmt.Println("ERRROR", result, err)
|
||||
}
|
||||
} else {
|
||||
for {
|
||||
fmt.Print("picol> ")
|
||||
scanner := bufio.NewReader(os.Stdin)
|
||||
clibuf, _ := scanner.ReadString('\n')
|
||||
result, err := interp.Eval(clibuf[:len(clibuf)-1])
|
||||
if len(result) != 0 {
|
||||
fmt.Println("ERRROR", result, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,91 +0,0 @@
|
|||
package router
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
gs "git.dragonheim.net/dragonheim/gagent/src/gstructs"
|
||||
|
||||
zmq "github.com/pebbe/zmq4"
|
||||
)
|
||||
|
||||
// @TODO -- This was documented in the example, and I am unclear what it does
|
||||
const (
|
||||
WORKER_READY = "\001" // Signals worker is ready
|
||||
)
|
||||
|
||||
// Main is the initiation function for a Router
|
||||
func Main(config gs.GagentConfig) {
|
||||
log.Printf("[INFO] Starting router\n")
|
||||
|
||||
clientSock, _ := zmq.NewSocket(zmq.ROUTER)
|
||||
defer clientSock.Close()
|
||||
|
||||
workerSock, _ := zmq.NewSocket(zmq.DEALER)
|
||||
defer workerSock.Close()
|
||||
|
||||
clientSock.Bind(fmt.Sprintf("tcp://%s:%d", config.ListenAddr, config.ClientPort))
|
||||
workerSock.Bind(fmt.Sprintf("tcp://%s:%d", config.ListenAddr, config.WorkerPort))
|
||||
|
||||
workers := make([]string, 0)
|
||||
|
||||
poller1 := zmq.NewPoller()
|
||||
poller1.Add(workerSock, zmq.POLLIN)
|
||||
|
||||
poller2 := zmq.NewPoller()
|
||||
poller2.Add(workerSock, zmq.POLLIN)
|
||||
poller2.Add(clientSock, zmq.POLLIN)
|
||||
|
||||
LOOP:
|
||||
for {
|
||||
// Poll frontend only if we have available workers
|
||||
var sockets []zmq.Polled
|
||||
var err error
|
||||
if len(workers) > 0 {
|
||||
sockets, err = poller2.Poll(-1)
|
||||
} else {
|
||||
sockets, err = poller1.Poll(-1)
|
||||
}
|
||||
if err != nil {
|
||||
break // Interrupted
|
||||
}
|
||||
for _, socket := range sockets {
|
||||
switch s := socket.Socket; s {
|
||||
case workerSock: // Handle worker activity on backend
|
||||
// Use worker identity for load-balancing
|
||||
msg, err := s.RecvMessage(0)
|
||||
if err != nil {
|
||||
break LOOP // Interrupted
|
||||
}
|
||||
var identity string
|
||||
identity, msg = unwrap(msg)
|
||||
log.Printf("[DEBUG] Worker message received: %s", msg)
|
||||
workers = append(workers, identity)
|
||||
|
||||
// Forward message to client if it's not a READY
|
||||
if msg[0] != WORKER_READY {
|
||||
clientSock.SendMessage(msg)
|
||||
}
|
||||
|
||||
case clientSock:
|
||||
// Get client request, route to first available worker
|
||||
msg, err := s.RecvMessage(0)
|
||||
log.Printf("[DEBUG] Client message received: %s", msg)
|
||||
if err == nil {
|
||||
workerSock.SendMessage(workers[0], "", msg)
|
||||
workers = workers[1:]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func unwrap(msg []string) (head string, tail []string) {
|
||||
head = msg[0]
|
||||
if len(msg) > 1 && msg[1] == "" {
|
||||
tail = msg[2:]
|
||||
} else {
|
||||
tail = msg[1:]
|
||||
}
|
||||
return
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
package worker
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
gs "git.dragonheim.net/dragonheim/gagent/src/gstructs"
|
||||
|
||||
// picol "git.dragonheim.net/dragonheim/gagent/src/picol"
|
||||
zmq "github.com/pebbe/zmq4"
|
||||
)
|
||||
|
||||
// Main is the initiation function for a Worker
|
||||
func Main(config gs.GagentConfig, rid int) {
|
||||
log.Printf("[INFO] Starting worker\n")
|
||||
|
||||
// Generate connect string for this router.
|
||||
var rport = int64(config.WorkerPort)
|
||||
if config.Routers[rid].WorkerPort != 0 {
|
||||
rport = config.Routers[rid].WorkerPort
|
||||
}
|
||||
connectString := fmt.Sprintf("tcp://%s:%d", config.Routers[rid].RouterAddr, rport)
|
||||
|
||||
subscriber, _ := zmq.NewSocket(zmq.REP)
|
||||
defer subscriber.Close()
|
||||
|
||||
log.Printf("[DEBUG] Attempting to connect to %s\n", connectString)
|
||||
subscriber.Connect(connectString)
|
||||
|
||||
msg, err := subscriber.Recv(0)
|
||||
if err != nil {
|
||||
log.Printf("[DEBUG] Received error: %v", err)
|
||||
}
|
||||
log.Printf("[DEBUG] Received message: %v", msg[0])
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
terraform {
|
||||
required_version = ">= 1.0.0"
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 3.46.0"
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue