// 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 ( GroupTypeName TypeName = "group" GroupTypeAddGroup GroupType = "addgroup" GroupTypeGroupAdd GroupType = "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:"-"` Resources data.ResourceMapper `json:"-" yaml:"-"` groupStatus *user.Group `json:"-" yaml:"-"` } func NewGroup() (g *Group) { g = &Group{} g.Common = NewCommon(GroupTypeName, true) g.Common.NormalizePath = g.NormalizePath return } func init() { folio.DocumentRegistry.ResourceTypes.Register([]string{"group"}, func(u *url.URL) (group data.Resource) { group = NewGroup() if u != nil { if err := folio.CastParsedURI(u).ConstructResource(group); err != nil { panic(err) } } return }) } func FindSystemGroupType() GroupType { for _, groupType := range []GroupType{GroupTypeAddGroup, GroupTypeGroupAdd} { c := groupType.NewCreateCommand() if c.Exists() { return groupType } } return GroupTypeAddGroup } func (g *Group) Init(u data.URIParser) error { if u == nil { u = folio.URI(g.URI()).Parse() } uri := u.URL() g.Name = uri.Hostname() g.GID = LookupGIDString(uri.Hostname()) if _, addGroupPathErr := exec.LookPath("addgroup"); addGroupPathErr == nil { g.GroupType = GroupTypeAddGroup } if _, pathErr := exec.LookPath("groupadd"); pathErr == nil { g.GroupType = GroupTypeGroupAdd } g.CreateCommand, g.ReadCommand, g.UpdateCommand, g.DeleteCommand = g.GroupType.NewCRUD() return g.SetParsedURI(u) } func (g *Group) NormalizePath() error { return nil } func (g *Group) SetResourceMapper(resources data.ResourceMapper) { g.Resources = resources } func (g *Group) Clone() data.Resource { newg := &Group { Common: g.Common, Name: g.Name, GID: g.GID, 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_stat": if statErr := g.ReadStat(); statErr == nil { if triggerErr := g.StateMachine().Trigger("exists"); triggerErr == nil { return } } else { if triggerErr := g.StateMachine().Trigger("notexists"); triggerErr == nil { return } } case "start_read": if _,readErr := g.Read(ctx); readErr == nil { if triggerErr := g.StateMachine().Trigger("state_read"); triggerErr == nil { return } else { g.Common.State = "absent" panic(triggerErr) } } else { g.Common.State = "absent" panic(readErr) } case "start_create": if e := g.Create(ctx); e == nil { if triggerErr := g.stater.Trigger("created"); triggerErr == nil { return } } g.Common.State = "absent" case "start_update": if updateErr := g.Update(ctx); updateErr == nil { if triggerErr := g.stater.Trigger("updated"); triggerErr == nil { return } else { g.Common.State = "absent" } } else { g.Common.State = "absent" panic(updateErr) } case "absent": g.Common.State = "absent" case "present", "created", "read": g.Common.State = "present" } case machine.EXITSTATEEVENT: } } func (g *Group) URI() string { return fmt.Sprintf("group://%s", g.Name) } 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.Common.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) ReadStat() (err error) { if g.groupStatus == nil { if g.groupStatus, err = user.LookupGroup(g.Name); err != nil { g.Common.State = "absent" return err } } if len(g.groupStatus.Gid) < 1 { g.Common.State = "absent" return ErrResourceStateAbsent } g.GID = g.groupStatus.Gid return } func (g *Group) Read(ctx context.Context) ([]byte, error) { exErr := g.ReadCommand.Extractor(nil, g) if exErr != nil { g.Common.State = "absent" } if yaml, yamlErr := yaml.Marshal(g); yamlErr != nil { return yaml, yamlErr } else { return yaml, exErr } } func (g *Group) Update(ctx context.Context) (err error) { _, err = g.UpdateCommand.Execute(g) return } 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.Common.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.Common.State = "present" } } return e } return c } func NewGroupUpdateCommand() *command.Command { c := command.NewCommand() c.Path = "addgroup" c.FailOnError = false c.Args = []command.CommandArg{ command.CommandArg("{{ if .GID }}-g {{ .GID }}{{ end }}"), command.CommandArg("{{ .Name }}"), } c.Extractor = func(out []byte, target any) error { return nil } return c } 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.Common.State = "present" g.GroupType = SystemGroupType lineIndex++ } return nil } return c }