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/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/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 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...) +}