// Copyright 2024 Matthew Rich . All rights reserved. // Service resource package resource import ( "context" "fmt" "log/slog" "net/url" "path/filepath" "io" "gopkg.in/yaml.v3" "gitea.rosskeen.house/rosskeen.house/machine" "decl/internal/codec" "decl/internal/data" "decl/internal/command" "decl/internal/folio" "encoding/json" "strings" "errors" ) var ( ErrUnsupportedServiceManagerType error = errors.New("Unsupported service manager") ) const ( ServiceTypeName TypeName = "service" ) type ServiceManagerType string const ( ServiceManagerTypeSystemd ServiceManagerType = "systemd" 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.Command `yaml:"-" json:"-"` ReadCommand *command.Command `yaml:"-" json:"-"` UpdateCommand *command.Command `yaml:"-" json:"-"` DeleteCommand *command.Command `yaml:"-" json:"-"` Resources data.ResourceMapper `yaml:"-" json:"-"` } func init() { ResourceTypes.Register([]string{"service"}, func(u *url.URL) data.Resource { s := NewService() if err := folio.CastParsedURI(u).ConstructResource(s); err != nil { panic(err) } return s }) } 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) Init(u data.URIParser) (err error) { if u == nil { u = folio.URI(s.URI()).Parse() } uri := u.URL() err = s.SetParsedURI(u) s.Name = filepath.Join(uri.Hostname(), uri.Path) 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 { if s.stater == nil { s.stater = ProcessMachine(s) } return s.stater } func (s *Service) Notify(m *machine.EventMessage) { ctx := context.Background() 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 { return } } s.Common.State = "absent" 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: } } func (s *Service) URI() string { return fmt.Sprintf("service://%s", s.Name) } func (s *Service) SetParsedURI(uri data.URIParser) (err error) { if err = s.Common.SetParsedURI(uri); err == nil { err = s.setFieldsFromPath() } return } 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) JSON() ([]byte, error) { return json.Marshal(s) } func (s *Service) Validate() error { return nil } func (s *Service) SetResourceMapper(resources data.ResourceMapper) { s.Resources = resources } func (s *Service) Clone() data.Resource { news := &Service{ Common: s.Common, Name: s.Name, ServiceManagerType: s.ServiceManagerType, } news.CreateCommand, news.ReadCommand, news.UpdateCommand, news.DeleteCommand = s.ServiceManagerType.NewCRUD() return news } func (s *Service) Apply() error { return nil } func (s *Service) Load(docData []byte, f codec.Format) (err error) { err = f.StringDecoder(string(docData)).Decode(s) return } func (s *Service) LoadReader(r io.ReadCloser, f codec.Format) (err error) { err = f.Decoder(r).Decode(s) return } func (s *Service) LoadString(docData string, f codec.Format) (err error) { err = f.StringDecoder(docData).Decode(s) return } func (s *Service) LoadDecl(yamlResourceDeclaration string) error { return s.LoadString(yamlResourceDeclaration, codec.FormatYaml) } func (s *Service) UnmarshalJSON(data []byte) error { if unmarshalErr := json.Unmarshal(data, s); unmarshalErr != nil { return unmarshalErr } s.CreateCommand, s.ReadCommand, s.UpdateCommand, s.DeleteCommand = s.ServiceManagerType.NewCRUD() return nil } func (s *Service) UnmarshalYAML(value *yaml.Node) error { type decodeService Service if unmarshalErr := value.Decode((*decodeService)(s)); unmarshalErr != nil { return unmarshalErr } s.CreateCommand, s.ReadCommand, s.UpdateCommand, s.DeleteCommand = s.ServiceManagerType.NewCRUD() return nil } 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() case ServiceManagerTypeSysV: return NewSysVCreateCommand(), NewSysVReadCommand(), NewSysVUpdateCommand(), NewSysVDeleteCommand() default: } return nil, nil, nil, nil } func (s *ServiceManagerType) NewReadCommand() (read *command.Command) { switch *s { case ServiceManagerTypeSystemd: return NewSystemdReadCommand() case ServiceManagerTypeSysV: return NewSysVReadCommand() default: } return nil } 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 { return nil } func (s *Service) Delete(ctx context.Context) error { return nil } func (s *Service) Type() string { return "service" } func (s *Service) ResolveId(ctx context.Context) string { return "" } func NewSystemdCreateCommand() *command.Command { c := command.NewCommand() c.Path = "systemctl" c.Args = []command.CommandArg{ command.CommandArg("enable"), command.CommandArg("{{ .Name }}"), } return c } func NewSystemdReadCommand() *command.Command { c := command.NewCommand() c.Path = "systemctl" c.Args = []command.CommandArg{ command.CommandArg("show"), command.CommandArg("{{ .Name }}"), } c.Extractor = func(out []byte, target any) error { s := target.(*Service) serviceStatus := strings.Split(string(out), "\n") for _, statusLine := range(serviceStatus) { if len(statusLine) > 1 { statusKeyValue := strings.Split(statusLine, "=") key := statusKeyValue[0] value := strings.TrimSpace(strings.Join(statusKeyValue[1:], "=")) switch key { case "Id": case "ActiveState": switch value { case "active": if stateCreatedErr := s.stater.Trigger("created"); stateCreatedErr != nil { return stateCreatedErr } case "inactive": } case "SubState": switch value { case "running": if stateRunningErr := s.stater.Trigger("running"); stateRunningErr != nil { return stateRunningErr } case "dead": } } } } return nil } return c } func NewSystemdUpdateCommand() *command.Command { return nil } func NewSystemdDeleteCommand() *command.Command { return nil } func NewSysVCreateCommand() *command.Command { return nil } 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.Command { return nil } func NewSysVDeleteCommand() *command.Command { return nil }