Merge pull request #358 from openziti/v0.4.0

v0.4.0
This commit is contained in:
Michael Quigley 2023-06-27 13:55:05 -04:00 committed by GitHub
commit 309b146fd8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
288 changed files with 17252 additions and 2431 deletions

View File

@ -13,6 +13,7 @@ jobs:
build-linux-amd64: build-linux-amd64:
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
steps: steps:
- run: sudo apt update
- run: sudo apt-get install gcc-multilib g++-multilib - run: sudo apt-get install gcc-multilib g++-multilib
- uses: actions/checkout@v3 - uses: actions/checkout@v3
@ -54,6 +55,7 @@ jobs:
build-linux-arm64: build-linux-arm64:
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
steps: steps:
- run: sudo apt update
- run: sudo apt-get install gcc-aarch64-linux-gnu - run: sudo apt-get install gcc-aarch64-linux-gnu
- uses: actions/checkout@v3 - uses: actions/checkout@v3
@ -95,6 +97,7 @@ jobs:
build-linux-arm: build-linux-arm:
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
steps: steps:
- run: sudo apt update
- run: sudo apt-get install gcc-arm-linux-gnueabi - run: sudo apt-get install gcc-arm-linux-gnueabi
- uses: actions/checkout@v3 - uses: actions/checkout@v3
@ -187,6 +190,7 @@ jobs:
build-windows: build-windows:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- run: sudo apt update
- run: sudo apt-get install gcc-mingw-w64-x86-64 - run: sudo apt-get install gcc-mingw-w64-x86-64
- uses: actions/checkout@v3 - uses: actions/checkout@v3

View File

