Compare commits
70 Commits
v2023.3.27
...
v2023.6.22
Author | SHA1 | Date | |
---|---|---|---|
8b5a97da54 | |||
d73e027ee3 | |||
054744e3e9 | |||
0e664bce44 | |||
58ef3b0077 | |||
6a976c0b51 | |||
d188af81af | |||
e975381b4f | |||
0d03a708f9 | |||
64ea0e05a9 | |||
10844a15a3 | |||
0394e34055 | |||
541d30d74f | |||
87a45090e0 | |||
fcd159c0fb | |||
eda02c6be3 | |||
ef3048b005 | |||
51e1dc3b2d | |||
3d01cf0f93 | |||
bb03b3a54a | |||
813f837291 | |||
ed35ee5002 | |||
4b5bc0bc82 | |||
dee62184b9 | |||
76656e8dbf | |||
41b1619fc1 | |||
35d5ee868f | |||
765257b4b1 | |||
2315ee7b61 | |||
86a6d81e1d | |||
c4427dfd2b | |||
280b0fbd50 | |||
8fb86c600f | |||
12f8b3aa25 | |||
2d2dc29c84 | |||
4cf53d9f15 | |||
34e4769b49 | |||
47c2546d54 | |||
21173911fb | |||
b213b8d1ae | |||
9dcddc5566 | |||
9a46c9d3d0 | |||
69f183d126 | |||
e8829170e5 | |||
253c93dbac | |||
d2f865ccbb | |||
7ee4344adc | |||
06b1235707 | |||
2e1ee44e6a | |||
242a247222 | |||
562d698066 | |||
909549f056 | |||
7d551a8312 | |||
d02eb91b11 | |||
d2bcdd2999 | |||
c638fe102b | |||
273265c3ef | |||
3e02a9f031 | |||
b52c687643 | |||
8119a01bf6 | |||
e5b6c5e949 | |||
9c69dc7ec8 | |||
4e6b450338 | |||
351f22e216 | |||
854a6ae41b | |||
a48c2ebe14 | |||
cdd78e4031 | |||
e832b7dedd | |||
3d0b0102c2 | |||
e60c441d43 |
5
.gitignore
vendored
5
.gitignore
vendored
@ -4,9 +4,10 @@ dist/
|
||||
/tools
|
||||
/tmp
|
||||
/state.json
|
||||
/emissary.sqlite
|
||||
/emissary.sqlite*
|
||||
/.gitea-release
|
||||
/agent-key.json
|
||||
/apps
|
||||
/server-key.json
|
||||
/.emissary-token
|
||||
/.emissary-token
|
||||
/out
|
85
Jenkinsfile
vendored
Normal file
85
Jenkinsfile
vendored
Normal file
@ -0,0 +1,85 @@
|
||||
@Library('cadoles') _
|
||||
|
||||
pipeline {
|
||||
agent {
|
||||
dockerfile {
|
||||
label 'docker'
|
||||
filename 'Dockerfile'
|
||||
dir 'misc/jenkins'
|
||||
}
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('Cancel older jobs') {
|
||||
steps {
|
||||
script {
|
||||
def buildNumber = env.BUILD_NUMBER as int
|
||||
if (buildNumber > 1) milestone(buildNumber - 1)
|
||||
milestone(buildNumber)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Run unit tests') {
|
||||
steps {
|
||||
script {
|
||||
withCredentials([
|
||||
usernamePassword([
|
||||
credentialsId: 'forge-jenkins',
|
||||
usernameVariable: 'GIT_USERNAME',
|
||||
passwordVariable: 'GIT_PASSWORD'
|
||||
])
|
||||
]) {
|
||||
sh '''
|
||||
git config --global credential.https://forge.cadoles.com.username "$GIT_USERNAME"
|
||||
git config --global credential.https://forge.cadoles.com.helper '!f() { test "$1" = get && echo "password=$GIT_PASSWORD"; }; f'
|
||||
|
||||
export GOPRIVATE=forge.cadoles.com/arcad/edge
|
||||
make test
|
||||
'''
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Release') {
|
||||
when {
|
||||
anyOf {
|
||||
branch 'master'
|
||||
branch 'develop'
|
||||
}
|
||||
}
|
||||
steps {
|
||||
script {
|
||||
withCredentials([
|
||||
usernamePassword([
|
||||
credentialsId: 'forge-jenkins',
|
||||
usernameVariable: 'GITEA_RELEASE_USERNAME',
|
||||
passwordVariable: 'GITEA_RELEASE_PASSWORD'
|
||||
])
|
||||
]) {
|
||||
sh 'make gitea-release'
|
||||
}
|
||||
def currentVersion = sh(returnStdout: true, script: 'make full-version').trim()
|
||||
if (currentVersion.endsWith('-dirty')) {
|
||||
unstable('Could not trigger emissary-firmware build, dirty version !')
|
||||
} else {
|
||||
build(
|
||||
job: "../emissary-firmware/${env.GIT_BRANCH}",
|
||||
parameters: [
|
||||
[$class: 'StringParameterValue', name: 'emissaryRelease', value: currentVersion]
|
||||
],
|
||||
wait: false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post {
|
||||
always {
|
||||
cleanWs()
|
||||
}
|
||||
}
|
||||
}
|
23
Makefile
23
Makefile
@ -135,7 +135,7 @@ gitea-release: tools/gitea-release/bin/gitea-release.sh goreleaser
|
||||
GITEA_RELEASE_COMMITISH_TARGET="$(GIT_VERSION)" \
|
||||
GITEA_RELEASE_IS_DRAFT="false" \
|
||||
GITEA_RELEASE_BODY="" \
|
||||
GITEA_RELEASE_ATTACHMENTS="$(shell find .gitea-release/* -type f)" \
|
||||
GITEA_RELEASE_ATTACHMENTS="$$(find .gitea-release/* -type f)" \
|
||||
tools/gitea-release/bin/gitea-release.sh
|
||||
|
||||
tools/gitea-release/bin/gitea-release.sh:
|
||||
@ -144,4 +144,23 @@ tools/gitea-release/bin/gitea-release.sh:
|
||||
chmod +x tools/gitea-release/bin/gitea-release.sh
|
||||
|
||||
.emissary-token:
|
||||
$(MAKE) run-emissary-server EMISSARY_CMD="--debug --config tmp/server.yml server auth create-token --role writer > .emissary-token"
|
||||
$(MAKE) run-emissary-server EMISSARY_CMD="--debug --config tmp/server.yml server auth create-token --role writer > .emissary-token"
|
||||
|
||||
AGENT_ID ?= 1
|
||||
|
||||
load-sample-specs:
|
||||
cat misc/spec-samples/app.emissary.cadoles.com.json | ./bin/server api agent spec update -a $(AGENT_ID) --no-patch --spec-data - --spec-name app.emissary.cadoles.com
|
||||
cat misc/spec-samples/proxy.emissary.cadoles.com.json | ./bin/server api agent spec update -a $(AGENT_ID) --no-patch --spec-data - --spec-name proxy.emissary.cadoles.com
|
||||
cat misc/spec-samples/mdns.emissary.cadoles.com.json | ./bin/server api agent spec update -a $(AGENT_ID) --no-patch --spec-data - --spec-name mdns.emissary.cadoles.com
|
||||
|
||||
full-version:
|
||||
@echo $(FULL_VERSION)
|
||||
|
||||
update-edge-lib:
|
||||
git pull --rebase
|
||||
GOPROXY=direct GOPRIVATE=forge.cadoles.com/arcad/edge go get -u forge.cadoles.com/arcad/edge
|
||||
go mod tidy
|
||||
$(MAKE) test
|
||||
git add go.mod go.sum
|
||||
git commit -m "feat: update arcad/edge dependency"
|
||||
git push
|
@ -1,3 +1,7 @@
|
||||
<div style="text-align:center">
|
||||
<img style="width:250px" src="./misc/resources/logo.svg" />
|
||||
</div>
|
||||
|
||||
# Emissary
|
||||
|
||||
Control plane for "edge" (and OpenWRT-based) devices.
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/command/agent"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/command/api"
|
||||
|
||||
_ "forge.cadoles.com/Cadoles/emissary/internal/imports/format"
|
||||
_ "forge.cadoles.com/Cadoles/emissary/internal/imports/spec"
|
||||
)
|
||||
|
||||
|
@ -1,20 +1,22 @@
|
||||
# Documentation
|
||||
|
||||
- (FR) - [Introduction](./fr/introduction.md)
|
||||
|
||||
## Tutorials
|
||||
|
||||
- (FR) - [Premiers pas](./tutorials/fr/first-steps.md)
|
||||
- (FR) - [Déployer un serveur mandataire inverse sur un agent](./tutorials/fr/deploy-reverse-proxy.md)
|
||||
- (FR) - [Déployer une configuration UCI personnalisée sur un agent](./tutorials/fr/deploy-uci-configuration.md)
|
||||
|
||||
## References
|
||||
|
||||
### API
|
||||
### Specifications
|
||||
|
||||
[See `misc/rest/server.rest`](../misc/rest/server.rest)
|
||||
|
||||
### Spécifications
|
||||
|
||||
- [Schéma `app.emissary.cadoles.com`](../internal/spec/app/schema.json)
|
||||
- [Schéma `app.emissary.cadoles.com`](../internal/agent/controller/app/spec/schema.json)
|
||||
- [Schéma `proxy.emissary.cadoles.com`](../internal/spec/proxy/schema.json)
|
||||
- [Schéma `mdns.emissary.cadoles.com`](../internal/agent/controller/mdns/spec/schema.json)
|
||||
- [Schéma `uci.emissary.cadoles.com`](../internal/spec/uci/schema.json)
|
||||
- [Schéma `gateway.emissary.cadoles.com`](../internal/spec/gateway/schema.json)
|
||||
- [Schéma `sysupgrade.openwrt.emissary.cadoles.com`](../internal/agent/controller/openwrt/spec/sysupgrade/schema.json)
|
||||
|
||||
### Configuration
|
||||
|
||||
|
30
doc/fr/introduction.md
Normal file
30
doc/fr/introduction.md
Normal file
@ -0,0 +1,30 @@
|
||||
# Introduction
|
||||
|
||||
"Emissary" est un programme entrant dans la catégorie des outils de gestion et déploiement de configuration.
|
||||
|
||||
En utilisant un agent déployé sur chaque système cible, il permet aux administrateurs système de centraliser le contrôle et la supervision de la configuration. Grâce à ses fonctionnalités avancées, il est capable de faire converger la configuration d'une machine vers un modèle précis défini par une ou plusieurs spécifications centralisées sur un serveur de pilotage dédié.
|
||||
|
||||
Le principal atout d'"Emissary" réside dans sa capacité à activer des "contrôleurs" spécifiques pour chaque aspect de la configuration système. Ces contrôleurs sont des modules intelligents qui agissent comme des agents spécialisés, veillant à ce que les paramètres de configuration soient correctement appliqués et respectent les spécifications définies.
|
||||
|
||||
Grâce à cette approche modulaire, "Emissary" peut gérer diverses facettes de la configuration, telles que les paramètres réseau, les règles de sécurité, les options de performance et bien plus encore. Chaque contrôleur est conçu pour répondre à des besoins spécifiques, offrant ainsi une flexibilité et une granularité optimales dans la gestion de la configuration.
|
||||
|
||||
Certains contrôleurs permettent également l'exécution de services spécialisés comme des serveurs mandataires inverses ou des applications web autonomes.
|
||||
|
||||
L'utilisation d'un serveur de pilotage centralisé permet à "Emissary" de stocker et de mettre à jour les spécifications de configuration de manière cohérente. Les administrateurs peuvent définir des modèles de configuration précis, les affiner au fil du temps et les appliquer en un seul clic sur l'ensemble du parc de machines gérées. Cela garantit une uniformité et une conformité accrues, tout en facilitant la maintenance et les mises à jour à grande échelle.
|
||||
|
||||
À l'heure actuelle, Emissary est conçu pour cibler spécifiquement le système d'exploitation OpenWRT. L'activation des "contrôleurs" spécifiques à cet OS permet de converger la configuration de la machine OpenWRT vers un modèle correspondant aux spécifications centralisées sur le serveur de pilotage. Ces spécifications peuvent inclure des paramètres réseau, des configurations de sécurité, des règles de pare-feu, des options de routage, des services système, et bien d'autres éléments spécifiques à OpenWRT.
|
||||
|
||||
## Vue d'ensemble de l'architecture
|
||||
|
||||

|
||||
|
||||
|
||||
## Contrôleurs
|
||||
|
||||
Voici la liste des contrôleurs implémentés à ce jour:
|
||||
|
||||
- **Contrôleur UCI** - Permet de modifier les données [UCI](https://openwrt.org/docs/guide-user/base-system/uci) (**U**nified **C**onfiguration **S**ystem) d'un système OpenWRT et ainsi configurer les services systèmes, les règles pare-feu, la configuration des NICs, etc sur celui-ci.
|
||||
- **Contrôleur SysUpgrade** - Permet de mettre à jour un système OpenWRT via l'outil [`sysupgrade`](https://openwrt.org/docs/guide-user/installation/generic.sysupgrade).
|
||||
- **Contrôleur Proxy** - Permet de déployer des services de type passerelle mandataire inverse ("reverse proxy") sur la machine cible.
|
||||
- **Contrôleur mDNS** - Permet d'annoncer des services via mDNS sur les différents réseaux de la machine cible.
|
||||
- **Contrôleur App** - Permet de déployer des applications web "embarquées" (s'exécutant localement et non dépendantes d'une connectivité internet) sur la machine cible. Voir le projet ["Edge App"](https://forge.cadoles.com/arcad/edge).
|
59
doc/fr/resources/overview.plantuml
Normal file
59
doc/fr/resources/overview.plantuml
Normal file
@ -0,0 +1,59 @@
|
||||
@startuml
|
||||
top to bottom direction
|
||||
skinparam linetype ortho
|
||||
|
||||
node PilotNode as "Pilot Node" {
|
||||
database DataStore as "Data Store"
|
||||
|
||||
component EmissaryServer as "Emissary Server" {
|
||||
|
||||
component SpecificationRegistry as "Specification Registry" {
|
||||
component UCISpecification as "UCI Spec"
|
||||
component MDNSSpecification as "mDNS Spec"
|
||||
component AppSpecification as "App Spec"
|
||||
component ProxySpecification as "Proxy Spec"
|
||||
component SysUpgradeSpecification as "SysUpgrade Spec"
|
||||
}
|
||||
|
||||
component HTTPHandler as "HTTP Handler"
|
||||
|
||||
HTTPHandler .down.> SpecificationRegistry: validates agents data with
|
||||
|
||||
HTTPHandler .right.> DataStore: saves agent data in
|
||||
}
|
||||
}
|
||||
|
||||
node OperatorNode as "Operator Node" {
|
||||
component EmissaryClient as "Emissary Client"
|
||||
|
||||
EmissaryClient -left-> HTTPHandler: administrates
|
||||
}
|
||||
|
||||
node OpenWRTNode as "OpenWRT Node" {
|
||||
component EmissaryAgent as "Emissary Agent" {
|
||||
|
||||
component StateManager as "State Manager"
|
||||
|
||||
StateManager --up-> HTTPHandler: fetches agent ^*specs from
|
||||
|
||||
component UCIController as "UCI Controller"
|
||||
|
||||
UCIController .up.> StateManager: reconciles with
|
||||
|
||||
component SysUpgradeController as "SysUpgrade Controller"
|
||||
|
||||
SysUpgradeController .up.> StateManager: reconciles with
|
||||
|
||||
component ProxyController as "Proxy Controller"
|
||||
|
||||
ProxyController .up.> StateManager: reconciles with
|
||||
|
||||
component MDNSController as "mDNS Controller"
|
||||
|
||||
MDNSController .up.> StateManager: reconciles with
|
||||
|
||||
component AppController as "App Controller"
|
||||
|
||||
AppController .up.> StateManager: reconciles with
|
||||
}
|
||||
}
|
99
doc/fr/resources/overview.svg
Normal file
99
doc/fr/resources/overview.svg
Normal file
@ -0,0 +1,99 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" contentStyleType="text/css" height="643px" preserveAspectRatio="none" style="width:1713px;height:643px;background:#FFFFFF;" version="1.1" viewBox="0 0 1713 643" width="1713px" zoomAndPan="magnify"><defs/><g><!--MD5=[d09f24f3d7c03358bd8c02f81fe1cb3f]
|
||||
cluster PilotNode--><g id="cluster_PilotNode"><polygon fill="none" points="16,16,26,6,685,6,685,511,675,521,16,521,16,16" style="stroke:#181818;stroke-width:1.0;"/><line style="stroke:#181818;stroke-width:1.0;" x1="675" x2="685" y1="16" y2="6"/><line style="stroke:#181818;stroke-width:1.0;" x1="16" x2="675" y1="16" y2="16"/><line style="stroke:#181818;stroke-width:1.0;" x1="675" x2="675" y1="16" y2="521"/><text fill="#000000" font-family="sans-serif" font-size="14" font-weight="bold" lengthAdjust="spacing" textLength="73" x="310" y="33.9659">Pilot Node</text></g><!--MD5=[9c6b5fd9fe3a3a3c784efc27685ccdf9]
|
||||
cluster EmissaryServer--><g id="cluster_EmissaryServer"><rect fill="none" height="440" rx="2.5" ry="2.5" style="stroke:#181818;stroke-width:1.0;" width="523" x="138" y="57"/><rect fill="none" height="10" style="stroke:#181818;stroke-width:1.0;" width="15" x="641" y="62"/><rect fill="none" height="2" style="stroke:#181818;stroke-width:1.0;" width="4" x="639" y="64"/><rect fill="none" height="2" style="stroke:#181818;stroke-width:1.0;" width="4" x="639" y="68"/><text fill="#000000" font-family="sans-serif" font-size="14" font-weight="bold" lengthAdjust="spacing" textLength="110" x="344.5" y="84.9659">Emissary Server</text></g><!--MD5=[5f6297313bdca82dad0981382bb4d88a]
|
||||
cluster SpecificationRegistry--><g id="cluster_SpecificationRegistry"><rect fill="none" height="273" rx="2.5" ry="2.5" style="stroke:#181818;stroke-width:1.0;" width="459" x="170" y="192"/><rect fill="none" height="10" style="stroke:#181818;stroke-width:1.0;" width="15" x="609" y="197"/><rect fill="none" height="2" style="stroke:#181818;stroke-width:1.0;" width="4" x="607" y="199"/><rect fill="none" height="2" style="stroke:#181818;stroke-width:1.0;" width="4" x="607" y="203"/><text fill="#000000" font-family="sans-serif" font-size="14" font-weight="bold" lengthAdjust="spacing" textLength="149" x="325" y="219.9659">Specification Registry</text></g><!--MD5=[b562d696a455f482404b155c6a8fbfca]
|
||||
cluster OperatorNode--><g id="cluster_OperatorNode"><polygon fill="none" points="709,62,719,52,889,52,889,150,879,160,709,160,709,62" style="stroke:#181818;stroke-width:1.0;"/><line style="stroke:#181818;stroke-width:1.0;" x1="879" x2="889" y1="62" y2="52"/><line style="stroke:#181818;stroke-width:1.0;" x1="709" x2="879" y1="62" y2="62"/><line style="stroke:#181818;stroke-width:1.0;" x1="879" x2="879" y1="62" y2="160"/><text fill="#000000" font-family="sans-serif" font-size="14" font-weight="bold" lengthAdjust="spacing" textLength="104" x="743" y="79.9659">Operator Node</text></g><!--MD5=[68861f6d3d90d2f41bc4d4a2796fc73e]
|
||||
cluster OpenWRTNode--><g id="cluster_OpenWRTNode"><polygon fill="none" points="709,313,719,303,1696,303,1696,616,1686,626,709,626,709,313" style="stroke:#181818;stroke-width:1.0;"/><line style="stroke:#181818;stroke-width:1.0;" x1="1686" x2="1696" y1="313" y2="303"/><line style="stroke:#181818;stroke-width:1.0;" x1="709" x2="1686" y1="313" y2="313"/><line style="stroke:#181818;stroke-width:1.0;" x1="1686" x2="1686" y1="313" y2="626"/><text fill="#000000" font-family="sans-serif" font-size="14" font-weight="bold" lengthAdjust="spacing" textLength="109" x="1144" y="330.9659">OpenWRT Node</text></g><!--MD5=[6e6320f5227e3e26302b14a131b17aa5]
|
||||
cluster EmissaryAgent--><g id="cluster_EmissaryAgent"><rect fill="none" height="248" rx="2.5" ry="2.5" style="stroke:#181818;stroke-width:1.0;" width="939" x="733" y="354"/><rect fill="none" height="10" style="stroke:#181818;stroke-width:1.0;" width="15" x="1652" y="359"/><rect fill="none" height="2" style="stroke:#181818;stroke-width:1.0;" width="4" x="1650" y="361"/><rect fill="none" height="2" style="stroke:#181818;stroke-width:1.0;" width="4" x="1650" y="365"/><text fill="#000000" font-family="sans-serif" font-size="14" font-weight="bold" lengthAdjust="spacing" textLength="108" x="1148.5" y="381.9659">Emissary Agent</text></g><!--MD5=[45eee4c5a57edb1e2ac175c76a239d17]
|
||||
entity DataStore--><g id="elem_DataStore"><path d="M32,105.5 C32,95.5 77,95.5 77,95.5 C77,95.5 122,95.5 122,105.5 L122,133.5679 C122,143.5679 77,143.5679 77,143.5679 C77,143.5679 32,143.5679 32,133.5679 L32,105.5 " fill="#F1F1F1" style="stroke:#181818;stroke-width:0.5;"/><path d="M32,105.5 C32,115.5 77,115.5 77,115.5 C77,115.5 122,115.5 122,105.5 " fill="none" style="stroke:#181818;stroke-width:0.5;"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="70" x="42" y="134.4659">Data Store</text></g><!--MD5=[7d2b259075cd0e421afb7965bd22532b]
|
||||
entity HTTPHandler--><g id="elem_HTTPHandler"><rect fill="#F1F1F1" height="49.0679" rx="2.5" ry="2.5" style="stroke:#181818;stroke-width:0.5;" width="132" x="335" y="95"/><rect fill="#F1F1F1" height="10" style="stroke:#181818;stroke-width:0.5;" width="15" x="447" y="100"/><rect fill="#F1F1F1" height="2" style="stroke:#181818;stroke-width:0.5;" width="4" x="445" y="102"/><rect fill="#F1F1F1" height="2" style="stroke:#181818;stroke-width:0.5;" width="4" x="445" y="106"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="92" x="350" y="129.9659">HTTP Handler</text></g><!--MD5=[d74c349cfc963885f78088443cb132a3]
|
||||
entity UCISpecification--><g id="elem_UCISpecification"><rect fill="#F1F1F1" height="49.0679" rx="2.5" ry="2.5" style="stroke:#181818;stroke-width:0.5;" width="100" x="213" y="238"/><rect fill="#F1F1F1" height="10" style="stroke:#181818;stroke-width:0.5;" width="15" x="293" y="243"/><rect fill="#F1F1F1" height="2" style="stroke:#181818;stroke-width:0.5;" width="4" x="291" y="245"/><rect fill="#F1F1F1" height="2" style="stroke:#181818;stroke-width:0.5;" width="4" x="291" y="249"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="60" x="228" y="272.9659">UCI Spec</text></g><!--MD5=[631d6ad5bad1f198f42ccf56fafe0582]
|
||||
entity MDNSSpecification--><g id="elem_MDNSSpecification"><rect fill="#F1F1F1" height="49.0679" rx="2.5" ry="2.5" style="stroke:#181818;stroke-width:0.5;" width="118" x="348" y="238"/><rect fill="#F1F1F1" height="10" style="stroke:#181818;stroke-width:0.5;" width="15" x="446" y="243"/><rect fill="#F1F1F1" height="2" style="stroke:#181818;stroke-width:0.5;" width="4" x="444" y="245"/><rect fill="#F1F1F1" height="2" style="stroke:#181818;stroke-width:0.5;" width="4" x="444" y="249"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="78" x="363" y="272.9659">mDNS Spec</text></g><!--MD5=[f8753067470155b04e2f3a693924c320]
|
||||
entity AppSpecification--><g id="elem_AppSpecification"><rect fill="#F1F1F1" height="49.0679" rx="2.5" ry="2.5" style="stroke:#181818;stroke-width:0.5;" width="103" x="501.5" y="238"/><rect fill="#F1F1F1" height="10" style="stroke:#181818;stroke-width:0.5;" width="15" x="584.5" y="243"/><rect fill="#F1F1F1" height="2" style="stroke:#181818;stroke-width:0.5;" width="4" x="582.5" y="245"/><rect fill="#F1F1F1" height="2" style="stroke:#181818;stroke-width:0.5;" width="4" x="582.5" y="249"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="63" x="516.5" y="272.9659">App Spec</text></g><!--MD5=[fd240f711946cd5d0dcadb1ea2ed786c]
|
||||
entity ProxySpecification--><g id="elem_ProxySpecification"><rect fill="#F1F1F1" height="49.0679" rx="2.5" ry="2.5" style="stroke:#181818;stroke-width:0.5;" width="112" x="213" y="392"/><rect fill="#F1F1F1" height="10" style="stroke:#181818;stroke-width:0.5;" width="15" x="305" y="397"/><rect fill="#F1F1F1" height="2" style="stroke:#181818;stroke-width:0.5;" width="4" x="303" y="399"/><rect fill="#F1F1F1" height="2" style="stroke:#181818;stroke-width:0.5;" width="4" x="303" y="403"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="72" x="228" y="426.9659">Proxy Spec</text></g><!--MD5=[74aafaf76d366e174271de99960a7b8d]
|
||||
entity SysUpgradeSpecification--><g id="elem_SysUpgradeSpecification"><rect fill="#F1F1F1" height="49.0679" rx="2.5" ry="2.5" style="stroke:#181818;stroke-width:0.5;" width="157" x="360.5" y="392"/><rect fill="#F1F1F1" height="10" style="stroke:#181818;stroke-width:0.5;" width="15" x="497.5" y="397"/><rect fill="#F1F1F1" height="2" style="stroke:#181818;stroke-width:0.5;" width="4" x="495.5" y="399"/><rect fill="#F1F1F1" height="2" style="stroke:#181818;stroke-width:0.5;" width="4" x="495.5" y="403"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="117" x="375.5" y="426.9659">SysUpgrade Spec</text></g><!--MD5=[584b4e495bc4cb9e5d46ff66335fc219]
|
||||
entity EmissaryClient--><g id="elem_EmissaryClient"><rect fill="#F1F1F1" height="49.0679" rx="2.5" ry="2.5" style="stroke:#181818;stroke-width:0.5;" width="143" x="725.5" y="95"/><rect fill="#F1F1F1" height="10" style="stroke:#181818;stroke-width:0.5;" width="15" x="848.5" y="100"/><rect fill="#F1F1F1" height="2" style="stroke:#181818;stroke-width:0.5;" width="4" x="846.5" y="102"/><rect fill="#F1F1F1" height="2" style="stroke:#181818;stroke-width:0.5;" width="4" x="846.5" y="106"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="103" x="740.5" y="129.9659">Emissary Client</text></g><!--MD5=[cbe48146c9698f81ea53c2c6f51c8eda]
|
||||
entity StateManager--><g id="elem_StateManager"><rect fill="#F1F1F1" height="49.0679" rx="2.5" ry="2.5" style="stroke:#181818;stroke-width:0.5;" width="139" x="1048.5" y="392"/><rect fill="#F1F1F1" height="10" style="stroke:#181818;stroke-width:0.5;" width="15" x="1167.5" y="397"/><rect fill="#F1F1F1" height="2" style="stroke:#181818;stroke-width:0.5;" width="4" x="1165.5" y="399"/><rect fill="#F1F1F1" height="2" style="stroke:#181818;stroke-width:0.5;" width="4" x="1165.5" y="403"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="99" x="1063.5" y="426.9659">State Manager</text></g><!--MD5=[27f8877b35bcf78d2c9b0e363caea569]
|
||||
entity UCIController--><g id="elem_UCIController"><rect fill="#F1F1F1" height="49.0679" rx="2.5" ry="2.5" style="stroke:#181818;stroke-width:0.5;" width="135" x="749.5" y="537"/><rect fill="#F1F1F1" height="10" style="stroke:#181818;stroke-width:0.5;" width="15" x="864.5" y="542"/><rect fill="#F1F1F1" height="2" style="stroke:#181818;stroke-width:0.5;" width="4" x="862.5" y="544"/><rect fill="#F1F1F1" height="2" style="stroke:#181818;stroke-width:0.5;" width="4" x="862.5" y="548"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="95" x="764.5" y="571.9659">UCI Controller</text></g><!--MD5=[f5a45e51cb66ff1d3b5626d0df038fee]
|
||||
entity SysUpgradeController--><g id="elem_SysUpgradeController"><rect fill="#F1F1F1" height="49.0679" rx="2.5" ry="2.5" style="stroke:#181818;stroke-width:0.5;" width="192" x="920" y="537"/><rect fill="#F1F1F1" height="10" style="stroke:#181818;stroke-width:0.5;" width="15" x="1092" y="542"/><rect fill="#F1F1F1" height="2" style="stroke:#181818;stroke-width:0.5;" width="4" x="1090" y="544"/><rect fill="#F1F1F1" height="2" style="stroke:#181818;stroke-width:0.5;" width="4" x="1090" y="548"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="152" x="935" y="571.9659">SysUpgrade Controller</text></g><!--MD5=[ed1f476319cb2bbabd1b988180210f61]
|
||||
entity ProxyController--><g id="elem_ProxyController"><rect fill="#F1F1F1" height="49.0679" rx="2.5" ry="2.5" style="stroke:#181818;stroke-width:0.5;" width="147" x="1147.5" y="537"/><rect fill="#F1F1F1" height="10" style="stroke:#181818;stroke-width:0.5;" width="15" x="1274.5" y="542"/><rect fill="#F1F1F1" height="2" style="stroke:#181818;stroke-width:0.5;" width="4" x="1272.5" y="544"/><rect fill="#F1F1F1" height="2" style="stroke:#181818;stroke-width:0.5;" width="4" x="1272.5" y="548"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="107" x="1162.5" y="571.9659">Proxy Controller</text></g><!--MD5=[dcaaaabc13b59746f8f74cc6285a228b]
|
||||
entity MDNSController--><g id="elem_MDNSController"><rect fill="#F1F1F1" height="49.0679" rx="2.5" ry="2.5" style="stroke:#181818;stroke-width:0.5;" width="153" x="1329.5" y="537"/><rect fill="#F1F1F1" height="10" style="stroke:#181818;stroke-width:0.5;" width="15" x="1462.5" y="542"/><rect fill="#F1F1F1" height="2" style="stroke:#181818;stroke-width:0.5;" width="4" x="1460.5" y="544"/><rect fill="#F1F1F1" height="2" style="stroke:#181818;stroke-width:0.5;" width="4" x="1460.5" y="548"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="113" x="1344.5" y="571.9659">mDNS Controller</text></g><!--MD5=[f7cab0dbd7f354492deaa54be612571f]
|
||||
entity AppController--><g id="elem_AppController"><rect fill="#F1F1F1" height="49.0679" rx="2.5" ry="2.5" style="stroke:#181818;stroke-width:0.5;" width="138" x="1518" y="537"/><rect fill="#F1F1F1" height="10" style="stroke:#181818;stroke-width:0.5;" width="15" x="1636" y="542"/><rect fill="#F1F1F1" height="2" style="stroke:#181818;stroke-width:0.5;" width="4" x="1634" y="544"/><rect fill="#F1F1F1" height="2" style="stroke:#181818;stroke-width:0.5;" width="4" x="1634" y="548"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="98" x="1533" y="571.9659">App Controller</text></g><!--MD5=[8c3501b26c9c3ea39952224ab3fba557]
|
||||
link HTTPHandler to SpecificationRegistry--><g id="link_HTTPHandler_SpecificationRegistry"><path d="M334.7,128 C268.96,128 178,128 178,128 C178,128 178,158.5475 178,190.5263 C178,190.7761 178,191.026 178,191.276 " fill="none" id="HTTPHandler-to-SpecificationRegistry" style="stroke:#181818;stroke-width:1.0;stroke-dasharray:7.0,7.0;"/><polygon fill="#181818" points="178,191.276,182,182.276,178,186.276,174,182.276,178,191.276" style="stroke:#181818;stroke-width:1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="156" x="151.03" y="142.897">validates agents data with</text></g><!--MD5=[1442fca2dc2f53bf9ae23d9e844c6c8b]
|
||||
link HTTPHandler to DataStore--><g id="link_HTTPHandler_DataStore"><path d="M334.65,112 C334.65,112 128.41,112 128.41,112 " fill="none" id="HTTPHandler-to-DataStore" style="stroke:#181818;stroke-width:1.0;stroke-dasharray:7.0,7.0;"/><polygon fill="#181818" points="123.41,112,132.41,116,128.41,112,132.41,108,123.41,112" style="stroke:#181818;stroke-width:1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="115" x="203.28" y="126.897">saves agent data in</text></g><!--MD5=[04b946759b53ef2d0699bda61eed296c]
|
||||
reverse link HTTPHandler to EmissaryClient--><g id="link_HTTPHandler_EmissaryClient"><path d="M473.28,112 C473.28,112 725.14,112 725.14,112 " fill="none" id="HTTPHandler-backto-EmissaryClient" style="stroke:#181818;stroke-width:1.0;"/><polygon fill="#181818" points="468.28,112,477.28,116,473.28,112,477.28,108,468.28,112" style="stroke:#181818;stroke-width:1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="82" x="516.21" y="107.897">administrates</text></g><!--MD5=[0dd7156c6052ea784bc88dfca61e007b]
|
||||
reverse link HTTPHandler to StateManager--><g id="link_HTTPHandler_StateManager"><path d="M473.28,128 C473.28,128 665,128 665,128 C665,128 665,409 665,409 C665,409 921.14,409 1048.21,409 " fill="none" id="HTTPHandler-backto-StateManager" style="stroke:#181818;stroke-width:1.0;"/><polygon fill="#181818" points="468.28,128,477.28,132,473.28,128,477.28,124,468.28,128" style="stroke:#181818;stroke-width:1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="163" x="501" y="360.137">fetches agent ^*specs from</text></g><!--MD5=[beea302d68a5d9dc6027ab4e0b987cea]
|
||||
reverse link StateManager to UCIController--><g id="link_StateManager_UCIController"><path d="M1042.15,425 C1042.15,425 876.5,425 876.5,425 C876.5,425 876.5,497.45 876.5,536.78 " fill="none" id="StateManager-backto-UCIController" style="stroke:#181818;stroke-width:1.0;stroke-dasharray:7.0,7.0;"/><polygon fill="#181818" points="1047.15,425,1038.15,421,1042.15,425,1038.15,429,1047.15,425" style="stroke:#181818;stroke-width:1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="88" x="814.43" y="420.897">reconciles with</text></g><!--MD5=[8cf138a1950decb1251fc88353171770]
|
||||
reverse link StateManager to SysUpgradeController--><g id="link_StateManager_SysUpgradeController"><path d="M1080.25,447.43 C1080.25,447.43 1080.25,536.9 1080.25,536.9 " fill="none" id="StateManager-backto-SysUpgradeController" style="stroke:#181818;stroke-width:1.0;stroke-dasharray:7.0,7.0;"/><polygon fill="#181818" points="1080.25,442.43,1076.25,451.43,1080.25,447.43,1084.25,451.43,1080.25,442.43" style="stroke:#181818;stroke-width:1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="88" x="991.25" y="488.057">reconciles with</text></g><!--MD5=[8dba0e44c0268b5c6a2c1c6565c41b8b]
|
||||
reverse link StateManager to ProxyController--><g id="link_StateManager_ProxyController"><path d="M1167.5,447.43 C1167.5,447.43 1167.5,536.9 1167.5,536.9 " fill="none" id="StateManager-backto-ProxyController" style="stroke:#181818;stroke-width:1.0;stroke-dasharray:7.0,7.0;"/><polygon fill="#181818" points="1167.5,442.43,1163.5,451.43,1167.5,447.43,1171.5,451.43,1167.5,442.43" style="stroke:#181818;stroke-width:1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="88" x="1078.5" y="507.057">reconciles with</text></g><!--MD5=[94df0ade1dc92be8853ce902c4f83dad]
|
||||
reverse link StateManager to MDNSController--><g id="link_StateManager_MDNSController"><path d="M1193.72,425 C1193.72,425 1406,425 1406,425 C1406,425 1406,497.45 1406,536.78 " fill="none" id="StateManager-backto-MDNSController" style="stroke:#181818;stroke-width:1.0;stroke-dasharray:7.0,7.0;"/><polygon fill="#181818" points="1188.72,425,1197.72,429,1193.72,425,1197.72,421,1188.72,425" style="stroke:#181818;stroke-width:1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="88" x="1266.75" y="420.897">reconciles with</text></g><!--MD5=[e74434bc519cba41fdddc7c857699717]
|
||||
reverse link StateManager to AppController--><g id="link_StateManager_AppController"><path d="M1193.96,409 C1193.96,409 1587,409 1587,409 C1587,409 1587,493.45 1587,536.66 " fill="none" id="StateManager-backto-AppController" style="stroke:#181818;stroke-width:1.0;stroke-dasharray:7.0,7.0;"/><polygon fill="#181818" points="1188.96,409,1197.96,413,1193.96,409,1197.96,405,1188.96,409" style="stroke:#181818;stroke-width:1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="88" x="1365.31" y="404.897">reconciles with</text></g><!--MD5=[6fc50b732d8962c26a79ff20e99a40e3]
|
||||
@startuml
|
||||
top to bottom direction
|
||||
skinparam linetype ortho
|
||||
|
||||
node PilotNode as "Pilot Node" {
|
||||
database DataStore as "Data Store"
|
||||
|
||||
component EmissaryServer as "Emissary Server" {
|
||||
|
||||
component SpecificationRegistry as "Specification Registry" {
|
||||
component UCISpecification as "UCI Spec"
|
||||
component MDNSSpecification as "mDNS Spec"
|
||||
component AppSpecification as "App Spec"
|
||||
component ProxySpecification as "Proxy Spec"
|
||||
component SysUpgradeSpecification as "SysUpgrade Spec"
|
||||
}
|
||||
|
||||
component HTTPHandler as "HTTP Handler"
|
||||
|
||||
HTTPHandler .down.> SpecificationRegistry: validates agents data with
|
||||
|
||||
HTTPHandler .right.> DataStore: saves agent data in
|
||||
}
|
||||
}
|
||||
|
||||
node OperatorNode as "Operator Node" {
|
||||
component EmissaryClient as "Emissary Client"
|
||||
|
||||
EmissaryClient -left-> HTTPHandler: administrates
|
||||
}
|
||||
|
||||
node OpenWRTNode as "OpenWRT Node" {
|
||||
component EmissaryAgent as "Emissary Agent" {
|
||||
|
||||
component StateManager as "State Manager"
|
||||
|
||||
StateManager - -up-> HTTPHandler: fetches agent ^*specs from
|
||||
|
||||
component UCIController as "UCI Controller"
|
||||
|
||||
UCIController .up.> StateManager: reconciles with
|
||||
|
||||
component SysUpgradeController as "SysUpgrade Controller"
|
||||
|
||||
SysUpgradeController .up.> StateManager: reconciles with
|
||||
|
||||
component ProxyController as "Proxy Controller"
|
||||
|
||||
ProxyController .up.> StateManager: reconciles with
|
||||
|
||||
component MDNSController as "mDNS Controller"
|
||||
|
||||
MDNSController .up.> StateManager: reconciles with
|
||||
|
||||
component AppController as "App Controller"
|
||||
|
||||
AppController .up.> StateManager: reconciles with
|
||||
}
|
||||
}
|
||||
@end
|
||||
|
||||
PlantUML version 1.2022.7(Mon Aug 22 19:01:30 CEST 2022)
|
||||
(GPL source distribution)
|
||||
Java Runtime: OpenJDK Runtime Environment
|
||||
JVM: OpenJDK 64-Bit Server VM
|
||||
Default Encoding: UTF-8
|
||||
Language: fr
|
||||
Country: FR
|
||||
--></g></svg>
|
After Width: | Height: | Size: 21 KiB |
3
doc/tutorials/fr/deploy-reverse-proxy.md
Normal file
3
doc/tutorials/fr/deploy-reverse-proxy.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Déployer un serveur mandataire inverse sur un agent
|
||||
|
||||
> TODO
|
130
doc/tutorials/fr/deploy-uci-configuration.md
Normal file
130
doc/tutorials/fr/deploy-uci-configuration.md
Normal file
@ -0,0 +1,130 @@
|
||||
# Déployer une configuration UCI personnalisée sur un agent
|
||||
|
||||
Via la spécification [`uci.emissary.cadoles.com`](../../../internal/spec/uci/schema.json) il est possible de configurer un agent avec un système OpenWRT. Dans ce tutoriel nous verrons:
|
||||
|
||||
- Comment exporter une configuration UCI existante au format attendu par Emissary;
|
||||
- Comment modifier la spécification d'un agent Emissary pour mettre à jour sa configuration via le serveur de pilotage.
|
||||
|
||||
## Étapes
|
||||
|
||||
### Identifier l'empreinte de votre agent
|
||||
|
||||
1. Sur la machine agent, utiliser la commande intégrée pour récupérer l'empreinte ("thumbprint") identifiant l'agent:
|
||||
|
||||
```
|
||||
emissary agent show-thumbprint
|
||||
```
|
||||
|
||||
**Noter la valeur retournée. Elle sera utilisée dans les étapes suivantes.**
|
||||
|
||||
### Exporter la configuration UCI de votre agent au format Emissary
|
||||
|
||||
1. Se connecter en SSH sur votre agent Emissary:
|
||||
|
||||
```
|
||||
ssh root@<agent_ip>
|
||||
```
|
||||
|
||||
2. Sur la machine agent, utiliser la commande intégrée pour exporter la configuration UCI de votre agent au format Emissary:
|
||||
|
||||
```
|
||||
uci export | emissary agent openwrt uci transform > my-agent-config.json
|
||||
```
|
||||
|
||||
> **Astuce**
|
||||
>
|
||||
> Par défaut, l'outil [LuCi](https://openwrt.org/fr/doc/howto/luci.essentials) est disponible sur votre agent. Vous pouvez y accéder via l'URL `http://<agent_ip>/`.
|
||||
>
|
||||
> Vous pouvez utiliser LuCi pour modifier la configuration de l'agent (par exemple, configurer le WiFi, créer des règles réseaux, etc) avant d'exporter la configuration.
|
||||
>
|
||||
> De cette manière, il est possible de répliquer celle ci sur plusieurs agents via Emissary !
|
||||
|
||||
3. Transférer le fichier `my-agent-config.json` sur la machine hébergeant votre serveur de pilotage Emissary.
|
||||
|
||||
### Transformer la configuration en spécification
|
||||
|
||||
#### Prérequis
|
||||
|
||||
- [`jq`](https://stedolan.github.io/jq/)
|
||||
- [`sponge`](https://linux.die.net/man/1/sponge) (paquet `moreutils` sur Ubuntu)
|
||||
|
||||
#### Étapes
|
||||
|
||||
1. Sur la machine hébergeant le serveur de pilotage Emissary, utiliser l'outil `jq` pour créer un objet JSON correspondant au schéma attendu par la spécification [`uci.emissary.cadoles.com`](../../../internal/spec/uci/schema.json):
|
||||
|
||||
```bash
|
||||
# Créer la structure de base de la spécification UCI
|
||||
cat >> my-uci-spec.json <<EOF
|
||||
{
|
||||
"config": null,
|
||||
"postImportCommands": [
|
||||
{ "command": "uci", "args": ["commit"] },
|
||||
{ "command": "reload_config", "args": [] }
|
||||
]
|
||||
}
|
||||
EOF
|
||||
|
||||
# Injecter la configuration récupérée de notre agent dans la spécification
|
||||
cat my-uci-spec.json | jq --slurpfile config my-agent-config.json '.config = $config[0]' | sponge my-uci-spec.json
|
||||
```
|
||||
|
||||
Notre spécification est prête à être assignée à notre agent !
|
||||
|
||||
|
||||
### Assigner la spécification à l'agent
|
||||
|
||||
1. Sur la machine hébergeant le serveur de pilotage Emissary, retrouver l'identifiant associé à l'agent:
|
||||
|
||||
```bash
|
||||
# Déclarer une variable contenant l'empreinte de l'agent précédemment trouvée
|
||||
AGENT_THUMBPRINT="<empreinte agent>"
|
||||
|
||||
# Récupérer l'identifiant de l'agent
|
||||
AGENT_ID=$(emissary api agent query -f json | jq -r --arg thumbprint "$AGENT_THUMBPRINT" '.[] | select(.thumbprint == $thumbprint) | .id')
|
||||
```
|
||||
|
||||
2. Assigner la spécification à l'agent UCI:
|
||||
|
||||
```bash
|
||||
cat my-uci-spec.json | emissary api agent spec update -a ${AGENT_ID} --no-patch --spec-data - --spec-name uci.emissary.cadoles.com
|
||||
```
|
||||
|
||||
**Bravo, vous avez déployé des spécifications UCI sur votre agent !**
|
||||
|
||||
### Exemple: modifier le `hostname` de votre agent
|
||||
|
||||
En intervenant directement sur notre spécification, il est possible de modifier la configuration et appliquer ces changements à notre agent.
|
||||
|
||||
1. Sur la machine hébergeant le serveur de pilotage, faire:
|
||||
|
||||
```bash
|
||||
# On créait une variable avec le nouveau hostname de notre agent
|
||||
MY_NEW_AGENT_HOSTNAME="MyEmissaryAgent"
|
||||
|
||||
# On utilise jq afin de modifier la valeur de configuration dans notre spécification UCI
|
||||
cat my-uci-spec.json | jq --arg hostname "$MY_NEW_AGENT_HOSTNAME" '( .config.packages[] | select(.name == "system") | .configs[].options[] | select(.name == "hostname").value ) |= $hostname' | sponge my-uci-spec.json
|
||||
```
|
||||
|
||||
> **Astuce**
|
||||
>
|
||||
> En utilisant la commande `grep -C 10 hostname my-uci-spec.json`, on peut voir que la valeur de configuration `hostname` a bien été mise à jour dans notre spécification.
|
||||
|
||||
2. Mettre à jour la configuration de l'agent:
|
||||
|
||||
```bash
|
||||
cat my-uci-spec.json | emissary api agent spec update -a ${AGENT_ID} --no-patch --spec-data - --spec-name uci.emissary.cadoles.com
|
||||
```
|
||||
|
||||
3. Sur l'agent, après quelques secondes (par défaut, la fréquence de mise à jour est de 1 fois par minute) l'agent devrait avoir son `hostname` mis à jour:
|
||||
|
||||
```
|
||||
uci show system.@system[].hostname
|
||||
```
|
||||
|
||||
Un message de ce type devrait s'afficher:
|
||||
|
||||
```
|
||||
system.cfg01e48a.hostname='MyEmissaryAgent'
|
||||
```
|
||||
|
||||
La modification devrait être également visible dans le prompt du shell de l'agent.
|
@ -1 +1,158 @@
|
||||
# Premiers pas
|
||||
# Premiers pas
|
||||
|
||||
## Prérequis
|
||||
|
||||
- Pour le serveur, une machine [Ubuntu 22.04](https://ubuntu.com/download/server)
|
||||
- Pour l'agent, un [RaspberryPi version 3](https://openwrt.org/toh/raspberry_pi_foundation/raspberry_pi)
|
||||
|
||||
## Étapes
|
||||
|
||||
### Préparer votre RaspberryPi
|
||||
|
||||
1. Sur la page des ["versions"](https://forge.cadoles.com/arcad/emissary-firmware/releases) des firmwares du projet Emissary, télécharger la dernière version disponibles correspondant à votre système cible, dans le cas présent `openwrt-<openwrt_version>-emissary-<emissary_firmware_version>-bcm27xx-bcm2710-rpi-3-ext4-factory.img.gz`
|
||||
|
||||
2. Brancher votre carte SD dans le lecteur, flasher celle ci avec le firmware:
|
||||
|
||||
```bash
|
||||
# Chemin vers le fichier de firmware précédemment téléchargé
|
||||
FIRMWARE_FILE="openwrt-<openwrt_version>-emissary-<emissary_firmware_version>-bcm27xx-bcm2710-rpi-3-ext4-factory.img.gz"
|
||||
|
||||
SDCARD_DEVICE=/dev/sdX # Chemin vers le "device" correspondant à votre carte SD
|
||||
|
||||
# Décompresser le firmware
|
||||
gzip -d "${FIRMWARE_FILE}"
|
||||
|
||||
# Flash la carte SD
|
||||
sudo dd if="${FIRMWARE_FILE%.gz}" of="${SDCARD_DEVICE}" bs=2M conv=fsync
|
||||
|
||||
# Attendre la fin des écritures
|
||||
sudo sync
|
||||
```
|
||||
|
||||
3. Placer votre carte SD dans votre RaspberryPi, le connecter à votre réseau en Ethernet puis l'allumer.
|
||||
|
||||
4. Scanner votre réseau pour trouver l'adresse IP de votre Raspberry Pi. Par exemple, avec l'outil `nmap`:
|
||||
|
||||
```bash
|
||||
sudo nmap -sP 192.168.0.* # À modifier par le préfixe correspondant à votre réseau local
|
||||
```
|
||||
|
||||
Une entrée équivalente à la suivante devrait être affichée:
|
||||
|
||||
```bash
|
||||
Starting Nmap 7.93 ( https://nmap.org ) at 2023-05-25 19:29 CEST
|
||||
Nmap scan report for 192.168.0.24
|
||||
Host is up (0.0034s latency).
|
||||
MAC Address: B8:27:EB:E5:7B:55 (Raspberry Pi Foundation)
|
||||
[...]
|
||||
```
|
||||
|
||||
5. Se connecter en SSH sur votre RaspberryPi et définir un mot de passe pour le compte administrateur:
|
||||
|
||||
```bash
|
||||
ssh root@<ip>
|
||||
|
||||
passwd
|
||||
```
|
||||
|
||||
### Installer le serveur Emissary
|
||||
|
||||
1. Sur la machine Ubuntu 22.04, télécharger les paquets Emissary sur la page ["Versions"](https://forge.cadoles.com/arcad/emissary/releases) du projet. Dans le cas présent, choisir le paquet Debian `emissary-server_<emissary_version>_linux_<arch>.deb` où `<arch>` correspond à l'architecture CPU de votre machine.
|
||||
|
||||
2. Installer le paquet télécharger via `dpkg`:
|
||||
|
||||
```
|
||||
sudo dpkg -i emissary-server_<emissary_version>_linux_<arch>.deb
|
||||
```
|
||||
|
||||
3. Appliquer les migrations sur la base de données:
|
||||
|
||||
```shell
|
||||
sudo emissary --workdir /usr/share/emissary --config /etc/emissary/server.yml server database migrate
|
||||
```
|
||||
|
||||
4. Redémarrer le service:
|
||||
|
||||
```shell
|
||||
sudo systemctl restart emissary-server
|
||||
```
|
||||
|
||||
5. Créer un jeton d'administration:
|
||||
|
||||
```shell
|
||||
sudo emissary --workdir /usr/share/emissary --config /etc/emissary/server.yml server auth create-token --role writer --subject $(whoami) > .emissary-token
|
||||
```
|
||||
|
||||
6. Vérifier l'authentification sur l'API:
|
||||
|
||||
```shell
|
||||
emissary api agent query
|
||||
```
|
||||
|
||||
Une réponse équivalente à la suivante devrait s'afficher:
|
||||
|
||||
```shell
|
||||
+----+-------+------------+--------+-------------+-----------+
|
||||
| ID | LABEL | THUMBPRINT | STATUS | CONTACTEDAT | UPDATEDAT |
|
||||
+----+-------+------------+--------+-------------+-----------+
|
||||
+----+-------+------------+--------+-------------+-----------+
|
||||
```
|
||||
|
||||
### Appairer l'agent avec votre serveur
|
||||
|
||||
1. Sur le RaspberryPi, exécuter la commande suivante:
|
||||
|
||||
```shell
|
||||
uci set emissary.agent.server_url='http://<server_ip>:3000'
|
||||
uci commit emissary
|
||||
reload_config
|
||||
```
|
||||
|
||||
2. Via la commande `logread`, vérifier que l'agent arrive à se connecter avec le serveur:
|
||||
|
||||
```shell
|
||||
logread -f
|
||||
```
|
||||
|
||||
Un message de ce type devrait s'afficher:
|
||||
|
||||
```
|
||||
Thu May 25 18:48:51 2023 daemon.info emissary[2202]: 2023-05-25 18:48:51.611 [INFO] <./internal/agent/controller/persistence/controller.go:58> (*Controller).Reconcile no changes detected, doing nothing {"controller": "persistence-controller"}
|
||||
Thu May 25 18:48:51 2023 daemon.info emissary[2202]: 2023-05-25 18:48:51.675 [ERROR] <./internal/agent/controller/spec/controller.go:43>(*Controller).reconcileAgent unexpected agent status {"controller": "spec-controller", "agentID": 1, "status": 0}
|
||||
Thu May 25 18:48:51 2023 daemon.info emissary[2202]: 2023-05-25 18:48:51.676 [INFO] <./internal/agent/controller/openwrt/uci_controller.go:32> (*UCIController).Reconcile could not find uci spec, doing nothing {"controller": "uci-controller"}
|
||||
Thu May 25 18:48:51 2023 daemon.info emissary[2202]: 2023-05-25 18:48:51.677 [INFO] <./internal/agent/controller/app/controller.go:43> (*Controller).Reconcile could not find app spec {"controller": "app-controller"}
|
||||
Thu May 25 18:48:51 2023 daemon.info emissary[2202]: 2023-05-25 18:48:51.678 [INFO] <./internal/agent/controller/proxy/controller.go:35>(*Controller).Reconcile could not find proxy spec {"controller": "proxy-controller"}
|
||||
Thu May 25 18:48:51 2023 daemon.info emissary[2202]: 2023-05-25 18:48:51.680 [INFO] <./internal/agent/controller/mdns/controller.go:38>(*Controller).Reconcile could not find mdns spec {"controller": "mdns-controller"}
|
||||
Thu May 25 18:48:51 2023 daemon.info emissary[2202]: 2023-05-25 18:48:51.680 [INFO] <./internal/agent/controller/openwrt/sysupgrade_controller.go:36> (*SysUpgradeController).Reconcile could not find sysupgrade spec, doing nothing {"controller": "sysupgrade-controller"}
|
||||
```
|
||||
|
||||
3. Sur le serveur, vérifier que l'agent a pu s'enregistrer:
|
||||
|
||||
```shell
|
||||
emissary api agent query
|
||||
```
|
||||
|
||||
Un message de ce type devrait s'afficher:
|
||||
|
||||
```
|
||||
+----+-------+-----------------------------------+--------+-----------------------------------+-----------------------------------+
|
||||
| ID | LABEL | THUMBPRINT | STATUS | CONTACTEDAT | UPDATEDAT |
|
||||
+----+-------+-----------------------------------+--------+-----------------------------------+-----------------------------------+
|
||||
| 1 | | 21CnUATcboKCaheb2uczWCuoxTZtnp... | 0 | 2023-05-25 18:49:51.652680196 ... | "2023-05-25T18:49:51.589225817... |
|
||||
+----+-------+-----------------------------------+--------+-----------------------------------+-----------------------------------+
|
||||
```
|
||||
|
||||
Noter l'identifiant associé à l'agent.
|
||||
|
||||
4. Mettre à jour le statut de l'agent afin qu'il soit en capacité à récupérer ses spécifications:
|
||||
|
||||
```
|
||||
emissary api agent update --agent-id <agent_id> --status 1
|
||||
```
|
||||
|
||||
**Bravo, vous avez appairé votre premier agent et son serveur Emissary !**
|
||||
|
||||
## Aller plus loin
|
||||
|
||||
- [Déployer une configuration UCI personnalisée sur un agent](./deploy-uci-configuration.md)
|
||||
- [Déployer un serveur mandataire inverse sur votre agent](./deploy-reverse-proxy.md)
|
28
go.mod
28
go.mod
@ -3,9 +3,11 @@ module forge.cadoles.com/Cadoles/emissary
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
forge.cadoles.com/arcad/edge v0.0.0-20230322170544-cf8a3f8ac077
|
||||
forge.cadoles.com/arcad/edge v0.0.0-20230426135323-17808d14c978
|
||||
github.com/Masterminds/sprig/v3 v3.2.3
|
||||
github.com/alecthomas/participle/v2 v2.0.0-beta.5
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883
|
||||
github.com/brutella/dnssd v1.2.6
|
||||
github.com/btcsuite/btcd/btcutil v1.1.3
|
||||
github.com/davecgh/go-spew v1.1.1
|
||||
github.com/denisbrodbeck/machineid v1.0.1
|
||||
@ -23,12 +25,14 @@ require (
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/qri-io/jsonschema v0.2.1
|
||||
github.com/urfave/cli/v2 v2.24.4
|
||||
gitlab.com/wpetit/goweb v0.0.0-20230227162855-a1f09bafccb3
|
||||
gitlab.com/wpetit/goweb v0.0.0-20230419082146-a94d9ed7202b
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
modernc.org/sqlite v1.21.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.2.0 // indirect
|
||||
github.com/barnybug/go-cast v0.0.0-20201201064555-a87ccbc26692 // indirect
|
||||
github.com/dop251/goja_nodejs v0.0.0-20230320130059-dcf93ba651dd // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.1 // indirect
|
||||
@ -37,11 +41,17 @@ require (
|
||||
github.com/google/pprof v0.0.0-20230309165930-d61513b1440d // indirect
|
||||
github.com/gorilla/websocket v1.5.0 // indirect
|
||||
github.com/hashicorp/mdns v1.0.5 // indirect
|
||||
github.com/huandu/xstrings v1.3.3 // indirect
|
||||
github.com/igm/sockjs-go/v3 v3.0.2 // indirect
|
||||
github.com/miekg/dns v1.1.51 // indirect
|
||||
github.com/imdario/mergo v0.3.12 // indirect
|
||||
github.com/miekg/dns v1.1.53 // indirect
|
||||
github.com/mitchellh/copystructure v1.0.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.0 // indirect
|
||||
github.com/oklog/ulid/v2 v2.1.0 // indirect
|
||||
github.com/orcaman/concurrent-map v1.0.0 // indirect
|
||||
golang.org/x/net v0.8.0 // indirect
|
||||
github.com/shopspring/decimal v1.2.0 // indirect
|
||||
github.com/spf13/cast v1.3.1 // indirect
|
||||
golang.org/x/net v0.9.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20220728213248-dd149ef739b9 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
)
|
||||
@ -86,12 +96,12 @@ require (
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
go.uber.org/atomic v1.10.0 // indirect
|
||||
golang.org/x/crypto v0.7.0 // indirect
|
||||
golang.org/x/mod v0.8.0 // indirect
|
||||
golang.org/x/mod v0.10.0 // indirect
|
||||
golang.org/x/sync v0.1.0 // indirect
|
||||
golang.org/x/sys v0.6.0 // indirect
|
||||
golang.org/x/term v0.6.0 // indirect
|
||||
golang.org/x/text v0.8.0 // indirect
|
||||
golang.org/x/tools v0.6.0 // indirect
|
||||
golang.org/x/sys v0.7.0 // indirect
|
||||
golang.org/x/term v0.7.0 // indirect
|
||||
golang.org/x/text v0.9.0 // indirect
|
||||
golang.org/x/tools v0.8.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
||||
google.golang.org/protobuf v1.28.1 // indirect
|
||||
gopkg.in/go-playground/validator.v9 v9.31.0 // indirect
|
||||
|
54
go.sum
54
go.sum
@ -54,8 +54,8 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl
|
||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
forge.cadoles.com/arcad/edge v0.0.0-20230322170544-cf8a3f8ac077 h1:vsYcNHZevZrs0VeOTasvJoqvPynb8OvH+MMpIUvNT6Q=
|
||||
forge.cadoles.com/arcad/edge v0.0.0-20230322170544-cf8a3f8ac077/go.mod h1:ONd6vyQ0IM0vHi1i+bmZBRc1Fd0BoXMuDdY/+0sZefw=
|
||||
forge.cadoles.com/arcad/edge v0.0.0-20230426135323-17808d14c978 h1:fekSRSb8gYcVx8C0B9K6B7+KiFHVixIwvPUkxcnRFp4=
|
||||
forge.cadoles.com/arcad/edge v0.0.0-20230426135323-17808d14c978/go.mod h1:uv3wBa+UbcEUb7IiJCj1T96Xo3cmx1BwNxbBYRZhln8=
|
||||
gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8=
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20210715213245-6c3934b029d8/go.mod h1:CzsSbkDixRphAF5hS6wbMKq0eI6ccJRb7/A0M6JBnwg=
|
||||
github.com/Azure/azure-pipeline-go v0.2.3/go.mod h1:x841ezTBIMG6O3lAcl8ATHnsOPVl2bqk7S3ta6S6u4k=
|
||||
@ -84,6 +84,12 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym
|
||||
github.com/ClickHouse/clickhouse-go v1.4.3/go.mod h1:EaI/sW7Azgz9UATzd5ZdZHRUhHgv5+JMS9NSr2smCJI=
|
||||
github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0=
|
||||
github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0=
|
||||
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
||||
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
||||
github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g=
|
||||
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
||||
github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA=
|
||||
github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM=
|
||||
github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
|
||||
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
|
||||
github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw=
|
||||
@ -195,6 +201,8 @@ github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnweb
|
||||
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
|
||||
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
|
||||
github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/brutella/dnssd v1.2.6 h1:/0P13JkHLRzeLQkWRPEn4hJCr4T3NfknIFw3aNPIC34=
|
||||
github.com/brutella/dnssd v1.2.6/go.mod h1:JoW2sJUrmVIef25G6lrLj7HS6Xdwh6q8WUIvMkkBYXs=
|
||||
github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=
|
||||
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
|
||||
github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M=
|
||||
@ -761,6 +769,8 @@ github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2p
|
||||
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
|
||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4=
|
||||
github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
@ -771,6 +781,7 @@ github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJ
|
||||
github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
github.com/imdario/mergo v0.3.10/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
||||
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
||||
github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
|
||||
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/intel/goresctrl v0.2.0/go.mod h1:+CZdzouYFn5EsxgqAQTEzMfwKwuc0fVdMrT9FCCAVRQ=
|
||||
@ -969,11 +980,14 @@ github.com/maxbrunsfeld/counterfeiter/v6 v6.2.2/go.mod h1:eD9eIE7cdwcMi9rYluz88J
|
||||
github.com/miekg/dns v0.0.0-20161006100029-fc4e1e2843d8/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
|
||||
github.com/miekg/dns v1.1.51 h1:0+Xg7vObnhrz/4ZCZcZh7zPXlmU0aveS2HDBd0m0qSo=
|
||||
github.com/miekg/dns v1.1.51/go.mod h1:2Z9d3CP1LQWihRZUf29mQ19yDThaI4DAYzte2CaQW5c=
|
||||
github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
|
||||
github.com/miekg/dns v1.1.53 h1:ZBkuHr5dxHtB1caEOlZTLPo7D3L3TWckgUUs/RHfDxw=
|
||||
github.com/miekg/dns v1.1.53/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY=
|
||||
github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
|
||||
github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4=
|
||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||
github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=
|
||||
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
|
||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||
@ -987,6 +1001,8 @@ github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A=
|
||||
github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY=
|
||||
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=
|
||||
github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c=
|
||||
github.com/moby/sys/mountinfo v0.4.0/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A=
|
||||
@ -1180,6 +1196,7 @@ github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
||||
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
||||
github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
|
||||
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
|
||||
@ -1204,6 +1221,8 @@ github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTd
|
||||
github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4=
|
||||
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
|
||||
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||
github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
|
||||
@ -1291,8 +1310,8 @@ github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPS
|
||||
github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg=
|
||||
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||
gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE=
|
||||
gitlab.com/wpetit/goweb v0.0.0-20230227162855-a1f09bafccb3 h1:ddXRTeqEr7LcHQEtkd6gogZOh9tI1Y6Gappr0a1oa2I=
|
||||
gitlab.com/wpetit/goweb v0.0.0-20230227162855-a1f09bafccb3/go.mod h1:3sus4zjoUv1GB7eDLL60QaPkUnXJCWBpjvbe0jWifeY=
|
||||
gitlab.com/wpetit/goweb v0.0.0-20230419082146-a94d9ed7202b h1:nkvOl8TCj/mErADnwFFynjxBtC+hHsrESw6rw56JGmg=
|
||||
gitlab.com/wpetit/goweb v0.0.0-20230419082146-a94d9ed7202b/go.mod h1:3sus4zjoUv1GB7eDLL60QaPkUnXJCWBpjvbe0jWifeY=
|
||||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
|
||||
@ -1379,6 +1398,7 @@ golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWP
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
@ -1429,9 +1449,9 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
|
||||
golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20161013035702-8b4af36cd21a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@ -1496,6 +1516,7 @@ golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qx
|
||||
golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
@ -1507,8 +1528,9 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
|
||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/oauth2 v0.0.0-20180227000427-d7d64896b5ff/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
@ -1672,8 +1694,9 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
@ -1682,8 +1705,9 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX
|
||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/term v0.7.0 h1:BEvjmm5fURWqcfbSKTdpkDXYBrUS1c0m8agp14W48vQ=
|
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@ -1697,8 +1721,9 @@ golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
@ -1786,10 +1811,11 @@ golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
|
||||
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y=
|
||||
golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4=
|
||||
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
@ -44,8 +44,6 @@ func (a *Agent) Run(ctx context.Context) error {
|
||||
|
||||
if err := a.registerAgent(ctx, client, state); err != nil {
|
||||
logger.Error(ctx, "could not register agent", logger.E(errors.WithStack(err)))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
logger.Debug(ctx, "state before reconciliation", logger.F("state", state))
|
||||
@ -81,7 +79,7 @@ func (a *Agent) Reconcile(ctx context.Context, state *State) error {
|
||||
)
|
||||
|
||||
if err := ctrl.Reconcile(ctrlCtx, state); err != nil {
|
||||
return errors.WithStack(err)
|
||||
logger.Error(ctx, "could not reconcile", logger.E(errors.WithStack(err)))
|
||||
}
|
||||
}
|
||||
|
||||
|
290
internal/agent/controller/app/app_handler.go
Normal file
290
internal/agent/controller/app/app_handler.go
Normal file
@ -0,0 +1,290 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"net"
|
||||
"path/filepath"
|
||||
"text/template"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/agent/controller/app/spec"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/jwk"
|
||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||
"forge.cadoles.com/arcad/edge/pkg/bus"
|
||||
"forge.cadoles.com/arcad/edge/pkg/bus/memory"
|
||||
edgeHTTP "forge.cadoles.com/arcad/edge/pkg/http"
|
||||
"forge.cadoles.com/arcad/edge/pkg/module"
|
||||
appModule "forge.cadoles.com/arcad/edge/pkg/module/app"
|
||||
"forge.cadoles.com/arcad/edge/pkg/module/blob"
|
||||
"forge.cadoles.com/arcad/edge/pkg/module/cast"
|
||||
fetchModule "forge.cadoles.com/arcad/edge/pkg/module/fetch"
|
||||
netModule "forge.cadoles.com/arcad/edge/pkg/module/net"
|
||||
shareModule "forge.cadoles.com/arcad/edge/pkg/module/share"
|
||||
shareSqlite "forge.cadoles.com/arcad/edge/pkg/module/share/sqlite"
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage"
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/sqlite"
|
||||
"github.com/Masterminds/sprig/v3"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/lestrrat-go/jwx/v2/jwa"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
type Dependencies struct {
|
||||
Bus bus.Bus
|
||||
DocumentStore storage.DocumentStore
|
||||
BlobStore storage.BlobStore
|
||||
KeySet jwk.Set
|
||||
AppRepository appModule.Repository
|
||||
AppID app.ID
|
||||
ShareRepository shareModule.Repository
|
||||
}
|
||||
|
||||
const defaultSQLiteParams = "?_pragma=foreign_keys(1)&_pragma=busy_timeout=60000"
|
||||
|
||||
func (c *Controller) getHandlerOptions(ctx context.Context, appKey string, specs *spec.Spec) ([]edgeHTTP.HandlerOptionFunc, error) {
|
||||
dataDir, err := c.ensureAppDataDir(ctx, appKey)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not retrieve app data dir")
|
||||
}
|
||||
|
||||
dbFile := filepath.Join(dataDir, appKey+".sqlite")
|
||||
db, err := sqlite.Open(dbFile + defaultSQLiteParams)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "could not open database file '%s'", dbFile)
|
||||
}
|
||||
|
||||
shareDBFile := filepath.Join(dataDir, "shared.sqlite")
|
||||
shareDB, err := sqlite.Open(shareDBFile + defaultSQLiteParams)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "could not open database file '%s'", shareDBFile)
|
||||
}
|
||||
|
||||
keySet, err := getAuthKeySet(specs.Config)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not retrieve auth key set")
|
||||
}
|
||||
|
||||
mounts := make([]func(r chi.Router), 0)
|
||||
|
||||
authMount, err := getAuthMount(specs.Config.Auth, keySet)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
if authMount != nil {
|
||||
mounts = append(mounts, authMount)
|
||||
}
|
||||
|
||||
mounts = append(mounts, appModule.Mount(c.appRepository))
|
||||
|
||||
deps := Dependencies{
|
||||
Bus: memory.NewBus(),
|
||||
DocumentStore: sqlite.NewDocumentStoreWithDB(db),
|
||||
BlobStore: sqlite.NewBlobStoreWithDB(db),
|
||||
KeySet: keySet,
|
||||
AppRepository: c.appRepository,
|
||||
AppID: app.ID(appKey),
|
||||
ShareRepository: shareSqlite.NewRepositoryWithDB(shareDB),
|
||||
}
|
||||
|
||||
modules := c.getAppModules(deps)
|
||||
|
||||
options := []edgeHTTP.HandlerOptionFunc{
|
||||
edgeHTTP.WithBus(deps.Bus),
|
||||
edgeHTTP.WithServerModules(modules...),
|
||||
edgeHTTP.WithHTTPMounts(mounts...),
|
||||
}
|
||||
|
||||
return options, nil
|
||||
}
|
||||
|
||||
func getAuthKeySet(config *spec.Config) (jwk.Set, error) {
|
||||
keySet := jwk.NewSet()
|
||||
if config == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
auth := config.Auth
|
||||
|
||||
if auth == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
switch {
|
||||
case auth.Local != nil:
|
||||
var (
|
||||
key jwk.Key
|
||||
err error
|
||||
)
|
||||
|
||||
switch typedKey := auth.Local.Key.(type) {
|
||||
case string:
|
||||
key, err = jwk.FromRaw([]byte(typedKey))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not parse local auth key")
|
||||
}
|
||||
|
||||
if err := key.Set(jwk.AlgorithmKey, jwa.HS256); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
default:
|
||||
return nil, errors.Errorf("unexpected key type '%T'", auth.Local.Key)
|
||||
}
|
||||
|
||||
if err := keySet.AddKey(key); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
return keySet, nil
|
||||
}
|
||||
|
||||
func createResolveAppURL(specs *spec.Spec) (ResolveAppURLFunc, error) {
|
||||
rawIfaceMappings := make(map[string]string, 0)
|
||||
if specs.Config != nil && specs.Config.AppURLResolving != nil && specs.Config.AppURLResolving.IfaceMappings != nil {
|
||||
rawIfaceMappings = specs.Config.AppURLResolving.IfaceMappings
|
||||
}
|
||||
|
||||
ifaceMappings := make(map[string]*template.Template, len(rawIfaceMappings))
|
||||
for iface, rawTemplate := range rawIfaceMappings {
|
||||
tmpl, err := template.New("").Funcs(sprig.TxtFuncMap()).Parse(rawTemplate)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "could not parse iface '%s' template", iface)
|
||||
}
|
||||
|
||||
ifaceMappings[iface] = tmpl
|
||||
}
|
||||
|
||||
defaultRawTemplate := `http://{{ .DeviceIP }}:{{ .AppPort }}`
|
||||
if specs.Config != nil && specs.Config.AppURLResolving != nil && specs.Config.AppURLResolving.DefaultURLTemplate != "" {
|
||||
defaultRawTemplate = specs.Config.AppURLResolving.DefaultURLTemplate
|
||||
}
|
||||
|
||||
defaultTemplate, err := template.New("").Funcs(sprig.TxtFuncMap()).Parse(defaultRawTemplate)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return func(ctx context.Context, manifest *app.Manifest, from string) (string, error) {
|
||||
var (
|
||||
urlTemplate *template.Template
|
||||
deviceIP net.IP
|
||||
)
|
||||
|
||||
fromIP := net.ParseIP(from)
|
||||
|
||||
if fromIP != nil {
|
||||
LOOP:
|
||||
for ifaceName, ifaceTmpl := range ifaceMappings {
|
||||
iface, err := net.InterfaceByName(ifaceName)
|
||||
if err != nil {
|
||||
logger.Error(
|
||||
ctx, "could not find interface",
|
||||
logger.E(errors.WithStack(err)), logger.F("iface", ifaceName),
|
||||
)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
addresses, err := iface.Addrs()
|
||||
if err != nil {
|
||||
logger.Error(
|
||||
ctx, "could not list interface addresses",
|
||||
logger.E(errors.WithStack(err)),
|
||||
logger.F("iface", iface.Name),
|
||||
)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
for _, addr := range addresses {
|
||||
ifaIP, network, err := net.ParseCIDR(addr.String())
|
||||
if err != nil {
|
||||
logger.Error(
|
||||
ctx, "could not parse interface ip",
|
||||
logger.E(errors.WithStack(err)),
|
||||
logger.F("iface", iface.Name),
|
||||
)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if !network.Contains(fromIP) {
|
||||
continue
|
||||
}
|
||||
|
||||
deviceIP = ifaIP
|
||||
urlTemplate = ifaceTmpl
|
||||
|
||||
break LOOP
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if urlTemplate == nil {
|
||||
urlTemplate = defaultTemplate
|
||||
}
|
||||
|
||||
if deviceIP == nil {
|
||||
deviceIP = net.ParseIP("127.0.0.1")
|
||||
}
|
||||
|
||||
var appEntry *spec.AppEntry
|
||||
for appID, entry := range specs.Apps {
|
||||
if manifest.ID != app.ID(appID) {
|
||||
continue
|
||||
}
|
||||
|
||||
appEntry = &entry
|
||||
break
|
||||
}
|
||||
|
||||
if appEntry == nil {
|
||||
return "", errors.Errorf("could not find app '%s' in specs", manifest.ID)
|
||||
}
|
||||
|
||||
_, port, err := net.SplitHostPort(appEntry.Address)
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
data := struct {
|
||||
Manifest *app.Manifest
|
||||
Specs *spec.Spec
|
||||
DeviceIP string
|
||||
AppPort string
|
||||
}{
|
||||
Manifest: manifest,
|
||||
Specs: specs,
|
||||
DeviceIP: deviceIP.String(),
|
||||
AppPort: port,
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
|
||||
if err := urlTemplate.Execute(&buf, data); err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Controller) getAppModules(deps Dependencies) []app.ServerModuleFactory {
|
||||
return []app.ServerModuleFactory{
|
||||
module.ContextModuleFactory(),
|
||||
module.ConsoleModuleFactory(),
|
||||
cast.CastModuleFactory(),
|
||||
module.LifecycleModuleFactory(),
|
||||
netModule.ModuleFactory(deps.Bus),
|
||||
module.RPCModuleFactory(deps.Bus),
|
||||
module.StoreModuleFactory(deps.DocumentStore),
|
||||
blob.ModuleFactory(deps.Bus, deps.BlobStore),
|
||||
authModuleFactory(deps.KeySet),
|
||||
appModule.ModuleFactory(deps.AppRepository),
|
||||
fetchModule.ModuleFactory(deps.Bus),
|
||||
shareModule.ModuleFactory(deps.AppID, deps.ShareRepository),
|
||||
}
|
||||
}
|
73
internal/agent/controller/app/app_handler_test.go
Normal file
73
internal/agent/controller/app/app_handler_test.go
Normal file
@ -0,0 +1,73 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/agent/controller/app/spec"
|
||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func TestCreateResolveAppURL(t *testing.T) {
|
||||
specs := &spec.Spec{
|
||||
Apps: map[string]spec.AppEntry{
|
||||
"app.arcad.test": {
|
||||
Address: ":8080",
|
||||
},
|
||||
"app.arcad.foo": {
|
||||
Address: ":8081",
|
||||
},
|
||||
"app.arcad.bar": {
|
||||
Address: ":8082",
|
||||
},
|
||||
},
|
||||
Config: &spec.Config{
|
||||
AppURLResolving: &spec.AppURLResolving{
|
||||
IfaceMappings: map[string]string{
|
||||
"lo": "http://{{ .DeviceIP }}:{{ .AppPort }}",
|
||||
"does-not-exists": "http://{{ .DeviceIP }}:{{ .AppPort }}",
|
||||
},
|
||||
DefaultURLTemplate: `http://{{ last ( splitList "." ( toString .Manifest.ID ) ) }}.arcad.local`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resolveAppURL, err := createResolveAppURL(specs)
|
||||
if err != nil {
|
||||
t.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
manifest := &app.Manifest{
|
||||
ID: "app.arcad.test",
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
url, err := resolveAppURL(ctx, manifest, "127.0.0.2")
|
||||
if err != nil {
|
||||
t.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
if e, g := "http://127.0.0.1:8080", url; e != g {
|
||||
t.Errorf("url: expected '%s', got '%s", e, g)
|
||||
}
|
||||
|
||||
url, err = resolveAppURL(ctx, manifest, "")
|
||||
if err != nil {
|
||||
t.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
if e, g := "http://test.arcad.local", url; e != g {
|
||||
t.Errorf("url: expected '%s', got '%s", e, g)
|
||||
}
|
||||
|
||||
url, err = resolveAppURL(ctx, manifest, "192.168.0.100")
|
||||
if err != nil {
|
||||
t.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
if e, g := "http://test.arcad.local", url; e != g {
|
||||
t.Errorf("url: expected '%s', got '%s", e, g)
|
||||
}
|
||||
}
|
137
internal/agent/controller/app/app_repository.go
Normal file
137
internal/agent/controller/app/app_repository.go
Normal file
@ -0,0 +1,137 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||
"forge.cadoles.com/arcad/edge/pkg/bundle"
|
||||
appModule "forge.cadoles.com/arcad/edge/pkg/module/app"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
type ResolveAppURLFunc func(context.Context, *app.Manifest, string) (string, error)
|
||||
|
||||
type AppRepository struct {
|
||||
resolveAppURL ResolveAppURLFunc
|
||||
bundles []string
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
// Get implements app.Repository
|
||||
func (r *AppRepository) Get(ctx context.Context, id app.ID) (*app.Manifest, error) {
|
||||
r.mutex.RLock()
|
||||
defer r.mutex.RUnlock()
|
||||
|
||||
manifest, err := r.findManifest(ctx, id)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return manifest, nil
|
||||
}
|
||||
|
||||
// GetURL implements app.Repository
|
||||
func (r *AppRepository) GetURL(ctx context.Context, id app.ID, from string) (string, error) {
|
||||
r.mutex.RLock()
|
||||
defer r.mutex.RUnlock()
|
||||
|
||||
manifest, err := r.findManifest(ctx, id)
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
url, err := r.resolveAppURL(ctx, manifest, from)
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
return url, nil
|
||||
}
|
||||
|
||||
// List implements app.Repository
|
||||
func (r *AppRepository) List(ctx context.Context) ([]*app.Manifest, error) {
|
||||
r.mutex.RLock()
|
||||
defer r.mutex.RUnlock()
|
||||
|
||||
manifests := make([]*app.Manifest, 0)
|
||||
|
||||
for _, path := range r.bundles {
|
||||
bundleCtx := logger.With(ctx, logger.F("path", path))
|
||||
|
||||
bundle, err := bundle.FromPath(path)
|
||||
if err != nil {
|
||||
logger.Error(bundleCtx, "could not load bundle", logger.E(errors.WithStack(err)))
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
manifest, err := app.LoadManifest(bundle)
|
||||
if err != nil {
|
||||
logger.Error(bundleCtx, "could not load manifest", logger.E(errors.WithStack(err)))
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
manifests = append(manifests, manifest)
|
||||
}
|
||||
|
||||
sort.Sort(ByID(manifests))
|
||||
|
||||
return manifests, nil
|
||||
}
|
||||
|
||||
func (r *AppRepository) Update(resolveAppURL ResolveAppURLFunc, bundles []string) {
|
||||
r.mutex.Lock()
|
||||
defer r.mutex.Unlock()
|
||||
|
||||
r.resolveAppURL = resolveAppURL
|
||||
r.bundles = bundles
|
||||
}
|
||||
|
||||
func (r *AppRepository) findManifest(ctx context.Context, id app.ID) (*app.Manifest, error) {
|
||||
for _, path := range r.bundles {
|
||||
bundleCtx := logger.With(ctx, logger.F("path", path))
|
||||
|
||||
bundle, err := bundle.FromPath(path)
|
||||
if err != nil {
|
||||
logger.Error(bundleCtx, "could not load bundle", logger.E(errors.WithStack(err)))
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
manifest, err := app.LoadManifest(bundle)
|
||||
if err != nil {
|
||||
logger.Error(bundleCtx, "could not load manifest", logger.E(errors.WithStack(err)))
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if manifest.ID != id {
|
||||
continue
|
||||
}
|
||||
|
||||
return manifest, nil
|
||||
}
|
||||
|
||||
return nil, errors.WithStack(appModule.ErrNotFound)
|
||||
}
|
||||
|
||||
func NewAppRepository() *AppRepository {
|
||||
return &AppRepository{
|
||||
resolveAppURL: func(ctx context.Context, m *app.Manifest, from string) (string, error) {
|
||||
return "", errors.New("unavailable")
|
||||
},
|
||||
bundles: []string{},
|
||||
}
|
||||
}
|
||||
|
||||
var _ appModule.Repository = &AppRepository{}
|
||||
|
||||
type ByID []*app.Manifest
|
||||
|
||||
func (a ByID) Len() int { return len(a) }
|
||||
func (a ByID) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a ByID) Less(i, j int) bool { return a[i].ID > a[j].ID }
|
93
internal/agent/controller/app/auth_module.go
Normal file
93
internal/agent/controller/app/auth_module.go
Normal file
@ -0,0 +1,93 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/agent/controller/app/spec"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/jwk"
|
||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||
"forge.cadoles.com/arcad/edge/pkg/module"
|
||||
"forge.cadoles.com/arcad/edge/pkg/module/auth"
|
||||
authModule "forge.cadoles.com/arcad/edge/pkg/module/auth"
|
||||
authHTTP "forge.cadoles.com/arcad/edge/pkg/module/auth/http"
|
||||
"github.com/dop251/goja"
|
||||
"github.com/lestrrat-go/jwx/v2/jwa"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
RoleVisitor string = "visitor"
|
||||
RoleUser string = "user"
|
||||
RoleSuperuser string = "superuser"
|
||||
RoleAdmin string = "admin"
|
||||
RoleSuperadmin string = "superadmin"
|
||||
)
|
||||
|
||||
func authModuleFactory(keySet jwk.Set) app.ServerModuleFactory {
|
||||
return module.Extends(
|
||||
authModule.ModuleFactory(
|
||||
authModule.WithJWT(func() (jwk.Set, error) {
|
||||
return keySet, nil
|
||||
}),
|
||||
),
|
||||
func(o *goja.Object) {
|
||||
if err := o.Set("ROLE_VISITOR", RoleVisitor); err != nil {
|
||||
panic(errors.New("could not set 'ROLE_VISITOR' property"))
|
||||
}
|
||||
|
||||
if err := o.Set("ROLE_USER", RoleUser); err != nil {
|
||||
panic(errors.New("could not set 'ROLE_USER' property"))
|
||||
}
|
||||
|
||||
if err := o.Set("ROLE_SUPERUSER", RoleSuperuser); err != nil {
|
||||
panic(errors.New("could not set 'ROLE_SUPERUSER' property"))
|
||||
}
|
||||
|
||||
if err := o.Set("ROLE_ADMIN", RoleAdmin); err != nil {
|
||||
panic(errors.New("could not set 'ROLE_ADMIN' property"))
|
||||
}
|
||||
|
||||
if err := o.Set("ROLE_SUPERADMIN", RoleSuperadmin); err != nil {
|
||||
panic(errors.New("could not set 'ROLE_SUPERADMIN' property"))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func getAuthMount(auth *spec.Auth, keySet jwk.Set) (auth.MountFunc, error) {
|
||||
switch {
|
||||
case auth.Local != nil:
|
||||
var rawKey any = auth.Local.Key
|
||||
if strKey, ok := rawKey.(string); ok {
|
||||
rawKey = []byte(strKey)
|
||||
}
|
||||
|
||||
key, err := jwk.FromRaw(rawKey)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
cookieDuration := defaultCookieDuration
|
||||
if auth.Local.CookieDuration != "" {
|
||||
cookieDuration, err = time.ParseDuration(auth.Local.CookieDuration)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
return authModule.Mount(
|
||||
authHTTP.NewLocalHandler(
|
||||
jwa.HS256, key,
|
||||
authHTTP.WithRoutePrefix("/auth"),
|
||||
authHTTP.WithAccounts(auth.Local.Accounts...),
|
||||
authHTTP.WithCookieOptions(getCookieDomain, cookieDuration),
|
||||
),
|
||||
authModule.WithJWT(func() (jwk.Set, error) {
|
||||
return keySet, nil
|
||||
}),
|
||||
), nil
|
||||
|
||||
default:
|
||||
return nil, nil
|
||||
}
|
||||
}
|
@ -8,24 +8,25 @@ import (
|
||||
"path/filepath"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/agent"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/spec/app"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/agent/controller/app/spec"
|
||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||
"forge.cadoles.com/arcad/edge/pkg/bundle"
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/sqlite"
|
||||
"github.com/mitchellh/hashstructure/v2"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
type serverEntry struct {
|
||||
SpecHash uint64
|
||||
Server *Server
|
||||
AppDefHash uint64
|
||||
Server *Server
|
||||
}
|
||||
|
||||
type Controller struct {
|
||||
client *http.Client
|
||||
downloadDir string
|
||||
dataDir string
|
||||
servers map[string]*serverEntry
|
||||
client *http.Client
|
||||
downloadDir string
|
||||
dataDir string
|
||||
servers map[string]*serverEntry
|
||||
appRepository *AppRepository
|
||||
}
|
||||
|
||||
// Name implements node.Controller.
|
||||
@ -35,9 +36,9 @@ func (c *Controller) Name() string {
|
||||
|
||||
// Reconcile implements node.Controller.
|
||||
func (c *Controller) Reconcile(ctx context.Context, state *agent.State) error {
|
||||
appSpec := app.NewSpec()
|
||||
appSpec := spec.NewSpec()
|
||||
|
||||
if err := state.GetSpec(app.NameApp, appSpec); err != nil {
|
||||
if err := state.GetSpec(spec.Name, appSpec); err != nil {
|
||||
if errors.Is(err, agent.ErrSpecNotFound) {
|
||||
logger.Info(ctx, "could not find app spec")
|
||||
|
||||
@ -56,7 +57,7 @@ func (c *Controller) Reconcile(ctx context.Context, state *agent.State) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Controller) stopAllApps(ctx context.Context, spec *app.Spec) {
|
||||
func (c *Controller) stopAllApps(ctx context.Context, spec *spec.Spec) {
|
||||
if len(c.servers) > 0 {
|
||||
logger.Info(ctx, "stopping all apps")
|
||||
}
|
||||
@ -76,122 +77,169 @@ func (c *Controller) stopAllApps(ctx context.Context, spec *app.Spec) {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Controller) updateApps(ctx context.Context, spec *app.Spec) {
|
||||
func (c *Controller) updateApps(ctx context.Context, specs *spec.Spec) {
|
||||
// Stop and remove obsolete apps
|
||||
for appID, entry := range c.servers {
|
||||
if _, exists := spec.Apps[appID]; exists {
|
||||
for appKey, server := range c.servers {
|
||||
if _, exists := specs.Apps[appKey]; exists {
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Info(ctx, "stopping app", logger.F("appID", appID))
|
||||
logger.Info(ctx, "stopping app", logger.F("appKey", appKey))
|
||||
|
||||
if err := entry.Server.Stop(); err != nil {
|
||||
if err := server.Server.Stop(); err != nil {
|
||||
logger.Error(
|
||||
ctx, "error while stopping app",
|
||||
logger.F("gatewayID", appID),
|
||||
logger.F("appKey", appKey),
|
||||
logger.E(errors.WithStack(err)),
|
||||
)
|
||||
|
||||
delete(c.servers, appID)
|
||||
delete(c.servers, appKey)
|
||||
}
|
||||
}
|
||||
|
||||
// (Re)start apps
|
||||
for appID, appSpec := range spec.Apps {
|
||||
appCtx := logger.With(ctx, logger.F("appID", appID))
|
||||
if err := c.updateAppRepository(ctx, specs); err != nil {
|
||||
logger.Error(
|
||||
ctx, "could not update app repository",
|
||||
logger.E(errors.WithStack(err)),
|
||||
)
|
||||
|
||||
if err := c.updateApp(ctx, appID, appSpec, spec.Auth); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// (Re)start apps if necessary
|
||||
for appKey := range specs.Apps {
|
||||
appCtx := logger.With(ctx, logger.F("appKey", appKey))
|
||||
|
||||
if err := c.updateApp(ctx, specs, appKey); err != nil {
|
||||
logger.Error(appCtx, "could not update app", logger.E(errors.WithStack(err)))
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Controller) updateApp(ctx context.Context, appID string, appSpec app.AppEntry, auth *app.Auth) (err error) {
|
||||
newAppSpecHash, err := hashstructure.Hash(appSpec, hashstructure.FormatV2, nil)
|
||||
func (c *Controller) updateAppRepository(ctx context.Context, specs *spec.Spec) error {
|
||||
bundles := make([]string, 0, len(specs.Apps))
|
||||
for appKey, app := range specs.Apps {
|
||||
path := c.getAppBundlePath(appKey, app.Format)
|
||||
bundles = append(bundles, path)
|
||||
}
|
||||
|
||||
resolveAppURL, err := createResolveAppURL(specs)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
bundle, sha256sum, err := c.ensureAppBundle(ctx, appID, appSpec)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not download app bundle")
|
||||
}
|
||||
|
||||
dataDir, err := c.ensureAppDataDir(ctx, appID)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not retrieve app data dir")
|
||||
}
|
||||
|
||||
var entry *serverEntry
|
||||
|
||||
entry, exists := c.servers[appID]
|
||||
if !exists {
|
||||
logger.Info(ctx, "app currently not running")
|
||||
} else if sha256sum != appSpec.SHA256Sum {
|
||||
logger.Info(
|
||||
ctx, "bundle hash mismatch, stopping app",
|
||||
logger.F("currentHash", sha256sum),
|
||||
logger.F("specHash", appSpec.SHA256Sum),
|
||||
)
|
||||
|
||||
if err := entry.Server.Stop(); err != nil {
|
||||
return errors.Wrap(err, "could not stop app")
|
||||
}
|
||||
|
||||
entry = nil
|
||||
}
|
||||
|
||||
if entry == nil {
|
||||
dbFile := filepath.Join(dataDir, appID+".sqlite")
|
||||
db, err := sqlite.Open(dbFile)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "could not opend database file '%s'", dbFile)
|
||||
}
|
||||
|
||||
entry = &serverEntry{
|
||||
Server: NewServer(bundle, db, auth),
|
||||
SpecHash: 0,
|
||||
}
|
||||
|
||||
c.servers[appID] = entry
|
||||
}
|
||||
|
||||
specChanged := newAppSpecHash != entry.SpecHash
|
||||
|
||||
if entry.Server.Running() && !specChanged {
|
||||
return nil
|
||||
}
|
||||
|
||||
if specChanged && entry.SpecHash != 0 {
|
||||
logger.Info(
|
||||
ctx, "restarting app",
|
||||
logger.F("address", appSpec.Address),
|
||||
)
|
||||
} else {
|
||||
logger.Info(
|
||||
ctx, "starting app",
|
||||
logger.F("address", appSpec.Address),
|
||||
)
|
||||
}
|
||||
|
||||
if err := entry.Server.Start(ctx, appSpec.Address); err != nil {
|
||||
delete(c.servers, appID)
|
||||
|
||||
return errors.Wrap(err, "could not start app")
|
||||
}
|
||||
|
||||
entry.SpecHash = newAppSpecHash
|
||||
c.appRepository.Update(resolveAppURL, bundles)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Controller) ensureAppBundle(ctx context.Context, appID string, spec app.AppEntry) (bundle.Bundle, string, error) {
|
||||
func (c *Controller) updateApp(ctx context.Context, specs *spec.Spec, appKey string) (err error) {
|
||||
appEntry := specs.Apps[appKey]
|
||||
|
||||
appDef := struct {
|
||||
App spec.AppEntry
|
||||
Config *spec.Config
|
||||
}{
|
||||
App: appEntry,
|
||||
Config: specs.Config,
|
||||
}
|
||||
|
||||
newAppDefHash, err := hashstructure.Hash(appDef, hashstructure.FormatV2, nil)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
bundle, sha256sum, err := c.ensureAppBundle(ctx, appKey, appEntry)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not download app bundle")
|
||||
}
|
||||
|
||||
server, exists := c.servers[appKey]
|
||||
if !exists {
|
||||
logger.Info(ctx, "app currently not running")
|
||||
} else if sha256sum != appEntry.SHA256Sum {
|
||||
logger.Info(
|
||||
ctx, "bundle hash mismatch, stopping app",
|
||||
logger.F("currentHash", sha256sum),
|
||||
logger.F("specHash", appEntry.SHA256Sum),
|
||||
)
|
||||
|
||||
if err := server.Server.Stop(); err != nil {
|
||||
return errors.Wrap(err, "could not stop app")
|
||||
}
|
||||
|
||||
server = nil
|
||||
}
|
||||
|
||||
newServerEntry := func() (*serverEntry, error) {
|
||||
options, err := c.getHandlerOptions(ctx, appKey, specs)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not create handler options")
|
||||
}
|
||||
|
||||
server = &serverEntry{
|
||||
Server: NewServer(bundle, specs.Config, options...),
|
||||
AppDefHash: 0,
|
||||
}
|
||||
|
||||
return server, nil
|
||||
}
|
||||
|
||||
if server == nil {
|
||||
serverEntry, err := newServerEntry()
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
c.servers[appKey] = serverEntry
|
||||
}
|
||||
|
||||
defChanged := newAppDefHash != server.AppDefHash
|
||||
if server.Server.Running() && !defChanged {
|
||||
return nil
|
||||
}
|
||||
|
||||
if defChanged && server.AppDefHash != 0 {
|
||||
logger.Info(
|
||||
ctx, "restarting app",
|
||||
logger.F("address", appEntry.Address),
|
||||
)
|
||||
|
||||
if err := server.Server.Stop(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
serverEntry, err := newServerEntry()
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
c.servers[appKey] = serverEntry
|
||||
} else {
|
||||
logger.Info(
|
||||
ctx, "starting app",
|
||||
logger.F("address", appEntry.Address),
|
||||
)
|
||||
}
|
||||
|
||||
if err := server.Server.Start(ctx, appEntry.Address); err != nil {
|
||||
delete(c.servers, appKey)
|
||||
|
||||
return errors.Wrap(err, "could not start app")
|
||||
}
|
||||
|
||||
server.AppDefHash = newAppDefHash
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Controller) ensureAppBundle(ctx context.Context, appID string, spec spec.AppEntry) (bundle.Bundle, string, error) {
|
||||
if err := os.MkdirAll(c.downloadDir, os.ModePerm); err != nil {
|
||||
return nil, "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
bundlePath := filepath.Join(c.downloadDir, appID+"."+spec.Format)
|
||||
bundlePath := c.getAppBundlePath(appID, spec.Format)
|
||||
|
||||
_, err := os.Stat(bundlePath)
|
||||
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
@ -229,7 +277,21 @@ func (c *Controller) ensureAppBundle(ctx context.Context, appID string, spec app
|
||||
return nil, "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
return bdle, "", nil
|
||||
manifest, err := app.LoadManifest(bdle)
|
||||
if err != nil {
|
||||
return nil, "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
valid, err := validateManifest(manifest)
|
||||
if err != nil {
|
||||
return nil, "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
if !valid {
|
||||
return nil, "", errors.New("bundle's manifest is invalid")
|
||||
}
|
||||
|
||||
return bdle, spec.SHA256Sum, nil
|
||||
}
|
||||
|
||||
func (c *Controller) downloadFile(url string, sha256sum string, dest string) error {
|
||||
@ -285,6 +347,10 @@ func (c *Controller) ensureAppDataDir(ctx context.Context, appID string) (string
|
||||
return dataDir, nil
|
||||
}
|
||||
|
||||
func (c *Controller) getAppBundlePath(appKey string, format string) string {
|
||||
return filepath.Join(c.downloadDir, appKey+"."+format)
|
||||
}
|
||||
|
||||
func NewController(funcs ...OptionFunc) *Controller {
|
||||
opts := defaultOptions()
|
||||
for _, fn := range funcs {
|
||||
@ -292,10 +358,11 @@ func NewController(funcs ...OptionFunc) *Controller {
|
||||
}
|
||||
|
||||
return &Controller{
|
||||
client: opts.Client,
|
||||
downloadDir: opts.DownloadDir,
|
||||
dataDir: opts.DataDir,
|
||||
servers: make(map[string]*serverEntry),
|
||||
client: opts.Client,
|
||||
downloadDir: opts.DownloadDir,
|
||||
dataDir: opts.DataDir,
|
||||
servers: make(map[string]*serverEntry),
|
||||
appRepository: NewAppRepository(),
|
||||
}
|
||||
}
|
||||
|
||||
|
19
internal/agent/controller/app/manifest.go
Normal file
19
internal/agent/controller/app/manifest.go
Normal file
@ -0,0 +1,19 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||
"forge.cadoles.com/arcad/edge/pkg/app/metadata"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func validateManifest(manifest *app.Manifest) (bool, error) {
|
||||
valid, err := manifest.Validate(
|
||||
metadata.WithMinimumRoleValidator(RoleVisitor, RoleUser, RoleSuperuser, RoleAdmin, RoleSuperadmin),
|
||||
metadata.WithNamedPathsValidator(metadata.NamedPathAdmin, metadata.NamedPathIcon),
|
||||
)
|
||||
if err != nil {
|
||||
return false, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return valid, nil
|
||||
}
|
@ -2,94 +2,62 @@ package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
appSpec "forge.cadoles.com/Cadoles/emissary/internal/spec/app"
|
||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||
"forge.cadoles.com/arcad/edge/pkg/bus"
|
||||
"forge.cadoles.com/arcad/edge/pkg/bus/memory"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/agent/controller/app/spec"
|
||||
appSpec "forge.cadoles.com/Cadoles/emissary/internal/agent/controller/app/spec"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/proxy/wildcard"
|
||||
edgeHTTP "forge.cadoles.com/arcad/edge/pkg/http"
|
||||
"forge.cadoles.com/arcad/edge/pkg/module"
|
||||
"forge.cadoles.com/arcad/edge/pkg/module/auth"
|
||||
authHTTP "forge.cadoles.com/arcad/edge/pkg/module/auth/http"
|
||||
"forge.cadoles.com/arcad/edge/pkg/module/cast"
|
||||
"forge.cadoles.com/arcad/edge/pkg/module/net"
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage"
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/sqlite"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/bundle"
|
||||
"github.com/dop251/goja"
|
||||
"github.com/go-chi/chi/middleware"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/lestrrat-go/jwx/v2/jwa"
|
||||
"github.com/lestrrat-go/jwx/v2/jwk"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
_ "forge.cadoles.com/Cadoles/emissary/internal/imports/passwd"
|
||||
)
|
||||
|
||||
const defaultCookieDuration time.Duration = 24 * time.Hour
|
||||
|
||||
type Server struct {
|
||||
bundle bundle.Bundle
|
||||
db *sql.DB
|
||||
server *http.Server
|
||||
serverMutex sync.RWMutex
|
||||
auth *appSpec.Auth
|
||||
keySet jwk.Set
|
||||
bundle bundle.Bundle
|
||||
handlerOptions []edgeHTTP.HandlerOptionFunc
|
||||
server *http.Server
|
||||
serverMutex sync.RWMutex
|
||||
config *appSpec.Config
|
||||
}
|
||||
|
||||
func (s *Server) Start(ctx context.Context, addr string) (err error) {
|
||||
if s.server != nil {
|
||||
if s.Running() {
|
||||
if err := s.Stop(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
s.serverMutex.Lock()
|
||||
defer s.serverMutex.Unlock()
|
||||
|
||||
router := chi.NewRouter()
|
||||
|
||||
router.Use(middleware.RealIP)
|
||||
router.Use(middleware.Logger)
|
||||
router.Use(middleware.Compress(5))
|
||||
|
||||
bus := memory.NewBus()
|
||||
ds := sqlite.NewDocumentStoreWithDB(s.db)
|
||||
bs := sqlite.NewBlobStoreWithDB(s.db)
|
||||
|
||||
handler := edgeHTTP.NewHandler(
|
||||
edgeHTTP.WithBus(bus),
|
||||
edgeHTTP.WithServerModules(s.getAppModules(bus, ds, bs)...),
|
||||
)
|
||||
handler := edgeHTTP.NewHandler(s.handlerOptions...)
|
||||
if err := handler.Load(s.bundle); err != nil {
|
||||
return errors.Wrap(err, "could not load app bundle")
|
||||
}
|
||||
|
||||
if s.auth != nil {
|
||||
if s.auth.Local != nil {
|
||||
var rawKey any = s.auth.Local.Key
|
||||
if strKey, ok := rawKey.(string); ok {
|
||||
rawKey = []byte(strKey)
|
||||
}
|
||||
|
||||
key, err := jwk.FromRaw(rawKey)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if err := key.Set(jwk.AlgorithmKey, jwa.HS256); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
keySet := jwk.NewSet()
|
||||
if err := keySet.AddKey(key); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
s.keySet = keySet
|
||||
|
||||
router.Handle("/auth/*", authHTTP.NewLocalHandler(
|
||||
jwa.HS256, key,
|
||||
authHTTP.WithRoutePrefix("/auth"),
|
||||
authHTTP.WithAccounts(s.auth.Local.Accounts...),
|
||||
if s.config != nil {
|
||||
if s.config.UnexpectedHostRedirect != nil {
|
||||
router.Use(unexpectedHostRedirect(
|
||||
s.config.UnexpectedHostRedirect.HostTarget,
|
||||
s.config.UnexpectedHostRedirect.AcceptedHostPatterns...,
|
||||
))
|
||||
}
|
||||
}
|
||||
@ -125,9 +93,7 @@ func (s *Server) Start(ctx context.Context, addr string) (err error) {
|
||||
}
|
||||
}()
|
||||
|
||||
s.serverMutex.Lock()
|
||||
s.server = server
|
||||
s.serverMutex.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -140,66 +106,84 @@ func (s *Server) Running() bool {
|
||||
}
|
||||
|
||||
func (s *Server) Stop() error {
|
||||
if !s.Running() {
|
||||
return nil
|
||||
}
|
||||
|
||||
s.serverMutex.Lock()
|
||||
defer s.serverMutex.Unlock()
|
||||
|
||||
if s.server == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
defer func() {
|
||||
s.serverMutex.Lock()
|
||||
s.server = nil
|
||||
s.serverMutex.Unlock()
|
||||
}()
|
||||
|
||||
if err := s.server.Close(); err != nil {
|
||||
panic(errors.WithStack(err))
|
||||
s.server = nil
|
||||
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
s.server = nil
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) getAppModules(bus bus.Bus, ds storage.DocumentStore, bs storage.BlobStore) []app.ServerModuleFactory {
|
||||
return []app.ServerModuleFactory{
|
||||
module.ContextModuleFactory(),
|
||||
module.ConsoleModuleFactory(),
|
||||
cast.CastModuleFactory(),
|
||||
module.LifecycleModuleFactory(),
|
||||
net.ModuleFactory(bus),
|
||||
module.RPCModuleFactory(bus),
|
||||
module.StoreModuleFactory(ds),
|
||||
module.BlobModuleFactory(bus, bs),
|
||||
module.Extends(
|
||||
auth.ModuleFactory(
|
||||
auth.WithJWT(s.getJWTKeySet),
|
||||
),
|
||||
func(o *goja.Object) {
|
||||
if err := o.Set("CLAIM_TENANT", "arcad_tenant"); err != nil {
|
||||
panic(errors.New("could not set 'CLAIM_TENANT' property"))
|
||||
}
|
||||
|
||||
if err := o.Set("CLAIM_ENTRYPOINT", "arcad_entrypoint"); err != nil {
|
||||
panic(errors.New("could not set 'CLAIM_ENTRYPOINT' property"))
|
||||
}
|
||||
|
||||
if err := o.Set("CLAIM_ROLE", "arcad_role"); err != nil {
|
||||
panic(errors.New("could not set 'CLAIM_ROLE' property"))
|
||||
}
|
||||
|
||||
if err := o.Set("CLAIM_PREFERRED_USERNAME", "preferred_username"); err != nil {
|
||||
panic(errors.New("could not set 'CLAIM_PREFERRED_USERNAME' property"))
|
||||
}
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) getJWTKeySet() (jwk.Set, error) {
|
||||
return s.keySet, nil
|
||||
}
|
||||
|
||||
func NewServer(bundle bundle.Bundle, db *sql.DB, auth *appSpec.Auth) *Server {
|
||||
func NewServer(bundle bundle.Bundle, config *spec.Config, handlerOptions ...edgeHTTP.HandlerOptionFunc) *Server {
|
||||
return &Server{
|
||||
bundle: bundle,
|
||||
db: db,
|
||||
auth: auth,
|
||||
bundle: bundle,
|
||||
config: config,
|
||||
handlerOptions: handlerOptions,
|
||||
}
|
||||
}
|
||||
|
||||
func getCookieDomain(r *http.Request) (string, error) {
|
||||
host, _, err := net.SplitHostPort(r.Host)
|
||||
if err != nil {
|
||||
host = r.Host
|
||||
}
|
||||
|
||||
// If host is an IP address
|
||||
if ip := net.ParseIP(host); ip != nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// If host is an domain, return top level domain
|
||||
domainParts := strings.Split(host, ".")
|
||||
if len(domainParts) >= 2 {
|
||||
topLevelDomain := strings.Join(domainParts[len(domainParts)-2:], ".")
|
||||
return topLevelDomain, nil
|
||||
}
|
||||
|
||||
// By default, return host
|
||||
return host, nil
|
||||
}
|
||||
|
||||
func unexpectedHostRedirect(hostTarget string, acceptedHostPatterns ...string) func(http.Handler) http.Handler {
|
||||
return func(h http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
host, port, err := net.SplitHostPort(r.Host)
|
||||
if err != nil {
|
||||
host = r.Host
|
||||
}
|
||||
|
||||
matched := wildcard.MatchAny(host, acceptedHostPatterns...)
|
||||
|
||||
if !matched {
|
||||
url := r.URL
|
||||
|
||||
url.Host = hostTarget
|
||||
if port != "" {
|
||||
url.Host += ":" + port
|
||||
}
|
||||
|
||||
http.Redirect(w, r, url.String(), http.StatusTemporaryRedirect)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
h.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
package app
|
||||
package spec
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
@ -11,7 +11,7 @@ import (
|
||||
var schema []byte
|
||||
|
||||
func init() {
|
||||
if err := spec.Register(NameApp, schema); err != nil {
|
||||
if err := spec.Register(Name, schema); err != nil {
|
||||
panic(errors.WithStack(err))
|
||||
}
|
||||
}
|
135
internal/agent/controller/app/spec/schema.json
Normal file
135
internal/agent/controller/app/spec/schema.json
Normal file
@ -0,0 +1,135 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://app.edge.emissary.cadoles.com/spec.json",
|
||||
"title": "AppSpec",
|
||||
"description": "Emissary 'App' specification",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"apps": {
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
".*": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"url": {
|
||||
"type": "string"
|
||||
},
|
||||
"sha256sum": {
|
||||
"type": "string"
|
||||
},
|
||||
"address": {
|
||||
"type": "string"
|
||||
},
|
||||
"format": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"zip",
|
||||
"tar.gz"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"url",
|
||||
"sha256sum",
|
||||
"address",
|
||||
"format"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"appUrlResolving": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ifaceMappings": {
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
".*": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultUrlTemplate": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["defaultUrlTemplate"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"unexpectedHostRedirect": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"acceptedHostPatterns": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"hostTarget": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["acceptedHostPatterns", "hostTarget"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"auth": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"local": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": ["object", "string"]
|
||||
},
|
||||
"accounts": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"username": {
|
||||
"type": "string"
|
||||
},
|
||||
"password": {
|
||||
"type": "string"
|
||||
},
|
||||
"algo": {
|
||||
"type": "string"
|
||||
},
|
||||
"claims": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"username",
|
||||
"password",
|
||||
"algo"
|
||||
]
|
||||
}
|
||||
},
|
||||
"cookieDomain": {
|
||||
"type": "string"
|
||||
},
|
||||
"cookieDuration": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"key"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"apps"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
71
internal/agent/controller/app/spec/spec.go
Normal file
71
internal/agent/controller/app/spec/spec.go
Normal file
@ -0,0 +1,71 @@
|
||||
package spec
|
||||
|
||||
import (
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/spec"
|
||||
edgeAuth "forge.cadoles.com/arcad/edge/pkg/module/auth/http"
|
||||
)
|
||||
|
||||
const Name spec.Name = "app.emissary.cadoles.com"
|
||||
|
||||
type Spec struct {
|
||||
Revision int `json:"revision"`
|
||||
Apps map[string]AppEntry `json:"apps"`
|
||||
Config *Config `json:"config"`
|
||||
}
|
||||
|
||||
type AppEntry struct {
|
||||
URL string `json:"url"`
|
||||
SHA256Sum string `json:"sha256sum"`
|
||||
Address string `json:"address"`
|
||||
Format string `json:"format"`
|
||||
}
|
||||
|
||||
type Auth struct {
|
||||
Local *LocalAuth `json:"local,omitempty"`
|
||||
}
|
||||
|
||||
type LocalAuth struct {
|
||||
Key any `json:"key"`
|
||||
Accounts []edgeAuth.LocalAccount `json:"accounts"`
|
||||
CookieDomain string `json:"cookieDomain"`
|
||||
CookieDuration string `json:"cookieDuration"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Auth *Auth `json:"auth"`
|
||||
UnexpectedHostRedirect *UnexpectedHostRedirect `json:"unexpectedHostRedirect"`
|
||||
AppURLResolving *AppURLResolving `json:"appUrlResolving"`
|
||||
}
|
||||
|
||||
type UnexpectedHostRedirect struct {
|
||||
AcceptedHostPatterns []string `json:"acceptedHostPatterns"`
|
||||
HostTarget string `json:"hostTarget"`
|
||||
}
|
||||
|
||||
type AppURLResolving struct {
|
||||
IfaceMappings map[string]string `json:"ifaceMappings"`
|
||||
DefaultURLTemplate string `json:"defaultUrlTemplate"`
|
||||
}
|
||||
|
||||
func (s *Spec) SpecName() spec.Name {
|
||||
return Name
|
||||
}
|
||||
|
||||
func (s *Spec) SpecRevision() int {
|
||||
return s.Revision
|
||||
}
|
||||
|
||||
func (s *Spec) SpecData() map[string]any {
|
||||
return map[string]any{
|
||||
"apps": s.Apps,
|
||||
"config": s.Config,
|
||||
}
|
||||
}
|
||||
|
||||
func NewSpec() *Spec {
|
||||
return &Spec{
|
||||
Revision: -1,
|
||||
}
|
||||
}
|
||||
|
||||
var _ spec.Spec = &Spec{}
|
54
internal/agent/controller/app/spec/testdata/spec-ok.json
vendored
Normal file
54
internal/agent/controller/app/spec/testdata/spec-ok.json
vendored
Normal file
@ -0,0 +1,54 @@
|
||||
{
|
||||
"name": "app.emissary.cadoles.com",
|
||||
"data": {
|
||||
"apps": {
|
||||
"edge.sdk.client.test": {
|
||||
"url": "http://example.com/edge.sdk.client.test_0.0.0.zip",
|
||||
"sha256sum": "58019192dacdae17755707719707db007e26dac856102280583fbd18427dd352",
|
||||
"address": ":8081",
|
||||
"format": "zip"
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"auth": {
|
||||
"local": {
|
||||
"key": {
|
||||
"d": "YOre0WZefGfUGFvDg42oL5Oad5Zsb1N_hqPyLVM5ajpTZzcHpB3wT6In9tFO_VshB6lxVtPA9ckPkpMTFY7ygt1Yomc1HkoOKRtmIaqdr4VgNQifU-4yiLiJkSbdYSeMV-KkkN8mGR1keJpJeS34W1X0W6CkU2nw7F5VueBCJfWJA0funRfuWdI68MTUgT9kRZFp-SfvptvRL6jVYHV_5hqxzHCvgEdBSF6QKwx4M6P6QBMt7ft6uMLmFx9abKFw2V51hX3PkxiSepVB3w5CYg4HtS3AHX6bILL4m0R2pdTIkap7i3tkH_xAOuKWt8D6JhadI8X1rEAwXmCS5KrRgQ",
|
||||
"dp": "U0HfvBC6hk-SCpuotGIv3vbHCVt1aF3SHK0y32EYCOe8e_9G6YCEILfcvEJ5fiOCc2kvx6TasHQu4qj1uWRKenZlK1sJ6KDybGCkZL1D3jYnbeLZYBuWBL__YbZiST3ewbxzj_EDMWiZ8sUltahza_1weSgg8auSzTHS2LJBHIE",
|
||||
"dq": "hVom4ScDxgqhCsQNVpZlN7M3v0tgWjl_gTOHjOyzKCHQJeC0QmJJaMKkQZPWJ8jjLqy7VwVpqC2nZU7QDuX1Cq5eJDQcXi9XtaAfIBico9WcYDre6mDyhL588YHpekyRke8HnZ810iesr0G3gU1h0QvZVVuW-pXTJOXhZTt6nFc",
|
||||
"e": "AQAB",
|
||||
"kty": "RSA",
|
||||
"n": "vPnpkE3-HfNgJSru_K40LstkjiG2Bq_Tt-m0d_yUBBSbirFxF3qH4EXi7WrtZdeDahg2iV2BvpbVVj9GlmGo9OLol6jc7AP2yvZrkbABiiJhCbuPdkYbNpx6B7Itl8RT_bUSYAMZhmux5lpsn4weQ01fzjICi1rA-bIJpOfotdOjP4_lol-LxGZOGJQv9kndP8bgmssJb3Y_2s4gPtkmXySLrhpr5So-_6dVksyuBD9aLcnsMLDbywusjEMCdhqzQbvOjryomnmEXwyz_Ewb5HFK2PfgFtoHkdjqDz-mrEs3tw5g4TdYhCftzJxgbyNAEq4aEiOQrAncYyrXlotP_w",
|
||||
"p": "8TNMF0WUe7CEeNVUTsuEcBAAXRguNtpvVifIjlwzFRGOYVGIpKuHsqQPKlZL07I9gPr9LifQnyQus3oEmTOrVs6LB9sfbukbg43ZRKoGVM40JYF5Xjs7R3mEZhgU0WaYOVe3iLtBGMfXNWFwlbfQP-zEb-dPCBX1jWT3LdgNBcE",
|
||||
"q": "yJJLNc9w6O4y2icME8k99FugV9E7ObwUxF3v5JN3y1cmAT0h2njyE3iAGqaDZwcY1_jGCisjwoqX6i5E8xqhxX3Gcy3J7SmUAf8fhY8wU3zv9DK7skg2IdvanDb8Y1OM6GchbYZAOVPEg2IvVio8zI-Ih3DDwDk8Df0ufzoHRb8",
|
||||
"qi": "zOE-4R3cjPesm3MX-4PdwmsaF9QZLUVRUvvHJ08pKs6kAXP18hzjctAoOjhQDxlTYqNYNePfKzKwost3OJoPgRIc9w9qwUCK1gNOS4Z_xozCIaXgMddNFhkoAfZ4JaKjNCiinzjGfqG99Lf-yzmmREuuhRv7SdS3ST4VQjiJQew"
|
||||
},
|
||||
"accounts": [
|
||||
{
|
||||
"username": "foo",
|
||||
"algo": "plain",
|
||||
"password": "bar",
|
||||
"claims": {
|
||||
"arcad_role": "user",
|
||||
"arcad_tenant": "dev.cli",
|
||||
"preferred_username": "Foo",
|
||||
"sub": "foo"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"unexpectedHostRedirect": {
|
||||
"acceptedHostPatterns": ["arcad.local", "*.arcad.local", "arcad-*.local", "*.*.*.*"],
|
||||
"hostTarget": "arcad.local"
|
||||
},
|
||||
"appUrlResolving": {
|
||||
"ifaceMappings": {
|
||||
"eth0": "http://{{ .DeviceIP }}:{{ .AppHost }}"
|
||||
},
|
||||
"defaultUrlTemplate": "http://{{ last ( splitList \".\" ( toString .Manifest.ID ) ) }}.arcad.local"
|
||||
}
|
||||
}
|
||||
},
|
||||
"revision": 0
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package app
|
||||
package spec
|
||||
|
||||
import (
|
||||
"context"
|
||||
@ -28,7 +28,7 @@ func TestValidator(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
validator := spec.NewValidator()
|
||||
if err := validator.Register(NameApp, schema); err != nil {
|
||||
if err := validator.Register(Name, schema); err != nil {
|
||||
t.Fatalf("+%v", errors.WithStack(err))
|
||||
}
|
||||
|
181
internal/agent/controller/mdns/controller.go
Normal file
181
internal/agent/controller/mdns/controller.go
Normal file
@ -0,0 +1,181 @@
|
||||
package mdns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/agent"
|
||||
mdns "forge.cadoles.com/Cadoles/emissary/internal/agent/controller/mdns/spec"
|
||||
"github.com/brutella/dnssd"
|
||||
"github.com/mitchellh/hashstructure/v2"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultDomain = "local"
|
||||
)
|
||||
|
||||
type Controller struct {
|
||||
serviceDefHash uint64
|
||||
cancel context.CancelFunc
|
||||
responder dnssd.Responder
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
// Name implements node.Controller.
|
||||
func (c *Controller) Name() string {
|
||||
return "mdns-controller"
|
||||
}
|
||||
|
||||
// Reconcile implements node.Controller.
|
||||
func (c *Controller) Reconcile(ctx context.Context, state *agent.State) error {
|
||||
mdnsSpec := mdns.NewSpec()
|
||||
|
||||
if err := state.GetSpec(mdns.Name, mdnsSpec); err != nil {
|
||||
if errors.Is(err, agent.ErrSpecNotFound) {
|
||||
logger.Info(ctx, "could not find mdns spec")
|
||||
|
||||
c.stopResponder(ctx)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
logger.Info(ctx, "retrieved spec", logger.F("spec", mdnsSpec.SpecName()), logger.F("revision", mdnsSpec.SpecRevision()))
|
||||
|
||||
if err := c.updateResponder(ctx, mdnsSpec); err != nil {
|
||||
return errors.Wrap(err, "could not update responder")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Controller) stopResponder(ctx context.Context) {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
|
||||
if c.responder == nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.cancel()
|
||||
c.responder = nil
|
||||
c.cancel = nil
|
||||
}
|
||||
|
||||
func (c *Controller) updateResponder(ctx context.Context, spec *mdns.Spec) error {
|
||||
serviceDef := struct {
|
||||
Services map[string]mdns.Service
|
||||
}{
|
||||
Services: spec.Services,
|
||||
}
|
||||
|
||||
newServerDefHash, err := hashstructure.Hash(serviceDef, hashstructure.FormatV2, nil)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
c.mutex.RLock()
|
||||
if newServerDefHash == c.serviceDefHash && c.responder != nil {
|
||||
c.mutex.RUnlock()
|
||||
return nil
|
||||
}
|
||||
c.mutex.RUnlock()
|
||||
|
||||
c.stopResponder(ctx)
|
||||
|
||||
defaultIfaces, err := c.getDefaultIfaces()
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
services := make([]dnssd.Service, 0, len(spec.Services))
|
||||
|
||||
for name, service := range spec.Services {
|
||||
domain := service.Domain
|
||||
if domain == "" {
|
||||
domain = DefaultDomain
|
||||
}
|
||||
|
||||
ifaces := service.Ifaces
|
||||
if len(ifaces) == 0 {
|
||||
ifaces = defaultIfaces
|
||||
}
|
||||
|
||||
config := dnssd.Config{
|
||||
Name: name,
|
||||
Type: service.Type,
|
||||
Domain: domain,
|
||||
Host: service.Host,
|
||||
Ifaces: ifaces,
|
||||
Port: service.Port,
|
||||
}
|
||||
|
||||
service, err := dnssd.NewService(config)
|
||||
if err != nil {
|
||||
logger.Error(ctx, "could not create mdns service", logger.E(errors.WithStack(err)))
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
services = append(services, service)
|
||||
}
|
||||
|
||||
responder, err := dnssd.NewResponder()
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
for _, service := range services {
|
||||
if _, err := responder.Add(service); err != nil {
|
||||
logger.Error(ctx, "could not add mdns service", logger.E(errors.WithStack(err)))
|
||||
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
c.responder = responder
|
||||
c.cancel = cancel
|
||||
c.serviceDefHash = newServerDefHash
|
||||
|
||||
go func() {
|
||||
defer c.stopResponder(ctx)
|
||||
|
||||
if err := responder.Respond(ctx); err != nil && !errors.Is(err, context.Canceled) {
|
||||
logger.Error(ctx, "could not respond to mdns queries", logger.E(errors.WithStack(err)))
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Controller) getDefaultIfaces() ([]string, error) {
|
||||
ifaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
ifaceNames := make([]string, len(ifaces))
|
||||
|
||||
for idx, ifa := range ifaces {
|
||||
ifaceNames[idx] = ifa.Name
|
||||
}
|
||||
|
||||
return ifaceNames, nil
|
||||
}
|
||||
|
||||
func NewController() *Controller {
|
||||
return &Controller{
|
||||
cancel: nil,
|
||||
responder: nil,
|
||||
serviceDefHash: 0,
|
||||
}
|
||||
}
|
||||
|
||||
var _ agent.Controller = &Controller{}
|
17
internal/agent/controller/mdns/spec/init.go
Normal file
17
internal/agent/controller/mdns/spec/init.go
Normal file
@ -0,0 +1,17 @@
|
||||
package spec
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/spec"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
//go:embed schema.json
|
||||
var schema []byte
|
||||
|
||||
func init() {
|
||||
if err := spec.Register(Name, schema); err != nil {
|
||||
panic(errors.WithStack(err))
|
||||
}
|
||||
}
|
47
internal/agent/controller/mdns/spec/schema.json
Normal file
47
internal/agent/controller/mdns/spec/schema.json
Normal file
@ -0,0 +1,47 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://mdns.edge.emissary.cadoles.com/spec.json",
|
||||
"title": "MDNSSpec",
|
||||
"description": "Emissary 'MDNS' specification",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"services": {
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
".*": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"domain": {
|
||||
"type": "string"
|
||||
},
|
||||
"host": {
|
||||
"type": "string"
|
||||
},
|
||||
"ifaces": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"port": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"host",
|
||||
"port"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"services"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
42
internal/agent/controller/mdns/spec/spec.go
Normal file
42
internal/agent/controller/mdns/spec/spec.go
Normal file
@ -0,0 +1,42 @@
|
||||
package spec
|
||||
|
||||
import (
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/spec"
|
||||
)
|
||||
|
||||
const Name spec.Name = "mdns.emissary.cadoles.com"
|
||||
|
||||
type Spec struct {
|
||||
Revision int `json:"revision"`
|
||||
Services map[string]Service `json:"services"`
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
Type string `json:"type"`
|
||||
Domain string `json:"domain"`
|
||||
Host string `json:"host"`
|
||||
Ifaces []string `json:"ifaces"`
|
||||
Port int `json:"port"`
|
||||
}
|
||||
|
||||
func (s *Spec) SpecName() spec.Name {
|
||||
return Name
|
||||
}
|
||||
|
||||
func (s *Spec) SpecRevision() int {
|
||||
return s.Revision
|
||||
}
|
||||
|
||||
func (s *Spec) SpecData() map[string]any {
|
||||
return map[string]any{
|
||||
"services": s.Services,
|
||||
}
|
||||
}
|
||||
|
||||
func NewSpec() *Spec {
|
||||
return &Spec{
|
||||
Revision: -1,
|
||||
}
|
||||
}
|
||||
|
||||
var _ spec.Spec = &Spec{}
|
15
internal/agent/controller/mdns/spec/testdata/spec-ok.json
vendored
Normal file
15
internal/agent/controller/mdns/spec/testdata/spec-ok.json
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "mdns.emissary.cadoles.com",
|
||||
"data": {
|
||||
"services": {
|
||||
"My Website": {
|
||||
"type": "_http._tcp",
|
||||
"domain": "local",
|
||||
"host": "mywebsite",
|
||||
"ifaces": ["lo", "eth0"],
|
||||
"port": 80
|
||||
}
|
||||
}
|
||||
},
|
||||
"revision": 0
|
||||
}
|
65
internal/agent/controller/mdns/spec/validator_test.go
Normal file
65
internal/agent/controller/mdns/spec/validator_test.go
Normal file
@ -0,0 +1,65 @@
|
||||
package spec
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/spec"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type validatorTestCase struct {
|
||||
Name string
|
||||
Source string
|
||||
ShouldFail bool
|
||||
}
|
||||
|
||||
var validatorTestCases = []validatorTestCase{
|
||||
{
|
||||
Name: "SpecOK",
|
||||
Source: "testdata/spec-ok.json",
|
||||
ShouldFail: false,
|
||||
},
|
||||
}
|
||||
|
||||
func TestValidator(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
validator := spec.NewValidator()
|
||||
if err := validator.Register(Name, schema); err != nil {
|
||||
t.Fatalf("+%v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
for _, tc := range validatorTestCases {
|
||||
func(tc validatorTestCase) {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rawSpec, err := ioutil.ReadFile(tc.Source)
|
||||
if err != nil {
|
||||
t.Fatalf("+%v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
var spec spec.RawSpec
|
||||
|
||||
if err := json.Unmarshal(rawSpec, &spec); err != nil {
|
||||
t.Fatalf("+%v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
err = validator.Validate(ctx, &spec)
|
||||
|
||||
if !tc.ShouldFail && err != nil {
|
||||
t.Errorf("+%v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
if tc.ShouldFail && err == nil {
|
||||
t.Error("validation should have failed")
|
||||
}
|
||||
})
|
||||
}(tc)
|
||||
}
|
||||
}
|
@ -5,8 +5,8 @@ import (
|
||||
"net/url"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/agent"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/spec/proxy"
|
||||
edgeProxy "forge.cadoles.com/arcad/edge/pkg/proxy"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/proxy"
|
||||
spec "forge.cadoles.com/Cadoles/emissary/internal/spec/proxy"
|
||||
"github.com/mitchellh/hashstructure/v2"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
@ -18,7 +18,7 @@ type proxyEntry struct {
|
||||
}
|
||||
|
||||
type Controller struct {
|
||||
proxies map[proxy.ID]*proxyEntry
|
||||
proxies map[spec.ID]*proxyEntry
|
||||
}
|
||||
|
||||
// Name implements node.Controller.
|
||||
@ -28,9 +28,9 @@ func (c *Controller) Name() string {
|
||||
|
||||
// Reconcile implements node.Controller.
|
||||
func (c *Controller) Reconcile(ctx context.Context, state *agent.State) error {
|
||||
proxySpec := proxy.NewSpec()
|
||||
proxySpec := spec.NewSpec()
|
||||
|
||||
if err := state.GetSpec(proxy.NameProxy, proxySpec); err != nil {
|
||||
if err := state.GetSpec(spec.NameProxy, proxySpec); err != nil {
|
||||
if errors.Is(err, agent.ErrSpecNotFound) {
|
||||
logger.Info(ctx, "could not find proxy spec")
|
||||
|
||||
@ -69,7 +69,7 @@ func (c *Controller) stopAllProxies(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Controller) updateProxies(ctx context.Context, spec *proxy.Spec) {
|
||||
func (c *Controller) updateProxies(ctx context.Context, spec *spec.Spec) {
|
||||
// Stop and remove obsolete proxys
|
||||
for proxyID, entry := range c.proxies {
|
||||
if _, exists := spec.Proxies[proxyID]; exists {
|
||||
@ -100,7 +100,7 @@ func (c *Controller) updateProxies(ctx context.Context, spec *proxy.Spec) {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Controller) updateProxy(ctx context.Context, proxyID proxy.ID, proxySpec proxy.ProxyEntry) (err error) {
|
||||
func (c *Controller) updateProxy(ctx context.Context, proxyID spec.ID, proxySpec spec.ProxyEntry) (err error) {
|
||||
newProxySpecHash, err := hashstructure.Hash(proxySpec, hashstructure.FormatV2, nil)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
@ -140,7 +140,7 @@ func (c *Controller) updateProxy(ctx context.Context, proxyID proxy.ID, proxySpe
|
||||
)
|
||||
}
|
||||
|
||||
options := make([]edgeProxy.OptionFunc, 0)
|
||||
options := make([]proxy.OptionFunc, 0)
|
||||
allowedHosts := make([]string, len(proxySpec.Mappings))
|
||||
mappings := make(map[string]*url.URL, len(proxySpec.Mappings))
|
||||
|
||||
@ -156,8 +156,8 @@ func (c *Controller) updateProxy(ctx context.Context, proxyID proxy.ID, proxySpe
|
||||
|
||||
options = append(
|
||||
options,
|
||||
edgeProxy.WithAllowedHosts(allowedHosts...),
|
||||
edgeProxy.WithRewriteHosts(mappings),
|
||||
proxy.WithAllowedHosts(allowedHosts...),
|
||||
proxy.WithRewriteHosts(mappings),
|
||||
)
|
||||
|
||||
if err := entry.Proxy.Start(ctx, proxySpec.Address, options...); err != nil {
|
||||
@ -173,7 +173,7 @@ func (c *Controller) updateProxy(ctx context.Context, proxyID proxy.ID, proxySpe
|
||||
|
||||
func NewController() *Controller {
|
||||
return &Controller{
|
||||
proxies: make(map[proxy.ID]*proxyEntry),
|
||||
proxies: make(map[spec.ID]*proxyEntry),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,10 +5,9 @@ import (
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/proxy"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/proxy"
|
||||
)
|
||||
|
||||
type ReverseProxy struct {
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/auth"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||
@ -13,8 +14,11 @@ import (
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
const DefaultAcceptableSkew = 5 * time.Minute
|
||||
|
||||
type Authenticator struct {
|
||||
repo datastore.AgentRepository
|
||||
repo datastore.AgentRepository
|
||||
acceptableSkew time.Duration
|
||||
}
|
||||
|
||||
// Authenticate implements auth.Authenticator.
|
||||
@ -71,11 +75,19 @@ func (a *Authenticator) Authenticate(ctx context.Context, r *http.Request) (auth
|
||||
[]byte(rawToken),
|
||||
jwt.WithKeySet(agent.KeySet.Set, jws.WithRequireKid(false)),
|
||||
jwt.WithValidate(true),
|
||||
jwt.WithAcceptableSkew(a.acceptableSkew),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
contactedAt := time.Now()
|
||||
|
||||
agent, err = a.repo.Update(ctx, agent.ID, datastore.WithAgentUpdateContactedAt(contactedAt))
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
user := &User{
|
||||
agent: agent,
|
||||
}
|
||||
@ -83,9 +95,10 @@ func (a *Authenticator) Authenticate(ctx context.Context, r *http.Request) (auth
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func NewAuthenticator(repo datastore.AgentRepository) *Authenticator {
|
||||
func NewAuthenticator(repo datastore.AgentRepository, acceptableSkew time.Duration) *Authenticator {
|
||||
return &Authenticator{
|
||||
repo: repo,
|
||||
repo: repo,
|
||||
acceptableSkew: acceptableSkew,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -18,7 +18,7 @@ func GenerateToken(key jwk.Key, thumbprint string) (string, error) {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
now := time.Now().UTC()
|
||||
|
||||
if err := token.Set(jwt.NotBeforeKey, now); err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
|
17
internal/auth/thirdparty/authenticator.go
vendored
17
internal/auth/thirdparty/authenticator.go
vendored
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/auth"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/jwk"
|
||||
@ -11,9 +12,12 @@ import (
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
const DefaultAcceptableSkew = 5 * time.Minute
|
||||
|
||||
type Authenticator struct {
|
||||
keys jwk.Set
|
||||
issuer string
|
||||
keys jwk.Set
|
||||
issuer string
|
||||
acceptableSkew time.Duration
|
||||
}
|
||||
|
||||
// Authenticate implements auth.Authenticator.
|
||||
@ -30,7 +34,7 @@ func (a *Authenticator) Authenticate(ctx context.Context, r *http.Request) (auth
|
||||
return nil, errors.WithStack(auth.ErrUnauthenticated)
|
||||
}
|
||||
|
||||
token, err := parseToken(ctx, a.keys, a.issuer, rawToken)
|
||||
token, err := parseToken(ctx, a.keys, a.issuer, rawToken, a.acceptableSkew)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
@ -57,10 +61,11 @@ func (a *Authenticator) Authenticate(ctx context.Context, r *http.Request) (auth
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func NewAuthenticator(keys jwk.Set, issuer string) *Authenticator {
|
||||
func NewAuthenticator(keys jwk.Set, issuer string, acceptableSkew time.Duration) *Authenticator {
|
||||
return &Authenticator{
|
||||
keys: keys,
|
||||
issuer: issuer,
|
||||
keys: keys,
|
||||
issuer: issuer,
|
||||
acceptableSkew: acceptableSkew,
|
||||
}
|
||||
}
|
||||
|
||||
|
5
internal/auth/thirdparty/jwt.go
vendored
5
internal/auth/thirdparty/jwt.go
vendored
@ -13,12 +13,13 @@ import (
|
||||
|
||||
const keyRole = "role"
|
||||
|
||||
func parseToken(ctx context.Context, keys jwk.Set, issuer string, rawToken string) (jwt.Token, error) {
|
||||
func parseToken(ctx context.Context, keys jwk.Set, issuer string, rawToken string, acceptableSkew time.Duration) (jwt.Token, error) {
|
||||
token, err := jwt.Parse(
|
||||
[]byte(rawToken),
|
||||
jwt.WithKeySet(keys, jws.WithRequireKid(false)),
|
||||
jwt.WithIssuer(issuer),
|
||||
jwt.WithValidate(true),
|
||||
jwt.WithAcceptableSkew(acceptableSkew),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
@ -42,7 +43,7 @@ func GenerateToken(ctx context.Context, key jwk.Key, issuer, subject string, rol
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
now := time.Now().UTC()
|
||||
|
||||
if err := token.Set(jwt.NotBeforeKey, now); err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
|
@ -10,6 +10,7 @@ import (
|
||||
|
||||
type UpdateAgentOptions struct {
|
||||
Status *int
|
||||
Label *string
|
||||
Options []OptionFunc
|
||||
}
|
||||
|
||||
@ -21,6 +22,12 @@ func WithAgentStatus(status int) UpdateAgentOptionFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func WithAgentLabel(label string) UpdateAgentOptionFunc {
|
||||
return func(opts *UpdateAgentOptions) {
|
||||
opts.Label = &label
|
||||
}
|
||||
}
|
||||
|
||||
func WithUpdateAgentsOptions(funcs ...OptionFunc) UpdateAgentOptionFunc {
|
||||
return func(opts *UpdateAgentOptions) {
|
||||
opts.Options = funcs
|
||||
@ -39,6 +46,10 @@ func (c *Client) UpdateAgent(ctx context.Context, agentID datastore.AgentID, fun
|
||||
payload["status"] = *opts.Status
|
||||
}
|
||||
|
||||
if opts.Label != nil {
|
||||
payload["label"] = *opts.Label
|
||||
}
|
||||
|
||||
response := withResponse[struct {
|
||||
Agent *datastore.Agent `json:"agent"`
|
||||
}]()
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/agent"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/agent/controller/app"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/agent/controller/mdns"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/agent/controller/openwrt"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/agent/controller/persistence"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/agent/controller/proxy"
|
||||
@ -49,10 +50,6 @@ func RunCommand() *cli.Command {
|
||||
controllers = append(controllers, spec.NewController())
|
||||
}
|
||||
|
||||
if ctrlConf.Proxy.Enabled {
|
||||
controllers = append(controllers, proxy.NewController())
|
||||
}
|
||||
|
||||
if ctrlConf.UCI.Enabled {
|
||||
controllers = append(controllers, openwrt.NewUCIController(
|
||||
string(ctrlConf.UCI.BinPath),
|
||||
@ -66,6 +63,14 @@ func RunCommand() *cli.Command {
|
||||
))
|
||||
}
|
||||
|
||||
if ctrlConf.Proxy.Enabled {
|
||||
controllers = append(controllers, proxy.NewController())
|
||||
}
|
||||
|
||||
if ctrlConf.MDNS.Enabled {
|
||||
controllers = append(controllers, mdns.NewController())
|
||||
}
|
||||
|
||||
if ctrlConf.SysUpgrade.Enabled {
|
||||
sysUpgradeArgs := make([]string, 0)
|
||||
if len(ctrlConf.SysUpgrade.SysUpgradeCommand) > 1 {
|
||||
|
@ -22,6 +22,11 @@ func UpdateCommand() *cli.Command {
|
||||
Usage: "Set `STATUS` to selected agent",
|
||||
Value: -1,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "label",
|
||||
Usage: "Set `LABEL` to selected agent",
|
||||
Value: "",
|
||||
},
|
||||
),
|
||||
Action: func(ctx *cli.Context) error {
|
||||
baseFlags := clientFlag.GetBaseFlags(ctx)
|
||||
@ -43,6 +48,11 @@ func UpdateCommand() *cli.Command {
|
||||
options = append(options, client.WithAgentStatus(status))
|
||||
}
|
||||
|
||||
label := ctx.String("label")
|
||||
if label != "" {
|
||||
options = append(options, client.WithAgentLabel(label))
|
||||
}
|
||||
|
||||
client := client.New(baseFlags.ServerURL, client.WithToken(token))
|
||||
|
||||
agent, err := client.UpdateAgent(ctx.Context, agentID, options...)
|
||||
|
@ -7,9 +7,10 @@ func agentHints(outputMode format.OutputMode) format.Hints {
|
||||
OutputMode: outputMode,
|
||||
Props: []format.Prop{
|
||||
format.NewProp("ID", "ID"),
|
||||
format.NewProp("Label", "Label"),
|
||||
format.NewProp("Thumbprint", "Thumbprint"),
|
||||
format.NewProp("Status", "Status"),
|
||||
format.NewProp("CreatedAt", "CreatedAt"),
|
||||
format.NewProp("ContactedAt", "ContactedAt"),
|
||||
format.NewProp("UpdatedAt", "UpdatedAt"),
|
||||
},
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ type ControllersConfig struct {
|
||||
UCI UCIControllerConfig `yaml:"uci"`
|
||||
App AppControllerConfig `yaml:"app"`
|
||||
SysUpgrade SysUpgradeControllerConfig `yaml:"sysupgrade"`
|
||||
MDNS MDNSControllerConfig `yaml:"mdns"`
|
||||
}
|
||||
|
||||
type PersistenceControllerConfig struct {
|
||||
@ -55,6 +56,10 @@ type SysUpgradeControllerConfig struct {
|
||||
FirmwareVersionCommand InterpolatedStringSlice `yaml:"firmwareVersionCommand"`
|
||||
}
|
||||
|
||||
type MDNSControllerConfig struct {
|
||||
Enabled InterpolatedBool `yaml:"enabled"`
|
||||
}
|
||||
|
||||
func NewDefaultAgentConfig() AgentConfig {
|
||||
return AgentConfig{
|
||||
ServerURL: "http://127.0.0.1:3000",
|
||||
@ -86,6 +91,9 @@ func NewDefaultAgentConfig() AgentConfig {
|
||||
SysUpgradeCommand: InterpolatedStringSlice{"sysupgrade", "--force", "-u", "-v", openwrt.FirmwareFileTemplate},
|
||||
FirmwareVersionCommand: InterpolatedStringSlice{"sh", "-c", `source /etc/openwrt_release && echo "$DISTRIB_ID-$DISTRIB_RELEASE-$DISTRIB_REVISION"`},
|
||||
},
|
||||
MDNS: MDNSControllerConfig{
|
||||
Enabled: true,
|
||||
},
|
||||
},
|
||||
Collectors: []ShellCollectorConfig{
|
||||
{
|
||||
|
@ -15,6 +15,6 @@ type DatabaseConfig struct {
|
||||
func NewDefaultDatabaseConfig() DatabaseConfig {
|
||||
return DatabaseConfig{
|
||||
Driver: "sqlite",
|
||||
DSN: "sqlite://emissary.sqlite",
|
||||
DSN: "sqlite://emissary.sqlite?_pragma=foreign_keys(1)&_pragma=busy_timeout=60000",
|
||||
}
|
||||
}
|
||||
|
@ -20,13 +20,15 @@ const (
|
||||
)
|
||||
|
||||
type Agent struct {
|
||||
ID AgentID `json:"id"`
|
||||
Thumbprint string `json:"thumbprint"`
|
||||
KeySet *SerializableKeySet `json:"keyset,omitempty"`
|
||||
Metadata map[string]any `json:"metadata,omitempty"`
|
||||
Status AgentStatus `json:"status"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
ID AgentID `json:"id"`
|
||||
Label string `json:"label"`
|
||||
Thumbprint string `json:"thumbprint"`
|
||||
KeySet *SerializableKeySet `json:"keyset,omitempty"`
|
||||
Metadata map[string]any `json:"metadata,omitempty"`
|
||||
Status AgentStatus `json:"status"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
ContactedAt *time.Time `json:"contactedAt,omitempty"`
|
||||
}
|
||||
|
||||
type SerializableKeySet struct {
|
||||
|
@ -2,6 +2,7 @@ package datastore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/lestrrat-go/jwx/v2/jwk"
|
||||
)
|
||||
@ -68,10 +69,12 @@ func WithAgentQueryThumbprints(thumbprints ...string) AgentQueryOptionFunc {
|
||||
type AgentUpdateOptionFunc func(*AgentUpdateOptions)
|
||||
|
||||
type AgentUpdateOptions struct {
|
||||
Status *AgentStatus
|
||||
Metadata *map[string]any
|
||||
KeySet *jwk.Set
|
||||
Thumbprint *string
|
||||
Label *string
|
||||
Status *AgentStatus
|
||||
ContactedAt *time.Time
|
||||
Metadata *map[string]any
|
||||
KeySet *jwk.Set
|
||||
Thumbprint *string
|
||||
}
|
||||
|
||||
func WithAgentUpdateStatus(status AgentStatus) AgentUpdateOptionFunc {
|
||||
@ -97,3 +100,15 @@ func WithAgentUpdateThumbprint(thumbprint string) AgentUpdateOptionFunc {
|
||||
opts.Thumbprint = &thumbprint
|
||||
}
|
||||
}
|
||||
|
||||
func WithAgentUpdateLabel(label string) AgentUpdateOptionFunc {
|
||||
return func(opts *AgentUpdateOptions) {
|
||||
opts.Label = &label
|
||||
}
|
||||
}
|
||||
|
||||
func WithAgentUpdateContactedAt(contactedAt time.Time) AgentUpdateOptionFunc {
|
||||
return func(opts *AgentUpdateOptions) {
|
||||
opts.ContactedAt = &contactedAt
|
||||
}
|
||||
}
|
||||
|
@ -20,9 +20,24 @@ type AgentRepository struct {
|
||||
|
||||
// DeleteSpec implements datastore.AgentRepository.
|
||||
func (r *AgentRepository) DeleteSpec(ctx context.Context, agentID datastore.AgentID, name string) error {
|
||||
query := `DELETE FROM specs WHERE agent_id = $1 AND name = $2`
|
||||
err := r.withTx(ctx, func(tx *sql.Tx) error {
|
||||
exists, err := r.agentExists(ctx, tx, agentID)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
_, err := r.db.ExecContext(ctx, query, agentID, name)
|
||||
if !exists {
|
||||
return errors.WithStack(datastore.ErrNotFound)
|
||||
}
|
||||
|
||||
query := `DELETE FROM specs WHERE agent_id = $1 AND name = $2`
|
||||
|
||||
if _, err = tx.ExecContext(ctx, query, agentID, name); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
@ -34,31 +49,55 @@ func (r *AgentRepository) DeleteSpec(ctx context.Context, agentID datastore.Agen
|
||||
func (r *AgentRepository) GetSpecs(ctx context.Context, agentID datastore.AgentID) ([]*datastore.Spec, error) {
|
||||
specs := make([]*datastore.Spec, 0)
|
||||
|
||||
query := `
|
||||
err := r.withTx(ctx, func(tx *sql.Tx) error {
|
||||
exists, err := r.agentExists(ctx, tx, agentID)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if !exists {
|
||||
return errors.WithStack(datastore.ErrNotFound)
|
||||
}
|
||||
|
||||
query := `
|
||||
SELECT id, name, revision, data, created_at, updated_at
|
||||
FROM specs
|
||||
WHERE agent_id = $1
|
||||
`
|
||||
`
|
||||
|
||||
rows, err := r.db.QueryContext(ctx, query, agentID)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
spec := &datastore.Spec{}
|
||||
|
||||
data := JSONMap{}
|
||||
|
||||
if err := rows.Scan(&spec.ID, &spec.Name, &spec.Revision, &data, &spec.CreatedAt, &spec.UpdatedAt); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
rows, err := tx.QueryContext(ctx, query, agentID)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
spec.Data = data
|
||||
defer func() {
|
||||
if err := rows.Close(); err != nil {
|
||||
logger.Error(ctx, "could not close rows", logger.E(errors.WithStack(err)))
|
||||
}
|
||||
}()
|
||||
|
||||
specs = append(specs, spec)
|
||||
for rows.Next() {
|
||||
spec := &datastore.Spec{}
|
||||
|
||||
data := JSONMap{}
|
||||
|
||||
if err := rows.Scan(&spec.ID, &spec.Name, &spec.Revision, &data, &spec.CreatedAt, &spec.UpdatedAt); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
spec.Data = data
|
||||
|
||||
specs = append(specs, spec)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return specs, nil
|
||||
@ -69,6 +108,15 @@ func (r *AgentRepository) UpdateSpec(ctx context.Context, agentID datastore.Agen
|
||||
spec := &datastore.Spec{}
|
||||
|
||||
err := r.withTx(ctx, func(tx *sql.Tx) error {
|
||||
exists, err := r.agentExists(ctx, tx, agentID)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if !exists {
|
||||
return errors.WithStack(datastore.ErrNotFound)
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
|
||||
query := `
|
||||
@ -88,7 +136,7 @@ func (r *AgentRepository) UpdateSpec(ctx context.Context, agentID datastore.Agen
|
||||
|
||||
data := JSONMap{}
|
||||
|
||||
err := row.Scan(&spec.ID, &spec.Name, &spec.Revision, &data, &spec.CreatedAt, &spec.UpdatedAt)
|
||||
err = row.Scan(&spec.ID, &spec.Name, &spec.Revision, &data, &spec.CreatedAt, &spec.UpdatedAt)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return errors.WithStack(datastore.ErrUnexpectedRevision)
|
||||
@ -119,7 +167,7 @@ func (r *AgentRepository) Query(ctx context.Context, opts ...datastore.AgentQuer
|
||||
count := 0
|
||||
|
||||
err := r.withTx(ctx, func(tx *sql.Tx) error {
|
||||
query := `SELECT id, thumbprint, status, created_at, updated_at FROM agents`
|
||||
query := `SELECT id, label, thumbprint, status, contacted_at, created_at, updated_at FROM agents`
|
||||
|
||||
limit := 10
|
||||
if options.Limit != nil {
|
||||
@ -176,22 +224,34 @@ func (r *AgentRepository) Query(ctx context.Context, opts ...datastore.AgentQuer
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
defer func() {
|
||||
if err := rows.Close(); err != nil {
|
||||
logger.Error(ctx, "could not close rows", logger.E(errors.WithStack(err)))
|
||||
}
|
||||
}()
|
||||
|
||||
for rows.Next() {
|
||||
agent := &datastore.Agent{}
|
||||
|
||||
metadata := JSONMap{}
|
||||
contactedAt := sql.NullTime{}
|
||||
|
||||
if err := rows.Scan(&agent.ID, &agent.Thumbprint, &agent.Status, &agent.CreatedAt, &agent.UpdatedAt); err != nil {
|
||||
if err := rows.Scan(&agent.ID, &agent.Label, &agent.Thumbprint, &agent.Status, &contactedAt, &agent.CreatedAt, &agent.UpdatedAt); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
agent.Metadata = metadata
|
||||
if contactedAt.Valid {
|
||||
agent.ContactedAt = &contactedAt.Time
|
||||
}
|
||||
|
||||
agents = append(agents, agent)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
row := tx.QueryRowContext(ctx, `SELECT count(id) FROM agents `+filters, args...)
|
||||
if err := row.Scan(&count); err != nil {
|
||||
return errors.WithStack(err)
|
||||
@ -299,7 +359,7 @@ func (r *AgentRepository) Get(ctx context.Context, id datastore.AgentID) (*datas
|
||||
|
||||
err := r.withTx(ctx, func(tx *sql.Tx) error {
|
||||
query := `
|
||||
SELECT "id", "thumbprint", "keyset", "metadata", "status", "created_at", "updated_at"
|
||||
SELECT "id", "label", "thumbprint", "keyset", "metadata", "status", "contacted_at", "created_at", "updated_at"
|
||||
FROM agents
|
||||
WHERE id = $1
|
||||
`
|
||||
@ -307,9 +367,10 @@ func (r *AgentRepository) Get(ctx context.Context, id datastore.AgentID) (*datas
|
||||
row := r.db.QueryRowContext(ctx, query, id)
|
||||
|
||||
metadata := JSONMap{}
|
||||
contactedAt := sql.NullTime{}
|
||||
var rawKeySet []byte
|
||||
|
||||
if err := row.Scan(&agent.ID, &agent.Thumbprint, &rawKeySet, &metadata, &agent.Status, &agent.CreatedAt, &agent.UpdatedAt); err != nil {
|
||||
if err := row.Scan(&agent.ID, &agent.Label, &agent.Thumbprint, &rawKeySet, &metadata, &agent.Status, &contactedAt, &agent.CreatedAt, &agent.UpdatedAt); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return datastore.ErrNotFound
|
||||
}
|
||||
@ -318,6 +379,9 @@ func (r *AgentRepository) Get(ctx context.Context, id datastore.AgentID) (*datas
|
||||
}
|
||||
|
||||
agent.Metadata = metadata
|
||||
if contactedAt.Valid {
|
||||
agent.ContactedAt = &contactedAt.Time
|
||||
}
|
||||
|
||||
keySet := jwk.NewSet()
|
||||
if err := json.Unmarshal(rawKeySet, &keySet); err != nil {
|
||||
@ -346,15 +410,11 @@ func (r *AgentRepository) Update(ctx context.Context, id datastore.AgentID, opts
|
||||
|
||||
err := r.withTx(ctx, func(tx *sql.Tx) error {
|
||||
query := `
|
||||
UPDATE agents SET updated_at = $2
|
||||
UPDATE agents SET id = $1
|
||||
`
|
||||
|
||||
now := time.Now().UTC()
|
||||
|
||||
args := []any{
|
||||
id, now,
|
||||
}
|
||||
index := 3
|
||||
args := []any{id}
|
||||
index := 2
|
||||
|
||||
if options.Status != nil {
|
||||
query += fmt.Sprintf(`, status = $%d`, index)
|
||||
@ -379,23 +439,51 @@ func (r *AgentRepository) Update(ctx context.Context, id datastore.AgentID, opts
|
||||
index++
|
||||
}
|
||||
|
||||
if options.Label != nil {
|
||||
query += fmt.Sprintf(`, label = $%d`, index)
|
||||
args = append(args, *options.Label)
|
||||
index++
|
||||
}
|
||||
|
||||
if options.ContactedAt != nil {
|
||||
query += fmt.Sprintf(`, contacted_at = $%d`, index)
|
||||
utc := options.ContactedAt.UTC()
|
||||
args = append(args, utc)
|
||||
index++
|
||||
}
|
||||
|
||||
if options.Metadata != nil {
|
||||
query += fmt.Sprintf(`, metadata = $%d`, index)
|
||||
args = append(args, JSONMap(*options.Metadata))
|
||||
index++
|
||||
}
|
||||
|
||||
updated := options.Metadata != nil ||
|
||||
options.Status != nil ||
|
||||
options.Label != nil ||
|
||||
options.KeySet != nil ||
|
||||
options.Thumbprint != nil
|
||||
if updated {
|
||||
now := time.Now().UTC()
|
||||
query += fmt.Sprintf(`, updated_at = $%d`, index)
|
||||
args = append(args, now)
|
||||
index++
|
||||
}
|
||||
|
||||
query += `
|
||||
WHERE id = $1
|
||||
RETURNING "id", "thumbprint", "keyset", "metadata", "status", "created_at", "updated_at"
|
||||
RETURNING "id", "label", "thumbprint", "keyset", "metadata", "status", "contacted_at", "created_at", "updated_at"
|
||||
`
|
||||
|
||||
logger.Debug(ctx, "executing query", logger.F("query", query), logger.F("args", args))
|
||||
|
||||
row := tx.QueryRowContext(ctx, query, args...)
|
||||
|
||||
metadata := JSONMap{}
|
||||
contactedAt := sql.NullTime{}
|
||||
var rawKeySet []byte
|
||||
|
||||
if err := row.Scan(&agent.ID, &agent.Thumbprint, &rawKeySet, &metadata, &agent.Status, &agent.CreatedAt, &agent.UpdatedAt); err != nil {
|
||||
if err := row.Scan(&agent.ID, &agent.Label, &agent.Thumbprint, &rawKeySet, &metadata, &agent.Status, &contactedAt, &agent.CreatedAt, &agent.UpdatedAt); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return datastore.ErrNotFound
|
||||
}
|
||||
@ -404,6 +492,9 @@ func (r *AgentRepository) Update(ctx context.Context, id datastore.AgentID, opts
|
||||
}
|
||||
|
||||
agent.Metadata = metadata
|
||||
if contactedAt.Valid {
|
||||
agent.ContactedAt = &contactedAt.Time
|
||||
}
|
||||
|
||||
keySet := jwk.NewSet()
|
||||
if err := json.Unmarshal(rawKeySet, &keySet); err != nil {
|
||||
@ -421,8 +512,28 @@ func (r *AgentRepository) Update(ctx context.Context, id datastore.AgentID, opts
|
||||
return agent, nil
|
||||
}
|
||||
|
||||
func (r *AgentRepository) agentExists(ctx context.Context, tx *sql.Tx, agentID datastore.AgentID) (bool, error) {
|
||||
row := tx.QueryRowContext(ctx, `SELECT count(id) FROM agents WHERE id = $1`, agentID)
|
||||
|
||||
var count int
|
||||
|
||||
if err := row.Scan(&count); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return false, errors.WithStack(datastore.ErrNotFound)
|
||||
}
|
||||
|
||||
return false, errors.WithStack(err)
|
||||
}
|
||||
|
||||
if count == 0 {
|
||||
return false, errors.WithStack(datastore.ErrNotFound)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *AgentRepository) withTx(ctx context.Context, fn func(*sql.Tx) error) error {
|
||||
tx, err := r.db.Begin()
|
||||
tx, err := r.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
46
internal/datastore/sqlite/agent_repository_test.go
Normal file
46
internal/datastore/sqlite/agent_repository_test.go
Normal file
@ -0,0 +1,46 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/datastore/testsuite"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/migrate"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
func TestSQLiteAgentRepository(t *testing.T) {
|
||||
logger.SetLevel(logger.LevelDebug)
|
||||
|
||||
file := "testdata/agent_repository_test.sqlite"
|
||||
|
||||
if err := os.Remove(file); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
t.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
dsn := fmt.Sprintf("%s?_pragma=foreign_keys(1)&_pragma=busy_timeout=%d", file, (60 * time.Second).Milliseconds())
|
||||
|
||||
migr, err := migrate.New("../../../migrations", "sqlite", "sqlite://"+dsn)
|
||||
if err != nil {
|
||||
t.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
if err := migr.Up(); err != nil {
|
||||
t.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
db, err := sql.Open("sqlite", dsn)
|
||||
if err != nil {
|
||||
t.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
repo := NewAgentRepository(db)
|
||||
|
||||
testsuite.TestAgentRepository(t, repo)
|
||||
}
|
1
internal/datastore/sqlite/testdata/.gitignore
vendored
Normal file
1
internal/datastore/sqlite/testdata/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
*.sqlite*
|
14
internal/datastore/testsuite/agent_repository.go
Normal file
14
internal/datastore/testsuite/agent_repository.go
Normal file
@ -0,0 +1,14 @@
|
||||
package testsuite
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||
)
|
||||
|
||||
func TestAgentRepository(t *testing.T, repo datastore.AgentRepository) {
|
||||
t.Run("Cases", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
runAgentRepositoryTests(t, repo)
|
||||
})
|
||||
}
|
129
internal/datastore/testsuite/agent_repository_cases.go
Normal file
129
internal/datastore/testsuite/agent_repository_cases.go
Normal file
@ -0,0 +1,129 @@
|
||||
package testsuite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/agent/controller/mdns/spec"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/jwk"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type agentRepositoryTestCase struct {
|
||||
Name string
|
||||
Skip bool
|
||||
Run func(ctx context.Context, repo datastore.AgentRepository) error
|
||||
}
|
||||
|
||||
var agentRepositoryTestCases = []agentRepositoryTestCase{
|
||||
{
|
||||
Name: "Create a new agent",
|
||||
Run: func(ctx context.Context, repo datastore.AgentRepository) error {
|
||||
thumbprint := "foo"
|
||||
keySet := jwk.NewSet()
|
||||
var metadata map[string]any
|
||||
|
||||
agent, err := repo.Create(ctx, thumbprint, keySet, metadata)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if agent.CreatedAt.IsZero() {
|
||||
return errors.Errorf("agent.CreatedAt should not be zero time")
|
||||
}
|
||||
|
||||
if agent.UpdatedAt.IsZero() {
|
||||
return errors.Errorf("agent.UpdatedAt should not be zero time")
|
||||
}
|
||||
|
||||
if e, g := datastore.AgentStatusPending, agent.Status; e != g {
|
||||
return errors.Errorf("agent.Status: expected '%v', got '%v'", e, g)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Try to update spec for an unexistant agent",
|
||||
Run: func(ctx context.Context, repo datastore.AgentRepository) error {
|
||||
var unexistantAgentID datastore.AgentID = 9999
|
||||
var specData map[string]any
|
||||
|
||||
agent, err := repo.UpdateSpec(ctx, unexistantAgentID, string(spec.Name), 0, specData)
|
||||
if err == nil {
|
||||
return errors.New("error should not be nil")
|
||||
}
|
||||
|
||||
if !errors.Is(err, datastore.ErrNotFound) {
|
||||
return errors.Errorf("error should be datastore.ErrNotFound, got '%+v'", err)
|
||||
}
|
||||
|
||||
if agent != nil {
|
||||
return errors.New("agent should be nil")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Try to delete spec of an unexistant agent",
|
||||
Run: func(ctx context.Context, repo datastore.AgentRepository) error {
|
||||
var unexistantAgentID datastore.AgentID = 9999
|
||||
|
||||
err := repo.DeleteSpec(ctx, unexistantAgentID, string(spec.Name))
|
||||
if err == nil {
|
||||
return errors.New("error should not be nil")
|
||||
}
|
||||
|
||||
if !errors.Is(err, datastore.ErrNotFound) {
|
||||
return errors.Errorf("error should be datastore.ErrNotFound, got '%+v'", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Try to get specs of an unexistant agent",
|
||||
Run: func(ctx context.Context, repo datastore.AgentRepository) error {
|
||||
var unexistantAgentID datastore.AgentID = 9999
|
||||
|
||||
specs, err := repo.GetSpecs(ctx, unexistantAgentID)
|
||||
if err == nil {
|
||||
return errors.New("error should not be nil")
|
||||
}
|
||||
|
||||
if !errors.Is(err, datastore.ErrNotFound) {
|
||||
return errors.Errorf("error should be datastore.ErrNotFound, got '%+v'", err)
|
||||
}
|
||||
|
||||
if specs != nil {
|
||||
return errors.Errorf("specs should be nil, got '%+v'", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func runAgentRepositoryTests(t *testing.T, repo datastore.AgentRepository) {
|
||||
for _, tc := range agentRepositoryTestCases {
|
||||
func(tc agentRepositoryTestCase) {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if tc.Skip {
|
||||
t.SkipNow()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
if err := tc.Run(ctx, repo); err != nil {
|
||||
t.Errorf("%+v", errors.WithStack(err))
|
||||
}
|
||||
})
|
||||
}(tc)
|
||||
}
|
||||
}
|
@ -1,8 +1,9 @@
|
||||
package spec
|
||||
|
||||
import (
|
||||
_ "forge.cadoles.com/Cadoles/emissary/internal/agent/controller/app/spec"
|
||||
_ "forge.cadoles.com/Cadoles/emissary/internal/agent/controller/mdns/spec"
|
||||
_ "forge.cadoles.com/Cadoles/emissary/internal/agent/controller/openwrt/spec/sysupgrade"
|
||||
_ "forge.cadoles.com/Cadoles/emissary/internal/spec/app"
|
||||
_ "forge.cadoles.com/Cadoles/emissary/internal/spec/proxy"
|
||||
_ "forge.cadoles.com/Cadoles/emissary/internal/spec/uci"
|
||||
)
|
||||
|
@ -23,6 +23,13 @@ type (
|
||||
ParseOption = jwk.ParseOption
|
||||
)
|
||||
|
||||
var (
|
||||
FromRaw = jwk.FromRaw
|
||||
NewSet = jwk.NewSet
|
||||
)
|
||||
|
||||
const AlgorithmKey = jwk.AlgorithmKey
|
||||
|
||||
func Parse(src []byte, options ...jwk.ParseOption) (Set, error) {
|
||||
return jwk.Parse(src, options...)
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ package migrate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
_ "github.com/golang-migrate/migrate/v4/database/postgres"
|
||||
@ -23,8 +22,6 @@ func New(migrationDir, driver, dsn string) (*migrate.Migrate, error) {
|
||||
fmt.Sprintf("file://%s/%s", migrationDir, driver),
|
||||
dsn,
|
||||
)
|
||||
|
||||
log.Println(migrationDir, driver, dsn)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
29
internal/proxy/host_filter.go
Normal file
29
internal/proxy/host_filter.go
Normal file
@ -0,0 +1,29 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/proxy/wildcard"
|
||||
)
|
||||
|
||||
func FilterHosts(allowedHostPatterns ...string) Middleware {
|
||||
return func(h http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
if matches := wildcard.MatchAny(r.Host, allowedHostPatterns...); !matches {
|
||||
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
h.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
||||
}
|
||||
|
||||
func WithAllowedHosts(allowedHostPatterns ...string) OptionFunc {
|
||||
return func(o *Options) {
|
||||
o.Middlewares = append(o.Middlewares, FilterHosts(allowedHostPatterns...))
|
||||
}
|
||||
}
|
66
internal/proxy/host_rewrite.go
Normal file
66
internal/proxy/host_rewrite.go
Normal file
@ -0,0 +1,66 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/proxy/wildcard"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
func RewriteHosts(mappings map[string]*url.URL) Middleware {
|
||||
patterns := make([]string, len(mappings))
|
||||
|
||||
for p := range mappings {
|
||||
patterns = append(patterns, p)
|
||||
}
|
||||
|
||||
sort.Strings(patterns)
|
||||
reverse(patterns)
|
||||
|
||||
return func(h http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
var match *url.URL
|
||||
|
||||
for _, p := range patterns {
|
||||
logger.Debug(ctx, "matching host to pattern", logger.F("host", r.Host), logger.F("pattern", p))
|
||||
|
||||
if matches := wildcard.Match(r.Host, p); !matches {
|
||||
continue
|
||||
}
|
||||
|
||||
match = mappings[p]
|
||||
break
|
||||
}
|
||||
|
||||
if match == nil {
|
||||
h.ServeHTTP(w, r)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
ctx = logger.With(ctx, logger.F("originalHost", r.Host))
|
||||
r = r.WithContext(ctx)
|
||||
|
||||
originalURL := r.URL.String()
|
||||
|
||||
r.URL.Host = match.Host
|
||||
r.URL.Scheme = match.Scheme
|
||||
|
||||
logger.Debug(ctx, "rewriting url", logger.F("from", originalURL), logger.F("to", r.URL.String()))
|
||||
|
||||
h.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
||||
}
|
||||
|
||||
func WithRewriteHosts(mappings map[string]*url.URL) OptionFunc {
|
||||
return func(o *Options) {
|
||||
o.Middlewares = append(o.Middlewares, RewriteHosts(mappings))
|
||||
}
|
||||
}
|
33
internal/proxy/middleware.go
Normal file
33
internal/proxy/middleware.go
Normal file
@ -0,0 +1,33 @@
|
||||
package proxy
|
||||
|
||||
import "net/http"
|
||||
|
||||
type Middleware func(h http.Handler) http.Handler
|
||||
|
||||
type ProxyResponseTransformer interface {
|
||||
TransformResponse(*http.Response) error
|
||||
}
|
||||
|
||||
type defaultProxyResponseTransformer struct{}
|
||||
|
||||
// TransformResponse implements ProxyResponseTransformer
|
||||
func (*defaultProxyResponseTransformer) TransformResponse(*http.Response) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ ProxyResponseTransformer = &defaultProxyResponseTransformer{}
|
||||
|
||||
type ProxyResponseMiddleware func(ProxyResponseTransformer) ProxyResponseTransformer
|
||||
|
||||
type ProxyRequestTransformer interface {
|
||||
TransformRequest(*http.Request)
|
||||
}
|
||||
|
||||
type ProxyRequestMiddleware func(ProxyRequestTransformer) ProxyRequestTransformer
|
||||
|
||||
type defaultProxyRequestTransformer struct{}
|
||||
|
||||
// TransformRequest implements ProxyRequestTransformer
|
||||
func (*defaultProxyRequestTransformer) TransformRequest(*http.Request) {}
|
||||
|
||||
var _ ProxyRequestTransformer = &defaultProxyRequestTransformer{}
|
29
internal/proxy/options.go
Normal file
29
internal/proxy/options.go
Normal file
@ -0,0 +1,29 @@
|
||||
package proxy
|
||||
|
||||
type Options struct {
|
||||
Middlewares []Middleware
|
||||
ProxyRequestMiddlewares []ProxyRequestMiddleware
|
||||
ProxyResponseMiddlewares []ProxyResponseMiddleware
|
||||
}
|
||||
|
||||
func defaultOptions() *Options {
|
||||
return &Options{
|
||||
Middlewares: make([]Middleware, 0),
|
||||
ProxyRequestMiddlewares: make([]ProxyRequestMiddleware, 0),
|
||||
ProxyResponseMiddlewares: make([]ProxyResponseMiddleware, 0),
|
||||
}
|
||||
}
|
||||
|
||||
type OptionFunc func(*Options)
|
||||
|
||||
func WithProxyRequestMiddlewares(middlewares ...ProxyRequestMiddleware) OptionFunc {
|
||||
return func(o *Options) {
|
||||
o.ProxyRequestMiddlewares = middlewares
|
||||
}
|
||||
}
|
||||
|
||||
func WithproxyResponseMiddlewares(middlewares ...ProxyResponseMiddleware) OptionFunc {
|
||||
return func(o *Options) {
|
||||
o.ProxyResponseMiddlewares = middlewares
|
||||
}
|
||||
}
|
131
internal/proxy/proxy.go
Normal file
131
internal/proxy/proxy.go
Normal file
@ -0,0 +1,131 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"sync"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
type Proxy struct {
|
||||
reversers sync.Map
|
||||
handler http.Handler
|
||||
proxyResponseTransformer ProxyResponseTransformer
|
||||
proxyRequestTransformer ProxyRequestTransformer
|
||||
}
|
||||
|
||||
// ServeHTTP implements http.Handler
|
||||
func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
p.handler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (p *Proxy) proxyRequest(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
var reverser *httputil.ReverseProxy
|
||||
|
||||
key := fmt.Sprintf("%s://%s", r.URL.Scheme, r.URL.Host)
|
||||
|
||||
createAndStore := func() {
|
||||
target := &url.URL{
|
||||
Scheme: r.URL.Scheme,
|
||||
Host: r.URL.Host,
|
||||
}
|
||||
|
||||
reverser = httputil.NewSingleHostReverseProxy(target)
|
||||
|
||||
originalDirector := reverser.Director
|
||||
|
||||
if p.proxyRequestTransformer != nil {
|
||||
reverser.Director = func(r *http.Request) {
|
||||
originalURL := r.URL.String()
|
||||
originalDirector(r)
|
||||
p.proxyRequestTransformer.TransformRequest(r)
|
||||
logger.Debug(ctx, "proxying request", logger.F("targetURL", r.URL.String()), logger.F("originalURL", originalURL))
|
||||
}
|
||||
}
|
||||
|
||||
if p.proxyResponseTransformer != nil {
|
||||
reverser.ModifyResponse = func(r *http.Response) error {
|
||||
if err := p.proxyResponseTransformer.TransformResponse(r); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
p.reversers.Store(key, reverser)
|
||||
}
|
||||
|
||||
raw, exists := p.reversers.Load(key)
|
||||
if !exists {
|
||||
createAndStore()
|
||||
}
|
||||
|
||||
reverser, ok := raw.(*httputil.ReverseProxy)
|
||||
if !ok {
|
||||
createAndStore()
|
||||
}
|
||||
|
||||
reverser.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func New(funcs ...OptionFunc) *Proxy {
|
||||
opts := defaultOptions()
|
||||
for _, fn := range funcs {
|
||||
fn(opts)
|
||||
}
|
||||
|
||||
proxy := &Proxy{}
|
||||
|
||||
handler := http.HandlerFunc(proxy.proxyRequest)
|
||||
proxy.handler = createMiddlewareChain(handler, opts.Middlewares)
|
||||
|
||||
proxy.proxyRequestTransformer = createProxyRequestChain(&defaultProxyRequestTransformer{}, opts.ProxyRequestMiddlewares)
|
||||
proxy.proxyResponseTransformer = createProxyResponseChain(&defaultProxyResponseTransformer{}, opts.ProxyResponseMiddlewares)
|
||||
|
||||
return proxy
|
||||
}
|
||||
|
||||
var _ http.Handler = &Proxy{}
|
||||
|
||||
func createMiddlewareChain(handler http.Handler, middlewares []Middleware) http.Handler {
|
||||
reverse(middlewares)
|
||||
|
||||
for _, m := range middlewares {
|
||||
handler = m(handler)
|
||||
}
|
||||
|
||||
return handler
|
||||
}
|
||||
|
||||
func createProxyResponseChain(transformer ProxyResponseTransformer, middlewares []ProxyResponseMiddleware) ProxyResponseTransformer {
|
||||
reverse(middlewares)
|
||||
|
||||
for _, m := range middlewares {
|
||||
transformer = m(transformer)
|
||||
}
|
||||
|
||||
return transformer
|
||||
}
|
||||
|
||||
func createProxyRequestChain(transformer ProxyRequestTransformer, middlewares []ProxyRequestMiddleware) ProxyRequestTransformer {
|
||||
reverse(middlewares)
|
||||
|
||||
for _, m := range middlewares {
|
||||
transformer = m(transformer)
|
||||
}
|
||||
|
||||
return transformer
|
||||
}
|
||||
|
||||
func reverse[S ~[]E, E any](s S) {
|
||||
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
|
||||
s[i], s[j] = s[j], s[i]
|
||||
}
|
||||
}
|
44
internal/proxy/wildcard/match.go
Normal file
44
internal/proxy/wildcard/match.go
Normal file
@ -0,0 +1,44 @@
|
||||
package wildcard
|
||||
|
||||
const wildcard = '*'
|
||||
|
||||
func Match(str, pattern string) bool {
|
||||
if pattern == "" {
|
||||
return str == pattern
|
||||
}
|
||||
|
||||
if pattern == string(wildcard) {
|
||||
return true
|
||||
}
|
||||
|
||||
return deepMatchRune([]rune(str), []rune(pattern))
|
||||
}
|
||||
|
||||
func MatchAny(str string, patterns ...string) bool {
|
||||
for _, p := range patterns {
|
||||
if matches := Match(str, p); matches {
|
||||
return matches
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func deepMatchRune(str, pattern []rune) bool {
|
||||
for len(pattern) > 0 {
|
||||
switch pattern[0] {
|
||||
default:
|
||||
if len(str) == 0 || str[0] != pattern[0] {
|
||||
return false
|
||||
}
|
||||
case wildcard:
|
||||
return deepMatchRune(str, pattern[1:]) ||
|
||||
(len(str) > 0 && deepMatchRune(str[1:], pattern))
|
||||
}
|
||||
|
||||
str = str[1:]
|
||||
pattern = pattern[1:]
|
||||
}
|
||||
|
||||
return len(str) == 0 && len(pattern) == 0
|
||||
}
|
@ -145,6 +145,7 @@ func (s *Server) registerAgent(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
type updateAgentRequest struct {
|
||||
Status *datastore.AgentStatus `json:"status" validate:"omitempty,oneof=0 1 2 3"`
|
||||
Label *string `json:"label" validate:"omitempty"`
|
||||
}
|
||||
|
||||
func (s *Server) updateAgent(w http.ResponseWriter, r *http.Request) {
|
||||
@ -166,6 +167,10 @@ func (s *Server) updateAgent(w http.ResponseWriter, r *http.Request) {
|
||||
options = append(options, datastore.WithAgentUpdateStatus(*updateAgentReq.Status))
|
||||
}
|
||||
|
||||
if updateAgentReq.Label != nil {
|
||||
options = append(options, datastore.WithAgentUpdateLabel(*updateAgentReq.Label))
|
||||
}
|
||||
|
||||
agent, err := s.agentRepo.Update(
|
||||
ctx,
|
||||
datastore.AgentID(agentID),
|
||||
|
@ -105,8 +105,8 @@ func (s *Server) run(parentCtx context.Context, addrs chan net.Addr, errs chan e
|
||||
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(auth.Middleware(
|
||||
thirdparty.NewAuthenticator(keys, string(s.conf.Issuer)),
|
||||
agent.NewAuthenticator(s.agentRepo),
|
||||
thirdparty.NewAuthenticator(keys, string(s.conf.Issuer), thirdparty.DefaultAcceptableSkew),
|
||||
agent.NewAuthenticator(s.agentRepo, agent.DefaultAcceptableSkew),
|
||||
))
|
||||
|
||||
r.Route("/agents", func(r chi.Router) {
|
||||
|
@ -58,6 +58,12 @@ func (s *Server) updateSpec(w http.ResponseWriter, r *http.Request) {
|
||||
updateSpecReq.SpecData(),
|
||||
)
|
||||
if err != nil {
|
||||
if errors.Is(err, datastore.ErrNotFound) {
|
||||
api.ErrorResponse(w, http.StatusNotFound, ErrCodeNotFound, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if errors.Is(err, datastore.ErrUnexpectedRevision) {
|
||||
api.ErrorResponse(w, http.StatusConflict, ErrCodeUnexpectedRevision, nil)
|
||||
|
||||
@ -87,6 +93,12 @@ func (s *Server) getAgentSpecs(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
specs, err := s.agentRepo.GetSpecs(ctx, agentID)
|
||||
if err != nil {
|
||||
if errors.Is(err, datastore.ErrNotFound) {
|
||||
api.ErrorResponse(w, http.StatusNotFound, ErrCodeNotFound, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
logger.Error(ctx, "could not list specs", logger.E(errors.WithStack(err)))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||
|
||||
|
@ -1,87 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://app.edge.emissary.cadoles.com/spec.json",
|
||||
"title": "AppSpec",
|
||||
"description": "Emissary 'App' specification",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"apps": {
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
".*": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"url": {
|
||||
"type": "string"
|
||||
},
|
||||
"sha256sum": {
|
||||
"type": "string"
|
||||
},
|
||||
"address": {
|
||||
"type": "string"
|
||||
},
|
||||
"format": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"zip",
|
||||
"tar.gz"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"url",
|
||||
"sha256sum",
|
||||
"address",
|
||||
"format"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"auth": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"local": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": ["object", "string"]
|
||||
},
|
||||
"accounts": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"username": {
|
||||
"type": "string"
|
||||
},
|
||||
"password": {
|
||||
"type": "string"
|
||||
},
|
||||
"algo": {
|
||||
"type": "string"
|
||||
},
|
||||
"claims": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"username",
|
||||
"password",
|
||||
"algo"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"key"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"apps"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/spec"
|
||||
edgeAuth "forge.cadoles.com/arcad/edge/pkg/module/auth/http"
|
||||
)
|
||||
|
||||
const NameApp spec.Name = "app.emissary.cadoles.com"
|
||||
|
||||
type Spec struct {
|
||||
Revision int `json:"revision"`
|
||||
Apps map[string]AppEntry `json:"apps"`
|
||||
Auth *Auth `json:"auth"`
|
||||
}
|
||||
|
||||
type AppEntry struct {
|
||||
URL string `json:"url"`
|
||||
SHA256Sum string `json:"sha256sum"`
|
||||
Address string `json:"address"`
|
||||
Format string `json:"format"`
|
||||
}
|
||||
|
||||
type Auth struct {
|
||||
Local *LocalAuth `json:"local,omitempty"`
|
||||
}
|
||||
|
||||
type LocalAuth struct {
|
||||
Key any `json:"key"`
|
||||
Accounts []edgeAuth.LocalAccount `json:"accounts"`
|
||||
}
|
||||
|
||||
func (s *Spec) SpecName() spec.Name {
|
||||
return NameApp
|
||||
}
|
||||
|
||||
func (s *Spec) SpecRevision() int {
|
||||
return s.Revision
|
||||
}
|
||||
|
||||
func (s *Spec) SpecData() map[string]any {
|
||||
return map[string]any{
|
||||
"apps": s.Apps,
|
||||
"auth": s.Auth,
|
||||
}
|
||||
}
|
||||
|
||||
func NewSpec() *Spec {
|
||||
return &Spec{
|
||||
Revision: -1,
|
||||
}
|
||||
}
|
||||
|
||||
var _ spec.Spec = &Spec{}
|
42
internal/spec/app/testdata/spec-ok.json
vendored
42
internal/spec/app/testdata/spec-ok.json
vendored
@ -1,42 +0,0 @@
|
||||
{
|
||||
"name": "app.emissary.cadoles.com",
|
||||
"data": {
|
||||
"apps": {
|
||||
"edge.sdk.client.test": {
|
||||
"url": "http://example.com/edge.sdk.client.test_0.0.0.zip",
|
||||
"sha256sum": "58019192dacdae17755707719707db007e26dac856102280583fbd18427dd352",
|
||||
"address": ":8081",
|
||||
"format": "zip"
|
||||
}
|
||||
},
|
||||
"auth": {
|
||||
"local": {
|
||||
"key": {
|
||||
"d": "YOre0WZefGfUGFvDg42oL5Oad5Zsb1N_hqPyLVM5ajpTZzcHpB3wT6In9tFO_VshB6lxVtPA9ckPkpMTFY7ygt1Yomc1HkoOKRtmIaqdr4VgNQifU-4yiLiJkSbdYSeMV-KkkN8mGR1keJpJeS34W1X0W6CkU2nw7F5VueBCJfWJA0funRfuWdI68MTUgT9kRZFp-SfvptvRL6jVYHV_5hqxzHCvgEdBSF6QKwx4M6P6QBMt7ft6uMLmFx9abKFw2V51hX3PkxiSepVB3w5CYg4HtS3AHX6bILL4m0R2pdTIkap7i3tkH_xAOuKWt8D6JhadI8X1rEAwXmCS5KrRgQ",
|
||||
"dp": "U0HfvBC6hk-SCpuotGIv3vbHCVt1aF3SHK0y32EYCOe8e_9G6YCEILfcvEJ5fiOCc2kvx6TasHQu4qj1uWRKenZlK1sJ6KDybGCkZL1D3jYnbeLZYBuWBL__YbZiST3ewbxzj_EDMWiZ8sUltahza_1weSgg8auSzTHS2LJBHIE",
|
||||
"dq": "hVom4ScDxgqhCsQNVpZlN7M3v0tgWjl_gTOHjOyzKCHQJeC0QmJJaMKkQZPWJ8jjLqy7VwVpqC2nZU7QDuX1Cq5eJDQcXi9XtaAfIBico9WcYDre6mDyhL588YHpekyRke8HnZ810iesr0G3gU1h0QvZVVuW-pXTJOXhZTt6nFc",
|
||||
"e": "AQAB",
|
||||
"kty": "RSA",
|
||||
"n": "vPnpkE3-HfNgJSru_K40LstkjiG2Bq_Tt-m0d_yUBBSbirFxF3qH4EXi7WrtZdeDahg2iV2BvpbVVj9GlmGo9OLol6jc7AP2yvZrkbABiiJhCbuPdkYbNpx6B7Itl8RT_bUSYAMZhmux5lpsn4weQ01fzjICi1rA-bIJpOfotdOjP4_lol-LxGZOGJQv9kndP8bgmssJb3Y_2s4gPtkmXySLrhpr5So-_6dVksyuBD9aLcnsMLDbywusjEMCdhqzQbvOjryomnmEXwyz_Ewb5HFK2PfgFtoHkdjqDz-mrEs3tw5g4TdYhCftzJxgbyNAEq4aEiOQrAncYyrXlotP_w",
|
||||
"p": "8TNMF0WUe7CEeNVUTsuEcBAAXRguNtpvVifIjlwzFRGOYVGIpKuHsqQPKlZL07I9gPr9LifQnyQus3oEmTOrVs6LB9sfbukbg43ZRKoGVM40JYF5Xjs7R3mEZhgU0WaYOVe3iLtBGMfXNWFwlbfQP-zEb-dPCBX1jWT3LdgNBcE",
|
||||
"q": "yJJLNc9w6O4y2icME8k99FugV9E7ObwUxF3v5JN3y1cmAT0h2njyE3iAGqaDZwcY1_jGCisjwoqX6i5E8xqhxX3Gcy3J7SmUAf8fhY8wU3zv9DK7skg2IdvanDb8Y1OM6GchbYZAOVPEg2IvVio8zI-Ih3DDwDk8Df0ufzoHRb8",
|
||||
"qi": "zOE-4R3cjPesm3MX-4PdwmsaF9QZLUVRUvvHJ08pKs6kAXP18hzjctAoOjhQDxlTYqNYNePfKzKwost3OJoPgRIc9w9qwUCK1gNOS4Z_xozCIaXgMddNFhkoAfZ4JaKjNCiinzjGfqG99Lf-yzmmREuuhRv7SdS3ST4VQjiJQew"
|
||||
},
|
||||
"accounts": [
|
||||
{
|
||||
"username": "foo",
|
||||
"algo": "plain",
|
||||
"password": "bar",
|
||||
"claims": {
|
||||
"arcad_role": "user",
|
||||
"arcad_tenant": "dev.cli",
|
||||
"preferred_username": "Foo",
|
||||
"sub": "foo"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"revision": 0
|
||||
}
|
1
migrations/sqlite/0000001_agent_label.down.sql
Normal file
1
migrations/sqlite/0000001_agent_label.down.sql
Normal file
@ -0,0 +1 @@
|
||||
ALTER TABLE agents DROP COLUMN label;
|
1
migrations/sqlite/0000001_agent_label.up.sql
Normal file
1
migrations/sqlite/0000001_agent_label.up.sql
Normal file
@ -0,0 +1 @@
|
||||
ALTER TABLE agents ADD COLUMN label TEXT DEFAULT "";
|
1
migrations/sqlite/0000002_agent_contactedat.down.sql
Normal file
1
migrations/sqlite/0000002_agent_contactedat.down.sql
Normal file
@ -0,0 +1 @@
|
||||
ALTER TABLE agents DROP COLUMN contacted_at;
|
1
migrations/sqlite/0000002_agent_contactedat.up.sql
Normal file
1
migrations/sqlite/0000002_agent_contactedat.up.sql
Normal file
@ -0,0 +1 @@
|
||||
ALTER TABLE agents ADD COLUMN contacted_at datetime;
|
24
misc/jenkins/Dockerfile
Normal file
24
misc/jenkins/Dockerfile
Normal file
@ -0,0 +1,24 @@
|
||||
FROM reg.cadoles.com/proxy_cache/library/ubuntu:22.04
|
||||
|
||||
ARG HTTP_PROXY=
|
||||
ARG HTTPS_PROXY=
|
||||
ARG http_proxy=
|
||||
ARG https_proxy=
|
||||
ARG GO_VERSION=1.20.2
|
||||
|
||||
# Install dev environment dependencies
|
||||
RUN export DEBIAN_FRONTEND=noninteractive &&\
|
||||
apt-get update -y &&\
|
||||
apt-get install -y --no-install-recommends curl ca-certificates build-essential wget unzip tar git jq
|
||||
|
||||
# Install Go
|
||||
RUN mkdir -p /tmp \
|
||||
&& wget -O /tmp/go${GO_VERSION}.linux-amd64.tar.gz https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz \
|
||||
&& rm -rf /usr/local/go \
|
||||
&& mkdir -p /usr/local \
|
||||
&& tar -C /usr/local -xzf /tmp/go${GO_VERSION}.linux-amd64.tar.gz
|
||||
|
||||
ENV PATH="${PATH}:/usr/local/go/bin"
|
||||
|
||||
# Add LetsEncrypt certificates
|
||||
RUN curl -k https://forge.cadoles.com/Cadoles/Jenkins/raw/branch/master/resources/com/cadoles/common/add-letsencrypt-ca.sh | bash
|
@ -9,7 +9,7 @@ server:
|
||||
port: 3000
|
||||
database:
|
||||
driver: sqlite
|
||||
dsn: sqlite:///var/lib/emissary/data.sqlite
|
||||
dsn: sqlite:///var/lib/emissary/data.sqlite?_pragma=foreign_keys(1)&_pragma=busy_timeout=60000
|
||||
cors:
|
||||
allowedOrigins: []
|
||||
allowCredentials: true
|
||||
|
@ -6,7 +6,7 @@ After=network.target
|
||||
Type=simple
|
||||
Restart=always
|
||||
WorkingDirectory=/usr/share/emissary
|
||||
ExecStart=/usr/bin/emissary --config /etc/emissary/config.yml agent run
|
||||
ExecStart=/usr/bin/emissary --config /etc/emissary/agent.yml agent run
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
@ -4,9 +4,9 @@ After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
Restart=always
|
||||
Restart=on-failure
|
||||
WorkingDirectory=/usr/share/emissary
|
||||
ExecStart=/usr/bin/emissary --config /etc/emissary/config.yml server run
|
||||
ExecStart=/usr/bin/emissary --config /etc/emissary/server.yml server run
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
339
misc/resources/logo.svg
Normal file
339
misc/resources/logo.svg
Normal file
@ -0,0 +1,339 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
viewBox="0 0 620 620"
|
||||
version="1.1"
|
||||
id="svg110"
|
||||
sodipodi:docname="logo.svg"
|
||||
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview112"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.38064516"
|
||||
inkscape:cx="310"
|
||||
inkscape:cy="306.05932"
|
||||
inkscape:current-layer="svg110" />
|
||||
<defs
|
||||
id="defs50">
|
||||
<style
|
||||
id="style2">.cls-1{fill:#fff;}.cls-2{fill:url(#Dégradé_sans_nom_22);}.cls-3{fill:url(#Dégradé_sans_nom_22-2);}.cls-4{fill:url(#Dégradé_sans_nom_22-3);}.cls-5{fill:#acdce9;}.cls-6{fill:none;}.cls-7{fill:url(#Dégradé_sans_nom_14);}.cls-8{fill:url(#Dégradé_sans_nom_14-2);}.cls-9{fill:url(#Dégradé_sans_nom_14-3);}.cls-10{fill:url(#Dégradé_sans_nom_14-4);}.cls-11{fill:url(#Dégradé_sans_nom_14-5);}.cls-12{fill:url(#Dégradé_sans_nom_14-6);}.cls-13{fill:url(#Dégradé_sans_nom_14-7);}</style>
|
||||
<linearGradient
|
||||
id="Dégradé_sans_nom_22"
|
||||
x1="389.04"
|
||||
y1="577.91"
|
||||
x2="843.52"
|
||||
y2="444.49"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop
|
||||
offset="0"
|
||||
stop-color="#f9b233"
|
||||
id="stop4" />
|
||||
<stop
|
||||
offset="0.05"
|
||||
stop-color="#ef9630"
|
||||
id="stop6" />
|
||||
<stop
|
||||
offset="0.12"
|
||||
stop-color="#e2742c"
|
||||
id="stop8" />
|
||||
<stop
|
||||
offset="0.2"
|
||||
stop-color="#d65729"
|
||||
id="stop10" />
|
||||
<stop
|
||||
offset="0.29"
|
||||
stop-color="#cd3f26"
|
||||
id="stop12" />
|
||||
<stop
|
||||
offset="0.39"
|
||||
stop-color="#c72d24"
|
||||
id="stop14" />
|
||||
<stop
|
||||
offset="0.51"
|
||||
stop-color="#c22023"
|
||||
id="stop16" />
|
||||
<stop
|
||||
offset="0.66"
|
||||
stop-color="#bf1822"
|
||||
id="stop18" />
|
||||
<stop
|
||||
offset="0.99"
|
||||
stop-color="#be1622"
|
||||
id="stop20" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="Dégradé_sans_nom_22-2"
|
||||
x1="347.14"
|
||||
y1="435.21"
|
||||
x2="801.63"
|
||||
y2="301.79"
|
||||
xlink:href="#Dégradé_sans_nom_22" />
|
||||
<linearGradient
|
||||
id="Dégradé_sans_nom_22-3"
|
||||
x1="363.28"
|
||||
y1="490.17"
|
||||
x2="817.76"
|
||||
y2="356.75"
|
||||
xlink:href="#Dégradé_sans_nom_22" />
|
||||
<linearGradient
|
||||
id="Dégradé_sans_nom_14"
|
||||
x1="295.82"
|
||||
y1="531.3"
|
||||
x2="915.72"
|
||||
y2="148.45"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop
|
||||
offset="0"
|
||||
stop-color="#36a9e1"
|
||||
id="stop25" />
|
||||
<stop
|
||||
offset="0.01"
|
||||
stop-color="#35a3db"
|
||||
id="stop27" />
|
||||
<stop
|
||||
offset="0.09"
|
||||
stop-color="#3281b9"
|
||||
id="stop29" />
|
||||
<stop
|
||||
offset="0.17"
|
||||
stop-color="#2f649c"
|
||||
id="stop31" />
|
||||
<stop
|
||||
offset="0.26"
|
||||
stop-color="#2d4c85"
|
||||
id="stop33" />
|
||||
<stop
|
||||
offset="0.37"
|
||||
stop-color="#2b3a72"
|
||||
id="stop35" />
|
||||
<stop
|
||||
offset="0.49"
|
||||
stop-color="#2a2d66"
|
||||
id="stop37" />
|
||||
<stop
|
||||
offset="0.65"
|
||||
stop-color="#29255e"
|
||||
id="stop39" />
|
||||
<stop
|
||||
offset="0.99"
|
||||
stop-color="#29235c"
|
||||
id="stop41" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="Dégradé_sans_nom_14-2"
|
||||
x1="302.24"
|
||||
y1="541.68"
|
||||
x2="922.14"
|
||||
y2="158.84"
|
||||
xlink:href="#Dégradé_sans_nom_14" />
|
||||
<linearGradient
|
||||
id="Dégradé_sans_nom_14-3"
|
||||
x1="308.78"
|
||||
y1="537.64"
|
||||
x2="795.27"
|
||||
y2="237.19"
|
||||
xlink:href="#Dégradé_sans_nom_14" />
|
||||
<linearGradient
|
||||
id="Dégradé_sans_nom_14-4"
|
||||
x1="416.02"
|
||||
y1="726.54"
|
||||
x2="1047.24"
|
||||
y2="336.7"
|
||||
xlink:href="#Dégradé_sans_nom_14" />
|
||||
<linearGradient
|
||||
id="Dégradé_sans_nom_14-5"
|
||||
x1="422.84"
|
||||
y1="722.33"
|
||||
x2="909.33"
|
||||
y2="421.88"
|
||||
xlink:href="#Dégradé_sans_nom_14" />
|
||||
<linearGradient
|
||||
id="Dégradé_sans_nom_14-6"
|
||||
x1="265.42"
|
||||
y1="482.7"
|
||||
x2="896.65"
|
||||
y2="92.86"
|
||||
xlink:href="#Dégradé_sans_nom_14" />
|
||||
<linearGradient
|
||||
id="Dégradé_sans_nom_14-7"
|
||||
x1="272.25"
|
||||
y1="478.49"
|
||||
x2="758.74"
|
||||
y2="178.04"
|
||||
xlink:href="#Dégradé_sans_nom_14" />
|
||||
</defs>
|
||||
<g
|
||||
id="Calque_4"
|
||||
data-name="Calque 4">
|
||||
<circle
|
||||
class="cls-1"
|
||||
cx="310"
|
||||
cy="310"
|
||||
r="310"
|
||||
id="circle52" />
|
||||
</g>
|
||||
<g
|
||||
id="Calque_3"
|
||||
data-name="Calque 3">
|
||||
<path
|
||||
class="cls-2"
|
||||
d="M382.43,551.91c-2.64,12.13-5,37.74,16.31,54a11.79,11.79,0,0,0,10.58,1.87L536.08,569a11.7,11.7,0,0,0,7.55-7.05c3.86-10.27,8.25-33.2-17.06-54.7A11.78,11.78,0,0,0,515.5,505l-7.95,2.44L446.89,526l-56.42,17.28A11.72,11.72,0,0,0,382.43,551.91Z"
|
||||
transform="translate(-270.7 -127.63)"
|
||||
id="path55" />
|
||||
<path
|
||||
class="cls-3"
|
||||
d="M479,384.26c4-2.64,8.43-5.3,13.2-7.93l.27-.14,0,0a4.79,4.79,0,0,0-4.38-.81l-88.29,27a4.79,4.79,0,0,0-3.24,3.35c-1.38,5.29-3.62,17.74,2.13,28.71a4.83,4.83,0,0,0,7.07,1.77,382.78,382.78,0,0,1,40.87-24.81,4.88,4.88,0,0,0,1.67-1.49C453.94,401.88,464.6,393.06,479,384.26Z"
|
||||
transform="translate(-270.7 -127.63)"
|
||||
id="path57" />
|
||||
<path
|
||||
class="cls-4"
|
||||
d="M681.16,333.19a.1.1,0,0,0,0,.2c11.53,2.22,19,6.52,21,12.92,6.28,20.52-46.38,54.83-117.63,76.63-57.32,17.55-109.18,21.65-130.54,11.9-5.18-2.37-8.59-5.55-9.82-9.55a12.49,12.49,0,0,1-.15-6.41.13.13,0,0,0-.19-.14A342.31,342.31,0,0,0,405.47,443c-23.59,17.58-35.56,33.72-31.8,46,4.78,15.62,34.14,22.17,78.27,20.26a.12.12,0,0,1,.13.16l-3,9.55a.13.13,0,0,0,.16.16L508.59,501l13.77-4.21a.13.13,0,0,1,.12,0c1.58,1.16,3,2.32,4.45,3.49a.1.1,0,0,0,.1,0A977.1,977.1,0,0,0,638.75,472.9c136.34-41.74,238.73-102.19,228.68-135C859.38,311.6,781.74,311,681.16,333.19Z"
|
||||
transform="translate(-270.7 -127.63)"
|
||||
id="path59" />
|
||||
<path
|
||||
class="cls-5"
|
||||
d="M694.39,348.33c-2-6.73-12.15-10.44-27.62-11.29a154.33,154.33,0,0,0-16.61.08c16,24.86-14.65,54-30.44,66.73C667.12,384.85,698.66,362.27,694.39,348.33Z"
|
||||
transform="translate(-270.7 -127.63)"
|
||||
id="path61" />
|
||||
<path
|
||||
class="cls-6"
|
||||
d="M873.93,336.73c-2.86-12.89-15.64-20-33.51-23.45A283.91,283.91,0,0,1,861,371.4C871.09,360.09,876.52,348.34,873.93,336.73Z"
|
||||
transform="translate(-270.7 -127.63)"
|
||||
id="path63" />
|
||||
<path
|
||||
class="cls-7"
|
||||
d="M299.08,479.05a2.32,2.32,0,0,0-2.2,2.73A309.22,309.22,0,0,0,323.68,562a2.27,2.27,0,0,0,2.43,1.21A283.59,283.59,0,0,1,299.91,479Z"
|
||||
transform="translate(-270.7 -127.63)"
|
||||
id="path65" />
|
||||
<path
|
||||
class="cls-8"
|
||||
d="M363,548.81q6.94-2.13,13.73-3.79a2.31,2.31,0,0,0,1.63-1.53c.36-1.14.75-2.3,1.18-3.47a2.3,2.3,0,0,1,1.5-1.43l59.3-18.15a2.31,2.31,0,0,0-.74-4.52c-14.07.37-63.57-.07-72-24.18,0,0-5.47-10.13,4.16-25.33-1.49.5-3,1-4.51,1.45-18.64,5.7-46.44,10.25-67.28,11.15a283.59,283.59,0,0,0,26.2,84.17,2.25,2.25,0,0,0,.62-.15A298.13,298.13,0,0,1,363,548.81Z"
|
||||
transform="translate(-270.7 -127.63)"
|
||||
id="path67" />
|
||||
<path
|
||||
class="cls-9"
|
||||
d="M363,548.81q6.94-2.13,13.73-3.79a2.31,2.31,0,0,0,1.63-1.53c.36-1.14.75-2.3,1.18-3.47a2.3,2.3,0,0,1,1.5-1.43l59.3-18.15a2.31,2.31,0,0,0-.74-4.52c-14.07.37-63.57-.07-72-24.18,0,0-5.47-10.13,4.16-25.33-1.49.5-3,1-4.51,1.45-18.64,5.7-46.44,10.25-67.28,11.15a283.59,283.59,0,0,0,26.2,84.17,2.25,2.25,0,0,0,.62-.15A298.13,298.13,0,0,1,363,548.81Z"
|
||||
transform="translate(-270.7 -127.63)"
|
||||
id="path69" />
|
||||
<path
|
||||
class="cls-10"
|
||||
d="M582.7,722.94c157.87,0,285.85-128,285.85-285.85A286.63,286.63,0,0,0,861,371.4c-37.61,42-140.24,77.8-140.27,77.8s-98,40.65-188,55.81c0,0,0,0,0,0,.64.42,36.32,24.49,13,68.42h0L403.46,615.88h0a41.35,41.35,0,0,1-5.21-3.19h0q-6.9,2.55-14.24,4.81c-5.69,1.74-12.1,3.41-18.87,5A285.19,285.19,0,0,0,582.7,722.94Z"
|
||||
transform="translate(-270.7 -127.63)"
|
||||
id="path71" />
|
||||
<path
|
||||
class="cls-11"
|
||||
d="M582.7,722.94c157.87,0,285.85-128,285.85-285.85A286.63,286.63,0,0,0,861,371.4c-37.61,42-140.24,77.8-140.27,77.8s-98,40.65-188,55.81c0,0,0,0,0,0,.64.42,36.32,24.49,13,68.42h0L403.46,615.88h0a41.35,41.35,0,0,1-5.21-3.19h0q-6.9,2.55-14.24,4.81c-5.69,1.74-12.1,3.41-18.87,5A285.19,285.19,0,0,0,582.7,722.94Z"
|
||||
transform="translate(-270.7 -127.63)"
|
||||
id="path73" />
|
||||
<path
|
||||
class="cls-12"
|
||||
d="M390,402.12h0a42,42,0,0,1,1.88-5.3h0l102.24-31.3h0l5.49,5.94h0c.52-.28,89.13-46.1,171-40.81h0c.13,0,111.77-28.45,169.78-17.37-46.14-95.88-144.2-162-257.72-162-156.72,0-284,126.12-285.81,282.4,16.25-9.51,36.55-18.94,52.53-23.83A177.82,177.82,0,0,1,390,402.12Z"
|
||||
transform="translate(-270.7 -127.63)"
|
||||
id="path75" />
|
||||
<path
|
||||
class="cls-13"
|
||||
d="M390,402.12h0a42,42,0,0,1,1.88-5.3h0l102.24-31.3h0l5.49,5.94h0c.52-.28,89.13-46.1,171-40.81h0c.13,0,111.77-28.45,169.78-17.37-46.14-95.88-144.2-162-257.72-162-156.72,0-284,126.12-285.81,282.4,16.25-9.51,36.55-18.94,52.53-23.83A177.82,177.82,0,0,1,390,402.12Z"
|
||||
transform="translate(-270.7 -127.63)"
|
||||
id="path77" />
|
||||
<path
|
||||
class="cls-5"
|
||||
d="M613.5,341.7a453.93,453.93,0,0,0-50.22,12.41c-26.81,8.2-50.89,18-69.82,28-20.21,10.63-34.52,21.39-40,30.47-2.22,3.7-3,7.13-2.08,10.14,1,3.39,4.13,6,8.91,7.89,19,7.48,64.86,3.18,116.58-12C635.88,378.72,622.92,352.06,613.5,341.7Z"
|
||||
transform="translate(-270.7 -127.63)"
|
||||
id="path79" />
|
||||
<circle
|
||||
class="cls-1"
|
||||
cx="432.9"
|
||||
cy="112.17"
|
||||
r="8.82"
|
||||
id="circle81" />
|
||||
<circle
|
||||
class="cls-1"
|
||||
cx="360.51"
|
||||
cy="174.46"
|
||||
r="4.41"
|
||||
id="circle83" />
|
||||
<circle
|
||||
class="cls-1"
|
||||
cx="253.85"
|
||||
cy="124.11"
|
||||
r="6.72"
|
||||
id="circle85" />
|
||||
<circle
|
||||
class="cls-1"
|
||||
cx="140.55"
|
||||
cy="150.46"
|
||||
r="4.41"
|
||||
id="circle87" />
|
||||
<circle
|
||||
class="cls-1"
|
||||
cx="221.77"
|
||||
cy="190.68"
|
||||
r="5.03"
|
||||
id="circle89" />
|
||||
<circle
|
||||
class="cls-1"
|
||||
cx="316"
|
||||
cy="56.58"
|
||||
r="4.41"
|
||||
id="circle91" />
|
||||
<circle
|
||||
class="cls-1"
|
||||
cx="272.6"
|
||||
cy="497.14"
|
||||
r="8.82"
|
||||
id="circle93" />
|
||||
<circle
|
||||
class="cls-1"
|
||||
cx="370.54"
|
||||
cy="480.67"
|
||||
r="4.41"
|
||||
id="circle95" />
|
||||
<circle
|
||||
class="cls-1"
|
||||
cx="434.95"
|
||||
cy="420.7"
|
||||
r="6.72"
|
||||
id="circle97" />
|
||||
<circle
|
||||
class="cls-1"
|
||||
cx="546.81"
|
||||
cy="388.76"
|
||||
r="4.41"
|
||||
id="circle99" />
|
||||
<circle
|
||||
class="cls-1"
|
||||
cx="495.3"
|
||||
cy="463.34"
|
||||
r="5.03"
|
||||
id="circle101" />
|
||||
<circle
|
||||
class="cls-1"
|
||||
cx="347.84"
|
||||
cy="391.81"
|
||||
r="4.41"
|
||||
id="circle103" />
|
||||
<circle
|
||||
class="cls-1"
|
||||
cx="346.13"
|
||||
cy="559.72"
|
||||
r="6.72"
|
||||
id="circle105" />
|
||||
<circle
|
||||
class="cls-1"
|
||||
cx="499.71"
|
||||
cy="334.37"
|
||||
r="4.41"
|
||||
id="circle107" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 11 KiB |
@ -1,10 +0,0 @@
|
||||
{
|
||||
"apps": {
|
||||
"edge.sdk.client.test": {
|
||||
"url": "http://localhost:3001/edge.sdk.client.test_0.0.0.zip",
|
||||
"sha256sum": "58019192dacdae17755707719707db007e26dac856102280583fbd18427dd352",
|
||||
"format": "zip",
|
||||
"address": "127.0.0.1:8081"
|
||||
}
|
||||
}
|
||||
}
|
@ -1,65 +0,0 @@
|
||||
@baseUrl = http://localhost:3000
|
||||
|
||||
### Get agents
|
||||
|
||||
# @name getAgents
|
||||
GET {{ baseUrl }}/api/v1/agents
|
||||
Content-Type: application/json
|
||||
|
||||
@agentId = {{ getAgents.response.body.Data.Agents.0.ID }}
|
||||
|
||||
### Update an agent (accept it)
|
||||
|
||||
PUT {{ baseUrl }}/api/v1/agents/{{ agentId }}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"Status": 1
|
||||
}
|
||||
|
||||
### Get an agent
|
||||
|
||||
GET {{ baseUrl }}/api/v1/agents/{{ agentId }}
|
||||
Content-Type: application/json
|
||||
|
||||
### Get an agent specs
|
||||
|
||||
# @name getSpecs
|
||||
GET {{ baseUrl }}/api/v1/agents/{{ agentId }}/specs
|
||||
Content-Type: application/json
|
||||
|
||||
@specName = {{ getSpecs.response.body.Data.Specs.0.Name }}
|
||||
|
||||
### Update an agent specs
|
||||
|
||||
POST {{ baseUrl }}/api/v1/agents/{{ agentId }}/specs
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"Name": "gateway.emissary.cadoles.com",
|
||||
"Revision": 2,
|
||||
"Data": {
|
||||
"gateways": {
|
||||
"cadoles.com":{
|
||||
"address":":3003",
|
||||
"target":"https://www.cadoles.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
### Delete an agent spec
|
||||
|
||||
DELETE {{ baseUrl }}/api/v1/agents/{{ agentId }}/specs
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"Name": "gateway.emissary.cadoles.com"
|
||||
}
|
||||
|
||||
### Update UCI spec with uhttpd config
|
||||
|
||||
POST {{ baseUrl }}/api/v1/agents/2/specs
|
||||
Content-Type: application/json
|
||||
|
||||
< ./uci-spec.payload.json
|
@ -1,163 +0,0 @@
|
||||
{
|
||||
"Name": "uci.emissary.cadoles.com",
|
||||
"Revision": 6,
|
||||
"Data": {
|
||||
"config": {
|
||||
"packages": [
|
||||
{
|
||||
"name": "uhttpd",
|
||||
"configs": [
|
||||
{
|
||||
"name": "uhttpd",
|
||||
"section": "main",
|
||||
"options": [
|
||||
{
|
||||
"type": "list",
|
||||
"name": "listen_http",
|
||||
"value": "0.0.0.0:8080"
|
||||
},
|
||||
{
|
||||
"type": "list",
|
||||
"name": "listen_http",
|
||||
"value": "[::]:8080"
|
||||
},
|
||||
{
|
||||
"type": "list",
|
||||
"name": "listen_https",
|
||||
"value": "0.0.0.0:8443"
|
||||
},
|
||||
{
|
||||
"type": "list",
|
||||
"name": "listen_https",
|
||||
"value": "[::]:8443"
|
||||
},
|
||||
{
|
||||
"type": "option",
|
||||
"name": "redirect_https",
|
||||
"value": "0"
|
||||
},
|
||||
{
|
||||
"type": "option",
|
||||
"name": "home",
|
||||
"value": "/www"
|
||||
},
|
||||
{
|
||||
"type": "option",
|
||||
"name": "rfc1918_filter",
|
||||
"value": "1"
|
||||
},
|
||||
{
|
||||
"type": "option",
|
||||
"name": "max_requests",
|
||||
"value": "3"
|
||||
},
|
||||
{
|
||||
"type": "option",
|
||||
"name": "max_connections",
|
||||
"value": "100"
|
||||
},
|
||||
{
|
||||
"type": "option",
|
||||
"name": "cert",
|
||||
"value": "/etc/uhttpd.crt"
|
||||
},
|
||||
{
|
||||
"type": "option",
|
||||
"name": "key",
|
||||
"value": "/etc/uhttpd.key"
|
||||
},
|
||||
{
|
||||
"type": "option",
|
||||
"name": "cgi_prefix",
|
||||
"value": "/cgi-bin"
|
||||
},
|
||||
{
|
||||
"type": "list",
|
||||
"name": "lua_prefix",
|
||||
"value": "/cgi-bin/luci=/usr/lib/lua/luci/sgi/uhttpd.lua"
|
||||
},
|
||||
{
|
||||
"type": "option",
|
||||
"name": "script_timeout",
|
||||
"value": "60"
|
||||
},
|
||||
{
|
||||
"type": "option",
|
||||
"name": "network_timeout",
|
||||
"value": "30"
|
||||
},
|
||||
{
|
||||
"type": "option",
|
||||
"name": "http_keepalive",
|
||||
"value": "20"
|
||||
},
|
||||
{
|
||||
"type": "option",
|
||||
"name": "tcp_keepalive",
|
||||
"value": "1"
|
||||
},
|
||||
{
|
||||
"type": "option",
|
||||
"name": "ubus_prefix",
|
||||
"value": "/ubus"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "cert",
|
||||
"section": "defaults",
|
||||
"options": [
|
||||
{
|
||||
"type": "option",
|
||||
"name": "days",
|
||||
"value": "730"
|
||||
},
|
||||
{
|
||||
"type": "option",
|
||||
"name": "key_type",
|
||||
"value": "ec"
|
||||
},
|
||||
{
|
||||
"type": "option",
|
||||
"name": "bits",
|
||||
"value": "2048"
|
||||
},
|
||||
{
|
||||
"type": "option",
|
||||
"name": "ec_curve",
|
||||
"value": "P-256"
|
||||
},
|
||||
{
|
||||
"type": "option",
|
||||
"name": "country",
|
||||
"value": "ZZ"
|
||||
},
|
||||
{
|
||||
"type": "option",
|
||||
"name": "state",
|
||||
"value": "Somewhere"
|
||||
},
|
||||
{
|
||||
"type": "option",
|
||||
"name": "location",
|
||||
"value": "Unknown"
|
||||
},
|
||||
{
|
||||
"type": "option",
|
||||
"name": "commonname",
|
||||
"value": "OpenWrt"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"postImportCommands": [
|
||||
{
|
||||
"command": "reload_config",
|
||||
"args": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
81
misc/spec-samples/app.emissary.cadoles.com.json
Normal file
81
misc/spec-samples/app.emissary.cadoles.com.json
Normal file
@ -0,0 +1,81 @@
|
||||
{
|
||||
"apps": {
|
||||
"app.arcad.edge.hextris": {
|
||||
"url": "https://emissary.cadol.es/files/apps/app.arcad.edge.hextris_v2023.4.20-2bbbe94.zip",
|
||||
"sha256sum": "67942ef4b623c46308c3f640b534bd4cb6b1d6021a422e40b62ab97658ba4586",
|
||||
"address": ":8083",
|
||||
"format": "zip"
|
||||
},
|
||||
"edge.sdk.client.test": {
|
||||
"url": "https://emissary.cadol.es/files/apps/edge.sdk.client.test_v2023.4.20-20c4189.zip",
|
||||
"sha256sum": "1edeb4aa75c1675db49cf27367b1537234a04526848ea6657931ca63f26e5dae",
|
||||
"address": ":8084",
|
||||
"format": "zip"
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"appUrlResolving": {
|
||||
"ifaceMappings": {
|
||||
"wlp4s0": "http://{{ .DeviceIP }}:{{ .AppPort }}",
|
||||
"enp0s31f6": "http://{{ .DeviceIP }}:{{ .AppPort }}"
|
||||
},
|
||||
"defaultUrlTemplate": "http://{{ last ( splitList \".\" ( toString .Manifest.ID ) ) }}.localhost.arcad.lan:8080"
|
||||
},
|
||||
"unexpectedHostRedirect": {
|
||||
"acceptedHostPatterns": ["arcad.lan", "*.arcad.lan", "arcad-*.local", "*.*.*.*"],
|
||||
"hostTarget": "localhost.arcad.lan"
|
||||
},
|
||||
"auth": {
|
||||
"local": {
|
||||
"key": "absolutlynotsecret",
|
||||
"cookieDuration": "1h",
|
||||
"accounts": [
|
||||
{
|
||||
"username": "admin",
|
||||
"algo": "plain",
|
||||
"password": "admin",
|
||||
"claims": {
|
||||
"edge_role": "admin",
|
||||
"edge_tenant": "emissary-dev",
|
||||
"preferred_username": "Admin",
|
||||
"sub": "admin"
|
||||
}
|
||||
},
|
||||
{
|
||||
"username": "superadmin",
|
||||
"algo": "plain",
|
||||
"password": "superadmin",
|
||||
"claims": {
|
||||
"edge_role": "superadmin",
|
||||
"edge_tenant": "emissary-dev",
|
||||
"preferred_username": "SuperAdmin",
|
||||
"sub": "superadmin"
|
||||
}
|
||||
},
|
||||
{
|
||||
"username": "user",
|
||||
"algo": "plain",
|
||||
"password": "user",
|
||||
"claims": {
|
||||
"edge_role": "user",
|
||||
"edge_tenant": "emissary-dev",
|
||||
"preferred_username": "User",
|
||||
"sub": "user"
|
||||
}
|
||||
},
|
||||
{
|
||||
"username": "superuser",
|
||||
"algo": "plain",
|
||||
"password": "superuser",
|
||||
"claims": {
|
||||
"edge_role": "superuser",
|
||||
"edge_tenant": "emissary-dev",
|
||||
"preferred_username": "SuperUser",
|
||||
"sub": "superuser"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
19
misc/spec-samples/mdns.emissary.cadoles.com.json
Normal file
19
misc/spec-samples/mdns.emissary.cadoles.com.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"services": {
|
||||
"arcad": {
|
||||
"type": "_http._tcp",
|
||||
"port": 8080,
|
||||
"host": "arcad"
|
||||
},
|
||||
"hextris": {
|
||||
"type": "_http._tcp",
|
||||
"port": 8080,
|
||||
"host": "arcad-hextris"
|
||||
},
|
||||
"test": {
|
||||
"type": "_http._tcp",
|
||||
"port": 8080,
|
||||
"host": "arcad-test"
|
||||
}
|
||||
}
|
||||
}
|
29
misc/spec-samples/proxy.emissary.cadoles.com.json
Normal file
29
misc/spec-samples/proxy.emissary.cadoles.com.json
Normal file
@ -0,0 +1,29 @@
|
||||
{
|
||||
"proxies": {
|
||||
"main": {
|
||||
"address": ":8080",
|
||||
"mappings": [
|
||||
{
|
||||
"hostPattern": "hextris.localhost.arcad.lan:*",
|
||||
"target": "http://localhost:8083"
|
||||
},
|
||||
{
|
||||
"hostPattern": "test.localhost.arcad.lan:*",
|
||||
"target": "http://localhost:8084"
|
||||
},
|
||||
{
|
||||
"hostPattern": "arcad-hextris.local:*",
|
||||
"target": "http://localhost:8083"
|
||||
},
|
||||
{
|
||||
"hostPattern": "arcad-test.local:*",
|
||||
"target": "http://localhost:8084"
|
||||
},
|
||||
{
|
||||
"hostPattern": "*",
|
||||
"target": "http://localhost:8084"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user