chore: add mercury
This commit is contained in:
157
mercury/app/app_test.go
Normal file
157
mercury/app/app_test.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"go.sour.is/pkg/mercury"
|
||||
"go.sour.is/pkg/ident"
|
||||
)
|
||||
|
||||
type mockUser struct {
|
||||
roles map[string]struct{}
|
||||
ident.SessionInfo
|
||||
}
|
||||
|
||||
func (m *mockUser) Identity() string { return "user" }
|
||||
func (m *mockUser) HasRole(roles ...string) bool {
|
||||
var found bool
|
||||
for _, role := range roles {
|
||||
if _, ok := m.roles[role]; ok {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
return found
|
||||
}
|
||||
|
||||
func Test_appConfig_GetRules(t *testing.T) {
|
||||
type args struct {
|
||||
u ident.Ident
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantLis mercury.Rules
|
||||
}{
|
||||
{"normal", args{&mockUser{}}, nil},
|
||||
{
|
||||
"admin",
|
||||
args{
|
||||
&mockUser{
|
||||
SessionInfo: ident.SessionInfo{Active: true},
|
||||
roles: map[string]struct{}{"admin": {}},
|
||||
},
|
||||
},
|
||||
mercury.Rules{
|
||||
mercury.Rule{
|
||||
Role: "read",
|
||||
Type: "NS",
|
||||
Match: "mercury.source.*",
|
||||
},
|
||||
mercury.Rule{
|
||||
Role: "read",
|
||||
Type: "NS",
|
||||
Match: "mercury.priority",
|
||||
},
|
||||
mercury.Rule{
|
||||
Role: "read",
|
||||
Type: "NS",
|
||||
Match: "mercury.host",
|
||||
},
|
||||
mercury.Rule{
|
||||
Role: "read",
|
||||
Type: "NS",
|
||||
Match: "mercury.environ",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
a := mercuryEnviron{}
|
||||
if gotLis, _ := a.GetRules(context.TODO(), tt.args.u); !reflect.DeepEqual(gotLis, tt.wantLis) {
|
||||
t.Errorf("appConfig.GetRules() = %v, want %v", gotLis, tt.wantLis)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// func Test_appConfig_GetIndex(t *testing.T) {
|
||||
// type args struct {
|
||||
// search mercury.NamespaceSearch
|
||||
// in1 *rsql.Program
|
||||
// }
|
||||
// tests := []struct {
|
||||
// name string
|
||||
// args args
|
||||
// wantLis mercury.Config
|
||||
// }{
|
||||
// {"nil", args{
|
||||
// nil,
|
||||
// nil,
|
||||
// }, nil},
|
||||
|
||||
// {"app.settings", args{
|
||||
// mercury.ParseNamespace("app.settings"),
|
||||
// nil,
|
||||
// }, mercury.Config{&mercury.Space{Space: "app.settings"}}},
|
||||
// }
|
||||
// for _, tt := range tests {
|
||||
// t.Run(tt.name, func(t *testing.T) {
|
||||
// a := mercuryEnviron{}
|
||||
// if gotLis, _ := a.GetIndex(tt.args.search, tt.args.in1); !reflect.DeepEqual(gotLis, tt.wantLis) {
|
||||
// t.Errorf("appConfig.GetIndex() = %#v, want %#v", gotLis, tt.wantLis)
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
|
||||
// func Test_appConfig_GetObjects(t *testing.T) {
|
||||
// cfg, err := mercury.ParseText(strings.NewReader(`
|
||||
// @mercury.source.mercury-settings.default
|
||||
// match :0 *
|
||||
// `))
|
||||
|
||||
// type args struct {
|
||||
// search mercury.NamespaceSearch
|
||||
// in1 *rsql.Program
|
||||
// in2 []string
|
||||
// }
|
||||
// tests := []struct {
|
||||
// name string
|
||||
// args args
|
||||
// wantLis mercury.Config
|
||||
// }{
|
||||
// {"nil", args{
|
||||
// nil,
|
||||
// nil,
|
||||
// nil,
|
||||
// }, nil},
|
||||
|
||||
// {"app.settings", args{
|
||||
// mercury.ParseNamespace("app.settings"),
|
||||
// nil,
|
||||
// nil,
|
||||
// }, mercury.Config{
|
||||
// &mercury.Space{
|
||||
// Space: "app.settings",
|
||||
// List: []mercury.Value{{
|
||||
// Space: "app.settings",
|
||||
// Name: "app.setting",
|
||||
// Values: []string{"TRUE"}},
|
||||
// },
|
||||
// },
|
||||
// }},
|
||||
// }
|
||||
// for _, tt := range tests {
|
||||
// cfg, err :=
|
||||
// t.Run(tt.name, func(t *testing.T) {
|
||||
// a := appConfig{cfg: }
|
||||
// if gotLis, _ := a.GetConfig(tt.args.search, tt.args.in1, tt.args.in2); !reflect.DeepEqual(gotLis, tt.wantLis) {
|
||||
// t.Errorf("appConfig.GetIndex() = %#v, want %#v", gotLis, tt.wantLis)
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
98
mercury/app/default-rules.go
Normal file
98
mercury/app/default-rules.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"go.sour.is/pkg/mercury"
|
||||
"go.sour.is/pkg/ident"
|
||||
)
|
||||
|
||||
type mercuryDefault struct {
|
||||
name string
|
||||
cfg mercury.SpaceMap
|
||||
}
|
||||
|
||||
var (
|
||||
_ mercury.GetRules = (*mercuryDefault)(nil)
|
||||
|
||||
_ mercury.GetIndex = (*mercuryEnviron)(nil)
|
||||
_ mercury.GetConfig = (*mercuryEnviron)(nil)
|
||||
_ mercury.GetRules = (*mercuryEnviron)(nil)
|
||||
)
|
||||
|
||||
// GetRules returns default rules for user role.
|
||||
func (app *mercuryDefault) GetRules(ctx context.Context, id ident.Ident) (lis mercury.Rules, err error) {
|
||||
identity := id.Identity()
|
||||
|
||||
lis = append(lis,
|
||||
mercury.Rule{
|
||||
Role: "write",
|
||||
Type: "NS",
|
||||
Match: "mercury.@" + identity,
|
||||
},
|
||||
mercury.Rule{
|
||||
Role: "write",
|
||||
Type: "NS",
|
||||
Match: "mercury.@" + identity + ".*",
|
||||
},
|
||||
)
|
||||
|
||||
groups := groups(identity, &app.cfg)
|
||||
|
||||
if s, ok := app.cfg.Space("mercury.policy."+app.name); ok {
|
||||
for _, p := range s.List {
|
||||
if groups.Has(p.Name) {
|
||||
for _, r := range p.Values {
|
||||
fds := strings.Fields(r)
|
||||
if len(fds) < 3 {
|
||||
continue
|
||||
}
|
||||
lis = append(lis, mercury.Rule{
|
||||
Role: fds[0],
|
||||
Type: fds[1],
|
||||
Match: fds[2],
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if u, ok := id.(hasRole); groups.Has("admin") || ok && u.HasRole("admin") {
|
||||
lis = append(lis,
|
||||
mercury.Rule{
|
||||
Role: "admin",
|
||||
Type: "NS",
|
||||
Match: "*",
|
||||
},
|
||||
mercury.Rule{
|
||||
Role: "write",
|
||||
Type: "NS",
|
||||
Match: "*",
|
||||
},
|
||||
mercury.Rule{
|
||||
Role: "admin",
|
||||
Type: "GR",
|
||||
Match: "*",
|
||||
},
|
||||
)
|
||||
} else if u.HasRole("write") {
|
||||
lis = append(lis,
|
||||
mercury.Rule{
|
||||
Role: "write",
|
||||
Type: "NS",
|
||||
Match: "*",
|
||||
},
|
||||
)
|
||||
} else if u.HasRole("read") {
|
||||
lis = append(lis,
|
||||
mercury.Rule{
|
||||
Role: "read",
|
||||
Type: "NS",
|
||||
Match: "*",
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return lis, nil
|
||||
}
|
||||
288
mercury/app/environ.go
Normal file
288
mercury/app/environ.go
Normal file
@@ -0,0 +1,288 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/user"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"go.sour.is/pkg/mercury"
|
||||
"go.sour.is/pkg/ident"
|
||||
"go.sour.is/pkg/rsql"
|
||||
"go.sour.is/pkg/set"
|
||||
)
|
||||
|
||||
const (
|
||||
mercurySource = "mercury.source.*"
|
||||
mercuryPriority = "mercury.priority"
|
||||
mercuryHost = "mercury.host"
|
||||
appDotEnviron = "mercury.environ"
|
||||
)
|
||||
var (
|
||||
mercuryPolicy = func(id string) string { return "mercury.@" + id + ".policy" }
|
||||
)
|
||||
|
||||
func Register(name string, cfg mercury.SpaceMap) {
|
||||
for _, c := range cfg {
|
||||
c.Tags = append(c.Tags, "RO")
|
||||
}
|
||||
mercury.Registry.Register("mercury-default", func(s *mercury.Space) any { return &mercuryDefault{name: name, cfg: cfg} })
|
||||
mercury.Registry.Register("mercury-environ", func(s *mercury.Space) any { return &mercuryEnviron{cfg: cfg, lookup: mercury.Registry.GetRules} })
|
||||
}
|
||||
|
||||
type hasRole interface {
|
||||
HasRole(r ...string) bool
|
||||
}
|
||||
|
||||
type mercuryEnviron struct {
|
||||
cfg mercury.SpaceMap
|
||||
lookup func(context.Context, ident.Ident) (mercury.Rules, error)
|
||||
}
|
||||
|
||||
// Index returns nil
|
||||
func (app *mercuryEnviron) GetIndex(ctx context.Context, search mercury.NamespaceSearch, _ *rsql.Program) (lis mercury.Config, err error) {
|
||||
|
||||
if search.Match(mercurySource) {
|
||||
for _, s := range app.cfg.ToArray() {
|
||||
if search.Match(s.Space) {
|
||||
lis = append(lis, &mercury.Space{Space: s.Space, Tags: []string{"RO"}})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if search.Match(mercuryPriority) {
|
||||
lis = append(lis, &mercury.Space{Space: mercuryPriority, Tags: []string{"RO"}})
|
||||
}
|
||||
|
||||
if search.Match(mercuryHost) {
|
||||
lis = append(lis, &mercury.Space{Space: mercuryHost, Tags: []string{"RO"}})
|
||||
}
|
||||
|
||||
if search.Match(appDotEnviron) {
|
||||
lis = append(lis, &mercury.Space{Space: appDotEnviron, Tags: []string{"RO"}})
|
||||
}
|
||||
if id := ident.FromContext(ctx); id != nil {
|
||||
identity := id.Identity()
|
||||
match := mercuryPolicy(identity)
|
||||
if search.Match(match) {
|
||||
lis = append(lis, &mercury.Space{Space: match, Tags: []string{"RO"}})
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Objects returns nil
|
||||
func (app *mercuryEnviron) GetConfig(ctx context.Context, search mercury.NamespaceSearch, _ *rsql.Program, _ []string) (lis mercury.Config, err error) {
|
||||
if search.Match(mercurySource) {
|
||||
for _, s := range app.cfg.ToArray() {
|
||||
if search.Match(s.Space) {
|
||||
lis = append(lis, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if search.Match(mercuryPriority) {
|
||||
space := mercury.Space{
|
||||
Space: mercuryPriority,
|
||||
Tags: []string{"RO"},
|
||||
}
|
||||
|
||||
// for i, key := range mercury.Registry {
|
||||
// space.List = append(space.List, mercury.Value{
|
||||
// Space: appDotPriority,
|
||||
// Seq: uint64(i),
|
||||
// Name: key.Match,
|
||||
// Values: []string{fmt.Sprint(key.Priority)},
|
||||
// })
|
||||
// }
|
||||
|
||||
lis = append(lis, &space)
|
||||
}
|
||||
|
||||
if search.Match(mercuryHost) {
|
||||
if usr, err := user.Current(); err == nil {
|
||||
space := mercury.Space{
|
||||
Space: mercuryHost,
|
||||
Tags: []string{"RO"},
|
||||
}
|
||||
|
||||
hostname, _ := os.Hostname()
|
||||
wd, _ := os.Getwd()
|
||||
grp, _ := usr.GroupIds()
|
||||
space.List = []mercury.Value{
|
||||
{
|
||||
Space: mercuryHost,
|
||||
Seq: 1,
|
||||
Name: "hostname",
|
||||
Values: []string{hostname},
|
||||
},
|
||||
{
|
||||
Space: mercuryHost,
|
||||
Seq: 2,
|
||||
Name: "username",
|
||||
Values: []string{usr.Username},
|
||||
},
|
||||
{
|
||||
Space: mercuryHost,
|
||||
Seq: 3,
|
||||
Name: "uid",
|
||||
Values: []string{usr.Uid},
|
||||
},
|
||||
{
|
||||
Space: mercuryHost,
|
||||
Seq: 4,
|
||||
Name: "gid",
|
||||
Values: []string{usr.Gid},
|
||||
},
|
||||
{
|
||||
Space: mercuryHost,
|
||||
Seq: 5,
|
||||
Name: "display",
|
||||
Values: []string{usr.Name},
|
||||
},
|
||||
{
|
||||
Space: mercuryHost,
|
||||
Seq: 6,
|
||||
Name: "home",
|
||||
Values: []string{usr.HomeDir},
|
||||
},
|
||||
{
|
||||
Space: mercuryHost,
|
||||
Seq: 7,
|
||||
Name: "groups",
|
||||
Values: grp,
|
||||
},
|
||||
{
|
||||
Space: mercuryHost,
|
||||
Seq: 8,
|
||||
Name: "pid",
|
||||
Values: []string{fmt.Sprintf("%v", os.Getpid())},
|
||||
},
|
||||
{
|
||||
Space: mercuryHost,
|
||||
Seq: 9,
|
||||
Name: "wd",
|
||||
Values: []string{wd},
|
||||
},
|
||||
}
|
||||
|
||||
lis = append(lis, &space)
|
||||
}
|
||||
}
|
||||
|
||||
if search.Match(appDotEnviron) {
|
||||
env := os.Environ()
|
||||
space := mercury.Space{
|
||||
Space: appDotEnviron,
|
||||
Tags: []string{"RO"},
|
||||
}
|
||||
|
||||
sort.Strings(env)
|
||||
for i, s := range env {
|
||||
key, val, _ := strings.Cut(s, "=")
|
||||
|
||||
vals := []string{val}
|
||||
if strings.Contains(key, "PATH") || strings.Contains(key, "XDG") {
|
||||
vals = strings.Split(val, ":")
|
||||
}
|
||||
|
||||
space.List = append(space.List, mercury.Value{
|
||||
Space: appDotEnviron,
|
||||
Seq: uint64(i),
|
||||
Name: key,
|
||||
Values: vals,
|
||||
})
|
||||
}
|
||||
lis = append(lis, &space)
|
||||
}
|
||||
|
||||
if id := ident.FromContext(ctx); id != nil {
|
||||
identity := id.Identity()
|
||||
groups := groups(identity, &app.cfg)
|
||||
match := mercuryPolicy(identity)
|
||||
if search.Match(match) {
|
||||
space := &mercury.Space{
|
||||
Space: match,
|
||||
Tags: []string{"RO"},
|
||||
}
|
||||
|
||||
lis = append(lis, space)
|
||||
rules, err := app.lookup(ctx, id)
|
||||
if err != nil {
|
||||
space.AddNotes(err.Error())
|
||||
} else {
|
||||
k := mercury.NewValue("groups")
|
||||
k.AddValues(strings.Join(groups.Values(), " "))
|
||||
space.AddKeys(k)
|
||||
|
||||
k = mercury.NewValue("rules")
|
||||
for _, r := range rules {
|
||||
k.AddValues(strings.Join([]string{r.Role, r.Type, r.Match}, " "))
|
||||
}
|
||||
space.AddKeys(k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Rules returns nil
|
||||
func (app *mercuryEnviron) GetRules(ctx context.Context, id ident.Ident) (lis mercury.Rules, err error) {
|
||||
identity := id.Identity()
|
||||
|
||||
lis = append(lis,
|
||||
mercury.Rule{
|
||||
Role: "read",
|
||||
Type: "NS",
|
||||
Match: mercuryPolicy(identity),
|
||||
},
|
||||
)
|
||||
|
||||
groups := groups(identity, &app.cfg)
|
||||
|
||||
if u, ok := id.(hasRole); groups.Has("admin") || ok && u.HasRole("admin") {
|
||||
lis = append(lis,
|
||||
mercury.Rule{
|
||||
Role: "read",
|
||||
Type: "NS",
|
||||
Match: mercurySource,
|
||||
},
|
||||
mercury.Rule{
|
||||
Role: "read",
|
||||
Type: "NS",
|
||||
Match: mercuryPriority,
|
||||
},
|
||||
mercury.Rule{
|
||||
Role: "read",
|
||||
Type: "NS",
|
||||
Match: mercuryHost,
|
||||
},
|
||||
mercury.Rule{
|
||||
Role: "read",
|
||||
Type: "NS",
|
||||
Match: appDotEnviron,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return lis, nil
|
||||
}
|
||||
|
||||
func groups(identity string, cfg *mercury.SpaceMap) set.Set[string] {
|
||||
groups := set.New[string]()
|
||||
if s, ok := cfg.Space("mercury.groups"); ok {
|
||||
for _, g := range s.List {
|
||||
for _, v := range g.Values {
|
||||
for _, u := range strings.Fields(v) {
|
||||
if u == identity {
|
||||
groups.Add(g.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return groups
|
||||
}
|
||||
63
mercury/http/notify.go
Normal file
63
mercury/http/notify.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"go.sour.is/pkg/lg"
|
||||
"go.sour.is/pkg/mercury"
|
||||
)
|
||||
|
||||
type httpNotify struct{}
|
||||
|
||||
func (httpNotify) SendNotify(ctx context.Context, n mercury.Notify) error {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
cl := &http.Client{}
|
||||
caCertPool, err := x509.SystemCertPool()
|
||||
if err != nil {
|
||||
caCertPool = x509.NewCertPool()
|
||||
}
|
||||
|
||||
// Setup HTTPS client
|
||||
tlsConfig := &tls.Config{
|
||||
RootCAs: caCertPool,
|
||||
}
|
||||
|
||||
transport := &http.Transport{TLSClientConfig: tlsConfig}
|
||||
|
||||
cl.Transport = transport
|
||||
|
||||
var req *http.Request
|
||||
req, err = http.NewRequestWithContext(ctx, n.Method, n.URL, bytes.NewBufferString(""))
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return err
|
||||
}
|
||||
req.Header.Set("content-type", "application/json")
|
||||
|
||||
span.AddEvent(fmt.Sprint("URL: ", n.URL))
|
||||
res, err := cl.Do(req)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return err
|
||||
}
|
||||
res.Body.Close()
|
||||
span.AddEvent(fmt.Sprint(res.Status))
|
||||
if res.StatusCode != 200 {
|
||||
span.RecordError(err)
|
||||
err = fmt.Errorf("unable to read config")
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Register() {
|
||||
mercury.Registry.Register("http-notify", func(s *mercury.Space) any { return httpNotify{} })
|
||||
}
|
||||
695
mercury/mercury.go
Normal file
695
mercury/mercury.go
Normal file
@@ -0,0 +1,695 @@
|
||||
package mercury
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/exp/maps"
|
||||
)
|
||||
|
||||
type Config []*Space
|
||||
|
||||
func NewConfig(spaces ...*Space) Config {
|
||||
return spaces
|
||||
}
|
||||
func (c *Config) AddSpace(spaces ...*Space) *Config {
|
||||
*c = append(*c, spaces...)
|
||||
return c
|
||||
}
|
||||
|
||||
// Len implements Len for sort.interface
|
||||
func (lis Config) Len() int {
|
||||
return len(lis)
|
||||
}
|
||||
|
||||
// Less implements Less for sort.interface
|
||||
func (lis Config) Less(i, j int) bool {
|
||||
return lis[i].Space < lis[j].Space
|
||||
}
|
||||
|
||||
// Swap implements Swap for sort.interface
|
||||
func (lis Config) Swap(i, j int) { lis[i], lis[j] = lis[j], lis[i] }
|
||||
|
||||
// StringList returns the space names as a list
|
||||
func (lis Config) StringList() string {
|
||||
var buf strings.Builder
|
||||
for _, o := range lis {
|
||||
if len(o.Notes) > 0 {
|
||||
buf.WriteString("# ")
|
||||
buf.WriteString(strings.Join(o.Notes, "\n# "))
|
||||
buf.WriteRune('\n')
|
||||
}
|
||||
buf.WriteRune('@')
|
||||
buf.WriteString(o.Space)
|
||||
if len(o.Tags) > 0 {
|
||||
buf.WriteRune(' ')
|
||||
buf.WriteString(strings.Join(o.Tags, " "))
|
||||
}
|
||||
buf.WriteRune('\n')
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// ToSpaceMap formats as SpaceMap
|
||||
func (lis Config) ToSpaceMap() SpaceMap {
|
||||
out := make(SpaceMap)
|
||||
for _, c := range lis {
|
||||
out[c.Space] = c
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// String format config as string
|
||||
func (lis Config) String() string {
|
||||
attLen := 0
|
||||
tagLen := 0
|
||||
|
||||
for _, o := range lis {
|
||||
for _, v := range o.List {
|
||||
l := len(v.Name)
|
||||
if attLen <= l {
|
||||
attLen = l
|
||||
}
|
||||
|
||||
t := len(strings.Join(v.Tags, " "))
|
||||
if tagLen <= t {
|
||||
tagLen = t
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var buf strings.Builder
|
||||
for _, o := range lis {
|
||||
if len(o.Notes) > 0 {
|
||||
buf.WriteString("# ")
|
||||
buf.WriteString(strings.Join(o.Notes, "\n# "))
|
||||
buf.WriteRune('\n')
|
||||
}
|
||||
|
||||
buf.WriteRune('@')
|
||||
buf.WriteString(o.Space)
|
||||
if len(o.Tags) > 0 {
|
||||
buf.WriteRune(' ')
|
||||
buf.WriteString(strings.Join(o.Tags, " "))
|
||||
}
|
||||
buf.WriteRune('\n')
|
||||
|
||||
for _, v := range o.List {
|
||||
if len(v.Notes) > 0 {
|
||||
buf.WriteString("# ")
|
||||
buf.WriteString(strings.Join(v.Notes, "\n# "))
|
||||
buf.WriteString("\n")
|
||||
}
|
||||
|
||||
buf.WriteString(v.Name)
|
||||
buf.WriteString(strings.Repeat(" ", attLen-len(v.Name)+1))
|
||||
|
||||
if len(v.Tags) > 0 {
|
||||
t := strings.Join(v.Tags, " ")
|
||||
buf.WriteString(t)
|
||||
buf.WriteString(strings.Repeat(" ", tagLen-len(t)+1))
|
||||
} else {
|
||||
buf.WriteString(strings.Repeat(" ", tagLen+1))
|
||||
}
|
||||
|
||||
switch len(v.Values) {
|
||||
case 0:
|
||||
buf.WriteString("\n")
|
||||
case 1:
|
||||
buf.WriteString(" :")
|
||||
buf.WriteString(v.Values[0])
|
||||
buf.WriteString("\n")
|
||||
default:
|
||||
buf.WriteString(" :")
|
||||
buf.WriteString(v.Values[0])
|
||||
buf.WriteString("\n")
|
||||
for _, s := range v.Values[1:] {
|
||||
buf.WriteString(strings.Repeat(" ", attLen+tagLen+3))
|
||||
buf.WriteString(":")
|
||||
buf.WriteString(s)
|
||||
buf.WriteString("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buf.WriteRune('\n')
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// EnvString format config as environ
|
||||
func (lis Config) EnvString() string {
|
||||
var buf strings.Builder
|
||||
for _, o := range lis {
|
||||
for _, v := range o.List {
|
||||
buf.WriteString(o.Space)
|
||||
for _, t := range o.Tags {
|
||||
buf.WriteRune(' ')
|
||||
buf.WriteString(t)
|
||||
}
|
||||
buf.WriteRune(':')
|
||||
buf.WriteString(v.Name)
|
||||
for _, t := range v.Tags {
|
||||
buf.WriteRune(' ')
|
||||
buf.WriteString(t)
|
||||
}
|
||||
switch len(v.Values) {
|
||||
case 0:
|
||||
buf.WriteRune('=')
|
||||
buf.WriteRune('\n')
|
||||
case 1:
|
||||
buf.WriteRune('=')
|
||||
buf.WriteString(v.Values[0])
|
||||
buf.WriteRune('\n')
|
||||
default:
|
||||
buf.WriteRune('+')
|
||||
buf.WriteRune('=')
|
||||
buf.WriteString(v.Values[0])
|
||||
buf.WriteRune('\n')
|
||||
for _, s := range v.Values[1:] {
|
||||
buf.WriteString(o.Space)
|
||||
buf.WriteRune(':')
|
||||
buf.WriteString(v.Name)
|
||||
buf.WriteRune('+')
|
||||
buf.WriteRune('=')
|
||||
buf.WriteString(s)
|
||||
buf.WriteRune('\n')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// INIString format config as ini
|
||||
func (lis Config) INIString() string {
|
||||
var buf strings.Builder
|
||||
for _, o := range lis {
|
||||
buf.WriteRune('[')
|
||||
buf.WriteString(o.Space)
|
||||
buf.WriteRune(']')
|
||||
buf.WriteRune('\n')
|
||||
for _, v := range o.List {
|
||||
buf.WriteString(v.Name)
|
||||
switch len(v.Values) {
|
||||
case 0:
|
||||
buf.WriteRune('=')
|
||||
buf.WriteRune('\n')
|
||||
case 1:
|
||||
buf.WriteRune('=')
|
||||
buf.WriteString(v.Values[0])
|
||||
buf.WriteRune('\n')
|
||||
default:
|
||||
buf.WriteRune('[')
|
||||
buf.WriteRune('0')
|
||||
buf.WriteRune(']')
|
||||
|
||||
buf.WriteRune('=')
|
||||
buf.WriteString(v.Values[0])
|
||||
buf.WriteRune('\n')
|
||||
for i, s := range v.Values[1:] {
|
||||
buf.WriteString(v.Name)
|
||||
buf.WriteRune('[')
|
||||
buf.WriteString(fmt.Sprintf("%d", i))
|
||||
buf.WriteRune(']')
|
||||
buf.WriteRune('=')
|
||||
buf.WriteString(s)
|
||||
buf.WriteRune('\n')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// String format config as string
|
||||
func (lis Config) HTMLString() string {
|
||||
attLen := 0
|
||||
tagLen := 0
|
||||
|
||||
for _, o := range lis {
|
||||
for _, v := range o.List {
|
||||
l := len(v.Name)
|
||||
if attLen <= l {
|
||||
attLen = l
|
||||
}
|
||||
|
||||
t := len(strings.Join(v.Tags, " "))
|
||||
if tagLen <= t {
|
||||
tagLen = t
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var buf strings.Builder
|
||||
for _, o := range lis {
|
||||
if len(o.Notes) > 0 {
|
||||
buf.WriteString("<i>")
|
||||
buf.WriteString("# ")
|
||||
buf.WriteString(strings.Join(o.Notes, "\n# "))
|
||||
buf.WriteString("</i>")
|
||||
buf.WriteRune('\n')
|
||||
}
|
||||
|
||||
buf.WriteString("<strong>")
|
||||
buf.WriteRune('@')
|
||||
buf.WriteString(o.Space)
|
||||
buf.WriteString("</strong>")
|
||||
if len(o.Tags) > 0 {
|
||||
buf.WriteRune(' ')
|
||||
buf.WriteString("<em>")
|
||||
buf.WriteString(strings.Join(o.Tags, " "))
|
||||
buf.WriteString("</em>")
|
||||
}
|
||||
buf.WriteRune('\n')
|
||||
|
||||
for _, v := range o.List {
|
||||
if len(v.Notes) > 0 {
|
||||
buf.WriteString("<i>")
|
||||
buf.WriteString("# ")
|
||||
buf.WriteString(strings.Join(v.Notes, "\n# "))
|
||||
buf.WriteString("</i>")
|
||||
buf.WriteString("\n")
|
||||
}
|
||||
|
||||
buf.WriteString("<dfn>")
|
||||
buf.WriteString(v.Name)
|
||||
buf.WriteString("</dfn>")
|
||||
buf.WriteString(strings.Repeat(" ", attLen-len(v.Name)+1))
|
||||
|
||||
if len(v.Tags) > 0 {
|
||||
t := strings.Join(v.Tags, " ")
|
||||
buf.WriteString("<em>")
|
||||
buf.WriteString(t)
|
||||
buf.WriteString(strings.Repeat(" ", tagLen-len(t)+1))
|
||||
buf.WriteString("</em>")
|
||||
} else {
|
||||
buf.WriteString(strings.Repeat(" ", tagLen+1))
|
||||
}
|
||||
|
||||
switch len(v.Values) {
|
||||
case 0:
|
||||
buf.WriteString("\n")
|
||||
case 1:
|
||||
buf.WriteString(" :")
|
||||
buf.WriteString(v.Values[0])
|
||||
buf.WriteString("\n")
|
||||
default:
|
||||
buf.WriteString(" :")
|
||||
buf.WriteString(v.Values[0])
|
||||
buf.WriteString("\n")
|
||||
for _, s := range v.Values[1:] {
|
||||
buf.WriteString(strings.Repeat(" ", attLen+tagLen+3))
|
||||
buf.WriteString(":")
|
||||
buf.WriteString(s)
|
||||
buf.WriteString("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buf.WriteRune('\n')
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// Space stores a registry of spaces
|
||||
type Space struct {
|
||||
Space string `json:"space"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
Notes []string `json:"notes,omitempty"`
|
||||
List []Value `json:"list,omitempty"`
|
||||
}
|
||||
|
||||
func NewSpace(space string) *Space {
|
||||
return &Space{Space: space}
|
||||
}
|
||||
|
||||
// HasTag returns true if needle is found
|
||||
// If the needle ends with a / it will be treated
|
||||
// as a prefix for tag meta data.
|
||||
func (s *Space) HasTag(needle string) bool {
|
||||
isPrefix := strings.HasSuffix(needle, "/")
|
||||
for i := range s.Tags {
|
||||
switch isPrefix {
|
||||
case true:
|
||||
if strings.HasPrefix(s.Tags[i], needle) {
|
||||
return true
|
||||
}
|
||||
case false:
|
||||
if s.Tags[i] == needle {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GetTagMeta retuns the value after a '/' in a tag.
|
||||
// Tags are in the format 'name' or 'name/value'
|
||||
// This function returns the value.
|
||||
func (s *Space) GetTagMeta(needle string, offset int) string {
|
||||
if !strings.HasSuffix(needle, "/") {
|
||||
needle += "/"
|
||||
}
|
||||
|
||||
for i := range s.Tags {
|
||||
if strings.HasPrefix(s.Tags[i], needle) {
|
||||
if offset > 0 {
|
||||
offset--
|
||||
continue
|
||||
}
|
||||
|
||||
return strings.TrimPrefix(s.Tags[i], needle)
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// FirstTagMeta returns the first meta tag value.
|
||||
func (s *Space) FirstTagMeta(needle string) string {
|
||||
return s.GetTagMeta(needle, 0)
|
||||
}
|
||||
|
||||
// GetValues that match name
|
||||
func (s *Space) GetValues(name string) (lis []Value) {
|
||||
for i := range s.List {
|
||||
if s.List[i].Name == name {
|
||||
lis = append(lis, s.List[i])
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// FirstValue that matches name
|
||||
func (s *Space) FirstValue(name string) Value {
|
||||
for i := range s.List {
|
||||
if s.List[i].Name == name {
|
||||
return s.List[i]
|
||||
}
|
||||
}
|
||||
return Value{}
|
||||
}
|
||||
|
||||
func (s *Space) SetTags(tags ...string) *Space {
|
||||
s.Tags = tags
|
||||
return s
|
||||
}
|
||||
func (s *Space) AddTags(tags ...string) *Space {
|
||||
s.Tags = append(s.Tags, tags...)
|
||||
return s
|
||||
}
|
||||
func (s *Space) SetNotes(notes ...string) *Space {
|
||||
s.Notes = notes
|
||||
return s
|
||||
}
|
||||
func (s *Space) AddNotes(notes ...string) *Space {
|
||||
s.Notes = append(s.Notes, notes...)
|
||||
return s
|
||||
}
|
||||
func (s *Space) SetKeys(keys ...*Value) *Space {
|
||||
for i := range keys {
|
||||
k := *keys[i]
|
||||
k.Seq = uint64(i)
|
||||
s.List = append(s.List, k)
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
func (s *Space) AddKeys(keys ...*Value) *Space {
|
||||
l := uint64(len(s.List))
|
||||
for i := range keys {
|
||||
k := *keys[i]
|
||||
k.Seq = uint64(i) + l
|
||||
s.List = append(s.List, k)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// SpaceMap generic map of space values
|
||||
type SpaceMap map[string]*Space
|
||||
|
||||
func (m SpaceMap) Space(name string) (*Space, bool) {
|
||||
s, ok := m[name]
|
||||
return s, ok
|
||||
}
|
||||
|
||||
// Rule is a type of rule
|
||||
type Rule struct {
|
||||
Role string
|
||||
Type string
|
||||
Match string
|
||||
}
|
||||
|
||||
// Rules is a list of rules
|
||||
type Rules []Rule
|
||||
|
||||
// GetNamespaceSearch returns a default search for users rules.
|
||||
func (r Rules) GetNamespaceSearch() (lis NamespaceSearch) {
|
||||
for _, o := range r {
|
||||
if o.Type == "NS" && (o.Role == "read" || o.Role == "write") {
|
||||
lis = append(lis, NamespaceStar(o.Match))
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Check if name matches rule
|
||||
func (r Rule) Check(name string) bool {
|
||||
ok, err := filepath.Match(r.Match, name)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return ok
|
||||
}
|
||||
|
||||
// CheckNamespace verifies user has access
|
||||
func (r Rules) CheckNamespace(search NamespaceSearch) bool {
|
||||
for _, ns := range search {
|
||||
if !r.GetRoles("NS", ns.Value()).HasRole("read", "write") {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (r Rules) Less(i, j int) bool {
|
||||
si, sj := scoreRule(r[i]), scoreRule(r[j])
|
||||
if si != sj {
|
||||
return si < sj
|
||||
}
|
||||
return len(r[i].Match) < len(r[j].Match)
|
||||
}
|
||||
func (r Rules) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
|
||||
func (r Rules) Len() int { return len(r) }
|
||||
|
||||
func scoreRule(r Rule) int {
|
||||
score := 0
|
||||
if r.Type == "GR" {
|
||||
score += 1000
|
||||
}
|
||||
switch r.Role {
|
||||
case "admin":
|
||||
score += 100
|
||||
case "write":
|
||||
score += 50
|
||||
case "read":
|
||||
score += 10
|
||||
}
|
||||
return score
|
||||
}
|
||||
|
||||
// ReduceSearch verifies user has access
|
||||
func (r Rules) ReduceSearch(search NamespaceSearch) (out NamespaceSearch) {
|
||||
rules := r.GetNamespaceSearch()
|
||||
skip := make(map[string]struct{})
|
||||
out = make(NamespaceSearch, 0, len(rules))
|
||||
|
||||
for _, rule := range rules {
|
||||
if _, ok := skip[rule.Raw()]; ok {
|
||||
continue
|
||||
}
|
||||
for _, ck := range search {
|
||||
if _, ok := skip[ck.Raw()]; ok {
|
||||
continue
|
||||
} else if rule.Match(ck.Raw()) {
|
||||
skip[ck.Raw()] = struct{}{}
|
||||
out = append(out, ck)
|
||||
} else if ck.Match(rule.Raw()) {
|
||||
out = append(out, rule)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Roles is a list of roles for a resource
|
||||
type Roles map[string]struct{}
|
||||
|
||||
// GetRoles returns a list of Roles
|
||||
func (r Rules) GetRoles(typ, name string) (lis Roles) {
|
||||
lis = make(Roles)
|
||||
for _, o := range r {
|
||||
if typ == o.Type && o.Check(name) {
|
||||
lis[o.Role] = struct{}{}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// HasRole is a valid role
|
||||
func (r Roles) HasRole(roles ...string) bool {
|
||||
for _, role := range roles {
|
||||
if _, ok := r[role]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ToArray converts SpaceMap to ArraySpace
|
||||
func (m SpaceMap) ToArray() Config {
|
||||
a := make(Config, 0, len(m))
|
||||
for _, s := range m {
|
||||
a = append(a, s)
|
||||
}
|
||||
return a
|
||||
}
|
||||
func (m *SpaceMap) MergeMap(s SpaceMap) {
|
||||
m.Merge(maps.Values(s)...)
|
||||
}
|
||||
func (m *SpaceMap) Merge(lis ...*Space) {
|
||||
for _, s := range lis {
|
||||
// Only accept first version based on priority.
|
||||
if _, ok := (*m)[s.Space]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
(*m)[s.Space] = s
|
||||
|
||||
// // Merge values together.
|
||||
// c, ok := (*m)[s.Space]
|
||||
// if ok {
|
||||
// c = &Space{}
|
||||
// }
|
||||
// c.Notes = append(c.Notes, s.Notes...)
|
||||
// c.Tags = append(c.Tags, s.Tags...)
|
||||
// last := c.List[len(c.List)-1].Seq
|
||||
// for i := range s.List {
|
||||
// v := s.List[i]
|
||||
// v.Seq += last
|
||||
// c.List = append(c.List, v)
|
||||
// }
|
||||
// (*m)[s.Space] = c
|
||||
}
|
||||
}
|
||||
|
||||
// Value stores the attributes for space values
|
||||
type Value struct {
|
||||
Space string `json:"-" db:"space"`
|
||||
Seq uint64 `json:"-" db:"seq"`
|
||||
Name string `json:"name"`
|
||||
Values []string `json:"values"`
|
||||
Notes []string `json:"notes"`
|
||||
Tags []string `json:"tags"`
|
||||
}
|
||||
|
||||
// func (v *Value) ID() string {
|
||||
// return gql.FmtID("MercurySpace:%v:%v", v.Space, v.Seq)
|
||||
// }
|
||||
|
||||
// HasTag returns true if needle is found
|
||||
// If the needle ends with a / it will be treated
|
||||
// as a prefix for tag meta data.
|
||||
func (v Value) HasTag(needle string) bool {
|
||||
isPrefix := strings.HasSuffix(needle, "/")
|
||||
for i := range v.Tags {
|
||||
switch isPrefix {
|
||||
case true:
|
||||
if strings.HasPrefix(v.Tags[i], needle) {
|
||||
return true
|
||||
}
|
||||
case false:
|
||||
if v.Tags[i] == needle {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GetTagMeta retuns the value after a '/' in a tag.
|
||||
// Tags are in the format 'name' or 'name/value'
|
||||
// This function returns the value.
|
||||
func (v Value) GetTagMeta(needle string, offset int) string {
|
||||
if !strings.HasSuffix(needle, "/") {
|
||||
needle += "/"
|
||||
}
|
||||
|
||||
for i := range v.Tags {
|
||||
if strings.HasPrefix(v.Tags[i], needle) {
|
||||
if offset > 0 {
|
||||
offset--
|
||||
continue
|
||||
}
|
||||
|
||||
return strings.TrimPrefix(v.Tags[i], needle)
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// FirstTagMeta returns the first meta tag value.
|
||||
func (v Value) FirstTagMeta(needle string) string {
|
||||
return v.GetTagMeta(needle, 0)
|
||||
}
|
||||
|
||||
// First value in array.
|
||||
func (v Value) First() string {
|
||||
if len(v.Values) < 1 {
|
||||
return ""
|
||||
}
|
||||
|
||||
return v.Values[0]
|
||||
}
|
||||
|
||||
// Join values with newlines.
|
||||
func (v Value) Join() string {
|
||||
return strings.Join(v.Values, "\n")
|
||||
}
|
||||
|
||||
func NewValue(name string) *Value {
|
||||
return &Value{Name: name}
|
||||
}
|
||||
func (v *Value) SetTags(tags ...string) *Value {
|
||||
v.Tags = tags
|
||||
return v
|
||||
}
|
||||
func (v *Value) AddTags(tags ...string) *Value {
|
||||
v.Tags = append(v.Tags, tags...)
|
||||
return v
|
||||
}
|
||||
func (v *Value) SetNotes(notes ...string) *Value {
|
||||
v.Notes = notes
|
||||
return v
|
||||
}
|
||||
func (v *Value) AddNotes(notes ...string) *Value {
|
||||
v.Notes = append(v.Notes, notes...)
|
||||
return v
|
||||
}
|
||||
func (v *Value) SetValues(values ...string) *Value {
|
||||
v.Values = values
|
||||
return v
|
||||
}
|
||||
func (v *Value) AddValues(values ...string) *Value {
|
||||
v.Values = append(v.Values, values...)
|
||||
return v
|
||||
}
|
||||
27
mercury/mqtt/notify.go
Normal file
27
mercury/mqtt/notify.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package mqtt
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.sour.is/pkg/lg"
|
||||
"go.sour.is/pkg/mercury"
|
||||
)
|
||||
|
||||
type mqttNotify struct{}
|
||||
|
||||
func (mqttNotify) SendNotify(ctx context.Context, n mercury.Notify) {
|
||||
_, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
// var m mqtt.Message
|
||||
// m, err = mqtt.NewMessage(n.URL, n)
|
||||
// if err != nil {
|
||||
// return
|
||||
// }
|
||||
// log.Debug(n)
|
||||
// err = mqtt.Publish(m)
|
||||
// return
|
||||
}
|
||||
|
||||
func Register() {
|
||||
mercury.Registry.Register("mqtt-notify", func(s *mercury.Space) any { return &mqttNotify{} })
|
||||
}
|
||||
125
mercury/namespace.go
Normal file
125
mercury/namespace.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package mercury
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// NamespaceSpec implements a parsed namespace search
|
||||
type NamespaceSpec interface {
|
||||
Value() string
|
||||
String() string
|
||||
Raw() string
|
||||
Match(string) bool
|
||||
}
|
||||
|
||||
// NamespaceSearch list of namespace specs
|
||||
type NamespaceSearch []NamespaceSpec
|
||||
|
||||
// ParseNamespace returns a list of parsed values
|
||||
func ParseNamespace(ns string) (lis NamespaceSearch) {
|
||||
for _, part := range strings.Split(ns, ";") {
|
||||
if strings.HasPrefix(part, "trace:") {
|
||||
for _, s := range strings.Split(part[6:], ",") {
|
||||
lis = append(lis, NamespaceTrace(s))
|
||||
}
|
||||
} else {
|
||||
for _, s := range strings.Split(part, ",") {
|
||||
if strings.Contains(s, "*") {
|
||||
lis = append(lis, NamespaceStar(s))
|
||||
} else {
|
||||
lis = append(lis, NamespaceNode(s))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// String output string value
|
||||
func (n NamespaceSearch) String() string {
|
||||
lis := make([]string, 0, len(n))
|
||||
|
||||
for _, v := range n {
|
||||
lis = append(lis, v.String())
|
||||
}
|
||||
return strings.Join(lis, ",")
|
||||
}
|
||||
|
||||
// Match returns true if any match.
|
||||
func (n NamespaceSearch) Match(s string) bool {
|
||||
for _, m := range n {
|
||||
ok, err := filepath.Match(m.Raw(), s)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// NamespaceNode implements a node search value
|
||||
type NamespaceNode string
|
||||
|
||||
// String output string value
|
||||
func (n NamespaceNode) String() string { return string(n) }
|
||||
|
||||
// Quote return quoted value.
|
||||
// func (n NamespaceNode) Quote() string { return `'` + n.Value() + `'` }
|
||||
|
||||
// Value to return the value
|
||||
func (n NamespaceNode) Value() string { return string(n) }
|
||||
|
||||
// Raw return raw value.
|
||||
func (n NamespaceNode) Raw() string { return string(n) }
|
||||
|
||||
// Match returns true if any match.
|
||||
func (n NamespaceNode) Match(s string) bool { return match(n, s) }
|
||||
|
||||
// NamespaceTrace implements a trace search value
|
||||
type NamespaceTrace string
|
||||
|
||||
// String output string value
|
||||
func (n NamespaceTrace) String() string { return "trace:" + string(n) }
|
||||
|
||||
// Quote return quoted value.
|
||||
// func (n NamespaceTrace) Quote() string { return `'` + n.Value() + `'` }
|
||||
|
||||
// Value to return the value
|
||||
func (n NamespaceTrace) Value() string { return strings.Replace(string(n), "*", "%", -1) }
|
||||
|
||||
// Raw return raw value.
|
||||
func (n NamespaceTrace) Raw() string { return string(n) }
|
||||
|
||||
// Match returns true if any match.
|
||||
func (n NamespaceTrace) Match(s string) bool { return match(n, s) }
|
||||
|
||||
// NamespaceStar implements a trace search value
|
||||
type NamespaceStar string
|
||||
|
||||
// String output string value
|
||||
func (n NamespaceStar) String() string { return string(n) }
|
||||
|
||||
// Quote return quoted value.
|
||||
// func (n NamespaceStar) Quote() string { return `'` + n.Value() + `'` }
|
||||
|
||||
// Value to return the value
|
||||
func (n NamespaceStar) Value() string { return strings.Replace(string(n), "*", "%", -1) }
|
||||
|
||||
// Raw return raw value.
|
||||
func (n NamespaceStar) Raw() string { return string(n) }
|
||||
|
||||
// Match returns true if any match.
|
||||
func (n NamespaceStar) Match(s string) bool { return match(n, s) }
|
||||
|
||||
func match(n NamespaceSpec, s string) bool {
|
||||
ok, err := filepath.Match(n.Raw(), s)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return ok
|
||||
}
|
||||
110
mercury/parse.go
Normal file
110
mercury/parse.go
Normal file
@@ -0,0 +1,110 @@
|
||||
package mercury
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func ParseText(body io.Reader) (config SpaceMap, err error) {
|
||||
config = make(SpaceMap)
|
||||
|
||||
var space string
|
||||
var name string
|
||||
var tags []string
|
||||
var notes []string
|
||||
var seq uint64
|
||||
|
||||
scanner := bufio.NewScanner(body)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(line, "#") {
|
||||
notes = append(notes, strings.TrimPrefix(line, "# "))
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(line, "@") {
|
||||
var c *Space
|
||||
var ok bool
|
||||
|
||||
sp := strings.Fields(strings.TrimPrefix(line, "@"))
|
||||
space = sp[0]
|
||||
|
||||
if c, ok = config[space]; !ok {
|
||||
c = &Space{Space: space}
|
||||
}
|
||||
|
||||
c.Notes = append(make([]string, 0, len(notes)), notes...)
|
||||
c.Tags = append(make([]string, 0, len(sp[1:])), sp[1:]...)
|
||||
|
||||
config[space] = c
|
||||
notes = notes[:0]
|
||||
tags = tags[:0]
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if space == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
sp := strings.SplitN(line, ":", 2)
|
||||
if len(sp) < 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.TrimSpace(sp[0]) == "" {
|
||||
var c *Space
|
||||
var ok bool
|
||||
|
||||
if c, ok = config[space]; !ok {
|
||||
c = &Space{Space: space}
|
||||
}
|
||||
|
||||
c.List[len(c.List)-1].Values = append(c.List[len(c.List)-1].Values, sp[1])
|
||||
config[space] = c
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
fields := strings.Fields(sp[0])
|
||||
name = fields[0]
|
||||
if len(fields) > 1 {
|
||||
tags = fields[1:]
|
||||
}
|
||||
|
||||
var c *Space
|
||||
var ok bool
|
||||
|
||||
if c, ok = config[space]; !ok {
|
||||
c = &Space{Space: space}
|
||||
}
|
||||
|
||||
seq++
|
||||
c.List = append(
|
||||
c.List,
|
||||
Value{
|
||||
Seq: seq,
|
||||
Name: name,
|
||||
Tags: append(make([]string, 0, len(tags)), tags...),
|
||||
Notes: append(make([]string, 0, len(notes)), notes...),
|
||||
Values: []string{sp[1]},
|
||||
},
|
||||
)
|
||||
config[space] = c
|
||||
|
||||
notes = notes[:0]
|
||||
tags = tags[:0]
|
||||
}
|
||||
|
||||
if err = scanner.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
BIN
mercury/public/favicon.ico
Normal file
BIN
mercury/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
47
mercury/public/index.html
Normal file
47
mercury/public/index.html
Normal file
@@ -0,0 +1,47 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>☿ Mercury ☿</title>
|
||||
<script src="https://unpkg.com/htmx.org@2.0.0-alpha1/dist/htmx.min.js"></script>
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/firacode@6.2.0/distr/fira_code.css" />
|
||||
<link rel="stylesheet" href="/style.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header>
|
||||
<nav style="position: absolute; top:0; right:50px" hx-trigger="load" hx-get="/ident"></nav>
|
||||
<h1>☿ Mercury ☿</h1>
|
||||
</header>
|
||||
|
||||
<div class="container">
|
||||
<div>
|
||||
<form class="search" hx-get="/api/v1/mercury/config" hx-target="#config-results"
|
||||
hx-headers='{"Accept": "text/html"}''>
|
||||
<div>@</div>
|
||||
<input id="space-config" name="space" type="text" placeholder="Space...">
|
||||
<button type="submit">Load</button>
|
||||
</form>
|
||||
<code tabindex="0"><pre id="config-results"></pre></code>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<form class="edit" hx-post="/api/v1/mercury/config" hx-target="#space-saved" hx-encoding="multipart/form-data">
|
||||
<button type="submit">Save</button>
|
||||
<br />
|
||||
<textarea name="content" rows="45" wrap="off"
|
||||
onkeyup="if (this.scrollHeight > this.clientHeight) this.style.height = this.scrollHeight + ' px';"
|
||||
style="overflow:auto; overflow-y:hidden; transition: height 0.2s ease-out;"></textarea>
|
||||
</form>
|
||||
<pre id="space-saved"></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
sour.is 🅭2024
|
||||
<span hx-trigger="load" hx-get="/api/v1/app-info"></span>
|
||||
</footer>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
212
mercury/public/style.css
Normal file
212
mercury/public/style.css
Normal file
@@ -0,0 +1,212 @@
|
||||
* {
|
||||
font-weight: lighter;
|
||||
font-family: 'fira code', monospace;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
header {
|
||||
margin: 0 50px;
|
||||
background: rgb(63, 94, 251);
|
||||
background: radial-gradient(circle, rgba(63, 94, 251, 1) 0%, rgba(252, 70, 107, 1) 100%);
|
||||
height: 100px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: white;
|
||||
line-height: 100px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
input,
|
||||
textarea {
|
||||
font-size: small;
|
||||
border: 2px solid cornflowerblue;
|
||||
}
|
||||
|
||||
input.invalid {
|
||||
border-color: red;
|
||||
}
|
||||
|
||||
button {
|
||||
border: 2px solid cornflowerblue;
|
||||
}
|
||||
|
||||
code,
|
||||
pre {
|
||||
font-size: small;
|
||||
-webkit-user-select: text;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
code strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
code dfn {
|
||||
color: green
|
||||
}
|
||||
|
||||
code i {
|
||||
color: grey
|
||||
}
|
||||
|
||||
code em {
|
||||
color: orange;
|
||||
}
|
||||
|
||||
code:focus {
|
||||
animation: select 100ms step-end forwards;
|
||||
}
|
||||
|
||||
footer {
|
||||
position: fixed;
|
||||
font-size: x-small;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1em;
|
||||
padding: 4px;
|
||||
color: white;
|
||||
border-top: 1px solid black;
|
||||
background: cornflowerblue;
|
||||
}
|
||||
|
||||
footer>span {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin: 0 50px;
|
||||
display: grid;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.container .open {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.container>div {
|
||||
overflow: auto;
|
||||
padding: 10px;
|
||||
background: rgb(238, 174, 202);
|
||||
border: 0px ;
|
||||
}
|
||||
|
||||
.container>div>code {
|
||||
-webkit-user-select: all;
|
||||
/* for Safari */
|
||||
user-select: all;
|
||||
}
|
||||
|
||||
.search {
|
||||
display: flex
|
||||
}
|
||||
|
||||
.search>div {
|
||||
background-color: lightgrey;
|
||||
border-radius: 5px 0px 0px 5px;
|
||||
border: 2px solid cornflowerblue;
|
||||
border-right: 0;
|
||||
user-select: none;
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
.search>button {
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
.search>div {
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
.search>input {
|
||||
flex-grow: 5;
|
||||
}
|
||||
|
||||
.search>button {
|
||||
flex-grow: 2;
|
||||
border-radius: 0px 5px 5px 0px;
|
||||
}
|
||||
|
||||
.edit {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.edit>button {
|
||||
line-height: 28px;
|
||||
border-bottom: 0;
|
||||
border-radius: 5px 5px 0 0;
|
||||
}
|
||||
|
||||
.edit>textarea {
|
||||
margin-top: 0;
|
||||
border-radius: 0 0 5px 5px;
|
||||
}
|
||||
|
||||
@keyframes select {
|
||||
to {
|
||||
-webkit-user-select: text;
|
||||
user-select: text;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
color: white;
|
||||
background: #111
|
||||
}
|
||||
|
||||
header {
|
||||
background: #111;
|
||||
background: radial-gradient(circle, rgba(2, 0, 36, 1) 0%, rgba(7, 80, 29, 1) 35%, rgba(0, 0, 0, 1) 100%);
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.container>div {
|
||||
background: #111;
|
||||
/* background: linear-gradient(304deg, rgba(2, 0, 36, 1) 0%, rgba(77, 77, 77, 1) 18%, rgba(0, 0, 0, 1) 100%); */
|
||||
border: 2px solid rgb(117, 117, 117);
|
||||
}
|
||||
|
||||
.search>div {
|
||||
background-color: grey;
|
||||
border-color: rgb(117, 117, 117);
|
||||
}
|
||||
|
||||
button {
|
||||
color: white;
|
||||
background: rgba(7, 80, 29, 1);
|
||||
border: 2px solid rgb(117, 117, 117);
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: rgb(11, 121, 44);
|
||||
}
|
||||
|
||||
button:active {
|
||||
background: rgb(5, 59, 21);
|
||||
}
|
||||
|
||||
input,
|
||||
textarea {
|
||||
color: white;
|
||||
background-color: #111;
|
||||
border: 2px solid rgb(117, 117, 117);
|
||||
}
|
||||
footer {
|
||||
color: white;
|
||||
border-top: 1px solid white;
|
||||
background: #222;
|
||||
|
||||
}
|
||||
}
|
||||
400
mercury/registry.go
Normal file
400
mercury/registry.go
Normal file
@@ -0,0 +1,400 @@
|
||||
package mercury
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"go.sour.is/pkg/lg"
|
||||
"go.sour.is/pkg/ident"
|
||||
"go.sour.is/pkg/rsql"
|
||||
"go.sour.is/pkg/set"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
type GetIndex interface {
|
||||
GetIndex(context.Context, NamespaceSearch, *rsql.Program) (Config, error)
|
||||
}
|
||||
type GetConfig interface {
|
||||
GetConfig(context.Context, NamespaceSearch, *rsql.Program, []string) (Config, error)
|
||||
}
|
||||
type WriteConfig interface {
|
||||
WriteConfig(context.Context, Config) error
|
||||
}
|
||||
type GetRules interface {
|
||||
GetRules(context.Context, ident.Ident) (Rules, error)
|
||||
}
|
||||
type GetNotify interface {
|
||||
GetNotify(context.Context, string) (ListNotify, error)
|
||||
}
|
||||
type SendNotify interface {
|
||||
SendNotify(context.Context, Notify) error
|
||||
}
|
||||
|
||||
// type nobody struct{}
|
||||
|
||||
// func (nobody) IsActive() bool { return true }
|
||||
// func (nobody) Identity() string { return "xuu" }
|
||||
// func (nobody) HasRole(r ...string) bool { return true }
|
||||
|
||||
func (reg *registry) accessFilter(rules Rules, lis Config) (out Config, err error) {
|
||||
accessList := make(map[string]struct{})
|
||||
for _, o := range lis {
|
||||
if _, ok := accessList[o.Space]; ok {
|
||||
out = append(out, o)
|
||||
continue
|
||||
}
|
||||
|
||||
if role := rules.GetRoles("NS", o.Space); role.HasRole("read", "write") && !role.HasRole("deny") {
|
||||
accessList[o.Space] = struct{}{}
|
||||
out = append(out, o)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// HandlerItem a single handler matching
|
||||
type matcher[T any] struct {
|
||||
Name string
|
||||
Match NamespaceSearch
|
||||
Priority int
|
||||
Handler T
|
||||
}
|
||||
type matchers struct {
|
||||
getIndex []matcher[GetIndex]
|
||||
getConfig []matcher[GetConfig]
|
||||
writeConfig []matcher[WriteConfig]
|
||||
getRules []matcher[GetRules]
|
||||
getNotify []matcher[GetNotify]
|
||||
sendNotify []matcher[SendNotify]
|
||||
}
|
||||
|
||||
// registry a list of handlers
|
||||
type registry struct {
|
||||
handlers map[string]func(*Space) any
|
||||
matchers matchers
|
||||
}
|
||||
|
||||
func (m matcher[T]) String() string {
|
||||
return fmt.Sprintf("%d: %s", m.Priority, m.Match)
|
||||
}
|
||||
|
||||
// Registry handler
|
||||
var Registry *registry = ®istry{}
|
||||
|
||||
func (r registry) String() string {
|
||||
var buf strings.Builder
|
||||
for h := range r.handlers {
|
||||
buf.WriteString(h)
|
||||
buf.WriteRune('\n')
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func (r *registry) resetMatchers() {
|
||||
r.matchers.getIndex = r.matchers.getIndex[:0]
|
||||
r.matchers.getConfig = r.matchers.getConfig[:0]
|
||||
r.matchers.writeConfig = r.matchers.writeConfig[:0]
|
||||
r.matchers.getRules = r.matchers.getRules[:0]
|
||||
r.matchers.getNotify = r.matchers.getNotify[:0]
|
||||
r.matchers.sendNotify = r.matchers.sendNotify[:0]
|
||||
}
|
||||
func (r *registry) sortMatchers() {
|
||||
sort.Slice(r.matchers.getConfig, func(i, j int) bool { return r.matchers.getConfig[i].Priority < r.matchers.getConfig[j].Priority })
|
||||
sort.Slice(r.matchers.getIndex, func(i, j int) bool { return r.matchers.getIndex[i].Priority < r.matchers.getIndex[j].Priority })
|
||||
sort.Slice(r.matchers.writeConfig, func(i, j int) bool { return r.matchers.writeConfig[i].Priority < r.matchers.writeConfig[j].Priority })
|
||||
sort.Slice(r.matchers.getRules, func(i, j int) bool { return r.matchers.getRules[i].Priority < r.matchers.getRules[j].Priority })
|
||||
sort.Slice(r.matchers.getNotify, func(i, j int) bool { return r.matchers.getNotify[i].Priority < r.matchers.getNotify[j].Priority })
|
||||
sort.Slice(r.matchers.sendNotify, func(i, j int) bool { return r.matchers.sendNotify[i].Priority < r.matchers.sendNotify[j].Priority })
|
||||
}
|
||||
func (r *registry) Register(name string, h func(*Space) any) {
|
||||
if r.handlers == nil {
|
||||
r.handlers = make(map[string]func(*Space) any)
|
||||
}
|
||||
r.handlers[name] = h
|
||||
}
|
||||
|
||||
func (r *registry) Configure(m SpaceMap) error {
|
||||
r.resetMatchers()
|
||||
for space, c := range m {
|
||||
space = strings.TrimPrefix(space, "mercury.source.")
|
||||
handler, name, _ := strings.Cut(space, ".")
|
||||
matches := c.FirstValue("match")
|
||||
for _, match := range matches.Values {
|
||||
ps := strings.Fields(match)
|
||||
priority, err := strconv.Atoi(ps[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.add(name, handler, ps[1], priority, c)
|
||||
}
|
||||
}
|
||||
|
||||
r.sortMatchers()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Register add a handler to registry
|
||||
func (r *registry) add(name, handler, match string, priority int, cfg *Space) error {
|
||||
// log.Infos("mercury regster", "match", match, "pri", priority)
|
||||
mkHandler, ok := r.handlers[handler]
|
||||
if !ok {
|
||||
return fmt.Errorf("handler not registered: %s", handler)
|
||||
}
|
||||
hdlr := mkHandler(cfg)
|
||||
if err, ok := hdlr.(error); ok {
|
||||
return fmt.Errorf("%w: failed to config %s as handler: %s", err, name, handler)
|
||||
}
|
||||
if hdlr == nil {
|
||||
return fmt.Errorf("failed to config %s as handler: %s", name, handler)
|
||||
}
|
||||
|
||||
if hdlr, ok := hdlr.(GetIndex); ok {
|
||||
r.matchers.getIndex = append(
|
||||
r.matchers.getIndex,
|
||||
matcher[GetIndex]{Name: name, Match: ParseNamespace(match), Priority: priority, Handler: hdlr},
|
||||
)
|
||||
}
|
||||
if hdlr, ok := hdlr.(GetConfig); ok {
|
||||
r.matchers.getConfig = append(
|
||||
r.matchers.getConfig,
|
||||
matcher[GetConfig]{Name: name, Match: ParseNamespace(match), Priority: priority, Handler: hdlr},
|
||||
)
|
||||
}
|
||||
|
||||
if hdlr, ok := hdlr.(WriteConfig); ok {
|
||||
|
||||
r.matchers.writeConfig = append(
|
||||
r.matchers.writeConfig,
|
||||
matcher[WriteConfig]{Name: name, Match: ParseNamespace(match), Priority: priority, Handler: hdlr},
|
||||
)
|
||||
}
|
||||
if hdlr, ok := hdlr.(GetRules); ok {
|
||||
r.matchers.getRules = append(
|
||||
r.matchers.getRules,
|
||||
matcher[GetRules]{Name: name, Match: ParseNamespace(match), Priority: priority, Handler: hdlr},
|
||||
)
|
||||
}
|
||||
if hdlr, ok := hdlr.(GetNotify); ok {
|
||||
r.matchers.getNotify = append(
|
||||
r.matchers.getNotify,
|
||||
matcher[GetNotify]{Name: name, Match: ParseNamespace(match), Priority: priority, Handler: hdlr},
|
||||
)
|
||||
}
|
||||
if hdlr, ok := hdlr.(SendNotify); ok {
|
||||
r.matchers.sendNotify = append(
|
||||
r.matchers.sendNotify,
|
||||
matcher[SendNotify]{Name: name, Match: ParseNamespace(match), Priority: priority, Handler: hdlr},
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetIndex query each handler that match namespace.
|
||||
func (r *registry) GetIndex(ctx context.Context, match, search string) (c Config, err error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
spec := ParseNamespace(match)
|
||||
pgm := rsql.DefaultParse(search)
|
||||
matches := make([]NamespaceSearch, len(r.matchers.getIndex))
|
||||
|
||||
for _, n := range spec {
|
||||
for i, hdlr := range r.matchers.getIndex {
|
||||
if hdlr.Match.Match(n.Raw()) {
|
||||
matches[i] = append(matches[i], n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wg, ctx := errgroup.WithContext(ctx)
|
||||
slots := make(chan Config, len(r.matchers.getConfig))
|
||||
wg.Go(func() error {
|
||||
i := 0
|
||||
for lis := range slots {
|
||||
c = append(c, lis...)
|
||||
i++
|
||||
if i > len(slots) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
for i, hdlr := range r.matchers.getIndex {
|
||||
i, hdlr := i, hdlr
|
||||
|
||||
wg.Go(func() error {
|
||||
span.AddEvent(fmt.Sprintf("INDEX %s %s", hdlr.Name, hdlr.Match))
|
||||
lis, err := hdlr.Handler.GetIndex(ctx, matches[i], pgm)
|
||||
slots <- lis
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
err = wg.Wait()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Search query each handler with a key=value search
|
||||
|
||||
// GetConfig query each handler that match for fully qualified namespaces.
|
||||
func (r *registry) GetConfig(ctx context.Context, match, search, fields string) (Config, error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
spec := ParseNamespace(match)
|
||||
pgm := rsql.DefaultParse(search)
|
||||
flds := strings.Split(fields, ",")
|
||||
|
||||
matches := make([]NamespaceSearch, len(r.matchers.getConfig))
|
||||
|
||||
for _, n := range spec {
|
||||
for i, hdlr := range r.matchers.getConfig {
|
||||
if hdlr.Match.Match(n.Raw()) {
|
||||
matches[i] = append(matches[i], n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m := make(SpaceMap)
|
||||
for i, hdlr := range r.matchers.getConfig {
|
||||
if len(matches[i]) == 0 {
|
||||
continue
|
||||
}
|
||||
span.AddEvent(fmt.Sprintf("QUERY %s %s", hdlr.Name, hdlr.Match))
|
||||
lis, err := hdlr.Handler.GetConfig(ctx, matches[i], pgm, flds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.Merge(lis...)
|
||||
}
|
||||
|
||||
return m.ToArray(), nil
|
||||
}
|
||||
|
||||
// WriteConfig write objects to backends
|
||||
func (r *registry) WriteConfig(ctx context.Context, spaces Config) error {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
matches := make([]Config, len(r.matchers.writeConfig))
|
||||
|
||||
for _, s := range spaces {
|
||||
for i, hdlr := range r.matchers.writeConfig {
|
||||
if hdlr.Match.Match(s.Space) {
|
||||
matches[i] = append(matches[i], s)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i, hdlr := range r.matchers.writeConfig {
|
||||
if len(matches[i]) == 0 {
|
||||
continue
|
||||
}
|
||||
span.AddEvent(fmt.Sprint("WRITE MATCH", hdlr.Name, hdlr.Match))
|
||||
err := hdlr.Handler.WriteConfig(ctx, matches[i])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetRules query each of the handlers for rules.
|
||||
func (r *registry) GetRules(ctx context.Context, user ident.Ident) (Rules, error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
s := set.New[Rule]()
|
||||
for _, hdlr := range r.matchers.getRules {
|
||||
span.AddEvent(fmt.Sprint("RULES", hdlr.Name, hdlr.Match))
|
||||
lis, err := hdlr.Handler.GetRules(ctx, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.Add(lis...)
|
||||
}
|
||||
var rules Rules = s.Values()
|
||||
sort.Sort(rules)
|
||||
return rules, nil
|
||||
}
|
||||
|
||||
// GetNotify query each of the handlers for rules.
|
||||
func (r *registry) GetNotify(ctx context.Context, event string) (ListNotify, error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
s := set.New[Notify]()
|
||||
for _, hdlr := range r.matchers.getNotify {
|
||||
span.AddEvent(fmt.Sprint("GET NOTIFY", hdlr.Name, hdlr.Match))
|
||||
|
||||
lis, err := hdlr.Handler.GetNotify(ctx, event)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.Add(lis...)
|
||||
}
|
||||
|
||||
return s.Values(), nil
|
||||
}
|
||||
|
||||
func (r *registry) SendNotify(ctx context.Context, n Notify) (err error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
for _, hdlr := range r.matchers.sendNotify {
|
||||
span.AddEvent(fmt.Sprint("SEND NOTIFY", hdlr.Name, hdlr.Match))
|
||||
|
||||
err := hdlr.Handler.SendNotify(ctx, n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Check if name matches notify
|
||||
func (n Notify) Check(name string) bool {
|
||||
ok, err := filepath.Match(n.Match, name)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return ok
|
||||
}
|
||||
|
||||
// Notify stores the attributes for a registry space
|
||||
type Notify struct {
|
||||
Name string
|
||||
Match string
|
||||
Event string
|
||||
Method string
|
||||
URL string
|
||||
}
|
||||
|
||||
// ListNotify array of notify
|
||||
type ListNotify []Notify
|
||||
|
||||
// Find returns list of notify that match name.
|
||||
func (ln ListNotify) Find(name string) (lis ListNotify) {
|
||||
lis = make(ListNotify, 0, len(ln))
|
||||
for _, o := range ln {
|
||||
if o.Check(name) {
|
||||
lis = append(lis, o)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
267
mercury/routes.go
Normal file
267
mercury/routes.go
Normal file
@@ -0,0 +1,267 @@
|
||||
package mercury
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
"github.com/golang/gddo/httputil"
|
||||
"go.sour.is/pkg/lg"
|
||||
"go.sour.is/pkg/ident"
|
||||
)
|
||||
|
||||
type root struct{}
|
||||
|
||||
func NewHTTP() *root {
|
||||
return &root{}
|
||||
}
|
||||
|
||||
func (s *root) RegisterHTTP(mux *http.ServeMux) {
|
||||
mux.Handle("/", http.FileServer(http.Dir("./mercury/public")))
|
||||
}
|
||||
func (s *root) RegisterAPIv1(mux *http.ServeMux) {
|
||||
mux.HandleFunc("GET /mercury", s.indexV1)
|
||||
// mux.HandleFunc("/mercury/config", s.configV1)
|
||||
mux.HandleFunc("GET /mercury/config", s.configV1)
|
||||
mux.HandleFunc("POST /mercury/config", s.storeV1)
|
||||
}
|
||||
|
||||
func (s *root) configV1(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodPost {
|
||||
s.storeV1(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
ctx, span := lg.Span(r.Context())
|
||||
defer span.End()
|
||||
|
||||
var id ident.Ident = ident.FromContext(ctx)
|
||||
|
||||
if !id.Session().Active {
|
||||
span.RecordError(fmt.Errorf("NO_AUTH"))
|
||||
http.Error(w, "NO_AUTH", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
rules, err := Registry.GetRules(ctx, id)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
http.Error(w, "ERR: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
space := r.URL.Query().Get("space")
|
||||
if space == "" {
|
||||
space = "*"
|
||||
}
|
||||
|
||||
log.Print("SPC: ", space)
|
||||
ns := ParseNamespace(space)
|
||||
log.Print("PRE: ", ns)
|
||||
ns = rules.ReduceSearch(ns)
|
||||
log.Print("POST: ", ns)
|
||||
|
||||
lis, err := Registry.GetConfig(ctx, ns.String(), "", "")
|
||||
if err != nil {
|
||||
http.Error(w, "ERR: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
lis, err = Registry.accessFilter(rules, lis)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
http.Error(w, "ERR: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
sort.Sort(lis)
|
||||
var content string
|
||||
|
||||
switch httputil.NegotiateContentType(r, []string{
|
||||
"text/plain",
|
||||
"text/html",
|
||||
"application/environ",
|
||||
"application/ini",
|
||||
"application/json",
|
||||
"application/toml",
|
||||
}, "text/plain") {
|
||||
case "text/plain":
|
||||
content = lis.String()
|
||||
case "text/html":
|
||||
content = lis.HTMLString()
|
||||
case "application/environ":
|
||||
content = lis.EnvString()
|
||||
case "application/ini":
|
||||
content = lis.INIString()
|
||||
case "application/json":
|
||||
json.NewEncoder(w).Encode(lis)
|
||||
case "application/toml":
|
||||
w.WriteHeader(200)
|
||||
m := make(map[string]map[string][]string)
|
||||
for _, o := range lis {
|
||||
if _, ok := m[o.Space]; !ok {
|
||||
m[o.Space] = make(map[string][]string)
|
||||
}
|
||||
for _, v := range o.List {
|
||||
m[o.Space][v.Name] = append(m[o.Space][v.Name], v.Values...)
|
||||
}
|
||||
}
|
||||
err := toml.NewEncoder(w).Encode(m)
|
||||
if err != nil {
|
||||
// log.Error(err)
|
||||
http.Error(w, "ERR", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Fprint(w, content)
|
||||
}
|
||||
|
||||
func (s *root) storeV1(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, span := lg.Span(r.Context())
|
||||
defer span.End()
|
||||
|
||||
var id = ident.FromContext(ctx)
|
||||
|
||||
if !id.Session().Active {
|
||||
span.RecordError(fmt.Errorf("NO_AUTH"))
|
||||
http.Error(w, "NO_AUTH", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
var config SpaceMap
|
||||
var err error
|
||||
contentType, _, _ := strings.Cut(r.Header.Get("Content-Type"), ";")
|
||||
switch contentType {
|
||||
case "text/plain":
|
||||
config, err = ParseText(r.Body)
|
||||
r.Body.Close()
|
||||
case "application/x-www-form-urlencoded":
|
||||
r.ParseForm()
|
||||
config, err = ParseText(strings.NewReader(r.Form.Get("content")))
|
||||
case "multipart/form-data":
|
||||
r.ParseMultipartForm(1 << 20)
|
||||
config, err = ParseText(strings.NewReader(r.Form.Get("content")))
|
||||
default:
|
||||
http.Error(w, "PARSE_ERR", http.StatusUnsupportedMediaType)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
http.Error(w, "PARSE_ERR", 400)
|
||||
return
|
||||
}
|
||||
|
||||
{
|
||||
rules, err := Registry.GetRules(ctx, id)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
http.Error(w, "ERR: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
notify, err := Registry.GetNotify(ctx, "updated")
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
http.Error(w, "ERR", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
_ = rules
|
||||
var notifyActive = make(map[string]struct{})
|
||||
var filteredConfigs Config
|
||||
for ns, c := range config {
|
||||
if !rules.GetRoles("NS", ns).HasRole("write") {
|
||||
span.AddEvent(fmt.Sprint("SKIP", ns))
|
||||
continue
|
||||
}
|
||||
|
||||
span.AddEvent(fmt.Sprint("SAVE", ns))
|
||||
for _, n := range notify.Find(ns) {
|
||||
notifyActive[n.Name] = struct{}{}
|
||||
}
|
||||
filteredConfigs = append(filteredConfigs, c)
|
||||
}
|
||||
|
||||
err = Registry.WriteConfig(ctx, filteredConfigs)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
http.Error(w, "ERR", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
span.AddEvent(fmt.Sprint("SEND NOTIFYS ", notifyActive))
|
||||
for _, n := range notify {
|
||||
if _, ok := notifyActive[n.Name]; ok {
|
||||
err = Registry.SendNotify(ctx, n)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
http.Error(w, "ERR", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
span.AddEvent("DONE!")
|
||||
}
|
||||
|
||||
w.WriteHeader(202)
|
||||
fmt.Fprint(w, "OK")
|
||||
}
|
||||
|
||||
func (s *root) indexV1(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, span := lg.Span(r.Context())
|
||||
defer span.End()
|
||||
|
||||
var id = ident.FromContext(ctx)
|
||||
|
||||
timer := time.Now()
|
||||
defer func() { fmt.Println(time.Since(timer)) }()
|
||||
|
||||
if !id.Session().Active {
|
||||
span.RecordError(fmt.Errorf("NO_AUTH"))
|
||||
http.Error(w, "NO_AUTH", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
rules, err := Registry.GetRules(ctx, id)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
http.Error(w, "ERR: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
span.AddEvent(fmt.Sprint(rules))
|
||||
|
||||
space := r.URL.Query().Get("space")
|
||||
if space == "" {
|
||||
space = "*"
|
||||
}
|
||||
|
||||
ns := ParseNamespace(space)
|
||||
ns = rules.ReduceSearch(ns)
|
||||
span.AddEvent(ns.String())
|
||||
|
||||
lis, err := Registry.GetIndex(ctx, ns.String(), "")
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
http.Error(w, "ERR: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
sort.Sort(lis)
|
||||
|
||||
switch httputil.NegotiateContentType(r, []string{
|
||||
"text/plain",
|
||||
"application/json",
|
||||
}, "text/plain") {
|
||||
case "text/plain":
|
||||
_, err = fmt.Fprint(w, lis.StringList())
|
||||
span.RecordError(err)
|
||||
case "application/json":
|
||||
err = json.NewEncoder(w).Encode(lis)
|
||||
span.RecordError(err)
|
||||
}
|
||||
}
|
||||
116
mercury/sql/init-pg.sql
Normal file
116
mercury/sql/init-pg.sql
Normal file
@@ -0,0 +1,116 @@
|
||||
CREATE SEQUENCE IF NOT EXISTS mercury_spaces_id_seq;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS mercury_spaces
|
||||
(
|
||||
space character varying NOT NULL,
|
||||
id integer NOT NULL DEFAULT nextval('mercury_spaces_id_seq'::regclass),
|
||||
notes character varying[] NOT NULL DEFAULT '{}'::character varying[],
|
||||
tags character varying[] NOT NULL DEFAULT '{}'::character varying[],
|
||||
CONSTRAINT mercury_namespace_pk PRIMARY KEY (id)
|
||||
);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS mercury_namespace_space_uindex
|
||||
ON mercury_spaces USING btree
|
||||
(space ASC NULLS LAST);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS mercury_values
|
||||
(
|
||||
id integer NOT NULL,
|
||||
seq integer NOT NULL,
|
||||
name character varying NOT NULL,
|
||||
"values" character varying[] NOT NULL DEFAULT '{}'::character varying[],
|
||||
tags character varying[] NOT NULL DEFAULT '{}'::character varying[],
|
||||
notes character varying[] NOT NULL DEFAULT '{}'::character varying[],
|
||||
CONSTRAINT mercury_values_pk PRIMARY KEY (id, seq)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS mercury_values_name_index
|
||||
ON mercury_values USING btree
|
||||
(name ASC NULLS LAST);
|
||||
|
||||
CREATE OR REPLACE VIEW mercury_registry_vw
|
||||
AS
|
||||
SELECT
|
||||
s.id,
|
||||
v.seq,
|
||||
s.space,
|
||||
v.name,
|
||||
v."values",
|
||||
v.notes,
|
||||
v.tags
|
||||
FROM mercury_spaces s
|
||||
JOIN mercury_values v ON s.id = v.id;
|
||||
|
||||
CREATE OR REPLACE VIEW mercury_groups_vw
|
||||
AS
|
||||
SELECT DISTINCT
|
||||
unnest(vw."values") AS user_id,
|
||||
vw.name AS group_id
|
||||
FROM mercury_registry_vw vw
|
||||
WHERE vw.space::text = 'mercury.groups'::text;
|
||||
|
||||
CREATE OR REPLACE VIEW mercury_group_rules_vw
|
||||
AS
|
||||
WITH
|
||||
tt as (
|
||||
SELECT DISTINCT
|
||||
vw.name AS group_id,
|
||||
unnest(vw."values") AS rules
|
||||
FROM mercury_registry_vw vw
|
||||
WHERE vw.space::text = 'mercury.policy'::text
|
||||
)
|
||||
SELECT tt.group_id,
|
||||
split_part(tt.rules::text, ' '::text, 1) AS role,
|
||||
split_part(tt.rules::text, ' '::text, 2) AS type,
|
||||
split_part(tt.rules::text, ' '::text, 3) AS match
|
||||
FROM tt;
|
||||
|
||||
CREATE OR REPLACE VIEW mercury_user_rules_vw
|
||||
AS
|
||||
WITH
|
||||
tt as (
|
||||
SELECT DISTINCT
|
||||
vw.name AS group_id,
|
||||
unnest(vw."values") AS rules
|
||||
FROM mercury_registry_vw vw
|
||||
WHERE vw.space::text = 'mercury.policy'::text
|
||||
)
|
||||
SELECT
|
||||
g.user_id,
|
||||
split_part(tt.rules::text, ' '::text, 1) AS role,
|
||||
split_part(tt.rules::text, ' '::text, 2) AS type,
|
||||
split_part(tt.rules::text, ' '::text, 3) AS match
|
||||
FROM mercury_groups_vw g
|
||||
JOIN tt ON g.group_id::text = tt.group_id::text;
|
||||
|
||||
CREATE OR REPLACE VIEW mercury_rules_vw
|
||||
AS
|
||||
SELECT
|
||||
'U-'::text || vw.user_id::text AS id,
|
||||
vw.role,
|
||||
vw.type,
|
||||
vw.match
|
||||
FROM mercury_user_rules_vw vw
|
||||
UNION
|
||||
SELECT
|
||||
'G-'::text || vw.group_id::text AS id,
|
||||
vw.role,
|
||||
vw.type,
|
||||
vw.match
|
||||
FROM mercury_group_rules_vw vw;
|
||||
|
||||
CREATE OR REPLACE VIEW mercury_notify_vw
|
||||
AS
|
||||
WITH
|
||||
tt as (
|
||||
SELECT DISTINCT
|
||||
vw.name,
|
||||
unnest(vw."values") AS rules
|
||||
FROM mercury_registry_vw vw
|
||||
WHERE vw.space::text = 'mercury.notify'::text
|
||||
)
|
||||
SELECT
|
||||
tt.name,
|
||||
split_part(tt.rules::text, ' '::text, 1) AS match,
|
||||
split_part(tt.rules::text, ' '::text, 2) AS event,
|
||||
split_part(tt.rules::text, ' '::text, 3) AS method,
|
||||
split_part(tt.rules::text, ' '::text, 4) AS url
|
||||
FROM tt;
|
||||
118
mercury/sql/init-sql3.sql
Normal file
118
mercury/sql/init-sql3.sql
Normal file
@@ -0,0 +1,118 @@
|
||||
CREATE TABLE IF NOT EXISTS mercury_spaces
|
||||
(
|
||||
space character varying NOT NULL unique,
|
||||
id integer NOT NULL CONSTRAINT mercury_namespace_pk PRIMARY KEY autoincrement,
|
||||
notes json NOT NULL DEFAULT '[]',
|
||||
tags json NOT NULL DEFAULT '[]'
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS mercury_values
|
||||
(
|
||||
id integer NOT NULL,
|
||||
seq integer NOT NULL,
|
||||
name character varying NOT NULL,
|
||||
"values" json NOT NULL DEFAULT '[]',
|
||||
tags json NOT NULL DEFAULT '[]',
|
||||
notes json NOT NULL DEFAULT '[]',
|
||||
CONSTRAINT mercury_values_pk PRIMARY KEY (id, seq)
|
||||
);
|
||||
|
||||
drop view if exists mercury_registry_vw;
|
||||
CREATE VIEW if not exists mercury_registry_vw
|
||||
AS
|
||||
SELECT
|
||||
s.id,
|
||||
v.seq,
|
||||
s.space,
|
||||
v.name,
|
||||
v."values",
|
||||
v.notes,
|
||||
v.tags
|
||||
FROM mercury_spaces s
|
||||
JOIN mercury_values v ON s.id = v.id;
|
||||
|
||||
drop view if exists mercury_groups_vw;
|
||||
CREATE VIEW if not exists mercury_groups_vw
|
||||
AS
|
||||
SELECT DISTINCT
|
||||
j.value AS user_id,
|
||||
vw.name AS group_id
|
||||
FROM mercury_registry_vw vw, json_each(vw."values") j
|
||||
WHERE vw.space = 'mercury.groups';
|
||||
|
||||
drop view if exists mercury_group_rules_vw;
|
||||
CREATE VIEW if not exists mercury_group_rules_vw
|
||||
AS
|
||||
WITH
|
||||
tt as (
|
||||
SELECT DISTINCT
|
||||
vw.name AS group_id,
|
||||
j.value AS rules
|
||||
FROM mercury_registry_vw vw, json_each(vw."values") j
|
||||
WHERE vw.space = 'mercury.policy'
|
||||
)
|
||||
SELECT tt.group_id,
|
||||
tt.rules rule,
|
||||
'' AS role,
|
||||
'' AS type,
|
||||
''AS match
|
||||
FROM tt;
|
||||
|
||||
drop view if exists mercury_user_rules_vw;
|
||||
CREATE VIEW if not exists mercury_user_rules_vw
|
||||
AS
|
||||
WITH
|
||||
tt as (
|
||||
SELECT DISTINCT
|
||||
vw.name AS group_id,
|
||||
j.value AS rules
|
||||
FROM mercury_registry_vw vw, json_each(vw."values") j
|
||||
WHERE vw.space = 'mercury.policy'
|
||||
)
|
||||
SELECT
|
||||
g.user_id,
|
||||
tt.rules rule,
|
||||
'' AS role,
|
||||
'' AS type,
|
||||
'' AS match
|
||||
FROM mercury_groups_vw g
|
||||
JOIN tt ON g.group_id = tt.group_id;
|
||||
|
||||
drop view if exists mercury_rules_vw;
|
||||
CREATE VIEW if not exists mercury_rules_vw
|
||||
AS
|
||||
SELECT
|
||||
'U-' || vw.user_id AS id,
|
||||
vw.rule,
|
||||
vw.role,
|
||||
vw.type,
|
||||
vw.match
|
||||
FROM mercury_user_rules_vw vw
|
||||
UNION
|
||||
SELECT
|
||||
'G-' || vw.group_id AS id,
|
||||
vw.rule,
|
||||
vw.role,
|
||||
vw.type,
|
||||
vw.match
|
||||
FROM mercury_group_rules_vw vw;
|
||||
|
||||
drop view if exists mercury_notify_vw;
|
||||
CREATE VIEW if not exists mercury_notify_vw
|
||||
AS
|
||||
WITH
|
||||
tt as (
|
||||
SELECT DISTINCT
|
||||
vw.name,
|
||||
j.value AS rules
|
||||
FROM mercury_registry_vw vw, json_each(vw."values") j
|
||||
WHERE vw.space = 'mercury.notify'
|
||||
)
|
||||
SELECT
|
||||
tt.name,
|
||||
tt.rules rule,
|
||||
substr(tt.rules, 1, instr(tt.rules, ' ')-1) AS match,
|
||||
substr(tt.rules, instr(tt.rules, ' ')+1, instr(substr(tt.rules, instr(tt.rules, ' ')+1), ' ')-1) AS event,
|
||||
'' AS method,
|
||||
'' as url
|
||||
FROM tt;
|
||||
130
mercury/sql/list-string.go
Normal file
130
mercury/sql/list-string.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package sql
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
type valueFn func() (v driver.Value, err error)
|
||||
|
||||
func (fn valueFn) Value() (v driver.Value, err error) {
|
||||
return fn()
|
||||
}
|
||||
|
||||
type scanFn func(value any) error
|
||||
|
||||
func (fn scanFn) Scan(v any) error {
|
||||
return fn(v)
|
||||
}
|
||||
|
||||
func listScan(e *[]string, ends [2]rune) scanFn {
|
||||
return func(value any) error {
|
||||
var str string
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
str = v
|
||||
case []byte:
|
||||
str = string(v)
|
||||
case []rune:
|
||||
str = string(v)
|
||||
default:
|
||||
return fmt.Errorf("array must be uint64, got: %T", value)
|
||||
}
|
||||
|
||||
if e == nil {
|
||||
*e = []string{}
|
||||
}
|
||||
|
||||
str = trim(str, ends[0], ends[1])
|
||||
if len(str) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, s := range splitComma(string(str)) {
|
||||
*e = append(*e, s)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func listValue(e []string, ends [2]rune) valueFn {
|
||||
return func() (value driver.Value, err error) {
|
||||
var b strings.Builder
|
||||
|
||||
if len(e) == 0 {
|
||||
return string(ends[:]), nil
|
||||
}
|
||||
|
||||
_, err = b.WriteRune(ends[0])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var arr []string
|
||||
for _, s := range e {
|
||||
arr = append(arr, `"`+s+`"`)
|
||||
}
|
||||
_, err = b.WriteString(strings.Join(arr, ","))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = b.WriteRune(ends[1])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return b.String(), nil
|
||||
}
|
||||
}
|
||||
|
||||
func splitComma(s string) []string {
|
||||
lastQuote := rune(0)
|
||||
f := func(c rune) bool {
|
||||
switch {
|
||||
case c == lastQuote:
|
||||
lastQuote = rune(0)
|
||||
return false
|
||||
case lastQuote != rune(0):
|
||||
return false
|
||||
case unicode.In(c, unicode.Quotation_Mark):
|
||||
lastQuote = c
|
||||
return false
|
||||
default:
|
||||
return c == ','
|
||||
}
|
||||
}
|
||||
lis := strings.FieldsFunc(s, f)
|
||||
|
||||
var out []string
|
||||
for _, s := range lis {
|
||||
s = trim(s, '"', '"')
|
||||
out = append(out, s)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func trim(s string, start, end rune) string {
|
||||
r0, size0 := utf8.DecodeRuneInString(s)
|
||||
if size0 == 0 {
|
||||
return s
|
||||
}
|
||||
if r0 != start {
|
||||
return s
|
||||
}
|
||||
|
||||
r1, size1 := utf8.DecodeLastRuneInString(s)
|
||||
if size1 == 0 {
|
||||
return s
|
||||
}
|
||||
if r1 != end {
|
||||
return s
|
||||
}
|
||||
|
||||
return s[size0 : len(s)-size1]
|
||||
}
|
||||
55
mercury/sql/notify.go
Normal file
55
mercury/sql/notify.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package sql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/Masterminds/squirrel"
|
||||
|
||||
"go.sour.is/pkg/lg"
|
||||
"go.sour.is/pkg/mercury"
|
||||
)
|
||||
|
||||
// Notify stores the attributes for a registry space
|
||||
type Notify struct {
|
||||
Name string `json:"name" view:"mercury_notify_vw"`
|
||||
Match string `json:"match"`
|
||||
Event string `json:"event"`
|
||||
Method string `json:"-" db:"method"`
|
||||
URL string `json:"-" db:"url"`
|
||||
}
|
||||
|
||||
// GetNotify get list of rules
|
||||
func (pgm *sqlHandler) GetNotify(ctx context.Context, event string) (lis mercury.ListNotify, err error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
rows, err := squirrel.Select(`"name"`, `"match"`, `"event"`, `"method"`, `"url"`, `"rule"`).
|
||||
From("mercury_notify_vw").
|
||||
Where(squirrel.Eq{"event": event}).
|
||||
PlaceholderFormat(squirrel.Dollar).
|
||||
RunWith(pgm.db).
|
||||
QueryContext(context.TODO())
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var s mercury.Notify
|
||||
var rule string
|
||||
err = rows.Scan(&s.Name, &s.Match, &s.Event, &s.Method, &s.URL, &rule)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if rule != "" {
|
||||
s.Match, rule, _ = strings.Cut(rule, " ")
|
||||
s.Event, rule, _ = strings.Cut(rule, " ")
|
||||
s.Method, s.URL, _ = strings.Cut(rule, " ")
|
||||
}
|
||||
lis = append(lis, s)
|
||||
}
|
||||
|
||||
return lis, rows.Err()
|
||||
}
|
||||
40
mercury/sql/otel.go
Normal file
40
mercury/sql/otel.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package sql
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"go.nhat.io/otelsql"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.20.0"
|
||||
)
|
||||
|
||||
func openDB(driver, dsn string) (*sql.DB, error) {
|
||||
system := semconv.DBSystemPostgreSQL
|
||||
if driver == "sqlite" {
|
||||
system = semconv.DBSystemSqlite
|
||||
}
|
||||
|
||||
// Register the otelsql wrapper for the provided postgres driver.
|
||||
driverName, err := otelsql.Register(driver,
|
||||
otelsql.AllowRoot(),
|
||||
otelsql.TraceQueryWithoutArgs(),
|
||||
otelsql.TraceRowsClose(),
|
||||
otelsql.TraceRowsAffected(),
|
||||
// otelsql.WithDatabaseName("my_database"), // Optional.
|
||||
otelsql.WithSystem(system), // Optional.
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Connect to a Postgres database using the postgres driver wrapper.
|
||||
db, err := sql.Open(driverName, dsn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := otelsql.RecordStats(db); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
99
mercury/sql/rules.go
Normal file
99
mercury/sql/rules.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package sql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/Masterminds/squirrel"
|
||||
"go.sour.is/pkg/lg"
|
||||
"go.sour.is/pkg/mercury"
|
||||
"go.sour.is/pkg/ident"
|
||||
)
|
||||
|
||||
type grouper interface {
|
||||
GetGroups() []string
|
||||
}
|
||||
|
||||
// GetRules get list of rules
|
||||
func (p *sqlHandler) GetRules(ctx context.Context, user ident.Ident) (lis mercury.Rules, err error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
var ids []string
|
||||
ids = append(ids, "U-"+user.Identity())
|
||||
switch u := user.(type) {
|
||||
case grouper:
|
||||
for _, g := range u.GetGroups() {
|
||||
ids = append(ids, "G-"+g)
|
||||
}
|
||||
}
|
||||
if groups, err := p.getGroups(ctx, user.Identity()); err != nil {
|
||||
for _, g := range groups {
|
||||
ids = append(ids, "G-"+g)
|
||||
}
|
||||
}
|
||||
|
||||
query := squirrel.Select(`"role"`, `"type"`, `"match"`, `"rule"`).
|
||||
From("mercury_rules_vw").
|
||||
Where(squirrel.Eq{"id": ids}).
|
||||
PlaceholderFormat(squirrel.Dollar)
|
||||
rows, err := query.
|
||||
RunWith(p.db).
|
||||
QueryContext(ctx)
|
||||
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var s mercury.Rule
|
||||
var rule string
|
||||
err = rows.Scan(&s.Role, &s.Type, &s.Match, &rule)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return nil, err
|
||||
}
|
||||
if rule != "" {
|
||||
s.Role, rule, _ = strings.Cut(rule, " ")
|
||||
s.Type, s.Match, _ = strings.Cut(rule, " ")
|
||||
}
|
||||
lis = append(lis, s)
|
||||
}
|
||||
err = rows.Err()
|
||||
span.RecordError(err)
|
||||
|
||||
span.AddEvent(fmt.Sprint("read rules ", len(lis)))
|
||||
return lis, err
|
||||
}
|
||||
|
||||
// getGroups get list of groups
|
||||
func (pgm *sqlHandler) getGroups(ctx context.Context, user string) (lis []string, err error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
rows, err := squirrel.Select("group_id").
|
||||
From("mercury_groups_vw").
|
||||
Where(squirrel.Eq{"user_id": user}).
|
||||
PlaceholderFormat(squirrel.Dollar).
|
||||
RunWith(pgm.db).
|
||||
QueryContext(ctx)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var s string
|
||||
err = rows.Scan(&s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lis = append(lis, s)
|
||||
}
|
||||
|
||||
return lis, rows.Err()
|
||||
}
|
||||
419
mercury/sql/sql.go
Normal file
419
mercury/sql/sql.go
Normal file
@@ -0,0 +1,419 @@
|
||||
package sql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
"go.sour.is/pkg/lg"
|
||||
"go.sour.is/pkg/mercury"
|
||||
"go.sour.is/pkg/rsql"
|
||||
"golang.org/x/exp/maps"
|
||||
)
|
||||
|
||||
type sqlHandler struct {
|
||||
db *sql.DB
|
||||
paceholderFormat sq.PlaceholderFormat
|
||||
listFormat [2]rune
|
||||
}
|
||||
|
||||
var (
|
||||
_ mercury.GetIndex = (*sqlHandler)(nil)
|
||||
_ mercury.GetConfig = (*sqlHandler)(nil)
|
||||
_ mercury.GetRules = (*sqlHandler)(nil)
|
||||
_ mercury.WriteConfig = (*sqlHandler)(nil)
|
||||
)
|
||||
|
||||
func Register() {
|
||||
mercury.Registry.Register("sql", func(s *mercury.Space) any {
|
||||
var dsn string
|
||||
var opts strings.Builder
|
||||
var dbtype string
|
||||
for _, c := range s.List {
|
||||
if c.Name == "match" {
|
||||
continue
|
||||
}
|
||||
if c.Name == "dbtype" {
|
||||
dbtype = c.First()
|
||||
continue
|
||||
}
|
||||
if c.Name == "dsn" {
|
||||
dsn = c.First()
|
||||
break
|
||||
}
|
||||
fmt.Fprintln(&opts, c.Name, "=", c.First())
|
||||
}
|
||||
if dsn == "" {
|
||||
dsn = opts.String()
|
||||
}
|
||||
|
||||
db, err := openDB(dbtype, dsn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = db.Ping(); err != nil {
|
||||
return err
|
||||
}
|
||||
switch dbtype {
|
||||
case "sqlite":
|
||||
return &sqlHandler{db, sq.Dollar, [2]rune{'[', ']'}}
|
||||
case "postgres":
|
||||
return &sqlHandler{db, sq.Dollar, [2]rune{'{', '}'}}
|
||||
default:
|
||||
return fmt.Errorf("unsupported dbtype: %s", dbtype)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type Space struct {
|
||||
mercury.Space
|
||||
ID uint64
|
||||
}
|
||||
type Value struct {
|
||||
mercury.Value
|
||||
ID uint64
|
||||
}
|
||||
|
||||
func (p *sqlHandler) GetIndex(ctx context.Context, search mercury.NamespaceSearch, pgm *rsql.Program) (mercury.Config, error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
cols := rsql.GetDbColumns(mercury.Space{})
|
||||
where, err := getWhere(search, cols)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lis, err := p.listSpace(ctx, nil, where)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config := make(mercury.Config, len(lis))
|
||||
for i, s := range lis {
|
||||
config[i] = &s.Space
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func (p *sqlHandler) GetConfig(ctx context.Context, search mercury.NamespaceSearch, pgm *rsql.Program, fields []string) (mercury.Config, error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
idx, err := p.GetIndex(ctx, search, pgm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
spaceMap := make(map[string]int, len(idx))
|
||||
for u, s := range idx {
|
||||
spaceMap[s.Space] = u
|
||||
}
|
||||
|
||||
where, err := getWhere(search, rsql.GetDbColumns(mercury.Value{}))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
query := sq.Select(`"space"`, `"name"`, `"seq"`, `"notes"`, `"tags"`, `"values"`).
|
||||
From("mercury_registry_vw").
|
||||
Where(where).
|
||||
OrderBy("space asc", "name asc").
|
||||
PlaceholderFormat(p.paceholderFormat)
|
||||
span.AddEvent(lg.LogQuery(query.ToSql()))
|
||||
rows, err := query.RunWith(p.db).
|
||||
QueryContext(ctx)
|
||||
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var s mercury.Value
|
||||
|
||||
err = rows.Scan(
|
||||
&s.Space,
|
||||
&s.Name,
|
||||
&s.Seq,
|
||||
listScan(&s.Notes, p.listFormat),
|
||||
listScan(&s.Tags, p.listFormat),
|
||||
listScan(&s.Values, p.listFormat),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if u, ok := spaceMap[s.Space]; ok {
|
||||
idx[u].List = append(idx[u].List, s)
|
||||
}
|
||||
}
|
||||
|
||||
err = rows.Err()
|
||||
span.RecordError(err)
|
||||
|
||||
span.AddEvent(fmt.Sprint("read index ", len(idx)))
|
||||
return idx, err
|
||||
}
|
||||
|
||||
func (p *sqlHandler) listSpace(ctx context.Context, tx sq.BaseRunner, where sq.Sqlizer) ([]*Space, error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
if tx == nil {
|
||||
tx = p.db
|
||||
}
|
||||
|
||||
query := sq.Select(`"id"`, `"space"`, `"tags"`, `"notes"`).
|
||||
From("mercury_spaces").
|
||||
Where(where).
|
||||
OrderBy("space asc").
|
||||
PlaceholderFormat(sq.Dollar)
|
||||
span.AddEvent(lg.LogQuery(query.ToSql()))
|
||||
rows, err := query.RunWith(tx).
|
||||
QueryContext(ctx)
|
||||
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var lis []*Space
|
||||
for rows.Next() {
|
||||
var s Space
|
||||
err = rows.Scan(
|
||||
&s.ID,
|
||||
&s.Space.Space,
|
||||
listScan(&s.Space.Notes, p.listFormat),
|
||||
listScan(&s.Space.Tags, p.listFormat),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lis = append(lis, &s)
|
||||
}
|
||||
|
||||
err = rows.Err()
|
||||
span.RecordError(err)
|
||||
|
||||
span.AddEvent(fmt.Sprint("read config ", len(lis)))
|
||||
return lis, err
|
||||
}
|
||||
|
||||
// WriteConfig writes a config map to database
|
||||
func (p *sqlHandler) WriteConfig(ctx context.Context, config mercury.Config) (err error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
// Delete spaces that are present in input but are empty.
|
||||
deleteSpaces := make(map[string]struct{})
|
||||
|
||||
// get names of each space
|
||||
var names = make(map[string]int)
|
||||
for i, v := range config {
|
||||
names[v.Space] = i
|
||||
|
||||
if len(v.Tags) == 0 && len(v.Notes) == 0 && len(v.List) == 0 {
|
||||
deleteSpaces[v.Space] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
tx, err := p.db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err != nil && tx != nil {
|
||||
tx.Rollback()
|
||||
}
|
||||
}()
|
||||
|
||||
// get current spaces
|
||||
lis, err := p.listSpace(ctx, tx, sq.Eq{"space": maps.Keys(names)})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// determine which are being updated
|
||||
var deleteIDs []uint64
|
||||
var updateIDs []uint64
|
||||
var currentNames = make(map[string]struct{}, len(lis))
|
||||
var updateSpaces []*mercury.Space
|
||||
var insertSpaces []*mercury.Space
|
||||
|
||||
for _, s := range lis {
|
||||
spaceName := s.Space.Space
|
||||
currentNames[spaceName] = struct{}{}
|
||||
|
||||
if _, ok := deleteSpaces[spaceName]; ok {
|
||||
deleteIDs = append(deleteIDs, s.ID)
|
||||
continue
|
||||
}
|
||||
|
||||
updateSpaces = append(updateSpaces, config[names[spaceName]])
|
||||
updateIDs = append(updateIDs, s.ID)
|
||||
}
|
||||
for _, s := range config {
|
||||
spaceName := s.Space
|
||||
if _, ok := currentNames[spaceName]; !ok {
|
||||
insertSpaces = append(insertSpaces, s)
|
||||
}
|
||||
}
|
||||
|
||||
// delete spaces
|
||||
if ids := deleteIDs; len(ids) > 0 {
|
||||
_, err = sq.Delete("mercury_spaces").Where(sq.Eq{"id": ids}).RunWith(tx).PlaceholderFormat(sq.Dollar).ExecContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// delete values
|
||||
if ids := append(updateIDs, deleteIDs...); len(ids) > 0 {
|
||||
_, err = sq.Delete("mercury_values").Where(sq.Eq{"id": ids}).RunWith(tx).PlaceholderFormat(sq.Dollar).ExecContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var newValues []*Value
|
||||
|
||||
// update spaces
|
||||
for i, u := range updateSpaces {
|
||||
query := sq.Update("mercury_spaces").
|
||||
Where(sq.Eq{"id": updateIDs[i]}).
|
||||
Set("tags", listValue(u.Tags, p.listFormat)).
|
||||
Set("notes", listValue(u.Notes, p.listFormat)).
|
||||
PlaceholderFormat(sq.Dollar)
|
||||
span.AddEvent(lg.LogQuery(query.ToSql()))
|
||||
_, err := query.RunWith(tx).ExecContext(ctx)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// log.Debugf("UPDATED %d SPACES", len(updateSpaces))
|
||||
for _, v := range u.List {
|
||||
newValues = append(newValues, &Value{Value: v, ID: updateIDs[i]})
|
||||
}
|
||||
}
|
||||
|
||||
// insert spaces
|
||||
for _, s := range insertSpaces {
|
||||
var id uint64
|
||||
query := sq.Insert("mercury_spaces").
|
||||
PlaceholderFormat(sq.Dollar).
|
||||
Columns("space", "tags", "notes").
|
||||
Values(s.Space, listValue(s.Tags, p.listFormat), listValue(s.Notes, p.listFormat)).
|
||||
Suffix("RETURNING \"id\"")
|
||||
span.AddEvent(lg.LogQuery(query.ToSql()))
|
||||
|
||||
err := query.
|
||||
RunWith(tx).
|
||||
QueryRowContext(ctx).
|
||||
Scan(&id)
|
||||
if err != nil {
|
||||
s, v, _ := query.ToSql()
|
||||
log.Println(s, v, err)
|
||||
return err
|
||||
}
|
||||
for _, v := range s.List {
|
||||
newValues = append(newValues, &Value{Value: v, ID: id})
|
||||
}
|
||||
}
|
||||
|
||||
// write all values to db.
|
||||
err = p.writeValues(ctx, tx, newValues)
|
||||
// log.Debugf("WROTE %d ATTRS", len(attrs))
|
||||
|
||||
tx.Commit()
|
||||
tx = nil
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// writeValues writes the values to db
|
||||
func (p *sqlHandler) writeValues(ctx context.Context, tx sq.BaseRunner, lis []*Value) (err error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
if len(lis) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
newInsert := func() sq.InsertBuilder {
|
||||
return sq.Insert("mercury_values").
|
||||
RunWith(tx).
|
||||
PlaceholderFormat(sq.Dollar).
|
||||
Columns(
|
||||
`"id"`,
|
||||
`"seq"`,
|
||||
`"name"`,
|
||||
`"values"`,
|
||||
`"notes"`,
|
||||
`"tags"`,
|
||||
)
|
||||
}
|
||||
chunk := int(65000 / 3)
|
||||
insert := newInsert()
|
||||
for i, s := range lis {
|
||||
insert = insert.Values(
|
||||
s.ID,
|
||||
s.Seq,
|
||||
s.Name,
|
||||
listValue(s.Values, p.listFormat),
|
||||
listValue(s.Notes, p.listFormat),
|
||||
listValue(s.Tags, p.listFormat),
|
||||
)
|
||||
// log.Debug(s.Name)
|
||||
|
||||
if i > 0 && i%chunk == 0 {
|
||||
// log.Debugf("inserting %v rows into %v", i%chunk, d.Table)
|
||||
// log.Debug(insert.ToSql())
|
||||
span.AddEvent(lg.LogQuery(insert.ToSql()))
|
||||
|
||||
_, err = insert.ExecContext(ctx)
|
||||
if err != nil {
|
||||
// log.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
insert = newInsert()
|
||||
}
|
||||
}
|
||||
if len(lis)%chunk > 0 {
|
||||
// log.Debugf("inserting %v rows into %v", len(lis)%chunk, d.Table)
|
||||
// log.Debug(insert.ToSql())
|
||||
span.AddEvent(lg.LogQuery(insert.ToSql()))
|
||||
|
||||
_, err = insert.ExecContext(ctx)
|
||||
if err != nil {
|
||||
// log.Error(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func getWhere(search mercury.NamespaceSearch, d *rsql.DbColumns) (sq.Sqlizer, error) {
|
||||
var where sq.Or
|
||||
space, err := d.Col("space")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, m := range search {
|
||||
switch m.(type) {
|
||||
case mercury.NamespaceNode:
|
||||
where = append(where, sq.Eq{space: m.Value()})
|
||||
case mercury.NamespaceStar:
|
||||
where = append(where, sq.Like{space: m.Value()})
|
||||
case mercury.NamespaceTrace:
|
||||
e := sq.Expr(`? LIKE `+space+` || '%'`, m.Value())
|
||||
where = append(where, e)
|
||||
}
|
||||
}
|
||||
return where, nil
|
||||
}
|
||||
Reference in New Issue
Block a user