// Copyright 2024 Matthew Rich . All rights reserved. package resource import ( "context" "encoding/json" "errors" "fmt" "gopkg.in/yaml.v3" "io" "log/slog" "net/url" _ "os" _ "os/exec" "path/filepath" "strings" "gitea.rosskeen.house/rosskeen.house/machine" ) type PackageType string const ( PackageTypeApk PackageType = "apk" PackageTypeApt PackageType = "apt" PackageTypeDeb PackageType = "deb" PackageTypeDnf PackageType = "dnf" PackageTypeRpm PackageType = "rpm" PackageTypePip PackageType = "pip" PackageTypeYum PackageType = "yum" ) type Package struct { stater machine.Stater `yaml:"-" json:"-"` Name string `yaml:"name" json:"name"` Required string `json:"required,omitempty" yaml:"required,omitempty"` Version string `yaml:"version,omitempty" json:"version,omitempty"` PackageType PackageType `yaml:"type" json:"type"` CreateCommand *Command `yaml:"-" json:"-"` ReadCommand *Command `yaml:"-" json:"-"` UpdateCommand *Command `yaml:"-" json:"-"` DeleteCommand *Command `yaml:"-" json:"-"` // state attributes State string `yaml:"state,omitempty" json:"state,omitempty"` } func init() { ResourceTypes.Register("package", func(u *url.URL) Resource { p := NewPackage() return p }) ResourceTypes.Register(string(PackageTypeApk), func(u *url.URL) Resource { p := NewPackage() return p }) ResourceTypes.Register(string(PackageTypeApt), func(u *url.URL) Resource { p := NewPackage() return p }) ResourceTypes.Register(string(PackageTypeDeb), func(u *url.URL) Resource { p := NewPackage() return p }) ResourceTypes.Register(string(PackageTypeDnf), func(u *url.URL) Resource { p := NewPackage() return p }) ResourceTypes.Register(string(PackageTypeRpm), func(u *url.URL) Resource { p := NewPackage() return p }) ResourceTypes.Register(string(PackageTypePip), func(u *url.URL) Resource { p := NewPackage() return p }) ResourceTypes.Register(string(PackageTypeYum), func(u *url.URL) Resource { p := NewPackage() return p }) } func NewPackage() *Package { return &Package{ PackageType: PackageTypeApk } } func (p *Package) Clone() Resource { newp := &Package { Name: p.Name, Required: p.Required, Version: p.Version, PackageType: p.PackageType, State: p.State, } newp.CreateCommand, newp.ReadCommand, newp.UpdateCommand, newp.DeleteCommand = newp.PackageType.NewCRUD() return newp } func (p *Package) StateMachine() machine.Stater { if p.stater == nil { p.stater = StorageMachine(p) } return p.stater } func (p *Package) Notify(m *machine.EventMessage) { ctx := context.Background() switch m.On { case machine.ENTERSTATEEVENT: switch m.Dest { case "start_create": if e := p.Create(ctx); e != nil { if triggerErr := p.stater.Trigger("created"); triggerErr != nil { // transition error } } case "present": p.State = "present" } case machine.EXITSTATEEVENT: } } func (p *Package) URI() string { return fmt.Sprintf("package://%s?version=%s&type=%s", p.Name, p.Version, p.PackageType) } func (p *Package) SetURI(uri string) error { resourceUri, e := url.Parse(uri) if e == nil { if resourceUri.Scheme == "package" { p.Name = filepath.Join(resourceUri.Hostname(), resourceUri.Path) p.Version = resourceUri.Query().Get("version") if p.Version == "" { p.Version = "latest" } p.PackageType = PackageType(resourceUri.Query().Get("type")) if p.PackageType == "" { e = fmt.Errorf("%w: %s is not a package known resource ", ErrInvalidResourceURI, uri) } } else { e = fmt.Errorf("%w: %s is not a package resource ", ErrInvalidResourceURI, uri) } } return e } func (p *Package) JSON() ([]byte, error) { return json.Marshal(p) } func (p *Package) Validate() error { s := NewSchema(p.Type()) jsonDoc, jsonErr := p.JSON() if jsonErr == nil { return s.Validate(string(jsonDoc)) } return jsonErr } func (p *Package) ResolveId(ctx context.Context) string { return "" } func (p *Package) Create(ctx context.Context) error { if p.Version == "latest" { p.Version = "" } _, err := p.CreateCommand.Execute(p) if err != nil { return err } _,e := p.Read(ctx) return e } func (p *Package) Apply() error { if p.Version == "latest" { p.Version = "" } _, err := p.CreateCommand.Execute(p) if err != nil { return err } _,e := p.Read(context.Background()) return e } func (p *Package) Load(r io.Reader) error { c := NewYAMLDecoder(r) return c.Decode(p) } func (p *Package) LoadDecl(yamlResourceDeclaration string) error { c := NewYAMLStringDecoder(yamlResourceDeclaration) return c.Decode(p) } func (p *Package) Type() string { return "package" } func (p *Package) Read(ctx context.Context) ([]byte, error) { out, err := p.ReadCommand.Execute(p) if err != nil { return nil, err } exErr := p.ReadCommand.Extractor(out, p) if exErr != nil { return nil, exErr } return yaml.Marshal(p) } func (p *Package) UnmarshalJSON(data []byte) error { if unmarshalErr := json.Unmarshal(data, p); unmarshalErr != nil { return unmarshalErr } p.CreateCommand, p.ReadCommand, p.UpdateCommand, p.DeleteCommand = p.PackageType.NewCRUD() return nil } func (p *Package) UnmarshalYAML(value *yaml.Node) error { type decodePackage Package if unmarshalErr := value.Decode((*decodePackage)(p)); unmarshalErr != nil { return unmarshalErr } p.CreateCommand, p.ReadCommand, p.UpdateCommand, p.DeleteCommand = p.PackageType.NewCRUD() return nil } func (p *PackageType) NewCRUD() (create *Command, read *Command, update *Command, del *Command) { switch *p { case PackageTypeApk: return NewApkCreateCommand(), NewApkReadCommand(), NewApkUpdateCommand(), NewApkDeleteCommand() case PackageTypeApt: return NewAptCreateCommand(), NewAptReadCommand(), NewAptUpdateCommand(), NewAptDeleteCommand() case PackageTypeDeb: case PackageTypeDnf: case PackageTypeRpm: case PackageTypePip: case PackageTypeYum: default: } return nil, nil, nil, nil } func (p *PackageType) UnmarshalValue(value string) error { switch value { case string(PackageTypeApk), string(PackageTypeApt), string(PackageTypeDeb), string(PackageTypeDnf), string(PackageTypeRpm), string(PackageTypePip), string(PackageTypeYum): *p = PackageType(value) return nil default: return errors.New("invalid PackageType value") } } func (p *PackageType) UnmarshalJSON(data []byte) error { var s string if unmarshalRouteTypeErr := json.Unmarshal(data, &s); unmarshalRouteTypeErr != nil { return unmarshalRouteTypeErr } return p.UnmarshalValue(s) } func (p *PackageType) UnmarshalYAML(value *yaml.Node) error { var s string if err := value.Decode(&s); err != nil { return err } return p.UnmarshalValue(s) } func NewApkCreateCommand() *Command { c := NewCommand() c.Path = "apk" c.Args = []CommandArg{ CommandArg("add"), CommandArg("{{ .Name }}{{ .Required }}"), } return c } func NewApkReadCommand() *Command { c := NewCommand() c.Path = "apk" c.Args = []CommandArg{ CommandArg("info"), CommandArg("-ev"), CommandArg("{{ .Name }}"), } c.Extractor = func(out []byte, target any) error { p := target.(*Package) pkg := strings.Split(string(out), "-") if pkg[0] == p.Name { p.Name = pkg[0] p.Version = pkg[1] p.State = "present" } else { p.State = "absent" } return nil } return c } func NewApkUpdateCommand() *Command { c := NewCommand() c.Path = "apk" c.Args = []CommandArg{ CommandArg("del"), CommandArg("{{ .Name }}"), } return c } func NewApkDeleteCommand() *Command { c := NewCommand() c.Path = "apk" c.Args = []CommandArg{ CommandArg("del"), CommandArg("{{ .Name }}"), } return c } func NewAptCreateCommand() *Command { c := NewCommand() c.Path = "apt-get" c.Split = false c.Args = []CommandArg{ CommandArg("satisfy"), CommandArg("-y"), CommandArg("{{ .Name }} ({{ if .Required }}{{ .Required }}{{ else }}>=0.0.0{{ end }})"), } return c } func NewAptReadCommand() *Command { c := NewCommand() c.Path = "dpkg" c.Args = []CommandArg{ CommandArg("-s"), CommandArg("{{ .Name }}"), } c.Extractor = func(out []byte, target any) error { p := target.(*Package) slog.Info("Extract()", "out", out) pkginfo := strings.Split(string(out), "\n") for _, infofield := range pkginfo { if len(infofield) > 0 && infofield[0] != ' ' { fieldKeyValue := strings.SplitN(infofield, ":", 2) if len(fieldKeyValue) > 1 { key := strings.TrimSpace(fieldKeyValue[0]) value := strings.TrimSpace(fieldKeyValue[1]) switch key { case "Package": if value != p.Name { p.State = "absent" return nil } case "Status": statusFields := strings.SplitN(value, " ", 3) if len(statusFields) > 1 { if statusFields[2] == "installed" { p.State = "present" } else { p.State = "absent" } } case "Version": p.Version = value } } } } slog.Info("Extract()", "package", p) return nil } return c } func NewAptUpdateCommand() *Command { c := NewCommand() c.Path = "apt" c.Args = []CommandArg{ CommandArg("install"), CommandArg("{{ .Name }}"), } return c } func NewAptDeleteCommand() *Command { c := NewCommand() c.Path = "apt" c.Args = []CommandArg{ CommandArg("remove"), CommandArg("{{ .Name }}"), } return c }