401 lines
9.1 KiB
Go
401 lines
9.1 KiB
Go
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. 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
|
|
}
|