Compare commits

...

3 Commits

37 changed files with 706 additions and 108 deletions

View File

@ -64,7 +64,7 @@ archives:
checksum:
name_template: 'checksums.txt'
snapshot:
name_template: "{{ incpatch .Version }}-next"
name_template: "{{ .Version }}"
changelog:
sort: asc
filters:

View File

@ -1,14 +1,16 @@
LINT_ARGS ?= --timeout 5m
GORELEASER_VERSION ?= v1.13.1
GORELEASER_ARGS ?= release --auto-snapshot --snapshot --rm-dist
GORELEASER_ARGS ?= release --snapshot --rm-dist
GITCHLOG_ARGS ?=
SHELL := /bin/bash
EMISSARY_VERSION ?=
GIT_VERSION ?= $(shell git describe --always)
GIT_VERSION := $(shell git describe --always)
DATE_VERSION := $(shell date +%Y.%-m.%-d)
FULL_VERSION := v$(DATE_VERSION)-$(GIT_VERSION)$(if $(shell git diff --stat),-dirty,)
DOCKER_IMAGE_NAME ?= bornholm/emissary
DOCKER_IMAGE_TAG ?= $(GIT_VERSION)$(if $(shell git diff --stat),-dirty,)
DOCKER_IMAGE_TAG ?= $(FULL_VERSION)
GOTEST_ARGS ?= -short
@ -43,7 +45,7 @@ build-emissary-%: deps ## Build executable
-v \
-ldflags "\
-X 'main.GitRef=$(shell git rev-parse --short HEAD)' \
-X 'main.ProjectVersion=$(shell git describe --always)' \
-X 'main.ProjectVersion=$(FULL_VERSION)' \
-X 'main.BuildDate=$(shell date --utc --rfc-3339=seconds)' \
" \
-o ./bin/$* \
@ -73,7 +75,7 @@ dump-config: build-emissary
.PHONY: goreleaser
goreleaser: deps
( set -o allexport && source .env && set +o allexport && VERSION=$(GORELEASER_VERSION) curl -sfL https://goreleaser.com/static/run | bash /dev/stdin $(GORELEASER_ARGS) )
( set -o allexport && source .env && set +o allexport && VERSION=$(GORELEASER_VERSION) curl -sfL https://goreleaser.com/static/run | GORELEASER_CURRENT_TAG="$(FULL_VERSION)" bash /dev/stdin $(GORELEASER_ARGS) )
.PHONY: start-release
start-release:
@ -126,8 +128,8 @@ gitea-release: tools/gitea-release/bin/gitea-release.sh goreleaser
GITEA_RELEASE_PROJECT="emissary" \
GITEA_RELEASE_ORG="arcad" \
GITEA_RELEASE_BASE_URL="https://forge.cadoles.com" \
GITEA_RELEASE_VERSION="$(GIT_VERSION)" \
GITEA_RELEASE_NAME="$(GIT_VERSION)" \
GITEA_RELEASE_VERSION="$(FULL_VERSION)" \
GITEA_RELEASE_NAME="$(FULL_VERSION)" \
GITEA_RELEASE_COMMITISH_TARGET="$(GIT_VERSION)" \
GITEA_RELEASE_IS_DRAFT="false" \
GITEA_RELEASE_BODY="" \

View File

@ -6,6 +6,8 @@ import (
"forge.cadoles.com/Cadoles/emissary/internal/command"
"forge.cadoles.com/Cadoles/emissary/internal/command/agent"
"forge.cadoles.com/Cadoles/emissary/internal/command/api"
_ "forge.cadoles.com/Cadoles/emissary/internal/imports/spec"
)
// nolint: gochecknoglobals

View File

@ -7,8 +7,9 @@ import (
"forge.cadoles.com/Cadoles/emissary/internal/command/api"
"forge.cadoles.com/Cadoles/emissary/internal/command/server"
_ "github.com/jackc/pgx/v5/stdlib"
_ "modernc.org/sqlite"
_ "forge.cadoles.com/Cadoles/emissary/internal/imports/format"
_ "forge.cadoles.com/Cadoles/emissary/internal/imports/spec"
_ "forge.cadoles.com/Cadoles/emissary/internal/imports/sql"
)
// nolint: gochecknoglobals

3
go.mod
View File

@ -3,7 +3,7 @@ module forge.cadoles.com/Cadoles/emissary
go 1.19
require (
forge.cadoles.com/arcad/edge v0.0.0-20230320204239-1f4f795d43ff
forge.cadoles.com/arcad/edge v0.0.0-20230322170544-cf8a3f8ac077
github.com/alecthomas/participle/v2 v2.0.0-beta.5
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883
github.com/btcsuite/btcd/btcutil v1.1.3
@ -22,7 +22,6 @@ require (
github.com/mitchellh/mapstructure v1.5.0
github.com/pkg/errors v0.9.1
github.com/qri-io/jsonschema v0.2.1
github.com/santhosh-tekuri/jsonschema/v5 v5.1.1
github.com/urfave/cli/v2 v2.24.4
gitlab.com/wpetit/goweb v0.0.0-20230227162855-a1f09bafccb3
gopkg.in/yaml.v3 v3.0.1

6
go.sum
View File

@ -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-20230320204239-1f4f795d43ff h1:dT7cZQWzSYfW3dVDOSe6OACBGBQNvn0wo32BIvDEowE=
forge.cadoles.com/arcad/edge v0.0.0-20230320204239-1f4f795d43ff/go.mod h1:ONd6vyQ0IM0vHi1i+bmZBRc1Fd0BoXMuDdY/+0sZefw=
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=
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=
@ -1169,8 +1169,6 @@ github.com/rwtodd/Go.Sed v0.0.0-20210816025313-55464686f9ef/go.mod h1:8AEUvGVi2u
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4=
github.com/safchain/ethtool v0.0.0-20210803160452-9aa261dae9b1/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4=
github.com/santhosh-tekuri/jsonschema/v5 v5.1.1 h1:lEOLY2vyGIqKWUI9nzsOJRV3mb3WC9dXYORsLEUcoeY=
github.com/santhosh-tekuri/jsonschema/v5 v5.1.1/go.mod h1:FKdcjfQW6rpZSnxxUvEA5H/cDPdvJ/SZJQLWWXWGrZ0=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
github.com/sclevine/spec v1.2.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24q7p+U=

View File

@ -28,8 +28,7 @@ import (
"github.com/lestrrat-go/jwx/v2/jwk"
"github.com/pkg/errors"
_ "forge.cadoles.com/arcad/edge/pkg/module/auth/http/passwd/argon2id"
_ "forge.cadoles.com/arcad/edge/pkg/module/auth/http/passwd/plain"
_ "forge.cadoles.com/Cadoles/emissary/internal/imports/passwd"
)
type Server struct {
@ -173,7 +172,15 @@ func (s *Server) getAppModules(bus bus.Bus, ds storage.DocumentStore, bs storage
auth.WithJWT(s.getJWTKeySet),
),
func(o *goja.Object) {
if err := o.Set("CLAIM_ROLE", "role"); err != nil {
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"))
}

View File

@ -0,0 +1,31 @@
package openwrt
import (
"crypto/sha256"
"encoding/hex"
"io"
"os"
"github.com/pkg/errors"
)
func hash(path string) (string, error) {
file, err := os.Open(path)
if err != nil {
return "", errors.WithStack(err)
}
hasher := sha256.New()
defer func() {
if err := file.Close(); err != nil {
panic(errors.WithStack(err))
}
}()
if _, err := io.Copy(hasher, file); err != nil {
return "", errors.WithStack(err)
}
return hex.EncodeToString(hasher.Sum(nil)), nil
}

View File

@ -0,0 +1,17 @@
package sysupgrade
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))
}
}

View File

@ -0,0 +1,20 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://sysupgrade.openwrt.emissary.cadoles.com/spec.json",
"title": "SysUpgradeSpec",
"description": "Emissary 'SysUpgrade' specification",
"type": "object",
"properties": {
"url": {
"type": "string"
},
"sha256sum": {
"type": "string"
},
"version": {
"type": "string"
}
},
"required": ["url", "sha256sum", "version"],
"additionalProperties": false
}

View File

@ -0,0 +1,38 @@
package sysupgrade
import (
"forge.cadoles.com/Cadoles/emissary/internal/spec"
)
const Name spec.Name = "sysupgrade.openwrt.emissary.cadoles.com"
type Spec struct {
Revision int `json:"revision"`
URL string `json:"url"`
SHA256Sum string `json:"sha256sum"`
Version string `json:"version"`
}
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{
"url": s.URL,
"version": s.Version,
"sha256sum": s.SHA256Sum,
}
}
func NewSpec() *Spec {
return &Spec{
Revision: -1,
}
}
var _ spec.Spec = &Spec{}

