// 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" ) 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) 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 = "present" } else { p.State = "absent" return nil } case "Version": p.Version = value } } } } 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 }