feat: initial commit

This commit is contained in:
2023-02-02 10:55:24 +01:00
commit 088b6843a9
92 changed files with 7300 additions and 0 deletions

View File

@ -0,0 +1,16 @@
package openwrt
import (
"forge.cadoles.com/Cadoles/emissary/internal/command/agent/openwrt/uci"
"github.com/urfave/cli/v2"
)
func Root() *cli.Command {
return &cli.Command{
Name: "openwrt",
Usage: "OpenWRT related commands",
Subcommands: []*cli.Command{
uci.Root(),
},
}
}

View File

@ -0,0 +1,15 @@
package uci
import (
"github.com/urfave/cli/v2"
)
func Root() *cli.Command {
return &cli.Command{
Name: "uci",
Usage: "UCI related commands",
Subcommands: []*cli.Command{
TransformCommand(),
},
}
}

View File

@ -0,0 +1,107 @@
package uci
import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"os"
"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"
)
const (
FormatJSON = "json"
FormatUCI = "uci"
)
func TransformCommand() *cli.Command {
flags := common.Flags()
flags = append(flags,
&cli.StringFlag{
Name: "format",
Aliases: []string{"f"},
Usage: "Define the source format ('uci' or 'json')",
Value: "uci",
},
&cli.StringFlag{
Name: "input",
Usage: "File to use as input (or '-' for STDIN)",
Aliases: []string{"i"},
TakesFile: true,
Value: "-",
},
)
return &cli.Command{
Name: "transform",
Usage: "Transform UCI configuration from/to JSON",
Flags: flags,
Action: func(ctx *cli.Context) error {
input := ctx.String("input")
format := ctx.String("format")
var reader io.Reader
switch input {
case "-":
reader = os.Stdin
default:
file, err := os.Open(input)
if err != nil {
return errors.WithStack(err)
}
defer func() {
if err := file.Close(); err != nil {
panic(errors.WithStack(err))
}
}()
reader = file
}
var conf *uci.UCI
switch format {
case FormatJSON:
decoder := json.NewDecoder(reader)
conf = uci.NewUCI()
if err := decoder.Decode(conf); err != nil {
return errors.WithStack(err)
}
fmt.Print(conf.Export())
case FormatUCI:
data, err := ioutil.ReadAll(reader)
if err != nil {
return errors.WithStack(err)
}
conf, err = uci.Parse(data)
if err != nil {
return errors.WithStack(err)
}
jsonData, err := json.MarshalIndent(conf, "", " ")
if err != nil {
return errors.WithStack(err)
}
fmt.Print(string(jsonData))
default:
return errors.Errorf("unexpected format '%s'", format)
}
return nil
},
}
}

View File

@ -0,0 +1,17 @@
package agent
import (
"forge.cadoles.com/Cadoles/emissary/internal/command/agent/openwrt"
"github.com/urfave/cli/v2"
)
func Root() *cli.Command {
return &cli.Command{
Name: "agent",
Usage: "Agent related commands",
Subcommands: []*cli.Command{
openwrt.Root(),
RunCommand(),
},
}
}

View File

