add resource event handler types
This commit is contained in:
parent
d88b67ea2f
commit
a73acb8b93
@ -197,7 +197,7 @@ attributes:
|
|||||||
gecos: "foo user"
|
gecos: "foo user"
|
||||||
`)
|
`)
|
||||||
|
|
||||||
rw.Write(userdecl)
|
assert.Nil(t, rw.Write(userdecl))
|
||||||
}))
|
}))
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
|
31
internal/folio/events.go
Normal file
31
internal/folio/events.go
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||||
|
|
||||||
|
package folio
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
_ "gitea.rosskeen.house/pylon/luaruntime"
|
||||||
|
_ "fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EventHandler string
|
||||||
|
|
||||||
|
type Events map[EventType]EventHandler
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidHandler error = errors.New("Invalid event handler")
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewEvents() *Events {
|
||||||
|
e := make(Events)
|
||||||
|
return &e
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Events) Set(t EventType, h EventHandler) (err error) {
|
||||||
|
if err = t.Validate(); err == nil {
|
||||||
|
(*e)[t] = h
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
|
22
internal/folio/events_test.go
Normal file
22
internal/folio/events_test.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||||
|
|
||||||
|
|
||||||
|
package folio
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func TestNewEvent(t *testing.T) {
|
||||||
|
var et EventType
|
||||||
|
events := NewEvents()
|
||||||
|
assert.NotNil(t, events)
|
||||||
|
|
||||||
|
et.Set(EventTypeLoad)
|
||||||
|
assert.Nil(t, events.Set(et, EventHandler(`
|
||||||
|
print('hello world')
|
||||||
|
`)))
|
||||||
|
|
||||||
|
}
|
39
internal/folio/eventtype.go
Normal file
39
internal/folio/eventtype.go
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||||
|
|
||||||
|
package folio
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EventType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
EventTypeLoad EventType = "load"
|
||||||
|
EventTypeCreate EventType = "create"
|
||||||
|
EventTypeRead EventType = "read"
|
||||||
|
EventTypeUpdate EventType = "update"
|
||||||
|
EventTypeDelete EventType = "delete"
|
||||||
|
EventTypeError EventType = "error"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrUnknownEventType error = errors.New("Unknown EventType")
|
||||||
|
)
|
||||||
|
|
||||||
|
func (e EventType) Validate() (err error) {
|
||||||
|
switch e {
|
||||||
|
case EventTypeLoad, EventTypeCreate, EventTypeRead, EventTypeUpdate, EventTypeDelete, EventTypeError:
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("%w: %s", ErrUnknownEventType, e)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *EventType) Set(v EventType) (err error) {
|
||||||
|
if err = v.Validate(); err == nil {
|
||||||
|
(*e) = v
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
29
internal/folio/eventtype_test.go
Normal file
29
internal/folio/eventtype_test.go
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||||
|
|
||||||
|
|
||||||
|
package folio
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func TestEventType(t *testing.T) {
|
||||||
|
for _, v := range []struct{ et EventType; expected error } {
|
||||||
|
{ et: EventType("load"), expected: nil },
|
||||||
|
{ et: EventType("create"), expected: nil },
|
||||||
|
{ et: EventType("read"), expected: nil },
|
||||||
|
{ et: EventType("update"), expected: nil },
|
||||||
|
{ et: EventType("delete"), expected: nil },
|
||||||
|
{ et: EventType("error"), expected: nil },
|
||||||
|
{ et: EventType("foo"), expected: ErrUnknownEventType },
|
||||||
|
} {
|
||||||
|
assert.ErrorIs(t, v.et.Validate(), v.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEventTypeSet(t *testing.T) {
|
||||||
|
var et EventType
|
||||||
|
assert.Nil(t, et.Set(EventTypeLoad))
|
||||||
|
}
|
@ -1,129 +0,0 @@
|
|||||||
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
|
||||||
|
|
||||||
package resource
|
|
||||||
|
|
||||||
import (
|
|
||||||
_ "context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
"io"
|
|
||||||
"log/slog"
|
|
||||||
_ "net/url"
|
|
||||||
_ "os"
|
|
||||||
"os/exec"
|
|
||||||
"strings"
|
|
||||||
"text/template"
|
|
||||||
"decl/internal/codec"
|
|
||||||
)
|
|
||||||
|
|
||||||
type CommandExecutor func(value any) ([]byte, error)
|
|
||||||
type CommandExtractAttributes func(output []byte, target any) error
|
|
||||||
|
|
||||||
type CommandArg string
|
|
||||||
|
|
||||||
type Command struct {
|
|
||||||
Path string `json:"path" yaml:"path"`
|
|
||||||
Args []CommandArg `json:"args" yaml:"args"`
|
|
||||||
Split bool `json:"split" yaml:"split"`
|
|
||||||
Executor CommandExecutor `json:"-" yaml:"-"`
|
|
||||||
Extractor CommandExtractAttributes `json:"-" yaml:"-"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewCommand() *Command {
|
|
||||||
c := &Command{ Split: true }
|
|
||||||
c.Executor = func(value any) ([]byte, error) {
|
|
||||||
args, err := c.Template(value)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
cmd := exec.Command(c.Path, args...)
|
|
||||||
|
|
||||||
slog.Info("execute() - cmd", "path", c.Path, "args", args)
|
|
||||||
output, stdoutPipeErr := cmd.StdoutPipe()
|
|
||||||
if stdoutPipeErr != nil {
|
|
||||||
return nil, stdoutPipeErr
|
|
||||||
}
|
|
||||||
|
|
||||||
stderr, pipeErr := cmd.StderrPipe()
|
|
||||||
if pipeErr != nil {
|
|
||||||
return nil, pipeErr
|
|
||||||
}
|
|
||||||
|
|
||||||
if startErr := cmd.Start(); startErr != nil {
|
|
||||||
return nil, startErr
|
|
||||||
}
|
|
||||||
|
|
||||||
slog.Info("execute() - start", "cmd", cmd)
|
|
||||||
stdOutOutput, _ := io.ReadAll(output)
|
|
||||||
stdErrOutput, _ := io.ReadAll(stderr)
|
|
||||||
|
|
||||||
slog.Info("execute() - io", "stdout", string(stdOutOutput), "stderr", string(stdErrOutput))
|
|
||||||
waitErr := cmd.Wait()
|
|
||||||
|
|
||||||
slog.Info("execute()", "path", c.Path, "args", args, "output", string(stdOutOutput), "error", string(stdErrOutput))
|
|
||||||
|
|
||||||
if len(stdErrOutput) > 0 {
|
|
||||||
return stdOutOutput, fmt.Errorf(string(stdErrOutput))
|
|
||||||
}
|
|
||||||
return stdOutOutput, waitErr
|
|
||||||
}
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Command) Load(r io.Reader) error {
|
|
||||||
return codec.NewYAMLDecoder(r).Decode(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Command) LoadDecl(yamlResourceDeclaration string) error {
|
|
||||||
return codec.NewYAMLStringDecoder(yamlResourceDeclaration).Decode(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Command) Template(value any) ([]string, error) {
|
|
||||||
var args []string = make([]string, 0, len(c.Args) * 2)
|
|
||||||
for i, arg := range c.Args {
|
|
||||||
var commandLineArg strings.Builder
|
|
||||||
err := template.Must(template.New(fmt.Sprintf("arg%d", i)).Parse(string(arg))).Execute(&commandLineArg, value)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if commandLineArg.Len() > 0 {
|
|
||||||
var splitArg []string
|
|
||||||
if c.Split {
|
|
||||||
splitArg = strings.Split(commandLineArg.String(), " ")
|
|
||||||
} else {
|
|
||||||
splitArg = []string{commandLineArg.String()}
|
|
||||||
}
|
|
||||||
slog.Info("Template()", "split", splitArg, "len", len(splitArg))
|
|
||||||
args = append(args, splitArg...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
slog.Info("Template()", "Args", c.Args, "lencargs", len(c.Args), "args", args, "lenargs", len(args), "value", value)
|
|
||||||
return args, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Command) Execute(value any) ([]byte, error) {
|
|
||||||
return c.Executor(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *CommandArg) UnmarshalValue(value string) error {
|
|
||||||
*c = CommandArg(value)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *CommandArg) UnmarshalJSON(data []byte) error {
|
|
||||||
var s string
|
|
||||||
if unmarshalRouteTypeErr := json.Unmarshal(data, &s); unmarshalRouteTypeErr != nil {
|
|
||||||
return unmarshalRouteTypeErr
|
|
||||||
}
|
|
||||||
return c.UnmarshalValue(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *CommandArg) UnmarshalYAML(value *yaml.Node) error {
|
|
||||||
var s string
|
|
||||||
if err := value.Decode(&s); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return c.UnmarshalValue(s)
|
|
||||||
}
|
|
@ -1,58 +0,0 @@
|
|||||||
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
|
||||||
|
|
||||||
|
|
||||||
package resource
|
|
||||||
|
|
||||||
import (
|
|
||||||
_ "fmt"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
_ "os"
|
|
||||||
_ "strings"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestNewCommand(t *testing.T) {
|
|
||||||
c := NewCommand()
|
|
||||||
assert.NotNil(t, c)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCommandLoad(t *testing.T) {
|
|
||||||
c := NewCommand()
|
|
||||||
assert.NotNil(t, c)
|
|
||||||
|
|
||||||
decl := `
|
|
||||||
path: find
|
|
||||||
args:
|
|
||||||
- "{{ .Path }}"
|
|
||||||
`
|
|
||||||
|
|
||||||
assert.Nil(t, c.LoadDecl(decl))
|
|
||||||
assert.Equal(t, "find", c.Path)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCommandTemplate(t *testing.T) {
|
|
||||||
c := NewCommand()
|
|
||||||
assert.NotNil(t, c)
|
|
||||||
|
|
||||||
decl := `
|
|
||||||
path: find
|
|
||||||
args:
|
|
||||||
- "{{ .Path }}"
|
|
||||||
`
|
|
||||||
|
|
||||||
assert.Nil(t, c.LoadDecl(decl))
|
|
||||||
assert.Equal(t, "find", c.Path)
|
|
||||||
assert.Equal(t, 1, len(c.Args))
|
|
||||||
|
|
||||||
f := NewFile()
|
|
||||||
f.Path = "./"
|
|
||||||
args, templateErr := c.Template(f)
|
|
||||||
assert.Nil(t, templateErr)
|
|
||||||
assert.Equal(t, 1, len(args))
|
|
||||||
|
|
||||||
assert.Equal(t, "./", string(args[0]))
|
|
||||||
|
|
||||||
out, err := c.Execute(f)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
assert.Greater(t, len(out), 0)
|
|
||||||
}
|
|
@ -146,7 +146,6 @@ type Iptable struct {
|
|||||||
UpdateCommand *command.Command `yaml:"-" json:"-"`
|
UpdateCommand *command.Command `yaml:"-" json:"-"`
|
||||||
DeleteCommand *command.Command `yaml:"-" json:"-"`
|
DeleteCommand *command.Command `yaml:"-" json:"-"`
|
||||||
|
|
||||||
config data.ConfigurationValueGetter
|
|
||||||
Resources data.ResourceMapper `yaml:"-" json:"-"`
|
Resources data.ResourceMapper `yaml:"-" json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -287,7 +286,7 @@ func (i *Iptable) URI() string {
|
|||||||
|
|
||||||
func (i *Iptable) SetParsedURI(uri *url.URL) (err error) {
|
func (i *Iptable) SetParsedURI(uri *url.URL) (err error) {
|
||||||
if err = i.Common.SetParsedURI(uri); err == nil {
|
if err = i.Common.SetParsedURI(uri); err == nil {
|
||||||
i.setFieldsFromPath()
|
err = i.setFieldsFromPath()
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,7 @@ _ "strconv"
|
|||||||
"decl/internal/codec"
|
"decl/internal/codec"
|
||||||
"decl/internal/data"
|
"decl/internal/data"
|
||||||
"decl/internal/folio"
|
"decl/internal/folio"
|
||||||
|
"decl/internal/command"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -126,10 +127,10 @@ type NetworkRoute struct {
|
|||||||
Scope NetworkRouteScope `json:"scope" yaml:"scope"`
|
Scope NetworkRouteScope `json:"scope" yaml:"scope"`
|
||||||
Proto NetworkRouteProto `json:"proto" yaml:"proto"`
|
Proto NetworkRouteProto `json:"proto" yaml:"proto"`
|
||||||
|
|
||||||
CreateCommand *Command `yaml:"-" json:"-"`
|
CreateCommand *command.Command `yaml:"-" json:"-"`
|
||||||
ReadCommand *Command `yaml:"-" json:"-"`
|
ReadCommand *command.Command `yaml:"-" json:"-"`
|
||||||
UpdateCommand *Command `yaml:"-" json:"-"`
|
UpdateCommand *command.Command `yaml:"-" json:"-"`
|
||||||
DeleteCommand *Command `yaml:"-" json:"-"`
|
DeleteCommand *command.Command `yaml:"-" json:"-"`
|
||||||
|
|
||||||
config data.ConfigurationValueGetter
|
config data.ConfigurationValueGetter
|
||||||
Resources data.ResourceMapper `json:"-" yaml:"-"`
|
Resources data.ResourceMapper `json:"-" yaml:"-"`
|
||||||
@ -478,41 +479,41 @@ func (n *NetworkRoute) UnmarshalJSON(data []byte) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func (n *NetworkRoute) NewCRUD() (create *Command, read *Command, update *Command, del *Command) {
|
func (n *NetworkRoute) NewCRUD() (create *command.Command, read *command.Command, update *command.Command, del *command.Command) {
|
||||||
return NewNetworkRouteCreateCommand(), NewNetworkRouteReadCommand(), NewNetworkRouteUpdateCommand(), NewNetworkRouteDeleteCommand()
|
return NewNetworkRouteCreateCommand(), NewNetworkRouteReadCommand(), NewNetworkRouteUpdateCommand(), NewNetworkRouteDeleteCommand()
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewNetworkRouteCreateCommand() *Command {
|
func NewNetworkRouteCreateCommand() *command.Command {
|
||||||
c := NewCommand()
|
c := command.NewCommand()
|
||||||
c.Path = "ip"
|
c.Path = "ip"
|
||||||
c.Args = []CommandArg{
|
c.Args = []command.CommandArg{
|
||||||
CommandArg("route"),
|
command.CommandArg("route"),
|
||||||
CommandArg("add"),
|
command.CommandArg("add"),
|
||||||
CommandArg("{{ if .To }}to {{ .To }}{{ end }}"),
|
command.CommandArg("{{ if .To }}to {{ .To }}{{ end }}"),
|
||||||
CommandArg("{{ if .Rtid }}table {{ .Rtid }}{{ end }}"),
|
command.CommandArg("{{ if .Rtid }}table {{ .Rtid }}{{ end }}"),
|
||||||
CommandArg("{{ if .Gateway }}via {{ .Gateway }}{{ end }}"),
|
command.CommandArg("{{ if .Gateway }}via {{ .Gateway }}{{ end }}"),
|
||||||
CommandArg("{{ if .Proto }}protocol {{ .Proto }}{{ end }}"),
|
command.CommandArg("{{ if .Proto }}protocol {{ .Proto }}{{ end }}"),
|
||||||
CommandArg("{{ if .Scope }}scope {{ .Scope }}{{ end }}"),
|
command.CommandArg("{{ if .Scope }}scope {{ .Scope }}{{ end }}"),
|
||||||
CommandArg("{{ if .RouteType }}type {{ .RouteType }}{{ end }}"),
|
command.CommandArg("{{ if .RouteType }}type {{ .RouteType }}{{ end }}"),
|
||||||
CommandArg("{{ if .Interface }}dev {{ .Interface }}{{ end }}"),
|
command.CommandArg("{{ if .Interface }}dev {{ .Interface }}{{ end }}"),
|
||||||
CommandArg("{{ if .Metric }}metric {{ .Metric }}{{ end }}"),
|
command.CommandArg("{{ if .Metric }}metric {{ .Metric }}{{ end }}"),
|
||||||
}
|
}
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewNetworkRouteReadCommand() *Command {
|
func NewNetworkRouteReadCommand() *command.Command {
|
||||||
c := NewCommand()
|
c := command.NewCommand()
|
||||||
c.Path = "ip"
|
c.Path = "ip"
|
||||||
c.Args = []CommandArg{
|
c.Args = []command.CommandArg{
|
||||||
CommandArg("route"),
|
command.CommandArg("route"),
|
||||||
CommandArg("show"),
|
command.CommandArg("show"),
|
||||||
CommandArg("{{ if .To }}to {{ .To }}{{ end }}"),
|
command.CommandArg("{{ if .To }}to {{ .To }}{{ end }}"),
|
||||||
CommandArg("{{ if .Rtid }}table {{ .Rtid }}{{ end }}"),
|
command.CommandArg("{{ if .Rtid }}table {{ .Rtid }}{{ end }}"),
|
||||||
CommandArg("{{ if .Gateway }}via {{ .Gateway }}{{ end }}"),
|
command.CommandArg("{{ if .Gateway }}via {{ .Gateway }}{{ end }}"),
|
||||||
CommandArg("{{ if .Proto }}protocol {{ .Proto }}{{ end }}"),
|
command.CommandArg("{{ if .Proto }}protocol {{ .Proto }}{{ end }}"),
|
||||||
CommandArg("{{ if .Scope }}scope {{ .Scope }}{{ end }}"),
|
command.CommandArg("{{ if .Scope }}scope {{ .Scope }}{{ end }}"),
|
||||||
CommandArg("{{ if .RouteType }}type {{ .RouteType }}{{ end }}"),
|
command.CommandArg("{{ if .RouteType }}type {{ .RouteType }}{{ end }}"),
|
||||||
CommandArg("{{ if .Interface }}dev {{ .Interface }}{{ end }}"),
|
command.CommandArg("{{ if .Interface }}dev {{ .Interface }}{{ end }}"),
|
||||||
}
|
}
|
||||||
c.Extractor = func(out []byte, target any) error {
|
c.Extractor = func(out []byte, target any) error {
|
||||||
n := target.(*NetworkRoute)
|
n := target.(*NetworkRoute)
|
||||||
@ -535,16 +536,16 @@ func NewNetworkRouteReadCommand() *Command {
|
|||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewNetworkRouteUpdateCommand() *Command {
|
func NewNetworkRouteUpdateCommand() *command.Command {
|
||||||
c := NewCommand()
|
c := command.NewCommand()
|
||||||
c.Path = "ip"
|
c.Path = "ip"
|
||||||
c.Args = []CommandArg{
|
c.Args = []command.CommandArg{
|
||||||
CommandArg("del"),
|
command.CommandArg("del"),
|
||||||
CommandArg("{{ .Name }}"),
|
command.CommandArg("{{ .Name }}"),
|
||||||
}
|
}
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewNetworkRouteDeleteCommand() *Command {
|
func NewNetworkRouteDeleteCommand() *command.Command {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -3,15 +3,15 @@ package resource
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
_ "encoding/json"
|
_ "encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
_ "io"
|
_ "io"
|
||||||
_ "net/http"
|
_ "net/http"
|
||||||
_ "net/http/httptest"
|
_ "net/http/httptest"
|
||||||
_ "net/url"
|
_ "net/url"
|
||||||
_ "os"
|
_ "os"
|
||||||
_ "strings"
|
_ "strings"
|
||||||
"testing"
|
"testing"
|
||||||
"decl/internal/data"
|
"decl/internal/data"
|
||||||
)
|
)
|
||||||
@ -107,7 +107,7 @@ func TestUserSecondaryGroups(t *testing.T) {
|
|||||||
"root",
|
"root",
|
||||||
"wheel",
|
"wheel",
|
||||||
}
|
}
|
||||||
u.ReadGroups()
|
assert.Nil(t, u.ReadGroups())
|
||||||
|
|
||||||
for _, groupName := range u.Groups {
|
for _, groupName := range u.Groups {
|
||||||
groupCounts[groupName]++
|
groupCounts[groupName]++
|
||||||
|
Loading…
Reference in New Issue
Block a user