View File

@ -0,0 +1,9 @@
{
"name": "sysupgrade.openwrt.emissary.cadoles.com",
"data": {
"url": "http://example.com/firmware.img",
"sha256sum": "58019192dacdae17755707719707db007e26dac856102280583fbd18427dd352",
"version": "0.0.0"
},
"revision": 0
}

View File

@ -0,0 +1,65 @@
package sysupgrade
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)
}
}

View File

@ -0,0 +1,177 @@
package openwrt
import (
"context"
"io"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
"forge.cadoles.com/Cadoles/emissary/internal/agent"
"forge.cadoles.com/Cadoles/emissary/internal/agent/controller/openwrt/spec/sysupgrade"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
type SysUpgradeController struct {
client *http.Client
command string
args []string
firmwareVersion FirmwareVersion
}
// Name implements agent.Controller
func (*SysUpgradeController) Name() string {
return "sysupgrade-controller"
}
// Reconcile implements agent.Controller
func (c *SysUpgradeController) Reconcile(ctx context.Context, state *agent.State) error {
sysSpec := sysupgrade.NewSpec()
if err := state.GetSpec(sysupgrade.Name, sysSpec); err != nil {
if errors.Is(err, agent.ErrSpecNotFound) {
logger.Info(ctx, "could not find sysupgrade spec, doing nothing")
return nil
}
return errors.WithStack(err)
}
firmwareVersion, err := c.firmwareVersion.FirmwareVersion(ctx)
if err != nil {
return errors.WithStack(err)
}
ctx = logger.With(ctx,
logger.F("currentFirmwareVersion", firmwareVersion),
logger.F("newFirmwareVersion", sysSpec.Version),
)
if firmwareVersion == sysSpec.Version {
logger.Info(ctx, "firmware version did not change, doing nothing")
return nil
}
downloadDir, err := os.MkdirTemp(os.TempDir(), "emissary_sysupgrade_*")
if err != nil {
return errors.WithStack(err)
}
defer func() {
if err := os.RemoveAll(downloadDir); err != nil {
logger.Error(
ctx, "could not remove download direction",
logger.E(errors.WithStack(err)),
logger.F("downloadDir", downloadDir),
)
}
}()
firmwareFile := filepath.Join(downloadDir, "firmware.bin")
logger.Info(
ctx, "downloading firmware",
logger.F("url", sysSpec.URL), logger.F("sha256sum", sysSpec.SHA256Sum),
)
if err := c.downloadFile(ctx, sysSpec.URL, sysSpec.SHA256Sum, firmwareFile); err != nil {
return errors.WithStack(err)
}
logger.Info(ctx, "upgrading firmware")
if err := c.upgradeFirmware(ctx, firmwareFile); err != nil {
return errors.WithStack(err)
}
return nil
}
func (c *SysUpgradeController) downloadFile(ctx context.Context, url string, sha256sum string, dest string) error {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return errors.WithStack(err)
}
res, err := c.client.Do(req)
if err != nil {
return errors.WithStack(err)
}
defer func() {
if err := res.Body.Close(); err != nil && !errors.Is(err, os.ErrClosed) {
panic(errors.WithStack(err))
}
}()
tmp, err := os.CreateTemp(filepath.Dir(dest), "download_")
if err != nil {
return errors.WithStack(err)
}
defer func() {
if err := os.Remove(tmp.Name()); err != nil && !os.IsNotExist(err) {
panic(errors.WithStack(err))
}
}()
if _, err := io.Copy(tmp, res.Body); err != nil {
return errors.WithStack(err)
}
tmpFileHash, err := hash(tmp.Name())
if err != nil {
return errors.WithStack(err)
}
if tmpFileHash != sha256sum {
return errors.Errorf("sha256 sum mismatch: expected '%s', got '%s'", sha256sum, tmpFileHash)
}
if err := os.Rename(tmp.Name(), dest); err != nil {
return errors.WithStack(err)
}
return nil
}
func (c *SysUpgradeController) upgradeFirmware(ctx context.Context, firmwareFile string) error {
templatizedArgs := make([]string, len(c.args))
for i, a := range c.args {
templatizedArgs[i] = strings.Replace(a, FirmwareFileTemplate, firmwareFile, 1)
}
command := exec.CommandContext(ctx, c.command, templatizedArgs...)
command.Stdout = os.Stdout
command.Stderr = os.Stderr
logger.Debug(ctx, "executing command", logger.F("command", c.command), logger.F("args", templatizedArgs))
if err := command.Run(); err != nil {
return errors.WithStack(err)
}
return nil
}
func NewSysUpgradeController(funcs ...SysUpgradeOptionFunc) *SysUpgradeController {
opts := defaultSysUpgradeOptions()
for _, fn := range funcs {
fn(opts)
}
return &SysUpgradeController{
command: opts.Command,
args: opts.Args,
client: opts.Client,
firmwareVersion: opts.FirmwareVersion,
}
}
var _ agent.Controller = &SysUpgradeController{}

