From 0b5402c010c05f3652e334808dee91a6f88d0d26 Mon Sep 17 00:00:00 2001 From: Christopher Obbard Date: Thu, 27 Jul 2023 12:00:34 +0100 Subject: [PATCH 1/2] actions: apt: Abstract apt commands into separate wrapper The apt commands will also be used by the install-dpkg wrapper, prepare for this by abstracting the generic apt commands into a wrapper class. Also create a wrapper base class to be able to abstract common action commands into generic functions. Signed-off-by: Christopher Obbard --- actions/apt_action.go | 40 +++++----------------------------- wrapper/apt_wrapper.go | 49 ++++++++++++++++++++++++++++++++++++++++++ wrapper/wrapper.go | 41 +++++++++++++++++++++++++++++++++++ 3 files changed, 95 insertions(+), 35 deletions(-) create mode 100644 wrapper/apt_wrapper.go create mode 100644 wrapper/wrapper.go diff --git a/actions/apt_action.go b/actions/apt_action.go index 59e4a130..dc3c6f60 100644 --- a/actions/apt_action.go +++ b/actions/apt_action.go @@ -28,6 +28,7 @@ package actions import ( "github.com/go-debos/debos" + "github.com/go-debos/debos/wrapper" ) type AptAction struct { @@ -44,50 +45,19 @@ func NewAptAction() *AptAction { } func (apt *AptAction) Run(context *debos.DebosContext) error { - aptConfig := []string{} - - /* Don't show progress update percentages */ - aptConfig = append(aptConfig, "-o=quiet::NoUpdate=1") - - aptOptions := []string{"apt-get", "-y"} - aptOptions = append(aptOptions, aptConfig...) - - if !apt.Recommends { - aptOptions = append(aptOptions, "--no-install-recommends") - } - - if apt.Unauthenticated { - aptOptions = append(aptOptions, "--allow-unauthenticated") - } - - aptOptions = append(aptOptions, "install") - aptOptions = append(aptOptions, apt.Packages...) - - c := debos.NewChrootCommandForContext(*context) - c.AddEnv("DEBIAN_FRONTEND=noninteractive") + aptCommand := wrapper.NewAptCommand(*context, "apt") if apt.Update { - cmd := []string{"apt-get"} - cmd = append(cmd, aptConfig...) - cmd = append(cmd, "update") - - err := c.Run("apt", cmd...) - if err != nil { + if err := aptCommand.Update(); err != nil { return err } } - err := c.Run("apt", aptOptions...) - if err != nil { + if err := aptCommand.Install(apt.Packages, apt.Recommends, apt.Unauthenticated); err != nil { return err } - cmd := []string{"apt-get"} - cmd = append(cmd, aptConfig...) - cmd = append(cmd, "clean") - - err = c.Run("apt", cmd...) - if err != nil { + if err := aptCommand.Clean(); err != nil { return err } diff --git a/wrapper/apt_wrapper.go b/wrapper/apt_wrapper.go new file mode 100644 index 00000000..19aa87a9 --- /dev/null +++ b/wrapper/apt_wrapper.go @@ -0,0 +1,49 @@ +/* Abstracts the apt command. */ +package wrapper + +import ( + "github.com/go-debos/debos" +) + +type AptCommand struct { + Wrapper +} + +func NewAptCommand(context debos.DebosContext, label string) AptCommand { + command := "apt-get" + + apt := AptCommand{ + Wrapper: NewCommandWrapper(context, command, label), + } + + apt.AddEnv("DEBIAN_FRONTEND=noninteractive") + + /* Don't show progress update percentages */ + apt.AppendGlobalArguments("-o=quiet::NoUpdate=1") + + return apt +} + +func (apt AptCommand) Clean() error { + return apt.Run("clean") +} + +func (apt AptCommand) Install(packages []string, recommends bool, unauthenticated bool) error { + arguments := []string{"install", "--yes"} + + if !recommends { + arguments = append(arguments, "--no-install-recommends") + } + + if unauthenticated { + arguments = append(arguments, "--allow-unauthenticated") + } + + arguments = append(arguments, packages...) + + return apt.Run(arguments...) +} + +func (apt AptCommand) Update() error { + return apt.Run("update") +} diff --git a/wrapper/wrapper.go b/wrapper/wrapper.go new file mode 100644 index 00000000..9346056c --- /dev/null +++ b/wrapper/wrapper.go @@ -0,0 +1,41 @@ +/* Base class to abstract commonly used commands. */ +package wrapper + +import ( + "github.com/go-debos/debos" +) + +type Wrapper struct { + debos.Command + command string + globalArgs []string + label string +} + +func NewCommandWrapper(context debos.DebosContext, command string, label string) Wrapper { + return Wrapper{ + Command: debos.NewChrootCommandForContext(context), + command: command, + label: label, + } +} + +func (cmd *Wrapper) SetCommand(command string) { + cmd.command = command +} + +func (cmd *Wrapper) AppendGlobalArguments(args string) { + cmd.globalArgs = append(cmd.globalArgs, args) +} + +func (cmd *Wrapper) SetLabel(label string) { + cmd.label = label +} + +func (cmd Wrapper) Run(additionalArgs ...string) error { + args := []string{cmd.command} + args = append(args, cmd.globalArgs...) + args = append(args, additionalArgs...) + + return cmd.Command.Run(cmd.label, args...) +} From 487fd181527b7ff0e638cc7b61269b307bd8b5da Mon Sep 17 00:00:00 2001 From: Christopher Obbard Date: Fri, 5 Jun 2020 10:31:04 +0100 Subject: [PATCH 2/2] actions: install-dpkg: Implement action to install local Debian packages The install-dpkg action allows local .deb packages to be installed using the apt command, much like the apt action but for local packages rather than for packages retrieved from remote apt repositories. Resolves: #157 Closes: #165 Signed-off-by: Christopher Obbard --- README.md | 1 + actions/install_dpkg_action.go | 180 +++++++++++++++++++++++++++++++++ actions/recipe.go | 4 + actions/recipe_test.go | 1 + 4 files changed, 186 insertions(+) create mode 100644 actions/install_dpkg_action.go diff --git a/README.md b/README.md index 6e1c08af..9071ae0d 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ Some of the actions provided by debos to customize and produce images are: * download: download a single file from the internet * filesystem-deploy: deploy a root filesystem to an image previously created * image-partition: create an image file, make partitions and format them +* install-dpkg: install packages and their dependencies from local 'deb' files * ostree-commit: create an OSTree commit from rootfs * ostree-deploy: deploy an OSTree branch to the image * overlay: do a recursive copy of directories or files to the target filesystem diff --git a/actions/install_dpkg_action.go b/actions/install_dpkg_action.go new file mode 100644 index 00000000..7861289c --- /dev/null +++ b/actions/install_dpkg_action.go @@ -0,0 +1,180 @@ +/* +InstallDpkg Action + +Install packages from .deb files and their dependencies to the target rootfs +using 'apt'. + +Dependencies will be satisfied first from the `packages` list (i.e. locally +available packages) and then from the target's configured apt repositories. If +`deps` is set to false, dependencies will not be installed and an error will be +thrown. TODO: check this + +Attempting to downgrade packages which are already installed is not allowed and +will throw an error. + + # Yaml syntax: + - action: install-dpkg + origin: name + recommends: bool + unauthenticated: bool + deps: bool + packages: + - package_path.deb + - *.deb + +Mandatory properties: + +- packages -- list of package files to install from the filesystem (or named +origin). Resolves Unix-style glob patterns. If installing from a named origin, +e.g. the result of a download action, the package path will be automatically +generated from the origin contents and the `packages` property can be omitted. + +Optional properties: + +- origin -- reference to named file or directory. Defaults to recipe directory. + +- recommends -- boolean indicating if suggested packages will be installed. Defaults to false. + +- unauthenticated -- boolean indicating if unauthenticated packages can be installed. Defaults to false. + +- update -- boolean indicating if `apt update` will be run. Default 'true'. + +Example to install all packages from recipe subdirectory `pkgs/`: + + - action: install-dpkg + description: Install Debian packages from local recipe + packages: + - pkgs/*.deb + +Example to install named packages from recipe subdirectory `pkgs/`: + + - action: install-dpkg + description: Install Debian packages from local recipe + packages: + - pkgs/bmap-tools_*_all.deb + - pkgs/fakemachine_*_amd64.deb + +Example to download and install a package: + + - action: download + description: Install Debian package from url + url: http://ftp.us.debian.org/debian/pool/main/b/bmap-tools/bmap-tools_3.5-2_all.deb + name: bmap-tools-pkg + + - action: install-dpkg + description: Install Debian package from url + origin: bmap-tools-pkg +*/ + +package actions + +import ( + "fmt" + "log" + "os" + "path" + "path/filepath" + "strings" + + "github.com/go-debos/debos" + "github.com/go-debos/debos/wrapper" +) + +type InstallDpkgAction struct { + debos.BaseAction `yaml:",inline"` + Recommends bool + Unauthenticated bool + Update bool + Origin string + Packages []string +} + +func NewInstallDpkgAction() *InstallDpkgAction { + a := &InstallDpkgAction{Update: true} + return a +} + +func (apt *InstallDpkgAction) Run(context *debos.DebosContext) error { + aptCommand := wrapper.NewAptCommand(*context, "install-dpkg") + + /* check if named origin exists or fallback to RecipeDir if no origin set */ + var origin string = context.RecipeDir + if len(apt.Origin) > 0 { + var found bool + if origin, found = context.Origins[apt.Origin]; !found { + return fmt.Errorf("origin %s not found", apt.Origin) + } + } + + /* create a list of full paths of packages to install: if the origin is a + * single file (e.g download action) then just return that package, otherwise + * append package name to the origin path and glob to create a list of packages. + * In other words, install all packages which are in the origin's directory. + */ + packages := []string{} + file, err := os.Stat(origin) + if err != nil { + return err + } + + if file.IsDir() { + if len(apt.Packages) == 0 { + return fmt.Errorf("no packages defined") + } + + for _, pkg := range apt.Packages { + // resolve globs + source := path.Join(origin, pkg) + matches, err := filepath.Glob(source) + if err != nil { + return err + } + if len(matches) == 0 { + return fmt.Errorf("file(s) not found after globbing: %s", pkg) + } + + packages = append(packages, matches...) + } + } else { + packages = append(packages, origin) + } + + /* bind mount each package into rootfs & update the list with the + * path relative to the chroot */ + for idx, pkg := range packages { + // check for duplicates after globbing + for j := idx + 1; j < len(packages); j++ { + if packages[j] == pkg { + return fmt.Errorf("duplicate package found: %s", pkg) + } + } + + log.Printf("Installing %s", pkg) + + /* Only bind mount the package if the file is outside the rootfs */ + if strings.HasPrefix(pkg, context.Rootdir) { + pkg = strings.TrimPrefix(pkg, context.Rootdir) + } else { + aptCommand.AddBindMount(pkg, "") + } + + /* update pkg list with the complete resolved path */ + packages[idx] = pkg + } + + if apt.Update { + if err := aptCommand.Update(); err != nil { + return err + } + } + + if err := aptCommand.Install(packages, apt.Recommends, apt.Unauthenticated); err != nil { + return err + } + + if err := aptCommand.Clean(); err != nil { + return err + } + + return nil +} diff --git a/actions/recipe.go b/actions/recipe.go index e27d2790..f7abf888 100644 --- a/actions/recipe.go +++ b/actions/recipe.go @@ -54,6 +54,8 @@ Supported actions - image-partition -- https://godoc.org/github.com/go-debos/debos/actions#hdr-ImagePartition_Action +- install-dpkg -- https://godoc.org/github.com/go-debos/debos/actions#hdr-InstallDpkg_Action + - ostree-commit -- https://godoc.org/github.com/go-debos/debos/actions#hdr-OstreeCommit_Action - ostree-deploy -- https://godoc.org/github.com/go-debos/debos/actions#hdr-OstreeDeploy_Action @@ -133,6 +135,8 @@ func (y *YamlAction) UnmarshalYAML(unmarshal func(interface{}) error) error { y.Action = &OverlayAction{} case "image-partition": y.Action = &ImagePartitionAction{} + case "install-dpkg": + y.Action = NewInstallDpkgAction() case "filesystem-deploy": y.Action = NewFilesystemDeployAction() case "raw": diff --git a/actions/recipe_test.go b/actions/recipe_test.go index ef2a755d..2026200b 100644 --- a/actions/recipe_test.go +++ b/actions/recipe_test.go @@ -54,6 +54,7 @@ actions: - action: download - action: filesystem-deploy - action: image-partition + - action: install-dpkg - action: ostree-commit - action: ostree-deploy - action: overlay