@ -0,0 +1,68 @@
package agent
import (
"time"
"forge.cadoles.com/Cadoles/emissary/internal/agent"
"forge.cadoles.com/Cadoles/emissary/internal/agent/controller/gateway"
"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/spec"
"forge.cadoles.com/Cadoles/emissary/internal/command/common"
"github.com/pkg/errors"
_ "github.com/santhosh-tekuri/jsonschema/v5/httploader"
"github.com/urfave/cli/v2"
"gitlab.com/wpetit/goweb/logger"
)
func RunCommand() *cli.Command {
flags := common.Flags()
return &cli.Command{
Name: "run",
Usage: "Run the emissary agent",
Flags: flags,
Action: func(ctx *cli.Context) error {
conf, err := common.LoadConfig(ctx)
if err != nil {
return errors.Wrap(err, "Could not load configuration")
}
logger.SetFormat(logger.Format(conf.Logger.Format))
logger.SetLevel(logger.Level(conf.Logger.Level))
controllers := make([]agent.Controller, 0)
ctrlConf := conf.Agent.Controllers
if ctrlConf.Persistence.Enabled {
controllers = append(controllers, persistence.NewController(string(ctrlConf.Persistence.StateFile)))
}
if ctrlConf.Spec.Enabled {
controllers = append(controllers, spec.NewController(string(ctrlConf.Spec.ServerURL)))
}
if ctrlConf.Gateway.Enabled {
controllers = append(controllers, gateway.NewController())
}
if ctrlConf.UCI.Enabled {
controllers = append(controllers, openwrt.NewUCIController(
string(ctrlConf.UCI.BinPath),
))
}
agent := agent.New(
agent.WithInterval(time.Duration(conf.Agent.ReconciliationInterval)*time.Second),
agent.WithControllers(controllers...),
)
if err := agent.Run(ctx.Context); err != nil {
return errors.WithStack(err)
}
return nil
},
}
}

View File

@ -0,0 +1,7 @@
package common
import "github.com/urfave/cli/v2"
func Flags() []cli.Flag {
return []cli.Flag{}
}

View File

@ -0,0 +1,27 @@
package common
import (
"forge.cadoles.com/Cadoles/emissary/internal/config"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
)
func LoadConfig(ctx *cli.Context) (*config.Config, error) {
configFile := ctx.String("config")
var (
conf *config.Config
err error
)
if configFile != "" {
conf, err = config.NewFromFile(configFile)
if err != nil {
return nil, errors.Wrapf(err, "Could not load config file '%s'", configFile)
}
} else {
conf = config.NewDefault()
}
return conf, nil
}

View File

@ -0,0 +1,37 @@
package config
import (
"os"
"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"
)
func Dump() *cli.Command {
flags := common.Flags()
return &cli.Command{
Name: "dump",
Usage: "Dump the current configuration",
Flags: flags,
Action: func(ctx *cli.Context) error {
conf, err := common.LoadConfig(ctx)
if err != nil {
return errors.Wrap(err, "Could not load configuration")
}
logger.SetFormat(logger.Format(conf.Logger.Format))
logger.SetLevel(logger.Level(conf.Logger.Level))
if err := config.Dump(conf, os.Stdout); err != nil {
return errors.Wrap(err, "Could not dump configuration")
}
return nil
},
}
}

View File

@ -0,0 +1,13 @@
package config
import "github.com/urfave/cli/v2"
func Root() *cli.Command {
return &cli.Command{
Name: "config",
Usage: "Config related commands",
Subcommands: []*cli.Command{
Dump(),
},
}
}

107
internal/command/main.go Normal file
View File

