From 1117882cedc27816b8cbbe135b852c598cc2bd9f Mon Sep 17 00:00:00 2001 From: Matthew Rich Date: Wed, 9 Oct 2024 22:26:39 +0000 Subject: [PATCH] update resources to common uri handling --- internal/resource/common.go | 5 +- internal/resource/container_image.go | 10 +- internal/resource/container_network.go | 12 +- internal/resource/file.go | 26 +- internal/resource/group.go | 108 ++++++-- internal/resource/http.go | 25 +- internal/resource/iptables.go | 359 +++++++++++++++++++------ internal/resource/iptables_test.go | 51 ++++ internal/resource/network_route.go | 8 +- internal/resource/pki.go | 31 +++ internal/resource/pki_test.go | 3 + internal/resource/service.go | 223 ++++++++++++--- internal/resource/service_test.go | 22 +- internal/resource/user.go | 209 ++++++++++---- internal/resource/user_test.go | 21 +- 15 files changed, 887 insertions(+), 226 deletions(-) diff --git a/internal/resource/common.go b/internal/resource/common.go index 150b8e3..0c952af 100644 --- a/internal/resource/common.go +++ b/internal/resource/common.go @@ -13,9 +13,11 @@ import ( ) type UriSchemeValidator func(scheme string) bool +type UriNormalize func() error type Common struct { SchemeCheck UriSchemeValidator `json:"-" yaml:"-"` + NormalizePath UriNormalize `json:"-" yaml:"-"` includeQueryParamsInURI bool `json:"-" yaml:"-"` resourceType TypeName `json:"-" yaml:"-"` Uri folio.URI `json:"uri,omitempty" yaml:"uri,omitempty"` @@ -34,6 +36,7 @@ type Common struct { func NewCommon(resourceType TypeName, includeQueryParams bool) *Common { c := &Common{ includeQueryParamsInURI: includeQueryParams, resourceType: resourceType } c.SchemeCheck = c.IsValidResourceScheme + c.NormalizePath = c.NormalizeFilePath return c } @@ -123,7 +126,7 @@ func (c *Common) ResolveId(ctx context.Context) string { return c.Path } -func (c *Common) NormalizePath() error { +func (c *Common) NormalizeFilePath() error { if c.config != nil { if prefixPath, configErr := c.config.GetValue("prefix"); configErr == nil { c.Path = filepath.Join(prefixPath.(string), c.Path) diff --git a/internal/resource/container_image.go b/internal/resource/container_image.go index 2dfb63f..fd88fc8 100644 --- a/internal/resource/container_image.go +++ b/internal/resource/container_image.go @@ -118,13 +118,15 @@ func NewContainerImage(containerClientApi ContainerImageClient) *ContainerImage panic(err) } } - return &ContainerImage{ - Common: &Common{ includeQueryParamsInURI: true, resourceType: ContainerImageTypeName }, + c := &ContainerImage{ + Common: NewCommon(ContainerImageTypeName, true), apiClient: apiClient, InjectJX: true, PushImage: false, ConverterTypes: folio.DocumentRegistry.ConverterTypes, } + c.Common.NormalizePath = c.NormalizePath + return c } func (c *ContainerImage) RegistryAuthConfig() (authConfig registry.AuthConfig, err error) { @@ -175,6 +177,10 @@ func (c *ContainerImage) RegistryAuth() (string, error) { } } +func (c *ContainerImage) NormalizePath() error { + return nil +} + func (c *ContainerImage) SetResourceMapper(resources data.ResourceMapper) { c.Resources = resources } diff --git a/internal/resource/container_network.go b/internal/resource/container_network.go index 632c5dd..2e3fa64 100644 --- a/internal/resource/container_network.go +++ b/internal/resource/container_network.go @@ -62,7 +62,7 @@ func init() { }) } -func NewContainerNetwork(containerClientApi ContainerNetworkClient) *ContainerNetwork { +func NewContainerNetwork(containerClientApi ContainerNetworkClient) (cn *ContainerNetwork) { var apiClient ContainerNetworkClient = containerClientApi if apiClient == nil { var err error @@ -71,10 +71,16 @@ func NewContainerNetwork(containerClientApi ContainerNetworkClient) *ContainerNe panic(err) } } - return &ContainerNetwork{ - Common: &Common{ includeQueryParamsInURI: true, resourceType: ContainerNetworkTypeName }, + cn = &ContainerNetwork{ apiClient: apiClient, } + cn.Common = NewCommon(ContainerNetworkTypeName, true) + cn.Common.NormalizePath = cn.NormalizePath + return cn +} + +func (n *ContainerNetwork) NormalizePath() error { + return nil } func (n *ContainerNetwork) SetResourceMapper(resources data.ResourceMapper) { diff --git a/internal/resource/file.go b/internal/resource/file.go index b6eb350..59ec671 100644 --- a/internal/resource/file.go +++ b/internal/resource/file.go @@ -135,10 +135,17 @@ func NewNormalizedFile() *File { } func (f *File) ContentType() string { - if f.parsedURI.Scheme != "file" { - return f.parsedURI.Scheme - } - return f.exttype + var ext strings.Builder + if f.parsedURI.Scheme != "file" { + return f.parsedURI.Scheme + } + if f.fileext == "" { + return f.exttype + } + ext.WriteString(f.exttype) + ext.WriteRune('.') + ext.WriteString(f.fileext) + return ext.String() } func (f *File) SetResourceMapper(resources data.ResourceMapper) { @@ -215,6 +222,17 @@ func (f *File) Notify(m *machine.EventMessage) { f.State = "absent" panic(e) } + case "start_update": + if updateErr := f.Update(ctx); updateErr == nil { + if triggerErr := f.stater.Trigger("updated"); triggerErr == nil { + return + } else { + f.State = "absent" + } + } else { + f.State = "absent" + panic(updateErr) + } case "start_delete": if deleteErr := f.Delete(ctx); deleteErr == nil { if triggerErr := f.StateMachine().Trigger("deleted"); triggerErr == nil { diff --git a/internal/resource/group.go b/internal/resource/group.go index ba5fe1c..51203bc 100644 --- a/internal/resource/group.go +++ b/internal/resource/group.go @@ -27,8 +27,9 @@ type decodeGroup Group type GroupType string const ( - GroupTypeAddGroup = "addgroup" - GroupTypeGroupAdd = "groupadd" + GroupTypeName TypeName = "group" + GroupTypeAddGroup GroupType = "addgroup" + GroupTypeGroupAdd GroupType = "groupadd" ) var ErrUnsupportedGroupType error = errors.New("The GroupType is not supported on this system") @@ -47,13 +48,17 @@ type Group struct { ReadCommand *command.Command `json:"-" yaml:"-"` UpdateCommand *command.Command `json:"-" yaml:"-"` DeleteCommand *command.Command `json:"-" yaml:"-"` - State string `json:"state,omitempty" yaml:"state,omitempty"` + //State string `json:"state,omitempty" yaml:"state,omitempty"` config data.ConfigurationValueGetter Resources data.ResourceMapper `json:"-" yaml:"-"` + groupStatus *user.Group `json:"-" yaml:"-"` } -func NewGroup() *Group { - return &Group{} +func NewGroup() (g *Group) { + g = &Group{} + g.Common = NewCommon(GroupTypeName, true) + g.Common.NormalizePath = g.NormalizePath + return } func init() { @@ -82,15 +87,19 @@ func FindSystemGroupType() 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, - State: g.State, GroupType: g.GroupType, } newg.CreateCommand, newg.ReadCommand, newg.UpdateCommand, newg.DeleteCommand = g.GroupType.NewCRUD() @@ -109,15 +118,53 @@ func (g *Group) Notify(m *machine.EventMessage) { 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.State = "absent" - case "present": - g.State = "present" + 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: } @@ -153,7 +200,7 @@ func (g *Group) Validate() error { func (g *Group) Apply() error { ctx := context.Background() - switch g.State { + switch g.Common.State { case "present": _, NoGroupExists := LookupGID(g.Name) if NoGroupExists != nil { @@ -198,10 +245,26 @@ func (g *Group) Create(ctx context.Context) (error) { 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.State = "absent" + g.Common.State = "absent" } if yaml, yamlErr := yaml.Marshal(g); yamlErr != nil { return yaml, yamlErr @@ -210,8 +273,9 @@ func (g *Group) Read(ctx context.Context) ([]byte, error) { } } -func (g *Group) Update(ctx context.Context) (error) { - return g.Create(ctx) +func (g *Group) Update(ctx context.Context) (err error) { + _, err = g.UpdateCommand.Execute(g) + return } func (g *Group) Delete(ctx context.Context) (error) { @@ -345,7 +409,7 @@ func NewGroupReadCommand() *command.Command { c := command.NewCommand() c.Extractor = func(out []byte, target any) error { g := target.(*Group) - g.State = "absent" + g.Common.State = "absent" var readGroup *user.Group var e error if g.Name != "" { @@ -360,7 +424,7 @@ func NewGroupReadCommand() *command.Command { g.Name = readGroup.Name g.GID = readGroup.Gid if g.GID != "" { - g.State = "present" + g.Common.State = "present" } } return e @@ -369,7 +433,17 @@ func NewGroupReadCommand() *command.Command { } func NewGroupUpdateCommand() *command.Command { - return nil + 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 { @@ -416,7 +490,7 @@ func NewReadGroupsCommand() *command.Command { g := (*Groups)[lineIndex] g.Name = groupRecord[0] g.GID = groupRecord[2] - g.State = "present" + g.Common.State = "present" g.GroupType = SystemGroupType lineIndex++ } diff --git a/internal/resource/http.go b/internal/resource/http.go index 5ded0e4..5d41aa0 100644 --- a/internal/resource/http.go +++ b/internal/resource/http.go @@ -102,17 +102,26 @@ type HTTP struct { } func NewHTTP() *HTTP { - h := &HTTP{ client: &http.Client{}, Common: &Common{ includeQueryParamsInURI: true, resourceType: HTTPTypeName, SchemeCheck: func(scheme string) bool { - switch scheme { - case "http", "https": - return true - } - return false - } } } + h := &HTTP{ client: &http.Client{} } + h.Common = NewCommon(HTTPTypeName, true) + h.Common.SchemeCheck = h.SchemeCheck + h.Common.NormalizePath = h.NormalizePath slog.Info("NewHTTP()", "http", h) return h } +func (h *HTTP) SchemeCheck(scheme string) bool { + switch scheme { + case "http", "https": + return true + } + return false +} + +func (h *HTTP) NormalizePath() error { + return nil +} + func (h *HTTP) SetResourceMapper(resources data.ResourceMapper) { h.Resources = resources } @@ -266,11 +275,13 @@ func (h *HTTP) ContentSourceRefStat() (info fs.FileInfo) { } func (h *HTTP) ReadStat() (err error) { + if h.reader == nil { if err = h.OpenGetter(); err != nil { return } } + var info fs.FileInfo info, err = h.reader.Stat() diff --git a/internal/resource/iptables.go b/internal/resource/iptables.go index 9326f35..06b9369 100644 --- a/internal/resource/iptables.go +++ b/internal/resource/iptables.go @@ -41,7 +41,7 @@ func init() { } else { i.ResourceType = IptableTypeRule id, _ := strconv.ParseUint(fields[1], 10, 32) - i.Id = uint(id) + i.SetId(uint(id)) } } i.CreateCommand, i.ReadCommand, i.UpdateCommand, i.DeleteCommand = i.ResourceType.NewCRUD() @@ -72,9 +72,9 @@ var IptableNumber = regexp.MustCompile(`^[0-9]+$`) type IptableChain string const ( - IptableChainInput = "INPUT" - IptableChainOutput = "OUTPUT" - IptableChainForward = "FORWARD" + IptableChainInput = "INPUT" + IptableChainOutput = "OUTPUT" + IptableChainForward = "FORWARD" IptableChainPreRouting = "PREROUTING" IptableChainPostRouting = "POSTROUTING" ) @@ -101,6 +101,8 @@ type ExtensionFlag struct { Value string `json:"value" yaml:"value"` } +type TargetFlag ExtensionFlag + type IptablePort uint16 type IptableType string @@ -110,6 +112,8 @@ const ( IptableTypeChain = "chain" ) +type IptableRule string + var ( ErrInvalidIptableName error = errors.New("The IptableName is not a valid table") ) @@ -117,32 +121,33 @@ var ( // Manage the state of iptables rules // iptable://filter/INPUT/0 type Iptable struct { - *Common `json:",inline" yaml:",inline"` - stater machine.Stater `json:"-" yaml:"-"` - parsedURI *url.URL `json:"-" yaml:"-"` - Id uint `json:"id,omitempty" yaml:"id,omitempty"` - Table IptableName `json:"table" yaml:"table"` - Chain IptableChain `json:"chain" yaml:"chain"` - Destination IptableCIDR `json:"destination,omitempty" yaml:"destination,omitempty"` - Source IptableCIDR `json:"source,omitempty" yaml:"source,omitempty"` - Dport IptablePort `json:"dport,omitempty" yaml:"dport,omitempty"` - Sport IptablePort `json:"sport,omitempty" yaml:"sport,omitempty"` - In string `json:"in,omitempty" yaml:"in,omitempty"` - Out string `json:"out,omitempty" yaml:"out,omitempty"` - Match []string `json:"match,omitempty" yaml:"match,omitempty"` - Flags []ExtensionFlag `json:"extension_flags,omitempty" yaml:"extension_flags,omitempty"` - Proto IptableProto `json:"proto,omitempty" yaml:"proto,omitempty"` - Jump string `json:"jump,omitempty" yaml:"jump,omitempty"` - ChainLength uint `json:"-" yaml:"-"` + *Common `json:",inline" yaml:",inline"` + stater machine.Stater `json:"-" yaml:"-"` + Id uint `json:"id,omitempty" yaml:"id,omitempty"` + Table IptableName `json:"table" yaml:"table"` + Chain IptableChain `json:"chain" yaml:"chain"` + Destination IptableCIDR `json:"destination,omitempty" yaml:"destination,omitempty"` + Source IptableCIDR `json:"source,omitempty" yaml:"source,omitempty"` + Dport IptablePort `json:"dport,omitempty" yaml:"dport,omitempty"` + Sport IptablePort `json:"sport,omitempty" yaml:"sport,omitempty"` + In string `json:"in,omitempty" yaml:"in,omitempty"` + Out string `json:"out,omitempty" yaml:"out,omitempty"` + Match []string `json:"match,omitempty" yaml:"match,omitempty"` + Flags []ExtensionFlag `json:"extension_flags,omitempty" yaml:"extension_flags,omitempty"` + Proto IptableProto `json:"proto,omitempty" yaml:"proto,omitempty"` + Jump string `json:"jump,omitempty" yaml:"jump,omitempty"` + TargetFlags []TargetFlag `json:"target_flags,omitempty" yaml:"target_flags,omitempty"` + ChainLength uint `json:"-" yaml:"-"` - ResourceType IptableType `json:"resourcetype,omitempty" yaml:"resourcetype,omitempty"` - CreateCommand *command.Command `yaml:"-" json:"-"` - ReadCommand *command.Command `yaml:"-" json:"-"` - UpdateCommand *command.Command `yaml:"-" json:"-"` - DeleteCommand *command.Command `yaml:"-" json:"-"` + ResourceType IptableType `json:"resourcetype,omitempty" yaml:"resourcetype,omitempty"` + CreateCommand *command.Command `yaml:"-" json:"-"` + ReadCommand *command.Command `yaml:"-" json:"-"` + ReadChainCommand *command.Command `yaml:"-" json:"-"` + UpdateCommand *command.Command `yaml:"-" json:"-"` + DeleteCommand *command.Command `yaml:"-" json:"-"` - config data.ConfigurationValueGetter - Resources data.ResourceMapper `yaml:"-" json:"-"` + config data.ConfigurationValueGetter + Resources data.ResourceMapper `yaml:"-" json:"-"` } @@ -155,10 +160,13 @@ func (n IptableName) Validate() error { } } -func NewIptable() *Iptable { - i := &Iptable{ ResourceType: IptableTypeRule, Common: &Common{ resourceType: IptableTypeName } } +func NewIptable() (i *Iptable) { + i = &Iptable{ ResourceType: IptableTypeRule } + i.Common = NewCommon(IptableTypeName, false) + i.Common.NormalizePath = i.NormalizePath i.CreateCommand, i.ReadCommand, i.UpdateCommand, i.DeleteCommand = i.ResourceType.NewCRUD() - return i + i.ReadChainCommand = NewIptableChainReadCommand() + return } func (i *Iptable) SetResourceMapper(resources data.ResourceMapper) { @@ -195,51 +203,126 @@ func (i *Iptable) Notify(m *machine.EventMessage) { switch m.On { case machine.ENTERSTATEEVENT: switch m.Dest { - case "start_create": - if e := i.Create(ctx); e == nil { - if triggerErr := i.stater.Trigger("created"); triggerErr == nil { + case "start_stat": + if statErr := i.ReadStat(ctx); statErr == nil { + if triggerErr := i.StateMachine().Trigger("exists"); triggerErr == nil { + return + } + } else { + if triggerErr := i.StateMachine().Trigger("notexists"); triggerErr == nil { return } } - i.Common.State = "absent" - case "present": + case "start_read": + if _,readErr := i.Read(ctx); readErr == nil { + if triggerErr := i.stater.Trigger("state_read"); triggerErr == nil { + return + } else { + i.Common.State = "absent" + panic(triggerErr) + } + } else { + i.Common.State = "absent" + panic(readErr) + } + case "start_create": + if createErr := i.Create(ctx); createErr == nil { + if triggerErr := i.stater.Trigger("created"); triggerErr == nil { + slog.Info("ContainerImage.Notify()", "created", i, "error", triggerErr) + return + } else { + slog.Info("ContainerImage.Notify()", "created", i, "error", triggerErr) + i.Common.State = "absent" + panic(triggerErr) + } + } else { + i.Common.State = "absent" + panic(createErr) + } + case "start_update": + if createErr := i.Update(ctx); createErr == nil { + if triggerErr := i.stater.Trigger("updated"); triggerErr == nil { + return + } else { + i.Common.State = "absent" + } + } else { + i.Common.State = "absent" + panic(createErr) + } + case "start_delete": + if deleteErr := i.Delete(ctx); deleteErr == nil { + if triggerErr := i.stater.Trigger("deleted"); triggerErr == nil { + return + } + } else { + panic(deleteErr) + } + case "present", "created", "read": i.Common.State = "present" + case "absent": + i.Common.State = "absent" } case machine.EXITSTATEEVENT: } } +// Set the chain ID and update the mapped URI +func (i *Iptable) SetId(id uint) { + if i.Id != id { + uri := i.URI() + i.Id = id + decl, ok := i.Resources.Get(uri) + if ok { + i.Resources.Delete(uri) + } + i.Resources.Set(i.URI(), decl) + } +} + func (i *Iptable) URI() string { return fmt.Sprintf("iptable://%s/%s/%d", i.Table, i.Chain, i.Id) } -func (i *Iptable) SetURI(uri string) (err error) { - i.parsedURI, err = url.Parse(uri) - if err == nil { - fields := strings.FieldsFunc(i.parsedURI.Path, func(c rune) bool { return c == '/' }) - fieldsLen := len(fields) - if i.parsedURI.Scheme == "iptable" && fieldsLen > 0 { - i.Table = IptableName(i.parsedURI.Hostname()) - if err = i.Table.Validate(); err != nil { - return err - } - i.Chain = IptableChain(fields[0]) - if fieldsLen < 2 { - i.ResourceType = IptableTypeChain - } else { - i.ResourceType = IptableTypeRule - id, _ := strconv.ParseUint(fields[1], 10, 32) - i.Id = uint(id) - } - } else { - err = fmt.Errorf("%w: %s is not an iptable rule", ErrInvalidResourceURI, uri) - } + +func (i *Iptable) SetParsedURI(uri *url.URL) (err error) { + if err = i.Common.SetParsedURI(uri); err == nil { + i.setFieldsFromPath() } return } -func (i *Iptable) UseConfig(config data.ConfigurationValueGetter) { - i.config = config +func (i *Iptable) NormalizePath() error { + return nil +} + +func (i *Iptable) setFieldsFromPath() (err error) { + fields := strings.FieldsFunc(i.Common.Path, func(c rune) bool { return c == '/' }) + fieldsLen := len(fields) + if fieldsLen > 0 { + i.Table = IptableName(fields[0]) + if err = i.Table.Validate(); err != nil { + return err + } + i.Chain = IptableChain(fields[1]) + if fieldsLen < 3 { + i.ResourceType = IptableTypeChain + } else { + i.ResourceType = IptableTypeRule + id, _ := strconv.ParseUint(fields[1], 10, 32) + i.Id = uint(id) + } + } else { + err = fmt.Errorf("%w: %s is not an iptable rule", ErrInvalidResourceURI, i.Common.Uri) + } + return +} + +func (i *Iptable) SetURI(uri string) (err error) { + if err = i.Common.SetURI(uri); err == nil { + err = i.setFieldsFromPath() + } + return } func (i *Iptable) Validate() error { @@ -316,6 +399,46 @@ func (i *Iptable) ResolveId(ctx context.Context) string { return fmt.Sprintf("%d", i.Id) } +func (f *ExtensionFlag) Match(name string, value string) bool { + start := 0 + if name[1] == '-' { + start = 2 + } else if name[0] == '-' { + start = 1 + } + return f.Name == name[start:] && f.Value == value +} + +func (i *Iptable) HasExtensionFlag(name string) bool { + optName := strings.Trim(name, "-") + for _, ext := range i.Flags { + if ext.Name == optName { + return true + } + } + return false +} + +func (i *Iptable) HasTargetFlag(name string) bool { + optName := strings.Trim(name, "-") + for _, ext := range i.TargetFlags { + if ext.Name == optName { + return true + } + } + return false +} + +func (f *TargetFlag) Match(name string, value string) bool { + start := 0 + if name[1] == '-' { + start = 2 + } else if name[0] == '-' { + start = 1 + } + return f.Name == name[start:] && f.Value == value +} + func (i *Iptable) SetFlagValue(opt, value string) bool { switch opt { case "-i": @@ -353,10 +476,14 @@ func (i *Iptable) SetFlagValue(opt, value string) bool { i.Sport = IptablePort(port) return true default: - if opt[0] == '-' { - i.Flags = append(i.Flags, ExtensionFlag{ Name: strings.Trim(opt, "-"), Value: strings.TrimSpace(value)}) + if opt[0] == '-' { + if len(i.Jump) > 0 { + i.TargetFlags = append(i.TargetFlags, TargetFlag{ Name: strings.Trim(opt, "-"), Value: strings.TrimSpace(value)}) + } else { + i.Flags = append(i.Flags, ExtensionFlag{ Name: strings.Trim(opt, "-"), Value: strings.TrimSpace(value)}) + } return true - } + } } return false } @@ -382,9 +509,15 @@ func (i *Iptable) GetFlagValue(opt string) any { case "--sport": return strconv.Itoa(int(i.Sport)) default: - if opt[0] == '-' { - return i.Flags - } + if opt[0] == '-' { + slog.Info("Iptable.GetFlagValue()", "opt", opt, "iptable", i) + if i.HasExtensionFlag(opt) { + return i.Flags + } + if i.HasTargetFlag(opt) { + return i.TargetFlags + } + } } return nil } @@ -407,9 +540,11 @@ func (i *Iptable) SetRule(flags []string) (assigned bool) { func (i *Iptable) MatchRule(flags []string) (match bool) { match = true + next: for index, flag := range flags { if flag[0] == '-' { value := flags[index + 1] + slog.Info("Iptable.MatchRule()", "flag", flag, "value", value) switch v := i.GetFlagValue(flag).(type) { case []string: for _,element := range v { @@ -417,48 +552,57 @@ func (i *Iptable) MatchRule(flags []string) (match bool) { continue } } + slog.Info("Iptable.MatchRule() - FAILED", "flag", flag, "value", v) match = false case []ExtensionFlag: for _,element := range v { - if element.Name == flag && element.Value == value { - continue + if element.Match(flag, value) { + continue next } } + slog.Info("Iptable.MatchRule() - FAILED", "flag", flag, "value", v) + match = false + case []TargetFlag: + for _,element := range v { + if element.Match(flag, value) { + continue next + } + } + slog.Info("Iptable.MatchRule() - FAILED", "flag", flag, "value", v) match = false case IptableCIDR: if v == IptableCIDR(value) { continue } + slog.Info("Iptable.MatchRule() - FAILED", "flag", flag, "value", v) match = false case IptableName: if v == IptableName(value) { continue } + slog.Info("Iptable.MatchRule() - FAILED", "flag", flag, "value", v) match = false case IptableChain: if v == IptableChain(value) { continue } + slog.Info("Iptable.MatchRule() - FAILED", "flag", flag, "value", v) match = false default: if v.(string) == value { continue } + slog.Info("Iptable.MatchRule() - FAILED", "flag", flag, "value", v) match = false } } } + slog.Info("Iptable.MatchRule()", "flags", flags, "match", match) return } func (i *Iptable) ReadChainLength() error { - c := command.NewCommand() - c.Path = "iptables" - c.Args = []command.CommandArg{ - command.CommandArg("-S"), - command.CommandArg("{{ .Chain }}"), - } - output,err := c.Execute(i) + output,err := i.ReadChainCommand.Execute(i) if err == nil { linesCount := strings.Count(string(output), "\n") if linesCount > 0 { @@ -470,13 +614,16 @@ func (i *Iptable) ReadChainLength() error { return err } -func (i *Iptable) Create(ctx context.Context) error { +func (i *Iptable) Create(ctx context.Context) (err error) { + slog.Info("Iptable.Create()", "iptable", i) if i.Id > 0 { if lenErr := i.ReadChainLength(); lenErr != nil { return lenErr } } - _, err := i.CreateCommand.Execute(i) + + _, err = i.CreateCommand.Execute(i) + slog.Info("Iptable.Create()", "err", err, "iptable", i, "createcommand", i.CreateCommand) //slog.Info("IptableChain Create()", "err", err, "errstr", err.Error(), "iptable", i, "createcommand", i.CreateCommand) // TODO add Command status/error handler rather than using the read extractor if i.CreateCommand.Extractor != nil { @@ -484,7 +631,20 @@ func (i *Iptable) Create(ctx context.Context) error { return i.CreateCommand.Extractor([]byte(err.Error()), i) } } - return nil + return err +} + +func (i *Iptable) ReadStat(ctx context.Context) (err error) { + if i.ReadCommand.Exists() { + var out []byte + if out, err = i.ReadCommand.Execute(i); err == nil { + err = i.ReadCommand.Extractor(out, i) + } + } + if i.Id == 0 { + return ErrResourceStateAbsent + } + return err } func (i *Iptable) Read(ctx context.Context) ([]byte, error) { @@ -503,8 +663,15 @@ func (i *Iptable) Update(ctx context.Context) error { return i.Create(ctx) } -func (i *Iptable) Delete(ctx context.Context) error { - return nil +func (i *Iptable) Delete(ctx context.Context) (err error) { + if i.Id < 1 { + return fmt.Errorf("Failed to find rule to delete") + } + var out []byte + if out, err = i.DeleteCommand.Execute(i); err == nil { + err = i.DeleteCommand.Extractor(out, i) + } + return } func (i *Iptable) Type() string { return "iptable" } @@ -552,9 +719,7 @@ func NewIptableCreateCommand() *command.Command { c.Args = []command.CommandArg{ command.CommandArg("-t"), command.CommandArg("{{ .Table }}"), - command.CommandArg("{{ if le .Id .ChainLength }}-R{{ else }}-A{{ end }}"), - command.CommandArg("{{ .Chain }}"), - command.CommandArg("{{ if le .Id .ChainLength }}{{ .Id }}{{ end }}"), + command.CommandArg("{{ if and (le .Id .ChainLength) (gt .Id 0) }}-R {{ .Chain }} {{ .Id }}{{ else }}-A {{ .Chain }}{{ end }}"), command.CommandArg("{{ if .In }}-i {{ .In }}{{ else if .Out }}-o {{ .Out }}{{ end }}"), command.CommandArg("{{ range .Match }}-m {{ . }} {{- end }}"), command.CommandArg("{{ if .Source }}-s {{ .Source }}{{ end }}"), @@ -562,8 +727,9 @@ func NewIptableCreateCommand() *command.Command { command.CommandArg("{{ if .Destination }}-d {{ .Destination }}{{ end }}"), command.CommandArg("{{ if .Dport }}--dport {{ .Dport }}{{ end }}"), command.CommandArg("{{ if .Proto }}-p {{ .Proto }}{{ end }}"), - command.CommandArg("{{ range .Flags }} --{{ .Name }} {{ .Value }} {{ end }}"), + command.CommandArg("{{ range .Flags -}} --{{ .Name }} {{ .Value }} {{- end }}"), command.CommandArg("{{ if .Jump }}-j {{ .Jump }}{{ end }}"), + command.CommandArg("{{ range .TargetFlags -}} --{{ .Name }} {{ .Value }}{{- end }}"), } return c } @@ -584,7 +750,7 @@ func IptableExtractRule(lineNumber uint, ruleLine string, target *Iptable) (stat } } else { if target.MatchRule(flags) { - target.Id = lineNumber + target.SetId(lineNumber) state = "present" err = nil } @@ -661,7 +827,7 @@ func NewIptableReadChainCommand() *command.Command { } for lineIndex, line := range lines[1:] { i := (*IptableChainRules)[lineIndex] - i.Id = uint(lineIndex + 1) + i.SetId(uint(lineIndex + 1)) ruleFields := strings.Split(strings.TrimSpace(line), " ") if ruleFields[0] == "-A" { flags := ruleFields[2:] @@ -682,7 +848,16 @@ func NewIptableUpdateCommand() *command.Command { } func NewIptableDeleteCommand() *command.Command { - return nil + c := command.NewCommand() + c.Path = "iptables" + c.Args = []command.CommandArg{ + command.CommandArg("-t"), + command.CommandArg("{{ .Table }}"), + command.CommandArg("-D"), + command.CommandArg("{{ .Chain }}"), + command.CommandArg("{{ .Id }}"), + } + return c } func NewIptableChainCreateCommand() *command.Command { @@ -766,7 +941,7 @@ func RuleExtractorMatchFlags(out []byte, target any) (err error) { slog.Info("RuleExtractorMatchFlags()", "flags", flags, "ipt", ipt) err = nil ipt.Common.State = "present" - ipt.Id = uint(linesIndex) + ipt.SetId(uint(linesIndex)) return } } @@ -838,6 +1013,14 @@ func NewIptableChainUpdateCommand() *command.Command { } func NewIptableChainDeleteCommand() *command.Command { - return nil + c := command.NewCommand() + c.Path = "iptables" + c.Args = []command.CommandArg{ + command.CommandArg("-t"), + command.CommandArg("{{ .Table }}"), + command.CommandArg("-X"), + command.CommandArg("{{ .Chain }}"), + } + return c } diff --git a/internal/resource/iptables_test.go b/internal/resource/iptables_test.go index 05b8ca8..6e9e35c 100644 --- a/internal/resource/iptables_test.go +++ b/internal/resource/iptables_test.go @@ -20,6 +20,7 @@ _ "syscall" "testing" _ "time" "decl/internal/command" + "decl/internal/data" ) func TestNewIptableResource(t *testing.T) { @@ -77,10 +78,59 @@ func TestReadIptable(t *testing.T) { } func TestCreateIptable(t *testing.T) { + ctx := context.Background() testRule := NewIptable() assert.NotNil(t, testRule) + declarationAttributes := ` + table: "filter" + id: 5 + chain: "INPUT" + source: "192.168.0.0/24" + destination: "192.168.0.1" + jump: "ACCEPT" + state: present +` + m := &MockCommand{ + Executor: func(value any) ([]byte, error) { + return nil, nil + }, + Extractor: func(output []byte, target any) error { + testRule.Table = "filter" + testRule.Id = 3 + testRule.Chain = "INPUT" + testRule.In = "eth0" + testRule.Source = "192.168.0.0/24" + testRule.State = "present" + return nil + }, + } + + mockReadChain := &MockCommand{ + Executor: func(value any) ([]byte, error) { + return []byte(` +-P INPUT ACCEPT +-A INPUT -j LIBVIRT_INP +`), nil + }, + } + + e := testRule.LoadDecl(declarationAttributes) + assert.Nil(t, e) + + testRule.ReadChainCommand = (*command.Command)(mockReadChain) + testRule.ReadCommand = (*command.Command)(m) + testRule.CreateCommand = (*command.Command)(m) + + assert.Nil(t, testRule.Create(ctx)) + + assert.Equal(t, uint(2), testRule.ChainLength) + _, err := testRule.Read(ctx) + assert.Nil(t, err) + + //assert.Equal(t, uint(3), testRule.ChainLength) + assert.Equal(t, uint(3), testRule.Id) } func TestIptableSetFlagValue(t *testing.T) { @@ -117,6 +167,7 @@ func TestIptableRuleExtractorById(t *testing.T) { func TestIptableRuleExtractorByFlags(t *testing.T) { ipt := NewIptable() + ipt.Resources = data.NewResourceMapper() assert.NotNil(t, ipt) ipt.Table = IptableName("filter") ipt.Chain = IptableChain("FOO") diff --git a/internal/resource/network_route.go b/internal/resource/network_route.go index 31e9af5..f105e6e 100644 --- a/internal/resource/network_route.go +++ b/internal/resource/network_route.go @@ -136,11 +136,17 @@ type NetworkRoute struct { } func NewNetworkRoute() *NetworkRoute { - n := &NetworkRoute{Rtid: NetworkRouteTableMain, Common: &Common{ resourceType: NetworkRouteTypeName } } + n := &NetworkRoute{Rtid: NetworkRouteTableMain} + n.Common = NewCommon(NetworkRouteTypeName, false) + n.Common.NormalizePath = n.NormalizePath n.CreateCommand, n.ReadCommand, n.UpdateCommand, n.DeleteCommand = n.NewCRUD() return n } +func (n *NetworkRoute) NormalizePath() error { + return nil +} + func (n *NetworkRoute) SetResourceMapper(resources data.ResourceMapper) { n.Resources = resources } diff --git a/internal/resource/pki.go b/internal/resource/pki.go index ea1b259..479223b 100644 --- a/internal/resource/pki.go +++ b/internal/resource/pki.go @@ -119,6 +119,16 @@ func (k *PKI) Notify(m *machine.EventMessage) { switch m.On { case machine.ENTERSTATEEVENT: switch m.Dest { + case "start_stat": + if statErr := k.ReadStat(); statErr == nil { + if triggerErr := k.StateMachine().Trigger("exists"); triggerErr == nil { + return + } + } else { + if triggerErr := k.StateMachine().Trigger("notexists"); triggerErr == nil { + return + } + } case "start_read": if _,readErr := k.Read(ctx); readErr == nil { if triggerErr := k.StateMachine().Trigger("state_read"); triggerErr == nil { @@ -228,6 +238,27 @@ func (k *PKI) LoadDecl(yamlResourceDeclaration string) error { return k.LoadString(yamlResourceDeclaration, codec.FormatYaml) } +func (k *PKI) ReadStat() (err error) { + var resourcesErr []string + if ! k.PrivateKeyRef.Exists() { + resourcesErr = append(resourcesErr, string(k.PrivateKeyRef)) + } + + if ! k.PublicKeyRef.Exists() { + resourcesErr = append(resourcesErr, string(k.PublicKeyRef)) + } + + if ! k.CertificateRef.Exists() { + resourcesErr = append(resourcesErr, string(k.CertificateRef)) + } + + if len(resourcesErr) > 0 { + err = fmt.Errorf("PKI resources missing: %s", strings.Join(resourcesErr, ",")) +// k.State = "absent" + } + return +} + func (k *PKI) ResolveId(ctx context.Context) string { return string(k.PrivateKeyRef) } diff --git a/internal/resource/pki_test.go b/internal/resource/pki_test.go index 0a3a960..96a1970 100644 --- a/internal/resource/pki_test.go +++ b/internal/resource/pki_test.go @@ -36,6 +36,9 @@ func (rm TestResourceMapper) Has(key string) (ok bool) { func (rm TestResourceMapper) Set(key string, value data.Declaration) { } +func (rm TestResourceMapper) Delete(key string) { +} + type StringContentReadWriter func() (any, error) func (s StringContentReadWriter) ContentWriterStream() (*transport.Writer, error) { diff --git a/internal/resource/service.go b/internal/resource/service.go index d7374e5..6500745 100644 --- a/internal/resource/service.go +++ b/internal/resource/service.go @@ -6,7 +6,7 @@ package resource import ( "context" "fmt" -_ "log/slog" + "log/slog" "net/url" "path/filepath" "io" @@ -14,8 +14,14 @@ _ "log/slog" "gitea.rosskeen.house/rosskeen.house/machine" "decl/internal/codec" "decl/internal/data" + "decl/internal/command" "encoding/json" "strings" + "errors" +) + +var ( + ErrUnsupportedServiceManagerType error = errors.New("Unsupported service manager") ) const ( @@ -29,16 +35,28 @@ const ( ServiceManagerTypeSysV ServiceManagerType = "sysv" ) +const ( + SysVStatusRunning int = 0 + SysVStatusStopped int = 1 + SysVStatusUnknown int = 2 + SysVStatusMissing int = 3 +) + +var ( + SupportedServiceManagerTypes []ServiceManagerType = []ServiceManagerType{ServiceManagerTypeSystemd, ServiceManagerTypeSysV} + SystemServiceManagerType ServiceManagerType = FindSystemServiceManagerType() +) + type Service struct { *Common `yaml:",inline" json:",inline"` stater machine.Stater `yaml:"-" json:"-"` Name string `json:"name" yaml:"name"` ServiceManagerType ServiceManagerType `json:"servicemanager,omitempty" yaml:"servicemanager,omitempty"` - CreateCommand *Command `yaml:"-" json:"-"` - ReadCommand *Command `yaml:"-" json:"-"` - UpdateCommand *Command `yaml:"-" json:"-"` - DeleteCommand *Command `yaml:"-" json:"-"` + CreateCommand *command.Command `yaml:"-" json:"-"` + ReadCommand *command.Command `yaml:"-" json:"-"` + UpdateCommand *command.Command `yaml:"-" json:"-"` + DeleteCommand *command.Command `yaml:"-" json:"-"` config data.ConfigurationValueGetter Resources data.ResourceMapper `yaml:"-" json:"-"` @@ -53,8 +71,25 @@ func init() { }) } -func NewService() *Service { - return &Service{ ServiceManagerType: ServiceManagerTypeSystemd, Common: &Common{ resourceType: ServiceTypeName } } +func FindSystemServiceManagerType() ServiceManagerType { + for _, servicemanagerType := range SupportedServiceManagerTypes { + if c := servicemanagerType.NewReadCommand(); c != nil && c.Exists() { + return servicemanagerType + } + } + return ServiceManagerTypeSystemd +} + +func NewService() (s *Service) { + s = &Service{ ServiceManagerType: SystemServiceManagerType } + s.Common = NewCommon(ServiceTypeName, false) + s.Common.NormalizePath = s.NormalizePath + s.CreateCommand, s.ReadCommand, s.UpdateCommand, s.DeleteCommand = s.ServiceManagerType.NewCRUD() + return +} + +func (s *Service) NormalizePath() error { + return nil } func (s *Service) StateMachine() machine.Stater { @@ -69,6 +104,16 @@ func (s *Service) Notify(m *machine.EventMessage) { switch m.On { case machine.ENTERSTATEEVENT: switch m.Dest { + case "start_stat": + if statErr := s.ReadStat(); statErr == nil { + if triggerErr := s.StateMachine().Trigger("exists"); triggerErr == nil { + return + } + } else { + if triggerErr := s.StateMachine().Trigger("notexists"); triggerErr == nil { + return + } + } case "start_create": if e := s.Create(ctx); e == nil { if triggerErr := s.stater.Trigger("created"); triggerErr == nil { @@ -76,10 +121,24 @@ func (s *Service) Notify(m *machine.EventMessage) { } } s.Common.State = "absent" - case "created": + case "start_read": + if _,readErr := s.Read(ctx); readErr == nil { + if triggerErr := s.stater.Trigger("state_read"); triggerErr == nil { + return + } else { + s.Common.State = "absent" + panic(triggerErr) + } + } else { + s.Common.State = "absent" + panic(readErr) + } + case "present", "created", "read": s.Common.State = "present" case "running": s.Common.State = "running" + case "absent": + s.Common.State = "absent" } case machine.EXITSTATEEVENT: } @@ -89,20 +148,27 @@ func (s *Service) URI() string { return fmt.Sprintf("service://%s", s.Name) } -func (s *Service) SetURI(uri string) error { - resourceUri, e := url.Parse(uri) - if e == nil { - if resourceUri.Scheme == s.Type() { - s.Name = filepath.Join(resourceUri.Hostname(), resourceUri.RequestURI()) - } else { - e = fmt.Errorf("%w: %s is not a %s", ErrInvalidResourceURI, uri, s.Type()) - } +func (s *Service) SetParsedURI(uri *url.URL) (err error) { + if err = s.Common.SetParsedURI(uri); err == nil { + err = s.setFieldsFromPath() } - return e + return } -func (s *Service) UseConfig(config data.ConfigurationValueGetter) { - s.config = config +func (s *Service) setFieldsFromPath() (err error) { + if len(s.Common.Path) > 0 { + s.Name = s.Common.Path + } else { + err = fmt.Errorf("%w: %s is not an iptable rule", ErrInvalidResourceURI, s.Common.Uri) + } + return +} + +func (s *Service) SetURI(uri string) (err error) { + if err = s.Common.SetURI(uri); err == nil { + err = s.setFieldsFromPath() + } + return } func (s *Service) JSON() ([]byte, error) { @@ -167,7 +233,7 @@ func (s *Service) UnmarshalYAML(value *yaml.Node) error { return nil } -func (s *ServiceManagerType) NewCRUD() (create *Command, read *Command, update *Command, del *Command) { +func (s *ServiceManagerType) NewCRUD() (create *command.Command, read *command.Command, update *command.Command, del *command.Command) { switch *s { case ServiceManagerTypeSystemd: return NewSystemdCreateCommand(), NewSystemdReadCommand(), NewSystemdUpdateCommand(), NewSystemdDeleteCommand() @@ -178,14 +244,52 @@ func (s *ServiceManagerType) NewCRUD() (create *Command, read *Command, update * return nil, nil, nil, nil } - -func (s *Service) Create(ctx context.Context) error { +func (s *ServiceManagerType) NewReadCommand() (read *command.Command) { + switch *s { + case ServiceManagerTypeSystemd: + return NewSystemdReadCommand() + case ServiceManagerTypeSysV: + return NewSysVReadCommand() + default: + } return nil } -func (s *Service) Read(ctx context.Context) ([]byte, error) { - - return yaml.Marshal(s) +func (s *Service) Create(ctx context.Context) (err error) { + var out []byte + out, err = s.CreateCommand.Execute(s) + slog.Info("Service.Create()", "out", out, "error", err) + return +} + +func (s *Service) ReadStat() (err error) { + if s.ReadCommand.Exists() { + _, err = s.ReadCommand.Execute(s) + } else { + err = ErrUnsupportedServiceManagerType + } + return +} + +func (s *Service) Read(ctx context.Context) (resourceYaml []byte, err error) { + if s.ReadCommand.Exists() { + var out []byte + out, err = s.ReadCommand.Execute(s) + if err == nil { + err = s.ReadCommand.Extractor(out, s) + } else { + err = fmt.Errorf("%w - %w", ErrResourceStateAbsent, err) + } + slog.Info("Service.Read()", "service", s, "error", err) + } else { + err = ErrUnsupportedServiceManagerType + } + var yamlErr error + resourceYaml, yamlErr = yaml.Marshal(s) + if err == nil { + err = yamlErr + } + return } func (s *Service) Update(ctx context.Context) error { @@ -202,22 +306,22 @@ func (s *Service) ResolveId(ctx context.Context) string { return "" } -func NewSystemdCreateCommand() *Command { - c := NewCommand() +func NewSystemdCreateCommand() *command.Command { + c := command.NewCommand() c.Path = "systemctl" - c.Args = []CommandArg{ - CommandArg("enable"), - CommandArg("{{ .Name }}"), + c.Args = []command.CommandArg{ + command.CommandArg("enable"), + command.CommandArg("{{ .Name }}"), } return c } -func NewSystemdReadCommand() *Command { - c := NewCommand() +func NewSystemdReadCommand() *command.Command { + c := command.NewCommand() c.Path = "systemctl" - c.Args = []CommandArg{ - CommandArg("show"), - CommandArg("{{ .Name }}"), + c.Args = []command.CommandArg{ + command.CommandArg("show"), + command.CommandArg("{{ .Name }}"), } c.Extractor = func(out []byte, target any) error { s := target.(*Service) @@ -253,26 +357,63 @@ func NewSystemdReadCommand() *Command { return c } -func NewSystemdUpdateCommand() *Command { +func NewSystemdUpdateCommand() *command.Command { return nil } -func NewSystemdDeleteCommand() *Command { +func NewSystemdDeleteCommand() *command.Command { return nil } -func NewSysVCreateCommand() *Command { +func NewSysVCreateCommand() *command.Command { return nil } -func NewSysVReadCommand() *Command { +func NewSysVReadCommand() *command.Command { + c := command.NewCommand() + c.Path = "service" + c.Args = []command.CommandArg{ + command.CommandArg("status"), + command.CommandArg("{{ .Name }}"), + } + c.Extractor = func(out []byte, target any) (err error) { + s := target.(*Service) + /* + serviceStatus := strings.Split(string(out), "\n") + for _, statusLine := range(serviceStatus) { + if len(statusLine) > 1 { + statusFields := strings.Fields(statusLine) + } + } + */ + if len(out) < 1 { + if err = s.stater.Trigger("notexist"); err != nil { + return + } + } + switch c.ExitCode { + case SysVStatusRunning: + if err = s.stater.Trigger("running"); err != nil { + return + } + case SysVStatusStopped: + if err = s.stater.Trigger("created"); err != nil { + return + } + case SysVStatusUnknown, SysVStatusMissing: + if err = s.stater.Trigger("notexist"); err != nil { + return + } + } + return + } return nil } -func NewSysVUpdateCommand() *Command { +func NewSysVUpdateCommand() *command.Command { return nil } -func NewSysVDeleteCommand() *Command { +func NewSysVDeleteCommand() *command.Command { return nil } diff --git a/internal/resource/service_test.go b/internal/resource/service_test.go index a854f12..7e6c6b7 100644 --- a/internal/resource/service_test.go +++ b/internal/resource/service_test.go @@ -5,6 +5,7 @@ package resource import ( "context" _ "decl/tests/mocks" + "decl/internal/command" _ "fmt" "github.com/stretchr/testify/assert" "testing" @@ -22,15 +23,28 @@ func TestUriServiceResource(t *testing.T) { } func TestReadServiceResource(t *testing.T) { + + s := NewService() + s.Name = "ssh" + yamlResult := ` name: "ssh" servicemanager: "systemd" state: "present" ` - c := NewService() - c.Name = "ssh" - c.State = "present" - yamlData, err := c.Read(context.Background()) + m := &MockCommand{ + CommandExists: func() error { return nil }, + Executor: func(value any) ([]byte, error) { + return nil, nil + }, + Extractor: func(output []byte, target any) error { + s.Common.State = "present" + return nil + }, + } + + s.ReadCommand = (*command.Command)(m) + yamlData, err := s.Read(context.Background()) assert.Nil(t, err) assert.YAMLEq(t, yamlResult, string(yamlData)) } diff --git a/internal/resource/user.go b/internal/resource/user.go index 67ce7ed..206daf0 100644 --- a/internal/resource/user.go +++ b/internal/resource/user.go @@ -28,13 +28,15 @@ type UserType string const ( UserTypeName TypeName = "user" - UserTypeAddUser UserType = "adduser" - UserTypeUserAdd UserType = "useradd" + UserTypeBusyBox UserType = "busybox" + UserTypeShadow UserType = "shadow" ) var ErrUnsupportedUserType error = errors.New("The UserType is not supported on this system") var ErrInvalidUserType error = errors.New("invalid UserType value") +var SupportedUserTypes []UserType = []UserType{UserTypeShadow, UserTypeBusyBox} + var SystemUserType UserType = FindSystemUserType() type User struct { @@ -44,11 +46,15 @@ type User struct { UID string `json:"uid,omitempty" yaml:"uid,omitempty"` Group string `json:"group,omitempty" yaml:"group,omitempty"` Groups []string `json:"groups,omitempty" yaml:"groups,omitempty"` + AppendGroups bool `json:"appendgroups,omitempty" yaml:"appendgroups,omitempty"` Gecos string `json:"gecos,omitempty" yaml:"gecos,omitempty"` Home string `json:"home" yaml:"home"` CreateHome bool `json:"createhome,omitempty" yaml:"createhome,omitempty"` Shell string `json:"shell,omitempty" yaml:"shell,omitempty"` - UserType UserType `json:"-" yaml:"-"` + UserType UserType `json:"type,omitempty" yaml:"type,omitempty"` + + userStatus *user.User `json:"-" yaml:"-"` + groupStatus *user.Group `json:"-" yaml:"-"` CreateCommand *command.Command `json:"-" yaml:"-"` ReadCommand *command.Command `json:"-" yaml:"-"` @@ -59,7 +65,10 @@ type User struct { } func NewUser() *User { - return &User{ CreateHome: true, Common: &Common{ resourceType: UserTypeName } } + u := &User{ CreateHome: true, AppendGroups: true, UserType: SystemUserType } + u.Common = NewCommon(UserTypeName, false) + u.Common.NormalizePath = u.NormalizePath + return u } func init() { @@ -67,25 +76,23 @@ func init() { user := NewUser() user.Name = u.Hostname() user.UID = LookupUIDString(u.Hostname()) - if _, addUserPathErr := exec.LookPath("adduser"); addUserPathErr == nil { - user.UserType = UserTypeAddUser - } - if _, pathErr := exec.LookPath("useradd"); pathErr == nil { - user.UserType = UserTypeUserAdd - } user.CreateCommand, user.ReadCommand, user.UpdateCommand, user.DeleteCommand = user.UserType.NewCRUD() return user }) } func FindSystemUserType() UserType { - for _, userType := range []UserType{UserTypeAddUser, UserTypeUserAdd} { + for _, userType := range SupportedUserTypes { c := userType.NewCreateCommand() if c.Exists() { return userType } } - return UserTypeAddUser + return UserTypeShadow +} + +func (u *User) NormalizePath() error { + return nil } func (u *User) SetResourceMapper(resources data.ResourceMapper) { @@ -121,6 +128,16 @@ func (u *User) Notify(m *machine.EventMessage) { switch m.On { case machine.ENTERSTATEEVENT: switch m.Dest { + case "start_stat": + if statErr := u.ReadStat(); statErr == nil { + if triggerErr := u.StateMachine().Trigger("exists"); triggerErr == nil { + return + } + } else { + if triggerErr := u.StateMachine().Trigger("notexists"); triggerErr == nil { + return + } + } case "start_read": if _,readErr := u.Read(ctx); readErr == nil { if triggerErr := u.StateMachine().Trigger("state_read"); triggerErr == nil { @@ -133,6 +150,17 @@ func (u *User) Notify(m *machine.EventMessage) { u.Common.State = "absent" panic(readErr) } + case "start_update": + if updateErr := u.Update(ctx); updateErr == nil { + if triggerErr := u.stater.Trigger("updated"); triggerErr == nil { + return + } else { + u.Common.State = "absent" + } + } else { + u.Common.State = "absent" + panic(updateErr) + } case "start_delete": if deleteErr := u.Delete(ctx); deleteErr == nil { if triggerErr := u.StateMachine().Trigger("deleted"); triggerErr == nil { @@ -183,7 +211,7 @@ func (u *User) UseConfig(config data.ConfigurationValueGetter) { func (u *User) ResolveId(ctx context.Context) string { if u.config != nil { - if configUser, configUserErr := u.config.GetValue("user"); configUserErr == nil && u.Name == "" { + if configUser, configUserErr := u.config.GetValue("user"); configUserErr == nil && u.Name == "self" { u.Name = configUser.(string) } } @@ -241,20 +269,74 @@ func (u *User) Create(ctx context.Context) (error) { return e } +func (u *User) ReadStat() (err error) { + if u.userStatus == nil { + if u.userStatus, err = user.Lookup(u.Name); err != nil { + return err + } + } + if len(u.userStatus.Uid) < 1 { + return ErrResourceStateAbsent + } + u.UID = u.userStatus.Uid + if len(u.Group) > 1 { + if u.groupStatus == nil { + if u.groupStatus, err = user.LookupGroup(u.Group); err != nil { + return err + } + } + if len(u.groupStatus.Gid) < 1 { + return ErrResourceStateAbsent + } + } + return +} + func (u *User) Read(ctx context.Context) ([]byte, error) { exErr := u.ReadCommand.Extractor(nil, u) if exErr != nil { u.Common.State = "absent" } - if yaml, yamlErr := yaml.Marshal(u); yamlErr != nil { - return yaml, yamlErr + _ = u.ReadGroups() + if yamlDoc, yamlErr := yaml.Marshal(u); yamlErr != nil { + return yamlDoc, yamlErr } else { - return yaml, exErr + return yamlDoc, exErr } } +func (u *User) ReadGroups() (err error) { + knownSecondaryGroups := make(map[string]bool) + for _, secondaryGroupName := range u.Groups { + knownSecondaryGroups[secondaryGroupName] = true + } + if u.ReadStat() == nil { + if groups, groupsErr := u.userStatus.GroupIds(); groupsErr == nil { + for _, secondaryGroup := range groups { + if readGroup, groupErr := user.LookupGroupId(secondaryGroup); groupErr == nil { + if ! knownSecondaryGroups[readGroup.Name] { + u.Groups = append(u.Groups, readGroup.Name) + knownSecondaryGroups[readGroup.Name] = true + } + } else { + err = groupErr + } + } + } else { + err = groupsErr + } + } + return +} + func (u *User) Update(ctx context.Context) (error) { - return u.Create(ctx) + _, err := u.UpdateCommand.Execute(u) + if err != nil { + return err + } + + _,e := u.Read(ctx) + return e } func (u *User) Delete(ctx context.Context) (error) { @@ -284,38 +366,38 @@ func (u *User) UnmarshalYAML(value *yaml.Node) error { func (u *UserType) NewCRUD() (create *command.Command, read *command.Command, update *command.Command, del *command.Command) { switch *u { - case UserTypeUserAdd: - return NewUserAddCreateCommand(), NewUserReadCommand(), NewUserUpdateCommand(), NewUserDelDeleteCommand() - case UserTypeAddUser: - return NewAddUserCreateCommand(), NewUserReadCommand(), NewUserUpdateCommand(), NewDelUserDeleteCommand() + case UserTypeShadow: + return NewUserShadowCreateCommand(), NewUserReadCommand(), NewUserShadowUpdateCommand(), NewUserShadowDeleteCommand() + case UserTypeBusyBox: + return NewUserBusyBoxCreateCommand(), NewUserReadCommand(), NewUserBusyBoxUpdateCommand(), NewUserBusyBoxDeleteCommand() default: if _, addUserPathErr := exec.LookPath("adduser"); addUserPathErr == nil { - *u = UserTypeAddUser - return NewAddUserCreateCommand(), NewUserReadCommand(), NewUserUpdateCommand(), NewDelUserDeleteCommand() + *u = UserTypeBusyBox + return NewUserBusyBoxCreateCommand(), NewUserReadCommand(), NewUserBusyBoxUpdateCommand(), NewUserBusyBoxDeleteCommand() } if _, pathErr := exec.LookPath("useradd"); pathErr == nil { - *u = UserTypeUserAdd - return NewUserAddCreateCommand(), NewUserReadCommand(), NewUserUpdateCommand(), NewUserDelDeleteCommand() + *u = UserTypeShadow + return NewUserShadowCreateCommand(), NewUserReadCommand(), NewUserShadowUpdateCommand(), NewUserShadowDeleteCommand() } - return NewUserAddCreateCommand(), NewUserReadCommand(), NewUserUpdateCommand(), NewUserDelDeleteCommand() + return NewUserShadowCreateCommand(), NewUserReadCommand(), NewUserShadowUpdateCommand(), NewUserShadowDeleteCommand() } } +func (u *UserType) NewReadCommand() (read *command.Command) { + return NewUserReadCommand() +} + func (u *UserType) NewCreateCommand() (create *command.Command) { switch *u { - case UserTypeUserAdd: - return NewUserAddCreateCommand() - case UserTypeAddUser: - return NewAddUserCreateCommand() + case UserTypeShadow: + return NewUserShadowCreateCommand() + case UserTypeBusyBox: + return NewUserBusyBoxCreateCommand() default: } return nil } -func (u *UserType) NewReadCommand() (*command.Command) { - return NewUserReadCommand() -} - func (p *UserType) NewReadUsersCommand() (*command.Command) { return NewReadUsersCommand() } @@ -323,7 +405,7 @@ func (p *UserType) NewReadUsersCommand() (*command.Command) { func (u *UserType) UnmarshalValue(value string) error { switch value { - case string(UserTypeUserAdd), string(UserTypeAddUser): + case string(UserTypeShadow), string(UserTypeBusyBox): *u = UserType(value) return nil default: @@ -390,7 +472,7 @@ func NewReadUsersCommand() *command.Command { return c } -func NewUserAddCreateCommand() *command.Command { +func NewUserShadowCreateCommand() *command.Command { c := command.NewCommand() c.Path = "useradd" c.Args = []command.CommandArg{ @@ -404,19 +486,11 @@ func NewUserAddCreateCommand() *command.Command { } 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 NewAddUserCreateCommand() *command.Command { +func NewUserBusyBoxCreateCommand() *command.Command { c := command.NewCommand() c.Path = "adduser" c.Args = []command.CommandArg{ @@ -430,14 +504,6 @@ func NewAddUserCreateCommand() *command.Command { } 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 } @@ -476,11 +542,40 @@ func NewUserReadCommand() *command.Command { return c } -func NewUserUpdateCommand() *command.Command { - return nil +func NewUserBusyBoxUpdateCommand() *command.Command { + c := command.NewCommand() + c.Path = "xargs" + c.StdinAvailable = true + c.Input = command.CommandInput("{{ if .Groups }}{{ range .Groups }}{{ . }}\n{{ end }}{{ end }}") + c.Args = []command.CommandArg{ + command.CommandArg("adduser"), + command.CommandArg("{{ .Name }}"), + } + c.Extractor = func(out []byte, target any) error { + return nil + } + return c } -func NewUserDelDeleteCommand() *command.Command { +func NewUserShadowUpdateCommand() *command.Command { + c := command.NewCommand() + c.Path = "usermod" + c.Args = []command.CommandArg{ + command.CommandArg("{{ if .UID }}-u {{ .UID }}{{ end }}"), + command.CommandArg("{{ if .Gecos }}-c {{ .Gecos }}{{ end }}"), + command.CommandArg("{{ if .Group }}-g {{ .groupStatus.Gid }}{{ end }}"), + command.CommandArg("{{ if .Home }}-d {{ .Home }}{{ if .CreateHome }} -m{{- end }}{{ end }}"), + command.CommandArg("{{ if .Groups }}-G {{- range $i, $g := .Groups -}}{{ if $i }}, {{- end }}{{ . }}{{- end }}{{ if .AppendGroups }} -a{{- end }}{{- end }}"), + command.CommandArg("{{ if .Shell }}-s {{ .Shell }}{{ end }}"), + command.CommandArg("{{ .Name }}"), + } + c.Extractor = func(out []byte, target any) error { + return nil + } + return c +} + +func NewUserShadowDeleteCommand() *command.Command { c := command.NewCommand() c.Path = "userdel" c.Args = []command.CommandArg{ @@ -492,7 +587,7 @@ func NewUserDelDeleteCommand() *command.Command { return c } -func NewDelUserDeleteCommand() *command.Command { +func NewUserBusyBoxDeleteCommand() *command.Command { c := command.NewCommand() c.Path = "deluser" c.Args = []command.CommandArg{ diff --git a/internal/resource/user_test.go b/internal/resource/user_test.go index 39930d9..f120c06 100644 --- a/internal/resource/user_test.go +++ b/internal/resource/user_test.go @@ -81,7 +81,7 @@ func TestCreateUser(t *testing.T) { func TestSystemUser(t *testing.T) { u := NewUser() - + u.Name = "self" u.UseConfig(MockConfigValueGetter(func(key string) (any, error) { switch key { case "user": @@ -97,3 +97,22 @@ func TestSystemUser(t *testing.T) { u.ResolveId(context.Background()) assert.Equal(t, "bar", u.Name) } + +func TestUserSecondaryGroups(t *testing.T) { + groupCounts := make(map[string]int) + u := NewUser() + u.Name = "root" + u.ResolveId(context.Background()) + u.Groups = []string{ + "root", + "wheel", + } + u.ReadGroups() + + for _, groupName := range u.Groups { + groupCounts[groupName]++ + } + assert.Greater(t, len(u.Groups), 2) + assert.Equal(t, 1, groupCounts["root"]) + assert.Equal(t, 1, groupCounts["wheel"]) +}