501 lines
12 KiB
Go
501 lines
12 KiB
Go
// 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"
|
|
)
|
|
|
|
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:"-"`
|
|
//State string `json:"state,omitempty" yaml:"state,omitempty"`
|
|
config data.ConfigurationValueGetter
|
|
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) 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) 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) 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.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
|
|
}
|