@ -0,0 +1,107 @@
package command
import (
"context"
"fmt"
"os"
"sort"
"time"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
)
func Main(buildDate, projectVersion, gitRef, defaultConfigPath string, commands ...*cli.Command) {
ctx := context.Background()
compiled, err := time.Parse(time.RFC3339, buildDate)
if err != nil {
panic(errors.Wrapf(err, "could not parse build date '%s'", buildDate))
}
app := &cli.App{
Version: fmt.Sprintf("%s (%s, %s)", projectVersion, gitRef, buildDate),
Compiled: compiled,
Name: "emissary",
Usage: "Control plane for edge devices",
Commands: commands,
Before: func(ctx *cli.Context) error {
workdir := ctx.String("workdir")
// Switch to new working directory if defined
if workdir != "" {
if err := os.Chdir(workdir); err != nil {
return errors.Wrap(err, "could not change working directory")
}
}
if err := ctx.Set("projectVersion", projectVersion); err != nil {
return errors.WithStack(err)
}
if err := ctx.Set("gitRef", gitRef); err != nil {
return errors.WithStack(err)
}
if err := ctx.Set("buildDate", buildDate); err != nil {
return errors.WithStack(err)
}
return nil
},
Flags: []cli.Flag{
&cli.StringFlag{
Name: "workdir",
Value: "",
Usage: "The working directory",
},
&cli.StringFlag{
Name: "projectVersion",
Value: "",
Hidden: true,
},
&cli.StringFlag{
Name: "gitRef",
Value: "",
Hidden: true,
},
&cli.StringFlag{
Name: "buildDate",
Value: "",
Hidden: true,
},
&cli.BoolFlag{
Name: "debug",
EnvVars: []string{"EMISSARY_DEBUG"},
Value: false,
},
&cli.StringFlag{
Name: "config",
Aliases: []string{"c"},
EnvVars: []string{"EMISSARY_CONFIG"},
Value: defaultConfigPath,
TakesFile: true,
},
},
}
app.ExitErrHandler = func(ctx *cli.Context, err error) {
if err == nil {
return
}
debug := ctx.Bool("debug")
if !debug {
fmt.Printf("[ERROR] %v\n", err)
} else {
fmt.Printf("%+v", err)
}
}
sort.Sort(cli.FlagsByName(app.Flags))
sort.Sort(cli.CommandsByName(app.Commands))
if err := app.RunContext(ctx, os.Args); err != nil {
os.Exit(1)
}
}

View File

@ -0,0 +1,105 @@
package database
import (
"forge.cadoles.com/Cadoles/emissary/internal/command/common"
"forge.cadoles.com/Cadoles/emissary/internal/migrate"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
"gitlab.com/wpetit/goweb/logger"
)
const (
MigrateVersionUp = "up"
MigrateVersionLatest = "latest"
MigrateVersionDown = "down"
)
func MigrateCommand() *cli.Command {
return &cli.Command{
Name: "migrate",
Usage: "Migrate database schema to latest version",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "target",
Usage: "Migration target, default to latest",
Value: "latest",
},
&cli.IntFlag{
Name: "force",
Usage: "Force migration to version",
Value: -1,
},
},
Action: func(ctx *cli.Context) error {
conf, err := common.LoadConfig(ctx)
if err != nil {
return errors.Wrap(err, "Could not load configuration")
}
driver := string(conf.Database.Driver)
dsn := string(conf.Database.DSN)
migr, err := migrate.New("migrations", driver, dsn)
if err != nil {
return errors.WithStack(err)
}
version, dirty, err := migr.Version()
if err != nil && !errors.Is(err, migrate.ErrNilVersion) {
return errors.WithStack(err)
}
logger.Info(
ctx.Context, "current database shema",
logger.F("version", version),
logger.F("dirty", dirty),
)
target := ctx.String("target")
force := ctx.Int("force")
if force != -1 {
logger.Info(ctx.Context, "forcing database schema version", logger.F("version", force))
if err := migr.Force(force); err != nil {
return errors.WithStack(err)
}
return nil
}
switch target {
case "":
fallthrough
case MigrateVersionLatest:
err = migr.Up()
case MigrateVersionDown:
err = migr.Steps(-1)
case MigrateVersionUp:
err = migr.Steps(1)
default:
return errors.Errorf(
"unknown migration target: '%s', available: '%s' (default), '%s' or '%s'",
target, MigrateVersionLatest, MigrateVersionUp, MigrateVersionDown,
)
}
if err != nil && !errors.Is(err, migrate.ErrNoChange) {
return errors.Wrap(err, "could not apply migration")
}
version, dirty, err = migr.Version()
if err != nil && !errors.Is(err, migrate.ErrNilVersion) {
return errors.WithStack(err)
}
logger.Info(
ctx.Context, "database shema after migration",
logger.F("version", version),
logger.F("dirty", dirty),
)
return nil
},
}
}

View File

