// 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" "encoding/json" "strings" ) type ServiceManagerType string const ( ServiceManagerTypeSystemd ServiceManagerType = "systemd" ServiceManagerTypeSysV ServiceManagerType = "sysv" ) type Service struct { 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:"-"` State string `yaml:"state,omitempty" json:"state,omitempty"` config ConfigurationValueGetter Resources ResourceMapper `yaml:"-" json:"-"` } func init() { ResourceTypes.Register([]string{"service"}, func(u *url.URL) Resource { s := NewService() s.Name = filepath.Join(u.Hostname(), u.Path) s.CreateCommand, s.ReadCommand, s.UpdateCommand, s.DeleteCommand = s.ServiceManagerType.NewCRUD() return s }) } func NewService() *Service { return &Service{ ServiceManagerType: ServiceManagerTypeSystemd } } 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_create": if e := s.Create(ctx); e == nil { if triggerErr := s.stater.Trigger("created"); triggerErr == nil { return } } s.State = "absent" case "created": s.State = "present" case "running": s.State = "running" } case machine.EXITSTATEEVENT: } } 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()) } } return e } func (s *Service) UseConfig(config ConfigurationValueGetter) { s.config = config } func (s *Service) JSON() ([]byte, error) { return json.Marshal(s) } func (s *Service) Validate() error { return nil } func (s *Service) SetResourceMapper(resources ResourceMapper) { s.Resources = resources } func (s *Service) Clone() Resource { news := &Service{ 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(r io.Reader) error { return codec.NewYAMLDecoder(r).Decode(s) } func (s *Service) LoadDecl(yamlResourceDeclaration string) error { return codec.NewYAMLStringDecoder(yamlResourceDeclaration).Decode(s) } 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, read *Command, update *Command, del *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 *Service) Create(ctx context.Context) error { return nil } func (s *Service) Read(ctx context.Context) ([]byte, error) { return yaml.Marshal(s) } 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 { c := NewCommand() c.Path = "systemctl" c.Args = []CommandArg{ CommandArg("enable"), CommandArg("{{ .Name }}"), } return c } func NewSystemdReadCommand() *Command { c := NewCommand() c.Path = "systemctl" c.Args = []CommandArg{ CommandArg("show"), 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 { return nil } func NewSystemdDeleteCommand() *Command { return nil } func NewSysVCreateCommand() *Command { return nil } func NewSysVReadCommand() *Command { return nil } func NewSysVUpdateCommand() *Command { return nil } func NewSysVDeleteCommand() *Command { return nil }