View File

@ -0,0 +1,46 @@
package openwrt
import (
"bytes"
"context"
"os"
"os/exec"
"strings"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
type ShellFirmwareVersion struct {
command string
args []string
}
// FirmwareVersion implements FirmwareVersion
func (fv *ShellFirmwareVersion) FirmwareVersion(ctx context.Context) (string, error) {
command := exec.CommandContext(ctx, fv.command, fv.args...)
var buf bytes.Buffer
command.Stdout = &buf
command.Stderr = os.Stderr
logger.Debug(ctx, "executing command", logger.F("command", fv.command), logger.F("args", fv.args))
if err := command.Run(); err != nil {
return "", errors.WithStack(err)
}
version := strings.TrimSpace(buf.String())
return version, nil
}
func NewShellFirmwareVersion(command string, args ...string) *ShellFirmwareVersion {
return &ShellFirmwareVersion{
command: command,
args: args,
}
}
var _ FirmwareVersion = &ShellFirmwareVersion{}

View File

@ -0,0 +1,58 @@
package openwrt
import (
"context"
"net/http"
"time"
)
const FirmwareFileTemplate = "%FIRMWARE%"
type FirmwareVersion interface {
FirmwareVersion(context.Context) (string, error)
}
type SysUpgradeOptions struct {
Command string
Args []string
FirmwareVersion FirmwareVersion
Client *http.Client
}
func defaultSysUpgradeOptions() *SysUpgradeOptions {
return &SysUpgradeOptions{
Command: `echo`,
Args: []string{`[DUMMY UPGRADE]`, FirmwareFileTemplate},
Client: &http.Client{
Timeout: 30 * time.Second,
},
FirmwareVersion: NewShellFirmwareVersion(`echo`, "0.0.0-dummy"),
}
}
type SysUpgradeOptionFunc func(*SysUpgradeOptions)
func WithSysUpgradeCommand(command string, args ...string) SysUpgradeOptionFunc {
return func(opts *SysUpgradeOptions) {
opts.Command = command
opts.Args = args
}
}
func WithSysUpgradeFirmwareVersion(firmwareVersion FirmwareVersion) SysUpgradeOptionFunc {
return func(opts *SysUpgradeOptions) {
opts.FirmwareVersion = firmwareVersion
}
}
func WithSysUpgradeShellFirmwareVersion(command string, args ...string) SysUpgradeOptionFunc {
return func(opts *SysUpgradeOptions) {
opts.FirmwareVersion = NewShellFirmwareVersion(command, args...)
}
}
func WithSysUpgradeClient(client *http.Client) SysUpgradeOptionFunc {
return func(opts *SysUpgradeOptions) {
opts.Client = client
}
}

View File

@ -2,16 +2,23 @@ package proxy
import (
"context"
"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"
"github.com/mitchellh/hashstructure/v2"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
type proxyEntry struct {
SpecHash uint64
Proxy *ReverseProxy
}
type Controller struct {
proxies map[proxy.ID]*ReverseProxy
currentSpecRevision int
proxies map[proxy.ID]*proxyEntry
}
// Name implements node.Controller.
@ -25,7 +32,7 @@ func (c *Controller) Reconcile(ctx context.Context, state *agent.State) error {
if err := state.GetSpec(proxy.NameProxy, proxySpec); err != nil {
if errors.Is(err, agent.ErrSpecNotFound) {
logger.Info(ctx, "could not find proxy spec, stopping all remaining proxies")
logger.Info(ctx, "could not find proxy spec")
c.stopAllProxies(ctx)
@ -37,25 +44,20 @@ func (c *Controller) Reconcile(ctx context.Context, state *agent.State) error {
logger.Info(ctx, "retrieved spec", logger.F("spec", proxySpec.SpecName()), logger.F("revision", proxySpec.SpecRevision()))
if c.currentSpecRevision == proxySpec.SpecRevision() {
logger.Info(ctx, "spec revision did not change, doing nothing")
return nil
}
c.updateProxies(ctx, proxySpec)
c.currentSpecRevision = proxySpec.SpecRevision()
logger.Info(ctx, "updating current spec revision", logger.F("revision", c.currentSpecRevision))
return nil
}
func (c *Controller) stopAllProxies(ctx context.Context) {
for proxyID, proxy := range c.proxies {
if len(c.proxies) > 0 {
logger.Info(ctx, "stopping all proxies")
}
for proxyID, entry := range c.proxies {
logger.Info(ctx, "stopping proxy", logger.F("proxyID", proxyID))
if err := proxy.Stop(); err != nil {
if err := entry.Proxy.Stop(); err != nil {
logger.Error(
ctx, "error while stopping proxy",
logger.F("proxyID", proxyID),
@ -69,14 +71,14 @@ func (c *Controller) stopAllProxies(ctx context.Context) {
func (c *Controller) updateProxies(ctx context.Context, spec *proxy.Spec) {
// Stop and remove obsolete proxys
for proxyID, proxy := range c.proxies {
for proxyID, entry := range c.proxies {
if _, exists := spec.Proxies[proxyID]; exists {
continue
}
logger.Info(ctx, "stopping proxy", logger.F("proxyID", proxyID))
if err := proxy.Stop(); err != nil {
if err := entry.Proxy.Stop(); err != nil {
logger.Error(
ctx, "error while stopping proxy",
logger.F("proxyID", proxyID),
@ -89,35 +91,89 @@ func (c *Controller) updateProxies(ctx context.Context, spec *proxy.Spec) {
// (Re)start proxys
for proxyID, proxySpec := range spec.Proxies {
proxy, exists := c.proxies[proxyID]
if !exists {
proxy = NewReverseProxy()
c.proxies[proxyID] = proxy
}
proxyCtx := logger.With(ctx, logger.F("proxyID", proxyID))
logger.Info(
ctx, "starting proxy",
logger.F("proxyID", proxyID),
logger.F("addr", proxySpec.Address),
logger.F("target", proxySpec.Target),
)
if err := proxy.Start(ctx, proxySpec.Address, proxySpec.Target); err != nil {
logger.Error(
ctx, "error while starting proxy",
logger.F("proxyID", proxyID),
logger.E(errors.WithStack(err)),
)
delete(c.proxies, proxyID)
if err := c.updateProxy(ctx, proxyID, proxySpec); err != nil {
logger.Error(proxyCtx, "could not update proxy", logger.E(errors.WithStack(err)))
continue
}
}
}
func (c *Controller) updateProxy(ctx context.Context, proxyID proxy.ID, proxySpec proxy.ProxyEntry) (err error) {
newProxySpecHash, err := hashstructure.Hash(proxySpec, hashstructure.FormatV2, nil)
if err != nil {
return errors.WithStack(err)
}
var entry *proxyEntry
entry, exists := c.proxies[proxyID]
if !exists {
logger.Info(ctx, "proxy currently not running")
}
if entry == nil {
entry = &proxyEntry{
Proxy: NewReverseProxy(),
SpecHash: 0,
}
c.proxies[proxyID] = entry
}
specChanged := newProxySpecHash != entry.SpecHash
if entry.Proxy.Running() && !specChanged {
return nil
}
if specChanged && entry.SpecHash != 0 {
logger.Info(
ctx, "restarting proxy",
logger.F("address", proxySpec.Address),
)
} else {
logger.Info(
ctx, "starting proxy",
logger.F("address", proxySpec.Address),
)
}
options := make([]edgeProxy.OptionFunc, 0)
allowedHosts := make([]string, len(proxySpec.Mappings))
mappings := make(map[string]*url.URL, len(proxySpec.Mappings))
for _, m := range proxySpec.Mappings {
target, err := url.Parse(m.Target)
if err != nil {
return errors.WithStack(err)
}
mappings[m.HostPattern] = target
allowedHosts = append(allowedHosts, m.HostPattern)
}
options = append(
options,
edgeProxy.WithAllowedHosts(allowedHosts...),
edgeProxy.WithRewriteHosts(mappings),
)
if err := entry.Proxy.Start(ctx, proxySpec.Address, options...); err != nil {
delete(c.proxies, proxyID)
return errors.Wrap(err, "could not start app")
}
entry.SpecHash = newProxySpecHash
return nil
}
func NewController() *Controller {
return &Controller{
proxies: make(map[proxy.ID]*ReverseProxy),
currentSpecRevision: -1,
proxies: make(map[proxy.ID]*proxyEntry),
}
}

View File

@ -3,26 +3,21 @@ package proxy
import (
"context"
"net/http"
"net/http/httputil"
"net/url"
"sync"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
"forge.cadoles.com/arcad/edge/pkg/proxy"
)
type ReverseProxy struct {
addr string
target string
server *http.Server
mutex sync.RWMutex
}
func (p *ReverseProxy) Start(ctx context.Context, addr, target string) error {
alreadyRunning := p.server != nil && target == p.target && addr == p.target
if alreadyRunning {
return nil
}
func (p *ReverseProxy) Start(ctx context.Context, addr string, funcs ...proxy.OptionFunc) error {
if p.server != nil {
if err := p.Stop(); err != nil {
return errors.WithStack(err)
@ -33,33 +28,40 @@ func (p *ReverseProxy) Start(ctx context.Context, addr, target string) error {
Addr: addr,
}
url, err := url.Parse(target)
if err != nil {
return errors.WithStack(err)
}
proxy := httputil.NewSingleHostReverseProxy(url)
proxy := proxy.New(funcs...)
server.Handler = proxy
p.mutex.Lock()
p.server = server
p.addr = addr
p.target = target
p.mutex.Unlock()
go func() {
defer func() {
if err := p.Stop(); err != nil {
logger.Error(ctx, "error while stopping gateway", logger.E(errors.WithStack(err)))
}
}()
if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
logger.Error(ctx, "error while listening", logger.E(errors.WithStack(err)))
}
if err := p.Stop(); err != nil {
logger.Error(ctx, "error while stopping gateway", logger.E(errors.WithStack(err)))
}
}()
return nil
}
func (p *ReverseProxy) Running() bool {
p.mutex.RLock()
defer p.mutex.RUnlock()
return p.server != nil
}
func (p *ReverseProxy) Stop() error {
p.mutex.Lock()
defer p.mutex.Unlock()
if p.server == nil {
return nil
}

View File

@ -10,7 +10,6 @@ import (
"forge.cadoles.com/Cadoles/emissary/internal/command/common"
"forge.cadoles.com/Cadoles/emissary/internal/openwrt/uci"
"github.com/pkg/errors"
_ "github.com/santhosh-tekuri/jsonschema/v5/httploader"
"github.com/urfave/cli/v2"
)

View File

@ -66,6 +66,29 @@ func RunCommand() *cli.Command {
))
}
if ctrlConf.SysUpgrade.Enabled {
sysUpgradeArgs := make([]string, 0)
if len(ctrlConf.SysUpgrade.SysUpgradeCommand) > 1 {
sysUpgradeArgs = ctrlConf.SysUpgrade.SysUpgradeCommand[1:]
}
firmwareVersionArgs := make([]string, 0)
if len(ctrlConf.SysUpgrade.FirmwareVersionCommand) > 1 {
firmwareVersionArgs = ctrlConf.SysUpgrade.FirmwareVersionCommand[1:]
}
controllers = append(controllers, openwrt.NewSysUpgradeController(
openwrt.WithSysUpgradeCommand(
ctrlConf.SysUpgrade.SysUpgradeCommand[0],
sysUpgradeArgs...,
),
openwrt.WithSysUpgradeShellFirmwareVersion(
ctrlConf.SysUpgrade.FirmwareVersionCommand[0],
firmwareVersionArgs...,
),
))
}
key, err := jwk.LoadOrGenerate(string(conf.Agent.PrivateKeyPath), jwk.DefaultKeySize)
if err != nil {
return errors.WithStack(err)

View File

@ -13,11 +13,6 @@ import (
jsonpatch "github.com/evanphx/json-patch/v5"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
// Import specs
_ "forge.cadoles.com/Cadoles/emissary/internal/spec/app"
_ "forge.cadoles.com/Cadoles/emissary/internal/spec/proxy"
_ "forge.cadoles.com/Cadoles/emissary/internal/spec/uci"
)
func UpdateCommand() *cli.Command {

View File

@ -3,10 +3,6 @@ package api
import (
"forge.cadoles.com/Cadoles/emissary/internal/command/api/agent"
"github.com/urfave/cli/v2"
// Output format
_ "forge.cadoles.com/Cadoles/emissary/internal/format/json"
_ "forge.cadoles.com/Cadoles/emissary/internal/format/table"
)
func Root() *cli.Command {

View File

@ -6,7 +6,6 @@ import (
"forge.cadoles.com/Cadoles/emissary/internal/command/common"
"forge.cadoles.com/Cadoles/emissary/internal/config"
"github.com/pkg/errors"
_ "github.com/santhosh-tekuri/jsonschema/v5/httploader"
"github.com/urfave/cli/v2"
"gitlab.com/wpetit/goweb/logger"
)

View File

@ -9,10 +9,6 @@ import (
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
// Spec validation
_ "forge.cadoles.com/Cadoles/emissary/internal/spec/proxy"
_ "forge.cadoles.com/Cadoles/emissary/internal/spec/uci"
)
func Main(buildDate, projectVersion, gitRef, defaultConfigPath string, commands ...*cli.Command) {

View File

@ -7,7 +7,6 @@ import (
"forge.cadoles.com/Cadoles/emissary/internal/command/common"
"forge.cadoles.com/Cadoles/emissary/internal/server"
"github.com/pkg/errors"
_ "github.com/santhosh-tekuri/jsonschema/v5/httploader"
"github.com/urfave/cli/v2"
"gitlab.com/wpetit/goweb/logger"
)

View File

@ -1,5 +1,7 @@
package config
import "forge.cadoles.com/Cadoles/emissary/internal/agent/controller/openwrt"
type AgentConfig struct {
ServerURL InterpolatedString `yaml:"serverUrl"`
PrivateKeyPath InterpolatedString `yaml:"privateKeyPath"`
@ -20,6 +22,7 @@ type ControllersConfig struct {
Proxy ProxyControllerConfig `yaml:"proxy"`
UCI UCIControllerConfig `yaml:"uci"`
App AppControllerConfig `yaml:"app"`
SysUpgrade SysUpgradeControllerConfig `yaml:"sysupgrade"`
}
type PersistenceControllerConfig struct {
@ -46,6 +49,12 @@ type AppControllerConfig struct {
DownloadDir InterpolatedString `yaml:"downloadDir"`
}
type SysUpgradeControllerConfig struct {
Enabled InterpolatedBool `yaml:"enabled"`
SysUpgradeCommand InterpolatedStringSlice `yaml:"sysupgradeCommand"`
FirmwareVersionCommand InterpolatedStringSlice `yaml:"firmwareVersionCommand"`
}
func NewDefaultAgentConfig() AgentConfig {
return AgentConfig{
ServerURL: "http://127.0.0.1:3000",
@ -72,6 +81,11 @@ func NewDefaultAgentConfig() AgentConfig {
DataDir: "apps/data",
DownloadDir: "apps/bundles",
},
SysUpgrade: SysUpgradeControllerConfig{
Enabled: true,
SysUpgradeCommand: InterpolatedStringSlice{"sysupgrade", "--force", "-u", "-v", openwrt.FirmwareFileTemplate},
FirmwareVersionCommand: InterpolatedStringSlice{"sh", "-c", `source /etc/openwrt_release && echo "$DISTRIB_ID-$DISTRIB_RELEASE-$DISTRIB_REVISION"`},
},
},
Collectors: []ShellCollectorConfig{
{

View File

@ -0,0 +1,6 @@
package format
import (
_ "forge.cadoles.com/Cadoles/emissary/internal/format/json"
_ "forge.cadoles.com/Cadoles/emissary/internal/format/table"
)

View File

@ -0,0 +1,6 @@
package passwd
import (
_ "forge.cadoles.com/arcad/edge/pkg/module/auth/http/passwd/argon2id"
_ "forge.cadoles.com/arcad/edge/pkg/module/auth/http/passwd/plain"
)

View File

@ -0,0 +1,8 @@
package spec
import (
_ "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"
)

View File

@ -0,0 +1,6 @@
package sql
import (
_ "github.com/jackc/pgx/v5/stdlib"
_ "modernc.org/sqlite"
)

View File

@ -10,11 +10,6 @@ import (
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/api"
"gitlab.com/wpetit/goweb/logger"
// Import specs
_ "forge.cadoles.com/Cadoles/emissary/internal/spec/app"
_ "forge.cadoles.com/Cadoles/emissary/internal/spec/proxy"
_ "forge.cadoles.com/Cadoles/emissary/internal/spec/uci"
)
const (

View File

@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://app.emissary.cadoles.com/spec.json",
"$id": "https://app.edge.emissary.cadoles.com/spec.json",
"title": "AppSpec",
"description": "Emissary 'App' specification",
"type": "object",

View File

@ -14,11 +14,23 @@
"address": {
"type": "string"
},
"target": {
"type": "string"
"mappings": {
"type": "array",
"items": {
"type": "object",
"properties": {
"hostPattern": {
"type": "string"
},
"target": {
"type": "string"
}
},
"required": ["hostPattern", "target"]
}
}
},
"required": ["address", "target"],
"required": ["address", "mappings"],
"additionalProperties": false
}
}

View File

@ -12,8 +12,13 @@ type Spec struct {
}
type ProxyEntry struct {
Address string `json:"address"`
Target string `json:"target"`
Address string `json:"address"`
Mappings []ProxyMapping `json:"mappings"`
}
type ProxyMapping struct {
HostPattern string `json:"hostPattern"`
Target string `json:"target"`
}
func (s *Spec) SpecName() spec.Name {

View File

@ -4,7 +4,12 @@
"proxies": {
"cadoles.com": {
"address": ":3003",
"target": "https://www.cadoles.com",
"mappings": [
{
"hostPattern": "localhost:3003",
"target": "https://www.cadoles.com"
}
],
"foo": "bar"
}
}

View File

@ -4,7 +4,12 @@
"proxies": {
"cadoles.com": {
"address": ":3003",
"target": "https://www.cadoles.com"
"mappings": [
{
"hostPattern": "localhost:3003",
"target": "https://www.cadoles.com"
}
]
}
}
},

View File

@ -1,4 +1,5 @@
**/*.go
**/*.json
modd.conf
tmp/config.yml
.env {