2024-07-01 21:54:18 +00:00
|
|
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
|
|
|
|
|
|
|
// Service resource
|
|
|
|
package resource
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"fmt"
|
2024-10-09 22:26:39 +00:00
|
|
|
"log/slog"
|
2024-07-01 21:54:18 +00:00
|
|
|
"net/url"
|
|
|
|
"path/filepath"
|
|
|
|
"io"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
|
|
"gitea.rosskeen.house/rosskeen.house/machine"
|
|
|
|
"decl/internal/codec"
|
2024-09-19 08:11:57 +00:00
|
|
|
"decl/internal/data"
|
2024-10-09 22:26:39 +00:00
|
|
|
"decl/internal/command"
|
2024-10-16 17:26:42 +00:00
|
|
|
"decl/internal/folio"
|
2024-07-01 21:54:18 +00:00
|
|
|
"encoding/json"
|
|
|
|
"strings"
|
2024-10-09 22:26:39 +00:00
|
|
|
"errors"
|
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
ErrUnsupportedServiceManagerType error = errors.New("Unsupported service manager")
|
2024-07-01 21:54:18 +00:00
|
|
|
)
|
|
|
|
|
2024-09-19 08:11:57 +00:00
|
|
|
const (
|
|
|
|
ServiceTypeName TypeName = "service"
|
|
|
|
)
|
|
|
|
|
2024-07-01 21:54:18 +00:00
|
|
|
type ServiceManagerType string
|
|
|
|
|
|
|
|
const (
|
|
|
|
ServiceManagerTypeSystemd ServiceManagerType = "systemd"
|
|
|
|
ServiceManagerTypeSysV ServiceManagerType = "sysv"
|
|
|
|
)
|
|
|
|
|
2024-10-09 22:26:39 +00:00
|
|
|
const (
|
|
|
|
SysVStatusRunning int = 0
|
|
|
|
SysVStatusStopped int = 1
|
|
|
|
SysVStatusUnknown int = 2
|
|
|
|
SysVStatusMissing int = 3
|
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
SupportedServiceManagerTypes []ServiceManagerType = []ServiceManagerType{ServiceManagerTypeSystemd, ServiceManagerTypeSysV}
|
|
|
|
SystemServiceManagerType ServiceManagerType = FindSystemServiceManagerType()
|
|
|
|
)
|
|
|
|
|
2024-07-01 21:54:18 +00:00
|
|
|
type Service struct {
|
2024-09-19 08:11:57 +00:00
|
|
|
*Common `yaml:",inline" json:",inline"`
|
2024-07-01 21:54:18 +00:00
|
|
|
stater machine.Stater `yaml:"-" json:"-"`
|
|
|
|
Name string `json:"name" yaml:"name"`
|
|
|
|
ServiceManagerType ServiceManagerType `json:"servicemanager,omitempty" yaml:"servicemanager,omitempty"`
|
|
|
|
|
2024-10-09 22:26:39 +00:00
|
|
|
CreateCommand *command.Command `yaml:"-" json:"-"`
|
|
|
|
ReadCommand *command.Command `yaml:"-" json:"-"`
|
|
|
|
UpdateCommand *command.Command `yaml:"-" json:"-"`
|
|
|
|
DeleteCommand *command.Command `yaml:"-" json:"-"`
|
2024-07-01 21:54:18 +00:00
|
|
|
|
2024-09-19 08:11:57 +00:00
|
|
|
Resources data.ResourceMapper `yaml:"-" json:"-"`
|
2024-07-01 21:54:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func init() {
|
2024-09-19 08:11:57 +00:00
|
|
|
ResourceTypes.Register([]string{"service"}, func(u *url.URL) data.Resource {
|
2024-07-01 21:54:18 +00:00
|
|
|
s := NewService()
|
2024-10-16 17:26:42 +00:00
|
|
|
if err := folio.CastParsedURI(u).ConstructResource(s); err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
2024-07-01 21:54:18 +00:00
|
|
|
return s
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2024-10-09 22:26:39 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2024-10-16 17:26:42 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2024-10-09 22:26:39 +00:00
|
|
|
func (s *Service) NormalizePath() error {
|
|
|
|
return nil
|
2024-07-01 21:54:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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 {
|
2024-10-09 22:26:39 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
2024-07-01 21:54:18 +00:00
|
|
|
case "start_create":
|
|
|
|
if e := s.Create(ctx); e == nil {
|
|
|
|
if triggerErr := s.stater.Trigger("created"); triggerErr == nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
2024-09-19 08:11:57 +00:00
|
|
|
s.Common.State = "absent"
|
2024-10-09 22:26:39 +00:00
|
|
|
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":
|
2024-09-19 08:11:57 +00:00
|
|
|
s.Common.State = "present"
|
2024-07-01 21:54:18 +00:00
|
|
|
case "running":
|
2024-09-19 08:11:57 +00:00
|
|
|
s.Common.State = "running"
|
2024-10-09 22:26:39 +00:00
|
|
|
case "absent":
|
|
|
|
s.Common.State = "absent"
|
2024-07-01 21:54:18 +00:00
|
|
|
}
|
|
|
|
case machine.EXITSTATEEVENT:
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Service) URI() string {
|
|
|
|
return fmt.Sprintf("service://%s", s.Name)
|
|
|
|
}
|
|
|
|
|
2024-10-16 17:26:42 +00:00
|
|
|
func (s *Service) SetParsedURI(uri data.URIParser) (err error) {
|
2024-10-09 22:26:39 +00:00
|
|
|
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 {
|
2024-10-16 17:26:42 +00:00
|
|
|
err = fmt.Errorf("%w: %s is not an iptable rule", ErrInvalidResourceURI, s.Common.URI())
|
2024-10-09 22:26:39 +00:00
|
|
|
}
|
|
|
|
return
|
2024-07-01 21:54:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Service) JSON() ([]byte, error) {
|
|
|
|
return json.Marshal(s)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Service) Validate() error {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2024-09-19 08:11:57 +00:00
|
|
|
func (s *Service) SetResourceMapper(resources data.ResourceMapper) {
|
2024-07-17 08:34:57 +00:00
|
|
|
s.Resources = resources
|
|
|
|
}
|
|
|
|
|
2024-09-19 08:11:57 +00:00
|
|
|
func (s *Service) Clone() data.Resource {
|
2024-07-01 21:54:18 +00:00
|
|
|
news := &Service{
|
2024-09-19 08:11:57 +00:00
|
|
|
Common: s.Common,
|
2024-07-01 21:54:18 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2024-09-19 08:11:57 +00:00
|
|
|
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
|
2024-07-01 21:54:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Service) LoadDecl(yamlResourceDeclaration string) error {
|
2024-09-19 08:11:57 +00:00
|
|
|
return s.LoadString(yamlResourceDeclaration, codec.FormatYaml)
|
2024-07-01 21:54:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2024-10-09 22:26:39 +00:00
|
|
|
func (s *ServiceManagerType) NewCRUD() (create *command.Command, read *command.Command, update *command.Command, del *command.Command) {
|
2024-07-01 21:54:18 +00:00
|
|
|
switch *s {
|
|
|
|
case ServiceManagerTypeSystemd:
|
|
|
|
return NewSystemdCreateCommand(), NewSystemdReadCommand(), NewSystemdUpdateCommand(), NewSystemdDeleteCommand()
|
|
|
|
case ServiceManagerTypeSysV:
|
|
|
|
return NewSysVCreateCommand(), NewSysVReadCommand(), NewSysVUpdateCommand(), NewSysVDeleteCommand()
|
|
|
|
default:
|
|
|
|
}
|
|
|
|
return nil, nil, nil, nil
|
|
|
|
}
|
|
|
|
|
2024-10-09 22:26:39 +00:00
|
|
|
func (s *ServiceManagerType) NewReadCommand() (read *command.Command) {
|
|
|
|
switch *s {
|
|
|
|
case ServiceManagerTypeSystemd:
|
|
|
|
return NewSystemdReadCommand()
|
|
|
|
case ServiceManagerTypeSysV:
|
|
|
|
return NewSysVReadCommand()
|
|
|
|
default:
|
|
|
|
}
|
2024-07-01 21:54:18 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2024-10-09 22:26:39 +00:00
|
|
|
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
|
2024-07-01 21:54:18 +00:00
|
|
|
}
|
|
|
|
|
2024-09-19 08:11:57 +00:00
|
|
|
func (s *Service) Update(ctx context.Context) error {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2024-07-01 21:54:18 +00:00
|
|
|
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 ""
|
|
|
|
}
|
|
|
|
|
2024-10-09 22:26:39 +00:00
|
|
|
func NewSystemdCreateCommand() *command.Command {
|
|
|
|
c := command.NewCommand()
|
2024-07-01 21:54:18 +00:00
|
|
|
c.Path = "systemctl"
|
2024-10-09 22:26:39 +00:00
|
|
|
c.Args = []command.CommandArg{
|
|
|
|
command.CommandArg("enable"),
|
|
|
|
command.CommandArg("{{ .Name }}"),
|
2024-07-01 21:54:18 +00:00
|
|
|
}
|
|
|
|
return c
|
|
|
|
}
|
|
|
|
|
2024-10-09 22:26:39 +00:00
|
|
|
func NewSystemdReadCommand() *command.Command {
|
|
|
|
c := command.NewCommand()
|
2024-07-01 21:54:18 +00:00
|
|
|
c.Path = "systemctl"
|
2024-10-09 22:26:39 +00:00
|
|
|
c.Args = []command.CommandArg{
|
|
|
|
command.CommandArg("show"),
|
|
|
|
command.CommandArg("{{ .Name }}"),
|
2024-07-01 21:54:18 +00:00
|
|
|
}
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2024-10-09 22:26:39 +00:00
|
|
|
func NewSystemdUpdateCommand() *command.Command {
|
2024-07-01 21:54:18 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2024-10-09 22:26:39 +00:00
|
|
|
func NewSystemdDeleteCommand() *command.Command {
|
2024-07-01 21:54:18 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2024-10-09 22:26:39 +00:00
|
|
|
func NewSysVCreateCommand() *command.Command {
|
2024-07-01 21:54:18 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2024-10-09 22:26:39 +00:00
|
|
|
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
|
|
|
|
}
|
2024-07-01 21:54:18 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2024-10-09 22:26:39 +00:00
|
|
|
func NewSysVUpdateCommand() *command.Command {
|
2024-07-01 21:54:18 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2024-10-09 22:26:39 +00:00
|
|
|
func NewSysVDeleteCommand() *command.Command {
|
2024-07-01 21:54:18 +00:00
|
|
|
return nil
|
|
|
|
}
|