// Copyright 2024 Matthew Rich . All rights reserved. package resource import ( "context" "fmt" "gopkg.in/yaml.v3" _ "log/slog" "net/url" _ "os" "os/exec" "os/user" "io" "encoding/json" "errors" "strings" "gitea.rosskeen.house/rosskeen.house/machine" "decl/internal/codec" "decl/internal/command" "decl/internal/data" "decl/internal/folio" ) type decodeGroup Group type GroupType string const ( GroupTypeAddGroup = "addgroup" GroupTypeGroupAdd = "groupadd" ) var ErrUnsupportedGroupType error = errors.New("The GroupType is not supported on this system") var ErrInvalidGroupType error = errors.New("invalid GroupType value") var SystemGroupType GroupType = FindSystemGroupType() type Group struct { *Common `json:"-" yaml:"-"` stater machine.Stater `json:"-" yaml:"-"` Name string `json:"name" yaml:"name"` GID string `json:"gid,omitempty" yaml:"gid,omitempty"` GroupType GroupType `json:"-" yaml:"-"` CreateCommand *command.Command `json:"-" yaml:"-"` ReadCommand *command.Command `json:"-" yaml:"-"` UpdateCommand *command.Command `json:"-" yaml:"-"` DeleteCommand *command.Command `json:"-" yaml:"-"` State string `json:"state,omitempty" yaml:"state,omitempty"` config data.ConfigurationValueGetter Resources data.ResourceMapper `json:"-" yaml:"-"` } func NewGroup() *Group { return &Group{} } func init() { folio.DocumentRegistry.ResourceTypes.Register([]string{"group"}, func(u *url.URL) data.Resource { group := NewGroup() group.Name = u.Hostname() group.GID = LookupGIDString(u.Hostname()) if _, addGroupPathErr := exec.LookPath("addgroup"); addGroupPathErr == nil { group.GroupType = GroupTypeAddGroup } if _, pathErr := exec.LookPath("groupadd"); pathErr == nil { group.GroupType = GroupTypeGroupAdd } group.CreateCommand, group.ReadCommand, group.UpdateCommand, group.DeleteCommand = group.GroupType.NewCRUD() return group }) } func FindSystemGroupType() GroupType { for _, groupType := range []GroupType{GroupTypeAddGroup, GroupTypeGroupAdd} { c := groupType.NewCreateCommand() if c.Exists() { return groupType } } return GroupTypeAddGroup } func (g *Group) SetResourceMapper(resources data.ResourceMapper) { g.Resources = resources } func (g *Group) Clone() data.Resource { newg := &Group { Name: g.Name, GID: g.GID, State: g.State, GroupType: g.GroupType, } newg.CreateCommand, newg.ReadCommand, newg.UpdateCommand, newg.DeleteCommand = g.GroupType.NewCRUD() return newg } func (g *Group) StateMachine() machine.Stater { if g.stater == nil { g.stater = StorageMachine(g) } return g.stater } func (g *Group) Notify(m *machine.EventMessage) { ctx := context.Background() switch m.On { case machine.ENTERSTATEEVENT: switch m.Dest { case "start_create": if e := g.Create(ctx); e == nil { if triggerErr := g.stater.Trigger("created"); triggerErr == nil { return } } g.State = "absent" case "present": g.State = "present" } case machine.EXITSTATEEVENT: } } func (g *Group) SetURI(uri string) error { resourceUri, e := url.Parse(uri) if e == nil { if resourceUri.Scheme == "group" { g.Name = resourceUri.Hostname() } else { e = fmt.Errorf("%w: %s is not a group", ErrInvalidResourceURI, uri) } } return e } func (g *Group) URI() string { return fmt.Sprintf("group://%s", g.Name) } func (g *Group) UseConfig(config data.ConfigurationValueGetter) { g.config = config } func (g *Group) ResolveId(ctx context.Context) string { return LookupUIDString(g.Name) } func (g *Group) Validate() error { return fmt.Errorf("failed") } func (g *Group) Apply() error { ctx := context.Background() switch g.State { case "present": _, NoGroupExists := LookupGID(g.Name) if NoGroupExists != nil { cmdErr := g.Create(ctx) return cmdErr } case "absent": cmdErr := g.Delete(ctx) return cmdErr } return nil } func (g *Group) Load(docData []byte, f codec.Format) (err error) { err = f.StringDecoder(string(docData)).Decode(g) return } func (g *Group) LoadReader(r io.ReadCloser, f codec.Format) (err error) { err = f.Decoder(r).Decode(g) return } func (g *Group) LoadString(docData string, f codec.Format) (err error) { err = f.StringDecoder(docData).Decode(g) return } func (g *Group) LoadDecl(yamlResourceDeclaration string) error { return g.LoadString(yamlResourceDeclaration, codec.FormatYaml) } func (g *Group) Type() string { return "group" } func (g *Group) Create(ctx context.Context) (error) { _, err := g.CreateCommand.Execute(g) if err != nil { return err } _,e := g.Read(ctx) return e } func (g *Group) Read(ctx context.Context) ([]byte, error) { exErr := g.ReadCommand.Extractor(nil, g) if exErr != nil { g.State = "absent" } if yaml, yamlErr := yaml.Marshal(g); yamlErr != nil { return yaml, yamlErr } else { return yaml, exErr } } func (g *Group) Update(ctx context.Context) (error) { return g.Create(ctx) } func (g *Group) Delete(ctx context.Context) (error) { _, err := g.DeleteCommand.Execute(g) if err != nil { return err } return err } func (g *Group) UnmarshalJSON(data []byte) error { if unmarshalErr := json.Unmarshal(data, (*decodeGroup)(g)); unmarshalErr != nil { return unmarshalErr } g.CreateCommand, g.ReadCommand, g.UpdateCommand, g.DeleteCommand = g.GroupType.NewCRUD() return nil } func (g *Group) UnmarshalYAML(value *yaml.Node) error { if unmarshalErr := value.Decode((*decodeGroup)(g)); unmarshalErr != nil { return unmarshalErr } g.CreateCommand, g.ReadCommand, g.UpdateCommand, g.DeleteCommand = g.GroupType.NewCRUD() return nil } func (g *GroupType) NewCRUD() (create *command.Command, read *command.Command, update *command.Command, del *command.Command) { switch *g { case GroupTypeGroupAdd: return NewGroupAddCreateCommand(), NewGroupReadCommand(), NewGroupUpdateCommand(), NewGroupDelDeleteCommand() case GroupTypeAddGroup: return NewAddGroupCreateCommand(), NewGroupReadCommand(), NewGroupUpdateCommand(), NewDelGroupDeleteCommand() default: if _, addGroupPathErr := exec.LookPath("addgroup"); addGroupPathErr == nil { *g = GroupTypeAddGroup return NewAddGroupCreateCommand(), NewGroupReadCommand(), NewGroupUpdateCommand(), NewDelGroupDeleteCommand() } if _, pathErr := exec.LookPath("groupadd"); pathErr == nil { *g = GroupTypeGroupAdd return NewGroupAddCreateCommand(), NewGroupReadCommand(), NewGroupUpdateCommand(), NewGroupDelDeleteCommand() } return NewGroupAddCreateCommand(), NewGroupReadCommand(), NewGroupUpdateCommand(), NewGroupDelDeleteCommand() } } func (g *GroupType) NewCreateCommand() (create *command.Command) { switch *g { case GroupTypeGroupAdd: return NewGroupAddCreateCommand() case GroupTypeAddGroup: return NewAddGroupCreateCommand() default: } return nil } func (g *GroupType) NewReadGroupsCommand() *command.Command { return NewReadGroupsCommand() } func (g *GroupType) UnmarshalValue(value string) error { switch value { case string(GroupTypeGroupAdd), string(GroupTypeAddGroup): *g = GroupType(value) return nil default: return errors.New("invalid GroupType value") } } func (g *GroupType) UnmarshalJSON(data []byte) error { var s string if unmarshalGroupTypeErr := json.Unmarshal(data, &s); unmarshalGroupTypeErr != nil { return unmarshalGroupTypeErr } return g.UnmarshalValue(s) } func (g *GroupType) UnmarshalYAML(value *yaml.Node) error { var s string if err := value.Decode(&s); err != nil { return err } return g.UnmarshalValue(s) } func NewGroupAddCreateCommand() *command.Command { c := command.NewCommand() c.Path = "groupadd" c.Args = []command.CommandArg{ command.CommandArg("{{ if .GID }}-g {{ .GID }}{{ end }}"), command.CommandArg("{{ .Name }}"), } c.Extractor = func(out []byte, target any) error { return nil /* for _,line := range strings.Split(string(out), "\n") { if line == "iptables: Chain already exists." { return nil } } return fmt.Errorf(string(out)) */ } return c } func NewAddGroupCreateCommand() *command.Command { c := command.NewCommand() c.Path = "addgroup" c.Args = []command.CommandArg{ command.CommandArg("{{ if .GID }}-g {{ .GID }}{{ end }}"), command.CommandArg("{{ .Name }}"), } c.Extractor = func(out []byte, target any) error { return nil /* for _,line := range strings.Split(string(out), "\n") { if line == "iptables: Chain already exists." { return nil } } return fmt.Errorf(string(out)) */ } return c } func NewGroupReadCommand() *command.Command { c := command.NewCommand() c.Extractor = func(out []byte, target any) error { g := target.(*Group) g.State = "absent" var readGroup *user.Group var e error if g.Name != "" { readGroup, e = user.LookupGroup(g.Name) } else { if g.GID != "" { readGroup, e = user.LookupGroupId(g.GID) } } if e == nil { g.Name = readGroup.Name g.GID = readGroup.Gid if g.GID != "" { g.State = "present" } } return e } return c } func NewGroupUpdateCommand() *command.Command { return nil } func NewGroupDelDeleteCommand() *command.Command { c := command.NewCommand() c.Path = "groupdel" c.Args = []command.CommandArg{ command.CommandArg("{{ .Name }}"), } c.Extractor = func(out []byte, target any) error { return nil } return c } func NewDelGroupDeleteCommand() *command.Command { c := command.NewCommand() c.Path = "delgroup" c.Args = []command.CommandArg{ command.CommandArg("{{ .Name }}"), } c.Extractor = func(out []byte, target any) error { return nil } return c } func NewReadGroupsCommand() *command.Command { c := command.NewCommand() c.Path = "getent" c.Args = []command.CommandArg{ command.CommandArg("passwd"), } c.Extractor = func(out []byte, target any) error { Groups := target.(*[]*Group) lines := strings.Split(strings.TrimSpace(string(out)), "\n") lineIndex := 0 for _, line := range lines { groupRecord := strings.Split(strings.TrimSpace(line), ":") if len(*Groups) <= lineIndex + 1 { *Groups = append(*Groups, NewGroup()) } g := (*Groups)[lineIndex] g.Name = groupRecord[0] g.GID = groupRecord[2] g.State = "present" g.GroupType = SystemGroupType lineIndex++ } return nil } return c }