diff --git a/base/fup.go b/base/fup.go index f2631dc..5a7cb3a 100644 --- a/base/fup.go +++ b/base/fup.go @@ -22,6 +22,7 @@ type Config struct { file string isRemote bool AcceptHostKeys []string `yaml:"accept_host_keys"` + AptRepos []entity.AptRepo `yaml:"apt_repos"` Archives []Archive `yaml:"archives"` Binaries []entity.Binary `yaml:"binaries"` Cargo []CargoPkg `yaml:"rust"` diff --git a/entity/aptrepo.go b/entity/aptrepo.go new file mode 100644 index 0000000..cab109a --- /dev/null +++ b/entity/aptrepo.go @@ -0,0 +1,142 @@ +package entity + +import ( + "fmt" + "github.com/femnad/fup/base/settings" + "github.com/femnad/fup/internal" + "github.com/femnad/fup/precheck" + "github.com/femnad/fup/precheck/unless" + "github.com/femnad/fup/remote" + marecmd "github.com/femnad/mare/cmd" + "io" + "os/exec" + "path" +) + +const ( + keyRingsDir = "/etc/apt/keyrings" + sourcesDir = "/etc/apt/sources.list.d" +) + +type AptRepo struct { + GPGKey string `yaml:"gpg_key"` + RepoName string `yaml:"name"` + Repo string `yaml:"repo"` + When string `yaml:"when"` +} + +func (a AptRepo) DefaultVersionCmd() string { + return "" +} + +func (a AptRepo) GetUnless() unless.Unless { + return unless.Unless{ + Stat: path.Join(sourcesDir, fmt.Sprintf("%s.list", a.RepoName)), + } +} + +func (AptRepo) GetVersion() string { + return "" +} + +func (AptRepo) HasPostProc() bool { + return false +} + +func (a AptRepo) Name() string { + return a.RepoName +} + +func (a AptRepo) RunWhen() string { + return a.When +} + +func (AptRepo) UpdateCmd() string { + return "apt update" +} + +func (AptRepo) ensureKeyFile(keyUrl, keyRingFile string) error { + key, err := remote.ReadResponseBytes(keyUrl) + if err != nil { + return err + } + + gpgCmd := exec.Command("gpg", "--dearmor", "-o", keyRingFile) + stdin, err := gpgCmd.StdinPipe() + if err != nil { + return err + } + defer stdin.Close() + + stdout, err := gpgCmd.StdoutPipe() + if err != nil { + return err + } + defer stdout.Close() + + if err = gpgCmd.Start(); err != nil { + return err + } + + _, err = stdin.Write(key) + if err != nil { + return err + } + stdin.Close() + + out, err := io.ReadAll(stdout) + if err != nil { + return err + } + + if err = gpgCmd.Wait(); err != nil { + return err + } + + _, err = internal.WriteContent(internal.ManagedFile{ + Content: string(out), + Path: keyRingFile, + Mode: 0o644, + User: "root", + Group: "root", + }) + return err +} + +func (a AptRepo) Install() error { + err := internal.EnsureDir(keyRingsDir) + if err != nil { + return err + } + keyRingFile := path.Join(keyRingsDir, fmt.Sprintf("%s.gpg", a.RepoName)) + + err = a.ensureKeyFile(a.Repo, keyRingFile) + if err != nil { + return err + } + + out, err := marecmd.RunFormatError(marecmd.Input{Command: "dpkg --print-architecture"}) + if err != nil { + return err + } + architecture := out.Stdout + + versionCodename, err := precheck.GetOSVersionCodename() + if err != nil { + return err + } + + content := fmt.Sprintf("deb [arch=${architecture} signed-by=%s] %s ${codename} stable", keyRingFile, a.Repo) + content = settings.Expand(content, map[string]string{ + "architecture": architecture, + "codename": versionCodename, + }) + repoFile := path.Join(sourcesDir, fmt.Sprintf("%s.list", a.RepoName)) + + _, err = internal.WriteContent(internal.ManagedFile{ + Content: content, + Path: repoFile, + }) + + return err +} diff --git a/entity/dnfrepo.go b/entity/dnfrepo.go index 88c2d68..bb11d0c 100644 --- a/entity/dnfrepo.go +++ b/entity/dnfrepo.go @@ -65,6 +65,10 @@ func (d DnfRepo) RunWhen() string { return d.When } +func (DnfRepo) UpdateCmd() string { + return "" +} + func (i installer) installCorePlugins() error { cmd := fmt.Sprintf("dnf install -y %s", pluginsCore) return i.runMaybeSudo(cmd) @@ -115,7 +119,7 @@ func (d DnfRepo) Install() error { } if len(d.Packages) > 0 { - osId, err := precheck.GetOsId() + osId, err := precheck.GetOSId() if err != nil { return err } diff --git a/entity/osrepo.go b/entity/osrepo.go new file mode 100644 index 0000000..85adbfe --- /dev/null +++ b/entity/osrepo.go @@ -0,0 +1,13 @@ +package entity + +import ( + "github.com/femnad/fup/precheck/unless" + "github.com/femnad/fup/precheck/when" +) + +type OSRepo interface { + unless.Unlessable + when.Whenable + Install() error + UpdateCmd() string +} diff --git a/precheck/fact.go b/precheck/fact.go index e7de1e9..80d7216 100644 --- a/precheck/fact.go +++ b/precheck/fact.go @@ -113,7 +113,7 @@ func isOk(cap string) (bool, error) { } func isOs(osId string) (bool, error) { - foundOsId, err := GetOsId() + foundOsId, err := GetOSId() if err != nil { return false, fmt.Errorf("error getting OS ID %v", err) } diff --git a/precheck/os.go b/precheck/os.go index fcc24d5..6d661b1 100644 --- a/precheck/os.go +++ b/precheck/os.go @@ -9,11 +9,12 @@ import ( ) const ( - osReleaseFile = "/etc/os-release" - osIdField = "ID" + osReleaseFile = "/etc/os-release" + osIdField = "ID" + versionCodenameField = "VERSION_CODENAME" ) -func GetOsId() (string, error) { +func getOSReleaseField(f string) (string, error) { file, err := os.Open(osReleaseFile) if err != nil { return "", err @@ -37,7 +38,7 @@ func GetOsId() (string, error) { } field, value := fields[0], fields[1] - if field != osIdField { + if field != f { continue } return value, nil @@ -45,3 +46,11 @@ func GetOsId() (string, error) { return "", fmt.Errorf("unable to locate OS ID line in %s", osReleaseFile) } + +func GetOSVersionCodename() (string, error) { + return getOSReleaseField(versionCodenameField) +} + +func GetOSId() (string, error) { + return getOSReleaseField(osIdField) +} diff --git a/provision/dnfrepo.go b/provision/dnfrepo.go deleted file mode 100644 index a77faac..0000000 --- a/provision/dnfrepo.go +++ /dev/null @@ -1,27 +0,0 @@ -package provision - -import ( - "errors" - "github.com/femnad/fup/base" - "github.com/femnad/fup/entity" - "github.com/femnad/fup/precheck/unless" - "github.com/femnad/fup/precheck/when" -) - -func addDnfRepos(config base.Config, repos []entity.DnfRepo) error { - var errs []error - for _, repo := range repos { - if !when.ShouldRun(repo) { - continue - } - - if unless.ShouldSkip(repo, config.Settings) { - continue - } - - err := repo.Install() - errs = append(errs, err) - } - - return errors.Join(errs...) -} diff --git a/provision/osrepo.go b/provision/osrepo.go new file mode 100644 index 0000000..3a2a391 --- /dev/null +++ b/provision/osrepo.go @@ -0,0 +1,66 @@ +package provision + +import ( + "errors" + marecmd "github.com/femnad/mare/cmd" + + mapset "github.com/deckarep/golang-set/v2" + + "github.com/femnad/fup/base" + "github.com/femnad/fup/entity" + "github.com/femnad/fup/internal" + "github.com/femnad/fup/precheck/unless" + "github.com/femnad/fup/precheck/when" +) + +func runUpdateCmds(cmds mapset.Set[string]) []error { + var errs []error + + isRoot, err := internal.IsUserRoot() + if err != nil { + return []error{err} + } + + cmds.Each(func(cmd string) bool { + input := marecmd.Input{Command: cmd, Sudo: !isRoot} + _, err = marecmd.RunFormatError(input) + errs = append(errs, err) + return false + }) + + return errs +} + +func addRepos(config base.Config) error { + var errs []error + + var repos []entity.OSRepo + for _, repo := range config.AptRepos { + repos = append(repos, repo) + } + for _, repo := range config.DnfRepos { + repos = append(repos, repo) + } + + updateCmds := mapset.NewSet[string]() + for _, repo := range repos { + if !when.ShouldRun(repo) { + continue + } + + if unless.ShouldSkip(repo, config.Settings) { + continue + } + + err := repo.Install() + if err == nil && repo.UpdateCmd() != "" { + updateCmds.Add(repo.UpdateCmd()) + } + errs = append(errs, err) + } + + updateErrs := runUpdateCmds(updateCmds) + errs = append(errs, updateErrs...) + + return errors.Join(errs...) +} diff --git a/provision/packages.go b/provision/packages.go index 939ccea..9b08041 100644 --- a/provision/packages.go +++ b/provision/packages.go @@ -52,7 +52,7 @@ type determiner struct { func newDeterminer() (determiner, error) { var d determiner - osId, err := precheck.GetOsId() + osId, err := precheck.GetOSId() if err != nil { return d, fmt.Errorf("error determining OS: %v", err) } diff --git a/provision/provision.go b/provision/provision.go index 93a56e5..bdfc3d6 100644 --- a/provision/provision.go +++ b/provision/provision.go @@ -75,7 +75,7 @@ func NewProvisioner(cfg base.Config, filter []string) (Provisioner, error) { all := []provisionFn{ {"preflight", p.runPreflightTasks}, - {"dnf-repo", p.AddDnfRepos}, + {"repo", p.AddOSRepos}, {"archive", p.extractArchives}, {"binary", p.downloadBinaries}, {"package", p.installPackages}, @@ -111,10 +111,10 @@ func (p Provisioner) Apply() error { return p.provisioners.apply() } -func (p Provisioner) AddDnfRepos() error { - internal.Log.Notice("Adding DNF repos") +func (p Provisioner) AddOSRepos() error { + internal.Log.Notice("Adding OS repos") - return addDnfRepos(p.Config, p.Config.DnfRepos) + return addRepos(p.Config) } func (p Provisioner) extractArchives() error {