jx/internal/resource/package.go
Matthew Rich a6ea2e8c8c
Some checks failed
Lint / golangci-lint (push) Failing after 9m49s
Declarative Tests / test (push) Failing after 1m15s
add cli sub commands
2024-04-19 00:52:10 -07:00

360 lines
8.2 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"
)
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 {
Name string `yaml:"name" json:"name"`
Required string `json:"required" yaml:"required"`
Version string `yaml:"version" json:"version"`
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" json:"state"`
}
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{}
}
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) 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) 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:
}
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.Args = []CommandArg{
CommandArg("satisfy"),
CommandArg("-y"),
CommandArg("{{ .Name }} ({{ .Required }})"),
}
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
}