@ -0,0 +1,48 @@
package database
import (
"database/sql"
"forge.cadoles.com/Cadoles/emissary/internal/command/common"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
"gitlab.com/wpetit/goweb/logger"
)
func PingCommand() *cli.Command {
return &cli.Command{
Name: "ping",
Usage: "Test database connectivity",
Action: func(ctx *cli.Context) error {
conf, err := common.LoadConfig(ctx)
if err != nil {
return errors.Wrap(err, "Could not load configuration")
}
logger.Info(ctx.Context, "connecting to database", logger.F("dsn", conf.Database.DSN))
driver := string(conf.Database.Driver)
dsn := string(conf.Database.DSN)
db, err := sql.Open(driver, dsn)
if err != nil {
return errors.WithStack(err)
}
defer func() {
if err := db.Close(); err != nil {
logger.Error(ctx.Context, "error while closing database connection", logger.E(errors.WithStack(err)))
}
}()
if err := db.PingContext(ctx.Context); err != nil {
return errors.WithStack(err)
}
logger.Info(ctx.Context, "connection succeeded", logger.F("dsn", conf.Database.DSN))
return nil
},
}
}

View File

@ -0,0 +1,38 @@
package database
import (
"forge.cadoles.com/Cadoles/emissary/internal/command/common"
"forge.cadoles.com/Cadoles/emissary/internal/migrate"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
"gitlab.com/wpetit/goweb/logger"
)
func ResetCommand() *cli.Command {
return &cli.Command{
Name: "reset",
Usage: "Reset database",
Action: func(ctx *cli.Context) error {
conf, err := common.LoadConfig(ctx)
if err != nil {
return errors.Wrap(err, "Could not load configuration")
}
driver := string(conf.Database.Driver)
dsn := string(conf.Database.DSN)
migr, err := migrate.New("migrations", driver, dsn)
if err != nil {
return errors.WithStack(err)
}
if err := migr.Drop(); err != nil {
return errors.Wrap(err, "could not drop tables")
}
logger.Info(ctx.Context, "database schema reinitialized")
return nil
},
}
}

View File

@ -0,0 +1,15 @@
package database
import "github.com/urfave/cli/v2"
func Root() *cli.Command {
return &cli.Command{
Name: "database",
Usage: "Database related commands",
Subcommands: []*cli.Command{
MigrateCommand(),
PingCommand(),
ResetCommand(),
},
}
}

View File

@ -0,0 +1,19 @@
package server
import (
"forge.cadoles.com/Cadoles/emissary/internal/command/config"
"forge.cadoles.com/Cadoles/emissary/internal/command/server/database"
"github.com/urfave/cli/v2"
)
func Root() *cli.Command {
return &cli.Command{
Name: "server",
Usage: "Server related commands",
Subcommands: []*cli.Command{
RunCommand(),
database.Root(),
config.Root(),
},
}
}

View File

@ -0,0 +1,54 @@
package server
import (
"fmt"
"strings"
"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"
)
func RunCommand() *cli.Command {
flags := common.Flags()
return &cli.Command{
Name: "run",
Usage: "Run the emissary server",
Flags: flags,
Action: func(ctx *cli.Context) error {
conf, err := common.LoadConfig(ctx)
if err != nil {
return errors.Wrap(err, "Could not load configuration")
}
logger.SetFormat(logger.Format(conf.Logger.Format))
logger.SetLevel(logger.Level(conf.Logger.Level))
srv := server.New(
server.WithConfig(conf),
)
addrs, srvErrs := srv.Start(ctx.Context)
select {
case addr := <-addrs:
url := fmt.Sprintf("http://%s", addr.String())
url = strings.Replace(url, "0.0.0.0", "127.0.0.1", 1)
logger.Info(ctx.Context, "listening", logger.F("url", url))
case err = <-srvErrs:
return errors.WithStack(err)
}
if err = <-srvErrs; err != nil {
return errors.WithStack(err)
}
return nil
},
}
}