@ -1,3 +1,27 @@
# v0.4.0
FEATURE: New `tcpTunnel` backend mode allowing for private sharing of local TCP sockets with other `zrok` users (https://github.com/openziti/zrok/issues/170)
FEATURE: New `udpTunnel` backend mode allowing for private sharing of local UDP sockets with other `zrok` users (https://github.com/openziti/zrok/issues/306)
FEATURE: New metrics infrastructure based on OpenZiti usage events (https://github.com/openziti/zrok/issues/128). See the [v0.4 Metrics Guide](docs/guides/metrics-and-limits/configuring-metrics.md) for more information.
FEATURE: New limits implementation based on the new metrics infrastructure (https://github.com/openziti/zrok/issues/235). See the [v0.4 Limits Guide](docs/guides/metrics-and-limits/configuring-limits.md) for more information.
FEATURE: The invite mechanism has been reworked to improve user experience. The configuration has been updated to include a new `invite` stanza, and now includes a boolean flag indicating whether or not the instance allows new invitations to be created, and also includes contact details for requesting a new invite. These values are used by the `zrok invite` command to provide a smoother end-user invite experience https://github.com/openziti/zrok/issues/229)
FEATURE: New password strength checking rules and configuration. See the example configuration file (`etc/ctrl.yml`) for details about how to configure the strength checking rules (https://github.com/openziti/zrok/issues/167)
FEATURE: A new `admin/profile_endpoint` configuration option is available to start a `net/http/pprof` listener. See `etc/ctrl.yml` for details.
CHANGE: The controller configuration version bumps from `v: 2` to `v: 3` to support all of the new `v0.4` functionality. See the [example ctrl.yml](etc/ctrl.yml) for details on the new configuration.
CHANGE: The underlying database store now utilizes a `deleted` flag on all tables to implement "soft deletes". This was necessary for the new metrics infrastructure, where we need to account for metrics data that arrived after the lifetime of a share or environment; and also we're going to need this for limits, where we need to see historical information about activity in the past (https://github.com/openziti/zrok/issues/262)
CHANGE: Updated to latest `github.com/openziti/sdk-golang` (https://github.com/openziti/zrok/issues/335)
FIX: `zrok share reserved --override-endpoint` now works correctly; `--override-endpoint` was being incorrectly ignore previously (https://github.com/openziti/zrok/pull/348)
# v0.3.7 # v0.3.7
FIX: Improved TUI word-wrapping (https://github.com/openziti/zrok/issues/180) FIX: Improved TUI word-wrapping (https://github.com/openziti/zrok/issues/180)
@ -46,7 +70,7 @@ CHANGE: Incorporate initial docker image build (https://github.com/openziti/zrok
CHANGE: Improve target URL parsing for `zrok share` when using `--backend-mode` proxy (https://github.com/openziti/zrok/issues/211) CHANGE: Improve target URL parsing for `zrok share` when using `--backend-mode` proxy (https://github.com/openziti/zrok/issues/211)
New and improved URL handling for proxy backends: New and improved URL handling for proxy backends:
9090 -> http://127.0.0.1:9090 9090 -> http://127.0.0.1:9090
localhost:9090 -> http://127.0.0.1:9090 localhost:9090 -> http://127.0.0.1:9090
https://localhost:9090 -> https://localhost:9090 https://localhost:9090 -> https://localhost:9090

View File

@ -36,7 +36,7 @@ See the [Concepts and Getting Started Guide](docs/getting-started.md) for a full
The single `zrok` binary contains everything you need to operate `zrok` environments and also host your own service instances. Just add an OpenZiti network and you're up and running. The single `zrok` binary contains everything you need to operate `zrok` environments and also host your own service instances. Just add an OpenZiti network and you're up and running.
See the [Self-Hosting Guide](docs/guides/v0.3_self_hosting_guide.md) for details on getting your own `zrok` service instance running. This builds on top of the [OpenZiti Quick Start](https://docs.openziti.io/docs/learn/quickstarts/network/) to have a running `zrok` service instance in minutes. See the [Self-Hosting Guide](docs/guides/self_hosting_guide.md) for details on getting your own `zrok` service instance running. This builds on top of the [OpenZiti Quick Start](https://docs.openziti.io/docs/learn/quickstarts/network/) to have a running `zrok` service instance in minutes.
## Building ## Building

View File

@ -5,7 +5,7 @@ import "fmt"
var Version string var Version string
var Hash string var Hash string
const Series = "v0.3" const Series = "v0.4"
func String() string { func String() string {
if Version != "" { if Version != "" {

View File

@ -5,7 +5,9 @@ import (
"github.com/go-openapi/runtime" "github.com/go-openapi/runtime"
httptransport "github.com/go-openapi/runtime/client" httptransport "github.com/go-openapi/runtime/client"
"github.com/openziti/zrok/endpoints" "github.com/openziti/zrok/endpoints"
"github.com/openziti/zrok/endpoints/privateFrontend" "github.com/openziti/zrok/endpoints/proxy"
"github.com/openziti/zrok/endpoints/tcpTunnel"
"github.com/openziti/zrok/endpoints/udpTunnel"
"github.com/openziti/zrok/rest_client_zrok" "github.com/openziti/zrok/rest_client_zrok"
"github.com/openziti/zrok/rest_client_zrok/share" "github.com/openziti/zrok/rest_client_zrok/share"
"github.com/openziti/zrok/rest_model_zrok" "github.com/openziti/zrok/rest_model_zrok"
@ -17,6 +19,7 @@ import (
"os" "os"
"os/signal" "os/signal"
"syscall" "syscall"
"time"
) )
func init() { func init() {
@ -45,14 +48,6 @@ func newAccessPrivateCommand() *accessPrivateCommand {
func (cmd *accessPrivateCommand) run(_ *cobra.Command, args []string) { func (cmd *accessPrivateCommand) run(_ *cobra.Command, args []string) {
shrToken := args[0] shrToken := args[0]
endpointUrl, err := url.Parse("http://" + cmd.bindAddress)
if err != nil {
if !panicInstead {
tui.Error("invalid endpoint address", err)
}
panic(err)
}
zrd, err := zrokdir.Load() zrd, err := zrokdir.Load()
if err != nil { if err != nil {
tui.Error("unable to load zrokdir", err) tui.Error("unable to load zrokdir", err)
@ -85,10 +80,89 @@ func (cmd *accessPrivateCommand) run(_ *cobra.Command, args []string) {
} }
logrus.Infof("allocated frontend '%v'", accessResp.Payload.FrontendToken) logrus.Infof("allocated frontend '%v'", accessResp.Payload.FrontendToken)
cfg := privateFrontend.DefaultConfig("backend") protocol := "http://"
cfg.ShrToken = shrToken switch accessResp.Payload.BackendMode {
cfg.Address = cmd.bindAddress case "tcpTunnel":
cfg.RequestsChan = make(chan *endpoints.Request, 1024) protocol = "tcp://"
case "udpTunnel":
protocol = "udp://"
}
endpointUrl, err := url.Parse(protocol + cmd.bindAddress)
if err != nil {
if !panicInstead {
tui.Error("invalid endpoint address", err)
}
panic(err)
}
requests := make(chan *endpoints.Request, 1024)
switch accessResp.Payload.BackendMode {
case "tcpTunnel":
fe, err := tcpTunnel.NewFrontend(&tcpTunnel.FrontendConfig{
BindAddress: cmd.bindAddress,
IdentityName: "backend",
ShrToken: args[0],
RequestsChan: requests,
})
if err != nil {
if !panicInstead {
tui.Error("unable to create private frontend", err)
}
panic(err)
}
go func() {
if err := fe.Run(); err != nil {
if !panicInstead {
tui.Error("error starting frontend", err)
}
panic(err)
}
}()
case "udpTunnel":
fe, err := udpTunnel.NewFrontend(&udpTunnel.FrontendConfig{
BindAddress: cmd.bindAddress,
IdentityName: "backend",
ShrToken: args[0],
RequestsChan: requests,
IdleTime: time.Minute,
})
if err != nil {
if !panicInstead {
tui.Error("unable to create private frontend", err)
}
panic(err)
}
go func() {
if err := fe.Run(); err != nil {
if !panicInstead {
tui.Error("error starting frontend", err)
}
panic(err)
}
}()
default:
cfg := proxy.DefaultFrontendConfig("backend")
cfg.ShrToken = shrToken
cfg.Address = cmd.bindAddress
cfg.RequestsChan = requests
fe, err := proxy.NewFrontend(cfg)
if err != nil {
if !panicInstead {
tui.Error("unable to create private frontend", err)
}
panic(err)
}
go func() {
if err := fe.Run(); err != nil {
if !panicInstead {
tui.Error("unable to run frontend", err)
}
}
}()
}
c := make(chan os.Signal) c := make(chan os.Signal)
signal.Notify(c, os.Interrupt, syscall.SIGTERM) signal.Notify(c, os.Interrupt, syscall.SIGTERM)
@ -98,27 +172,11 @@ func (cmd *accessPrivateCommand) run(_ *cobra.Command, args []string) {
os.Exit(0) os.Exit(0)
}() }()
frontend, err := privateFrontend.NewHTTP(cfg)
if err != nil {
if !panicInstead {
tui.Error("unable to create private frontend", err)
}
panic(err)
}
go func() {
if err := frontend.Run(); err != nil {
if !panicInstead {
tui.Error("unable to run frontend", err)
}
}
}()
if cmd.headless { if cmd.headless {
logrus.Infof("access the zrok share at the followind endpoint: %v", endpointUrl.String()) logrus.Infof("access the zrok share at the followind endpoint: %v", endpointUrl.String())
for { for {
select { select {
case req := <-cfg.RequestsChan: case req := <-requests:
logrus.Infof("%v -> %v %v", req.RemoteAddr, req.Method, req.Path) logrus.Infof("%v -> %v %v", req.RemoteAddr, req.Method, req.Path)
} }
} }
@ -132,7 +190,7 @@ func (cmd *accessPrivateCommand) run(_ *cobra.Command, args []string) {
go func() { go func() {
for { for {
select { select {
case req := <-cfg.RequestsChan: case req := <-requests:
if req != nil { if req != nil {
prg.Send(req) prg.Send(req)
} }
@ -144,17 +202,16 @@ func (cmd *accessPrivateCommand) run(_ *cobra.Command, args []string) {
tui.Error("An error occurred", err) tui.Error("An error occurred", err)
} }
close(cfg.RequestsChan) close(requests)
cmd.destroy(accessResp.Payload.FrontendToken, zrd.Env.ZId, shrToken, zrok, auth) cmd.destroy(accessResp.Payload.FrontendToken, zrd.Env.ZId, shrToken, zrok, auth)
} }
} }
func (cmd *accessPrivateCommand) destroy(frotendName, envZId, shrToken string, zrok *rest_client_zrok.Zrok, auth runtime.ClientAuthInfoWriter) { func (cmd *accessPrivateCommand) destroy(frontendName, envZId, shrToken string, zrok *rest_client_zrok.Zrok, auth runtime.ClientAuthInfoWriter) {
logrus.Debugf("shutting down '%v'", shrToken) logrus.Debugf("shutting down '%v'", shrToken)
req := share.NewUnaccessParams() req := share.NewUnaccessParams()
req.Body = &rest_model_zrok.UnaccessRequest{ req.Body = &rest_model_zrok.UnaccessRequest{
FrontendToken: frotendName, FrontendToken: frontendName,
ShrToken: shrToken, ShrToken: shrToken,
EnvZID: envZId, EnvZID: envZId,
} }

View File

@ -3,7 +3,7 @@ package main
import ( import (
"fmt" "fmt"
"github.com/michaelquigley/cf" "github.com/michaelquigley/cf"
"github.com/openziti/zrok/endpoints/publicFrontend" "github.com/openziti/zrok/endpoints/publicProxy"
"github.com/openziti/zrok/tui" "github.com/openziti/zrok/tui"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -22,10 +22,9 @@ type accessPublicCommand struct {
func newAccessPublicCommand() *accessPublicCommand { func newAccessPublicCommand() *accessPublicCommand {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "public [<configPath>]", Use: "public [<configPath>]",
Aliases: []string{"fe"}, Short: "Create a public access HTTP frontend",
Short: "Create a public access HTTP frontend", Args: cobra.RangeArgs(0, 1),
Args: cobra.RangeArgs(0, 1),
} }
command := &accessPublicCommand{cmd: cmd} command := &accessPublicCommand{cmd: cmd}
cmd.Run = command.run cmd.Run = command.run
@ -33,7 +32,7 @@ func newAccessPublicCommand() *accessPublicCommand {
} }
func (cmd *accessPublicCommand) run(_ *cobra.Command, args []string) { func (cmd *accessPublicCommand) run(_ *cobra.Command, args []string) {
cfg := publicFrontend.DefaultConfig() cfg := publicProxy.DefaultConfig()
if len(args) == 1 { if len(args) == 1 {
if err := cfg.Load(args[0]); err != nil { if err := cfg.Load(args[0]); err != nil {
if !panicInstead { if !panicInstead {
@ -43,7 +42,7 @@ func (cmd *accessPublicCommand) run(_ *cobra.Command, args []string) {
} }
} }
logrus.Infof(cf.Dump(cfg, cf.DefaultOptions())) logrus.Infof(cf.Dump(cfg, cf.DefaultOptions()))
frontend, err := publicFrontend.NewHTTP(cfg) frontend, err := publicProxy.NewHTTP(cfg)
if err != nil { if err != nil {
if !panicInstead { if !panicInstead {
tui.Error("unable to create http frontend", err) tui.Error("unable to create http frontend", err)

View File

@ -3,7 +3,7 @@ package main
import ( import (
"fmt" "fmt"
"github.com/michaelquigley/cf" "github.com/michaelquigley/cf"
"github.com/openziti/zrok/endpoints/publicFrontend" "github.com/openziti/zrok/endpoints/publicProxy"
"github.com/openziti/zrok/tui" "github.com/openziti/zrok/tui"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -29,7 +29,7 @@ func newAccessPublicValidateCommand() *accessPublicValidateCommand {
} }
func (cmd *accessPublicValidateCommand) run(_ *cobra.Command, args []string) { func (cmd *accessPublicValidateCommand) run(_ *cobra.Command, args []string) {
cfg := publicFrontend.DefaultConfig() cfg := publicProxy.DefaultConfig()
if err := cfg.Load(args[0]); err != nil { if err := cfg.Load(args[0]); err != nil {
tui.Error(fmt.Sprintf("unable to load configuration '%v'", args[0]), err) tui.Error(fmt.Sprintf("unable to load configuration '%v'", args[0]), err)
} }

View File

@ -3,6 +3,7 @@ package main
import ( import (
"github.com/michaelquigley/cf" "github.com/michaelquigley/cf"
"github.com/openziti/zrok/controller" "github.com/openziti/zrok/controller"
"github.com/openziti/zrok/controller/config"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -26,13 +27,13 @@ func newAdminBootstrap() *adminBootstrap {
command := &adminBootstrap{cmd: cmd} command := &adminBootstrap{cmd: cmd}
cmd.Run = command.run cmd.Run = command.run
cmd.Flags().BoolVar(&command.skipCtrl, "skip-ctrl", false, "Skip controller (ctrl) identity bootstrapping") cmd.Flags().BoolVar(&command.skipCtrl, "skip-ctrl", false, "Skip controller (ctrl) identity bootstrapping")
cmd.Flags().BoolVar(&command.skipFrontend, "skip-frontend", false, "Slip frontend identity bootstrapping") cmd.Flags().BoolVar(&command.skipFrontend, "skip-frontend", false, "Skip frontend identity bootstrapping")
return command return command
} }
func (cmd *adminBootstrap) run(_ *cobra.Command, args []string) { func (cmd *adminBootstrap) run(_ *cobra.Command, args []string) {
configPath := args[0] configPath := args[0]
inCfg, err := controller.LoadConfig(configPath) inCfg, err := config.LoadConfig(configPath)
if err != nil { if err != nil {
panic(err) panic(err)
} }

View File

@ -21,10 +21,9 @@ type adminCreateFrontendCommand struct {
func newAdminCreateFrontendCommand() *adminCreateFrontendCommand { func newAdminCreateFrontendCommand() *adminCreateFrontendCommand {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "frontend <zitiId> <publicName> <urlTemplate>", Use: "frontend <zitiId> <publicName> <urlTemplate>",
Aliases: []string{"fe"}, Short: "Create a global public frontend",
Short: "Create a global public frontend", Args: cobra.ExactArgs(3),
Args: cobra.ExactArgs(3),
} }
command := &adminCreateFrontendCommand{cmd: cmd} command := &adminCreateFrontendCommand{cmd: cmd}
cmd.Run = command.run cmd.Run = command.run

View File

@ -18,10 +18,9 @@ type adminDeleteFrontendCommand struct {
func newAdminDeleteFrontendCommand() *adminDeleteFrontendCommand { func newAdminDeleteFrontendCommand() *adminDeleteFrontendCommand {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "frontend <frontendToken>", Use: "frontend <frontendToken>",
Aliases: []string{"fe"}, Short: "Delete a global public frontend",
Short: "Delete a global public frontend", Args: cobra.ExactArgs(1),
Args: cobra.ExactArgs(1),
} }
command := &adminDeleteFrontendCommand{cmd: cmd} command := &adminDeleteFrontendCommand{cmd: cmd}
cmd.Run = command.run cmd.Run = command.run

View File

@ -3,6 +3,7 @@ package main
import ( import (
"github.com/michaelquigley/cf" "github.com/michaelquigley/cf"
"github.com/openziti/zrok/controller" "github.com/openziti/zrok/controller"
"github.com/openziti/zrok/controller/config"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -27,7 +28,7 @@ func newAdminGcCommand() *adminGcCommand {
} }
func (gc *adminGcCommand) run(_ *cobra.Command, args []string) { func (gc *adminGcCommand) run(_ *cobra.Command, args []string) {
cfg, err := controller.LoadConfig(args[0]) cfg, err := config.LoadConfig(args[0])
if err != nil { if err != nil {
panic(err) panic(err)
} }

41
cmd/zrok/adminMigrate.go Normal file
View File

@ -0,0 +1,41 @@
package main
import (
"github.com/michaelquigley/cf"
"github.com/openziti/zrok/controller/config"
"github.com/openziti/zrok/controller/store"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
func init() {
adminCmd.AddCommand(newAdminMigrate().cmd)
}
type adminMigrate struct {
cmd *cobra.Command
}
func newAdminMigrate() *adminMigrate {
cmd := &cobra.Command{
Use: "migrate <configPath>",
Short: "Migrate the underlying datastore",
Args: cobra.ExactArgs(1),
}
command := &adminMigrate{cmd}
cmd.Run = command.run
return command
}
func (cmd *adminMigrate) run(_ *cobra.Command, args []string) {
configPath := args[0]
inCfg, err := config.LoadConfig(configPath)
if err != nil {
panic(err)
}
logrus.Info(cf.Dump(inCfg, cf.DefaultOptions()))
if _, err := store.Open(inCfg.Store); err != nil {
panic(err)
}
logrus.Info("migration complete")
}

View File

@ -3,14 +3,21 @@ package main
import ( import (
"github.com/michaelquigley/cf" "github.com/michaelquigley/cf"
"github.com/openziti/zrok/controller" "github.com/openziti/zrok/controller"
"github.com/openziti/zrok/controller/config"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var controllerCmd *controllerCommand var controllerCmd *controllerCommand
var metricsCmd = &cobra.Command{
Use: "metrics",
Short: "Metrics related commands",
}
func init() { func init() {
controllerCmd = newControllerCommand() controllerCmd = newControllerCommand()
controllerCmd.cmd.AddCommand(metricsCmd)
rootCmd.AddCommand(controllerCmd.cmd) rootCmd.AddCommand(controllerCmd.cmd)
} }
@ -31,7 +38,7 @@ func newControllerCommand() *controllerCommand {
} }
func (cmd *controllerCommand) run(_ *cobra.Command, args []string) { func (cmd *controllerCommand) run(_ *cobra.Command, args []string) {
cfg, err := controller.LoadConfig(args[0]) cfg, err := config.LoadConfig(args[0])
if err != nil { if err != nil {
panic(err) panic(err)
} }

View File

@ -0,0 +1,61 @@
package main
import (
"github.com/michaelquigley/cf"
"github.com/openziti/zrok/controller/config"
"github.com/openziti/zrok/controller/env"
"github.com/openziti/zrok/controller/metrics"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"os"
"os/signal"
"syscall"
"time"
)
func init() {
metricsCmd.AddCommand(newBridgeCommand().cmd)
}
type bridgeCommand struct {
cmd *cobra.Command
}
func newBridgeCommand() *bridgeCommand {
cmd := &cobra.Command{
Use: "bridge <configPath>",
Short: "Start a zrok metrics bridge",
Args: cobra.ExactArgs(1),
}
command := &bridgeCommand{cmd}
cmd.Run = command.run
return command
}
func (cmd *bridgeCommand) run(_ *cobra.Command, args []string) {
cfg, err := config.LoadConfig(args[0])
if err != nil {
panic(err)
}
logrus.Infof(cf.Dump(cfg, env.GetCfOptions()))
bridge, err := metrics.NewBridge(cfg.Bridge)
if err != nil {
panic(err)
}
if _, err = bridge.Start(); err != nil {
panic(err)
}
c := make(chan os.Signal)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func() {
<-c
bridge.Stop()
os.Exit(0)
}()
for {
time.Sleep(24 * 60 * time.Minute)
}
}

View File

@ -2,7 +2,7 @@ package main
import ( import (
"github.com/michaelquigley/cf" "github.com/michaelquigley/cf"
"github.com/openziti/zrok/controller" "github.com/openziti/zrok/controller/config"
"github.com/openziti/zrok/tui" "github.com/openziti/zrok/tui"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -28,7 +28,7 @@ func newControllerValidateCommand() *controllerValidateCommand {
} }
func (cmd *controllerValidateCommand) run(_ *cobra.Command, args []string) { func (cmd *controllerValidateCommand) run(_ *cobra.Command, args []string) {
cfg, err := controller.LoadConfig(args[0]) cfg, err := config.LoadConfig(args[0])
if err != nil { if err != nil {
tui.Error("controller config validation failed", err) tui.Error("controller config validation failed", err)
} }

View File

@ -9,6 +9,7 @@ import (
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
"github.com/openziti/zrok/rest_client_zrok/account" "github.com/openziti/zrok/rest_client_zrok/account"
"github.com/openziti/zrok/rest_client_zrok/metadata"
"github.com/openziti/zrok/rest_model_zrok" "github.com/openziti/zrok/rest_model_zrok"
"github.com/openziti/zrok/tui" "github.com/openziti/zrok/tui"
"github.com/openziti/zrok/util" "github.com/openziti/zrok/util"
@ -21,9 +22,8 @@ func init() {
} }
type inviteCommand struct { type inviteCommand struct {
cmd *cobra.Command cmd *cobra.Command
token string tui inviteTui
tui inviteTui
} }
func newInviteCommand() *inviteCommand { func newInviteCommand() *inviteCommand {
@ -38,8 +38,6 @@ func newInviteCommand() *inviteCommand {
} }
cmd.Run = command.run cmd.Run = command.run
cmd.Flags().StringVar(&command.token, "token", "", "Invite token required when zrok running in token store mode")
return command return command
} }
@ -58,17 +56,33 @@ func (cmd *inviteCommand) run(_ *cobra.Command, _ []string) {
panic(err) panic(err)
} }
md, err := zrok.Metadata.Configuration(metadata.NewConfigurationParams())
if err != nil {
tui.Error("unable to get server metadata", err)
}
if md != nil {
if !md.GetPayload().InvitesOpen {
apiEndpoint, _ := zrd.ApiEndpoint()
tui.Error(fmt.Sprintf("'%v' is not currently accepting new users", apiEndpoint), nil)
}
cmd.tui.invitesOpen = md.GetPayload().InvitesOpen
cmd.tui.RequiresInviteToken(md.GetPayload().RequiresInviteToken)
cmd.tui.invitesContact = md.GetPayload().InviteTokenContact
}
if _, err := tea.NewProgram(&cmd.tui).Run(); err != nil { if _, err := tea.NewProgram(&cmd.tui).Run(); err != nil {
tui.Error("unable to run interface", err) tui.Error("unable to run interface", err)
os.Exit(1) os.Exit(1)
} }
if cmd.tui.done { if cmd.tui.done {
email := cmd.tui.inputs[0].Value() email := cmd.tui.emailInputs[0].Value()
token := cmd.tui.tokenInput.Value()
req := account.NewInviteParams() req := account.NewInviteParams()
req.Body = &rest_model_zrok.InviteRequest{ req.Body = &rest_model_zrok.InviteRequest{
Email: email, Email: email,
Token: cmd.token, Token: token,
} }
_, err = zrok.Account.Invite(req) _, err = zrok.Account.Invite(req)
if err != nil { if err != nil {
@ -83,18 +97,22 @@ func (cmd *inviteCommand) run(_ *cobra.Command, _ []string) {
func (cmd *inviteCommand) endpointError(apiEndpoint, _ string) { func (cmd *inviteCommand) endpointError(apiEndpoint, _ string) {
fmt.Printf("%v\n\n", tui.SeriousBusiness.Render("there was a problem creating an invitation!")) fmt.Printf("%v\n\n", tui.SeriousBusiness.Render("there was a problem creating an invitation!"))
fmt.Printf("you are trying to use the zrok service at: %v\n\n", tui.Code.Render(apiEndpoint)) fmt.Printf("you are trying to use the zrok service at: %v\n\n", tui.Code.Render(apiEndpoint))
fmt.Printf("%v\n\n", tui.Attention.Render("should you be using a --token? check with your instance administrator!"))
fmt.Printf("you can change your zrok service endpoint using this command:\n\n") fmt.Printf("you can change your zrok service endpoint using this command:\n\n")
fmt.Printf("%v\n\n", tui.Code.Render("$ zrok config set apiEndpoint <newEndpoint>")) fmt.Printf("%v\n\n", tui.Code.Render("$ zrok config set apiEndpoint <newEndpoint>"))
fmt.Printf("(where newEndpoint is something like: %v)\n\n", tui.Code.Render("https://some.zrok.io")) fmt.Printf("(where newEndpoint is something like: %v)\n\n", tui.Code.Render("https://some.zrok.io"))
} }
type inviteTui struct { type inviteTui struct {
focusIndex int focusIndex int
msg string msg string
inputs []textinput.Model emailInputs []textinput.Model
cursorMode textinput.CursorMode tokenInput textinput.Model
done bool cursorMode textinput.CursorMode
done bool
invitesOpen bool
requireInviteToken bool
invitesContact string
maxIndex int
msgOk string msgOk string
msgMismatch string msgMismatch string
@ -110,7 +128,8 @@ type inviteTui struct {
func newInviteTui() inviteTui { func newInviteTui() inviteTui {
m := inviteTui{ m := inviteTui{
inputs: make([]textinput.Model, 2), emailInputs: make([]textinput.Model, 2),
maxIndex: 2,
} }
m.focusedStyle = tui.Attention.Copy() m.focusedStyle = tui.Attention.Copy()
m.blurredStyle = tui.Code.Copy() m.blurredStyle = tui.Code.Copy()
@ -125,7 +144,7 @@ func newInviteTui() inviteTui {
m.msgMismatch = m.errorStyle.Render("email is invalid or does not match confirmation...") m.msgMismatch = m.errorStyle.Render("email is invalid or does not match confirmation...")
var t textinput.Model var t textinput.Model
for i := range m.inputs { for i := range m.emailInputs {
t = textinput.New() t = textinput.New()
t.CursorStyle = m.cursorStyle t.CursorStyle = m.cursorStyle
t.CharLimit = 96 t.CharLimit = 96
@ -140,9 +159,13 @@ func newInviteTui() inviteTui {
t.Placeholder = "Confirm Email" t.Placeholder = "Confirm Email"
} }
m.inputs[i] = t m.emailInputs[i] = t
} }
m.tokenInput = textinput.New()
m.tokenInput.CursorStyle = m.cursorStyle
m.tokenInput.Placeholder = "Token"
return m return m
} }
@ -158,8 +181,8 @@ func (m *inviteTui) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case "tab", "shift+tab", "enter", "up", "down": case "tab", "shift+tab", "enter", "up", "down":
s := msg.String() s := msg.String()
if s == "enter" && m.focusIndex == len(m.inputs) { if s == "enter" && m.focusIndex == m.maxIndex {
if util.IsValidEmail(m.inputs[0].Value()) && m.inputs[0].Value() == m.inputs[1].Value() { if util.IsValidEmail(m.emailInputs[0].Value()) && m.emailInputs[0].Value() == m.emailInputs[1].Value() {
m.done = true m.done = true
return m, tea.Quit return m, tea.Quit
} }
@ -175,23 +198,34 @@ func (m *inviteTui) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.focusIndex++ m.focusIndex++
} }
if m.focusIndex > len(m.inputs) { if m.focusIndex > m.maxIndex {
m.focusIndex = 0 m.focusIndex = 0
} else if m.focusIndex < 0 { } else if m.focusIndex < 0 {
m.focusIndex = len(m.inputs) m.focusIndex = m.maxIndex
} }
cmds := make([]tea.Cmd, len(m.inputs)) cmds := make([]tea.Cmd, m.maxIndex)
for i := 0; i <= len(m.inputs)-1; i++ { for i := 0; i <= len(m.emailInputs)-1; i++ {
if i == m.focusIndex { if i == m.focusIndex {
cmds[i] = m.inputs[i].Focus() cmds[i] = m.emailInputs[i].Focus()
m.inputs[i].PromptStyle = m.focusedStyle m.emailInputs[i].PromptStyle = m.focusedStyle
m.inputs[i].TextStyle = m.focusedStyle m.emailInputs[i].TextStyle = m.focusedStyle
continue continue
} }
m.inputs[i].Blur() m.emailInputs[i].Blur()
m.inputs[i].PromptStyle = m.noStyle m.emailInputs[i].PromptStyle = m.noStyle
m.inputs[i].TextStyle = m.noStyle m.emailInputs[i].TextStyle = m.noStyle
}
if m.requireInviteToken {
if m.focusIndex == 2 {
cmds[2] = m.tokenInput.Focus()
m.tokenInput.PromptStyle = m.focusedStyle
m.tokenInput.TextStyle = m.focusedStyle
} else {
m.tokenInput.Blur()
m.tokenInput.PromptStyle = m.noStyle
m.tokenInput.TextStyle = m.noStyle
}
} }
return m, tea.Batch(cmds...) return m, tea.Batch(cmds...)
@ -204,29 +238,49 @@ func (m *inviteTui) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
} }
func (m *inviteTui) updateInputs(msg tea.Msg) tea.Cmd { func (m *inviteTui) updateInputs(msg tea.Msg) tea.Cmd {
cmds := make([]tea.Cmd, len(m.inputs)) cmds := make([]tea.Cmd, m.maxIndex)
for i := range m.inputs { for i := range m.emailInputs {
m.inputs[i], cmds[i] = m.inputs[i].Update(msg) m.emailInputs[i], cmds[i] = m.emailInputs[i].Update(msg)
}
if m.requireInviteToken {
m.tokenInput, cmds[2] = m.tokenInput.Update(msg)
} }
return tea.Batch(cmds...) return tea.Batch(cmds...)
} }
func (m inviteTui) View() string { func (m inviteTui) View() string {
var b strings.Builder var b strings.Builder
b.WriteString(fmt.Sprintf("\n%v\n\n", m.msg)) b.WriteString(fmt.Sprintf("\n%v\n\n", m.msg))
for i := range m.inputs { if m.requireInviteToken && m.invitesContact != "" {
b.WriteString(m.inputs[i].View()) b.WriteString(fmt.Sprintf("If you don't already have one, request an invite token at: %v\n\n", m.invitesContact))
if i < len(m.inputs)-1 { }
b.WriteRune('\n')
} for i := 0; i < len(m.emailInputs); i++ {
b.WriteString(m.emailInputs[i].View())
b.WriteRune('\n')
}
if m.requireInviteToken {
b.WriteString(m.tokenInput.View())
b.WriteRune('\n')
} }
button := &m.blurredButton button := &m.blurredButton
if m.focusIndex == len(m.inputs) { if m.focusIndex == m.maxIndex {
button = &m.focusedButton button = &m.focusedButton
} }
_, _ = fmt.Fprintf(&b, "\n\n%s\n\n", *button) _, _ = fmt.Fprintf(&b, "\n\n%s\n\n", *button)
return b.String() return b.String()
} }
func (m *inviteTui) RequiresInviteToken(require bool) {
m.requireInviteToken = require
if require {
m.maxIndex = 3
} else {
m.maxIndex = 2
}
}

View File

@ -2,6 +2,9 @@ package main
import ( import (
"github.com/michaelquigley/pfxlog" "github.com/michaelquigley/pfxlog"
"github.com/openziti/transport/v2"
"github.com/openziti/transport/v2/tcp"
"github.com/openziti/transport/v2/udp"
"github.com/openziti/zrok/tui" "github.com/openziti/zrok/tui"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -24,6 +27,8 @@ func init() {
rootCmd.AddCommand(configCmd) rootCmd.AddCommand(configCmd)
rootCmd.AddCommand(shareCmd) rootCmd.AddCommand(shareCmd)
rootCmd.AddCommand(testCmd) rootCmd.AddCommand(testCmd)
transport.AddAddressParser(tcp.AddressParser{})
transport.AddAddressParser(udp.AddressParser{})
} }
var rootCmd = &cobra.Command{ var rootCmd = &cobra.Command{

View File

@ -33,7 +33,7 @@ func newReserveCommand() *reserveCommand {
command := &reserveCommand{cmd: cmd} command := &reserveCommand{cmd: cmd}
cmd.Flags().StringArrayVar(&command.basicAuth, "basic-auth", []string{}, "Basic authentication users (<username:password>,...)") cmd.Flags().StringArrayVar(&command.basicAuth, "basic-auth", []string{}, "Basic authentication users (<username:password>,...)")
cmd.Flags().StringArrayVar(&command.frontendSelection, "frontends", []string{"public"}, "Selected frontends to use for the share") cmd.Flags().StringArrayVar(&command.frontendSelection, "frontends", []string{"public"}, "Selected frontends to use for the share")
cmd.Flags().StringVar(&command.backendMode, "backend-mode", "proxy", "The backend mode {proxy, web}") cmd.Flags().StringVar(&command.backendMode, "backend-mode", "proxy", "The backend mode {proxy, web, <tcpTunnel, udpTunnel>}")
cmd.Run = command.run cmd.Run = command.run
return command return command
} }

View File

@ -6,8 +6,9 @@ import (
"github.com/go-openapi/runtime" "github.com/go-openapi/runtime"
httptransport "github.com/go-openapi/runtime/client" httptransport "github.com/go-openapi/runtime/client"
"github.com/openziti/zrok/endpoints" "github.com/openziti/zrok/endpoints"
"github.com/openziti/zrok/endpoints/proxyBackend" "github.com/openziti/zrok/endpoints/proxy"
"github.com/openziti/zrok/endpoints/webBackend" "github.com/openziti/zrok/endpoints/tcpTunnel"
"github.com/openziti/zrok/endpoints/udpTunnel"
"github.com/openziti/zrok/model" "github.com/openziti/zrok/model"
"github.com/openziti/zrok/rest_client_zrok" "github.com/openziti/zrok/rest_client_zrok"
"github.com/openziti/zrok/rest_client_zrok/share" "github.com/openziti/zrok/rest_client_zrok/share"
@ -43,7 +44,7 @@ func newSharePrivateCommand() *sharePrivateCommand {
} }
command := &sharePrivateCommand{cmd: cmd} command := &sharePrivateCommand{cmd: cmd}
cmd.Flags().StringArrayVar(&command.basicAuth, "basic-auth", []string{}, "Basic authentication users (<username:password>,...") cmd.Flags().StringArrayVar(&command.basicAuth, "basic-auth", []string{}, "Basic authentication users (<username:password>,...")
cmd.Flags().StringVar(&command.backendMode, "backend-mode", "proxy", "The backend mode {proxy, web}") cmd.Flags().StringVar(&command.backendMode, "backend-mode", "proxy", "The backend mode {proxy, web, tcpTunnel, udpTunnel}")
cmd.Flags().BoolVar(&command.headless, "headless", false, "Disable TUI and run headless") cmd.Flags().BoolVar(&command.headless, "headless", false, "Disable TUI and run headless")
cmd.Flags().BoolVar(&command.insecure, "insecure", false, "Enable insecure TLS certificate validation for <target>") cmd.Flags().BoolVar(&command.insecure, "insecure", false, "Enable insecure TLS certificate validation for <target>")
cmd.Run = command.run cmd.Run = command.run
@ -67,8 +68,14 @@ func (cmd *sharePrivateCommand) run(_ *cobra.Command, args []string) {
case "web": case "web":
target = args[0] target = args[0]
case "tcpTunnel":
target = args[0]
case "udpTunnel":
target = args[0]
default: default:
tui.Error(fmt.Sprintf("invalid backend mode '%v'; expected {proxy, web}", cmd.backendMode), nil) tui.Error(fmt.Sprintf("invalid backend mode '%v'; expected {proxy, web, tcpTunnel}", cmd.backendMode), nil)
} }
zrd, err := zrokdir.Load() zrd, err := zrokdir.Load()
@ -139,7 +146,7 @@ func (cmd *sharePrivateCommand) run(_ *cobra.Command, args []string) {
requestsChan := make(chan *endpoints.Request, 1024) requestsChan := make(chan *endpoints.Request, 1024)
switch cmd.backendMode { switch cmd.backendMode {
case "proxy": case "proxy":
cfg := &proxyBackend.Config{ cfg := &proxy.BackendConfig{
IdentityPath: zif, IdentityPath: zif,
EndpointAddress: target, EndpointAddress: target,
ShrToken: resp.Payload.ShrToken, ShrToken: resp.Payload.ShrToken,
@ -155,7 +162,7 @@ func (cmd *sharePrivateCommand) run(_ *cobra.Command, args []string) {
} }
case "web": case "web":
cfg := &webBackend.Config{ cfg := &proxy.WebBackendConfig{
IdentityPath: zif, IdentityPath: zif,
WebRoot: target, WebRoot: target,
ShrToken: resp.Payload.ShrToken, ShrToken: resp.Payload.ShrToken,
@ -169,6 +176,46 @@ func (cmd *sharePrivateCommand) run(_ *cobra.Command, args []string) {
panic(err) panic(err)
} }
case "tcpTunnel":
cfg := &tcpTunnel.BackendConfig{
IdentityPath: zif,
EndpointAddress: target,
ShrToken: resp.Payload.ShrToken,
RequestsChan: requestsChan,
}
be, err := tcpTunnel.NewBackend(cfg)
if err != nil {
if !panicInstead {
tui.Error("unable to create tcpTunnel backend", err)
}
panic(err)
}
go func() {
if err := be.Run(); err != nil {
logrus.Errorf("error running tcpTunnel backend: %v", err)
}
}()
case "udpTunnel":
cfg := &udpTunnel.BackendConfig{
IdentityPath: zif,
EndpointAddress: target,
ShrToken: resp.Payload.ShrToken,
RequestsChan: requestsChan,
}
be, err := udpTunnel.NewBackend(cfg)
if err != nil {
if !panicInstead {
tui.Error("unable to create udpTunnel backend", err)
}
panic(err)
}
go func() {
if err := be.Run(); err != nil {
logrus.Errorf("error running udpTunnel backend: %v", err)
}
}()
default: default:
tui.Error("invalid backend mode", nil) tui.Error("invalid backend mode", nil)
} }
@ -207,8 +254,8 @@ func (cmd *sharePrivateCommand) run(_ *cobra.Command, args []string) {
} }
} }
func (cmd *sharePrivateCommand) proxyBackendMode(cfg *proxyBackend.Config) (endpoints.RequestHandler, error) { func (cmd *sharePrivateCommand) proxyBackendMode(cfg *proxy.BackendConfig) (endpoints.RequestHandler, error) {
be, err := proxyBackend.NewBackend(cfg) be, err := proxy.NewBackend(cfg)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "error creating http proxy backend") return nil, errors.Wrap(err, "error creating http proxy backend")
} }
@ -222,8 +269,8 @@ func (cmd *sharePrivateCommand) proxyBackendMode(cfg *proxyBackend.Config) (endp
return be, nil return be, nil
} }
func (cmd *sharePrivateCommand) webBackendMode(cfg *webBackend.Config) (endpoints.RequestHandler, error) { func (cmd *sharePrivateCommand) webBackendMode(cfg *proxy.WebBackendConfig) (endpoints.RequestHandler, error) {
be, err := webBackend.NewBackend(cfg) be, err := proxy.NewWebBackend(cfg)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "error creating http web backend") return nil, errors.Wrap(err, "error creating http web backend")
} }

View File

@ -6,8 +6,7 @@ import (
"github.com/go-openapi/runtime" "github.com/go-openapi/runtime"
httptransport "github.com/go-openapi/runtime/client" httptransport "github.com/go-openapi/runtime/client"
"github.com/openziti/zrok/endpoints" "github.com/openziti/zrok/endpoints"
"github.com/openziti/zrok/endpoints/proxyBackend" "github.com/openziti/zrok/endpoints/proxy"
"github.com/openziti/zrok/endpoints/webBackend"
"github.com/openziti/zrok/model" "github.com/openziti/zrok/model"
"github.com/openziti/zrok/rest_client_zrok" "github.com/openziti/zrok/rest_client_zrok"
"github.com/openziti/zrok/rest_client_zrok/share" "github.com/openziti/zrok/rest_client_zrok/share"
@ -142,7 +141,7 @@ func (cmd *sharePublicCommand) run(_ *cobra.Command, args []string) {
requestsChan := make(chan *endpoints.Request, 1024) requestsChan := make(chan *endpoints.Request, 1024)
switch cmd.backendMode { switch cmd.backendMode {
case "proxy": case "proxy":
cfg := &proxyBackend.Config{ cfg := &proxy.BackendConfig{
IdentityPath: zif, IdentityPath: zif,
EndpointAddress: target, EndpointAddress: target,
ShrToken: resp.Payload.ShrToken, ShrToken: resp.Payload.ShrToken,
@ -158,7 +157,7 @@ func (cmd *sharePublicCommand) run(_ *cobra.Command, args []string) {
} }
case "web": case "web":
cfg := &webBackend.Config{ cfg := &proxy.WebBackendConfig{
IdentityPath: zif, IdentityPath: zif,
WebRoot: target, WebRoot: target,
ShrToken: resp.Payload.ShrToken, ShrToken: resp.Payload.ShrToken,
@ -209,8 +208,8 @@ func (cmd *sharePublicCommand) run(_ *cobra.Command, args []string) {
} }
} }
func (cmd *sharePublicCommand) proxyBackendMode(cfg *proxyBackend.Config) (endpoints.RequestHandler, error) { func (cmd *sharePublicCommand) proxyBackendMode(cfg *proxy.BackendConfig) (endpoints.RequestHandler, error) {
be, err := proxyBackend.NewBackend(cfg) be, err := proxy.NewBackend(cfg)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "error creating http proxy backend") return nil, errors.Wrap(err, "error creating http proxy backend")
} }
@ -224,8 +223,8 @@ func (cmd *sharePublicCommand) proxyBackendMode(cfg *proxyBackend.Config) (endpo
return be, nil return be, nil
} }
func (cmd *sharePublicCommand) webBackendMode(cfg *webBackend.Config) (endpoints.RequestHandler, error) { func (cmd *sharePublicCommand) webBackendMode(cfg *proxy.WebBackendConfig) (endpoints.RequestHandler, error) {
be, err := webBackend.NewBackend(cfg) be, err := proxy.NewWebBackend(cfg)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "error creating http web backend") return nil, errors.Wrap(err, "error creating http web backend")
} }

View File

@ -5,8 +5,7 @@ import (
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
httptransport "github.com/go-openapi/runtime/client" httptransport "github.com/go-openapi/runtime/client"
"github.com/openziti/zrok/endpoints" "github.com/openziti/zrok/endpoints"
"github.com/openziti/zrok/endpoints/proxyBackend" "github.com/openziti/zrok/endpoints/proxy"
"github.com/openziti/zrok/endpoints/webBackend"
"github.com/openziti/zrok/rest_client_zrok/metadata" "github.com/openziti/zrok/rest_client_zrok/metadata"
"github.com/openziti/zrok/rest_client_zrok/share" "github.com/openziti/zrok/rest_client_zrok/share"
"github.com/openziti/zrok/rest_model_zrok" "github.com/openziti/zrok/rest_model_zrok"
@ -74,6 +73,7 @@ func (cmd *shareReservedCommand) run(_ *cobra.Command, args []string) {
} }
panic(err) panic(err)
} }
target = cmd.overrideEndpoint
if target == "" { if target == "" {
target = resp.Payload.BackendProxyEndpoint target = resp.Payload.BackendProxyEndpoint
} }
@ -108,7 +108,7 @@ func (cmd *shareReservedCommand) run(_ *cobra.Command, args []string) {
requestsChan := make(chan *endpoints.Request, 1024) requestsChan := make(chan *endpoints.Request, 1024)
switch resp.Payload.BackendMode { switch resp.Payload.BackendMode {
case "proxy": case "proxy":
cfg := &proxyBackend.Config{ cfg := &proxy.BackendConfig{
IdentityPath: zif, IdentityPath: zif,
EndpointAddress: target, EndpointAddress: target,
ShrToken: shrToken, ShrToken: shrToken,
@ -124,7 +124,7 @@ func (cmd *shareReservedCommand) run(_ *cobra.Command, args []string) {
} }
case "web": case "web":
cfg := &webBackend.Config{ cfg := &proxy.WebBackendConfig{
IdentityPath: zif, IdentityPath: zif,
WebRoot: target, WebRoot: target,
ShrToken: shrToken, ShrToken: shrToken,
@ -187,8 +187,8 @@ func (cmd *shareReservedCommand) run(_ *cobra.Command, args []string) {
} }
} }
func (cmd *shareReservedCommand) proxyBackendMode(cfg *proxyBackend.Config) (endpoints.RequestHandler, error) { func (cmd *shareReservedCommand) proxyBackendMode(cfg *proxy.BackendConfig) (endpoints.RequestHandler, error) {
be, err := proxyBackend.NewBackend(cfg) be, err := proxy.NewBackend(cfg)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "error creating http proxy backend") return nil, errors.Wrap(err, "error creating http proxy backend")
} }
@ -202,8 +202,8 @@ func (cmd *shareReservedCommand) proxyBackendMode(cfg *proxyBackend.Config) (end
return be, nil return be, nil
} }
func (cmd *shareReservedCommand) webBackendMode(cfg *webBackend.Config) (endpoints.RequestHandler, error) { func (cmd *shareReservedCommand) webBackendMode(cfg *proxy.WebBackendConfig) (endpoints.RequestHandler, error) {
be, err := webBackend.NewBackend(cfg) be, err := proxy.NewWebBackend(cfg)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "error creating http web backend") return nil, errors.Wrap(err, "error creating http web backend")
} }

View File

@ -13,7 +13,6 @@ import (
"time" "time"
"github.com/openziti/sdk-golang/ziti" "github.com/openziti/sdk-golang/ziti"
"github.com/openziti/sdk-golang/ziti/config"
"github.com/openziti/zrok/cmd/zrok/endpointUi" "github.com/openziti/zrok/cmd/zrok/endpointUi"
"github.com/openziti/zrok/tui" "github.com/openziti/zrok/tui"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -79,13 +78,17 @@ func (cmd *testEndpointCommand) run(_ *cobra.Command, _ []string) {
flag.Usage() flag.Usage()
os.Exit(1) os.Exit(1)
} }
config := config.Config{} config := ziti.Config{}
err = json.Unmarshal(identityJsonBytes, &config) err = json.Unmarshal(identityJsonBytes, &config)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "failed to load ziti configuration JSON: %v", err) fmt.Fprintf(os.Stderr, "failed to load ziti configuration JSON: %v", err)
os.Exit(1) os.Exit(1)
} }
zitiContext := ziti.NewContextWithConfig(&config) zitiContext, err := ziti.NewContext(&config)
if err != nil {
fmt.Printf("error loading ziti context: %v", err)
os.Exit(1)
}
if err := zitiContext.Authenticate(); err != nil { if err := zitiContext.Authenticate(); err != nil {
fmt.Fprintf(os.Stderr, "Error: Unable to authenticate ziti: %v\n\n", err) fmt.Fprintf(os.Stderr, "Error: Unable to authenticate ziti: %v\n\n", err)
os.Exit(1) os.Exit(1)

View File

@ -7,7 +7,6 @@ import (
"github.com/go-openapi/runtime" "github.com/go-openapi/runtime"
httptransport "github.com/go-openapi/runtime/client" httptransport "github.com/go-openapi/runtime/client"
"github.com/openziti/sdk-golang/ziti" "github.com/openziti/sdk-golang/ziti"
"github.com/openziti/sdk-golang/ziti/config"
"github.com/openziti/sdk-golang/ziti/edge" "github.com/openziti/sdk-golang/ziti/edge"
"github.com/openziti/zrok/model" "github.com/openziti/zrok/model"
"github.com/openziti/zrok/rest_client_zrok" "github.com/openziti/zrok/rest_client_zrok"
@ -144,7 +143,7 @@ func (l *looper) run() {
} }
func (l *looper) serviceListener() { func (l *looper) serviceListener() {
zcfg, err := config.NewFromFile(l.zif) zcfg, err := ziti.NewConfigFromFile(l.zif)
if err != nil { if err != nil {
logrus.Errorf("error opening ziti config '%v': %v", l.zif, err) logrus.Errorf("error opening ziti config '%v': %v", l.zif, err)
return return
@ -153,7 +152,12 @@ func (l *looper) serviceListener() {
ConnectTimeout: 5 * time.Minute, ConnectTimeout: 5 * time.Minute,
MaxConnections: 10, MaxConnections: 10,
} }
if l.listener, err = ziti.NewContextWithConfig(zcfg).ListenWithOptions(l.shrToken, &opts); err == nil { zctx, err := ziti.NewContext(zcfg)
if err != nil {
logrus.Errorf("error loading ziti context: %v", err)
return
}
if l.listener, err = zctx.ListenWithOptions(l.shrToken, &opts); err == nil {
if err := http.Serve(l.listener, l); err != nil { if err := http.Serve(l.listener, l); err != nil {
logrus.Errorf("looper #%d, error serving: %v", l.id, err) logrus.Errorf("looper #%d, error serving: %v", l.id, err)
} }

View File

@ -12,7 +12,6 @@ import (
"time" "time"
"github.com/openziti/sdk-golang/ziti" "github.com/openziti/sdk-golang/ziti"
"github.com/openziti/sdk-golang/ziti/config"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"nhooyr.io/websocket" "nhooyr.io/websocket"
@ -64,14 +63,17 @@ func (cmd *testWebsocketCommand) run(_ *cobra.Command, args []string) {
os.Exit(1) os.Exit(1)
} }
cfg := &config.Config{} cfg := &ziti.Config{}
err = json.Unmarshal(identityJsonBytes, cfg) err = json.Unmarshal(identityJsonBytes, cfg)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "failed to load ziti configuration JSON: %v", err) fmt.Fprintf(os.Stderr, "failed to load ziti configuration JSON: %v", err)
os.Exit(1) os.Exit(1)
} }
zitiContext := ziti.NewContextWithConfig(cfg) zitiContext, err := ziti.NewContext(cfg)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to load ziti context: %v", err)
os.Exit(1)
}
dial := func(_ context.Context, _, addr string) (net.Conn, error) { dial := func(_ context.Context, _, addr string) (net.Conn, error) {
service := strings.Split(addr, ":")[0] service := strings.Split(addr, ":")[0]
return zitiContext.Dial(service) return zitiContext.Dial(service)

View File

@ -2,10 +2,12 @@ package controller
import ( import (
"github.com/go-openapi/runtime/middleware" "github.com/go-openapi/runtime/middleware"
"github.com/jmoiron/sqlx"
"github.com/openziti/zrok/controller/store" "github.com/openziti/zrok/controller/store"
"github.com/openziti/zrok/controller/zrokEdgeSdk" "github.com/openziti/zrok/controller/zrokEdgeSdk"
"github.com/openziti/zrok/rest_model_zrok" "github.com/openziti/zrok/rest_model_zrok"
"github.com/openziti/zrok/rest_server_zrok/operations/share" "github.com/openziti/zrok/rest_server_zrok/operations/share"
"github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@ -16,16 +18,16 @@ func newAccessHandler() *accessHandler {
} }
func (h *accessHandler) Handle(params share.AccessParams, principal *rest_model_zrok.Principal) middleware.Responder { func (h *accessHandler) Handle(params share.AccessParams, principal *rest_model_zrok.Principal) middleware.Responder {
tx, err := str.Begin() trx, err := str.Begin()
if err != nil { if err != nil {
logrus.Errorf("error starting transaction for user '%v': %v", principal.Email, err) logrus.Errorf("error starting transaction for user '%v': %v", principal.Email, err)
return share.NewAccessInternalServerError() return share.NewAccessInternalServerError()
} }
defer func() { _ = tx.Rollback() }() defer func() { _ = trx.Rollback() }()
envZId := params.Body.EnvZID envZId := params.Body.EnvZID
envId := 0 envId := 0
if envs, err := str.FindEnvironmentsForAccount(int(principal.ID), tx); err == nil { if envs, err := str.FindEnvironmentsForAccount(int(principal.ID), trx); err == nil {
found := false found := false
for _, env := range envs { for _, env := range envs {
if env.ZId == envZId { if env.ZId == envZId {
@ -45,28 +47,33 @@ func (h *accessHandler) Handle(params share.AccessParams, principal *rest_model_
} }
shrToken := params.Body.ShrToken shrToken := params.Body.ShrToken
sshr, err := str.FindShareWithToken(shrToken, tx) shr, err := str.FindShareWithToken(shrToken, trx)
if err != nil { if err != nil {
logrus.Errorf("error finding share") logrus.Errorf("error finding share")
return share.NewAccessNotFound() return share.NewAccessNotFound()
} }
if sshr == nil { if shr == nil {
logrus.Errorf("unable to find share '%v' for user '%v'", shrToken, principal.Email) logrus.Errorf("unable to find share '%v' for user '%v'", shrToken, principal.Email)
return share.NewAccessNotFound() return share.NewAccessNotFound()
} }
if err := h.checkLimits(shr, trx); err != nil {
logrus.Errorf("cannot access limited share for '%v': %v", principal.Email, err)
return share.NewAccessNotFound()
}
feToken, err := createToken() feToken, err := createToken()
if err != nil { if err != nil {
logrus.Error(err) logrus.Error(err)
return share.NewAccessInternalServerError() return share.NewAccessInternalServerError()
} }
if _, err := str.CreateFrontend(envId, &store.Frontend{Token: feToken, ZId: envZId}, tx); err != nil { if _, err := str.CreateFrontend(envId, &store.Frontend{PrivateShareId: &shr.Id, Token: feToken, ZId: envZId}, trx); err != nil {
logrus.Errorf("error creating frontend record for user '%v': %v", principal.Email, err) logrus.Errorf("error creating frontend record for user '%v': %v", principal.Email, err)
return share.NewAccessInternalServerError() return share.NewAccessInternalServerError()
} }
edge, err := edgeClient() edge, err := zrokEdgeSdk.Client(cfg.Ziti)
if err != nil { if err != nil {
logrus.Error(err) logrus.Error(err)
return share.NewAccessInternalServerError() return share.NewAccessInternalServerError()
@ -76,15 +83,31 @@ func (h *accessHandler) Handle(params share.AccessParams, principal *rest_model_
"zrokFrontendToken": feToken, "zrokFrontendToken": feToken,
"zrokShareToken": shrToken, "zrokShareToken": shrToken,
} }
if err := zrokEdgeSdk.CreateServicePolicyDial(envZId+"-"+sshr.ZId+"-dial", sshr.ZId, []string{envZId}, addlTags, edge); err != nil { if err := zrokEdgeSdk.CreateServicePolicyDial(feToken+"-"+envZId+"-"+shr.ZId+"-dial", shr.ZId, []string{envZId}, addlTags, edge); err != nil {
logrus.Errorf("unable to create dial policy for user '%v': %v", principal.Email, err) logrus.Errorf("unable to create dial policy for user '%v': %v", principal.Email, err)
return share.NewAccessInternalServerError() return share.NewAccessInternalServerError()
} }
if err := tx.Commit(); err != nil { if err := trx.Commit(); err != nil {
logrus.Errorf("error committing frontend record: %v", err) logrus.Errorf("error committing frontend record: %v", err)
return share.NewAccessInternalServerError() return share.NewAccessInternalServerError()
} }
return share.NewAccessCreated().WithPayload(&rest_model_zrok.AccessResponse{FrontendToken: feToken}) return share.NewAccessCreated().WithPayload(&rest_model_zrok.AccessResponse{
FrontendToken: feToken,
BackendMode: shr.BackendMode,
})
}
func (h *accessHandler) checkLimits(shr *store.Share, trx *sqlx.Tx) error {
if limitsAgent != nil {
ok, err := limitsAgent.CanAccessShare(shr.Id, trx)
if err != nil {
return errors.Wrapf(err, "error checking share limits for '%v'", shr.Token)
}
if !ok {
return errors.Errorf("share limit check failed for '%v'", shr.Token)
}
}
return nil
} }

View File

@ -0,0 +1,55 @@
package controller
import (
"github.com/go-openapi/runtime/middleware"
"github.com/openziti/zrok/rest_model_zrok"
"github.com/openziti/zrok/rest_server_zrok/operations/metadata"
"github.com/sirupsen/logrus"
)
type accountDetailHandler struct{}
func newAccountDetailHandler() *accountDetailHandler {
return &accountDetailHandler{}
}
func (h *accountDetailHandler) Handle(params metadata.GetAccountDetailParams, principal *rest_model_zrok.Principal) middleware.Responder {
trx, err := str.Begin()
if err != nil {
logrus.Errorf("error stasrting transaction for '%v': %v", principal.Email, err)
return metadata.NewGetAccountDetailInternalServerError()
}
defer func() { _ = trx.Rollback() }()
envs, err := str.FindEnvironmentsForAccount(int(principal.ID), trx)
if err != nil {
logrus.Errorf("error retrieving environments for '%v': %v", principal.Email, err)
return metadata.NewGetAccountDetailInternalServerError()
}
sparkRx := make(map[int][]int64)
sparkTx := make(map[int][]int64)
if cfg.Metrics != nil && cfg.Metrics.Influx != nil {
sparkRx, sparkTx, err = sparkDataForEnvironments(envs)
if err != nil {
logrus.Errorf("error querying spark data for environments for '%v': %v", principal.Email, err)
}
} else {
logrus.Debug("skipping spark data for environments; no influx configuration")
}
var payload []*rest_model_zrok.Environment
for _, env := range envs {
var sparkData []*rest_model_zrok.SparkDataSample
for i := 0; i < len(sparkRx[env.Id]) && i < len(sparkTx[env.Id]); i++ {
sparkData = append(sparkData, &rest_model_zrok.SparkDataSample{Rx: float64(sparkRx[env.Id][i]), Tx: float64(sparkTx[env.Id][i])})
}
payload = append(payload, &rest_model_zrok.Environment{
Activity: sparkData,
Address: env.Address,
CreatedAt: env.CreatedAt.UnixMilli(),
Description: env.Description,
Host: env.Host,
UpdatedAt: env.UpdatedAt.UnixMilli(),
ZID: env.ZId,
})
}
return metadata.NewGetAccountDetailOK().WithPayload(payload)
}

View File

@ -5,17 +5,13 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/openziti/edge/rest_management_api_client" "github.com/openziti/edge-api/rest_management_api_client"
"github.com/openziti/edge/rest_management_api_client/config" "github.com/openziti/edge-api/rest_management_api_client/config"
"github.com/openziti/edge/rest_management_api_client/edge_router_policy" "github.com/openziti/edge-api/rest_management_api_client/edge_router_policy"
"github.com/openziti/edge/rest_management_api_client/identity" "github.com/openziti/edge-api/rest_management_api_client/identity"
"github.com/openziti/edge/rest_management_api_client/service" rest_model_edge "github.com/openziti/edge-api/rest_model"
"github.com/openziti/edge/rest_management_api_client/service_edge_router_policy"
"github.com/openziti/edge/rest_management_api_client/service_policy"
"github.com/openziti/edge/rest_model"
rest_model_edge "github.com/openziti/edge/rest_model"
"github.com/openziti/sdk-golang/ziti" "github.com/openziti/sdk-golang/ziti"
config2 "github.com/openziti/sdk-golang/ziti/config" zrok_config "github.com/openziti/zrok/controller/config"
"github.com/openziti/zrok/controller/store" "github.com/openziti/zrok/controller/store"
"github.com/openziti/zrok/controller/zrokEdgeSdk" "github.com/openziti/zrok/controller/zrokEdgeSdk"
"github.com/openziti/zrok/model" "github.com/openziti/zrok/model"
@ -25,7 +21,7 @@ import (
"time" "time"
) )
func Bootstrap(skipCtrl, skipFrontend bool, inCfg *Config) error { func Bootstrap(skipCtrl, skipFrontend bool, inCfg *zrok_config.Config) error {
cfg = inCfg cfg = inCfg
if v, err := store.Open(cfg.Store); err == nil { if v, err := store.Open(cfg.Store); err == nil {
@ -35,7 +31,7 @@ func Bootstrap(skipCtrl, skipFrontend bool, inCfg *Config) error {
} }
logrus.Info("connecting to the ziti edge management api") logrus.Info("connecting to the ziti edge management api")
edge, err := edgeClient() edge, err := zrokEdgeSdk.Client(cfg.Ziti)
if err != nil { if err != nil {
return errors.Wrap(err, "error connecting to the ziti edge management api") return errors.Wrap(err, "error connecting to the ziti edge management api")
} }
@ -100,27 +96,6 @@ func Bootstrap(skipCtrl, skipFrontend bool, inCfg *Config) error {
return err return err
} }
var metricsSvcZId string
if metricsSvcZId, err = assertMetricsService(cfg, edge); err != nil {
return err
}
if err := assertMetricsSerp(metricsSvcZId, cfg, edge); err != nil {
return err
}
if !skipCtrl {
if err := assertCtrlMetricsBind(ctrlZId, metricsSvcZId, edge); err != nil {
return err
}
}
if !skipFrontend {
if err := assertFrontendMetricsDial(frontendZId, metricsSvcZId, edge); err != nil {
return err
}
}
return nil return nil
} }
@ -141,7 +116,7 @@ func assertZrokProxyConfigType(edge *rest_management_api_client.ZitiEdgeManageme
} }
if len(listResp.Payload.Data) < 1 { if len(listResp.Payload.Data) < 1 {
name := model.ZrokProxyConfig name := model.ZrokProxyConfig
ct := &rest_model.ConfigTypeCreate{Name: &name} ct := &rest_model_edge.ConfigTypeCreate{Name: &name}
createReq := &config.CreateConfigTypeParams{ConfigType: ct} createReq := &config.CreateConfigTypeParams{ConfigType: ct}
createReq.SetTimeout(30 * time.Second) createReq.SetTimeout(30 * time.Second)
createResp, err := edge.Config.CreateConfigType(createReq, nil) createResp, err := edge.Config.CreateConfigType(createReq, nil)
@ -162,16 +137,22 @@ func getIdentityId(identityName string) (string, error) {
if err != nil { if err != nil {
return "", errors.Wrapf(err, "error opening identity '%v' from zrokdir", identityName) return "", errors.Wrapf(err, "error opening identity '%v' from zrokdir", identityName)
} }
zcfg, err := config2.NewFromFile(zif) zcfg, err := ziti.NewConfigFromFile(zif)
if err != nil { if err != nil {
return "", errors.Wrapf(err, "error loading ziti config from file '%v'", zif) return "", errors.Wrapf(err, "error loading ziti config from file '%v'", zif)
} }
zctx := ziti.NewContextWithConfig(zcfg) zctx, err := ziti.NewContext(zcfg)
if err != nil {
return "", errors.Wrap(err, "error loading ziti context")
}
id, err := zctx.GetCurrentIdentity() id, err := zctx.GetCurrentIdentity()
if err != nil { if err != nil {
return "", errors.Wrapf(err, "error getting current identity from '%v'", zif) return "", errors.Wrapf(err, "error getting current identity from '%v'", zif)
} }
return id.Id, nil if id.ID != nil {
return *id.ID, nil
}
return "", nil
} }
func assertIdentity(zId string, edge *rest_management_api_client.ZitiEdgeManagement) error { func assertIdentity(zId string, edge *rest_management_api_client.ZitiEdgeManagement) error {
@ -243,105 +224,3 @@ func assertErpForIdentity(name, zId string, edge *rest_management_api_client.Zit
logrus.Infof("asserted erps for '%v' (%v)", name, zId) logrus.Infof("asserted erps for '%v' (%v)", name, zId)
return nil return nil
} }
func assertMetricsService(cfg *Config, edge *rest_management_api_client.ZitiEdgeManagement) (string, error) {
filter := fmt.Sprintf("name=\"%v\" and tags.zrok != null", cfg.Metrics.ServiceName)
limit := int64(0)
offset := int64(0)
listReq := &service.ListServicesParams{
Filter: &filter,
Limit: &limit,
Offset: &offset,
}
listReq.SetTimeout(30 * time.Second)
listResp, err := edge.Service.ListServices(listReq, nil)
if err != nil {
return "", errors.Wrapf(err, "error listing '%v' service", cfg.Metrics.ServiceName)
}
var svcZId string
if len(listResp.Payload.Data) != 1 {
logrus.Infof("creating '%v' service", cfg.Metrics.ServiceName)
svcZId, err = zrokEdgeSdk.CreateService("metrics", nil, nil, edge)
if err != nil {
return "", errors.Wrapf(err, "error creating '%v' service", cfg.Metrics.ServiceName)
}
} else {
svcZId = *listResp.Payload.Data[0].ID
}
logrus.Infof("asserted '%v' service (%v)", cfg.Metrics.ServiceName, svcZId)
return svcZId, nil
}
func assertMetricsSerp(metricsSvcZId string, cfg *Config, edge *rest_management_api_client.ZitiEdgeManagement) error {
filter := fmt.Sprintf("allOf(serviceRoles) = \"@%v\" and allOf(edgeRouterRoles) = \"#all\" and tags.zrok != null", metricsSvcZId)
limit := int64(0)
offset := int64(0)
listReq := &service_edge_router_policy.ListServiceEdgeRouterPoliciesParams{
Filter: &filter,
Limit: &limit,
Offset: &offset,
}
listReq.SetTimeout(30 * time.Second)
listResp, err := edge.ServiceEdgeRouterPolicy.ListServiceEdgeRouterPolicies(listReq, nil)
if err != nil {
return errors.Wrapf(err, "error listing '%v' serps", cfg.Metrics.ServiceName)
}
if len(listResp.Payload.Data) != 1 {
logrus.Infof("creating '%v' serp", cfg.Metrics.ServiceName)
_, err := zrokEdgeSdk.CreateServiceEdgeRouterPolicy(cfg.Metrics.ServiceName, metricsSvcZId, nil, edge)
if err != nil {
return errors.Wrapf(err, "error creating '%v' serp", cfg.Metrics.ServiceName)
}
}
logrus.Infof("asserted '%v' serp", cfg.Metrics.ServiceName)
return nil
}
func assertCtrlMetricsBind(ctrlZId, metricsSvcZId string, edge *rest_management_api_client.ZitiEdgeManagement) error {
filter := fmt.Sprintf("allOf(serviceRoles) = \"@%v\" and allOf(identityRoles) = \"@%v\" and type = 2 and tags.zrok != null", metricsSvcZId, ctrlZId)
limit := int64(0)
offset := int64(0)
listReq := &service_policy.ListServicePoliciesParams{
Filter: &filter,
Limit: &limit,
Offset: &offset,
}
listReq.SetTimeout(30 * time.Second)
listResp, err := edge.ServicePolicy.ListServicePolicies(listReq, nil)
if err != nil {
return errors.Wrapf(err, "error listing 'ctrl-metrics-bind' service policy")
}
if len(listResp.Payload.Data) != 1 {
logrus.Info("creating 'ctrl-metrics-bind' service policy")
if err = zrokEdgeSdk.CreateServicePolicyBind("ctrl-metrics-bind", metricsSvcZId, ctrlZId, nil, edge); err != nil {
return errors.Wrap(err, "error creating 'ctrl-metrics-bind' service policy")
}
}
logrus.Infof("asserted 'ctrl-metrics-bind' service policy")
return nil
}
func assertFrontendMetricsDial(frontendZId, metricsSvcZId string, edge *rest_management_api_client.ZitiEdgeManagement) error {
filter := fmt.Sprintf("allOf(serviceRoles) = \"@%v\" and allOf(identityRoles) = \"@%v\" and type = 1 and tags.zrok != null", metricsSvcZId, frontendZId)
limit := int64(0)
offset := int64(0)
listReq := &service_policy.ListServicePoliciesParams{
Filter: &filter,
Limit: &limit,
Offset: &offset,
}
listReq.SetTimeout(30 * time.Second)
listResp, err := edge.ServicePolicy.ListServicePolicies(listReq, nil)
if err != nil {
return errors.Wrapf(err, "error listing 'frontend-metrics-dial' service policy")
}
if len(listResp.Payload.Data) != 1 {
logrus.Info("creating 'frontend-metrics-dial' service policy")
if err = zrokEdgeSdk.CreateServicePolicyDial("frontend-metrics-dial", metricsSvcZId, []string{frontendZId}, nil, edge); err != nil {
return errors.Wrap(err, "error creating 'frontend-metrics-dial' service policy")
}
}
logrus.Infof("asserted 'frontend-metrics-dial' service policy")
return nil
}

View File

@ -1,33 +1,42 @@
package controller package config
import ( import (
"time" "time"
"github.com/openziti/zrok/controller/emailUi"
"github.com/openziti/zrok/controller/env"
"github.com/openziti/zrok/controller/limits"
"github.com/openziti/zrok/controller/metrics"
"github.com/openziti/zrok/controller/zrokEdgeSdk"
"github.com/michaelquigley/cf" "github.com/michaelquigley/cf"
"github.com/openziti/zrok/controller/store" "github.com/openziti/zrok/controller/store"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
const ConfigVersion = 2 const ConfigVersion = 3
type Config struct { type Config struct {
V int V int
Admin *AdminConfig Admin *AdminConfig
Bridge *metrics.BridgeConfig
Endpoint *EndpointConfig Endpoint *EndpointConfig
Email *EmailConfig Email *emailUi.Config
Influx *InfluxConfig Invites *InvitesConfig
Limits *LimitsConfig Limits *limits.Config
Maintenance *MaintenanceConfig Maintenance *MaintenanceConfig
Metrics *MetricsConfig Metrics *metrics.Config
Passwords *PasswordsConfig
Registration *RegistrationConfig Registration *RegistrationConfig
ResetPassword *ResetPasswordConfig ResetPassword *ResetPasswordConfig
Store *store.Config Store *store.Config
Ziti *ZitiConfig Ziti *zrokEdgeSdk.Config
} }
type AdminConfig struct { type AdminConfig struct {
Secrets []string `cf:"+secret"` Secrets []string `cf:"+secret"`
TouLink string TouLink string
ProfileEndpoint string
} }
type EndpointConfig struct { type EndpointConfig struct {
@ -35,38 +44,10 @@ type EndpointConfig struct {
Port int Port int
} }
type EmailConfig struct { type InvitesConfig struct {
Host string InvitesOpen bool
Port int TokenStrategy string
Username string TokenContact string
Password string `cf:"+secret"`
From string
}
type RegistrationConfig struct {
RegistrationUrlTemplate string
TokenStrategy string
}
type ResetPasswordConfig struct {
ResetUrlTemplate string
}
type ZitiConfig struct {
ApiEndpoint string
Username string
Password string `cf:"+secret"`
}
type MetricsConfig struct {
ServiceName string
}
type InfluxConfig struct {
Url string
Bucket string
Org string
Token string `cf:"+secret"`
} }
type MaintenanceConfig struct { type MaintenanceConfig struct {
@ -74,6 +55,22 @@ type MaintenanceConfig struct {
Registration *RegistrationMaintenanceConfig Registration *RegistrationMaintenanceConfig
} }
type PasswordsConfig struct {
Length int
RequireCapital bool
RequireNumeric bool
RequireSpecial bool
ValidSpecialCharacters string
}
type RegistrationConfig struct {
RegistrationUrlTemplate string
}
type ResetPasswordConfig struct {
ResetUrlTemplate string
}
type RegistrationMaintenanceConfig struct { type RegistrationMaintenanceConfig struct {
ExpirationTimeout time.Duration ExpirationTimeout time.Duration
CheckFrequency time.Duration CheckFrequency time.Duration
@ -86,22 +83,9 @@ type ResetPasswordMaintenanceConfig struct {
BatchLimit int BatchLimit int
} }
const Unlimited = -1
type LimitsConfig struct {
Environments int
Shares int
}
func DefaultConfig() *Config { func DefaultConfig() *Config {
return &Config{ return &Config{
Limits: &LimitsConfig{ Limits: limits.DefaultConfig(),
Environments: Unlimited,
Shares: Unlimited,
},
Metrics: &MetricsConfig{
ServiceName: "metrics",
},
Maintenance: &MaintenanceConfig{ Maintenance: &MaintenanceConfig{
ResetPassword: &ResetPasswordMaintenanceConfig{ ResetPassword: &ResetPasswordMaintenanceConfig{
ExpirationTimeout: time.Minute * 15, ExpirationTimeout: time.Minute * 15,
@ -114,12 +98,19 @@ func DefaultConfig() *Config {
BatchLimit: 500, BatchLimit: 500,
}, },
}, },
Passwords: &PasswordsConfig{
Length: 8,
RequireCapital: true,
RequireNumeric: true,
RequireSpecial: true,
ValidSpecialCharacters: `!@$&*_-., "#%'()+/:;<=>?[\]^{|}~`,
},
} }
} }
func LoadConfig(path string) (*Config, error) { func LoadConfig(path string) (*Config, error) {
cfg := DefaultConfig() cfg := DefaultConfig()
if err := cf.BindYaml(cfg, path, cf.DefaultOptions()); err != nil { if err := cf.BindYaml(cfg, path, env.GetCfOptions()); err != nil {
return nil, errors.Wrapf(err, "error loading controller config '%v'", path) return nil, errors.Wrapf(err, "error loading controller config '%v'", path)
} }
if cfg.V != ConfigVersion { if cfg.V != ConfigVersion {

View File

@ -3,28 +3,41 @@ package controller
import ( import (
"github.com/go-openapi/runtime/middleware" "github.com/go-openapi/runtime/middleware"
"github.com/openziti/zrok/build" "github.com/openziti/zrok/build"
"github.com/openziti/zrok/controller/config"
"github.com/openziti/zrok/rest_model_zrok" "github.com/openziti/zrok/rest_model_zrok"
"github.com/openziti/zrok/rest_server_zrok/operations/metadata" "github.com/openziti/zrok/rest_server_zrok/operations/metadata"
) )
type configurationHandler struct { type configurationHandler struct {
cfg *Config cfg *config.Config
} }
func newConfigurationHandler(cfg *Config) *configurationHandler { func newConfigurationHandler(cfg *config.Config) *configurationHandler {
return &configurationHandler{ return &configurationHandler{
cfg: cfg, cfg: cfg,
} }
} }
func (ch *configurationHandler) Handle(_ metadata.ConfigurationParams) middleware.Responder { func (ch *configurationHandler) Handle(_ metadata.ConfigurationParams) middleware.Responder {
tou := ""
if cfg.Admin != nil {
tou = cfg.Admin.TouLink
}
data := &rest_model_zrok.Configuration{ data := &rest_model_zrok.Configuration{
Version: build.String(), Version: build.String(),
TouLink: tou, InvitesOpen: cfg.Invites != nil && cfg.Invites.InvitesOpen,
RequiresInviteToken: cfg.Invites != nil && cfg.Invites.TokenStrategy == "store",
}
if cfg.Admin != nil {
data.TouLink = cfg.Admin.TouLink
}
if cfg.Invites != nil {
data.InviteTokenContact = cfg.Invites.TokenContact
}
if cfg.Passwords != nil {
data.PasswordRequirements = &rest_model_zrok.PasswordRequirements{
Length: int64(cfg.Passwords.Length),
RequireCapital: cfg.Passwords.RequireCapital,
RequireNumeric: cfg.Passwords.RequireNumeric,
RequireSpecial: cfg.Passwords.RequireSpecial,
ValidSpecialCharacters: cfg.Passwords.ValidSpecialCharacters,
}
} }
return metadata.NewConfigurationOK().WithPayload(data) return metadata.NewConfigurationOK().WithPayload(data)
} }

View File

@ -2,6 +2,13 @@ package controller
import ( import (
"context" "context"
"github.com/openziti/zrok/controller/config"
"github.com/openziti/zrok/controller/limits"
"github.com/openziti/zrok/controller/metrics"
"github.com/sirupsen/logrus"
"log"
"net/http"
_ "net/http/pprof"
"github.com/go-openapi/loads" "github.com/go-openapi/loads"
influxdb2 "github.com/influxdata/influxdb-client-go/v2" influxdb2 "github.com/influxdata/influxdb-client-go/v2"
@ -13,14 +20,20 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
) )
var cfg *Config var cfg *config.Config
var str *store.Store var str *store.Store
var mtr *metricsAgent
var idb influxdb2.Client var idb influxdb2.Client
var limitsAgent *limits.Agent
func Run(inCfg *Config) error { func Run(inCfg *config.Config) error {
cfg = inCfg cfg = inCfg
if cfg.Admin != nil && cfg.Admin.ProfileEndpoint != "" {
go func() {
log.Println(http.ListenAndServe(cfg.Admin.ProfileEndpoint, nil))
}()
}
swaggerSpec, err := loads.Embedded(rest_server_zrok.SwaggerJSON, rest_server_zrok.FlatSwaggerJSON) swaggerSpec, err := loads.Embedded(rest_server_zrok.SwaggerJSON, rest_server_zrok.FlatSwaggerJSON)
if err != nil { if err != nil {
return errors.Wrap(err, "error loading embedded swagger spec") return errors.Wrap(err, "error loading embedded swagger spec")
@ -30,8 +43,8 @@ func Run(inCfg *Config) error {
api.KeyAuth = newZrokAuthenticator(cfg).authenticate api.KeyAuth = newZrokAuthenticator(cfg).authenticate
api.AccountInviteHandler = newInviteHandler(cfg) api.AccountInviteHandler = newInviteHandler(cfg)
api.AccountLoginHandler = account.LoginHandlerFunc(loginHandler) api.AccountLoginHandler = account.LoginHandlerFunc(loginHandler)
api.AccountRegisterHandler = newRegisterHandler() api.AccountRegisterHandler = newRegisterHandler(cfg)
api.AccountResetPasswordHandler = newResetPasswordHandler() api.AccountResetPasswordHandler = newResetPasswordHandler(cfg)
api.AccountResetPasswordRequestHandler = newResetPasswordRequestHandler() api.AccountResetPasswordRequestHandler = newResetPasswordRequestHandler()
api.AccountVerifyHandler = newVerifyHandler() api.AccountVerifyHandler = newVerifyHandler()
api.AdminCreateFrontendHandler = newCreateFrontendHandler() api.AdminCreateFrontendHandler = newCreateFrontendHandler()
@ -40,15 +53,22 @@ func Run(inCfg *Config) error {
api.AdminInviteTokenGenerateHandler = newInviteTokenGenerateHandler() api.AdminInviteTokenGenerateHandler = newInviteTokenGenerateHandler()
api.AdminListFrontendsHandler = newListFrontendsHandler() api.AdminListFrontendsHandler = newListFrontendsHandler()
api.AdminUpdateFrontendHandler = newUpdateFrontendHandler() api.AdminUpdateFrontendHandler = newUpdateFrontendHandler()
api.EnvironmentEnableHandler = newEnableHandler(cfg.Limits) api.EnvironmentEnableHandler = newEnableHandler()
api.EnvironmentDisableHandler = newDisableHandler() api.EnvironmentDisableHandler = newDisableHandler()
api.MetadataGetAccountDetailHandler = newAccountDetailHandler()
api.MetadataConfigurationHandler = newConfigurationHandler(cfg) api.MetadataConfigurationHandler = newConfigurationHandler(cfg)
if cfg.Metrics != nil && cfg.Metrics.Influx != nil {
api.MetadataGetAccountMetricsHandler = newGetAccountMetricsHandler(cfg.Metrics.Influx)
api.MetadataGetEnvironmentMetricsHandler = newGetEnvironmentMetricsHandler(cfg.Metrics.Influx)
api.MetadataGetShareMetricsHandler = newGetShareMetricsHandler(cfg.Metrics.Influx)
}
api.MetadataGetEnvironmentDetailHandler = newEnvironmentDetailHandler() api.MetadataGetEnvironmentDetailHandler = newEnvironmentDetailHandler()
api.MetadataGetFrontendDetailHandler = newGetFrontendDetailHandler()
api.MetadataGetShareDetailHandler = newShareDetailHandler() api.MetadataGetShareDetailHandler = newShareDetailHandler()
api.MetadataOverviewHandler = metadata.OverviewHandlerFunc(overviewHandler) api.MetadataOverviewHandler = newOverviewHandler()
api.MetadataVersionHandler = metadata.VersionHandlerFunc(versionHandler) api.MetadataVersionHandler = metadata.VersionHandlerFunc(versionHandler)
api.ShareAccessHandler = newAccessHandler() api.ShareAccessHandler = newAccessHandler()
api.ShareShareHandler = newShareHandler(cfg.Limits) api.ShareShareHandler = newShareHandler()
api.ShareUnaccessHandler = newUnaccessHandler() api.ShareUnaccessHandler = newUnaccessHandler()
api.ShareUnshareHandler = newUnshareHandler() api.ShareUnshareHandler = newUnshareHandler()
api.ShareUpdateShareHandler = newUpdateShareHandler() api.ShareUpdateShareHandler = newUpdateShareHandler()
@ -63,17 +83,31 @@ func Run(inCfg *Config) error {
return errors.Wrap(err, "error opening store") return errors.Wrap(err, "error opening store")
} }
if cfg.Influx != nil { if cfg.Metrics != nil && cfg.Metrics.Influx != nil {
idb = influxdb2.NewClient(cfg.Influx.Url, cfg.Influx.Token) idb = influxdb2.NewClient(cfg.Metrics.Influx.Url, cfg.Metrics.Influx.Token)
} else {
logrus.Warn("skipping influx client; no configuration")
} }
if cfg.Metrics != nil { if cfg.Metrics != nil && cfg.Metrics.Agent != nil && cfg.Metrics.Influx != nil {
mtr = newMetricsAgent() ma, err := metrics.NewAgent(cfg.Metrics.Agent, str, cfg.Metrics.Influx)
go mtr.run() if err != nil {
defer func() { return errors.Wrap(err, "error creating metrics agent")
mtr.stop() }
mtr.join() if err := ma.Start(); err != nil {
}() return errors.Wrap(err, "error starting metrics agent")
}
defer func() { ma.Stop() }()
if cfg.Limits != nil && cfg.Limits.Enforcing {
limitsAgent, err = limits.NewAgent(cfg.Limits, cfg.Metrics.Influx, cfg.Ziti, cfg.Email, str)
if err != nil {
return errors.Wrap(err, "error creating limits agent")
}
ma.AddUsageSink(limitsAgent)
limitsAgent.Start()
defer func() { limitsAgent.Stop() }()
}
} }
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())

View File

@ -25,7 +25,7 @@ func (h *createFrontendHandler) Handle(params admin.CreateFrontendParams, princi
return admin.NewCreateFrontendUnauthorized() return admin.NewCreateFrontendUnauthorized()
} }
client, err := edgeClient() client, err := zrokEdgeSdk.Client(cfg.Ziti)
if err != nil { if err != nil {
logrus.Errorf("error getting edge client: %v", err) logrus.Errorf("error getting edge client: %v", err)
return admin.NewCreateFrontendInternalServerError() return admin.NewCreateFrontendInternalServerError()

View File

@ -3,15 +3,12 @@ package controller
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt"
"github.com/go-openapi/runtime/middleware" "github.com/go-openapi/runtime/middleware"
"github.com/openziti/edge/rest_management_api_client/service" rest_model_edge "github.com/openziti/edge-api/rest_model"
rest_model_edge "github.com/openziti/edge/rest_model"
"github.com/openziti/zrok/controller/zrokEdgeSdk" "github.com/openziti/zrok/controller/zrokEdgeSdk"
"github.com/openziti/zrok/rest_model_zrok" "github.com/openziti/zrok/rest_model_zrok"
"github.com/openziti/zrok/rest_server_zrok/operations/admin" "github.com/openziti/zrok/rest_server_zrok/operations/admin"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"time"
) )
type createIdentityHandler struct{} type createIdentityHandler struct{}
@ -28,7 +25,7 @@ func (h *createIdentityHandler) Handle(params admin.CreateIdentityParams, princi
return admin.NewCreateIdentityUnauthorized() return admin.NewCreateIdentityUnauthorized()
} }
edge, err := edgeClient() edge, err := zrokEdgeSdk.Client(cfg.Ziti)
if err != nil { if err != nil {
logrus.Errorf("error getting edge client: %v", err) logrus.Errorf("error getting edge client: %v", err)
return admin.NewCreateIdentityInternalServerError() return admin.NewCreateIdentityInternalServerError()
@ -52,32 +49,6 @@ func (h *createIdentityHandler) Handle(params admin.CreateIdentityParams, princi
return admin.NewCreateIdentityInternalServerError() return admin.NewCreateIdentityInternalServerError()
} }
filter := fmt.Sprintf("name=\"%v\" and tags.zrok != null", cfg.Metrics.ServiceName)
limit := int64(0)
offset := int64(0)
listSvcReq := &service.ListServicesParams{
Filter: &filter,
Limit: &limit,
Offset: &offset,
}
listSvcReq.SetTimeout(30 * time.Second)
listSvcResp, err := edge.Service.ListServices(listSvcReq, nil)
if err != nil {
logrus.Errorf("error listing metrics service: %v", err)
return admin.NewCreateIdentityInternalServerError()
}
if len(listSvcResp.Payload.Data) != 1 {
logrus.Errorf("could not find metrics service")
return admin.NewCreateIdentityInternalServerError()
}
svcZId := *listSvcResp.Payload.Data[0].ID
spName := fmt.Sprintf("%v-%v-dial", name, cfg.Metrics.ServiceName)
if err := zrokEdgeSdk.CreateServicePolicyDial(spName, svcZId, []string{zId}, nil, edge); err != nil {
logrus.Errorf("error creating named dial service policy '%v': %v", spName, err)
return admin.NewCreateIdentityInternalServerError()
}
var out bytes.Buffer var out bytes.Buffer
enc := json.NewEncoder(&out) enc := json.NewEncoder(&out)
enc.SetEscapeHTML(false) enc.SetEscapeHTML(false)

View File

@ -4,7 +4,7 @@ import (
"fmt" "fmt"
"github.com/go-openapi/runtime/middleware" "github.com/go-openapi/runtime/middleware"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
"github.com/openziti/edge/rest_management_api_client" "github.com/openziti/edge-api/rest_management_api_client"
"github.com/openziti/zrok/controller/zrokEdgeSdk" "github.com/openziti/zrok/controller/zrokEdgeSdk"
"github.com/openziti/zrok/rest_model_zrok" "github.com/openziti/zrok/rest_model_zrok"
"github.com/openziti/zrok/rest_server_zrok/operations/environment" "github.com/openziti/zrok/rest_server_zrok/operations/environment"
@ -36,7 +36,11 @@ func (h *disableHandler) Handle(params environment.DisableParams, principal *res
logrus.Errorf("error getting environment for user '%v': %v", principal.Email, err) logrus.Errorf("error getting environment for user '%v': %v", principal.Email, err)
return environment.NewDisableInternalServerError() return environment.NewDisableInternalServerError()
} }
edge, err := edgeClient() if env.Deleted {
logrus.Errorf("environment '%v' for user '%v' deleted", env.ZId, principal.Email)
return environment.NewDisableUnauthorized()
}
edge, err := zrokEdgeSdk.Client(cfg.Ziti)
if err != nil { if err != nil {
logrus.Errorf("error getting edge client for user '%v': %v", principal.Email, err) logrus.Errorf("error getting edge client for user '%v': %v", principal.Email, err)
return environment.NewDisableInternalServerError() return environment.NewDisableInternalServerError()
@ -85,29 +89,31 @@ func (h *disableHandler) removeSharesForEnvironment(envId int, tx *sqlx.Tx, edge
if err != nil { if err != nil {
return err return err
} }
shrs, err := str.FindSharesForEnvironment(envId, tx) if !env.Deleted {
if err != nil { shrs, err := str.FindSharesForEnvironment(envId, tx)
return err if err != nil {
} return err
for _, shr := range shrs {
shrToken := shr.Token
logrus.Infof("garbage collecting share '%v' for environment '%v'", shrToken, env.ZId)
if err := zrokEdgeSdk.DeleteServiceEdgeRouterPolicy(env.ZId, shrToken, edge); err != nil {
logrus.Error(err)
} }
if err := zrokEdgeSdk.DeleteServicePolicyDial(env.ZId, shrToken, edge); err != nil { for _, shr := range shrs {
logrus.Error(err) shrToken := shr.Token
logrus.Infof("garbage collecting share '%v' for environment '%v'", shrToken, env.ZId)
if err := zrokEdgeSdk.DeleteServiceEdgeRouterPolicy(env.ZId, shrToken, edge); err != nil {
logrus.Error(err)
}
if err := zrokEdgeSdk.DeleteServicePoliciesDial(env.ZId, shrToken, edge); err != nil {
logrus.Error(err)
}
if err := zrokEdgeSdk.DeleteServicePoliciesBind(env.ZId, shrToken, edge); err != nil {
logrus.Error(err)
}
if err := zrokEdgeSdk.DeleteConfig(env.ZId, shrToken, edge); err != nil {
logrus.Error(err)
}
if err := zrokEdgeSdk.DeleteService(env.ZId, shr.ZId, edge); err != nil {
logrus.Error(err)
}
logrus.Infof("removed share '%v' for environment '%v'", shr.Token, env.ZId)
} }
if err := zrokEdgeSdk.DeleteServicePolicyBind(env.ZId, shrToken, edge); err != nil {
logrus.Error(err)
}
if err := zrokEdgeSdk.DeleteConfig(env.ZId, shrToken, edge); err != nil {
logrus.Error(err)
}
if err := zrokEdgeSdk.DeleteService(env.ZId, shr.ZId, edge); err != nil {
logrus.Error(err)
}
logrus.Infof("removed share '%v' for environment '%v'", shr.Token, env.ZId)
} }
return nil return nil
} }
@ -117,13 +123,15 @@ func (h *disableHandler) removeFrontendsForEnvironment(envId int, tx *sqlx.Tx, e
if err != nil { if err != nil {
return err return err
} }
fes, err := str.FindFrontendsForEnvironment(envId, tx) if !env.Deleted {
if err != nil { fes, err := str.FindFrontendsForEnvironment(envId, tx)
return err if err != nil {
} return err
for _, fe := range fes { }
if err := zrokEdgeSdk.DeleteServicePolicy(env.ZId, fmt.Sprintf("tags.zrokFrontendToken=\"%v\" and type=1", fe.Token), edge); err != nil { for _, fe := range fes {
logrus.Errorf("error removing frontend access for '%v': %v", fe.Token, err) if err := zrokEdgeSdk.DeleteServicePolicies(env.ZId, fmt.Sprintf("tags.zrokFrontendToken=\"%v\" and type=1", fe.Token), edge); err != nil {
logrus.Errorf("error removing frontend access for '%v': %v", fe.Token, err)
}
} }
} }
return nil return nil

View File

@ -0,0 +1,9 @@
package emailUi
type Config struct {
Host string
Port int
Username string
Password string `cf:"+secret"`
From string
}

View File

@ -2,5 +2,5 @@ package emailUi
import "embed" import "embed"
//go:embed verify.gohtml verify.gotext resetPassword.gohtml resetPassword.gotext //go:embed verify.gohtml verify.gotext resetPassword.gohtml resetPassword.gotext limitWarning.gohtml limitWarning.gotext
var FS embed.FS var FS embed.FS

View File

@ -0,0 +1,156 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>Transfer limit warning!</title>
<meta name="description" content="zrok Transfer Limit Warning">
<meta name="viewport" content="width=device-width">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono&display=swap" rel="stylesheet">
<style>
body {
margin: 0;
padding: 25;
font-family: 'JetBrains Mono', 'Courier New', monospace;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #ffffff;
background-color: #3b2693;
}
a:link {
color: #00d7e4;
}
a:visited {
color: #00d7e4;
}
a:hover,
a:active {
color: #ff0100;
}
.claim {
font-size: 2em;
margin: 0.5em 0 1em 0;
}
.container {
width: 62em;
margin: 2em auto;
max-width: 100%;
text-align: center;
}
.btn {
display: inline-block;
margin: .25em;
padding: 10px 16px;
font-size: 1.15em;
line-height: 1.33;
border-radius: 6px;
text-align: center;
white-space: nowrap;
vertical-align: middle;
text-decoration: none;
}
.btn-primary {
color: #ffffff;
background-color: #ff0100;
border-color: #ff0100;
}
a.btn-primary:link,
a.btn-primary:visited {
color: #ffffff;
}
a.btn-primary:hover,
a.btn-primary:active {
background-color: #cf0100;
}
.btn-secondary {
background-color: #b3b3b3;
border-color: #b3b3b3;
color: #252525;
font-weight: 800;
}
a.btn-secondary:link,
a.btn-secondary:visited {
color: #666;
}
a.btn-secondary:hover,
a.btn-secondary:hover {
background-color: #ccc;
color: #333;
}
.about {
margin: 1em auto;
}
.about td {
text-align: left;
}
.about td:first-child {
width: 80px;
}
@media screen and (max-width: 600px) {
img {
height: auto !important;
}
}
@media screen and (max-width: 400px) {
body {
font-size: 14px;
}
}
@media screen and (max-width: 320px) {
body {
font-size: 12px;
}
}
</style>
</head>
<body style="font-family: 'JetBrains Mono', 'Courier New', monospace; color: #ffffff; background-color: #3b2693; font-weight: 600;">
<div class="container">
<div class="banner" style="margin: auto;">
<img src="https://zrok.io/wp-content/uploads/2023/03/warning.jpg" width="363px" height="500px" style="padding-bottom: 10px;"/>
</div>
<div class="cta" style="text-align: center;">
<h3 style="text-align: center;">Your account is reaching a transfer limit, {{ .EmailAddress }}.</h3>
</div>
<div>
{{ .Detail }}
</div>
<table border="0" cellpadding="0" cellspacing="0" align="center" class="about">
<tr>
<td><a href="https://github.com/openziti/zrok" target="_blank">github.com/openziti/zrok</a></td>
</tr>
<tr>
<td>{{ .Version }}</td>
</tr>
</table>
<p style="text-align: center;"></a>Copyright © 2023 <a href="http://www.netfoundry.io" target="_blank" style="color: #00d7e4;">NetFoundry, Inc.</a></p>
</div>
</body>
</html>

View File

@ -0,0 +1,3 @@
Your account is nearing a transfer size limit, {{ .EmailAddress }}!
{{ .Detail }}

View File

@ -0,0 +1,25 @@
package emailUi
import (
"bytes"
"github.com/pkg/errors"
"text/template"
)
type WarningEmail struct {
EmailAddress string
Detail string
Version string
}
func (we WarningEmail) MergeTemplate(filename string) (string, error) {
t, err := template.ParseFS(FS, filename)
if err != nil {
return "", errors.Wrapf(err, "error parsing warning email template '%v'", filename)
}
buf := new(bytes.Buffer)
if err := t.Execute(buf, we); err != nil {
return "", errors.Wrapf(err, "error executing warning email template '%v'", filename)
}
return buf.String(), nil
}

View File

@ -13,29 +13,27 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
type enableHandler struct { type enableHandler struct{}
cfg *LimitsConfig
}
func newEnableHandler(cfg *LimitsConfig) *enableHandler { func newEnableHandler() *enableHandler {
return &enableHandler{cfg: cfg} return &enableHandler{}
} }
func (h *enableHandler) Handle(params environment.EnableParams, principal *rest_model_zrok.Principal) middleware.Responder { func (h *enableHandler) Handle(params environment.EnableParams, principal *rest_model_zrok.Principal) middleware.Responder {
// start transaction early; if it fails, don't bother creating ziti resources // start transaction early; if it fails, don't bother creating ziti resources
tx, err := str.Begin() trx, err := str.Begin()
if err != nil { if err != nil {
logrus.Errorf("error starting transaction for user '%v': %v", principal.Email, err) logrus.Errorf("error starting transaction for user '%v': %v", principal.Email, err)
return environment.NewEnableInternalServerError() return environment.NewEnableInternalServerError()
} }
defer func() { _ = tx.Rollback() }() defer func() { _ = trx.Rollback() }()
if err := h.checkLimits(principal, tx); err != nil { if err := h.checkLimits(principal, trx); err != nil {
logrus.Errorf("limits error for user '%v': %v", principal.Email, err) logrus.Errorf("limits error for user '%v': %v", principal.Email, err)
return environment.NewEnableUnauthorized() return environment.NewEnableUnauthorized()
} }
client, err := edgeClient() client, err := zrokEdgeSdk.Client(cfg.Ziti)
if err != nil { if err != nil {
logrus.Errorf("error getting edge client for user '%v': %v", principal.Email, err) logrus.Errorf("error getting edge client for user '%v': %v", principal.Email, err)
return environment.NewEnableInternalServerError() return environment.NewEnableInternalServerError()
@ -70,14 +68,14 @@ func (h *enableHandler) Handle(params environment.EnableParams, principal *rest_
Host: params.Body.Host, Host: params.Body.Host,
Address: realRemoteAddress(params.HTTPRequest), Address: realRemoteAddress(params.HTTPRequest),
ZId: envZId, ZId: envZId,
}, tx) }, trx)
if err != nil { if err != nil {
logrus.Errorf("error storing created identity for user '%v': %v", principal.Email, err) logrus.Errorf("error storing created identity for user '%v': %v", principal.Email, err)
_ = tx.Rollback() _ = trx.Rollback()
return environment.NewEnableInternalServerError() return environment.NewEnableInternalServerError()
} }
if err := tx.Commit(); err != nil { if err := trx.Commit(); err != nil {
logrus.Errorf("error committing for user '%v': %v", principal.Email, err) logrus.Errorf("error committing for user '%v': %v", principal.Email, err)
return environment.NewEnableInternalServerError() return environment.NewEnableInternalServerError()
} }
@ -99,14 +97,16 @@ func (h *enableHandler) Handle(params environment.EnableParams, principal *rest_
return resp return resp
} }
func (h *enableHandler) checkLimits(principal *rest_model_zrok.Principal, tx *sqlx.Tx) error { func (h *enableHandler) checkLimits(principal *rest_model_zrok.Principal, trx *sqlx.Tx) error {
if !principal.Limitless && h.cfg.Environments > Unlimited { if !principal.Limitless {
envs, err := str.FindEnvironmentsForAccount(int(principal.ID), tx) if limitsAgent != nil {
if err != nil { ok, err := limitsAgent.CanCreateEnvironment(int(principal.ID), trx)
return errors.Errorf("unable to find environments for account '%v': %v", principal.Email, err) if err != nil {
} return errors.Wrapf(err, "error checking environment limits for '%v'", principal.Email)
if len(envs)+1 > h.cfg.Environments { }
return errors.Errorf("would exceed environments limit of %d for '%v'", h.cfg.Environments, principal.Email) if !ok {
return errors.Errorf("environment limit check failed for '%v'", principal.Email)
}
} }
} }
return nil return nil

14
controller/env/cf.go vendored Normal file
View File

@ -0,0 +1,14 @@
package env
import (
"github.com/michaelquigley/cf"
)
var cfOpts *cf.Options
func GetCfOptions() *cf.Options {
if cfOpts == nil {
cfOpts = cf.DefaultOptions()
}
return cfOpts
}

View File

@ -25,7 +25,7 @@ func (h *environmentDetailHandler) Handle(params metadata.GetEnvironmentDetailPa
logrus.Errorf("environment '%v' not found for account '%v': %v", params.EnvZID, principal.Email, err) logrus.Errorf("environment '%v' not found for account '%v': %v", params.EnvZID, principal.Email, err)
return metadata.NewGetEnvironmentDetailNotFound() return metadata.NewGetEnvironmentDetailNotFound()
} }
es := &rest_model_zrok.EnvironmentShares{ es := &rest_model_zrok.EnvironmentAndResources{
Environment: &rest_model_zrok.Environment{ Environment: &rest_model_zrok.Environment{
Address: senv.Address, Address: senv.Address,
CreatedAt: senv.CreatedAt.UnixMilli(), CreatedAt: senv.CreatedAt.UnixMilli(),
@ -40,12 +40,15 @@ func (h *environmentDetailHandler) Handle(params metadata.GetEnvironmentDetailPa
logrus.Errorf("error finding shares for environment '%v' for user '%v': %v", senv.ZId, principal.Email, err) logrus.Errorf("error finding shares for environment '%v' for user '%v': %v", senv.ZId, principal.Email, err)
return metadata.NewGetEnvironmentDetailInternalServerError() return metadata.NewGetEnvironmentDetailInternalServerError()
} }
var sparkData map[string][]int64 sparkRx := make(map[string][]int64)
if cfg.Influx != nil { sparkTx := make(map[string][]int64)
sparkData, err = sparkDataForShares(shrs) if cfg.Metrics != nil && cfg.Metrics.Influx != nil {
sparkRx, sparkTx, err = sparkDataForShares(shrs)
if err != nil { if err != nil {
logrus.Errorf("error querying spark data for shares for user '%v': %v", principal.Email, err) logrus.Errorf("error querying spark data for shares for user '%v': %v", principal.Email, err)
} }
} else {
logrus.Debug("skipping spark data for shares; no influx configuration")
} }
for _, shr := range shrs { for _, shr := range shrs {
feEndpoint := "" feEndpoint := ""
@ -60,6 +63,10 @@ func (h *environmentDetailHandler) Handle(params metadata.GetEnvironmentDetailPa
if shr.BackendProxyEndpoint != nil { if shr.BackendProxyEndpoint != nil {
beProxyEndpoint = *shr.BackendProxyEndpoint beProxyEndpoint = *shr.BackendProxyEndpoint
} }
var sparkData []*rest_model_zrok.SparkDataSample
for i := 0; i < len(sparkRx[shr.Token]) && i < len(sparkTx[shr.Token]); i++ {
sparkData = append(sparkData, &rest_model_zrok.SparkDataSample{Rx: float64(sparkRx[shr.Token][i]), Tx: float64(sparkTx[shr.Token][i])})
}
es.Shares = append(es.Shares, &rest_model_zrok.Share{ es.Shares = append(es.Shares, &rest_model_zrok.Share{
Token: shr.Token, Token: shr.Token,
ZID: shr.ZId, ZID: shr.ZId,
@ -69,7 +76,7 @@ func (h *environmentDetailHandler) Handle(params metadata.GetEnvironmentDetailPa
FrontendEndpoint: feEndpoint, FrontendEndpoint: feEndpoint,
BackendProxyEndpoint: beProxyEndpoint, BackendProxyEndpoint: beProxyEndpoint,
Reserved: shr.Reserved, Reserved: shr.Reserved,
Metrics: sparkData[shr.Token], Activity: sparkData,
CreatedAt: shr.CreatedAt.UnixMilli(), CreatedAt: shr.CreatedAt.UnixMilli(),
UpdatedAt: shr.UpdatedAt.UnixMilli(), UpdatedAt: shr.UpdatedAt.UnixMilli(),
}) })

View File

@ -0,0 +1,63 @@
package controller
import (
"github.com/go-openapi/runtime/middleware"
"github.com/openziti/zrok/rest_model_zrok"
"github.com/openziti/zrok/rest_server_zrok/operations/metadata"
"github.com/sirupsen/logrus"
)
type getFrontendDetailHandler struct{}
func newGetFrontendDetailHandler() *getFrontendDetailHandler {
return &getFrontendDetailHandler{}
}
func (h *getFrontendDetailHandler) Handle(params metadata.GetFrontendDetailParams, principal *rest_model_zrok.Principal) middleware.Responder {
trx, err := str.Begin()
if err != nil {
logrus.Errorf("error starting transaction: %v", err)
return metadata.NewGetFrontendDetailInternalServerError()
}
defer func() { _ = trx.Rollback() }()
fe, err := str.GetFrontend(int(params.FeID), trx)
if err != nil {
logrus.Errorf("error finding share '%d': %v", params.FeID, err)
return metadata.NewGetFrontendDetailNotFound()
}
envs, err := str.FindEnvironmentsForAccount(int(principal.ID), trx)
if err != nil {
logrus.Errorf("error finding environments for account '%v': %v", principal.Email, err)
return metadata.NewGetFrontendDetailInternalServerError()
}
found := false
if fe.EnvironmentId == nil {
logrus.Errorf("non owned environment '%d' for '%v'", fe.Id, principal.Email)
return metadata.NewGetFrontendDetailNotFound()
}
for _, env := range envs {
if *fe.EnvironmentId == env.Id {
found = true
break
}
}
if !found {
logrus.Errorf("environment not matched for frontend '%d' for account '%v'", fe.Id, principal.Email)
return metadata.NewGetFrontendDetailNotFound()
}
payload := &rest_model_zrok.Frontend{
ID: int64(fe.Id),
ZID: fe.ZId,
CreatedAt: fe.CreatedAt.UnixMilli(),
UpdatedAt: fe.UpdatedAt.UnixMilli(),
}
if fe.PrivateShareId != nil {
shr, err := str.GetShare(*fe.PrivateShareId, trx)
if err != nil {
logrus.Errorf("error getting share for frontend '%d': %v", fe.Id, err)
return metadata.NewGetFrontendDetailInternalServerError()
}
payload.ShrToken = shr.Token
}
return metadata.NewGetFrontendDetailOK().WithPayload(payload)
}

View File

@ -3,11 +3,12 @@ package controller
import ( import (
"context" "context"
"fmt" "fmt"
"github.com/openziti/edge/rest_management_api_client" "github.com/openziti/edge-api/rest_management_api_client"
"github.com/openziti/edge/rest_management_api_client/config" "github.com/openziti/edge-api/rest_management_api_client/config"
"github.com/openziti/edge/rest_management_api_client/service" "github.com/openziti/edge-api/rest_management_api_client/service"
"github.com/openziti/edge/rest_management_api_client/service_edge_router_policy" "github.com/openziti/edge-api/rest_management_api_client/service_edge_router_policy"
"github.com/openziti/edge/rest_management_api_client/service_policy" "github.com/openziti/edge-api/rest_management_api_client/service_policy"
zrok_config "github.com/openziti/zrok/controller/config"
"github.com/openziti/zrok/controller/store" "github.com/openziti/zrok/controller/store"
"github.com/openziti/zrok/controller/zrokEdgeSdk" "github.com/openziti/zrok/controller/zrokEdgeSdk"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -16,7 +17,7 @@ import (
"time" "time"
) )
func GC(inCfg *Config) error { func GC(inCfg *zrok_config.Config) error {
cfg = inCfg cfg = inCfg
if v, err := store.Open(cfg.Store); err == nil { if v, err := store.Open(cfg.Store); err == nil {
str = v str = v
@ -28,7 +29,7 @@ func GC(inCfg *Config) error {
logrus.Errorf("error closing store: %v", err) logrus.Errorf("error closing store: %v", err)
} }
}() }()
edge, err := edgeClient() edge, err := zrokEdgeSdk.Client(cfg.Ziti)
if err != nil { if err != nil {
return err return err
} }
@ -37,7 +38,7 @@ func GC(inCfg *Config) error {
return err return err
} }
defer func() { _ = tx.Rollback() }() defer func() { _ = tx.Rollback() }()
sshrs, err := str.GetAllShares(tx) sshrs, err := str.FindAllShares(tx)
if err != nil { if err != nil {
return err return err
} }
@ -75,10 +76,10 @@ func gcServices(edge *rest_management_api_client.ZitiEdgeManagement, liveMap map
if err := zrokEdgeSdk.DeleteServiceEdgeRouterPolicy("gc", *svc.Name, edge); err != nil { if err := zrokEdgeSdk.DeleteServiceEdgeRouterPolicy("gc", *svc.Name, edge); err != nil {
logrus.Errorf("error garbage collecting service edge router policy: %v", err) logrus.Errorf("error garbage collecting service edge router policy: %v", err)
} }
if err := zrokEdgeSdk.DeleteServicePolicyDial("gc", *svc.Name, edge); err != nil { if err := zrokEdgeSdk.DeleteServicePoliciesDial("gc", *svc.Name, edge); err != nil {
logrus.Errorf("error garbage collecting service dial policy: %v", err) logrus.Errorf("error garbage collecting service dial policy: %v", err)
} }
if err := zrokEdgeSdk.DeleteServicePolicyBind("gc", *svc.Name, edge); err != nil { if err := zrokEdgeSdk.DeleteServicePoliciesBind("gc", *svc.Name, edge); err != nil {
logrus.Errorf("error garbage collecting service bind policy: %v", err) logrus.Errorf("error garbage collecting service bind policy: %v", err)
} }
if err := zrokEdgeSdk.DeleteConfig("gc", *svc.Name, edge); err != nil { if err := zrokEdgeSdk.DeleteConfig("gc", *svc.Name, edge); err != nil {
@ -136,7 +137,7 @@ func gcServicePolicies(edge *rest_management_api_client.ZitiEdgeManagement, live
if _, found := liveMap[spName]; !found { if _, found := liveMap[spName]; !found {
logrus.Infof("garbage collecting, svcId='%v'", spName) logrus.Infof("garbage collecting, svcId='%v'", spName)
deleteFilter := fmt.Sprintf("id=\"%v\"", *sp.ID) deleteFilter := fmt.Sprintf("id=\"%v\"", *sp.ID)
if err := zrokEdgeSdk.DeleteServicePolicy("gc", deleteFilter, edge); err != nil { if err := zrokEdgeSdk.DeleteServicePolicies("gc", deleteFilter, edge); err != nil {
logrus.Errorf("error garbage collecting service policy: %v", err) logrus.Errorf("error garbage collecting service policy: %v", err)
} }
} else { } else {

View File

@ -2,24 +2,28 @@ package controller
import ( import (
"github.com/go-openapi/runtime/middleware" "github.com/go-openapi/runtime/middleware"
"github.com/openziti/zrok/controller/config"
"github.com/openziti/zrok/controller/store" "github.com/openziti/zrok/controller/store"
"github.com/openziti/zrok/rest_model_zrok"
"github.com/openziti/zrok/rest_server_zrok/operations/account" "github.com/openziti/zrok/rest_server_zrok/operations/account"
"github.com/openziti/zrok/util" "github.com/openziti/zrok/util"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
type inviteHandler struct { type inviteHandler struct {
cfg *Config cfg *config.Config
} }
func newInviteHandler(cfg *Config) *inviteHandler { func newInviteHandler(cfg *config.Config) *inviteHandler {
return &inviteHandler{ return &inviteHandler{
cfg: cfg, cfg: cfg,
} }
} }
func (self *inviteHandler) Handle(params account.InviteParams) middleware.Responder { func (h *inviteHandler) Handle(params account.InviteParams) middleware.Responder {
if h.cfg.Invites == nil || !h.cfg.Invites.InvitesOpen {
logrus.Warnf("not accepting invites; attempt from '%v'", params.Body.Email)
return account.NewInviteBadRequest()
}
if params.Body == nil || params.Body.Email == "" { if params.Body == nil || params.Body.Email == "" {
logrus.Errorf("missing email") logrus.Errorf("missing email")
return account.NewInviteBadRequest() return account.NewInviteBadRequest()
@ -38,11 +42,11 @@ func (self *inviteHandler) Handle(params account.InviteParams) middleware.Respon
} }
defer func() { _ = tx.Rollback() }() defer func() { _ = tx.Rollback() }()
if self.cfg.Registration != nil && self.cfg.Registration.TokenStrategy == "store" { if h.cfg.Invites != nil && h.cfg.Invites.TokenStrategy == "store" {
inviteToken, err := str.GetInviteTokenByToken(params.Body.Token, tx) inviteToken, err := str.FindInviteTokenByToken(params.Body.Token, tx)
if err != nil { if err != nil {
logrus.Errorf("cannot get invite token '%v' for '%v': %v", params.Body.Token, params.Body.Email, err) logrus.Errorf("cannot get invite token '%v' for '%v': %v", params.Body.Token, params.Body.Email, err)
return account.NewInviteBadRequest().WithPayload(rest_model_zrok.ErrorMessage("Missing invite token")) return account.NewInviteBadRequest().WithPayload("missing invite token")
} }
if err := str.DeleteInviteToken(inviteToken.Id, tx); err != nil { if err := str.DeleteInviteToken(inviteToken.Id, tx); err != nil {
logrus.Error(err) logrus.Error(err)
@ -62,9 +66,10 @@ func (self *inviteHandler) Handle(params account.InviteParams) middleware.Respon
SourceAddress: params.HTTPRequest.RemoteAddr, SourceAddress: params.HTTPRequest.RemoteAddr,
} }
if _, err := str.FindAccountWithEmail(params.Body.Email, tx); err == nil { // deleted accounts still exist as far as invites are concerned (ignore deleted flag)
if _, err := str.FindAccountWithEmailAndDeleted(params.Body.Email, tx); err == nil {
logrus.Errorf("found account for '%v', cannot process account request", params.Body.Email) logrus.Errorf("found account for '%v', cannot process account request", params.Body.Email)
return account.NewInviteBadRequest().WithPayload(rest_model_zrok.ErrorMessage("Duplicate email found")) return account.NewInviteBadRequest().WithPayload("duplicate email found")
} else { } else {
logrus.Infof("no account found for '%v': %v", params.Body.Email, err) logrus.Infof("no account found for '%v': %v", params.Body.Email, err)
} }

View File

@ -0,0 +1,48 @@
package limits
import (
"github.com/jmoiron/sqlx"
"github.com/openziti/zrok/controller/store"
"github.com/openziti/zrok/controller/zrokEdgeSdk"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
type accountLimitAction struct {
str *store.Store
zCfg *zrokEdgeSdk.Config
}
func newAccountLimitAction(str *store.Store, zCfg *zrokEdgeSdk.Config) *accountLimitAction {
return &accountLimitAction{str, zCfg}
}
func (a *accountLimitAction) HandleAccount(acct *store.Account, _, _ int64, _ *BandwidthPerPeriod, trx *sqlx.Tx) error {
logrus.Infof("limiting '%v'", acct.Email)
envs, err := a.str.FindEnvironmentsForAccount(acct.Id, trx)
if err != nil {
return errors.Wrapf(err, "error finding environments for account '%v'", acct.Email)
}
edge, err := zrokEdgeSdk.Client(a.zCfg)
if err != nil {
return err
}
for _, env := range envs {
shrs, err := a.str.FindSharesForEnvironment(env.Id, trx)
if err != nil {
return errors.Wrapf(err, "error finding shares for environment '%v'", env.ZId)
}
for _, shr := range shrs {
if err := zrokEdgeSdk.DeleteServicePoliciesDial(env.ZId, shr.Token, edge); err != nil {
return errors.Wrapf(err, "error deleting dial service policy for '%v'", shr.Token)
}
logrus.Infof("removed dial service policy for share '%v' of environment '%v'", shr.Token, env.ZId)
}
}
return nil
}

View File

@ -0,0 +1,54 @@
package limits
import (
"github.com/jmoiron/sqlx"
"github.com/openziti/zrok/controller/store"
"github.com/openziti/zrok/controller/zrokEdgeSdk"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
type accountRelaxAction struct {
str *store.Store
zCfg *zrokEdgeSdk.Config
}
func newAccountRelaxAction(str *store.Store, zCfg *zrokEdgeSdk.Config) *accountRelaxAction {
return &accountRelaxAction{str, zCfg}
}
func (a *accountRelaxAction) HandleAccount(acct *store.Account, _, _ int64, _ *BandwidthPerPeriod, trx *sqlx.Tx) error {
logrus.Infof("relaxing '%v'", acct.Email)
envs, err := a.str.FindEnvironmentsForAccount(acct.Id, trx)
if err != nil {
return errors.Wrapf(err, "error finding environments for account '%v'", acct.Email)
}
edge, err := zrokEdgeSdk.Client(a.zCfg)
if err != nil {
return err
}
for _, env := range envs {
shrs, err := a.str.FindSharesForEnvironment(env.Id, trx)
if err != nil {
return errors.Wrapf(err, "error finding shares for environment '%v'", env.ZId)
}
for _, shr := range shrs {
switch shr.ShareMode {
case "public":
if err := relaxPublicShare(a.str, edge, shr, trx); err != nil {
return errors.Wrap(err, "error relaxing public share")
}
case "private":
if err := relaxPrivateShare(a.str, edge, shr, trx); err != nil {
return errors.Wrap(err, "error relaxing private share")
}
}
}
}
return nil
}

View File

@ -0,0 +1,51 @@
package limits
import (
"github.com/jmoiron/sqlx"
"github.com/openziti/zrok/controller/emailUi"
"github.com/openziti/zrok/controller/store"
"github.com/openziti/zrok/util"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
type accountWarningAction struct {
str *store.Store
cfg *emailUi.Config
}
func newAccountWarningAction(cfg *emailUi.Config, str *store.Store) *accountWarningAction {
return &accountWarningAction{str, cfg}
}
func (a *accountWarningAction) HandleAccount(acct *store.Account, rxBytes, txBytes int64, limit *BandwidthPerPeriod, _ *sqlx.Tx) error {
logrus.Infof("warning '%v'", acct.Email)
if a.cfg != nil {
rxLimit := "(unlimited bytes)"
if limit.Limit.Rx != Unlimited {
rxLimit = util.BytesToSize(limit.Limit.Rx)
}
txLimit := "(unlimited bytes)"
if limit.Limit.Tx != Unlimited {
txLimit = util.BytesToSize(limit.Limit.Tx)
}
totalLimit := "(unlimited bytes)"
if limit.Limit.Total != Unlimited {
totalLimit = util.BytesToSize(limit.Limit.Total)
}
detail := newDetailMessage()
detail = detail.append("Your account has received %v and sent %v (for a total of %v), which has triggered a transfer limit warning.", util.BytesToSize(rxBytes), util.BytesToSize(txBytes), util.BytesToSize(rxBytes+txBytes))
detail = detail.append("This zrok instance only allows an account to receive %v, send %v, totalling not more than %v for each %v.", rxLimit, txLimit, totalLimit, limit.Period)
detail = detail.append("If you exceed the transfer limit, access to your shares will be temporarily disabled (until the last %v falls below the transfer limit)", limit.Period)
if err := sendLimitWarningEmail(a.cfg, acct.Email, detail); err != nil {
return errors.Wrapf(err, "error sending limit warning email to '%v'", acct.Email)
}
} else {
logrus.Warnf("skipping warning email for account limit; no email configuration specified")
}
return nil
}

732
controller/limits/agent.go Normal file
View File

@ -0,0 +1,732 @@
package limits
import (
"fmt"
"github.com/jmoiron/sqlx"
"github.com/openziti/zrok/controller/emailUi"
"github.com/openziti/zrok/controller/metrics"
"github.com/openziti/zrok/controller/store"
"github.com/openziti/zrok/controller/zrokEdgeSdk"
"github.com/openziti/zrok/util"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"reflect"
"time"
)
type Agent struct {
cfg *Config
ifx *influxReader
zCfg *zrokEdgeSdk.Config
str *store.Store
queue chan *metrics.Usage
acctWarningActions []AccountAction
acctLimitActions []AccountAction
acctRelaxActions []AccountAction
envWarningActions []EnvironmentAction
envLimitActions []EnvironmentAction
envRelaxActions []EnvironmentAction
shrWarningActions []ShareAction
shrLimitActions []ShareAction
shrRelaxActions []ShareAction
close chan struct{}
join chan struct{}
}
func NewAgent(cfg *Config, ifxCfg *metrics.InfluxConfig, zCfg *zrokEdgeSdk.Config, emailCfg *emailUi.Config, str *store.Store) (*Agent, error) {
a := &Agent{
cfg: cfg,
ifx: newInfluxReader(ifxCfg),
zCfg: zCfg,
str: str,
queue: make(chan *metrics.Usage, 1024),
acctWarningActions: []AccountAction{newAccountWarningAction(emailCfg, str)},
acctLimitActions: []AccountAction{newAccountLimitAction(str, zCfg)},
acctRelaxActions: []AccountAction{newAccountRelaxAction(str, zCfg)},
envWarningActions: []EnvironmentAction{newEnvironmentWarningAction(emailCfg, str)},
envLimitActions: []EnvironmentAction{newEnvironmentLimitAction(str, zCfg)},
envRelaxActions: []EnvironmentAction{newEnvironmentRelaxAction(str, zCfg)},
shrWarningActions: []ShareAction{newShareWarningAction(emailCfg, str)},
shrLimitActions: []ShareAction{newShareLimitAction(str, zCfg)},
shrRelaxActions: []ShareAction{newShareRelaxAction(str, zCfg)},
close: make(chan struct{}),
join: make(chan struct{}),
}
return a, nil
}
func (a *Agent) Start() {
go a.run()
}
func (a *Agent) Stop() {
close(a.close)
<-a.join
}
func (a *Agent) CanCreateEnvironment(acctId int, trx *sqlx.Tx) (bool, error) {
if a.cfg.Enforcing {
if empty, err := a.str.IsAccountLimitJournalEmpty(acctId, trx); err == nil && !empty {
alj, err := a.str.FindLatestAccountLimitJournal(acctId, trx)
if err != nil {
return false, err
}
if alj.Action == store.LimitAction {
return false, nil
}
} else if err != nil {
return false, err
}
if a.cfg.Environments > Unlimited {
envs, err := a.str.FindEnvironmentsForAccount(acctId, trx)
if err != nil {
return false, err
}
if len(envs)+1 > a.cfg.Environments {
return false, nil
}
}
}
return true, nil
}
func (a *Agent) CanCreateShare(acctId, envId int, trx *sqlx.Tx) (bool, error) {
if a.cfg.Enforcing {
if empty, err := a.str.IsAccountLimitJournalEmpty(acctId, trx); err == nil && !empty {
alj, err := a.str.FindLatestAccountLimitJournal(acctId, trx)
if err != nil {
return false, err
}
if alj.Action == store.LimitAction {
return false, nil
}
} else if err != nil {
return false, err
}
if empty, err := a.str.IsEnvironmentLimitJournalEmpty(envId, trx); err == nil && !empty {
elj, err := a.str.FindLatestEnvironmentLimitJournal(envId, trx)
if err != nil {
return false, err
}
if elj.Action == store.LimitAction {
return false, nil
}
} else if err != nil {
return false, err
}
if a.cfg.Shares > Unlimited {
envs, err := a.str.FindEnvironmentsForAccount(acctId, trx)
if err != nil {
return false, err
}
total := 0
for i := range envs {
shrs, err := a.str.FindSharesForEnvironment(envs[i].Id, trx)
if err != nil {
return false, errors.Wrapf(err, "unable to find shares for environment '%v'", envs[i].ZId)
}
total += len(shrs)
if total+1 > a.cfg.Shares {
return false, nil
}
logrus.Infof("total = %d", total)
}
}
}
return true, nil
}
func (a *Agent) CanAccessShare(shrId int, trx *sqlx.Tx) (bool, error) {
if a.cfg.Enforcing {
shr, err := a.str.GetShare(shrId, trx)
if err != nil {
return false, err
}
if empty, err := a.str.IsShareLimitJournalEmpty(shr.Id, trx); err == nil && !empty {
slj, err := a.str.FindLatestShareLimitJournal(shr.Id, trx)
if err != nil {
return false, err
}
if slj.Action == store.LimitAction {
return false, nil
}
} else if err != nil {
return false, err
}
env, err := a.str.GetEnvironment(shr.EnvironmentId, trx)
if err != nil {
return false, err
}
if empty, err := a.str.IsEnvironmentLimitJournalEmpty(env.Id, trx); err == nil && !empty {
elj, err := a.str.FindLatestEnvironmentLimitJournal(env.Id, trx)
if err != nil {
return false, err
}
if elj.Action == store.LimitAction {
return false, nil
}
} else if err != nil {
return false, err
}
if env.AccountId != nil {
acct, err := a.str.GetAccount(*env.AccountId, trx)
if err != nil {
return false, err
}
if empty, err := a.str.IsAccountLimitJournalEmpty(acct.Id, trx); err == nil && !empty {
alj, err := a.str.FindLatestAccountLimitJournal(acct.Id, trx)
if err != nil {
return false, err
}
if alj.Action == store.LimitAction {
return false, nil
}
} else if err != nil {
return false, err
}
}
}
return true, nil
}
func (a *Agent) Handle(u *metrics.Usage) error {
logrus.Debugf("handling: %v", u)
a.queue <- u
return nil
}
func (a *Agent) run() {
logrus.Info("started")
defer logrus.Info("stopped")
lastCycle := time.Now()
mainLoop:
for {
select {
case usage := <-a.queue:
if usage.ShareToken != "" {
if err := a.enforce(usage); err != nil {
logrus.Errorf("error running enforcement: %v", err)
}
if time.Since(lastCycle) > a.cfg.Cycle {
if err := a.relax(); err != nil {
logrus.Errorf("error running relax cycle: %v", err)
}
lastCycle = time.Now()
}
} else {
logrus.Warnf("not enforcing for usage with no share token: %v", usage.String())
}
case <-time.After(a.cfg.Cycle):
if err := a.relax(); err != nil {
logrus.Errorf("error running relax cycle: %v", err)
}
lastCycle = time.Now()
case <-a.close:
close(a.join)
break mainLoop
}
}
}
func (a *Agent) enforce(u *metrics.Usage) error {
trx, err := a.str.Begin()
if err != nil {
return errors.Wrap(err, "error starting transaction")
}
defer func() { _ = trx.Rollback() }()
acct, err := a.str.GetAccount(int(u.AccountId), trx)
if err != nil {
return err
}
if acct.Limitless {
return nil
}
if enforce, warning, rxBytes, txBytes, err := a.checkAccountLimit(u.AccountId); err == nil {
if enforce {
enforced := false
var enforcedAt time.Time
if empty, err := a.str.IsAccountLimitJournalEmpty(int(u.AccountId), trx); err == nil && !empty {
if latest, err := a.str.FindLatestAccountLimitJournal(int(u.AccountId), trx); err == nil {
enforced = latest.Action == store.LimitAction
enforcedAt = latest.UpdatedAt
}
}
if !enforced {
_, err := a.str.CreateAccountLimitJournal(&store.AccountLimitJournal{
AccountId: int(u.AccountId),
RxBytes: rxBytes,
TxBytes: txBytes,
Action: store.LimitAction,
}, trx)
if err != nil {
return err
}
acct, err := a.str.GetAccount(int(u.AccountId), trx)
if err != nil {
return err
}
// run account limit actions
for _, action := range a.acctLimitActions {
if err := action.HandleAccount(acct, rxBytes, txBytes, a.cfg.Bandwidth.PerAccount, trx); err != nil {
return errors.Wrapf(err, "%v", reflect.TypeOf(action).String())
}
}
if err := trx.Commit(); err != nil {
return err
}
} else {
logrus.Debugf("already enforced limit for account '#%d' at %v", u.AccountId, enforcedAt)
}
} else if warning {
warned := false
var warnedAt time.Time
if empty, err := a.str.IsAccountLimitJournalEmpty(int(u.AccountId), trx); err == nil && !empty {
if latest, err := a.str.FindLatestAccountLimitJournal(int(u.AccountId), trx); err == nil {
warned = latest.Action == store.WarningAction || latest.Action == store.LimitAction
warnedAt = latest.UpdatedAt
}
}
if !warned {
_, err := a.str.CreateAccountLimitJournal(&store.AccountLimitJournal{
AccountId: int(u.AccountId),
RxBytes: rxBytes,
TxBytes: txBytes,
Action: store.WarningAction,
}, trx)
if err != nil {
return err
}
acct, err := a.str.GetAccount(int(u.AccountId), trx)
if err != nil {
return err
}
// run account warning actions
for _, action := range a.acctWarningActions {
if err := action.HandleAccount(acct, rxBytes, txBytes, a.cfg.Bandwidth.PerAccount, trx); err != nil {
return errors.Wrapf(err, "%v", reflect.TypeOf(action).String())
}
}
if err := trx.Commit(); err != nil {
return err
}
} else {
logrus.Debugf("already warned account '#%d' at %v", u.AccountId, warnedAt)
}
} else {
if enforce, warning, rxBytes, txBytes, err := a.checkEnvironmentLimit(u.EnvironmentId); err == nil {
if enforce {
enforced := false
var enforcedAt time.Time
if empty, err := a.str.IsEnvironmentLimitJournalEmpty(int(u.EnvironmentId), trx); err == nil && !empty {
if latest, err := a.str.FindLatestEnvironmentLimitJournal(int(u.EnvironmentId), trx); err == nil {
enforced = latest.Action == store.LimitAction
enforcedAt = latest.UpdatedAt
}
}
if !enforced {
_, err := a.str.CreateEnvironmentLimitJournal(&store.EnvironmentLimitJournal{
EnvironmentId: int(u.EnvironmentId),
RxBytes: rxBytes,
TxBytes: txBytes,
Action: store.LimitAction,
}, trx)
if err != nil {
return err
}
env, err := a.str.GetEnvironment(int(u.EnvironmentId), trx)
if err != nil {
return err
}
// run environment limit actions
for _, action := range a.envLimitActions {
if err := action.HandleEnvironment(env, rxBytes, txBytes, a.cfg.Bandwidth.PerEnvironment, trx); err != nil {
return errors.Wrapf(err, "%v", reflect.TypeOf(action).String())
}
}
if err := trx.Commit(); err != nil {
return err
}
} else {
logrus.Debugf("already enforced limit for environment '#%d' at %v", u.EnvironmentId, enforcedAt)
}
} else if warning {
warned := false
var warnedAt time.Time
if empty, err := a.str.IsEnvironmentLimitJournalEmpty(int(u.EnvironmentId), trx); err == nil && !empty {
if latest, err := a.str.FindLatestEnvironmentLimitJournal(int(u.EnvironmentId), trx); err == nil {
warned = latest.Action == store.WarningAction || latest.Action == store.LimitAction
warnedAt = latest.UpdatedAt
}
}
if !warned {
_, err := a.str.CreateEnvironmentLimitJournal(&store.EnvironmentLimitJournal{
EnvironmentId: int(u.EnvironmentId),
RxBytes: rxBytes,
TxBytes: txBytes,
Action: store.WarningAction,
}, trx)
if err != nil {
return err
}
env, err := a.str.GetEnvironment(int(u.EnvironmentId), trx)
if err != nil {
return err
}
// run environment warning actions
for _, action := range a.envWarningActions {
if err := action.HandleEnvironment(env, rxBytes, txBytes, a.cfg.Bandwidth.PerEnvironment, trx); err != nil {
return errors.Wrapf(err, "%v", reflect.TypeOf(action).String())
}
}
if err := trx.Commit(); err != nil {
return err
}
} else {
logrus.Debugf("already warned environment '#%d' at %v", u.EnvironmentId, warnedAt)
}
} else {
if enforce, warning, rxBytes, txBytes, err := a.checkShareLimit(u.ShareToken); err == nil {
if enforce {
shr, err := a.str.FindShareWithToken(u.ShareToken, trx)
if err != nil {
return err
}
enforced := false
var enforcedAt time.Time
if empty, err := a.str.IsShareLimitJournalEmpty(shr.Id, trx); err == nil && !empty {
if latest, err := a.str.FindLatestShareLimitJournal(shr.Id, trx); err == nil {
enforced = latest.Action == store.LimitAction
enforcedAt = latest.UpdatedAt
}
}
if !enforced {
_, err := a.str.CreateShareLimitJournal(&store.ShareLimitJournal{
ShareId: shr.Id,
RxBytes: rxBytes,
TxBytes: txBytes,
Action: store.LimitAction,
}, trx)
if err != nil {
return err
}
// run share limit actions
for _, action := range a.shrLimitActions {
if err := action.HandleShare(shr, rxBytes, txBytes, a.cfg.Bandwidth.PerShare, trx); err != nil {
return errors.Wrapf(err, "%v", reflect.TypeOf(action).String())
}
}
if err := trx.Commit(); err != nil {
return err
}
} else {
logrus.Debugf("already enforced limit for share '%v' at %v", shr.Token, enforcedAt)
}
} else if warning {
shr, err := a.str.FindShareWithToken(u.ShareToken, trx)
if err != nil {
return err
}
warned := false
var warnedAt time.Time
if empty, err := a.str.IsShareLimitJournalEmpty(shr.Id, trx); err == nil && !empty {
if latest, err := a.str.FindLatestShareLimitJournal(shr.Id, trx); err == nil {
warned = latest.Action == store.WarningAction || latest.Action == store.LimitAction
warnedAt = latest.UpdatedAt
}
}
if !warned {
_, err := a.str.CreateShareLimitJournal(&store.ShareLimitJournal{
ShareId: shr.Id,
RxBytes: rxBytes,
TxBytes: txBytes,
Action: store.WarningAction,
}, trx)
if err != nil {
return err
}
// run share warning actions
for _, action := range a.shrWarningActions {
if err := action.HandleShare(shr, rxBytes, txBytes, a.cfg.Bandwidth.PerShare, trx); err != nil {
return errors.Wrapf(err, "%v", reflect.TypeOf(action).String())
}
}
if err := trx.Commit(); err != nil {
return err
}
} else {
logrus.Debugf("already warned share '%v' at %v", shr.Token, warnedAt)
}
}
} else {
logrus.Error(err)
}
}
} else {
logrus.Error(err)
}
}
} else {
logrus.Error(err)
}
return nil
}
func (a *Agent) relax() error {
logrus.Debug("relaxing")
trx, err := a.str.Begin()
if err != nil {
return errors.Wrap(err, "error starting transaction")
}
defer func() { _ = trx.Rollback() }()
commit := false
if sljs, err := a.str.FindAllLatestShareLimitJournal(trx); err == nil {
for _, slj := range sljs {
if shr, err := a.str.GetShare(slj.ShareId, trx); err == nil {
if slj.Action == store.WarningAction || slj.Action == store.LimitAction {
if enforce, warning, rxBytes, txBytes, err := a.checkShareLimit(shr.Token); err == nil {
if !enforce && !warning {
if slj.Action == store.LimitAction {
// run relax actions for share
for _, action := range a.shrRelaxActions {
if err := action.HandleShare(shr, rxBytes, txBytes, a.cfg.Bandwidth.PerShare, trx); err != nil {
return errors.Wrapf(err, "%v", reflect.TypeOf(action).String())
}
}
} else {
logrus.Infof("relaxing warning for '%v'", shr.Token)
}
if err := a.str.DeleteShareLimitJournalForShare(shr.Id, trx); err == nil {
commit = true
} else {
logrus.Errorf("error deleting share_limit_journal for '%v'", shr.Token)
}
} else {
logrus.Infof("share '%v' still over limit", shr.Token)
}
} else {
logrus.Errorf("error checking share limit for '%v': %v", shr.Token, err)
}
}
} else {
logrus.Errorf("error getting share for '#%d': %v", slj.ShareId, err)
}
}
} else {
return err
}
if eljs, err := a.str.FindAllLatestEnvironmentLimitJournal(trx); err == nil {
for _, elj := range eljs {
if env, err := a.str.GetEnvironment(elj.EnvironmentId, trx); err == nil {
if elj.Action == store.WarningAction || elj.Action == store.LimitAction {
if enforce, warning, rxBytes, txBytes, err := a.checkEnvironmentLimit(int64(elj.EnvironmentId)); err == nil {
if !enforce && !warning {
if elj.Action == store.LimitAction {
// run relax actions for environment
for _, action := range a.envRelaxActions {
if err := action.HandleEnvironment(env, rxBytes, txBytes, a.cfg.Bandwidth.PerEnvironment, trx); err != nil {
return errors.Wrapf(err, "%v", reflect.TypeOf(action).String())
}
}
} else {
logrus.Infof("relaxing warning for '%v'", env.ZId)
}
if err := a.str.DeleteEnvironmentLimitJournalForEnvironment(env.Id, trx); err == nil {
commit = true
} else {
logrus.Errorf("error deleteing environment_limit_journal for '%v': %v", env.ZId, err)
}
} else {
logrus.Infof("environment '%v' still over limit", env.ZId)
}
} else {
logrus.Errorf("error checking environment limit for '%v': %v", env.ZId, err)
}
}
} else {
logrus.Errorf("error getting environment for '#%d': %v", elj.EnvironmentId, err)
}
}
} else {
return err
}
if aljs, err := a.str.FindAllLatestAccountLimitJournal(trx); err == nil {
for _, alj := range aljs {
if acct, err := a.str.GetAccount(alj.AccountId, trx); err == nil {
if alj.Action == store.WarningAction || alj.Action == store.LimitAction {
if enforce, warning, rxBytes, txBytes, err := a.checkAccountLimit(int64(alj.AccountId)); err == nil {
if !enforce && !warning {
if alj.Action == store.LimitAction {
// run relax actions for account
for _, action := range a.acctRelaxActions {
if err := action.HandleAccount(acct, rxBytes, txBytes, a.cfg.Bandwidth.PerAccount, trx); err != nil {
return errors.Wrapf(err, "%v", reflect.TypeOf(action).String())
}
}
} else {
logrus.Infof("relaxing warning for '%v'", acct.Email)
}
if err := a.str.DeleteAccountLimitJournalForAccount(acct.Id, trx); err == nil {
commit = true
} else {
logrus.Errorf("error deleting account_limit_journal for '%v': %v", acct.Email, err)
}
} else {
logrus.Infof("account '%v' still over limit", acct.Email)
}
} else {
logrus.Errorf("error checking account limit for '%v': %v", acct.Email, err)
}
}
} else {
logrus.Errorf("error getting account for '#%d': %v", alj.AccountId, err)
}
}
} else {
return err
}
if commit {
if err := trx.Commit(); err != nil {
return err
}
}
return nil
}
func (a *Agent) checkAccountLimit(acctId int64) (enforce, warning bool, rxBytes, txBytes int64, err error) {
period := 24 * time.Hour
limit := DefaultBandwidthPerPeriod()
if a.cfg.Bandwidth != nil && a.cfg.Bandwidth.PerAccount != nil {
limit = a.cfg.Bandwidth.PerAccount
}
if limit.Period > 0 {
period = limit.Period
}
rx, tx, err := a.ifx.totalRxTxForAccount(acctId, period)
if err != nil {
logrus.Error(err)
}
enforce, warning = a.checkLimit(limit, rx, tx)
return enforce, warning, rx, tx, nil
}
func (a *Agent) checkEnvironmentLimit(envId int64) (enforce, warning bool, rxBytes, txBytes int64, err error) {
period := 24 * time.Hour
limit := DefaultBandwidthPerPeriod()
if a.cfg.Bandwidth != nil && a.cfg.Bandwidth.PerEnvironment != nil {
limit = a.cfg.Bandwidth.PerEnvironment
}
if limit.Period > 0 {
period = limit.Period
}
rx, tx, err := a.ifx.totalRxTxForEnvironment(envId, period)
if err != nil {
logrus.Error(err)
}
enforce, warning = a.checkLimit(limit, rx, tx)
return enforce, warning, rx, tx, nil
}
func (a *Agent) checkShareLimit(shrToken string) (enforce, warning bool, rxBytes, txBytes int64, err error) {
period := 24 * time.Hour
limit := DefaultBandwidthPerPeriod()
if a.cfg.Bandwidth != nil && a.cfg.Bandwidth.PerShare != nil {
limit = a.cfg.Bandwidth.PerShare
}
if limit.Period > 0 {
period = limit.Period
}
rx, tx, err := a.ifx.totalRxTxForShare(shrToken, period)
if err != nil {
logrus.Error(err)
}
enforce, warning = a.checkLimit(limit, rx, tx)
if enforce || warning {
logrus.Debugf("'%v': %v", shrToken, describeLimit(limit, rx, tx))
}
return enforce, warning, rx, tx, nil
}
func (a *Agent) checkLimit(cfg *BandwidthPerPeriod, rx, tx int64) (enforce, warning bool) {
if cfg.Limit.Rx != Unlimited && rx > cfg.Limit.Rx {
return true, false
}
if cfg.Limit.Tx != Unlimited && tx > cfg.Limit.Tx {
return true, false
}
if cfg.Limit.Total != Unlimited && rx+tx > cfg.Limit.Total {
return true, false
}
if cfg.Warning.Rx != Unlimited && rx > cfg.Warning.Rx {
return false, true
}
if cfg.Warning.Tx != Unlimited && tx > cfg.Warning.Tx {
return false, true
}
if cfg.Warning.Total != Unlimited && rx+tx > cfg.Warning.Total {
return false, true
}
return false, false
}
func describeLimit(cfg *BandwidthPerPeriod, rx, tx int64) string {
out := ""
if cfg.Limit.Rx != Unlimited && rx > cfg.Limit.Rx {
out += fmt.Sprintf("['%v' over rx limit '%v']", util.BytesToSize(rx), util.BytesToSize(cfg.Limit.Rx))
}
if cfg.Limit.Tx != Unlimited && tx > cfg.Limit.Tx {
out += fmt.Sprintf("['%v' over tx limit '%v']", util.BytesToSize(tx), util.BytesToSize(cfg.Limit.Tx))
}
if cfg.Limit.Total != Unlimited && rx+tx > cfg.Limit.Total {
out += fmt.Sprintf("['%v' over total limit '%v']", util.BytesToSize(rx+tx), util.BytesToSize(cfg.Limit.Total))
}
if cfg.Warning.Rx != Unlimited && rx > cfg.Warning.Rx {
out += fmt.Sprintf("['%v' over rx warning '%v']", util.BytesToSize(rx), util.BytesToSize(cfg.Warning.Rx))
}
if cfg.Warning.Tx != Unlimited && tx > cfg.Warning.Tx {
out += fmt.Sprintf("['%v' over tx warning '%v']", util.BytesToSize(tx), util.BytesToSize(cfg.Warning.Tx))
}
if cfg.Warning.Total != Unlimited && rx+tx > cfg.Warning.Total {
out += fmt.Sprintf("['%v' over total warning '%v']", util.BytesToSize(rx+tx), util.BytesToSize(cfg.Warning.Total))
}
return out
}

View File

@ -0,0 +1,61 @@
package limits
import "time"
const Unlimited = -1
type Config struct {
Environments int
Shares int
Bandwidth *BandwidthConfig
Cycle time.Duration
Enforcing bool
}
type BandwidthConfig struct {
PerAccount *BandwidthPerPeriod
PerEnvironment *BandwidthPerPeriod
PerShare *BandwidthPerPeriod
}
type BandwidthPerPeriod struct {
Period time.Duration
Warning *Bandwidth
Limit *Bandwidth
}
type Bandwidth struct {
Rx int64
Tx int64
Total int64
}
func DefaultBandwidthPerPeriod() *BandwidthPerPeriod {
return &BandwidthPerPeriod{
Period: 24 * time.Hour,
Warning: &Bandwidth{
Rx: Unlimited,
Tx: Unlimited,
Total: Unlimited,
},
Limit: &Bandwidth{
Rx: Unlimited,
Tx: Unlimited,
Total: Unlimited,
},
}
}
func DefaultConfig() *Config {
return &Config{
Environments: Unlimited,
Shares: Unlimited,
Bandwidth: &BandwidthConfig{
PerAccount: DefaultBandwidthPerPeriod(),
PerEnvironment: DefaultBandwidthPerPeriod(),
PerShare: DefaultBandwidthPerPeriod(),
},
Enforcing: false,
Cycle: 15 * time.Minute,
}
}

View File

@ -0,0 +1,92 @@
package limits
import (
"fmt"
"github.com/openziti/zrok/build"
"github.com/openziti/zrok/controller/emailUi"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/wneessen/go-mail"
)
type detailMessage struct {
lines []string
}
func newDetailMessage() *detailMessage {
return &detailMessage{}
}
func (m *detailMessage) append(msg string, args ...interface{}) *detailMessage {
m.lines = append(m.lines, fmt.Sprintf(msg, args...))
return m
}
func (m *detailMessage) html() string {
out := ""
for i := range m.lines {
out += fmt.Sprintf("<p style=\"text-align: left;\">%s</p>\n", m.lines[i])
}
return out
}
func (m *detailMessage) plain() string {
out := ""
for i := range m.lines {
out += fmt.Sprintf("%s\n\n", m.lines[i])
}
return out
}
func sendLimitWarningEmail(cfg *emailUi.Config, emailTo string, d *detailMessage) error {
emailData := &emailUi.WarningEmail{
EmailAddress: emailTo,
Version: build.String(),
}
emailData.Detail = d.plain()
plainBody, err := emailData.MergeTemplate("limitWarning.gotext")
if err != nil {
return err
}
emailData.Detail = d.html()
htmlBody, err := emailData.MergeTemplate("limitWarning.gohtml")
if err != nil {
return err
}
msg := mail.NewMsg()
if err := msg.From(cfg.From); err != nil {
return errors.Wrap(err, "failed to set from address in limit warning email")
}
if err := msg.To(emailTo); err != nil {
return errors.Wrap(err, "failed to set to address in limit warning email")
}
msg.Subject("zrok Limit Warning Notification")
msg.SetDate()
msg.SetMessageID()
msg.SetBulk()
msg.SetImportance(mail.ImportanceHigh)
msg.SetBodyString(mail.TypeTextPlain, plainBody)
msg.SetBodyString(mail.TypeTextHTML, htmlBody)
client, err := mail.NewClient(cfg.Host,
mail.WithPort(cfg.Port),
mail.WithSMTPAuth(mail.SMTPAuthPlain),
mail.WithUsername(cfg.Username),
mail.WithPassword(cfg.Password),
mail.WithTLSPolicy(mail.TLSMandatory),
)
if err != nil {
return errors.Wrap(err, "error creating limit warning email client")
}
if err := client.DialAndSend(msg); err != nil {
return errors.Wrap(err, "error sending limit warning email")
}
logrus.Infof("limit warning email sent to '%v'", emailTo)
return nil
}

View File

@ -0,0 +1,41 @@
package limits
import (
"github.com/jmoiron/sqlx"
"github.com/openziti/zrok/controller/store"
"github.com/openziti/zrok/controller/zrokEdgeSdk"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
type environmentLimitAction struct {
str *store.Store
zCfg *zrokEdgeSdk.Config
}
func newEnvironmentLimitAction(str *store.Store, zCfg *zrokEdgeSdk.Config) *environmentLimitAction {
return &environmentLimitAction{str, zCfg}
}
func (a *environmentLimitAction) HandleEnvironment(env *store.Environment, _, _ int64, _ *BandwidthPerPeriod, trx *sqlx.Tx) error {
logrus.Infof("limiting '%v'", env.ZId)
shrs, err := a.str.FindSharesForEnvironment(env.Id, trx)
if err != nil {
return errors.Wrapf(err, "error finding shares for environment '%v'", env.ZId)
}
edge, err := zrokEdgeSdk.Client(a.zCfg)
if err != nil {
return err
}
for _, shr := range shrs {
if err := zrokEdgeSdk.DeleteServicePoliciesDial(env.ZId, shr.Token, edge); err != nil {
return errors.Wrapf(err, "error deleting dial service policy for '%v'", shr.Token)
}
logrus.Infof("removed dial service policy for share '%v' of environment '%v'", shr.Token, env.ZId)
}
return nil
}

View File

@ -0,0 +1,49 @@
package limits
import (
"github.com/jmoiron/sqlx"
"github.com/openziti/zrok/controller/store"
"github.com/openziti/zrok/controller/zrokEdgeSdk"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
type environmentRelaxAction struct {
str *store.Store
zCfg *zrokEdgeSdk.Config
}
func newEnvironmentRelaxAction(str *store.Store, zCfg *zrokEdgeSdk.Config) *environmentRelaxAction {
return &environmentRelaxAction{str, zCfg}
}
func (a *environmentRelaxAction) HandleEnvironment(env *store.Environment, rxBytes, txBytes int64, limit *BandwidthPerPeriod, trx *sqlx.Tx) error {
logrus.Infof("relaxing '%v'", env.ZId)
shrs, err := a.str.FindSharesForEnvironment(env.Id, trx)
if err != nil {
return errors.Wrapf(err, "error finding shares for environment '%v'", env.ZId)
}
edge, err := zrokEdgeSdk.Client(a.zCfg)
if err != nil {
return err
}
for _, shr := range shrs {
if !shr.Deleted {
switch shr.ShareMode {
case "public":
if err := relaxPublicShare(a.str, edge, shr, trx); err != nil {
return err
}
case "private":
if err := relaxPrivateShare(a.str, edge, shr, trx); err != nil {
return err
}
}
}
}
return nil
}

View File

@ -0,0 +1,58 @@
package limits
import (
"github.com/jmoiron/sqlx"
"github.com/openziti/zrok/controller/emailUi"
"github.com/openziti/zrok/controller/store"
"github.com/openziti/zrok/util"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
type environmentWarningAction struct {
str *store.Store
cfg *emailUi.Config
}
func newEnvironmentWarningAction(cfg *emailUi.Config, str *store.Store) *environmentWarningAction {
return &environmentWarningAction{str, cfg}
}
func (a *environmentWarningAction) HandleEnvironment(env *store.Environment, rxBytes, txBytes int64, limit *BandwidthPerPeriod, trx *sqlx.Tx) error {
logrus.Infof("warning '%v'", env.ZId)
if a.cfg != nil {
if env.AccountId != nil {
acct, err := a.str.GetAccount(*env.AccountId, trx)
if err != nil {
return err
}
rxLimit := "unlimited bytes"
if limit.Limit.Rx != Unlimited {
rxLimit = util.BytesToSize(limit.Limit.Rx)
}
txLimit := "unlimited bytes"
if limit.Limit.Tx != Unlimited {
txLimit = util.BytesToSize(limit.Limit.Tx)
}
totalLimit := "unlimited bytes"
if limit.Limit.Total != Unlimited {
totalLimit = util.BytesToSize(limit.Limit.Total)
}
detail := newDetailMessage()
detail = detail.append("Your environment '%v' has received %v and sent %v (for a total of %v), which has triggered a transfer limit warning.", env.Description, util.BytesToSize(rxBytes), util.BytesToSize(txBytes), util.BytesToSize(rxBytes+txBytes))
detail = detail.append("This zrok instance only allows a share to receive %v, send %v, totalling not more than %v for each %v.", rxLimit, txLimit, totalLimit, limit.Period)
detail = detail.append("If you exceed the transfer limit, access to your shares will be temporarily disabled (until the last %v falls below the transfer limit).", limit.Period)
if err := sendLimitWarningEmail(a.cfg, acct.Email, detail); err != nil {
return errors.Wrapf(err, "error sending limit warning email to '%v'", acct.Email)
}
}
} else {
logrus.Warnf("skipping warning email for environment limit; no email configuration specified")
}
return nil
}

View File

@ -0,0 +1,88 @@
package limits
import (
"context"
"fmt"
influxdb2 "github.com/influxdata/influxdb-client-go/v2"
"github.com/influxdata/influxdb-client-go/v2/api"
"github.com/openziti/zrok/controller/metrics"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"strings"
"time"
)
type influxReader struct {
cfg *metrics.InfluxConfig
idb influxdb2.Client
queryApi api.QueryAPI
}
func newInfluxReader(cfg *metrics.InfluxConfig) *influxReader {
idb := influxdb2.NewClient(cfg.Url, cfg.Token)
queryApi := idb.QueryAPI(cfg.Org)
return &influxReader{cfg, idb, queryApi}
}
func (r *influxReader) totalRxTxForAccount(acctId int64, duration time.Duration) (int64, int64, error) {
query := fmt.Sprintf("from(bucket: \"%v\")\n", r.cfg.Bucket) +
fmt.Sprintf("|> range(start: -%v)\n", duration) +
"|> filter(fn: (r) => r[\"_measurement\"] == \"xfer\")\n" +
"|> filter(fn: (r) => r[\"_field\"] == \"rx\" or r[\"_field\"] == \"tx\")\n" +
"|> filter(fn: (r) => r[\"namespace\"] == \"backend\")\n" +
fmt.Sprintf("|> filter(fn: (r) => r[\"acctId\"] == \"%d\")\n", acctId) +
"|> drop(columns: [\"share\", \"envId\"])\n" +
"|> sum()"
return r.runQueryForRxTx(query)
}
func (r *influxReader) totalRxTxForEnvironment(envId int64, duration time.Duration) (int64, int64, error) {
query := fmt.Sprintf("from(bucket: \"%v\")\n", r.cfg.Bucket) +
fmt.Sprintf("|> range(start: -%v)\n", duration) +
"|> filter(fn: (r) => r[\"_measurement\"] == \"xfer\")\n" +
"|> filter(fn: (r) => r[\"_field\"] == \"rx\" or r[\"_field\"] == \"tx\")\n" +
"|> filter(fn: (r) => r[\"namespace\"] == \"backend\")\n" +
fmt.Sprintf("|> filter(fn: (r) => r[\"envId\"] == \"%d\")\n", envId) +
"|> drop(columns: [\"share\", \"acctId\"])\n" +
"|> sum()"
return r.runQueryForRxTx(query)
}
func (r *influxReader) totalRxTxForShare(shrToken string, duration time.Duration) (int64, int64, error) {
query := fmt.Sprintf("from(bucket: \"%v\")\n", r.cfg.Bucket) +
fmt.Sprintf("|> range(start: -%v)\n", duration) +
"|> filter(fn: (r) => r[\"_measurement\"] == \"xfer\")\n" +
"|> filter(fn: (r) => r[\"_field\"] == \"rx\" or r[\"_field\"] == \"tx\")\n" +
"|> filter(fn: (r) => r[\"namespace\"] == \"backend\")\n" +
fmt.Sprintf("|> filter(fn: (r) => r[\"share\"] == \"%v\")\n", shrToken) +
"|> sum()"
return r.runQueryForRxTx(query)
}
func (r *influxReader) runQueryForRxTx(query string) (rx int64, tx int64, err error) {
result, err := r.queryApi.Query(context.Background(), query)
if err != nil {
return -1, -1, err
}
count := 0
for result.Next() {
if v, ok := result.Record().Value().(int64); ok {
switch result.Record().Field() {
case "tx":
tx = v
case "rx":
rx = v
default:
logrus.Warnf("field '%v'?", result.Record().Field())
}
} else {
return -1, -1, errors.New("error asserting value type")
}
count++
}
if count != 0 && count != 2 {
return -1, -1, errors.Errorf("expected 2 results; got '%d' (%v)", count, strings.ReplaceAll(query, "\n", ""))
}
return rx, tx, nil
}

View File

@ -0,0 +1,18 @@
package limits
import (
"github.com/jmoiron/sqlx"
"github.com/openziti/zrok/controller/store"
)
type AccountAction interface {
HandleAccount(a *store.Account, rxBytes, txBytes int64, limit *BandwidthPerPeriod, trx *sqlx.Tx) error
}
type EnvironmentAction interface {
HandleEnvironment(e *store.Environment, rxBytes, txBytes int64, limit *BandwidthPerPeriod, trx *sqlx.Tx) error
}
type ShareAction interface {
HandleShare(s *store.Share, rxBytes, txBytes int64, limit *BandwidthPerPeriod, trx *sqlx.Tx) error
}

View File

@ -0,0 +1,38 @@
package limits
import (
"github.com/jmoiron/sqlx"
"github.com/openziti/zrok/controller/store"
"github.com/openziti/zrok/controller/zrokEdgeSdk"
"github.com/sirupsen/logrus"
)
type shareLimitAction struct {
str *store.Store
zCfg *zrokEdgeSdk.Config
}
func newShareLimitAction(str *store.Store, zCfg *zrokEdgeSdk.Config) *shareLimitAction {
return &shareLimitAction{str, zCfg}
}
func (a *shareLimitAction) HandleShare(shr *store.Share, _, _ int64, _ *BandwidthPerPeriod, trx *sqlx.Tx) error {
logrus.Infof("limiting '%v'", shr.Token)
env, err := a.str.GetEnvironment(shr.EnvironmentId, trx)
if err != nil {
return err
}
edge, err := zrokEdgeSdk.Client(a.zCfg)
if err != nil {
return err
}
if err := zrokEdgeSdk.DeleteServicePoliciesDial(env.ZId, shr.Token, edge); err != nil {
return err
}
logrus.Infof("removed dial service policy for '%v'", shr.Token)
return nil
}

View File

@ -0,0 +1,88 @@
package limits
import (
"github.com/jmoiron/sqlx"
"github.com/openziti/edge-api/rest_management_api_client"
"github.com/openziti/zrok/controller/store"
"github.com/openziti/zrok/controller/zrokEdgeSdk"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
type shareRelaxAction struct {
str *store.Store
zCfg *zrokEdgeSdk.Config
}
func newShareRelaxAction(str *store.Store, zCfg *zrokEdgeSdk.Config) *shareRelaxAction {
return &shareRelaxAction{str, zCfg}
}
func (a *shareRelaxAction) HandleShare(shr *store.Share, _, _ int64, _ *BandwidthPerPeriod, trx *sqlx.Tx) error {
logrus.Infof("relaxing '%v'", shr.Token)
if !shr.Deleted {
edge, err := zrokEdgeSdk.Client(a.zCfg)
if err != nil {
return err
}
switch shr.ShareMode {
case "public":
if err := relaxPublicShare(a.str, edge, shr, trx); err != nil {
return err
}
case "private":
if err := relaxPrivateShare(a.str, edge, shr, trx); err != nil {
return err
}
}
}
return nil
}
func relaxPublicShare(str *store.Store, edge *rest_management_api_client.ZitiEdgeManagement, shr *store.Share, trx *sqlx.Tx) error {
env, err := str.GetEnvironment(shr.EnvironmentId, trx)
if err != nil {
return errors.Wrap(err, "error finding environment")
}
fe, err := str.FindFrontendPubliclyNamed(*shr.FrontendSelection, trx)
if err != nil {
return errors.Wrapf(err, "error finding frontend name '%v' for '%v'", *shr.FrontendSelection, shr.Token)
}
if err := zrokEdgeSdk.CreateServicePolicyDial(env.ZId+"-"+shr.ZId+"-dial", shr.ZId, []string{fe.ZId}, zrokEdgeSdk.ZrokShareTags(shr.Token).SubTags, edge); err != nil {
return errors.Wrapf(err, "error creating dial service policy for '%v'", shr.Token)
}
logrus.Infof("added dial service policy for '%v'", shr.Token)
return nil
}
func relaxPrivateShare(str *store.Store, edge *rest_management_api_client.ZitiEdgeManagement, shr *store.Share, trx *sqlx.Tx) error {
fes, err := str.FindFrontendsForPrivateShare(shr.Id, trx)
if err != nil {
return errors.Wrapf(err, "error finding frontends for share '%v'", shr.Token)
}
for _, fe := range fes {
if fe.EnvironmentId != nil {
env, err := str.GetEnvironment(*fe.EnvironmentId, trx)
if err != nil {
return errors.Wrapf(err, "error getting environment for frontend '%v'", fe.Token)
}
addlTags := map[string]interface{}{
"zrokEnvironmentZId": env.ZId,
"zrokFrontendToken": fe.Token,
"zrokShareToken": shr.Token,
}
if err := zrokEdgeSdk.CreateServicePolicyDial(fe.Token+"-"+env.ZId+"-"+shr.ZId+"-dial", shr.ZId, []string{env.ZId}, addlTags, edge); err != nil {
return errors.Wrapf(err, "unable to create dial policy for frontend '%v'", fe.Token)
}
logrus.Infof("added dial service policy for share '%v' to private frontend '%v'", shr.Token, fe.Token)
}
}
return nil
}

View File

@ -0,0 +1,63 @@
package limits
import (
"github.com/jmoiron/sqlx"
"github.com/openziti/zrok/controller/emailUi"
"github.com/openziti/zrok/controller/store"
"github.com/openziti/zrok/util"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
type shareWarningAction struct {
str *store.Store
cfg *emailUi.Config
}
func newShareWarningAction(cfg *emailUi.Config, str *store.Store) *shareWarningAction {
return &shareWarningAction{str, cfg}
}
func (a *shareWarningAction) HandleShare(shr *store.Share, rxBytes, txBytes int64, limit *BandwidthPerPeriod, trx *sqlx.Tx) error {
logrus.Infof("warning '%v'", shr.Token)
if a.cfg != nil {
env, err := a.str.GetEnvironment(shr.EnvironmentId, trx)
if err != nil {
return err
}
if env.AccountId != nil {
acct, err := a.str.GetAccount(*env.AccountId, trx)
if err != nil {
return err
}
rxLimit := "unlimited bytes"
if limit.Limit.Rx != Unlimited {
rxLimit = util.BytesToSize(limit.Limit.Rx)
}
txLimit := "unlimited bytes"
if limit.Limit.Tx != Unlimited {
txLimit = util.BytesToSize(limit.Limit.Tx)
}
totalLimit := "unlimited bytes"
if limit.Limit.Total != Unlimited {
totalLimit = util.BytesToSize(limit.Limit.Total)
}
detail := newDetailMessage()
detail = detail.append("Your share '%v' has received %v and sent %v (for a total of %v), which has triggered a transfer limit warning.", shr.Token, util.BytesToSize(rxBytes), util.BytesToSize(txBytes), util.BytesToSize(rxBytes+txBytes))
detail = detail.append("This zrok instance only allows a share to receive %v, send %v, totalling not more than %v for each %v.", rxLimit, txLimit, totalLimit, limit.Period)
detail = detail.append("If you exceed the transfer limit, access to your shares will be temporarily disabled (until the last %v falls below the transfer limit).", limit.Period)
if err := sendLimitWarningEmail(a.cfg, acct.Email, detail); err != nil {
return errors.Wrapf(err, "error sending limit warning email to '%v'", acct.Email)
}
}
} else {
logrus.Warnf("skipping warning email for share limit; no email configuration specified")
}
return nil
}

View File

@ -3,6 +3,7 @@ package controller
import ( import (
"context" "context"
"fmt" "fmt"
"github.com/openziti/zrok/controller/config"
"strings" "strings"
"time" "time"
@ -11,11 +12,11 @@ import (
) )
type maintenanceRegistrationAgent struct { type maintenanceRegistrationAgent struct {
cfg *RegistrationMaintenanceConfig cfg *config.RegistrationMaintenanceConfig
ctx context.Context ctx context.Context
} }
func newRegistrationMaintenanceAgent(ctx context.Context, cfg *RegistrationMaintenanceConfig) *maintenanceRegistrationAgent { func newRegistrationMaintenanceAgent(ctx context.Context, cfg *config.RegistrationMaintenanceConfig) *maintenanceRegistrationAgent {
return &maintenanceRegistrationAgent{ return &maintenanceRegistrationAgent{
cfg: cfg, cfg: cfg,
ctx: ctx, ctx: ctx,
@ -78,11 +79,11 @@ func (ma *maintenanceRegistrationAgent) deleteExpiredAccountRequests() error {
} }
type maintenanceResetPasswordAgent struct { type maintenanceResetPasswordAgent struct {
cfg *ResetPasswordMaintenanceConfig cfg *config.ResetPasswordMaintenanceConfig
ctx context.Context ctx context.Context
} }
func newMaintenanceResetPasswordAgent(ctx context.Context, cfg *ResetPasswordMaintenanceConfig) *maintenanceResetPasswordAgent { func newMaintenanceResetPasswordAgent(ctx context.Context, cfg *config.ResetPasswordMaintenanceConfig) *maintenanceResetPasswordAgent {
return &maintenanceResetPasswordAgent{ return &maintenanceResetPasswordAgent{
cfg: cfg, cfg: cfg,
ctx: ctx, ctx: ctx,

View File

@ -1,181 +1,261 @@
package controller package controller
import ( import (
"bytes"
"context" "context"
"fmt" "fmt"
"github.com/go-openapi/runtime/middleware"
influxdb2 "github.com/influxdata/influxdb-client-go/v2" influxdb2 "github.com/influxdata/influxdb-client-go/v2"
"github.com/influxdata/influxdb-client-go/v2/api" "github.com/influxdata/influxdb-client-go/v2/api"
"github.com/influxdata/influxdb-client-go/v2/api/write" "github.com/openziti/zrok/controller/metrics"
"github.com/openziti/sdk-golang/ziti" "github.com/openziti/zrok/rest_model_zrok"
"github.com/openziti/sdk-golang/ziti/config" "github.com/openziti/zrok/rest_server_zrok/operations/metadata"
"github.com/openziti/sdk-golang/ziti/edge"
"github.com/openziti/zrok/model"
"github.com/openziti/zrok/util"
"github.com/openziti/zrok/zrokdir"
"github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"gopkg.in/mgo.v2/bson"
"net"
"time" "time"
) )
type metricsAgent struct { type getAccountMetricsHandler struct {
writeApi api.WriteAPIBlocking cfg *metrics.InfluxConfig
metricsQueue chan *model.Metrics idb influxdb2.Client
envCache map[string]*envCacheEntry queryApi api.QueryAPI
zCtx ziti.Context
zListener edge.Listener
shutdown chan struct{}
joined chan struct{}
} }
type envCacheEntry struct { func newGetAccountMetricsHandler(cfg *metrics.InfluxConfig) *getAccountMetricsHandler {
env string idb := influxdb2.NewClient(cfg.Url, cfg.Token)
lastAccess time.Time queryApi := idb.QueryAPI(cfg.Org)
} return &getAccountMetricsHandler{
cfg: cfg,
func newMetricsAgent() *metricsAgent { idb: idb,
ma := &metricsAgent{ queryApi: queryApi,
metricsQueue: make(chan *model.Metrics, 1024),
envCache: make(map[string]*envCacheEntry),
shutdown: make(chan struct{}),
joined: make(chan struct{}),
}
if idb != nil {
ma.writeApi = idb.WriteAPIBlocking(cfg.Influx.Org, cfg.Influx.Bucket)
}
return ma
}
func (ma *metricsAgent) run() {
logrus.Info("starting")
defer logrus.Info("exiting")
defer close(ma.joined)
if err := ma.bindService(); err != nil {
logrus.Errorf("error binding metrics service: %v", err)
return
}
work:
for {
select {
case <-ma.shutdown:
break work
case m := <-ma.metricsQueue:
if err := ma.processMetrics(m); err != nil {
logrus.Errorf("error processing metrics: %v", err)
}
}
}
if err := ma.zListener.Close(); err != nil {
logrus.Errorf("error closing metrics service listener: %v", err)
} }
} }
func (ma *metricsAgent) bindService() error { func (h *getAccountMetricsHandler) Handle(params metadata.GetAccountMetricsParams, principal *rest_model_zrok.Principal) middleware.Responder {
zif, err := zrokdir.ZitiIdentityFile("ctrl") duration := 30 * 24 * time.Hour
if err != nil { if params.Duration != nil {
return errors.Wrap(err, "error getting 'ctrl' identity") v, err := time.ParseDuration(*params.Duration)
}
zCfg, err := config.NewFromFile(zif)
if err != nil {
return errors.Wrap(err, "error loading 'ctrl' identity")
}
ma.zCtx = ziti.NewContextWithConfig(zCfg)
opts := &ziti.ListenOptions{
ConnectTimeout: 5 * time.Minute,
MaxConnections: 1024,
}
ma.zListener, err = ma.zCtx.ListenWithOptions(cfg.Metrics.ServiceName, opts)
if err != nil {
return errors.Wrapf(err, "error listening for metrics on '%v'", cfg.Metrics.ServiceName)
}
go ma.listen()
return nil
}
func (ma *metricsAgent) listen() {
logrus.Info("started")
defer logrus.Info("exited")
for {
conn, err := ma.zListener.Accept()
if err != nil { if err != nil {
logrus.Errorf("error accepting: %v", err) logrus.Errorf("bad duration '%v' for '%v': %v", *params.Duration, principal.Email, err)
return return metadata.NewGetAccountMetricsBadRequest()
} }
logrus.Debugf("accepted metrics connetion from '%v'", conn.RemoteAddr()) duration = v
go newMetricsHandler(conn, ma.metricsQueue).run() }
slice := sliceSize(duration)
query := fmt.Sprintf("from(bucket: \"%v\")\n", h.cfg.Bucket) +
fmt.Sprintf("|> range(start: -%v)\n", duration) +
"|> filter(fn: (r) => r[\"_measurement\"] == \"xfer\")\n" +
"|> filter(fn: (r) => r[\"_field\"] == \"rx\" or r[\"_field\"] == \"tx\")\n" +
"|> filter(fn: (r) => r[\"namespace\"] == \"backend\")\n" +
fmt.Sprintf("|> filter(fn: (r) => r[\"acctId\"] == \"%d\")\n", principal.ID) +
"|> drop(columns: [\"share\", \"envId\"])\n" +
fmt.Sprintf("|> aggregateWindow(every: %v, fn: sum, createEmpty: true)", slice)
rx, tx, timestamps, err := runFluxForRxTxArray(query, h.queryApi)
if err != nil {
logrus.Errorf("error running account metrics query for '%v': %v", principal.Email, err)
return metadata.NewGetAccountMetricsInternalServerError()
}
response := &rest_model_zrok.Metrics{
Scope: "account",
ID: fmt.Sprintf("%d", principal.ID),
Period: duration.Seconds(),
}
for i := 0; i < len(rx) && i < len(tx) && i < len(timestamps); i++ {
response.Samples = append(response.Samples, &rest_model_zrok.MetricsSample{
Rx: rx[i],
Tx: tx[i],
Timestamp: timestamps[i],
})
}
return metadata.NewGetAccountMetricsOK().WithPayload(response)
}
type getEnvironmentMetricsHandler struct {
cfg *metrics.InfluxConfig
idb influxdb2.Client
queryApi api.QueryAPI
}
func newGetEnvironmentMetricsHandler(cfg *metrics.InfluxConfig) *getEnvironmentMetricsHandler {
idb := influxdb2.NewClient(cfg.Url, cfg.Token)
queryApi := idb.QueryAPI(cfg.Org)
return &getEnvironmentMetricsHandler{
cfg: cfg,
idb: idb,
queryApi: queryApi,
} }
} }
func (ma *metricsAgent) processMetrics(m *model.Metrics) error { func (h *getEnvironmentMetricsHandler) Handle(params metadata.GetEnvironmentMetricsParams, principal *rest_model_zrok.Principal) middleware.Responder {
var pts []*write.Point trx, err := str.Begin()
if len(m.Sessions) > 0 { if err != nil {
out := "metrics = {\n" logrus.Errorf("error starting transaction: %v", err)
for k, v := range m.Sessions { return metadata.NewGetEnvironmentMetricsInternalServerError()
if ma.writeApi != nil { }
pt := influxdb2.NewPoint("xfer", defer func() { _ = trx.Rollback() }()
map[string]string{"namespace": m.Namespace, "share": k}, env, err := str.FindEnvironmentForAccount(params.EnvID, int(principal.ID), trx)
map[string]interface{}{"bytesRead": v.BytesRead, "bytesWritten": v.BytesWritten}, if err != nil {
time.UnixMilli(v.LastUpdate)) logrus.Errorf("error finding environment '%s' for '%s': %v", params.EnvID, principal.Email, err)
pts = append(pts, pt) return metadata.NewGetEnvironmentMetricsUnauthorized()
}
out += fmt.Sprintf("\t[%v.%v]: %v/%v (%v)\n", m.Namespace, k, util.BytesToSize(v.BytesRead), util.BytesToSize(v.BytesWritten), time.Since(time.UnixMilli(v.LastUpdate)))
}
out += "}"
logrus.Info(out)
} }
if len(pts) > 0 { duration := 30 * 24 * time.Hour
if err := ma.writeApi.WritePoint(context.Background(), pts...); err == nil { if params.Duration != nil {
logrus.Debugf("wrote metrics to influx") v, err := time.ParseDuration(*params.Duration)
} else {
return err
}
}
return nil
}
func (ma *metricsAgent) stop() {
close(ma.shutdown)
}
func (ma *metricsAgent) join() {
<-ma.joined
}
type metricsHandler struct {
conn net.Conn
metricsQueue chan *model.Metrics
}
func newMetricsHandler(conn net.Conn, metricsQueue chan *model.Metrics) *metricsHandler {
return &metricsHandler{conn, metricsQueue}
}
func (mh *metricsHandler) run() {
logrus.Debugf("handling metrics connection: %v", mh.conn.RemoteAddr())
var mtrBuf bytes.Buffer
buf := make([]byte, 4096)
for {
n, err := mh.conn.Read(buf)
if err != nil { if err != nil {
break logrus.Errorf("bad duration '%v' for '%v': %v", *params.Duration, principal.Email, err)
return metadata.NewGetAccountMetricsBadRequest()
} }
mtrBuf.Write(buf[:n]) duration = v
} }
if err := mh.conn.Close(); err != nil { slice := sliceSize(duration)
logrus.Errorf("error closing metrics connection")
query := fmt.Sprintf("from(bucket: \"%v\")\n", h.cfg.Bucket) +
fmt.Sprintf("|> range(start: -%v)\n", duration) +
"|> filter(fn: (r) => r[\"_measurement\"] == \"xfer\")\n" +
"|> filter(fn: (r) => r[\"_field\"] == \"rx\" or r[\"_field\"] == \"tx\")\n" +
"|> filter(fn: (r) => r[\"namespace\"] == \"backend\")\n" +
fmt.Sprintf("|> filter(fn: (r) => r[\"envId\"] == \"%d\")\n", int64(env.Id)) +
"|> drop(columns: [\"share\", \"acctId\"])\n" +
fmt.Sprintf("|> aggregateWindow(every: %v, fn: sum, createEmpty: true)", slice)
rx, tx, timestamps, err := runFluxForRxTxArray(query, h.queryApi)
if err != nil {
logrus.Errorf("error running account metrics query for '%v': %v", principal.Email, err)
return metadata.NewGetAccountMetricsInternalServerError()
} }
m := &model.Metrics{}
if err := bson.Unmarshal(mtrBuf.Bytes(), &m); err == nil { response := &rest_model_zrok.Metrics{
mh.metricsQueue <- m Scope: "account",
} else { ID: fmt.Sprintf("%d", principal.ID),
logrus.Errorf("error unmarshaling metrics: %v", err) Period: duration.Seconds(),
}
for i := 0; i < len(rx) && i < len(tx) && i < len(timestamps); i++ {
response.Samples = append(response.Samples, &rest_model_zrok.MetricsSample{
Rx: rx[i],
Tx: tx[i],
Timestamp: timestamps[i],
})
}
return metadata.NewGetEnvironmentMetricsOK().WithPayload(response)
}
type getShareMetricsHandler struct {
cfg *metrics.InfluxConfig
idb influxdb2.Client
queryApi api.QueryAPI
}
func newGetShareMetricsHandler(cfg *metrics.InfluxConfig) *getShareMetricsHandler {
idb := influxdb2.NewClient(cfg.Url, cfg.Token)
queryApi := idb.QueryAPI(cfg.Org)
return &getShareMetricsHandler{
cfg: cfg,
idb: idb,
queryApi: queryApi,
}
}
func (h *getShareMetricsHandler) Handle(params metadata.GetShareMetricsParams, principal *rest_model_zrok.Principal) middleware.Responder {
trx, err := str.Begin()
if err != nil {
logrus.Errorf("error starting transaction: %v", err)
return metadata.NewGetEnvironmentMetricsInternalServerError()
}
defer func() { _ = trx.Rollback() }()
shr, err := str.FindShareWithToken(params.ShrToken, trx)
if err != nil {
logrus.Errorf("error finding share '%v' for '%v': %v", params.ShrToken, principal.Email, err)
return metadata.NewGetShareMetricsUnauthorized()
}
env, err := str.GetEnvironment(shr.EnvironmentId, trx)
if err != nil {
logrus.Errorf("error finding environment '%d' for '%v': %v", shr.EnvironmentId, principal.Email, err)
return metadata.NewGetShareMetricsUnauthorized()
}
if env.AccountId != nil && int64(*env.AccountId) != principal.ID {
logrus.Errorf("user '%v' does not own share '%v'", principal.Email, params.ShrToken)
return metadata.NewGetShareMetricsUnauthorized()
}
duration := 30 * 24 * time.Hour
if params.Duration != nil {
v, err := time.ParseDuration(*params.Duration)
if err != nil {
logrus.Errorf("bad duration '%v' for '%v': %v", *params.Duration, principal.Email, err)
return metadata.NewGetAccountMetricsBadRequest()
}
duration = v
}
slice := sliceSize(duration)
query := fmt.Sprintf("from(bucket: \"%v\")\n", h.cfg.Bucket) +
fmt.Sprintf("|> range(start: -%v)\n", duration) +
"|> filter(fn: (r) => r[\"_measurement\"] == \"xfer\")\n" +
"|> filter(fn: (r) => r[\"_field\"] == \"rx\" or r[\"_field\"] == \"tx\")\n" +
"|> filter(fn: (r) => r[\"namespace\"] == \"backend\")\n" +
fmt.Sprintf("|> filter(fn: (r) => r[\"share\"] == \"%v\")\n", shr.Token) +
fmt.Sprintf("|> aggregateWindow(every: %v, fn: sum, createEmpty: true)", slice)
rx, tx, timestamps, err := runFluxForRxTxArray(query, h.queryApi)
if err != nil {
logrus.Errorf("error running account metrics query for '%v': %v", principal.Email, err)
return metadata.NewGetAccountMetricsInternalServerError()
}
response := &rest_model_zrok.Metrics{
Scope: "account",
ID: fmt.Sprintf("%d", principal.ID),
Period: duration.Seconds(),
}
for i := 0; i < len(rx) && i < len(tx) && i < len(timestamps); i++ {
response.Samples = append(response.Samples, &rest_model_zrok.MetricsSample{
Rx: rx[i],
Tx: tx[i],
Timestamp: timestamps[i],
})
}
return metadata.NewGetShareMetricsOK().WithPayload(response)
}
func runFluxForRxTxArray(query string, queryApi api.QueryAPI) (rx, tx, timestamps []float64, err error) {
result, err := queryApi.Query(context.Background(), query)
if err != nil {
return nil, nil, nil, err
}
for result.Next() {
switch result.Record().Field() {
case "rx":
rxV := int64(0)
if v, ok := result.Record().Value().(int64); ok {
rxV = v
}
rx = append(rx, float64(rxV))
timestamps = append(timestamps, float64(result.Record().Time().UnixMilli()))
case "tx":
txV := int64(0)
if v, ok := result.Record().Value().(int64); ok {
txV = v
}
tx = append(tx, float64(txV))
}
}
return rx, tx, timestamps, nil
}
func sliceSize(duration time.Duration) time.Duration {
switch duration {
case 30 * 24 * time.Hour:
return 24 * time.Hour
case 7 * 24 * time.Hour:
return 4 * time.Hour
case 24 * time.Hour:
return 30 * time.Minute
default:
return duration
} }
} }

View File

@ -0,0 +1,80 @@
package metrics
import (
"github.com/openziti/zrok/controller/store"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
type Agent struct {
events chan ZitiEventMsg
src ZitiEventJsonSource
srcJoin chan struct{}
cache *cache
snks []UsageSink
}
func NewAgent(cfg *AgentConfig, str *store.Store, ifxCfg *InfluxConfig) (*Agent, error) {
a := &Agent{}
if v, ok := cfg.Source.(ZitiEventJsonSource); ok {
a.src = v
} else {
return nil, errors.New("invalid event json source")
}
a.cache = newShareCache(str)
a.snks = append(a.snks, newInfluxWriter(ifxCfg))
return a, nil
}
func (a *Agent) AddUsageSink(snk UsageSink) {
a.snks = append(a.snks, snk)
}
func (a *Agent) Start() error {
a.events = make(chan ZitiEventMsg)
srcJoin, err := a.src.Start(a.events)
if err != nil {
return err
}
a.srcJoin = srcJoin
go func() {
logrus.Info("started")
defer logrus.Info("stopped")
for {
select {
case event := <-a.events:
if usage, err := Ingest(event.Data()); err == nil {
if usage.ZitiServiceId != "" {
if err := a.cache.addZrokDetail(usage); err != nil {
logrus.Errorf("unable to add zrok detail for: %v: %v", usage.String(), err)
}
}
shouldAck := true
for _, snk := range a.snks {
if err := snk.Handle(usage); err != nil {
logrus.Errorf("error handling usage: %v", err)
if shouldAck {
shouldAck = false
}
}
}
if shouldAck {
if err := event.Ack(); err != nil {
logrus.Errorf("unable to ack handled message: %v", err)
}
}
} else {
logrus.Errorf("unable to ingest '%v': %v", event.Data(), err)
}
}
}
}()
return nil
}
func (a *Agent) Stop() {
a.src.Stop()
close(a.events)
}

View File

@ -0,0 +1,81 @@
package metrics
import (
"context"
"github.com/michaelquigley/cf"
"github.com/openziti/zrok/controller/env"
"github.com/pkg/errors"
amqp "github.com/rabbitmq/amqp091-go"
"github.com/sirupsen/logrus"
"time"
)
func init() {
env.GetCfOptions().AddFlexibleSetter("amqpSink", loadAmqpSinkConfig)
}
type AmqpSinkConfig struct {
Url string `cf:"+secret"`
QueueName string
}
func loadAmqpSinkConfig(v interface{}, _ *cf.Options) (interface{}, error) {
if submap, ok := v.(map[string]interface{}); ok {
cfg := &AmqpSinkConfig{}
if err := cf.Bind(cfg, submap, cf.DefaultOptions()); err != nil {
return nil, err
}
return newAmqpSink(cfg)
}
return nil, errors.New("invalid config structure for 'amqpSink'")
}
type amqpSink struct {
cfg *AmqpSinkConfig
conn *amqp.Connection
ch *amqp.Channel
queue amqp.Queue
connected bool
}
func newAmqpSink(cfg *AmqpSinkConfig) (*amqpSink, error) {
as := &amqpSink{cfg: cfg}
return as, nil
}
func (s *amqpSink) Handle(event ZitiEventJson) error {
if !s.connected {
if err := s.connect(); err != nil {
return err
}
logrus.Infof("connected to '%v'", s.cfg.Url)
s.connected = true
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
logrus.Infof("pushing '%v'", event)
err := s.ch.PublishWithContext(ctx, "", s.queue.Name, false, false, amqp.Publishing{
ContentType: "application/json",
Body: []byte(event),
})
if err != nil {
s.connected = false
}
return err
}
func (s *amqpSink) connect() (err error) {
s.conn, err = amqp.Dial(s.cfg.Url)
if err != nil {
return errors.Wrapf(err, "error dialing '%v'", s.cfg.Url)
}
s.ch, err = s.conn.Channel()
if err != nil {
return errors.Wrapf(err, "error getting amqp channel from '%v'", s.cfg.Url)
}
s.queue, err = s.ch.QueueDeclare(s.cfg.QueueName, true, false, false, false, nil)
if err != nil {
return errors.Wrapf(err, "error declaring queue '%v' with '%v'", s.cfg.QueueName, s.cfg.Url)
}
return nil
}

View File

@ -0,0 +1,143 @@
package metrics
import (
"github.com/michaelquigley/cf"
"github.com/openziti/zrok/controller/env"
"github.com/pkg/errors"
amqp "github.com/rabbitmq/amqp091-go"
"github.com/sirupsen/logrus"
"time"
)
func init() {
env.GetCfOptions().AddFlexibleSetter("amqpSource", loadAmqpSourceConfig)
}
type AmqpSourceConfig struct {
Url string `cf:"+secret"`
QueueName string
}
func loadAmqpSourceConfig(v interface{}, _ *cf.Options) (interface{}, error) {
if submap, ok := v.(map[string]interface{}); ok {
cfg := &AmqpSourceConfig{}
if err := cf.Bind(cfg, submap, cf.DefaultOptions()); err != nil {
return nil, err
}
return newAmqpSource(cfg)
}
return nil, errors.New("invalid config structure for 'amqpSource'")
}
type amqpSource struct {
cfg *AmqpSourceConfig
conn *amqp.Connection
ch *amqp.Channel
queue amqp.Queue
msgs <-chan amqp.Delivery
errs chan *amqp.Error
events chan ZitiEventMsg
close chan struct{}
join chan struct{}
}
func newAmqpSource(cfg *AmqpSourceConfig) (*amqpSource, error) {
as := &amqpSource{
cfg: cfg,
close: make(chan struct{}),
join: make(chan struct{}),
}
return as, nil
}
func (s *amqpSource) Start(events chan ZitiEventMsg) (join chan struct{}, err error) {
s.events = events
go s.run()
return s.join, nil
}
func (s *amqpSource) Stop() {
close(s.close)
<-s.join
}
func (s *amqpSource) run() {
logrus.Info("started")
defer logrus.Info("stopped")
defer close(s.join)
mainLoop:
for {
logrus.Infof("connecting to '%v'", s.cfg.Url)
if err := s.connect(); err != nil {
logrus.Errorf("error connecting to '%v': %v", s.cfg.Url, err)
select {
case <-time.After(10 * time.Second):
continue mainLoop
case <-s.close:
break mainLoop
}
}
logrus.Infof("connected to '%v'", s.cfg.Url)
msgLoop:
for {
select {
case err, ok := <-s.errs:
if err != nil || !ok {
logrus.Error(err)
break msgLoop
}
case <-s.close:
break mainLoop
case event, ok := <-s.msgs:
if !ok {
logrus.Debug("selecting on msg !ok")
break msgLoop
}
if event.Body != nil {
s.events <- &ZitiEventAMQP{
data: ZitiEventJson(event.Body),
msg: event,
}
} else {
logrus.Debug("event body was nil!")
break msgLoop
}
}
}
}
}
func (s *amqpSource) connect() error {
conn, err := amqp.Dial(s.cfg.Url)
if err != nil {
return errors.Wrap(err, "error dialing amqp broker")
}
ch, err := conn.Channel()
if err != nil {
return errors.Wrap(err, "error getting amqp channel")
}
queue, err := ch.QueueDeclare(s.cfg.QueueName, true, false, false, false, nil)
if err != nil {
return errors.Wrap(err, "error declaring queue")
}
msgs, err := ch.Consume(s.cfg.QueueName, "zrok", false, false, false, false, nil)
if err != nil {
return errors.Wrap(err, "error consuming")
}
s.errs = make(chan *amqp.Error)
conn.NotifyClose(s.errs)
s.conn = conn
s.ch = ch
s.queue = queue
s.msgs = msgs
return nil
}

View File

@ -0,0 +1,78 @@
package metrics
import (
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
type BridgeConfig struct {
Source interface{}
Sink interface{}
}
type Bridge struct {
src ZitiEventJsonSource
srcJoin chan struct{}
snk ZitiEventJsonSink
events chan ZitiEventMsg
close chan struct{}
join chan struct{}
}
func NewBridge(cfg *BridgeConfig) (*Bridge, error) {
b := &Bridge{
events: make(chan ZitiEventMsg),
join: make(chan struct{}),
close: make(chan struct{}),
}
if v, ok := cfg.Source.(ZitiEventJsonSource); ok {
b.src = v
} else {
return nil, errors.New("invalid source type")
}
if v, ok := cfg.Sink.(ZitiEventJsonSink); ok {
b.snk = v
} else {
return nil, errors.New("invalid sink type")
}
return b, nil
}
func (b *Bridge) Start() (join chan struct{}, err error) {
if b.srcJoin, err = b.src.Start(b.events); err != nil {
return nil, err
}
go func() {
logrus.Info("started")
defer logrus.Info("stopped")
defer close(b.join)
eventLoop:
for {
select {
case eventJson := <-b.events:
logrus.Info(eventJson)
if err := b.snk.Handle(eventJson.Data()); err == nil {
logrus.Infof("-> %v", eventJson.Data())
} else {
logrus.Error(err)
}
eventJson.Ack()
case <-b.close:
logrus.Info("received close signal")
break eventLoop
}
}
}()
return b.join, nil
}
func (b *Bridge) Stop() {
b.src.Stop()
close(b.close)
<-b.srcJoin
<-b.join
}

View File

@ -0,0 +1,35 @@
package metrics
import (
"github.com/openziti/zrok/controller/store"
)
type cache struct {
str *store.Store
}
func newShareCache(str *store.Store) *cache {
return &cache{str}
}
func (c *cache) addZrokDetail(u *Usage) error {
tx, err := c.str.Begin()
if err != nil {
return err
}
defer func() { _ = tx.Rollback() }()
shr, err := c.str.FindShareWithZIdAndDeleted(u.ZitiServiceId, tx)
if err != nil {
return err
}
u.ShareToken = shr.Token
env, err := c.str.GetEnvironment(shr.EnvironmentId, tx)
if err != nil {
return err
}
u.EnvironmentId = int64(env.Id)
u.AccountId = int64(*env.AccountId)
return nil
}

View File

@ -0,0 +1,17 @@
package metrics
type Config struct {
Influx *InfluxConfig
Agent *AgentConfig
}
type AgentConfig struct {
Source interface{}
}
type InfluxConfig struct {
Url string
Bucket string
Org string
Token string `cf:"+secret"`
}

View File

@ -0,0 +1,133 @@
package metrics
import (
"encoding/binary"
"os"
"github.com/michaelquigley/cf"
"github.com/nxadm/tail"
"github.com/openziti/zrok/controller/env"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
func init() {
env.GetCfOptions().AddFlexibleSetter("fileSource", loadFileSourceConfig)
}
type FileSourceConfig struct {
Path string
PointerPath string
}
func loadFileSourceConfig(v interface{}, _ *cf.Options) (interface{}, error) {
if submap, ok := v.(map[string]interface{}); ok {
cfg := &FileSourceConfig{}
if err := cf.Bind(cfg, submap, cf.DefaultOptions()); err != nil {
return nil, err
}
return &fileSource{cfg: cfg}, nil
}
return nil, errors.New("invalid config structure for 'fileSource'")
}
type fileSource struct {
cfg *FileSourceConfig
ptrF *os.File
t *tail.Tail
}
func (s *fileSource) Start(events chan ZitiEventMsg) (join chan struct{}, err error) {
f, err := os.Open(s.cfg.Path)
if err != nil {
return nil, errors.Wrapf(err, "error opening '%v'", s.cfg.Path)
}
_ = f.Close()
s.ptrF, err = os.OpenFile(s.pointerPath(), os.O_CREATE|os.O_RDWR, os.ModePerm)
if err != nil {
return nil, errors.Wrapf(err, "error opening pointer '%v'", s.pointerPath())
}
ptr, err := s.readPtr()
if err != nil {
logrus.Errorf("error reading pointer: %v", err)
}
logrus.Infof("retrieved stored position pointer at '%d'", ptr)
join = make(chan struct{})
go func() {
s.tail(ptr, events)
close(join)
}()
return join, nil
}
func (s *fileSource) Stop() {
if err := s.t.Stop(); err != nil {
logrus.Error(err)
}
}
func (s *fileSource) tail(ptr int64, events chan ZitiEventMsg) {
logrus.Info("started")
defer logrus.Info("stopped")
var err error
s.t, err = tail.TailFile(s.cfg.Path, tail.Config{
ReOpen: true,
Follow: true,
Location: &tail.SeekInfo{Offset: ptr},
})
if err != nil {
logrus.Errorf("error starting tail: %v", err)
return
}
for event := range s.t.Lines {
events <- &ZitiEventJsonMsg{
data: ZitiEventJson(event.Text),
}
if err := s.writePtr(event.SeekInfo.Offset); err != nil {
logrus.Error(err)
}
}
}
func (s *fileSource) pointerPath() string {
if s.cfg.PointerPath == "" {
return s.cfg.Path + ".ptr"
} else {
return s.cfg.PointerPath
}
}
func (s *fileSource) readPtr() (int64, error) {
ptr := int64(0)
buf := make([]byte, 8)
if n, err := s.ptrF.Seek(0, 0); err == nil && n == 0 {
if n, err := s.ptrF.Read(buf); err == nil && n == 8 {
ptr = int64(binary.LittleEndian.Uint64(buf))
return ptr, nil
} else {
return 0, errors.Wrapf(err, "error reading pointer (%d): %v", n, err)
}
} else {
return 0, errors.Wrapf(err, "error seeking pointer (%d): %v", n, err)
}
}
func (s *fileSource) writePtr(ptr int64) error {
buf := make([]byte, 8)
binary.LittleEndian.PutUint64(buf, uint64(ptr))
if n, err := s.ptrF.Seek(0, 0); err == nil && n == 0 {
if n, err := s.ptrF.Write(buf); err != nil || n != 8 {
return errors.Wrapf(err, "error writing pointer (%d): %v", n, err)
}
} else {
return errors.Wrapf(err, "error seeking pointer (%d): %v", n, err)
}
return nil
}

View File

@ -0,0 +1,63 @@
package metrics
import (
"context"
"fmt"
influxdb2 "github.com/influxdata/influxdb-client-go/v2"
"github.com/influxdata/influxdb-client-go/v2/api"
"github.com/influxdata/influxdb-client-go/v2/api/write"
"github.com/openziti/zrok/util"
"github.com/sirupsen/logrus"
)
type influxWriter struct {
idb influxdb2.Client
writeApi api.WriteAPIBlocking
}
func newInfluxWriter(cfg *InfluxConfig) *influxWriter {
idb := influxdb2.NewClient(cfg.Url, cfg.Token)
writeApi := idb.WriteAPIBlocking(cfg.Org, cfg.Bucket)
return &influxWriter{idb, writeApi}
}
func (w *influxWriter) Handle(u *Usage) error {
if u.ShareToken != "" {
out := fmt.Sprintf("share: %v, circuit: %v", u.ShareToken, u.ZitiCircuitId)
envId := fmt.Sprintf("%d", u.EnvironmentId)
acctId := fmt.Sprintf("%d", u.AccountId)
var pts []*write.Point
circuitPt := influxdb2.NewPoint("circuits",
map[string]string{"share": u.ShareToken, "envId": envId, "acctId": acctId},
map[string]interface{}{"circuit": u.ZitiCircuitId},
u.IntervalStart)
pts = append(pts, circuitPt)
if u.BackendTx > 0 || u.BackendRx > 0 {
pt := influxdb2.NewPoint("xfer",
map[string]string{"namespace": "backend", "share": u.ShareToken, "envId": envId, "acctId": acctId},
map[string]interface{}{"rx": u.BackendRx, "tx": u.BackendTx},
u.IntervalStart)
pts = append(pts, pt)
out += fmt.Sprintf(" backend {rx: %v, tx: %v}", util.BytesToSize(u.BackendRx), util.BytesToSize(u.BackendTx))
}
if u.FrontendTx > 0 || u.FrontendRx > 0 {
pt := influxdb2.NewPoint("xfer",
map[string]string{"namespace": "frontend", "share": u.ShareToken, "envId": envId, "acctId": acctId},
map[string]interface{}{"rx": u.FrontendRx, "tx": u.FrontendTx},
u.IntervalStart)
pts = append(pts, pt)
out += fmt.Sprintf(" frontend {rx: %v, tx: %v}", util.BytesToSize(u.FrontendRx), util.BytesToSize(u.FrontendTx))
}
if err := w.writeApi.WritePoint(context.Background(), pts...); err == nil {
logrus.Info(out)
} else {
return err
}
}
return nil
}

View File

@ -0,0 +1,83 @@
package metrics
import (
"fmt"
"time"
"github.com/openziti/zrok/util"
amqp "github.com/rabbitmq/amqp091-go"
)
type Usage struct {
ProcessedStamp time.Time
IntervalStart time.Time
ZitiServiceId string
ZitiCircuitId string
ShareToken string
EnvironmentId int64
AccountId int64
FrontendTx int64
FrontendRx int64
BackendTx int64
BackendRx int64
}
func (u Usage) String() string {
out := "Usage {"
out += fmt.Sprintf("processed '%v'", u.ProcessedStamp)
out += ", " + fmt.Sprintf("interval '%v'", u.IntervalStart)
out += ", " + fmt.Sprintf("service '%v'", u.ZitiServiceId)
out += ", " + fmt.Sprintf("circuit '%v'", u.ZitiCircuitId)
out += ", " + fmt.Sprintf("share '%v'", u.ShareToken)
out += ", " + fmt.Sprintf("environment '%d'", u.EnvironmentId)
out += ", " + fmt.Sprintf("account '%v'", u.AccountId)
out += ", " + fmt.Sprintf("fe {rx %v, tx %v}", util.BytesToSize(u.FrontendRx), util.BytesToSize(u.FrontendTx))
out += ", " + fmt.Sprintf("be {rx %v, tx %v}", util.BytesToSize(u.BackendRx), util.BytesToSize(u.BackendTx))
out += "}"
return out
}
type UsageSink interface {
Handle(u *Usage) error
}
type ZitiEventJson string
type ZitiEventJsonMsg struct {
data ZitiEventJson
}
func (e *ZitiEventJsonMsg) Data() ZitiEventJson {
return e.data
}
func (e *ZitiEventJsonMsg) Ack() error {
return nil
}
type ZitiEventAMQP struct {
data ZitiEventJson
msg amqp.Delivery
}
func (e *ZitiEventAMQP) Data() ZitiEventJson {
return e.data
}
func (e *ZitiEventAMQP) Ack() error {
return e.msg.Ack(false)
}
type ZitiEventMsg interface {
Data() ZitiEventJson
Ack() error
}
type ZitiEventJsonSource interface {
Start(chan ZitiEventMsg) (join chan struct{}, err error)
Stop()
}
type ZitiEventJsonSink interface {
Handle(event ZitiEventJson) error
}

View File

@ -0,0 +1,94 @@
package metrics
import (
"encoding/json"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"reflect"
"time"
)
func Ingest(event ZitiEventJson) (*Usage, error) {
eventMap := make(map[string]interface{})
if err := json.Unmarshal([]byte(event), &eventMap); err == nil {
u := &Usage{ProcessedStamp: time.Now()}
if ns, found := eventMap["namespace"]; found && ns == "fabric.usage" {
if v, found := eventMap["interval_start_utc"]; found {
if vFloat64, ok := v.(float64); ok {
u.IntervalStart = time.Unix(int64(vFloat64), 0)
} else {
logrus.Errorf("unable to assert 'interval_start_utc': %v", event)
}
} else {
logrus.Errorf("missing 'interval_start_utc': %v", event)
}
if v, found := eventMap["tags"]; found {
if tags, ok := v.(map[string]interface{}); ok {
if v, found := tags["serviceId"]; found {
if vStr, ok := v.(string); ok {
u.ZitiServiceId = vStr
} else {
logrus.Errorf("unable to assert 'tags/serviceId': %v", event)
}
} else {
logrus.Errorf("missing 'tags/serviceId': %v", event)
}
} else {
logrus.Errorf("unable to assert 'tags': %v", event)
}
} else {
logrus.Errorf("missing 'tags': %v", event)
}
if v, found := eventMap["usage"]; found {
if usage, ok := v.(map[string]interface{}); ok {
if v, found := usage["ingress.tx"]; found {
if vFloat64, ok := v.(float64); ok {
u.FrontendTx = int64(vFloat64)
} else {
logrus.Errorf("unable to assert 'usage/ingress.tx': %v", event)
}
}
if v, found := usage["ingress.rx"]; found {
if vFloat64, ok := v.(float64); ok {
u.FrontendRx = int64(vFloat64)
} else {
logrus.Errorf("unable to assert 'usage/ingress.rx': %v", event)
}
}
if v, found := usage["egress.tx"]; found {
if vFloat64, ok := v.(float64); ok {
u.BackendRx = int64(vFloat64)
} else {
logrus.Errorf("unable to assert 'usage/egress.tx': %v", event)
}
}
if v, found := usage["egress.rx"]; found {
if vFloat64, ok := v.(float64); ok {
u.BackendTx = int64(vFloat64)
} else {
logrus.Errorf("unable to assert 'usage/egress.rx': %v", event)
}
}
} else {
logrus.Errorf("unable to assert 'usage' (%v) %v", reflect.TypeOf(v), event)
}
} else {
logrus.Warnf("missing 'usage': %v", event)
}
if v, found := eventMap["circuit_id"]; found {
if vStr, ok := v.(string); ok {
u.ZitiCircuitId = vStr
} else {
logrus.Errorf("unable to assert 'circuit_id': %v", event)
}
} else {
logrus.Warnf("missing 'circuit_id': %v", event)
}
} else {
logrus.Errorf("not 'fabric.usage': %v", event)
}
return u, nil
} else {
return nil, errors.Wrap(err, "error unmarshaling")
}
}

View File

@ -0,0 +1,158 @@
package metrics
import (
"crypto/tls"
"crypto/x509"
"encoding/json"
"io"
"net/http"
"net/url"
"time"
"github.com/gorilla/websocket"
"github.com/michaelquigley/cf"
"github.com/openziti/channel/v2"
"github.com/openziti/channel/v2/websockets"
"github.com/openziti/edge-api/rest_util"
"github.com/openziti/fabric/event"
"github.com/openziti/fabric/pb/mgmt_pb"
"github.com/openziti/identity"
"github.com/openziti/zrok/controller/env"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
const ZitiSession = "zt-session"
func init() {
env.GetCfOptions().AddFlexibleSetter("websocketSource", loadWebsocketSourceConfig)
}
type WebsocketSourceConfig struct {
WebsocketEndpoint string // wss://127.0.0.1:1280/fabric/v1/ws-api
ApiEndpoint string // https://127.0.0.1:1280
Username string
Password string `cf:"+secret"`
}
func loadWebsocketSourceConfig(v interface{}, _ *cf.Options) (interface{}, error) {
if submap, ok := v.(map[string]interface{}); ok {
cfg := &WebsocketSourceConfig{}
if err := cf.Bind(cfg, submap, cf.DefaultOptions()); err != nil {
return nil, err
}
return &websocketSource{cfg: cfg}, nil
}
return nil, errors.New("invalid config structure for 'websocketSource'")
}
type websocketSource struct {
cfg *WebsocketSourceConfig
ch channel.Channel
events chan ZitiEventMsg
join chan struct{}
}
func (s *websocketSource) Start(events chan ZitiEventMsg) (join chan struct{}, err error) {
caCerts, err := rest_util.GetControllerWellKnownCas(s.cfg.ApiEndpoint)
if err != nil {
return nil, err
}
caPool := x509.NewCertPool()
for _, ca := range caCerts {
caPool.AddCert(ca)
}
authenticator := rest_util.NewAuthenticatorUpdb(s.cfg.Username, s.cfg.Password)
authenticator.RootCas = caPool
apiEndpointUrl, err := url.Parse(s.cfg.ApiEndpoint)
if err != nil {
return nil, err
}
apiSession, err := authenticator.Authenticate(apiEndpointUrl)
if err != nil {
return nil, err
}
dialer := &websocket.Dialer{
TLSClientConfig: &tls.Config{
RootCAs: caPool,
},
HandshakeTimeout: 5 * time.Second,
}
conn, resp, err := dialer.Dial(s.cfg.WebsocketEndpoint, http.Header{ZitiSession: []string{*apiSession.Token}})
if err != nil {
if resp != nil {
if body, rerr := io.ReadAll(resp.Body); rerr == nil {
logrus.Errorf("response body '%v': %v", string(body), err)
}
} else {
logrus.Errorf("no response from websocket dial: %v", err)
}
}
id := &identity.TokenId{Token: "mgmt"}
underlayFactory := websockets.NewUnderlayFactory(id, conn, nil)
s.join = make(chan struct{})
s.events = events
bindHandler := func(binding channel.Binding) error {
binding.AddReceiveHandler(int32(mgmt_pb.ContentType_StreamEventsEventType), s)
binding.AddCloseHandler(channel.CloseHandlerF(func(ch channel.Channel) {
close(s.join)
}))
return nil
}
s.ch, err = channel.NewChannel("mgmt", underlayFactory, channel.BindHandlerF(bindHandler), nil)
if err != nil {
return nil, err
}
streamEventsRequest := map[string]interface{}{}
streamEventsRequest["format"] = "json"
streamEventsRequest["subscriptions"] = []*event.Subscription{
{
Type: "fabric.usage",
Options: map[string]interface{}{
"version": uint8(3),
},
},
}
msgBytes, err := json.Marshal(streamEventsRequest)
if err != nil {
return nil, err
}
requestMsg := channel.NewMessage(int32(mgmt_pb.ContentType_StreamEventsRequestType), msgBytes)
responseMsg, err := requestMsg.WithTimeout(5 * time.Second).SendForReply(s.ch)
if err != nil {
return nil, err
}
if responseMsg.ContentType == channel.ContentTypeResultType {
result := channel.UnmarshalResult(responseMsg)
if result.Success {
logrus.Infof("event stream started: %v", result.Message)
} else {
return nil, errors.Wrap(err, "error starting event streaming")
}
} else {
return nil, errors.Errorf("unexpected response type %v", responseMsg.ContentType)
}
return s.join, nil
}
func (s *websocketSource) Stop() {
_ = s.ch.Close()
}
func (s *websocketSource) HandleReceive(msg *channel.Message, _ channel.Channel) {
s.events <- &ZitiEventJsonMsg{
data: ZitiEventJson(msg.Body),
}
}

View File

@ -2,41 +2,63 @@ package controller
import ( import (
"github.com/go-openapi/runtime/middleware" "github.com/go-openapi/runtime/middleware"
"github.com/jmoiron/sqlx"
"github.com/openziti/zrok/controller/store"
"github.com/openziti/zrok/rest_model_zrok" "github.com/openziti/zrok/rest_model_zrok"
"github.com/openziti/zrok/rest_server_zrok/operations/metadata" "github.com/openziti/zrok/rest_server_zrok/operations/metadata"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
func overviewHandler(_ metadata.OverviewParams, principal *rest_model_zrok.Principal) middleware.Responder { type overviewHandler struct{}
tx, err := str.Begin()
func newOverviewHandler() *overviewHandler {
return &overviewHandler{}
}
func (h *overviewHandler) Handle(_ metadata.OverviewParams, principal *rest_model_zrok.Principal) middleware.Responder {
trx, err := str.Begin()
if err != nil { if err != nil {
logrus.Errorf("error starting transaction: %v", err) logrus.Errorf("error starting transaction: %v", err)
return metadata.NewOverviewInternalServerError() return metadata.NewOverviewInternalServerError()
} }
defer func() { _ = tx.Rollback() }() defer func() { _ = trx.Rollback() }()
envs, err := str.FindEnvironmentsForAccount(int(principal.ID), tx) envs, err := str.FindEnvironmentsForAccount(int(principal.ID), trx)
if err != nil { if err != nil {
logrus.Errorf("error finding environments for '%v': %v", principal.Email, err) logrus.Errorf("error finding environments for '%v': %v", principal.Email, err)
return metadata.NewOverviewInternalServerError() return metadata.NewOverviewInternalServerError()
} }
var out rest_model_zrok.EnvironmentSharesList elm, err := newEnvironmentsLimitedMap(envs, trx)
if err != nil {
logrus.Errorf("error finding limited environments for '%v': %v", principal.Email, err)
return metadata.NewOverviewInternalServerError()
}
accountLimited, err := h.isAccountLimited(principal, trx)
if err != nil {
logrus.Errorf("error checking account limited for '%v': %v", principal.Email, err)
}
ovr := &rest_model_zrok.Overview{AccountLimited: accountLimited}
for _, env := range envs { for _, env := range envs {
shrs, err := str.FindSharesForEnvironment(env.Id, tx) envRes := &rest_model_zrok.EnvironmentAndResources{
Environment: &rest_model_zrok.Environment{
Address: env.Address,
Description: env.Description,
Host: env.Host,
ZID: env.ZId,
Limited: elm.isLimited(env),
CreatedAt: env.CreatedAt.UnixMilli(),
UpdatedAt: env.UpdatedAt.UnixMilli(),
},
}
shrs, err := str.FindSharesForEnvironment(env.Id, trx)
if err != nil { if err != nil {
logrus.Errorf("error finding shares for environment '%v': %v", env.ZId, err) logrus.Errorf("error finding shares for environment '%v': %v", env.ZId, err)
return metadata.NewOverviewInternalServerError() return metadata.NewOverviewInternalServerError()
} }
es := &rest_model_zrok.EnvironmentShares{ slm, err := newSharesLimitedMap(shrs, trx)
Environment: &rest_model_zrok.Environment{ if err != nil {
Address: env.Address, logrus.Errorf("error finding limited shares for environment '%v': %v", env.ZId, err)
CreatedAt: env.CreatedAt.UnixMilli(), return metadata.NewOverviewInternalServerError()
Description: env.Description,
Host: env.Host,
UpdatedAt: env.UpdatedAt.UnixMilli(),
ZID: env.ZId,
},
} }
for _, shr := range shrs { for _, shr := range shrs {
feEndpoint := "" feEndpoint := ""
if shr.FrontendEndpoint != nil { if shr.FrontendEndpoint != nil {
@ -50,7 +72,7 @@ func overviewHandler(_ metadata.OverviewParams, principal *rest_model_zrok.Princ
if shr.BackendProxyEndpoint != nil { if shr.BackendProxyEndpoint != nil {
beProxyEndpoint = *shr.BackendProxyEndpoint beProxyEndpoint = *shr.BackendProxyEndpoint
} }
es.Shares = append(es.Shares, &rest_model_zrok.Share{ envShr := &rest_model_zrok.Share{
Token: shr.Token, Token: shr.Token,
ZID: shr.ZId, ZID: shr.ZId,
ShareMode: shr.ShareMode, ShareMode: shr.ShareMode,
@ -59,11 +81,104 @@ func overviewHandler(_ metadata.OverviewParams, principal *rest_model_zrok.Princ
FrontendEndpoint: feEndpoint, FrontendEndpoint: feEndpoint,
BackendProxyEndpoint: beProxyEndpoint, BackendProxyEndpoint: beProxyEndpoint,
Reserved: shr.Reserved, Reserved: shr.Reserved,
Limited: slm.isLimited(shr),
CreatedAt: shr.CreatedAt.UnixMilli(), CreatedAt: shr.CreatedAt.UnixMilli(),
UpdatedAt: shr.UpdatedAt.UnixMilli(), UpdatedAt: shr.UpdatedAt.UnixMilli(),
}) }
envRes.Shares = append(envRes.Shares, envShr)
} }
out = append(out, es) fes, err := str.FindFrontendsForEnvironment(env.Id, trx)
if err != nil {
logrus.Errorf("error finding frontends for environment '%v': %v", env.ZId, err)
return metadata.NewOverviewInternalServerError()
}
for _, fe := range fes {
envFe := &rest_model_zrok.Frontend{
ID: int64(fe.Id),
ZID: fe.ZId,
CreatedAt: fe.CreatedAt.UnixMilli(),
UpdatedAt: fe.UpdatedAt.UnixMilli(),
}
if fe.PrivateShareId != nil {
feShr, err := str.GetShare(*fe.PrivateShareId, trx)
if err != nil {
logrus.Errorf("error getting share for frontend '%v': %v", fe.ZId, err)
return metadata.NewOverviewInternalServerError()
}
envFe.ShrToken = feShr.Token
}
envRes.Frontends = append(envRes.Frontends, envFe)
}
ovr.Environments = append(ovr.Environments, envRes)
} }
return metadata.NewOverviewOK().WithPayload(out) return metadata.NewOverviewOK().WithPayload(ovr)
}
func (h *overviewHandler) isAccountLimited(principal *rest_model_zrok.Principal, trx *sqlx.Tx) (bool, error) {
var alj *store.AccountLimitJournal
aljEmpty, err := str.IsAccountLimitJournalEmpty(int(principal.ID), trx)
if err != nil {
return false, err
}
if !aljEmpty {
alj, err = str.FindLatestAccountLimitJournal(int(principal.ID), trx)
if err != nil {
return false, err
}
}
return alj != nil && alj.Action == store.LimitAction, nil
}
type sharesLimitedMap struct {
v map[int]struct{}
}
func newSharesLimitedMap(shrs []*store.Share, trx *sqlx.Tx) (*sharesLimitedMap, error) {
var shrIds []int
for i := range shrs {
shrIds = append(shrIds, shrs[i].Id)
}
shrsLimited, err := str.FindSelectedLatestShareLimitjournal(shrIds, trx)
if err != nil {
return nil, err
}
slm := &sharesLimitedMap{v: make(map[int]struct{})}
for i := range shrsLimited {
if shrsLimited[i].Action == store.LimitAction {
slm.v[shrsLimited[i].ShareId] = struct{}{}
}
}
return slm, nil
}
func (m *sharesLimitedMap) isLimited(shr *store.Share) bool {
_, limited := m.v[shr.Id]
return limited
}
type environmentsLimitedMap struct {
v map[int]struct{}
}
func newEnvironmentsLimitedMap(envs []*store.Environment, trx *sqlx.Tx) (*environmentsLimitedMap, error) {
var envIds []int
for i := range envs {
envIds = append(envIds, envs[i].Id)
}
envsLimited, err := str.FindSelectedLatestEnvironmentLimitJournal(envIds, trx)
if err != nil {
return nil, err
}
elm := &environmentsLimitedMap{v: make(map[int]struct{})}
for i := range envsLimited {
if envsLimited[i].Action == store.LimitAction {
elm.v[envsLimited[i].EnvironmentId] = struct{}{}
}
}
return elm, nil
}
func (m *environmentsLimitedMap) isLimited(env *store.Environment) bool {
_, limited := m.v[env.Id]
return limited
} }

View File

@ -2,16 +2,21 @@ package controller
import ( import (
"github.com/go-openapi/runtime/middleware" "github.com/go-openapi/runtime/middleware"
"github.com/openziti/zrok/controller/config"
"github.com/openziti/zrok/controller/store" "github.com/openziti/zrok/controller/store"
"github.com/openziti/zrok/rest_model_zrok" "github.com/openziti/zrok/rest_model_zrok"
"github.com/openziti/zrok/rest_server_zrok/operations/account" "github.com/openziti/zrok/rest_server_zrok/operations/account"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
type registerHandler struct{} type registerHandler struct {
cfg *config.Config
}
func newRegisterHandler() *registerHandler { func newRegisterHandler(cfg *config.Config) *registerHandler {
return &registerHandler{} return &registerHandler{
cfg: cfg,
}
} }
func (h *registerHandler) Handle(params account.RegisterParams) middleware.Responder { func (h *registerHandler) Handle(params account.RegisterParams) middleware.Responder {
if params.Body == nil || params.Body.Token == "" || params.Body.Password == "" { if params.Body == nil || params.Body.Token == "" || params.Body.Password == "" {
@ -38,6 +43,12 @@ func (h *registerHandler) Handle(params account.RegisterParams) middleware.Respo
logrus.Errorf("error creating token for request '%v' (%v): %v", params.Body.Token, ar.Email, err) logrus.Errorf("error creating token for request '%v' (%v): %v", params.Body.Token, ar.Email, err)
return account.NewRegisterInternalServerError() return account.NewRegisterInternalServerError()
} }
if err := validatePassword(h.cfg, params.Body.Password); err != nil {
logrus.Errorf("password not valid for request '%v', (%v): %v", params.Body.Token, ar.Email, err)
return account.NewRegisterUnprocessableEntity().WithPayload(rest_model_zrok.ErrorMessage(err.Error()))
}
hpwd, err := hashPassword(params.Body.Password) hpwd, err := hashPassword(params.Body.Password)
if err != nil { if err != nil {
logrus.Errorf("error hashing password for request '%v' (%v): %v", params.Body.Token, ar.Email, err) logrus.Errorf("error hashing password for request '%v' (%v): %v", params.Body.Token, ar.Email, err)

View File

@ -2,14 +2,20 @@ package controller
import ( import (
"github.com/go-openapi/runtime/middleware" "github.com/go-openapi/runtime/middleware"
"github.com/openziti/zrok/controller/config"
"github.com/openziti/zrok/rest_model_zrok"
"github.com/openziti/zrok/rest_server_zrok/operations/account" "github.com/openziti/zrok/rest_server_zrok/operations/account"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
type resetPasswordHandler struct{} type resetPasswordHandler struct {
cfg *config.Config
}
func newResetPasswordHandler() *resetPasswordHandler { func newResetPasswordHandler(cfg *config.Config) *resetPasswordHandler {
return &resetPasswordHandler{} return &resetPasswordHandler{
cfg: cfg,
}
} }
func (handler *resetPasswordHandler) Handle(params account.ResetPasswordParams) middleware.Responder { func (handler *resetPasswordHandler) Handle(params account.ResetPasswordParams) middleware.Responder {
@ -37,6 +43,16 @@ func (handler *resetPasswordHandler) Handle(params account.ResetPasswordParams)
logrus.Errorf("error finding account for '%v': %v", params.Body.Token, err) logrus.Errorf("error finding account for '%v': %v", params.Body.Token, err)
return account.NewResetPasswordNotFound() return account.NewResetPasswordNotFound()
} }
if a.Deleted {
logrus.Errorf("account '%v' for '%v' deleted", a.Email, a.Token)
return account.NewResetPasswordNotFound()
}
if err := validatePassword(handler.cfg, params.Body.Password); err != nil {
logrus.Errorf("password not valid for request '%v', (%v): %v", params.Body.Token, a.Email, err)
return account.NewResetPasswordUnprocessableEntity().WithPayload(rest_model_zrok.ErrorMessage(err.Error()))
}
hpwd, err := hashPassword(params.Body.Password) hpwd, err := hashPassword(params.Body.Password)
if err != nil { if err != nil {
logrus.Errorf("error hashing password for '%v' (%v): %v", params.Body.Token, a.Email, err) logrus.Errorf("error hashing password for '%v' (%v): %v", params.Body.Token, a.Email, err)

View File

@ -42,7 +42,7 @@ func (handler *resetPasswordRequestHandler) Handle(params account.ResetPasswordR
a, err := str.FindAccountWithEmail(params.Body.EmailAddress, tx) a, err := str.FindAccountWithEmail(params.Body.EmailAddress, tx)
if err != nil { if err != nil {
logrus.Infof("no account found for '%v': %v", params.Body.EmailAddress, err) logrus.Errorf("no account found for '%v': %v", params.Body.EmailAddress, err)
return account.NewResetPasswordRequestInternalServerError() return account.NewResetPasswordRequestInternalServerError()
} }

View File

@ -4,33 +4,32 @@ import (
"github.com/go-openapi/runtime/middleware" "github.com/go-openapi/runtime/middleware"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
"github.com/openziti/zrok/controller/store" "github.com/openziti/zrok/controller/store"
"github.com/openziti/zrok/controller/zrokEdgeSdk"
"github.com/openziti/zrok/rest_model_zrok" "github.com/openziti/zrok/rest_model_zrok"
"github.com/openziti/zrok/rest_server_zrok/operations/share" "github.com/openziti/zrok/rest_server_zrok/operations/share"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
type shareHandler struct { type shareHandler struct{}
cfg *LimitsConfig
}
func newShareHandler(cfg *LimitsConfig) *shareHandler { func newShareHandler() *shareHandler {
return &shareHandler{cfg: cfg} return &shareHandler{}
} }
func (h *shareHandler) Handle(params share.ShareParams, principal *rest_model_zrok.Principal) middleware.Responder { func (h *shareHandler) Handle(params share.ShareParams, principal *rest_model_zrok.Principal) middleware.Responder {
logrus.Infof("handling") logrus.Info("handling")
tx, err := str.Begin() trx, err := str.Begin()
if err != nil { if err != nil {
logrus.Errorf("error starting transaction: %v", err) logrus.Errorf("error starting transaction: %v", err)
return share.NewShareInternalServerError() return share.NewShareInternalServerError()
} }
defer func() { _ = tx.Rollback() }() defer func() { _ = trx.Rollback() }()
envZId := params.Body.EnvZID envZId := params.Body.EnvZID
envId := 0 envId := 0
envs, err := str.FindEnvironmentsForAccount(int(principal.ID), tx) envs, err := str.FindEnvironmentsForAccount(int(principal.ID), trx)
if err == nil { if err == nil {
found := false found := false
for _, env := range envs { for _, env := range envs {
@ -50,12 +49,12 @@ func (h *shareHandler) Handle(params share.ShareParams, principal *rest_model_zr
return share.NewShareInternalServerError() return share.NewShareInternalServerError()
} }
if err := h.checkLimits(principal, envs, tx); err != nil { if err := h.checkLimits(envId, principal, trx); err != nil {
logrus.Errorf("limits error: %v", err) logrus.Errorf("limits error: %v", err)
return share.NewShareUnauthorized() return share.NewShareUnauthorized()
} }
edge, err := edgeClient() edge, err := zrokEdgeSdk.Client(cfg.Ziti)
if err != nil { if err != nil {
logrus.Error(err) logrus.Error(err)
return share.NewShareInternalServerError() return share.NewShareInternalServerError()
@ -78,7 +77,7 @@ func (h *shareHandler) Handle(params share.ShareParams, principal *rest_model_zr
var frontendZIds []string var frontendZIds []string
var frontendTemplates []string var frontendTemplates []string
for _, frontendSelection := range params.Body.FrontendSelection { for _, frontendSelection := range params.Body.FrontendSelection {
sfe, err := str.FindFrontendPubliclyNamed(frontendSelection, tx) sfe, err := str.FindFrontendPubliclyNamed(frontendSelection, trx)
if err != nil { if err != nil {
logrus.Error(err) logrus.Error(err)
return share.NewShareNotFound() return share.NewShareNotFound()
@ -96,6 +95,7 @@ func (h *shareHandler) Handle(params share.ShareParams, principal *rest_model_zr
} }
case "private": case "private":
logrus.Info("doing private")
shrZId, frontendEndpoints, err = newPrivateResourceAllocator().allocate(envZId, shrToken, params, edge) shrZId, frontendEndpoints, err = newPrivateResourceAllocator().allocate(envZId, shrToken, params, edge)
if err != nil { if err != nil {
logrus.Error(err) logrus.Error(err)
@ -118,19 +118,22 @@ func (h *shareHandler) Handle(params share.ShareParams, principal *rest_model_zr
BackendProxyEndpoint: &params.Body.BackendProxyEndpoint, BackendProxyEndpoint: &params.Body.BackendProxyEndpoint,
Reserved: reserved, Reserved: reserved,
} }
if len(params.Body.FrontendSelection) > 0 {
sshr.FrontendSelection = &params.Body.FrontendSelection[0]
}
if len(frontendEndpoints) > 0 { if len(frontendEndpoints) > 0 {
sshr.FrontendEndpoint = &frontendEndpoints[0] sshr.FrontendEndpoint = &frontendEndpoints[0]
} else if sshr.ShareMode == "private" { } else if sshr.ShareMode == "private" {
sshr.FrontendEndpoint = &sshr.ShareMode sshr.FrontendEndpoint = &sshr.ShareMode
} }
sid, err := str.CreateShare(envId, sshr, tx) sid, err := str.CreateShare(envId, sshr, trx)
if err != nil { if err != nil {
logrus.Errorf("error creating share record: %v", err) logrus.Errorf("error creating share record: %v", err)
return share.NewShareInternalServerError() return share.NewShareInternalServerError()
} }
if err := tx.Commit(); err != nil { if err := trx.Commit(); err != nil {
logrus.Errorf("error committing share record: %v", err) logrus.Errorf("error committing share record: %v", err)
return share.NewShareInternalServerError() return share.NewShareInternalServerError()
} }
@ -142,17 +145,15 @@ func (h *shareHandler) Handle(params share.ShareParams, principal *rest_model_zr
}) })
} }
func (h *shareHandler) checkLimits(principal *rest_model_zrok.Principal, envs []*store.Environment, tx *sqlx.Tx) error { func (h *shareHandler) checkLimits(envId int, principal *rest_model_zrok.Principal, trx *sqlx.Tx) error {
if !principal.Limitless && h.cfg.Shares > Unlimited { if !principal.Limitless {
total := 0 if limitsAgent != nil {
for i := range envs { ok, err := limitsAgent.CanCreateShare(int(principal.ID), envId, trx)
shrs, err := str.FindSharesForEnvironment(envs[i].Id, tx)
if err != nil { if err != nil {
return errors.Errorf("unable to find shares for environment '%v': %v", envs[i].ZId, err) return errors.Wrapf(err, "error checking share limits for '%v'", principal.Email)
} }
total += len(shrs) if !ok {
if total+1 > h.cfg.Shares { return errors.Errorf("share limit check failed for '%v'", principal.Email)
return errors.Errorf("would exceed shares limit of %d for '%v'", h.cfg.Shares, principal.Email)
} }
} }
} }

View File

@ -42,12 +42,15 @@ func (h *shareDetailHandler) Handle(params metadata.GetShareDetailParams, princi
logrus.Errorf("environment not matched for share '%v' for account '%v'", params.ShrToken, principal.Email) logrus.Errorf("environment not matched for share '%v' for account '%v'", params.ShrToken, principal.Email)
return metadata.NewGetShareDetailNotFound() return metadata.NewGetShareDetailNotFound()
} }
var sparkData map[string][]int64 sparkRx := make(map[string][]int64)
if cfg.Influx != nil { sparkTx := make(map[string][]int64)
sparkData, err = sparkDataForShares([]*store.Share{shr}) if cfg.Metrics != nil && cfg.Metrics.Influx != nil {
sparkRx, sparkTx, err = sparkDataForShares([]*store.Share{shr})
if err != nil { if err != nil {
logrus.Errorf("error querying spark data for share: %v", err) logrus.Errorf("error querying spark data for share: %v", err)
} }
} else {
logrus.Debug("skipping spark data; no influx configuration")
} }
feEndpoint := "" feEndpoint := ""
if shr.FrontendEndpoint != nil { if shr.FrontendEndpoint != nil {
@ -61,6 +64,10 @@ func (h *shareDetailHandler) Handle(params metadata.GetShareDetailParams, princi
if shr.BackendProxyEndpoint != nil { if shr.BackendProxyEndpoint != nil {
beProxyEndpoint = *shr.BackendProxyEndpoint beProxyEndpoint = *shr.BackendProxyEndpoint
} }
var sparkData []*rest_model_zrok.SparkDataSample
for i := 0; i < len(sparkRx[shr.Token]) && i < len(sparkTx[shr.Token]); i++ {
sparkData = append(sparkData, &rest_model_zrok.SparkDataSample{Rx: float64(sparkRx[shr.Token][i]), Tx: float64(sparkTx[shr.Token][i])})
}
return metadata.NewGetShareDetailOK().WithPayload(&rest_model_zrok.Share{ return metadata.NewGetShareDetailOK().WithPayload(&rest_model_zrok.Share{
Token: shr.Token, Token: shr.Token,
ZID: shr.ZId, ZID: shr.ZId,
@ -70,7 +77,7 @@ func (h *shareDetailHandler) Handle(params metadata.GetShareDetailParams, princi
FrontendEndpoint: feEndpoint, FrontendEndpoint: feEndpoint,
BackendProxyEndpoint: beProxyEndpoint, BackendProxyEndpoint: beProxyEndpoint,
Reserved: shr.Reserved, Reserved: shr.Reserved,
Metrics: sparkData[shr.Token], Activity: sparkData,
CreatedAt: shr.CreatedAt.UnixMilli(), CreatedAt: shr.CreatedAt.UnixMilli(),
UpdatedAt: shr.UpdatedAt.UnixMilli(), UpdatedAt: shr.UpdatedAt.UnixMilli(),
}) })

View File

@ -1,7 +1,7 @@
package controller package controller
import ( import (
"github.com/openziti/edge/rest_management_api_client" "github.com/openziti/edge-api/rest_management_api_client"
"github.com/openziti/zrok/controller/zrokEdgeSdk" "github.com/openziti/zrok/controller/zrokEdgeSdk"
"github.com/openziti/zrok/model" "github.com/openziti/zrok/model"
"github.com/openziti/zrok/rest_server_zrok/operations/share" "github.com/openziti/zrok/rest_server_zrok/operations/share"

View File

@ -1,7 +1,7 @@
package controller package controller
import ( import (
"github.com/openziti/edge/rest_management_api_client" "github.com/openziti/edge-api/rest_management_api_client"
"github.com/openziti/zrok/controller/zrokEdgeSdk" "github.com/openziti/zrok/controller/zrokEdgeSdk"
"github.com/openziti/zrok/model" "github.com/openziti/zrok/model"
"github.com/openziti/zrok/rest_server_zrok/operations/share" "github.com/openziti/zrok/rest_server_zrok/operations/share"

View File

@ -4,55 +4,114 @@ import (
"context" "context"
"fmt" "fmt"
"github.com/openziti/zrok/controller/store" "github.com/openziti/zrok/controller/store"
"github.com/sirupsen/logrus"
"strconv"
) )
func sparkDataForShares(shrs []*store.Share) (map[string][]int64, error) { func sparkDataForEnvironments(envs []*store.Environment) (rx, tx map[int][]int64, err error) {
out := make(map[string][]int64) rx = make(map[int][]int64)
tx = make(map[int][]int64)
if len(envs) > 0 {
qapi := idb.QueryAPI(cfg.Metrics.Influx.Org)
if len(shrs) > 0 { envFilter := "|> filter(fn: (r) =>"
qapi := idb.QueryAPI(cfg.Influx.Org) for i, env := range envs {
if i > 0 {
envFilter += " or"
}
envFilter += fmt.Sprintf(" r[\"envId\"] == \"%d\"", env.Id)
}
envFilter += ")"
query := fmt.Sprintf("from(bucket: \"%v\")\n", cfg.Metrics.Influx.Bucket) +
"|> range(start: -5m)\n" +
"|> filter(fn: (r) => r[\"_measurement\"] == \"xfer\")\n" +
"|> filter(fn: (r) => r[\"_field\"] == \"rx\" or r[\"_field\"] == \"tx\")\n" +
"|> filter(fn: (r) => r[\"namespace\"] == \"backend\")\n" +
envFilter +
"|> drop(columns: [\"share\", \"acctId\"])\n" +
"|> aggregateWindow(every: 10s, fn: sum, createEmpty: true)\n"
result, err := qapi.Query(context.Background(), sparkFluxQuery(shrs)) result, err := qapi.Query(context.Background(), query)
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
for result.Next() { for result.Next() {
combinedRate := int64(0) envIdS := result.Record().ValueByKey("envId").(string)
readRate := result.Record().ValueByKey("bytesRead") envId, err := strconv.ParseInt(envIdS, 10, 32)
if readRate != nil { if err != nil {
combinedRate += readRate.(int64) logrus.Errorf("error parsing '%v': %v", envIdS, err)
continue
} }
writeRate := result.Record().ValueByKey("bytesWritten") switch result.Record().Field() {
if writeRate != nil { case "rx":
combinedRate += writeRate.(int64) rxV := int64(0)
if v, ok := result.Record().Value().(int64); ok {
rxV = v
}
rxData := append(rx[int(envId)], rxV)
rx[int(envId)] = rxData
case "tx":
txV := int64(0)
if v, ok := result.Record().Value().(int64); ok {
txV = v
}
txData := append(tx[int(envId)], txV)
tx[int(envId)] = txData
} }
shrToken := result.Record().ValueByKey("share").(string)
shrMetrics := out[shrToken]
shrMetrics = append(shrMetrics, combinedRate)
out[shrToken] = shrMetrics
} }
} }
return out, nil return rx, tx, nil
} }
func sparkFluxQuery(shrs []*store.Share) string { func sparkDataForShares(shrs []*store.Share) (rx, tx map[string][]int64, err error) {
shrFilter := "|> filter(fn: (r) =>" rx = make(map[string][]int64)
for i, shr := range shrs { tx = make(map[string][]int64)
if i > 0 { if len(shrs) > 0 {
shrFilter += " or" qapi := idb.QueryAPI(cfg.Metrics.Influx.Org)
shrFilter := "|> filter(fn: (r) =>"
for i, shr := range shrs {
if i > 0 {
shrFilter += " or"
}
shrFilter += fmt.Sprintf(" r[\"share\"] == \"%v\"", shr.Token)
}
shrFilter += ")"
query := fmt.Sprintf("from(bucket: \"%v\")\n", cfg.Metrics.Influx.Bucket) +
"|> range(start: -5m)\n" +
"|> filter(fn: (r) => r[\"_measurement\"] == \"xfer\")\n" +
"|> filter(fn: (r) => r[\"_field\"] == \"rx\" or r[\"_field\"] == \"tx\")\n" +
"|> filter(fn: (r) => r[\"namespace\"] == \"backend\")\n" +
shrFilter +
"|> aggregateWindow(every: 10s, fn: sum, createEmpty: true)\n"
result, err := qapi.Query(context.Background(), query)
if err != nil {
return nil, nil, err
}
for result.Next() {
shrToken := result.Record().ValueByKey("share").(string)
switch result.Record().Field() {
case "rx":
rxV := int64(0)
if v, ok := result.Record().Value().(int64); ok {
rxV = v
}
rxData := append(rx[shrToken], rxV)
rx[shrToken] = rxData
case "tx":
txV := int64(0)
if v, ok := result.Record().Value().(int64); ok {
txV = v
}
txData := append(tx[shrToken], txV)
tx[shrToken] = txData
}
} }
shrFilter += fmt.Sprintf(" r[\"share\"] == \"%v\"", shr.Token)
} }
shrFilter += ")" return rx, tx, nil
query := "read = from(bucket: \"zrok\")" +
"|> range(start: -5m)" +
"|> filter(fn: (r) => r[\"_measurement\"] == \"xfer\")" +
"|> filter(fn: (r) => r[\"_field\"] == \"bytesRead\" or r[\"_field\"] == \"bytesWritten\")" +
"|> filter(fn: (r) => r[\"namespace\"] == \"frontend\")" +
shrFilter +
"|> aggregateWindow(every: 5s, fn: sum, createEmpty: true)\n" +
"|> pivot(rowKey:[\"_time\"], columnKey: [\"_field\"], valueColumn: \"_value\")" +
"|> yield(name: \"last\")"
return query
} }

View File

@ -3,8 +3,9 @@ package controller
import ( import (
"context" "context"
"fmt" "fmt"
"github.com/openziti/edge/rest_management_api_client" "github.com/openziti/edge-api/rest_management_api_client"
"github.com/openziti/edge/rest_management_api_client/config" "github.com/openziti/edge-api/rest_management_api_client/config"
"github.com/openziti/zrok/controller/zrokEdgeSdk"
"github.com/openziti/zrok/model" "github.com/openziti/zrok/model"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@ -23,7 +24,7 @@ func controllerStartup() error {
func inspectZiti() error { func inspectZiti() error {
logrus.Infof("inspecting ziti controller configuration") logrus.Infof("inspecting ziti controller configuration")
edge, err := edgeClient() edge, err := zrokEdgeSdk.Client(cfg.Ziti)
if err != nil { if err != nil {
return errors.Wrap(err, "error getting ziti edge client") return errors.Wrap(err, "error getting ziti edge client")
} }

View File

@ -12,9 +12,10 @@ type Account struct {
Password string Password string
Token string Token string
Limitless bool Limitless bool
Deleted bool
} }
func (self *Store) CreateAccount(a *Account, tx *sqlx.Tx) (int, error) { func (str *Store) CreateAccount(a *Account, tx *sqlx.Tx) (int, error) {
stmt, err := tx.Prepare("insert into accounts (email, salt, password, token, limitless) values ($1, $2, $3, $4, $5) returning id") stmt, err := tx.Prepare("insert into accounts (email, salt, password, token, limitless) values ($1, $2, $3, $4, $5) returning id")
if err != nil { if err != nil {
return 0, errors.Wrap(err, "error preparing accounts insert statement") return 0, errors.Wrap(err, "error preparing accounts insert statement")
@ -26,7 +27,7 @@ func (self *Store) CreateAccount(a *Account, tx *sqlx.Tx) (int, error) {
return id, nil return id, nil
} }
func (self *Store) GetAccount(id int, tx *sqlx.Tx) (*Account, error) { func (str *Store) GetAccount(id int, tx *sqlx.Tx) (*Account, error) {
a := &Account{} a := &Account{}
if err := tx.QueryRowx("select * from accounts where id = $1", id).StructScan(a); err != nil { if err := tx.QueryRowx("select * from accounts where id = $1", id).StructScan(a); err != nil {
return nil, errors.Wrap(err, "error selecting account by id") return nil, errors.Wrap(err, "error selecting account by id")
@ -34,23 +35,31 @@ func (self *Store) GetAccount(id int, tx *sqlx.Tx) (*Account, error) {
return a, nil return a, nil
} }
func (self *Store) FindAccountWithEmail(email string, tx *sqlx.Tx) (*Account, error) { func (str *Store) FindAccountWithEmail(email string, tx *sqlx.Tx) (*Account, error) {
a := &Account{} a := &Account{}
if err := tx.QueryRowx("select * from accounts where email = $1", email).StructScan(a); err != nil { if err := tx.QueryRowx("select * from accounts where email = $1 and not deleted", email).StructScan(a); err != nil {
return nil, errors.Wrap(err, "error selecting account by email") return nil, errors.Wrap(err, "error selecting account by email")
} }
return a, nil return a, nil
} }
func (self *Store) FindAccountWithToken(token string, tx *sqlx.Tx) (*Account, error) { func (str *Store) FindAccountWithEmailAndDeleted(email string, tx *sqlx.Tx) (*Account, error) {
a := &Account{} a := &Account{}
if err := tx.QueryRowx("select * from accounts where token = $1", token).StructScan(a); err != nil { if err := tx.QueryRowx("select * from accounts where email = $1", email).StructScan(a); err != nil {
return nil, errors.Wrap(err, "error selecting acount by email")
}
return a, nil
}
func (str *Store) FindAccountWithToken(token string, tx *sqlx.Tx) (*Account, error) {
a := &Account{}
if err := tx.QueryRowx("select * from accounts where token = $1 and not deleted", token).StructScan(a); err != nil {
return nil, errors.Wrap(err, "error selecting account by token") return nil, errors.Wrap(err, "error selecting account by token")
} }
return a, nil return a, nil
} }
func (self *Store) UpdateAccount(a *Account, tx *sqlx.Tx) (int, error) { func (str *Store) UpdateAccount(a *Account, tx *sqlx.Tx) (int, error) {
stmt, err := tx.Prepare("update accounts set email=$1, salt=$2, password=$3, token=$4, limitless=$5 where id = $6") stmt, err := tx.Prepare("update accounts set email=$1, salt=$2, password=$3, token=$4, limitless=$5 where id = $6")
if err != nil { if err != nil {
return 0, errors.Wrap(err, "error preparing accounts update statement") return 0, errors.Wrap(err, "error preparing accounts update statement")

View File

@ -0,0 +1,65 @@
package store
import (
"github.com/jmoiron/sqlx"
"github.com/pkg/errors"
)
type AccountLimitJournal struct {
Model
AccountId int
RxBytes int64
TxBytes int64
Action LimitJournalAction
}
func (str *Store) CreateAccountLimitJournal(j *AccountLimitJournal, trx *sqlx.Tx) (int, error) {
stmt, err := trx.Prepare("insert into account_limit_journal (account_id, rx_bytes, tx_bytes, action) values ($1, $2, $3, $4) returning id")
if err != nil {
return 0, errors.Wrap(err, "error preparing account_limit_journal insert statement")
}
var id int
if err := stmt.QueryRow(j.AccountId, j.RxBytes, j.TxBytes, j.Action).Scan(&id); err != nil {
return 0, errors.Wrap(err, "error executing account_limit_journal insert statement")
}
return id, nil
}
func (str *Store) IsAccountLimitJournalEmpty(acctId int, trx *sqlx.Tx) (bool, error) {
count := 0
if err := trx.QueryRowx("select count(0) from account_limit_journal where account_id = $1", acctId).Scan(&count); err != nil {
return false, err
}
return count == 0, nil
}
func (str *Store) FindLatestAccountLimitJournal(acctId int, trx *sqlx.Tx) (*AccountLimitJournal, error) {
j := &AccountLimitJournal{}
if err := trx.QueryRowx("select * from account_limit_journal where account_id = $1 order by id desc limit 1", acctId).StructScan(j); err != nil {
return nil, errors.Wrap(err, "error finding account_limit_journal by account_id")
}
return j, nil
}
func (str *Store) FindAllLatestAccountLimitJournal(trx *sqlx.Tx) ([]*AccountLimitJournal, error) {
rows, err := trx.Queryx("select id, account_id, rx_bytes, tx_bytes, action, created_at, updated_at from account_limit_journal where id in (select max(id) as id from account_limit_journal group by account_id)")
if err != nil {
return nil, errors.Wrap(err, "error selecting all latest account_limit_journal")
}
var aljs []*AccountLimitJournal
for rows.Next() {
alj := &AccountLimitJournal{}
if err := rows.StructScan(alj); err != nil {
return nil, errors.Wrap(err, "error scanning account_limit_journal")
}
aljs = append(aljs, alj)
}
return aljs, nil
}
func (str *Store) DeleteAccountLimitJournalForAccount(acctId int, trx *sqlx.Tx) error {
if _, err := trx.Exec("delete from account_limit_journal where account_id = $1", acctId); err != nil {
return errors.Wrapf(err, "error deleting account_limit journal for '#%d'", acctId)
}
return nil
}

View File

@ -0,0 +1,79 @@
package store
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestAccountLimitJournal(t *testing.T) {
str, err := Open(&Config{Path: ":memory:", Type: "sqlite3"})
assert.Nil(t, err)
assert.NotNil(t, str)
trx, err := str.Begin()
assert.Nil(t, err)
assert.NotNil(t, trx)
aljEmpty, err := str.IsAccountLimitJournalEmpty(1, trx)
assert.Nil(t, err)
assert.True(t, aljEmpty)
acctId, err := str.CreateAccount(&Account{Email: "nobody@nowehere.com", Salt: "salt", Password: "password", Token: "token", Limitless: false, Deleted: false}, trx)
assert.Nil(t, err)
_, err = str.CreateAccountLimitJournal(&AccountLimitJournal{AccountId: acctId, RxBytes: 1024, TxBytes: 2048, Action: WarningAction}, trx)
assert.Nil(t, err)
aljEmpty, err = str.IsAccountLimitJournalEmpty(acctId, trx)
assert.Nil(t, err)
assert.False(t, aljEmpty)
latestAlj, err := str.FindLatestAccountLimitJournal(acctId, trx)
assert.Nil(t, err)
assert.NotNil(t, latestAlj)
assert.Equal(t, int64(1024), latestAlj.RxBytes)
assert.Equal(t, int64(2048), latestAlj.TxBytes)
assert.Equal(t, WarningAction, latestAlj.Action)
_, err = str.CreateAccountLimitJournal(&AccountLimitJournal{AccountId: acctId, RxBytes: 2048, TxBytes: 4096, Action: LimitAction}, trx)
assert.Nil(t, err)
latestAlj, err = str.FindLatestAccountLimitJournal(acctId, trx)
assert.Nil(t, err)
assert.NotNil(t, latestAlj)
assert.Equal(t, int64(2048), latestAlj.RxBytes)
assert.Equal(t, int64(4096), latestAlj.TxBytes)
assert.Equal(t, LimitAction, latestAlj.Action)
}
func TestFindAllLatestAccountLimitJournal(t *testing.T) {
str, err := Open(&Config{Path: ":memory:", Type: "sqlite3"})
assert.Nil(t, err)
assert.NotNil(t, str)
trx, err := str.Begin()
assert.Nil(t, err)
assert.NotNil(t, trx)
acctId1, err := str.CreateAccount(&Account{Email: "nobody@nowehere.com", Salt: "salt1", Password: "password1", Token: "token1", Limitless: false, Deleted: false}, trx)
assert.Nil(t, err)
_, err = str.CreateAccountLimitJournal(&AccountLimitJournal{AccountId: acctId1, RxBytes: 2048, TxBytes: 4096, Action: WarningAction}, trx)
assert.Nil(t, err)
_, err = str.CreateAccountLimitJournal(&AccountLimitJournal{AccountId: acctId1, RxBytes: 2048, TxBytes: 4096, Action: ClearAction}, trx)
assert.Nil(t, err)
aljId13, err := str.CreateAccountLimitJournal(&AccountLimitJournal{AccountId: acctId1, RxBytes: 2048, TxBytes: 4096, Action: LimitAction}, trx)
assert.Nil(t, err)
acctId2, err := str.CreateAccount(&Account{Email: "someone@somewhere.com", Salt: "salt2", Password: "password2", Token: "token2", Limitless: false, Deleted: false}, trx)
assert.Nil(t, err)
aljId21, err := str.CreateAccountLimitJournal(&AccountLimitJournal{AccountId: acctId2, RxBytes: 2048, TxBytes: 4096, Action: WarningAction}, trx)
assert.Nil(t, err)
aljs, err := str.FindAllLatestAccountLimitJournal(trx)
assert.Nil(t, err)
assert.Equal(t, 2, len(aljs))
assert.Equal(t, aljId13, aljs[0].Id)
assert.Equal(t, aljId21, aljs[1].Id)
}

View File

@ -14,9 +14,10 @@ type AccountRequest struct {
Token string Token string
Email string Email string
SourceAddress string SourceAddress string
Deleted bool
} }
func (self *Store) CreateAccountRequest(ar *AccountRequest, tx *sqlx.Tx) (int, error) { func (str *Store) CreateAccountRequest(ar *AccountRequest, tx *sqlx.Tx) (int, error) {
stmt, err := tx.Prepare("insert into account_requests (token, email, source_address) values ($1, $2, $3) returning id") stmt, err := tx.Prepare("insert into account_requests (token, email, source_address) values ($1, $2, $3) returning id")
if err != nil { if err != nil {
return 0, errors.Wrap(err, "error preparing account_requests insert statement") return 0, errors.Wrap(err, "error preparing account_requests insert statement")
@ -28,7 +29,7 @@ func (self *Store) CreateAccountRequest(ar *AccountRequest, tx *sqlx.Tx) (int, e
return id, nil return id, nil
} }
func (self *Store) GetAccountRequest(id int, tx *sqlx.Tx) (*AccountRequest, error) { func (str *Store) GetAccountRequest(id int, tx *sqlx.Tx) (*AccountRequest, error) {
ar := &AccountRequest{} ar := &AccountRequest{}
if err := tx.QueryRowx("select * from account_requests where id = $1", id).StructScan(ar); err != nil { if err := tx.QueryRowx("select * from account_requests where id = $1", id).StructScan(ar); err != nil {
return nil, errors.Wrap(err, "error selecting account_request by id") return nil, errors.Wrap(err, "error selecting account_request by id")
@ -36,25 +37,25 @@ func (self *Store) GetAccountRequest(id int, tx *sqlx.Tx) (*AccountRequest, erro
return ar, nil return ar, nil
} }
func (self *Store) FindAccountRequestWithToken(token string, tx *sqlx.Tx) (*AccountRequest, error) { func (str *Store) FindAccountRequestWithToken(token string, tx *sqlx.Tx) (*AccountRequest, error) {
ar := &AccountRequest{} ar := &AccountRequest{}
if err := tx.QueryRowx("select * from account_requests where token = $1", token).StructScan(ar); err != nil { if err := tx.QueryRowx("select * from account_requests where token = $1 and not deleted", token).StructScan(ar); err != nil {
return nil, errors.Wrap(err, "error selecting account_request by token") return nil, errors.Wrap(err, "error selecting account_request by token")
} }
return ar, nil return ar, nil
} }
func (self *Store) FindExpiredAccountRequests(before time.Time, limit int, tx *sqlx.Tx) ([]*AccountRequest, error) { func (str *Store) FindExpiredAccountRequests(before time.Time, limit int, tx *sqlx.Tx) ([]*AccountRequest, error) {
var sql string var sql string
switch self.cfg.Type { switch str.cfg.Type {
case "postgres": case "postgres":
sql = "select * from account_requests where created_at < $1 limit %d for update" sql = "select * from account_requests where created_at < $1 and not deleted limit %d for update"
case "sqlite3": case "sqlite3":
sql = "select * from account_requests where created_at < $1 limit %d" sql = "select * from account_requests where created_at < $1 and not deleted limit %d"
default: default:
return nil, errors.Errorf("unknown database type '%v'", self.cfg.Type) return nil, errors.Errorf("unknown database type '%v'", str.cfg.Type)
} }
rows, err := tx.Queryx(fmt.Sprintf(sql, limit), before) rows, err := tx.Queryx(fmt.Sprintf(sql, limit), before)
@ -72,16 +73,16 @@ func (self *Store) FindExpiredAccountRequests(before time.Time, limit int, tx *s
return ars, nil return ars, nil
} }
func (self *Store) FindAccountRequestWithEmail(email string, tx *sqlx.Tx) (*AccountRequest, error) { func (str *Store) FindAccountRequestWithEmail(email string, tx *sqlx.Tx) (*AccountRequest, error) {
ar := &AccountRequest{} ar := &AccountRequest{}
if err := tx.QueryRowx("select * from account_requests where email = $1", email).StructScan(ar); err != nil { if err := tx.QueryRowx("select * from account_requests where email = $1 and not deleted", email).StructScan(ar); err != nil {
return nil, errors.Wrap(err, "error selecting account_request by email") return nil, errors.Wrap(err, "error selecting account_request by email")
} }
return ar, nil return ar, nil
} }
func (self *Store) DeleteAccountRequest(id int, tx *sqlx.Tx) error { func (str *Store) DeleteAccountRequest(id int, tx *sqlx.Tx) error {
stmt, err := tx.Prepare("delete from account_requests where id = $1") stmt, err := tx.Prepare("update account_requests set deleted = true, updated_at = current_timestamp where id = $1")
if err != nil { if err != nil {
return errors.Wrap(err, "error preparing account_requests delete statement") return errors.Wrap(err, "error preparing account_requests delete statement")
} }
@ -92,7 +93,7 @@ func (self *Store) DeleteAccountRequest(id int, tx *sqlx.Tx) error {
return nil return nil
} }
func (self *Store) DeleteMultipleAccountRequests(ids []int, tx *sqlx.Tx) error { func (str *Store) DeleteMultipleAccountRequests(ids []int, tx *sqlx.Tx) error {
if len(ids) == 0 { if len(ids) == 0 {
return nil return nil
} }
@ -105,7 +106,7 @@ func (self *Store) DeleteMultipleAccountRequests(ids []int, tx *sqlx.Tx) error {
indexes[i] = fmt.Sprintf("$%d", i+1) indexes[i] = fmt.Sprintf("$%d", i+1)
} }
stmt, err := tx.Prepare(fmt.Sprintf("delete from account_requests where id in (%s)", strings.Join(indexes, ","))) stmt, err := tx.Prepare(fmt.Sprintf("update account_requests set deleted = true, updated_at = current_timestamp where id in (%s)", strings.Join(indexes, ",")))
if err != nil { if err != nil {
return errors.Wrap(err, "error preparing account_requests delete multiple statement") return errors.Wrap(err, "error preparing account_requests delete multiple statement")
} }

View File

@ -12,9 +12,10 @@ type Environment struct {
Host string Host string
Address string Address string
ZId string ZId string
Deleted bool
} }
func (self *Store) CreateEnvironment(accountId int, i *Environment, tx *sqlx.Tx) (int, error) { func (str *Store) CreateEnvironment(accountId int, i *Environment, tx *sqlx.Tx) (int, error) {
stmt, err := tx.Prepare("insert into environments (account_id, description, host, address, z_id) values ($1, $2, $3, $4, $5) returning id") stmt, err := tx.Prepare("insert into environments (account_id, description, host, address, z_id) values ($1, $2, $3, $4, $5) returning id")
if err != nil { if err != nil {
return 0, errors.Wrap(err, "error preparing environments insert statement") return 0, errors.Wrap(err, "error preparing environments insert statement")
@ -26,7 +27,7 @@ func (self *Store) CreateEnvironment(accountId int, i *Environment, tx *sqlx.Tx)
return id, nil return id, nil
} }
func (self *Store) CreateEphemeralEnvironment(i *Environment, tx *sqlx.Tx) (int, error) { func (str *Store) CreateEphemeralEnvironment(i *Environment, tx *sqlx.Tx) (int, error) {
stmt, err := tx.Prepare("insert into environments (description, host, address, z_id) values ($1, $2, $3, $4) returning id") stmt, err := tx.Prepare("insert into environments (description, host, address, z_id) values ($1, $2, $3, $4) returning id")
if err != nil { if err != nil {
return 0, errors.Wrap(err, "error preparing environments (ephemeral) insert statement") return 0, errors.Wrap(err, "error preparing environments (ephemeral) insert statement")
@ -38,7 +39,7 @@ func (self *Store) CreateEphemeralEnvironment(i *Environment, tx *sqlx.Tx) (int,
return id, nil return id, nil
} }
func (self *Store) GetEnvironment(id int, tx *sqlx.Tx) (*Environment, error) { func (str *Store) GetEnvironment(id int, tx *sqlx.Tx) (*Environment, error) {
i := &Environment{} i := &Environment{}
if err := tx.QueryRowx("select * from environments where id = $1", id).StructScan(i); err != nil { if err := tx.QueryRowx("select * from environments where id = $1", id).StructScan(i); err != nil {
return nil, errors.Wrap(err, "error selecting environment by id") return nil, errors.Wrap(err, "error selecting environment by id")
@ -46,8 +47,8 @@ func (self *Store) GetEnvironment(id int, tx *sqlx.Tx) (*Environment, error) {
return i, nil return i, nil
} }
func (self *Store) FindEnvironmentsForAccount(accountId int, tx *sqlx.Tx) ([]*Environment, error) { func (str *Store) FindEnvironmentsForAccount(accountId int, tx *sqlx.Tx) ([]*Environment, error) {
rows, err := tx.Queryx("select environments.* from environments where account_id = $1", accountId) rows, err := tx.Queryx("select environments.* from environments where account_id = $1 and not deleted", accountId)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "error selecting environments by account id") return nil, errors.Wrap(err, "error selecting environments by account id")
} }
@ -62,16 +63,16 @@ func (self *Store) FindEnvironmentsForAccount(accountId int, tx *sqlx.Tx) ([]*En
return is, nil return is, nil
} }
func (self *Store) FindEnvironmentForAccount(envZId string, accountId int, tx *sqlx.Tx) (*Environment, error) { func (str *Store) FindEnvironmentForAccount(envZId string, accountId int, tx *sqlx.Tx) (*Environment, error) {
env := &Environment{} env := &Environment{}
if err := tx.QueryRowx("select environments.* from environments where z_id = $1 and account_id = $2", envZId, accountId).StructScan(env); err != nil { if err := tx.QueryRowx("select environments.* from environments where z_id = $1 and account_id = $2 and not deleted", envZId, accountId).StructScan(env); err != nil {
return nil, errors.Wrap(err, "error finding environment by z_id and account_id") return nil, errors.Wrap(err, "error finding environment by z_id and account_id")
} }
return env, nil return env, nil
} }
func (self *Store) DeleteEnvironment(id int, tx *sqlx.Tx) error { func (str *Store) DeleteEnvironment(id int, tx *sqlx.Tx) error {
stmt, err := tx.Prepare("delete from environments where id = $1") stmt, err := tx.Prepare("update environments set updated_at = current_timestamp, deleted = true where id = $1")
if err != nil { if err != nil {
return errors.Wrap(err, "error preparing environments delete statement") return errors.Wrap(err, "error preparing environments delete statement")
} }

View File

@ -0,0 +1,93 @@
package store
import (
"fmt"
"github.com/jmoiron/sqlx"
"github.com/pkg/errors"
)
type EnvironmentLimitJournal struct {
Model
EnvironmentId int
RxBytes int64
TxBytes int64
Action LimitJournalAction
}
func (str *Store) CreateEnvironmentLimitJournal(j *EnvironmentLimitJournal, trx *sqlx.Tx) (int, error) {
stmt, err := trx.Prepare("insert into environment_limit_journal (environment_id, rx_bytes, tx_bytes, action) values ($1, $2, $3, $4) returning id")
if err != nil {
return 0, errors.Wrap(err, "error preparing environment_limit_journal insert statement")
}
var id int
if err := stmt.QueryRow(j.EnvironmentId, j.RxBytes, j.TxBytes, j.Action).Scan(&id); err != nil {
return 0, errors.Wrap(err, "error executing environment_limit_journal insert statement")
}
return id, nil
}
func (str *Store) IsEnvironmentLimitJournalEmpty(envId int, trx *sqlx.Tx) (bool, error) {
count := 0
if err := trx.QueryRowx("select count(0) from environment_limit_journal where environment_id = $1", envId).Scan(&count); err != nil {
return false, err
}
return count == 0, nil
}
func (str *Store) FindLatestEnvironmentLimitJournal(envId int, trx *sqlx.Tx) (*EnvironmentLimitJournal, error) {
j := &EnvironmentLimitJournal{}
if err := trx.QueryRowx("select * from environment_limit_journal where environment_id = $1 order by created_at desc limit 1", envId).StructScan(j); err != nil {
return nil, errors.Wrap(err, "error finding environment_limit_journal by environment_id")
}
return j, nil
}
func (str *Store) FindSelectedLatestEnvironmentLimitJournal(envIds []int, trx *sqlx.Tx) ([]*EnvironmentLimitJournal, error) {
if len(envIds) < 1 {
return nil, nil
}
in := "("
for i := range envIds {
if i > 0 {
in += ", "
}
in += fmt.Sprintf("%d", envIds[i])
}
in += ")"
rows, err := trx.Queryx("select id, environment_id, rx_bytes, tx_bytes, action, created_at, updated_at from environment_limit_journal where id in (select max(id) as id from environment_limit_journal group by environment_id) and environment_id in " + in)
if err != nil {
return nil, errors.Wrap(err, "error selecting all latest environment_limit_journal")
}
var eljs []*EnvironmentLimitJournal
for rows.Next() {
elj := &EnvironmentLimitJournal{}
if err := rows.StructScan(elj); err != nil {
return nil, errors.Wrap(err, "error scanning environment_limit_journal")
}
eljs = append(eljs, elj)
}
return eljs, nil
}
func (str *Store) FindAllLatestEnvironmentLimitJournal(trx *sqlx.Tx) ([]*EnvironmentLimitJournal, error) {
rows, err := trx.Queryx("select id, environment_id, rx_bytes, tx_bytes, action, created_at, updated_at from environment_limit_journal where id in (select max(id) as id from environment_limit_journal group by environment_id)")
if err != nil {
return nil, errors.Wrap(err, "error selecting all latest environment_limit_journal")
}
var eljs []*EnvironmentLimitJournal
for rows.Next() {
elj := &EnvironmentLimitJournal{}
if err := rows.StructScan(elj); err != nil {
return nil, errors.Wrap(err, "error scanning environment_limit_journal")
}
eljs = append(eljs, elj)
}
return eljs, nil
}
func (str *Store) DeleteEnvironmentLimitJournalForEnvironment(envId int, trx *sqlx.Tx) error {
if _, err := trx.Exec("delete from environment_limit_journal where environment_id = $1", envId); err != nil {
return errors.Wrapf(err, "error deleteing environment_limit_journal for '#%d'", envId)
}
return nil
}

View File

@ -26,6 +26,7 @@ func TestEphemeralEnvironment(t *testing.T) {
assert.Nil(t, err) assert.Nil(t, err)
assert.NotNil(t, env) assert.NotNil(t, env)
assert.Nil(t, env.AccountId) assert.Nil(t, env.AccountId)
assert.False(t, env.Deleted)
} }
func TestEnvironment(t *testing.T) { func TestEnvironment(t *testing.T) {
@ -57,4 +58,5 @@ func TestEnvironment(t *testing.T) {
assert.NotNil(t, env) assert.NotNil(t, env)
assert.NotNil(t, env.AccountId) assert.NotNil(t, env.AccountId)
assert.Equal(t, acctId, *env.AccountId) assert.Equal(t, acctId, *env.AccountId)
assert.False(t, env.Deleted)
} }

View File

@ -7,21 +7,23 @@ import (
type Frontend struct { type Frontend struct {
Model Model
EnvironmentId *int EnvironmentId *int
Token string PrivateShareId *int
ZId string Token string
PublicName *string ZId string
UrlTemplate *string PublicName *string
Reserved bool UrlTemplate *string
Reserved bool
Deleted bool
} }
func (str *Store) CreateFrontend(envId int, f *Frontend, tx *sqlx.Tx) (int, error) { func (str *Store) CreateFrontend(envId int, f *Frontend, tx *sqlx.Tx) (int, error) {
stmt, err := tx.Prepare("insert into frontends (environment_id, token, z_id, public_name, url_template, reserved) values ($1, $2, $3, $4, $5, $6) returning id") stmt, err := tx.Prepare("insert into frontends (environment_id, private_share_id, token, z_id, public_name, url_template, reserved) values ($1, $2, $3, $4, $5, $6, $7) returning id")
if err != nil { if err != nil {
return 0, errors.Wrap(err, "error preparing frontends insert statement") return 0, errors.Wrap(err, "error preparing frontends insert statement")
} }
var id int var id int
if err := stmt.QueryRow(envId, f.Token, f.ZId, f.PublicName, f.UrlTemplate, f.Reserved).Scan(&id); err != nil { if err := stmt.QueryRow(envId, f.PrivateShareId, f.Token, f.ZId, f.PublicName, f.UrlTemplate, f.Reserved).Scan(&id); err != nil {
return 0, errors.Wrap(err, "error executing frontends insert statement") return 0, errors.Wrap(err, "error executing frontends insert statement")
} }
return id, nil return id, nil
@ -49,7 +51,7 @@ func (str *Store) GetFrontend(id int, tx *sqlx.Tx) (*Frontend, error) {
func (str *Store) FindFrontendWithToken(token string, tx *sqlx.Tx) (*Frontend, error) { func (str *Store) FindFrontendWithToken(token string, tx *sqlx.Tx) (*Frontend, error) {
i := &Frontend{} i := &Frontend{}
if err := tx.QueryRowx("select frontends.* from frontends where token = $1", token).StructScan(i); err != nil { if err := tx.QueryRowx("select frontends.* from frontends where token = $1 and not deleted", token).StructScan(i); err != nil {
return nil, errors.Wrap(err, "error selecting frontend by name") return nil, errors.Wrap(err, "error selecting frontend by name")
} }
return i, nil return i, nil
@ -57,7 +59,7 @@ func (str *Store) FindFrontendWithToken(token string, tx *sqlx.Tx) (*Frontend, e
func (str *Store) FindFrontendWithZId(zId string, tx *sqlx.Tx) (*Frontend, error) { func (str *Store) FindFrontendWithZId(zId string, tx *sqlx.Tx) (*Frontend, error) {
i := &Frontend{} i := &Frontend{}
if err := tx.QueryRowx("select frontends.* from frontends where z_id = $1", zId).StructScan(i); err != nil { if err := tx.QueryRowx("select frontends.* from frontends where z_id = $1 and not deleted", zId).StructScan(i); err != nil {
return nil, errors.Wrap(err, "error selecting frontend by ziti id") return nil, errors.Wrap(err, "error selecting frontend by ziti id")
} }
return i, nil return i, nil
@ -65,14 +67,14 @@ func (str *Store) FindFrontendWithZId(zId string, tx *sqlx.Tx) (*Frontend, error
func (str *Store) FindFrontendPubliclyNamed(publicName string, tx *sqlx.Tx) (*Frontend, error) { func (str *Store) FindFrontendPubliclyNamed(publicName string, tx *sqlx.Tx) (*Frontend, error) {
i := &Frontend{} i := &Frontend{}
if err := tx.QueryRowx("select frontends.* from frontends where public_name = $1", publicName).StructScan(i); err != nil { if err := tx.QueryRowx("select frontends.* from frontends where public_name = $1 and not deleted", publicName).StructScan(i); err != nil {
return nil, errors.Wrap(err, "error selecting frontend by public_name") return nil, errors.Wrap(err, "error selecting frontend by public_name")
} }
return i, nil return i, nil
} }
func (str *Store) FindFrontendsForEnvironment(envId int, tx *sqlx.Tx) ([]*Frontend, error) { func (str *Store) FindFrontendsForEnvironment(envId int, tx *sqlx.Tx) ([]*Frontend, error) {
rows, err := tx.Queryx("select frontends.* from frontends where environment_id = $1", envId) rows, err := tx.Queryx("select frontends.* from frontends where environment_id = $1 and not deleted", envId)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "error selecting frontends by environment_id") return nil, errors.Wrap(err, "error selecting frontends by environment_id")
} }
@ -88,7 +90,7 @@ func (str *Store) FindFrontendsForEnvironment(envId int, tx *sqlx.Tx) ([]*Fronte
} }
func (str *Store) FindPublicFrontends(tx *sqlx.Tx) ([]*Frontend, error) { func (str *Store) FindPublicFrontends(tx *sqlx.Tx) ([]*Frontend, error) {
rows, err := tx.Queryx("select frontends.* from frontends where environment_id is null and reserved = true") rows, err := tx.Queryx("select frontends.* from frontends where environment_id is null and reserved = true and not deleted")
if err != nil { if err != nil {
return nil, errors.Wrap(err, "error selecting public frontends") return nil, errors.Wrap(err, "error selecting public frontends")
} }
@ -103,13 +105,29 @@ func (str *Store) FindPublicFrontends(tx *sqlx.Tx) ([]*Frontend, error) {
return frontends, nil return frontends, nil
} }
func (str *Store) FindFrontendsForPrivateShare(shrId int, tx *sqlx.Tx) ([]*Frontend, error) {
rows, err := tx.Queryx("select frontends.* from frontends where private_share_id = $1 and not deleted", shrId)
if err != nil {
return nil, errors.Wrap(err, "error selecting frontends by private_share_id")
}
var is []*Frontend
for rows.Next() {
i := &Frontend{}
if err := rows.StructScan(i); err != nil {
return nil, errors.Wrap(err, "error scanning frontend")
}
is = append(is, i)
}
return is, nil
}
func (str *Store) UpdateFrontend(fe *Frontend, tx *sqlx.Tx) error { func (str *Store) UpdateFrontend(fe *Frontend, tx *sqlx.Tx) error {
sql := "update frontends set environment_id = $1, token = $2, z_id = $3, public_name = $4, url_template = $5, reserved = $6, updated_at = current_timestamp where id = $7" sql := "update frontends set environment_id = $1, private_share_id = $2, token = $3, z_id = $4, public_name = $5, url_template = $6, reserved = $7, updated_at = current_timestamp where id = $8"
stmt, err := tx.Prepare(sql) stmt, err := tx.Prepare(sql)
if err != nil { if err != nil {
return errors.Wrap(err, "error preparing frontends update statement") return errors.Wrap(err, "error preparing frontends update statement")
} }
_, err = stmt.Exec(fe.EnvironmentId, fe.Token, fe.ZId, fe.PublicName, fe.UrlTemplate, fe.Reserved, fe.Id) _, err = stmt.Exec(fe.EnvironmentId, fe.PrivateShareId, fe.Token, fe.ZId, fe.PublicName, fe.UrlTemplate, fe.Reserved, fe.Id)
if err != nil { if err != nil {
return errors.Wrap(err, "error executing frontends update statement") return errors.Wrap(err, "error executing frontends update statement")
} }
@ -118,7 +136,7 @@ func (str *Store) UpdateFrontend(fe *Frontend, tx *sqlx.Tx) error {
func (str *Store) DeleteFrontend(id int, tx *sqlx.Tx) error { func (str *Store) DeleteFrontend(id int, tx *sqlx.Tx) error {
stmt, err := tx.Prepare("delete from frontends where id = $1") stmt, err := tx.Prepare("update frontends set updated_at = current_timestamp, deleted = true where id = $1")
if err != nil { if err != nil {
return errors.Wrap(err, "error preparing frontends delete statement") return errors.Wrap(err, "error preparing frontends delete statement")
} }

View File

@ -42,6 +42,7 @@ func TestPublicFrontend(t *testing.T) {
assert.NotNil(t, fe) assert.NotNil(t, fe)
assert.Equal(t, envId, *fe.EnvironmentId) assert.Equal(t, envId, *fe.EnvironmentId)
assert.Equal(t, feName, *fe.PublicName) assert.Equal(t, feName, *fe.PublicName)
assert.False(t, fe.Deleted)
fe0, err := str.FindFrontendPubliclyNamed(feName, tx) fe0, err := str.FindFrontendPubliclyNamed(feName, tx)
assert.Nil(t, err) assert.Nil(t, err)
@ -56,6 +57,7 @@ func TestPublicFrontend(t *testing.T) {
assert.Nil(t, fe0) assert.Nil(t, fe0)
fe0, err = str.GetFrontend(fe.Id, tx) fe0, err = str.GetFrontend(fe.Id, tx)
assert.NotNil(t, err) assert.Nil(t, err)
assert.Nil(t, fe0) assert.NotNil(t, fe0)
assert.True(t, fe0.Deleted)
} }

View File

@ -10,7 +10,8 @@ import (
type InviteToken struct { type InviteToken struct {
Model Model
Token string Token string
Deleted bool
} }
func (str *Store) CreateInviteTokens(inviteTokens []*InviteToken, tx *sqlx.Tx) error { func (str *Store) CreateInviteTokens(inviteTokens []*InviteToken, tx *sqlx.Tx) error {
@ -31,16 +32,16 @@ func (str *Store) CreateInviteTokens(inviteTokens []*InviteToken, tx *sqlx.Tx) e
return nil return nil
} }
func (str *Store) GetInviteTokenByToken(token string, tx *sqlx.Tx) (*InviteToken, error) { func (str *Store) FindInviteTokenByToken(token string, tx *sqlx.Tx) (*InviteToken, error) {
inviteToken := &InviteToken{} inviteToken := &InviteToken{}
if err := tx.QueryRowx("select * from invite_tokens where token = $1", token).StructScan(inviteToken); err != nil { if err := tx.QueryRowx("select * from invite_tokens where token = $1 and not deleted", token).StructScan(inviteToken); err != nil {
return nil, errors.Wrap(err, "error getting unused invite_token") return nil, errors.Wrap(err, "error getting unused invite_token")
} }
return inviteToken, nil return inviteToken, nil
} }
func (str *Store) DeleteInviteToken(id int, tx *sqlx.Tx) error { func (str *Store) DeleteInviteToken(id int, tx *sqlx.Tx) error {
stmt, err := tx.Prepare("delete from invite_tokens where id = $1") stmt, err := tx.Prepare("update invite_tokens set updated_at = current_timestamp, deleted = true where id = $1")
if err != nil { if err != nil {
return errors.Wrap(err, "error preparing invite_tokens delete statement") return errors.Wrap(err, "error preparing invite_tokens delete statement")
} }

View File

@ -0,0 +1,9 @@
package store
type LimitJournalAction string
const (
LimitAction LimitJournalAction = "limit"
WarningAction LimitJournalAction = "warning"
ClearAction LimitJournalAction = "clear"
)

View File

@ -13,9 +13,10 @@ type PasswordResetRequest struct {
Model Model
Token string Token string
AccountId int AccountId int
Deleted bool
} }
func (self *Store) CreatePasswordResetRequest(prr *PasswordResetRequest, tx *sqlx.Tx) (int, error) { func (str *Store) CreatePasswordResetRequest(prr *PasswordResetRequest, tx *sqlx.Tx) (int, error) {
stmt, err := tx.Prepare("insert into password_reset_requests (account_id, token) values ($1, $2) ON CONFLICT(account_id) DO UPDATE SET token=$2 returning id") stmt, err := tx.Prepare("insert into password_reset_requests (account_id, token) values ($1, $2) ON CONFLICT(account_id) DO UPDATE SET token=$2 returning id")
if err != nil { if err != nil {
return 0, errors.Wrap(err, "error preparing password_reset_requests insert statement") return 0, errors.Wrap(err, "error preparing password_reset_requests insert statement")
@ -27,24 +28,24 @@ func (self *Store) CreatePasswordResetRequest(prr *PasswordResetRequest, tx *sql
return id, nil return id, nil
} }
func (self *Store) FindPasswordResetRequestWithToken(token string, tx *sqlx.Tx) (*PasswordResetRequest, error) { func (str *Store) FindPasswordResetRequestWithToken(token string, tx *sqlx.Tx) (*PasswordResetRequest, error) {
prr := &PasswordResetRequest{} prr := &PasswordResetRequest{}
if err := tx.QueryRowx("select * from password_reset_requests where token = $1", token).StructScan(prr); err != nil { if err := tx.QueryRowx("select * from password_reset_requests where token = $1 and not deleted", token).StructScan(prr); err != nil {
return nil, errors.Wrap(err, "error selecting password_reset_requests by token") return nil, errors.Wrap(err, "error selecting password_reset_requests by token")
} }
return prr, nil return prr, nil
} }
func (self *Store) FindExpiredPasswordResetRequests(before time.Time, limit int, tx *sqlx.Tx) ([]*PasswordResetRequest, error) { func (str *Store) FindExpiredPasswordResetRequests(before time.Time, limit int, tx *sqlx.Tx) ([]*PasswordResetRequest, error) {
var sql string var sql string
switch self.cfg.Type { switch str.cfg.Type {
case "postgres": case "postgres":
sql = "select * from password_reset_requests where created_at < $1 limit %d for update" sql = "select * from password_reset_requests where created_at < $1 and not deleted limit %d for update"
case "sqlite3": case "sqlite3":
sql = "select * from password_reset_requests where created_at < $1 limit %d" sql = "select * from password_reset_requests where created_at < $1 and not deleted limit %d"
default: default:
return nil, errors.Errorf("unknown database type '%v'", self.cfg.Type) return nil, errors.Errorf("unknown database type '%v'", str.cfg.Type)
} }
rows, err := tx.Queryx(fmt.Sprintf(sql, limit), before) rows, err := tx.Queryx(fmt.Sprintf(sql, limit), before)
@ -62,8 +63,8 @@ func (self *Store) FindExpiredPasswordResetRequests(before time.Time, limit int,
return prrs, nil return prrs, nil
} }
func (self *Store) DeletePasswordResetRequest(id int, tx *sqlx.Tx) error { func (str *Store) DeletePasswordResetRequest(id int, tx *sqlx.Tx) error {
stmt, err := tx.Prepare("delete from password_reset_requests where id = $1") stmt, err := tx.Prepare("update password_reset_requests set updated_at = current_timestamp, deleted = true where id = $1")
if err != nil { if err != nil {
return errors.Wrap(err, "error preparing password_reset_requests delete statement") return errors.Wrap(err, "error preparing password_reset_requests delete statement")
} }
@ -74,7 +75,7 @@ func (self *Store) DeletePasswordResetRequest(id int, tx *sqlx.Tx) error {
return nil return nil
} }
func (self *Store) DeleteMultiplePasswordResetRequests(ids []int, tx *sqlx.Tx) error { func (str *Store) DeleteMultiplePasswordResetRequests(ids []int, tx *sqlx.Tx) error {
if len(ids) == 0 { if len(ids) == 0 {
return nil return nil
} }
@ -87,7 +88,7 @@ func (self *Store) DeleteMultiplePasswordResetRequests(ids []int, tx *sqlx.Tx) e
indexes[i] = fmt.Sprintf("$%d", i+1) indexes[i] = fmt.Sprintf("$%d", i+1)
} }
stmt, err := tx.Prepare(fmt.Sprintf("delete from password_reset_requests where id in (%s)", strings.Join(indexes, ","))) stmt, err := tx.Prepare(fmt.Sprintf("update password_reset_requests set updated_at = current_timestamp, deleted = true where id in (%s)", strings.Join(indexes, ",")))
if err != nil { if err != nil {
return errors.Wrap(err, "error preparing password_reset_requests delete multiple statement") return errors.Wrap(err, "error preparing password_reset_requests delete multiple statement")
} }

View File

@ -16,9 +16,10 @@ type Share struct {
FrontendEndpoint *string FrontendEndpoint *string
BackendProxyEndpoint *string BackendProxyEndpoint *string
Reserved bool Reserved bool
Deleted bool
} }
func (self *Store) CreateShare(envId int, shr *Share, tx *sqlx.Tx) (int, error) { func (str *Store) CreateShare(envId int, shr *Share, tx *sqlx.Tx) (int, error) {
stmt, err := tx.Prepare("insert into shares (environment_id, z_id, token, share_mode, backend_mode, frontend_selection, frontend_endpoint, backend_proxy_endpoint, reserved) values ($1, $2, $3, $4, $5, $6, $7, $8, $9) returning id") stmt, err := tx.Prepare("insert into shares (environment_id, z_id, token, share_mode, backend_mode, frontend_selection, frontend_endpoint, backend_proxy_endpoint, reserved) values ($1, $2, $3, $4, $5, $6, $7, $8, $9) returning id")
if err != nil { if err != nil {
return 0, errors.Wrap(err, "error preparing shares insert statement") return 0, errors.Wrap(err, "error preparing shares insert statement")
@ -30,7 +31,7 @@ func (self *Store) CreateShare(envId int, shr *Share, tx *sqlx.Tx) (int, error)
return id, nil return id, nil
} }
func (self *Store) GetShare(id int, tx *sqlx.Tx) (*Share, error) { func (str *Store) GetShare(id int, tx *sqlx.Tx) (*Share, error) {
shr := &Share{} shr := &Share{}
if err := tx.QueryRowx("select * from shares where id = $1", id).StructScan(shr); err != nil { if err := tx.QueryRowx("select * from shares where id = $1", id).StructScan(shr); err != nil {
return nil, errors.Wrap(err, "error selecting share by id") return nil, errors.Wrap(err, "error selecting share by id")
@ -38,8 +39,8 @@ func (self *Store) GetShare(id int, tx *sqlx.Tx) (*Share, error) {
return shr, nil return shr, nil
} }
func (self *Store) GetAllShares(tx *sqlx.Tx) ([]*Share, error) { func (str *Store) FindAllShares(tx *sqlx.Tx) ([]*Share, error) {
rows, err := tx.Queryx("select * from shares order by id") rows, err := tx.Queryx("select * from shares where not deleted order by id")
if err != nil { if err != nil {
return nil, errors.Wrap(err, "error selecting all shares") return nil, errors.Wrap(err, "error selecting all shares")
} }
@ -54,16 +55,24 @@ func (self *Store) GetAllShares(tx *sqlx.Tx) ([]*Share, error) {
return shrs, nil return shrs, nil
} }
func (self *Store) FindShareWithToken(shrToken string, tx *sqlx.Tx) (*Share, error) { func (str *Store) FindShareWithToken(shrToken string, tx *sqlx.Tx) (*Share, error) {
shr := &Share{} shr := &Share{}
if err := tx.QueryRowx("select * from shares where token = $1", shrToken).StructScan(shr); err != nil { if err := tx.QueryRowx("select * from shares where token = $1 and not deleted", shrToken).StructScan(shr); err != nil {
return nil, errors.Wrap(err, "error selecting share by token") return nil, errors.Wrap(err, "error selecting share by token")
} }
return shr, nil return shr, nil
} }
func (self *Store) FindSharesForEnvironment(envId int, tx *sqlx.Tx) ([]*Share, error) { func (str *Store) FindShareWithZIdAndDeleted(zId string, tx *sqlx.Tx) (*Share, error) {
rows, err := tx.Queryx("select shares.* from shares where environment_id = $1", envId) shr := &Share{}
if err := tx.QueryRowx("select * from shares where z_id = $1", zId).StructScan(shr); err != nil {
return nil, errors.Wrap(err, "error selecting share by z_id")
}
return shr, nil
}
func (str *Store) FindSharesForEnvironment(envId int, tx *sqlx.Tx) ([]*Share, error) {
rows, err := tx.Queryx("select shares.* from shares where environment_id = $1 and not deleted", envId)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "error selecting shares by environment id") return nil, errors.Wrap(err, "error selecting shares by environment id")
} }
@ -78,7 +87,7 @@ func (self *Store) FindSharesForEnvironment(envId int, tx *sqlx.Tx) ([]*Share, e
return shrs, nil return shrs, nil
} }
func (self *Store) UpdateShare(shr *Share, tx *sqlx.Tx) error { func (str *Store) UpdateShare(shr *Share, tx *sqlx.Tx) error {
sql := "update shares set z_id = $1, token = $2, share_mode = $3, backend_mode = $4, frontend_selection = $5, frontend_endpoint = $6, backend_proxy_endpoint = $7, reserved = $8, updated_at = current_timestamp where id = $9" sql := "update shares set z_id = $1, token = $2, share_mode = $3, backend_mode = $4, frontend_selection = $5, frontend_endpoint = $6, backend_proxy_endpoint = $7, reserved = $8, updated_at = current_timestamp where id = $9"
stmt, err := tx.Prepare(sql) stmt, err := tx.Prepare(sql)
if err != nil { if err != nil {
@ -91,8 +100,8 @@ func (self *Store) UpdateShare(shr *Share, tx *sqlx.Tx) error {
return nil return nil
} }
func (self *Store) DeleteShare(id int, tx *sqlx.Tx) error { func (str *Store) DeleteShare(id int, tx *sqlx.Tx) error {
stmt, err := tx.Prepare("delete from shares where id = $1") stmt, err := tx.Prepare("update shares set updated_at = current_timestamp, deleted = true where id = $1")
if err != nil { if err != nil {
return errors.Wrap(err, "error preparing shares delete statement") return errors.Wrap(err, "error preparing shares delete statement")
} }

View File

@ -0,0 +1,93 @@
package store
import (
"fmt"
"github.com/jmoiron/sqlx"
"github.com/pkg/errors"
)
type ShareLimitJournal struct {
Model
ShareId int
RxBytes int64
TxBytes int64
Action LimitJournalAction
}
func (str *Store) CreateShareLimitJournal(j *ShareLimitJournal, trx *sqlx.Tx) (int, error) {
stmt, err := trx.Prepare("insert into share_limit_journal (share_id, rx_bytes, tx_bytes, action) values ($1, $2, $3, $4) returning id")
if err != nil {
return 0, errors.Wrap(err, "error preparing share_limit_journal insert statement")
}
var id int
if err := stmt.QueryRow(j.ShareId, j.RxBytes, j.TxBytes, j.Action).Scan(&id); err != nil {
return 0, errors.Wrap(err, "error executing share_limit_journal insert statement")
}
return id, nil
}
func (str *Store) IsShareLimitJournalEmpty(shrId int, trx *sqlx.Tx) (bool, error) {
count := 0
if err := trx.QueryRowx("select count(0) from share_limit_journal where share_id = $1", shrId).Scan(&count); err != nil {
return false, err
}
return count == 0, nil
}
func (str *Store) FindLatestShareLimitJournal(shrId int, trx *sqlx.Tx) (*ShareLimitJournal, error) {
j := &ShareLimitJournal{}
if err := trx.QueryRowx("select * from share_limit_journal where share_id = $1 order by created_at desc limit 1", shrId).StructScan(j); err != nil {
return nil, errors.Wrap(err, "error finding share_limit_journal by share_id")
}
return j, nil
}
func (str *Store) FindSelectedLatestShareLimitjournal(shrIds []int, trx *sqlx.Tx) ([]*ShareLimitJournal, error) {
if len(shrIds) < 1 {
return nil, nil
}
in := "("
for i := range shrIds {
if i > 0 {
in += ", "
}
in += fmt.Sprintf("%d", shrIds[i])
}
in += ")"
rows, err := trx.Queryx("select id, share_id, rx_bytes, tx_bytes, action, created_at, updated_at from share_limit_journal where id in (select max(id) as id from share_limit_journal group by share_id) and share_id in " + in)
if err != nil {
return nil, errors.Wrap(err, "error selecting all latest share_limit_journal")
}
var sljs []*ShareLimitJournal
for rows.Next() {
slj := &ShareLimitJournal{}
if err := rows.StructScan(slj); err != nil {
return nil, errors.Wrap(err, "error scanning share_limit_journal")
}
sljs = append(sljs, slj)
}
return sljs, nil
}
func (str *Store) FindAllLatestShareLimitJournal(trx *sqlx.Tx) ([]*ShareLimitJournal, error) {
rows, err := trx.Queryx("select id, share_id, rx_bytes, tx_bytes, action, created_at, updated_at from share_limit_journal where id in (select max(id) as id from share_limit_journal group by share_id)")
if err != nil {
return nil, errors.Wrap(err, "error selecting all latest share_limit_journal")
}
var sljs []*ShareLimitJournal
for rows.Next() {
slj := &ShareLimitJournal{}
if err := rows.StructScan(slj); err != nil {
return nil, errors.Wrap(err, "error scanning share_limit_journal")
}
sljs = append(sljs, slj)
}
return sljs, nil
}
func (str *Store) DeleteShareLimitJournalForShare(shrId int, trx *sqlx.Tx) error {
if _, err := trx.Exec("delete from share_limit_journal where share_id = $1", shrId); err != nil {
return errors.Wrapf(err, "error deleting share_limit_journal for '#%d'", shrId)
}
return nil
}

View File

@ -0,0 +1,9 @@
-- +migrate Up
alter table account_requests add column deleted boolean not null default(false);
alter table accounts add column deleted boolean not null default(false);
alter table environments add column deleted boolean not null default(false);
alter table frontends add column deleted boolean not null default(false);
alter table invite_tokens add column deleted boolean not null default(false);
alter table password_reset_requests add column deleted boolean not null default(false);
alter table shares add column deleted boolean not null default(false);

View File

@ -0,0 +1,33 @@
-- +migrate Up
create type limit_action_type as enum ('clear', 'warning', 'limit');
create table account_limit_journal (
id serial primary key,
account_id integer references accounts(id),
rx_bytes bigint not null,
tx_bytes bigint not null,
action limit_action_type not null,
created_at timestamptz not null default(current_timestamp),
updated_at timestamptz not null default(current_timestamp)
);
create table environment_limit_journal (
id serial primary key,
environment_id integer references environments(id),
rx_bytes bigint not null,
tx_bytes bigint not null,
action limit_action_type not null,
created_at timestamptz not null default(current_timestamp),
updated_at timestamptz not null default(current_timestamp)
);
create table share_limit_journal (
id serial primary key,
share_id integer references shares(id),
rx_bytes bigint not null,
tx_bytes bigint not null,
action limit_action_type not null,
created_at timestamptz not null default(current_timestamp),
updated_at timestamptz not null default(current_timestamp)
);

View File

@ -0,0 +1,31 @@
-- +migrate Up
alter table frontends rename to frontends_old;
alter sequence frontends_id_seq rename to frontends_id_seq_old;
create table frontends (
id serial primary key,
environment_id integer references environments(id),
private_share_id integer references shares(id),
token varchar(32) not null unique,
z_id varchar(32) not null,
url_template varchar(1024),
public_name varchar(64) unique,
reserved boolean not null default(false),
created_at timestamptz not null default(current_timestamp),
updated_at timestamptz not null default(current_timestamp),
deleted boolean not null default(false)
);
insert into frontends (id, environment_id, token, z_id, url_template, public_name, reserved, created_at, updated_at, deleted)
select id, environment_id, token, z_id, url_template, public_name, reserved, created_at, updated_at, deleted from frontends_old;
select setval('frontends_id_seq', (select max(id) from frontends));
drop table frontends_old;
alter index frontends_pkey1 rename to frontends_pkey;
alter index frontends_public_name_key1 rename to frontends_public_name_key;
alter index frontends_token_key1 rename to frontends_token_key;
alter table frontends rename constraint frontends_environment_id_fkey1 to frontends_environment_id_fkey;

View File

@ -0,0 +1,4 @@
-- +migrate Up
alter type backend_mode rename value 'dav' to 'tcpTunnel';
alter type backend_mode add value 'udpTunnel';

View File

@ -0,0 +1,3 @@
-- +migrate Up
ALTER TABLE account_requests DROP CONSTRAINT account_requests_email_key;

Some files were not shown because too many files have changed in this diff Show More