jx/internal/resource/group.go

496 lines
12 KiB
Go
Raw Normal View History

2024-07-17 08:34:57 +00:00
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. 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"
2024-07-17 08:34:57 +00:00
)
type decodeGroup Group
type GroupType string
const (
GroupTypeName TypeName = "group"
GroupTypeAddGroup GroupType = "addgroup"
GroupTypeGroupAdd GroupType = "groupadd"
2024-07-17 08:34:57 +00:00
)
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:"-"`
2024-07-17 08:34:57 +00:00
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:"-"`
2024-07-17 08:34:57 +00:00
}
func NewGroup() (g *Group) {
g = &Group{}
g.Common = NewCommon(GroupTypeName, true)
g.Common.NormalizePath = g.NormalizePath
return
2024-07-17 08:34:57 +00:00
}
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)
}
2024-07-17 08:34:57 +00:00
}
return
2024-07-17 08:34:57 +00:00
})
}
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()
2024-10-16 17:51:58 +00:00
return g.SetParsedURI(u)
}
func (g *Group) NormalizePath() error {
return nil
}
func (g *Group) SetResourceMapper(resources data.ResourceMapper) {
2024-07-17 08:34:57 +00:00
g.Resources = resources
}
func (g *Group) Clone() data.Resource {
2024-07-17 08:34:57 +00:00
newg := &Group {
Common: g.Common,
2024-07-17 08:34:57 +00:00
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)
}
2024-07-17 08:34:57 +00:00
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"
2024-07-17 08:34:57 +00:00
}
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 {
2024-07-17 08:34:57 +00:00
case "present":
_, NoGroupExists := LookupGID(g.Name)
if NoGroupExists != nil {
cmdErr := g.Create(ctx)
2024-07-17 08:34:57 +00:00
return cmdErr
}
case "absent":
cmdErr := g.Delete(ctx)
2024-07-17 08:34:57 +00:00
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
2024-07-17 08:34:57 +00:00
}
func (g *Group) LoadDecl(yamlResourceDeclaration string) error {
return g.LoadString(yamlResourceDeclaration, codec.FormatYaml)
2024-07-17 08:34:57 +00:00
}
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
}
2024-07-17 08:34:57 +00:00
func (g *Group) Read(ctx context.Context) ([]byte, error) {
exErr := g.ReadCommand.Extractor(nil, g)
if exErr != nil {
g.Common.State = "absent"
2024-07-17 08:34:57 +00:00
}
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) {
2024-07-17 08:34:57 +00:00
_, 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"
2024-07-17 08:34:57 +00:00
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"
2024-07-17 08:34:57 +00:00
}
}
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
2024-07-17 08:34:57 +00:00
}
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"
2024-07-17 08:34:57 +00:00
g.GroupType = SystemGroupType
lineIndex++
}
return nil
}
return c
}