// 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" "decl/internal/codec" "decl/internal/command" "decl/internal/data" "decl/internal/folio" "decl/internal/tempdir" ) const ( PackageTypeName TypeName = "package" ) var ( PackageTempDir tempdir.Path = "jx_package_resource" ) 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" ) var ( ErrUnsupportedPackageType error = errors.New("The PackageType is not supported on this system") ErrInvalidPackageType error = errors.New("invalid PackageType value") ErrRpmPackageInstalled error = errors.New("is already installed") ) var SupportedPackageTypes []PackageType = []PackageType{PackageTypeApk, PackageTypeApt, PackageTypeDeb, PackageTypeDnf, PackageTypeRpm, PackageTypePip, PackageTypeYum} var SystemPackageType PackageType = FindSystemPackageType() type Package struct { *Common `yaml:",inline" json:",inline"` stater machine.Stater `yaml:"-" json:"-"` Source string `yaml:"source,omitempty" json:"source,omitempty"` 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"` SourceRef folio.ResourceReference `yaml:"sourceref,omitempty" json:"sourceref,omitempty"` CreateCommand *command.Command `yaml:"-" json:"-"` ReadCommand *command.Command `yaml:"-" json:"-"` UpdateCommand *command.Command `yaml:"-" json:"-"` DeleteCommand *command.Command `yaml:"-" json:"-"` Resources data.ResourceMapper `yaml:"-" json:"-"` } func init() { folio.DocumentRegistry.ResourceTypes.Register([]string{"package", string(PackageTypeApk), string(PackageTypeApt), string(PackageTypeDeb), string(PackageTypeDnf), string(PackageTypeRpm), string(PackageTypePip), string(PackageTypeYum)}, func(u *url.URL) data.Resource { return ConstructNewPackage(u) }) } func FindSystemPackageType() PackageType { for _, packageType := range SupportedPackageTypes { c := packageType.NewReadCommand() if c.Exists() { return packageType } } return PackageTypeApk } func NewPackage() *Package { return &Package{ Common: NewCommon(PackageTypeName, true), PackageType: SystemPackageType, } } func ConstructNewPackage(uri *url.URL) (p *Package) { p = NewPackage() if uri != nil { if err := folio.CastParsedURI(uri).ConstructResource(p); err != nil { panic(err) } } return } func (p *Package) Init(u data.URIParser) error { if u == nil { u = folio.URI(p.URI()).Parse() } uri := u.URL() p.Name = filepath.Join(uri.Hostname(), uri.Path) p.Version = uri.Query().Get("version") if p.Version == "" { p.Version = "latest" } indicatedPackageType := PackageType(uri.Query().Get("type")) if indicatedPackageType.Validate() != nil { p.PackageType = SystemPackageType } p.CreateCommand, p.ReadCommand, p.UpdateCommand, p.DeleteCommand = p.PackageType.NewCRUD() return p.SetParsedURI(u) } func (p *Package) SetResourceMapper(resources data.ResourceMapper) { p.Resources = resources } func (p *Package) Clone() data.Resource { newp := &Package { Common: p.Common.Clone(), Name: p.Name, Required: p.Required, Version: p.Version, PackageType: p.PackageType, } 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() slog.Info("Notify()", "package", p, "m", m) switch m.On { case machine.ENTERSTATEEVENT: switch m.Dest { case "start_stat": if statErr := p.ReadStat(ctx); statErr == nil { if triggerErr := p.StateMachine().Trigger("exists"); triggerErr == nil { return } } else { if triggerErr := p.StateMachine().Trigger("notexists"); triggerErr == nil { return } } case "start_read": if _,readErr := p.Read(ctx); readErr == nil { if triggerErr := p.StateMachine().Trigger("state_read"); triggerErr == nil { return } else { p.Common.State = "absent" panic(triggerErr) } } else { p.Common.State = "absent" if ! errors.Is(readErr, ErrResourceStateAbsent) { panic(readErr) } } case "start_create": if e := p.Create(ctx); e == nil { if triggerErr := p.StateMachine().Trigger("created"); triggerErr == nil { return } } else { panic(e) } // p.State = "absent" case "start_update": if e := p.Update(ctx); e == nil { if triggerErr := p.StateMachine().Trigger("updated"); triggerErr == nil { return } } case "start_delete": if deleteErr := p.Delete(ctx); deleteErr == nil { if triggerErr := p.StateMachine().Trigger("deleted"); triggerErr == nil { return } else { p.Common.State = "present" panic(triggerErr) } } else { p.Common.State = "present" panic(deleteErr) } case "absent": p.Common.State = "absent" case "present", "created", "updated", "read": p.Common.State = "present" } case machine.EXITSTATEEVENT: } } func (p *Package) ReadStat(ctx context.Context) (err error) { if p.ReadCommand.Exists() { _, err = p.ReadCommand.Execute(p) } return } func (p *Package) URI() string { return fmt.Sprintf("package://%s?version=%s&type=%s", p.Name, url.QueryEscape(p.Version), p.PackageType) } func (p *Package) SetParsedURI(uri data.URIParser) (err error) { u := uri.URL() if u.Scheme == "package" { p.Name = filepath.Join(u.Hostname(), u.Path) p.Version = u.Query().Get("version") if p.Version == "" { p.Version = "latest" } indicatedPackageType := PackageType(u.Query().Get("type")) if indicatedPackageType.Validate() != nil { p.PackageType = SystemPackageType } } else { err = fmt.Errorf("%w: %s is not a package resource ", ErrInvalidResourceURI, u.String()) } p.CreateCommand, p.ReadCommand, p.UpdateCommand, p.DeleteCommand = p.PackageType.NewCRUD() return } 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 { slog.Info("Package.ResolveId()", "name", p.Name, "machine.state", p.StateMachine().CurrentState()) if p.ReadCommand.Exists() { if _, err := p.ReadCommand.Execute(p); err != nil { if triggerResult := p.StateMachine().Trigger("notexists"); triggerResult != nil { panic(fmt.Errorf("%w: %s %s", err, p.Type(), p.Name)) } } } return p.Name } func (p *Package) Create(ctx context.Context) (err error) { if p.Version == "latest" { p.Version = "" } slog.Info("Package.Create()") if source := p.SourceRef.Lookup(p.Resources); source != nil { r, _ := source.ContentReaderStream() if p.CreateCommand.StdinAvailable { p.CreateCommand.SetStdinReader(r) } else { if err = PackageTempDir.Create(); err != nil { return } defer PackageTempDir.Remove() defer func() { p.Source = "" }() if p.Source, err = PackageTempDir.CreateFileFromReader(fmt.Sprintf("%s.rpm", p.Name), r); err != nil { return } } } if _, err = p.CreateCommand.Execute(p); err != nil { slog.Info("Package.Create() ERROR", "error", err) msg := err.Error() lenMsg := len(msg) - 1 lenErr := len(ErrRpmPackageInstalled.Error()) if msg[lenMsg - lenErr:lenMsg] != ErrRpmPackageInstalled.Error() { return } } _, err = p.Read(ctx) slog.Info("Package.Create()", "package", p, "readerr", err) return } func (p *Package) Update(ctx context.Context) error { return p.Create(ctx) } func (p *Package) Delete(ctx context.Context) error { _, err := p.DeleteCommand.Execute(p) if err != nil { return err } _,e := p.Read(ctx) return e } func (p *Package) Apply() (err error) { if p.Version == "latest" { p.Version = "" } if _, err = p.CreateCommand.Execute(p); err != nil { return } _, err = p.Read(context.Background()) return } func (p *Package) Load(docData []byte, f codec.Format) (err error) { err = f.StringDecoder(string(docData)).Decode(p) return } func (p *Package) LoadReader(r io.ReadCloser, f codec.Format) (err error) { err = f.Decoder(r).Decode(p) return } func (p *Package) LoadString(docData string, f codec.Format) (err error) { err = f.StringDecoder(docData).Decode(p) return } func (p *Package) LoadDecl(yamlResourceDeclaration string) error { return p.LoadString(yamlResourceDeclaration, codec.FormatYaml) } func (p *Package) Type() string { return "package" } func (p *Package) Read(ctx context.Context) (resourceYaml []byte, err error) { if p.Version == "latest" { p.Version = "" } if p.ReadCommand.Exists() { var out []byte out, err = p.ReadCommand.Execute(p) if err == nil { err = p.ReadCommand.Extractor(out, p) } else { err = fmt.Errorf("%w - %w", ErrResourceStateAbsent, err) } slog.Info("Package.Read()", "package", p, "error", err) } else { err = ErrUnsupportedPackageType } var yamlErr error resourceYaml, yamlErr = yaml.Marshal(p) if err == nil { err = yamlErr } return } 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.Command, read *command.Command, update *command.Command, del *command.Command) { switch *p { case PackageTypeApk: return NewApkCreateCommand(), NewApkReadCommand(), NewApkUpdateCommand(), NewApkDeleteCommand() case PackageTypeApt: return NewAptCreateCommand(), NewAptReadCommand(), NewAptUpdateCommand(), NewAptDeleteCommand() case PackageTypeDeb: return NewDebCreateCommand(), NewDebReadCommand(), NewDebUpdateCommand(), NewDebDeleteCommand() case PackageTypeDnf: return NewDnfCreateCommand(), NewDnfReadCommand(), NewDnfUpdateCommand(), NewDnfDeleteCommand() case PackageTypeRpm: return NewRpmCreateCommand(), NewRpmReadCommand(), NewRpmUpdateCommand(), NewRpmDeleteCommand() case PackageTypePip: return NewPipCreateCommand(), NewPipReadCommand(), NewPipUpdateCommand(), NewPipDeleteCommand() case PackageTypeYum: return NewYumCreateCommand(), NewYumReadCommand(), NewYumUpdateCommand(), NewYumDeleteCommand() default: } return nil, nil, nil, nil } func (p *PackageType) NewReadCommand() (read *command.Command) { switch *p { case PackageTypeApk: return NewApkReadCommand() case PackageTypeApt: return NewAptReadCommand() case PackageTypeDeb: return NewDebReadCommand() case PackageTypeDnf: return NewDnfReadCommand() case PackageTypeRpm: return NewRpmReadCommand() case PackageTypePip: return NewPipReadCommand() case PackageTypeYum: return NewYumReadCommand() default: } return nil } func (p *PackageType) NewReadPackagesCommand() (read *command.Command) { switch *p { case PackageTypeApk: return NewApkReadPackagesCommand() case PackageTypeApt: return NewAptReadPackagesCommand() case PackageTypeDeb: return NewDebReadPackagesCommand() case PackageTypeDnf: return NewDnfReadPackagesCommand() case PackageTypeRpm: return NewRpmReadPackagesCommand() case PackageTypePip: return NewPipReadPackagesCommand() case PackageTypeYum: return NewYumReadPackagesCommand() default: } return nil } func (p PackageType) Validate() error { switch p { case PackageTypeApk, PackageTypeApt, PackageTypeDeb, PackageTypeDnf, PackageTypeRpm, PackageTypePip, PackageTypeYum: return nil default: return ErrInvalidPackageType } } func (p *PackageType) UnmarshalValue(value string) (err error) { if err = PackageType(value).Validate(); err == nil { *p = PackageType(value) } return } 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 (p PackageType) Exists() bool { return p.NewReadCommand().Exists() } func NewApkCreateCommand() *command.Command { c := command.NewCommand() c.Path = "apk" c.Args = []command.CommandArg{ command.CommandArg("add"), command.CommandArg("{{ .Name }}{{ .Required }}"), } return c } func NewApkReadCommand() *command.Command { c := command.NewCommand() c.Path = "apk" c.Args = []command.CommandArg{ command.CommandArg("info"), command.CommandArg("-ev"), command.CommandArg("{{ .Name }}"), } c.Extractor = func(out []byte, target any) error { p := target.(*Package) pkg := strings.Split(strings.TrimSpace(string(out)), "-") numberOfFields := len(pkg) packageName := strings.Join(pkg[0:numberOfFields - 2], "-") packageVersion := strings.Join(pkg[numberOfFields - 2:numberOfFields - 1], "-") if packageName == p.Name { p.Name = packageName p.Version = packageVersion p.Common.State = "present" } else { slog.Info("NewApkReadCommand().Extrctor() mismatch", "name", p.Name, "parsed", packageName) p.Common.State = "absent" } return nil } return c } func NewApkUpdateCommand() *command.Command { c := command.NewCommand() c.Path = "apk" c.Args = []command.CommandArg{ command.CommandArg("add"), command.CommandArg("{{ .Name }}{{ .Required }}"), } return c } func NewApkDeleteCommand() *command.Command { c := command.NewCommand() c.Path = "apk" c.Args = []command.CommandArg{ command.CommandArg("del"), command.CommandArg("{{ .Name }}"), } return c } func NewApkReadPackagesCommand() *command.Command { c := command.NewCommand() c.Path = "apk" c.Args = []command.CommandArg{ command.CommandArg("list"), command.CommandArg("--installed"), } c.Extractor = func(out []byte, target any) error { Packages := target.(*[]*Package) numberOfPackages := len(*Packages) lines := strings.Split(strings.TrimSpace(string(out)), "\n") numberOfLines := len(lines) diff := (numberOfLines) - numberOfPackages if diff > 0 { for i := 0; i < diff; i++ { *Packages = append(*Packages, NewPackage()) } } for lineIndex, line := range lines { p := (*Packages)[lineIndex] installedPackage := strings.Fields(strings.TrimSpace(line)) packageFields := strings.Split(installedPackage[0], "-") numberOfFields := len(packageFields) if numberOfFields > 2 { packageName := strings.Join(packageFields[0:numberOfFields - 2], "-") packageVersion := strings.Join(packageFields[numberOfFields - 2:numberOfFields - 1], "-") p.Name = packageName p.Common.State = "present" p.Version = packageVersion } } return nil } return c } func NewAptCreateCommand() *command.Command { c := command.NewCommand() c.Path = "apt-get" c.Split = false c.Args = []command.CommandArg{ command.CommandArg("satisfy"), command.CommandArg("-y"), command.CommandArg("{{ .Name }} ({{ if .Required }}{{ .Required }}{{ else }}>=0.0.0{{ end }})"), } return c } func NewAptReadCommand() *command.Command { c := command.NewCommand() c.Path = "dpkg" c.Args = []command.CommandArg{ command.CommandArg("-s"), command.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.Common.State = "absent" return nil } case "Status": statusFields := strings.SplitN(value, " ", 3) if len(statusFields) > 1 { if statusFields[2] == "installed" { p.Common.State = "present" } else { p.Common.State = "absent" } } case "Version": p.Version = value } } } } slog.Info("Extract()", "package", p) return nil } return c } func NewAptUpdateCommand() *command.Command { c := command.NewCommand() c.Path = "apt-get" c.Split = false c.Args = []command.CommandArg{ command.CommandArg("satisfy"), command.CommandArg("-y"), command.CommandArg("{{ .Name }} ({{ if .Required }}{{ .Required }}{{ else }}>=0.0.0{{ end }})"), } return c } func NewAptDeleteCommand() *command.Command { c := command.NewCommand() c.Path = "apt" c.FailOnError = false c.Args = []command.CommandArg{ command.CommandArg("remove"), command.CommandArg("{{ .Name }}"), } return c } func NewAptReadPackagesCommand() *command.Command { c := command.NewCommand() c.Path = "apt" c.FailOnError = false c.Args = []command.CommandArg{ command.CommandArg("list"), command.CommandArg("--installed"), } c.Env = []string{ "DEBIAN_FRONTEND=noninteractive" } c.Extractor = func(out []byte, target any) error { Packages := target.(*[]*Package) numberOfPackages := len(*Packages) lines := strings.Split(strings.TrimSpace(string(out)), "\n") numberOfLines := len(lines) diff := (numberOfLines - 1) - numberOfPackages if diff > 0 { for i := 0; i < diff; i++ { *Packages = append(*Packages, NewPackage()) } } for lineIndex, line := range lines[1:] { p := (*Packages)[lineIndex] installedPackage := strings.Fields(strings.TrimSpace(line)) packageFields := strings.Split(installedPackage[0], "/") packageName := packageFields[0] packageVersion := installedPackage[1] p.Name = packageName p.Common.State = "present" p.Version = packageVersion p.PackageType = PackageTypeApt } return nil } return c } func NewDebCreateCommand() *command.Command { c := command.NewCommand() c.Path = "dpkg" c.Split = false c.Args = []command.CommandArg{ command.CommandArg("-i"), command.CommandArg("{{ .Source }}"), } return c } func NewDebReadCommand() *command.Command { c := command.NewCommand() c.Path = "dpkg" c.Args = []command.CommandArg{ command.CommandArg("-s"), command.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.Common.State = "absent" return nil } case "Status": statusFields := strings.SplitN(value, " ", 3) if len(statusFields) > 1 { if statusFields[2] == "installed" { p.Common.State = "present" } else { p.Common.State = "absent" } } case "Version": p.Version = value } } } } slog.Info("Extract()", "package", p) return nil } return c } func NewDebUpdateCommand() *command.Command { c := command.NewCommand() c.Path = "dpkg" c.Args = []command.CommandArg{ command.CommandArg("-i"), command.CommandArg("{{ .Source }}"), } return c } func NewDebDeleteCommand() *command.Command { c := command.NewCommand() c.Path = "dpkg" c.Args = []command.CommandArg{ command.CommandArg("-r"), command.CommandArg("{{ .Name }}"), } return c } func NewDebReadPackagesCommand() *command.Command { c := command.NewCommand() c.Path = "dpkg-query" c.FailOnError = false c.Split = false c.Args = []command.CommandArg{ command.CommandArg("-W"), command.CommandArg("-f"), command.CommandArg("${binary:Package} ${Version} ${Status}\n"), } c.Env = []string{ "DEBIAN_FRONTEND=noninteractive" } c.Extractor = func(out []byte, target any) error { Packages := target.(*[]*Package) lines := strings.Split(strings.TrimSpace(string(out)), "\n") lineIndex := 0 for _, line := range lines { installedPackage := strings.Fields(strings.TrimSpace(line)) status := strings.Join(installedPackage[2:], " ") if status == "install ok installed" { if len(*Packages) <= lineIndex + 1 { *Packages = append(*Packages, NewPackage()) } p := (*Packages)[lineIndex] packageNameFields := strings.Split(installedPackage[0], ":") packageName := packageNameFields[0] packageVersionFields := strings.Split(installedPackage[1], ":") if len(packageVersionFields) > 1 { p.Version = packageVersionFields[1] } else { p.Version = packageVersionFields[0] } p.Name = packageName p.Common.State = "present" p.PackageType = PackageTypeDeb lineIndex++ } } return nil } return c } func NewDnfCreateCommand() *command.Command { c := command.NewCommand() c.Path = "dnf" c.Split = false c.Args = []command.CommandArg{ command.CommandArg("install"), command.CommandArg("-q"), command.CommandArg("-y"), command.CommandArg("{{ .Name }}{{ if .Required }}{{ .Required }}{{ else }} >= 0.0.0{{ end }}"), } return c } func NewDnfReadCommand() *command.Command { c := command.NewCommand() c.Path = "dnf" c.Args = []command.CommandArg{ command.CommandArg("-q"), command.CommandArg("list"), command.CommandArg("installed"), command.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 _, packageLines := range pkginfo[1:] { if len(packageLines) > 0 { fields := strings.Fields(packageLines) slog.Info("DnfReadCommaond.Extract()", "fields", fields, "package", p) packageNameField := strings.Split(fields[0], ".") lenName := len(packageNameField) packageName := strings.TrimSpace(strings.Join(packageNameField[0:lenName - 1], ".")) if packageName == p.Name { p.Common.State = "present" packageVersionField := strings.Split(fields[1], ":") if len(packageVersionField) > 1 { //packageEpoch := strings.TrimSpace(packageVersionField[0]) p.Version = strings.TrimSpace(packageVersionField[1]) } else { p.Version = strings.TrimSpace(packageVersionField[0]) } return nil } slog.Info("DnfReadCommaond.Extract()", "package", packageName, "package", p) } } p.Common.State = "absent" slog.Info("Extract()", "package", p) return nil } return c } func NewDnfUpdateCommand() *command.Command { c := command.NewCommand() c.Path = "dnf" c.Args = []command.CommandArg{ command.CommandArg("-q"), command.CommandArg("-y"), command.CommandArg("install"), command.CommandArg("{{ .Name }}"), } return c } func NewDnfDeleteCommand() *command.Command { c := command.NewCommand() c.Path = "dnf" c.Args = []command.CommandArg{ command.CommandArg("-q"), command.CommandArg("-y"), command.CommandArg("remove"), command.CommandArg("{{ .Name }}"), } return c } func NewDnfReadPackagesCommand() *command.Command { c := command.NewCommand() c.Path = "dnf" c.FailOnError = false c.Split = false c.Args = []command.CommandArg{ command.CommandArg("-q"), command.CommandArg("list"), command.CommandArg("installed"), } c.Extractor = func(out []byte, target any) error { Packages := target.(*[]*Package) lines := strings.Split(strings.TrimSpace(string(out)), "\n") lineIndex := 0 for _, line := range lines[1:] { installedPackage := strings.Fields(strings.TrimSpace(line)) if len(*Packages) <= lineIndex + 1 { *Packages = append(*Packages, NewPackage()) } p := (*Packages)[lineIndex] packageNameFields := strings.Split(installedPackage[0], ".") packageName := packageNameFields[0] //packageArch := packageNameFields[1] packageVersionFields := strings.Split(installedPackage[1], ":") if len(packageVersionFields) > 1 { p.Version = packageVersionFields[1] } else { p.Version = packageVersionFields[0] } p.Name = packageName p.Common.State = "present" p.PackageType = PackageTypeDnf lineIndex++ } return nil } return c } func NewRpmCreateCommand() *command.Command { c := command.NewCommand() c.Path = "rpm" c.Split = false c.StdinAvailable = false c.Args = []command.CommandArg{ command.CommandArg("-i"), command.CommandArg("{{ .Source }}"), } return c } func NewRpmReadCommand() *command.Command { c := command.NewCommand() c.Path = "rpm" c.Args = []command.CommandArg{ command.CommandArg("-q"), command.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 _, packageLine := range pkginfo { // package-name-ver-rel.arch packageFields := strings.Split(packageLine, "-") numberOfFields := len(packageFields) if numberOfFields > 2 { packageName := strings.Join(packageFields[:numberOfFields - 2], "-") packageVersion := strings.Join(packageFields[numberOfFields - 2:numberOfFields - 1], "-") slog.Info("Package[RPM].Extract()", "name", packageName, "version", packageVersion, "package", p) if packageName == p.Name { p.Common.State = "present" p.Version = packageVersion return nil } } } p.Common.State = "absent" slog.Info("Extract()", "package", p) return nil } return c } func NewRpmUpdateCommand() *command.Command { c := command.NewCommand() c.Path = "rpm" c.Args = []command.CommandArg{ command.CommandArg("-i"), command.CommandArg("{{ .Source }}"), } return c } func NewRpmDeleteCommand() *command.Command { c := command.NewCommand() c.Path = "rpm" c.Args = []command.CommandArg{ command.CommandArg("-e"), command.CommandArg("{{ .Name }}"), } return c } func NewRpmReadPackagesCommand() *command.Command { return nil } func NewPipCreateCommand() *command.Command { c := command.NewCommand() c.Path = "pip" c.Split = false c.Args = []command.CommandArg{ command.CommandArg("install"), command.CommandArg("{{ .Name }}{{ if .Required }}{{ .Required }}{{ else }}>=0.0.0{{ end }}"), } return c } func NewPipReadCommand() *command.Command { c := command.NewCommand() c.Path = "pip" c.Args = []command.CommandArg{ command.CommandArg("list"), } c.Extractor = func(out []byte, target any) error { p := target.(*Package) pkginfo := strings.Split(string(out), "\n") for _, packageLine := range pkginfo[2:] { packageFields := strings.Fields(packageLine) numberOfFields := len(packageFields) if numberOfFields == 2 { packageName := packageFields[0] packageVersion := packageFields[1] if packageName == p.Name { p.Common.State = "present" p.Version = packageVersion return nil } } } p.Common.State = "absent" slog.Info("Extract()", "package", p) return nil } return c } func NewPipUpdateCommand() *command.Command { c := command.NewCommand() c.Path = "pip" c.Args = []command.CommandArg{ command.CommandArg("install"), command.CommandArg("{{ .Name }}{{ if .Required }}{{ .Required }}{{ else }}>=0.0.0{{ end }}"), } return c } func NewPipDeleteCommand() *command.Command { c := command.NewCommand() c.Path = "pip" c.Args = []command.CommandArg{ command.CommandArg("uninstall"), command.CommandArg("{{ .Name }}"), } return c } func NewPipReadPackagesCommand() *command.Command { return nil } func NewYumCreateCommand() *command.Command { c := command.NewCommand() c.Path = "yum" c.Split = false c.Args = []command.CommandArg{ command.CommandArg("install"), command.CommandArg("-q"), command.CommandArg("-y"), command.CommandArg("{{ .Name }}{{ if .Required }}{{ .Required }}{{ else }}>=0.0.0{{ end }}"), } return c } func NewYumReadCommand() *command.Command { c := command.NewCommand() c.Path = "yum" c.Args = []command.CommandArg{ command.CommandArg("-q"), command.CommandArg("list"), command.CommandArg("installed"), command.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 _, packageLines := range pkginfo { fields := strings.Fields(packageLines) packageNameField := strings.Split(fields[0], ".") packageName := strings.TrimSpace(packageNameField[0]) //packageArch := strings.TrimSpace(packageNameField[1]) if packageName == p.Name { p.Common.State = "present" packageVersionField := strings.Split(fields[1], ":") //packageEpoch := strings.TrimSpace(packageVersionField[0]) packageVersion := strings.TrimSpace(packageVersionField[1]) p.Version = packageVersion return nil } } p.Common.State = "absent" slog.Info("Extract()", "package", p) return nil } return c } func NewYumUpdateCommand() *command.Command { c := command.NewCommand() c.Path = "yum" c.Args = []command.CommandArg{ command.CommandArg("-q"), command.CommandArg("-y"), command.CommandArg("install"), command.CommandArg("{{ .Name }}"), } return c } func NewYumDeleteCommand() *command.Command { c := command.NewCommand() c.Path = "yum" c.Args = []command.CommandArg{ command.CommandArg("-q"), command.CommandArg("-y"), command.CommandArg("remove"), command.CommandArg("{{ .Name }}"), } return c } func NewYumReadPackagesCommand() *command.Command { return nil }