jx/internal/resource/group.go
Matthew Rich 8094d1c063
Some checks are pending
Lint / golangci-lint (push) Waiting to run
Declarative Tests / test (push) Waiting to run
Declarative Tests / build-fedora (push) Waiting to run
Declarative Tests / build-ubuntu-focal (push) Waiting to run
add build support to container_image resource; add more testing
2024-09-19 08:11:57 +00:00

427 lines
10 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 (
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
}