Skip to content

Commit

Permalink
Ad install plugin (#189)
Browse files Browse the repository at this point in the history
Signed-off-by: Itxaka <[email protected]>
  • Loading branch information
Itxaka authored Dec 5, 2024
1 parent 614a362 commit 891bdaa
Show file tree
Hide file tree
Showing 4 changed files with 286 additions and 0 deletions.
1 change: 1 addition & 0 deletions pkg/executor/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ func NewExecutor(opts ...Options) Executor {
plugins.SystemdFirstboot,
plugins.DataSources,
plugins.Layout,
plugins.Packages,
},
}

Expand Down
164 changes: 164 additions & 0 deletions pkg/plugins/packages.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
package plugins

import (
"errors"
"fmt"
"io/fs"
"os"
"strings"

"github.com/joho/godotenv"
"github.com/mudler/yip/pkg/logger"
"github.com/mudler/yip/pkg/schema"
"github.com/twpayne/go-vfs/v4"
)

type Installer string

const (
APTInstaller Installer = "apt-get"
DNFInstaller Installer = "dnf"
PacmanInstaller Installer = "pacman"
SUSEInstaller Installer = "zypper"
AlpineInstaller Installer = "apk"
UnknownInstaller Installer = "unknown"
)

func (d Installer) String() string {
return string(d)
}

type Distro string

const (
Debian Distro = "debian"
Ubuntu Distro = "ubuntu"
RedHat Distro = "redhat"
CentOS Distro = "centos"
RockyLinux Distro = "rocky"
AlmaLinux Distro = "almalinux"
Fedora Distro = "fedora"
Arch Distro = "arch"
Alpine Distro = "alpine"
OpenSUSELeap Distro = "opensuse-leap"
OpenSUSETumbleweed Distro = "opensuse-tumbleweed"
)

// Packages runs the package manager to try to install/remove/refresh packages
// It will try to identify the package manager based on the distro
// If it can't identify the package manager, it will return an error
// Order is Refresh -> Install -> Remove
func Packages(l logger.Interface, s schema.Stage, fs vfs.FS, console Console) error {
// Don't do anything if empty
if len(s.Packages.Remove) == 0 && len(s.Packages.Install) == 0 && s.Packages.Refresh == false {
return nil
}

var installArgs, updateArgs, removeArgs []string

cmd := identifyInstaller(fs)

switch cmd {
case APTInstaller:
// Needed so it doesn't ask for user input
_ = os.Setenv("DEBIAN_FRONTEND", "noninteractive")
defer func() {
_ = os.Unsetenv("DEBIAN_FRONTEND")
}()
updateArgs = []string{"-y", "update"}
installArgs = []string{"-y", "--no-install-recommends", "install"}
removeArgs = []string{"-y", "remove"}
case AlpineInstaller:
updateArgs = []string{"update"}
installArgs = []string{"add", "--no-cache"}
removeArgs = []string{"del", "--no-cache"}
case DNFInstaller, SUSEInstaller:
updateArgs = []string{"-y", "update"}
installArgs = []string{"-y", "install"}
removeArgs = []string{"-y", "remove"}
case PacmanInstaller:
updateArgs = []string{"-Sy"}
installArgs = []string{"-S", "--noconfirm"}
removeArgs = []string{"-R", "--noconfirm"}
default:
l.Errorf("Unknown installer")
return errors.New("unknown package manager")
}
// Run update
if s.Packages.Refresh {
l.Debugf("Running update")
out, err := console.Run(templateSysData(l, strings.Join(append([]string{cmd.String()}, updateArgs...), " ")))
if err != nil {
return err
}
if strings.TrimSpace(out) != "" {
l.Debug(fmt.Sprintf("Command output: %s", out))
} else {
l.Debugf("Empty command output")
}
}

if s.Packages.Install != nil {
// Run install
installArgs = append(installArgs, s.Packages.Install...)
l.Debugf("Running install")
out, err := console.Run(templateSysData(l, strings.Join(append([]string{cmd.String()}, installArgs...), " ")))
if err != nil {
return err
}
if strings.TrimSpace(out) != "" {
l.Debug(fmt.Sprintf("Command output: %s", out))
} else {
l.Debugf("Empty command output")
}
}

if s.Packages.Remove != nil {
// Run remove
removeArgs = append(removeArgs, s.Packages.Remove...)
l.Debugf("Running remove")
out, err := console.Run(templateSysData(l, strings.Join(append([]string{cmd.String()}, removeArgs...), " ")))
if err != nil {
return err
}
if strings.TrimSpace(out) != "" {
l.Debug(fmt.Sprintf("Command output: %s", out))
} else {
l.Debugf("Empty command output")
}
}

return nil
}

// identifyInstaller returns the package manager based on the distro
func identifyInstaller(fsys vfs.FS) Installer {
file, err := fsys.Open("/etc/os-release")
if err != nil {
return UnknownInstaller
}
defer func(file fs.File) {
err := file.Close()
if err != nil {

}
}(file)
val, err := godotenv.Parse(file)
if err != nil {
return UnknownInstaller
}
switch Distro(val["ID"]) {
case Debian, Ubuntu:
return APTInstaller
case Fedora, RockyLinux, AlmaLinux, RedHat, CentOS:
return DNFInstaller
case Arch:
return PacmanInstaller
case Alpine:
return AlpineInstaller
case OpenSUSELeap, OpenSUSETumbleweed:
return SUSEInstaller
default:
return UnknownInstaller
}
}
114 changes: 114 additions & 0 deletions pkg/plugins/packages_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package plugins

