Compare commits
15 Commits
v2023.3.28
...
v2023.4.1-
Author | SHA1 | Date | |
---|---|---|---|
7d551a8312 | |||
d02eb91b11 | |||
d2bcdd2999 | |||
c638fe102b | |||
273265c3ef | |||
3e02a9f031 | |||
b52c687643 | |||
8119a01bf6 | |||
e5b6c5e949 | |||
9c69dc7ec8 | |||
4e6b450338 | |||
351f22e216 | |||
854a6ae41b | |||
a48c2ebe14 | |||
cdd78e4031 |
2
.gitignore
vendored
2
.gitignore
vendored
@ -4,7 +4,7 @@ dist/
|
|||||||
/tools
|
/tools
|
||||||
/tmp
|
/tmp
|
||||||
/state.json
|
/state.json
|
||||||
/emissary.sqlite
|
/emissary.sqlite*
|
||||||
/.gitea-release
|
/.gitea-release
|
||||||
/agent-key.json
|
/agent-key.json
|
||||||
/apps
|
/apps
|
||||||
|
70
Jenkinsfile
vendored
Normal file
70
Jenkinsfile
vendored
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
@Library('cadoles') _
|
||||||
|
|
||||||
|
pipeline {
|
||||||
|
agent {
|
||||||
|
dockerfile {
|
||||||
|
label 'docker'
|
||||||
|
filename 'Dockerfile'
|
||||||
|
dir 'misc/jenkins'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stages {
|
||||||
|
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()
|
||||||
|
build(
|
||||||
|
job: "../emissary-firmware/${env.GIT_BRANCH}",
|
||||||
|
parameters: [
|
||||||
|
[$class: 'StringParameterValue', name: 'emissaryRelease', value: currentVersion]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
always {
|
||||||
|
cleanWs()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
7
Makefile
7
Makefile
@ -135,7 +135,7 @@ gitea-release: tools/gitea-release/bin/gitea-release.sh goreleaser
|
|||||||
GITEA_RELEASE_COMMITISH_TARGET="$(GIT_VERSION)" \
|
GITEA_RELEASE_COMMITISH_TARGET="$(GIT_VERSION)" \
|
||||||
GITEA_RELEASE_IS_DRAFT="false" \
|
GITEA_RELEASE_IS_DRAFT="false" \
|
||||||
GITEA_RELEASE_BODY="" \
|
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
|
||||||
|
|
||||||
tools/gitea-release/bin/gitea-release.sh:
|
tools/gitea-release/bin/gitea-release.sh:
|
||||||
@ -150,4 +150,7 @@ AGENT_ID ?= 1
|
|||||||
|
|
||||||
load-sample-specs:
|
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/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/proxy.emissary.cadoles.com.json | ./bin/server api agent spec update -a $(AGENT_ID) --no-patch --spec-data - --spec-name proxy.emissary.cadoles.com
|
||||||
|
|
||||||
|
full-version:
|
||||||
|
@echo $(FULL_VERSION)
|
@ -7,6 +7,7 @@ import (
|
|||||||
"forge.cadoles.com/Cadoles/emissary/internal/command/agent"
|
"forge.cadoles.com/Cadoles/emissary/internal/command/agent"
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/command/api"
|
"forge.cadoles.com/Cadoles/emissary/internal/command/api"
|
||||||
|
|
||||||
|
_ "forge.cadoles.com/Cadoles/emissary/internal/imports/format"
|
||||||
_ "forge.cadoles.com/Cadoles/emissary/internal/imports/spec"
|
_ "forge.cadoles.com/Cadoles/emissary/internal/imports/spec"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
2
go.mod
2
go.mod
@ -4,6 +4,7 @@ go 1.19
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
forge.cadoles.com/arcad/edge v0.0.0-20230328183829-d8ce2901d2ab
|
forge.cadoles.com/arcad/edge v0.0.0-20230328183829-d8ce2901d2ab
|
||||||
|
github.com/Masterminds/sprig/v3 v3.2.3
|
||||||
github.com/alecthomas/participle/v2 v2.0.0-beta.5
|
github.com/alecthomas/participle/v2 v2.0.0-beta.5
|
||||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883
|
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883
|
||||||
github.com/btcsuite/btcd/btcutil v1.1.3
|
github.com/btcsuite/btcd/btcutil v1.1.3
|
||||||
@ -31,7 +32,6 @@ require (
|
|||||||
require (
|
require (
|
||||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||||
github.com/Masterminds/semver/v3 v3.2.0 // indirect
|
github.com/Masterminds/semver/v3 v3.2.0 // indirect
|
||||||
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
|
|
||||||
github.com/barnybug/go-cast v0.0.0-20201201064555-a87ccbc26692 // 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/dop251/goja_nodejs v0.0.0-20230320130059-dcf93ba651dd // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.1 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.1 // indirect
|
||||||
|
4
go.sum
4
go.sum
@ -54,10 +54,6 @@ 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.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
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=
|
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-20230328081549-e09de0b0a4f4 h1:ZBBOOKqCEt6F9/Ikkwc2xwYDr7JpLybvxtoRJwXt7Gw=
|
|
||||||
forge.cadoles.com/arcad/edge v0.0.0-20230328081549-e09de0b0a4f4/go.mod h1:ONd6vyQ0IM0vHi1i+bmZBRc1Fd0BoXMuDdY/+0sZefw=
|
|
||||||
forge.cadoles.com/arcad/edge v0.0.0-20230328183829-d8ce2901d2ab h1:xOtzLAYOUcKd/VBx/PzL2riC0zNuQ/cxxf5r3AmEvJE=
|
forge.cadoles.com/arcad/edge v0.0.0-20230328183829-d8ce2901d2ab h1:xOtzLAYOUcKd/VBx/PzL2riC0zNuQ/cxxf5r3AmEvJE=
|
||||||
forge.cadoles.com/arcad/edge v0.0.0-20230328183829-d8ce2901d2ab/go.mod h1:ONd6vyQ0IM0vHi1i+bmZBRc1Fd0BoXMuDdY/+0sZefw=
|
forge.cadoles.com/arcad/edge v0.0.0-20230328183829-d8ce2901d2ab/go.mod h1:ONd6vyQ0IM0vHi1i+bmZBRc1Fd0BoXMuDdY/+0sZefw=
|
||||||
gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8=
|
gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8=
|
||||||
|
@ -28,6 +28,8 @@ import (
|
|||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const defaultSQLiteParams = "?_pragma=foreign_keys(1)&_pragma=journal_mode(WAL)&_txlock=immediate"
|
||||||
|
|
||||||
func (c *Controller) getHandlerOptions(ctx context.Context, appKey string, specs *spec.Spec) ([]edgeHTTP.HandlerOptionFunc, error) {
|
func (c *Controller) getHandlerOptions(ctx context.Context, appKey string, specs *spec.Spec) ([]edgeHTTP.HandlerOptionFunc, error) {
|
||||||
dataDir, err := c.ensureAppDataDir(ctx, appKey)
|
dataDir, err := c.ensureAppDataDir(ctx, appKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -35,7 +37,7 @@ func (c *Controller) getHandlerOptions(ctx context.Context, appKey string, specs
|
|||||||
}
|
}
|
||||||
|
|
||||||
dbFile := filepath.Join(dataDir, appKey+".sqlite")
|
dbFile := filepath.Join(dataDir, appKey+".sqlite")
|
||||||
db, err := sqlite.Open(dbFile)
|
db, err := sqlite.Open(dbFile + defaultSQLiteParams)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrapf(err, "could not open database file '%s'", dbFile)
|
return nil, errors.Wrapf(err, "could not open database file '%s'", dbFile)
|
||||||
}
|
}
|
||||||
@ -51,10 +53,8 @@ func (c *Controller) getHandlerOptions(ctx context.Context, appKey string, specs
|
|||||||
bundles = append(bundles, path)
|
bundles = append(bundles, path)
|
||||||
}
|
}
|
||||||
|
|
||||||
getAppURL := createGetAppURL(specs)
|
|
||||||
|
|
||||||
bus := memory.NewBus()
|
bus := memory.NewBus()
|
||||||
modules := getAppModules(bus, db, specs, keySet, getAppURL, bundles)
|
modules := c.getAppModules(bus, db, specs, keySet)
|
||||||
|
|
||||||
options := []edgeHTTP.HandlerOptionFunc{
|
options := []edgeHTTP.HandlerOptionFunc{
|
||||||
edgeHTTP.WithBus(bus),
|
edgeHTTP.WithBus(bus),
|
||||||
@ -148,7 +148,7 @@ func createGetAppURL(specs *spec.Spec) GetURLFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAppModules(bus bus.Bus, db *sql.DB, spec *appSpec.Spec, keySet jwk.Set, getAppURL GetURLFunc, bundles []string) []app.ServerModuleFactory {
|
func (c *Controller) getAppModules(bus bus.Bus, db *sql.DB, spec *appSpec.Spec, keySet jwk.Set) []app.ServerModuleFactory {
|
||||||
ds := sqlite.NewDocumentStoreWithDB(db)
|
ds := sqlite.NewDocumentStoreWithDB(db)
|
||||||
bs := sqlite.NewBlobStoreWithDB(db)
|
bs := sqlite.NewBlobStoreWithDB(db)
|
||||||
|
|
||||||
@ -185,6 +185,6 @@ func getAppModules(bus bus.Bus, db *sql.DB, spec *appSpec.Spec, keySet jwk.Set,
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
appModule.ModuleFactory(NewAppRepository(getAppURL, bundles...)),
|
appModule.ModuleFactory(c.appRepository),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ package app
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||||
"forge.cadoles.com/arcad/edge/pkg/bundle"
|
"forge.cadoles.com/arcad/edge/pkg/bundle"
|
||||||
@ -15,10 +16,14 @@ type GetURLFunc func(context.Context, *app.Manifest) (string, error)
|
|||||||
type AppRepository struct {
|
type AppRepository struct {
|
||||||
getURL GetURLFunc
|
getURL GetURLFunc
|
||||||
bundles []string
|
bundles []string
|
||||||
|
mutex sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get implements app.Repository
|
// Get implements app.Repository
|
||||||
func (r *AppRepository) Get(ctx context.Context, id app.ID) (*app.Manifest, error) {
|
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)
|
manifest, err := r.findManifest(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, errors.WithStack(err)
|
||||||
@ -29,6 +34,9 @@ func (r *AppRepository) Get(ctx context.Context, id app.ID) (*app.Manifest, erro
|
|||||||
|
|
||||||
// GetURL implements app.Repository
|
// GetURL implements app.Repository
|
||||||
func (r *AppRepository) GetURL(ctx context.Context, id app.ID) (string, error) {
|
func (r *AppRepository) GetURL(ctx context.Context, id app.ID) (string, error) {
|
||||||
|
r.mutex.RLock()
|
||||||
|
defer r.mutex.RUnlock()
|
||||||
|
|
||||||
manifest, err := r.findManifest(ctx, id)
|
manifest, err := r.findManifest(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", errors.WithStack(err)
|
return "", errors.WithStack(err)
|
||||||
@ -44,6 +52,9 @@ func (r *AppRepository) GetURL(ctx context.Context, id app.ID) (string, error) {
|
|||||||
|
|
||||||
// List implements app.Repository
|
// List implements app.Repository
|
||||||
func (r *AppRepository) List(ctx context.Context) ([]*app.Manifest, error) {
|
func (r *AppRepository) List(ctx context.Context) ([]*app.Manifest, error) {
|
||||||
|
r.mutex.RLock()
|
||||||
|
defer r.mutex.RUnlock()
|
||||||
|
|
||||||
manifests := make([]*app.Manifest, 0)
|
manifests := make([]*app.Manifest, 0)
|
||||||
|
|
||||||
for _, path := range r.bundles {
|
for _, path := range r.bundles {
|
||||||
@ -69,6 +80,14 @@ func (r *AppRepository) List(ctx context.Context) ([]*app.Manifest, error) {
|
|||||||
return manifests, nil
|
return manifests, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *AppRepository) Update(getURL GetURLFunc, bundles []string) {
|
||||||
|
r.mutex.Lock()
|
||||||
|
defer r.mutex.Unlock()
|
||||||
|
|
||||||
|
r.getURL = getURL
|
||||||
|
r.bundles = bundles
|
||||||
|
}
|
||||||
|
|
||||||
func (r *AppRepository) findManifest(ctx context.Context, id app.ID) (*app.Manifest, error) {
|
func (r *AppRepository) findManifest(ctx context.Context, id app.ID) (*app.Manifest, error) {
|
||||||
for _, path := range r.bundles {
|
for _, path := range r.bundles {
|
||||||
bundleCtx := logger.With(ctx, logger.F("path", path))
|
bundleCtx := logger.With(ctx, logger.F("path", path))
|
||||||
@ -97,8 +116,13 @@ func (r *AppRepository) findManifest(ctx context.Context, id app.ID) (*app.Manif
|
|||||||
return nil, errors.WithStack(appModule.ErrNotFound)
|
return nil, errors.WithStack(appModule.ErrNotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAppRepository(getURL GetURLFunc, bundles ...string) *AppRepository {
|
func NewAppRepository() *AppRepository {
|
||||||
return &AppRepository{getURL, bundles}
|
return &AppRepository{
|
||||||
|
getURL: func(ctx context.Context, m *app.Manifest) (string, error) {
|
||||||
|
return "", errors.New("unavailable")
|
||||||
|
},
|
||||||
|
bundles: []string{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ appModule.Repository = &AppRepository{}
|
var _ appModule.Repository = &AppRepository{}
|
||||||
|
@ -16,15 +16,16 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type serverEntry struct {
|
type serverEntry struct {
|
||||||
SpecHash uint64
|
AppDefHash uint64
|
||||||
Server *Server
|
Server *Server
|
||||||
}
|
}
|
||||||
|
|
||||||
type Controller struct {
|
type Controller struct {
|
||||||
client *http.Client
|
client *http.Client
|
||||||
downloadDir string
|
downloadDir string
|
||||||
dataDir string
|
dataDir string
|
||||||
servers map[string]*serverEntry
|
servers map[string]*serverEntry
|
||||||
|
appRepository *AppRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
// Name implements node.Controller.
|
// Name implements node.Controller.
|
||||||
@ -95,7 +96,9 @@ func (c *Controller) updateApps(ctx context.Context, specs *spec.Spec) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// (Re)start apps
|
c.updateAppRepository(ctx, specs)
|
||||||
|
|
||||||
|
// (Re)start apps if necessary
|
||||||
for appKey := range specs.Apps {
|
for appKey := range specs.Apps {
|
||||||
appCtx := logger.With(ctx, logger.F("appKey", appKey))
|
appCtx := logger.With(ctx, logger.F("appKey", appKey))
|
||||||
|
|
||||||
@ -106,10 +109,35 @@ func (c *Controller) updateApps(ctx context.Context, specs *spec.Spec) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Controller) updateAppRepository(ctx context.Context, specs *spec.Spec) {
|
||||||
|
bundles := make([]string, 0, len(specs.Apps))
|
||||||
|
for appKey, app := range specs.Apps {
|
||||||
|
path := c.getAppBundlePath(appKey, app.Format)
|
||||||
|
bundles = append(bundles, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
getURL := createGetAppURL(specs)
|
||||||
|
|
||||||
|
c.appRepository.Update(getURL, bundles)
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Controller) updateApp(ctx context.Context, specs *spec.Spec, appKey string) (err error) {
|
func (c *Controller) updateApp(ctx context.Context, specs *spec.Spec, appKey string) (err error) {
|
||||||
appEntry := specs.Apps[appKey]
|
appEntry := specs.Apps[appKey]
|
||||||
|
|
||||||
newAppSpecHash, err := hashstructure.Hash(appEntry, hashstructure.FormatV2, nil)
|
var auth *spec.Auth
|
||||||
|
if specs.Config != nil {
|
||||||
|
auth = specs.Config.Auth
|
||||||
|
}
|
||||||
|
|
||||||
|
appDef := struct {
|
||||||
|
App spec.AppEntry
|
||||||
|
Auth *spec.Auth
|
||||||
|
}{
|
||||||
|
App: appEntry,
|
||||||
|
Auth: auth,
|
||||||
|
}
|
||||||
|
|
||||||
|
newAppDefHash, err := hashstructure.Hash(appDef, hashstructure.FormatV2, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
@ -148,20 +176,20 @@ func (c *Controller) updateApp(ctx context.Context, specs *spec.Spec, appKey str
|
|||||||
}
|
}
|
||||||
|
|
||||||
server = &serverEntry{
|
server = &serverEntry{
|
||||||
Server: NewServer(bundle, auth, options...),
|
Server: NewServer(bundle, auth, options...),
|
||||||
SpecHash: 0,
|
AppDefHash: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
c.servers[appKey] = server
|
c.servers[appKey] = server
|
||||||
}
|
}
|
||||||
|
|
||||||
specChanged := newAppSpecHash != server.SpecHash
|
defChanged := newAppDefHash != server.AppDefHash
|
||||||
|
|
||||||
if server.Server.Running() && !specChanged {
|
if server.Server.Running() && !defChanged {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if specChanged && server.SpecHash != 0 {
|
if defChanged && server.AppDefHash != 0 {
|
||||||
logger.Info(
|
logger.Info(
|
||||||
ctx, "restarting app",
|
ctx, "restarting app",
|
||||||
logger.F("address", appEntry.Address),
|
logger.F("address", appEntry.Address),
|
||||||
@ -179,7 +207,7 @@ func (c *Controller) updateApp(ctx context.Context, specs *spec.Spec, appKey str
|
|||||||
return errors.Wrap(err, "could not start app")
|
return errors.Wrap(err, "could not start app")
|
||||||
}
|
}
|
||||||
|
|
||||||
server.SpecHash = newAppSpecHash
|
server.AppDefHash = newAppDefHash
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -294,10 +322,11 @@ func NewController(funcs ...OptionFunc) *Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &Controller{
|
return &Controller{
|
||||||
client: opts.Client,
|
client: opts.Client,
|
||||||
downloadDir: opts.DownloadDir,
|
downloadDir: opts.DownloadDir,
|
||||||
dataDir: opts.DataDir,
|
dataDir: opts.DataDir,
|
||||||
servers: make(map[string]*serverEntry),
|
servers: make(map[string]*serverEntry),
|
||||||
|
appRepository: NewAppRepository(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,11 +3,13 @@ package app
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/agent/controller/app/spec"
|
"forge.cadoles.com/Cadoles/emissary/internal/agent/controller/app/spec"
|
||||||
appSpec "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"
|
edgeHTTP "forge.cadoles.com/arcad/edge/pkg/http"
|
||||||
authHTTP "forge.cadoles.com/arcad/edge/pkg/module/auth/http"
|
authHTTP "forge.cadoles.com/arcad/edge/pkg/module/auth/http"
|
||||||
"gitlab.com/wpetit/goweb/logger"
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
@ -33,12 +35,15 @@ type Server struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) Start(ctx context.Context, addr string) (err error) {
|
func (s *Server) Start(ctx context.Context, addr string) (err error) {
|
||||||
if s.server != nil {
|
if s.Running() {
|
||||||
if err := s.Stop(); err != nil {
|
if err := s.Stop(); err != nil {
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s.serverMutex.Lock()
|
||||||
|
defer s.serverMutex.Unlock()
|
||||||
|
|
||||||
router := chi.NewRouter()
|
router := chi.NewRouter()
|
||||||
|
|
||||||
router.Use(middleware.Logger)
|
router.Use(middleware.Logger)
|
||||||
@ -83,9 +88,7 @@ func (s *Server) Start(ctx context.Context, addr string) (err error) {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
s.serverMutex.Lock()
|
|
||||||
s.server = server
|
s.server = server
|
||||||
s.serverMutex.Unlock()
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -98,20 +101,25 @@ func (s *Server) Running() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) Stop() error {
|
func (s *Server) Stop() error {
|
||||||
|
if !s.Running() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
s.serverMutex.Lock()
|
||||||
|
defer s.serverMutex.Unlock()
|
||||||
|
|
||||||
if s.server == nil {
|
if s.server == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
defer func() {
|
|
||||||
s.serverMutex.Lock()
|
|
||||||
s.server = nil
|
|
||||||
s.serverMutex.Unlock()
|
|
||||||
}()
|
|
||||||
|
|
||||||
if err := s.server.Close(); err != nil {
|
if err := s.server.Close(); err != nil {
|
||||||
panic(errors.WithStack(err))
|
s.server = nil
|
||||||
|
|
||||||
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s.server = nil
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -140,6 +148,10 @@ func (s *Server) configureAuth(router chi.Router, auth *spec.Auth) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s.auth.Local.CookieDomain != "" {
|
||||||
|
router.Use(invalidCookieDomainRedirect(s.auth.Local.CookieDomain))
|
||||||
|
}
|
||||||
|
|
||||||
router.Handle("/auth/*", authHTTP.NewLocalHandler(
|
router.Handle("/auth/*", authHTTP.NewLocalHandler(
|
||||||
jwa.HS256, key,
|
jwa.HS256, key,
|
||||||
authHTTP.WithRoutePrefix("/auth"),
|
authHTTP.WithRoutePrefix("/auth"),
|
||||||
@ -158,3 +170,33 @@ func NewServer(bundle bundle.Bundle, auth *appSpec.Auth, handlerOptions ...edgeH
|
|||||||
handlerOptions: handlerOptions,
|
handlerOptions: handlerOptions,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func invalidCookieDomainRedirect(cookieDomain string) func(http.Handler) http.Handler {
|
||||||
|
domain := strings.TrimPrefix(cookieDomain, ".")
|
||||||
|
hostPattern := "*" + domain
|
||||||
|
|
||||||
|
return func(h http.Handler) http.Handler {
|
||||||
|
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
hostParts := strings.SplitN(r.Host, ":", 2)
|
||||||
|
|
||||||
|
if !wildcard.Match(hostParts[0], hostPattern) {
|
||||||
|
url := r.URL
|
||||||
|
|
||||||
|
newHost := domain
|
||||||
|
if len(hostParts) > 1 {
|
||||||
|
newHost += ":" + hostParts[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
url.Host = newHost
|
||||||
|
|
||||||
|
http.Redirect(w, r, url.String(), http.StatusTemporaryRedirect)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.HandlerFunc(fn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/auth"
|
"forge.cadoles.com/Cadoles/emissary/internal/auth"
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||||
@ -13,8 +14,11 @@ import (
|
|||||||
"gitlab.com/wpetit/goweb/logger"
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const DefaultAcceptableSkew = 5 * time.Minute
|
||||||
|
|
||||||
type Authenticator struct {
|
type Authenticator struct {
|
||||||
repo datastore.AgentRepository
|
repo datastore.AgentRepository
|
||||||
|
acceptableSkew time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
// Authenticate implements auth.Authenticator.
|
// Authenticate implements auth.Authenticator.
|
||||||
@ -71,11 +75,19 @@ func (a *Authenticator) Authenticate(ctx context.Context, r *http.Request) (auth
|
|||||||
[]byte(rawToken),
|
[]byte(rawToken),
|
||||||
jwt.WithKeySet(agent.KeySet.Set, jws.WithRequireKid(false)),
|
jwt.WithKeySet(agent.KeySet.Set, jws.WithRequireKid(false)),
|
||||||
jwt.WithValidate(true),
|
jwt.WithValidate(true),
|
||||||
|
jwt.WithAcceptableSkew(a.acceptableSkew),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.WithStack(err)
|
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{
|
user := &User{
|
||||||
agent: agent,
|
agent: agent,
|
||||||
}
|
}
|
||||||
@ -83,9 +95,10 @@ func (a *Authenticator) Authenticate(ctx context.Context, r *http.Request) (auth
|
|||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAuthenticator(repo datastore.AgentRepository) *Authenticator {
|
func NewAuthenticator(repo datastore.AgentRepository, acceptableSkew time.Duration) *Authenticator {
|
||||||
return &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)
|
return "", errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now().UTC()
|
||||||
|
|
||||||
if err := token.Set(jwt.NotBeforeKey, now); err != nil {
|
if err := token.Set(jwt.NotBeforeKey, now); err != nil {
|
||||||
return "", errors.WithStack(err)
|
return "", errors.WithStack(err)
|
||||||
|
17
internal/auth/thirdparty/authenticator.go
vendored
17
internal/auth/thirdparty/authenticator.go
vendored
@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/auth"
|
"forge.cadoles.com/Cadoles/emissary/internal/auth"
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/jwk"
|
"forge.cadoles.com/Cadoles/emissary/internal/jwk"
|
||||||
@ -11,9 +12,12 @@ import (
|
|||||||
"gitlab.com/wpetit/goweb/logger"
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const DefaultAcceptableSkew = 5 * time.Minute
|
||||||
|
|
||||||
type Authenticator struct {
|
type Authenticator struct {
|
||||||
keys jwk.Set
|
keys jwk.Set
|
||||||
issuer string
|
issuer string
|
||||||
|
acceptableSkew time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
// Authenticate implements auth.Authenticator.
|
// 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)
|
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 {
|
if err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, errors.WithStack(err)
|
||||||
}
|
}
|
||||||
@ -57,10 +61,11 @@ func (a *Authenticator) Authenticate(ctx context.Context, r *http.Request) (auth
|
|||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAuthenticator(keys jwk.Set, issuer string) *Authenticator {
|
func NewAuthenticator(keys jwk.Set, issuer string, acceptableSkew time.Duration) *Authenticator {
|
||||||
return &Authenticator{
|
return &Authenticator{
|
||||||
keys: keys,
|
keys: keys,
|
||||||
issuer: issuer,
|
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"
|
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(
|
token, err := jwt.Parse(
|
||||||
[]byte(rawToken),
|
[]byte(rawToken),
|
||||||
jwt.WithKeySet(keys, jws.WithRequireKid(false)),
|
jwt.WithKeySet(keys, jws.WithRequireKid(false)),
|
||||||
jwt.WithIssuer(issuer),
|
jwt.WithIssuer(issuer),
|
||||||
jwt.WithValidate(true),
|
jwt.WithValidate(true),
|
||||||
|
jwt.WithAcceptableSkew(acceptableSkew),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.WithStack(err)
|
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)
|
return "", errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now().UTC()
|
||||||
|
|
||||||
if err := token.Set(jwt.NotBeforeKey, now); err != nil {
|
if err := token.Set(jwt.NotBeforeKey, now); err != nil {
|
||||||
return "", errors.WithStack(err)
|
return "", errors.WithStack(err)
|
||||||
|
@ -10,6 +10,7 @@ import (
|
|||||||
|
|
||||||
type UpdateAgentOptions struct {
|
type UpdateAgentOptions struct {
|
||||||
Status *int
|
Status *int
|
||||||
|
Label *string
|
||||||
Options []OptionFunc
|
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 {
|
func WithUpdateAgentsOptions(funcs ...OptionFunc) UpdateAgentOptionFunc {
|
||||||
return func(opts *UpdateAgentOptions) {
|
return func(opts *UpdateAgentOptions) {
|
||||||
opts.Options = funcs
|
opts.Options = funcs
|
||||||
@ -39,6 +46,10 @@ func (c *Client) UpdateAgent(ctx context.Context, agentID datastore.AgentID, fun
|
|||||||
payload["status"] = *opts.Status
|
payload["status"] = *opts.Status
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if opts.Label != nil {
|
||||||
|
payload["label"] = *opts.Label
|
||||||
|
}
|
||||||
|
|
||||||
response := withResponse[struct {
|
response := withResponse[struct {
|
||||||
Agent *datastore.Agent `json:"agent"`
|
Agent *datastore.Agent `json:"agent"`
|
||||||
}]()
|
}]()
|
||||||
|
@ -49,10 +49,6 @@ func RunCommand() *cli.Command {
|
|||||||
controllers = append(controllers, spec.NewController())
|
controllers = append(controllers, spec.NewController())
|
||||||
}
|
}
|
||||||
|
|
||||||
if ctrlConf.Proxy.Enabled {
|
|
||||||
controllers = append(controllers, proxy.NewController())
|
|
||||||
}
|
|
||||||
|
|
||||||
if ctrlConf.UCI.Enabled {
|
if ctrlConf.UCI.Enabled {
|
||||||
controllers = append(controllers, openwrt.NewUCIController(
|
controllers = append(controllers, openwrt.NewUCIController(
|
||||||
string(ctrlConf.UCI.BinPath),
|
string(ctrlConf.UCI.BinPath),
|
||||||
@ -66,6 +62,10 @@ func RunCommand() *cli.Command {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ctrlConf.Proxy.Enabled {
|
||||||
|
controllers = append(controllers, proxy.NewController())
|
||||||
|
}
|
||||||
|
|
||||||
if ctrlConf.SysUpgrade.Enabled {
|
if ctrlConf.SysUpgrade.Enabled {
|
||||||
sysUpgradeArgs := make([]string, 0)
|
sysUpgradeArgs := make([]string, 0)
|
||||||
if len(ctrlConf.SysUpgrade.SysUpgradeCommand) > 1 {
|
if len(ctrlConf.SysUpgrade.SysUpgradeCommand) > 1 {
|
||||||
|
@ -22,6 +22,11 @@ func UpdateCommand() *cli.Command {
|
|||||||
Usage: "Set `STATUS` to selected agent",
|
Usage: "Set `STATUS` to selected agent",
|
||||||
Value: -1,
|
Value: -1,
|
||||||
},
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "label",
|
||||||
|
Usage: "Set `LABEL` to selected agent",
|
||||||
|
Value: "",
|
||||||
|
},
|
||||||
),
|
),
|
||||||
Action: func(ctx *cli.Context) error {
|
Action: func(ctx *cli.Context) error {
|
||||||
baseFlags := clientFlag.GetBaseFlags(ctx)
|
baseFlags := clientFlag.GetBaseFlags(ctx)
|
||||||
@ -43,6 +48,11 @@ func UpdateCommand() *cli.Command {
|
|||||||
options = append(options, client.WithAgentStatus(status))
|
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))
|
client := client.New(baseFlags.ServerURL, client.WithToken(token))
|
||||||
|
|
||||||
agent, err := client.UpdateAgent(ctx.Context, agentID, options...)
|
agent, err := client.UpdateAgent(ctx.Context, agentID, options...)
|
||||||
|
@ -7,9 +7,10 @@ func agentHints(outputMode format.OutputMode) format.Hints {
|
|||||||
OutputMode: outputMode,
|
OutputMode: outputMode,
|
||||||
Props: []format.Prop{
|
Props: []format.Prop{
|
||||||
format.NewProp("ID", "ID"),
|
format.NewProp("ID", "ID"),
|
||||||
|
format.NewProp("Label", "Label"),
|
||||||
format.NewProp("Thumbprint", "Thumbprint"),
|
format.NewProp("Thumbprint", "Thumbprint"),
|
||||||
format.NewProp("Status", "Status"),
|
format.NewProp("Status", "Status"),
|
||||||
format.NewProp("CreatedAt", "CreatedAt"),
|
format.NewProp("ContactedAt", "ContactedAt"),
|
||||||
format.NewProp("UpdatedAt", "UpdatedAt"),
|
format.NewProp("UpdatedAt", "UpdatedAt"),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,6 @@ type DatabaseConfig struct {
|
|||||||
func NewDefaultDatabaseConfig() DatabaseConfig {
|
func NewDefaultDatabaseConfig() DatabaseConfig {
|
||||||
return DatabaseConfig{
|
return DatabaseConfig{
|
||||||
Driver: "sqlite",
|
Driver: "sqlite",
|
||||||
DSN: "sqlite://emissary.sqlite",
|
DSN: "sqlite://emissary.sqlite?_pragma=foreign_keys(1)&_pragma=journal_mode(WAL)&_txlock=immediate",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,13 +20,15 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Agent struct {
|
type Agent struct {
|
||||||
ID AgentID `json:"id"`
|
ID AgentID `json:"id"`
|
||||||
Thumbprint string `json:"thumbprint"`
|
Label string `json:"label"`
|
||||||
KeySet *SerializableKeySet `json:"keyset,omitempty"`
|
Thumbprint string `json:"thumbprint"`
|
||||||
Metadata map[string]any `json:"metadata,omitempty"`
|
KeySet *SerializableKeySet `json:"keyset,omitempty"`
|
||||||
Status AgentStatus `json:"status"`
|
Metadata map[string]any `json:"metadata,omitempty"`
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
Status AgentStatus `json:"status"`
|
||||||
UpdatedAt time.Time `json:"updatedAt"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
|
ContactedAt *time.Time `json:"contactedAt,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SerializableKeySet struct {
|
type SerializableKeySet struct {
|
||||||
|
@ -2,6 +2,7 @@ package datastore
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/lestrrat-go/jwx/v2/jwk"
|
"github.com/lestrrat-go/jwx/v2/jwk"
|
||||||
)
|
)
|
||||||
@ -68,10 +69,12 @@ func WithAgentQueryThumbprints(thumbprints ...string) AgentQueryOptionFunc {
|
|||||||
type AgentUpdateOptionFunc func(*AgentUpdateOptions)
|
type AgentUpdateOptionFunc func(*AgentUpdateOptions)
|
||||||
|
|
||||||
type AgentUpdateOptions struct {
|
type AgentUpdateOptions struct {
|
||||||
Status *AgentStatus
|
Label *string
|
||||||
Metadata *map[string]any
|
Status *AgentStatus
|
||||||
KeySet *jwk.Set
|
ContactedAt *time.Time
|
||||||
Thumbprint *string
|
Metadata *map[string]any
|
||||||
|
KeySet *jwk.Set
|
||||||
|
Thumbprint *string
|
||||||
}
|
}
|
||||||
|
|
||||||
func WithAgentUpdateStatus(status AgentStatus) AgentUpdateOptionFunc {
|
func WithAgentUpdateStatus(status AgentStatus) AgentUpdateOptionFunc {
|
||||||
@ -97,3 +100,15 @@ func WithAgentUpdateThumbprint(thumbprint string) AgentUpdateOptionFunc {
|
|||||||
opts.Thumbprint = &thumbprint
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -45,7 +45,11 @@ func (r *AgentRepository) GetSpecs(ctx context.Context, agentID datastore.AgentI
|
|||||||
return nil, errors.WithStack(err)
|
return nil, 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() {
|
for rows.Next() {
|
||||||
spec := &datastore.Spec{}
|
spec := &datastore.Spec{}
|
||||||
@ -61,6 +65,10 @@ func (r *AgentRepository) GetSpecs(ctx context.Context, agentID datastore.AgentI
|
|||||||
specs = append(specs, spec)
|
specs = append(specs, spec)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
return specs, nil
|
return specs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -119,7 +127,7 @@ func (r *AgentRepository) Query(ctx context.Context, opts ...datastore.AgentQuer
|
|||||||
count := 0
|
count := 0
|
||||||
|
|
||||||
err := r.withTx(ctx, func(tx *sql.Tx) error {
|
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
|
limit := 10
|
||||||
if options.Limit != nil {
|
if options.Limit != nil {
|
||||||
@ -176,22 +184,34 @@ func (r *AgentRepository) Query(ctx context.Context, opts ...datastore.AgentQuer
|
|||||||
return errors.WithStack(err)
|
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() {
|
for rows.Next() {
|
||||||
agent := &datastore.Agent{}
|
agent := &datastore.Agent{}
|
||||||
|
|
||||||
metadata := JSONMap{}
|
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)
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
agent.Metadata = metadata
|
agent.Metadata = metadata
|
||||||
|
if contactedAt.Valid {
|
||||||
|
agent.ContactedAt = &contactedAt.Time
|
||||||
|
}
|
||||||
|
|
||||||
agents = append(agents, agent)
|
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...)
|
row := tx.QueryRowContext(ctx, `SELECT count(id) FROM agents `+filters, args...)
|
||||||
if err := row.Scan(&count); err != nil {
|
if err := row.Scan(&count); err != nil {
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
@ -299,7 +319,7 @@ func (r *AgentRepository) Get(ctx context.Context, id datastore.AgentID) (*datas
|
|||||||
|
|
||||||
err := r.withTx(ctx, func(tx *sql.Tx) error {
|
err := r.withTx(ctx, func(tx *sql.Tx) error {
|
||||||
query := `
|
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
|
FROM agents
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
`
|
`
|
||||||
@ -307,9 +327,10 @@ func (r *AgentRepository) Get(ctx context.Context, id datastore.AgentID) (*datas
|
|||||||
row := r.db.QueryRowContext(ctx, query, id)
|
row := r.db.QueryRowContext(ctx, query, id)
|
||||||
|
|
||||||
metadata := JSONMap{}
|
metadata := JSONMap{}
|
||||||
|
contactedAt := sql.NullTime{}
|
||||||
var rawKeySet []byte
|
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) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
return datastore.ErrNotFound
|
return datastore.ErrNotFound
|
||||||
}
|
}
|
||||||
@ -318,6 +339,9 @@ func (r *AgentRepository) Get(ctx context.Context, id datastore.AgentID) (*datas
|
|||||||
}
|
}
|
||||||
|
|
||||||
agent.Metadata = metadata
|
agent.Metadata = metadata
|
||||||
|
if contactedAt.Valid {
|
||||||
|
agent.ContactedAt = &contactedAt.Time
|
||||||
|
}
|
||||||
|
|
||||||
keySet := jwk.NewSet()
|
keySet := jwk.NewSet()
|
||||||
if err := json.Unmarshal(rawKeySet, &keySet); err != nil {
|
if err := json.Unmarshal(rawKeySet, &keySet); err != nil {
|
||||||
@ -346,15 +370,11 @@ func (r *AgentRepository) Update(ctx context.Context, id datastore.AgentID, opts
|
|||||||
|
|
||||||
err := r.withTx(ctx, func(tx *sql.Tx) error {
|
err := r.withTx(ctx, func(tx *sql.Tx) error {
|
||||||
query := `
|
query := `
|
||||||
UPDATE agents SET updated_at = $2
|
UPDATE agents SET id = $1
|
||||||
`
|
`
|
||||||
|
|
||||||
now := time.Now().UTC()
|
args := []any{id}
|
||||||
|
index := 2
|
||||||
args := []any{
|
|
||||||
id, now,
|
|
||||||
}
|
|
||||||
index := 3
|
|
||||||
|
|
||||||
if options.Status != nil {
|
if options.Status != nil {
|
||||||
query += fmt.Sprintf(`, status = $%d`, index)
|
query += fmt.Sprintf(`, status = $%d`, index)
|
||||||
@ -379,23 +399,51 @@ func (r *AgentRepository) Update(ctx context.Context, id datastore.AgentID, opts
|
|||||||
index++
|
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 {
|
if options.Metadata != nil {
|
||||||
query += fmt.Sprintf(`, metadata = $%d`, index)
|
query += fmt.Sprintf(`, metadata = $%d`, index)
|
||||||
args = append(args, JSONMap(*options.Metadata))
|
args = append(args, JSONMap(*options.Metadata))
|
||||||
index++
|
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 += `
|
query += `
|
||||||
WHERE id = $1
|
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...)
|
row := tx.QueryRowContext(ctx, query, args...)
|
||||||
|
|
||||||
metadata := JSONMap{}
|
metadata := JSONMap{}
|
||||||
|
contactedAt := sql.NullTime{}
|
||||||
var rawKeySet []byte
|
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) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
return datastore.ErrNotFound
|
return datastore.ErrNotFound
|
||||||
}
|
}
|
||||||
@ -404,6 +452,9 @@ func (r *AgentRepository) Update(ctx context.Context, id datastore.AgentID, opts
|
|||||||
}
|
}
|
||||||
|
|
||||||
agent.Metadata = metadata
|
agent.Metadata = metadata
|
||||||
|
if contactedAt.Valid {
|
||||||
|
agent.ContactedAt = &contactedAt.Time
|
||||||
|
}
|
||||||
|
|
||||||
keySet := jwk.NewSet()
|
keySet := jwk.NewSet()
|
||||||
if err := json.Unmarshal(rawKeySet, &keySet); err != nil {
|
if err := json.Unmarshal(rawKeySet, &keySet); err != nil {
|
||||||
|
@ -145,6 +145,7 @@ func (s *Server) registerAgent(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
type updateAgentRequest struct {
|
type updateAgentRequest struct {
|
||||||
Status *datastore.AgentStatus `json:"status" validate:"omitempty,oneof=0 1 2 3"`
|
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) {
|
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))
|
options = append(options, datastore.WithAgentUpdateStatus(*updateAgentReq.Status))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if updateAgentReq.Label != nil {
|
||||||
|
options = append(options, datastore.WithAgentUpdateLabel(*updateAgentReq.Label))
|
||||||
|
}
|
||||||
|
|
||||||
agent, err := s.agentRepo.Update(
|
agent, err := s.agentRepo.Update(
|
||||||
ctx,
|
ctx,
|
||||||
datastore.AgentID(agentID),
|
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.Group(func(r chi.Router) {
|
||||||
r.Use(auth.Middleware(
|
r.Use(auth.Middleware(
|
||||||
thirdparty.NewAuthenticator(keys, string(s.conf.Issuer)),
|
thirdparty.NewAuthenticator(keys, string(s.conf.Issuer), thirdparty.DefaultAcceptableSkew),
|
||||||
agent.NewAuthenticator(s.agentRepo),
|
agent.NewAuthenticator(s.agentRepo, agent.DefaultAcceptableSkew),
|
||||||
))
|
))
|
||||||
|
|
||||||
r.Route("/agents", func(r chi.Router) {
|
r.Route("/agents", func(r chi.Router) {
|
||||||
|
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.19.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
|
port: 3000
|
||||||
database:
|
database:
|
||||||
driver: sqlite
|
driver: sqlite
|
||||||
dsn: sqlite:///var/lib/emissary/data.sqlite
|
dsn: sqlite:///var/lib/emissary/data.sqlite?_pragma=foreign_keys(1)&_pragma=journal_mode(WAL)&_txlock=immediate
|
||||||
cors:
|
cors:
|
||||||
allowedOrigins: []
|
allowedOrigins: []
|
||||||
allowCredentials: true
|
allowCredentials: true
|
||||||
|
@ -17,6 +17,12 @@
|
|||||||
"sha256sum": "e97b7b79159bb5d6a13b05644c091272b02a1a3cbb1b613dd5eda37e1eb84623",
|
"sha256sum": "e97b7b79159bb5d6a13b05644c091272b02a1a3cbb1b613dd5eda37e1eb84623",
|
||||||
"address": "127.0.0.1:8084",
|
"address": "127.0.0.1:8084",
|
||||||
"format": "zip"
|
"format": "zip"
|
||||||
|
},
|
||||||
|
"diffusion": {
|
||||||
|
"url": "https://emissary.cadol.es/files/apps/arcad.diffusion_v2023.3.29-5b3fab4.zip",
|
||||||
|
"sha256sum": "1282e75719beedbc7c7e67879389d0f3e11c86d3d2c37cf13da624a66faaeb58",
|
||||||
|
"address": "127.0.0.1:8085",
|
||||||
|
"format": "zip"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
|
@ -15,6 +15,10 @@
|
|||||||
"hostPattern": "test.arcad.local:*",
|
"hostPattern": "test.arcad.local:*",
|
||||||
"target": "http://localhost:8084"
|
"target": "http://localhost:8084"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"hostPattern": "diffusion.arcad.local:*",
|
||||||
|
"target": "http://localhost:8085"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"hostPattern": "*",
|
"hostPattern": "*",
|
||||||
"target": "http://localhost:8082"
|
"target": "http://localhost:8082"
|
||||||
|
Reference in New Issue
Block a user