import (
"github.com/mudler/yip/pkg/schema"
consoletests "github.com/mudler/yip/tests/console"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/sanity-io/litter"
"github.com/sirupsen/logrus"
"github.com/twpayne/go-vfs/v4/vfst"
"io"
)

var _ = Describe("Commands", func() {
Context("parsing yip file", func() {
testConsole := consoletests.TestConsole{}
l := logrus.New()
l.SetOutput(io.Discard)

BeforeEach(func() {
consoletests.Reset()
})
It("execute proper install commands", func() {
fs, cleanup, err := vfst.NewTestFS(map[string]interface{}{})
Expect(err).Should(BeNil())
defer cleanup()
Expect(fs.Mkdir("/etc", 0755)).ToNot(HaveOccurred())
Expect(fs.WriteFile("/etc/os-release", []byte("ID=debian\nVERSION=10\n"), 0644)).ToNot(HaveOccurred())

err = Packages(l, schema.Stage{
Packages: schema.Packages{
Install: []string{"foo", "bar"},
Remove: []string{"baz", "qux"},
Refresh: true,
},
}, fs, testConsole)
Expect(err).ShouldNot(HaveOccurred())
Expect(consoletests.Commands).Should(Equal([]string{"apt-get -y update", "apt-get -y --no-install-recommends install foo bar", "apt-get -y remove baz qux"}))
})
It("execute proper install commands for different OS", func() {
fs, cleanup, err := vfst.NewTestFS(map[string]interface{}{})
Expect(err).Should(BeNil())
defer cleanup()
Expect(fs.Mkdir("/etc", 0755)).ToNot(HaveOccurred())
stage := schema.Stage{
Packages: schema.Packages{
Install: []string{"foo", "bar"},
Remove: []string{"baz", "qux"},
Refresh: true,
},
}
type test struct {
osRelease string
expected []string
}
tests := []test{
{
osRelease: "ID=debian\nVERSION=10\n",
expected: []string{"apt-get -y update", "apt-get -y --no-install-recommends install foo bar", "apt-get -y remove baz qux"},
},
{
osRelease: "ID=debian\nVERSION=11\n",
expected: []string{"apt-get -y update", "apt-get -y --no-install-recommends install foo bar", "apt-get -y remove baz qux"},
},
{
osRelease: "ID=ubuntu\nVERSION=20.04\n",
expected: []string{"apt-get -y update", "apt-get -y --no-install-recommends install foo bar", "apt-get -y remove baz qux"},
},
{
osRelease: "ID=centos\nVERSION=8\n",
expected: []string{"dnf -y update", "dnf -y install foo bar", "dnf -y remove baz qux"},
},
{
osRelease: "ID=fedora\nVERSION=34\n",
expected: []string{"dnf -y update", "dnf -y install foo bar", "dnf -y remove baz qux"},
},
{
osRelease: "ID=alpine\nVERSION=3.14\n",
expected: []string{"apk update", "apk add --no-cache foo bar", "apk del --no-cache baz qux"},
},
{
osRelease: "ID=opensuse-leap\nVERSION=15.3\n",
expected: []string{"zypper -y update", "zypper -y install foo bar", "zypper -y remove baz qux"},
},
{
osRelease: "ID=arch\nVERSION=rolling\n",
expected: []string{"pacman -Sy", "pacman -S --noconfirm foo bar", "pacman -R --noconfirm baz qux"},
},
}

for _, t := range tests {
Expect(fs.WriteFile("/etc/os-release", []byte(t.osRelease), 0644)).ToNot(HaveOccurred())
err = Packages(l, stage, fs, testConsole)
Expect(err).ShouldNot(HaveOccurred(), t.osRelease)
Expect(consoletests.Commands).Should(Equal(t.expected), litter.Sdump(t.osRelease))
consoletests.Reset()
}
})
It("fails if it cant identify the systems package manager", func() {
fs, cleanup, err := vfst.NewTestFS(map[string]interface{}{})
Expect(err).Should(BeNil())
defer cleanup()
err = Packages(l, schema.Stage{
Packages: schema.Packages{
Install: []string{"foo", "bar"},
Remove: []string{"baz", "qux"},
Refresh: true,
},
}, fs, testConsole)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("unknown package manager"))
})
})
})
7 changes: 7 additions & 0 deletions pkg/schema/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ type Stage struct {
Systemctl Systemctl `yaml:"systemctl,omitempty"`
Environment map[string]string `yaml:"environment,omitempty"`
EnvironmentFile string `yaml:"environment_file,omitempty"`
Packages Packages `yaml:"packages,omitempty"`

After []Dependency `yaml:"after,omitempty"`

Expand All @@ -171,6 +172,12 @@ type Systemctl struct {
Mask []string `yaml:"mask,omitempty"`
}

type Packages struct {
Install []string `yaml:"install,omitempty"`
Remove []string `yaml:"remove,omitempty"`
Refresh bool `yaml:"refresh,omitempty"`
}

type DNS struct {
Nameservers []string `yaml:"nameservers,omitempty"`
DnsSearch []string `yaml:"search,omitempty"`
Expand Down

0 comments on commit 891bdaa

Please sign in to comment.