diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..32eae19 --- /dev/null +++ b/.gitignore @@ -0,0 +1,51 @@ + +# Created by https://www.gitignore.io/api/vim,linux,go + +### Go ### +# Binaries for programs and plugins +*.exe +*.dll +*.so +*.dylib + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 +.glide/ + +# Golang project vendor packages which should be ignored +vendor/ + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### Vim ### +# swap +[._]*.s[a-v][a-z] +[._]*.sw[a-p] +[._]s[a-v][a-z] +[._]sw[a-p] +# session +Session.vim +# temporary +.netrwhist +# auto-generated tag files +tags + +# End of https://www.gitignore.io/api/vim,linux,go diff --git a/actions/actions_doc.go b/actions/actions_doc.go new file mode 100644 index 0000000..7ebd532 --- /dev/null +++ b/actions/actions_doc.go @@ -0,0 +1,6 @@ +// Copyright 2017, Collabora Ltd. + +/* +Package 'actions' implements 'debos' modules used for OS creation. +*/ +package actions diff --git a/actions/apt_action.go b/actions/apt_action.go index 49d3434..2045522 100644 --- a/actions/apt_action.go +++ b/actions/apt_action.go @@ -1,3 +1,23 @@ +/* +Apt Action + +Install packages and their dependencies to the target rootfs with 'apt'. + +Yaml syntax: + - action: apt + recommends: bool + packages: + - package1 + - package2 + +Mandatory properties: + +- packages -- list of packages to install + +Optional properties: + +- recommends -- boolean indicating if suggested packages will be installed +*/ package actions import ( @@ -6,8 +26,8 @@ import ( type AptAction struct { debos.BaseAction `yaml:",inline"` - Recommends bool - Packages []string + Recommends bool + Packages []string } func (apt *AptAction) Run(context *debos.DebosContext) error { diff --git a/actions/debootstrap_action.go b/actions/debootstrap_action.go index 7545204..e257501 100644 --- a/actions/debootstrap_action.go +++ b/actions/debootstrap_action.go @@ -1,3 +1,32 @@ +/* +Debootstrap Action + +Construct the target rootfs with debootstrap tool. + +Yaml syntax: + - action: debootstrap + mirror: URL + suite: "name" + components: + variant: "name" + keyring-package: + +Mandatory properties: + +- suite -- release code name or symbolic name (e.g. "stable") + +Optional properties: + +- mirror -- URL with Debian-compatible repository + +- variant -- name of the bootstrap script variant to use + +- components -- list of components to use for packages selection. +Example: + components: [ main, contrib ] + +- keyring-package -- keyring for packages validation. Currently ignored. +*/ package actions import ( @@ -11,13 +40,13 @@ import ( ) type DebootstrapAction struct { - debos.BaseAction `yaml:",inline"` - Suite string - Mirror string - Variant string - KeyringPackage string `yaml:"keyring-package"` - Components []string - MergedUsr bool `yaml:"merged-usr"` + debos.BaseAction `yaml:",inline"` + Suite string + Mirror string + Variant string + KeyringPackage string `yaml:"keyring-package"` + Components []string + MergedUsr bool `yaml:"merged-usr"` } func NewDebootstrapAction() *DebootstrapAction { @@ -25,6 +54,7 @@ func NewDebootstrapAction() *DebootstrapAction { // Use filesystem with merged '/usr' by default d.MergedUsr = true return &d + } func (d *DebootstrapAction) RunSecondStage(context debos.DebosContext) error { diff --git a/actions/download_action.go b/actions/download_action.go index 0de5e48..a4099e7 100644 --- a/actions/download_action.go +++ b/actions/download_action.go @@ -1,3 +1,35 @@ +/* +Download Action + +Download a single file from Internet and unpack it in place if needed. + +Yaml syntax: + - action: download + url: http://example.domain/path/filename.ext + name: firmware + filename: output_name + unpack: bool + compression: gz + +Mandatory properties: + +- url -- URL to an object for download + +- name -- string which allow to use downloaded object in other actions +via 'origin' property. If 'unpack' property is set to 'true' name will +refer to temporary directory with extracted content. + +Optional properties: + +- filename -- use this property as the name for saved file. Useful if URL does not +contain file name in path, for example it is possible to download files from URLs without path part. + +- unpack -- hint for action to extract all files from downloaded archive. +See the 'Unpack' action for more information. + +- compression -- optional hint for unpack allowing to use proper compression method. +See the 'Unpack' action for more information. +*/ package actions import ( @@ -37,13 +69,58 @@ func (d *DownloadAction) validateUrl() (*url.URL, error) { return url, nil } +func (d *DownloadAction) validateFilename(context *debos.DebosContext, url *url.URL) (filename string, err error) { + if len(d.Filename) == 0 { + // Trying to guess the name from URL Path + filename = path.Base(url.Path) + } else { + filename = path.Base(d.Filename) + } + if len(filename) == 0 { + return "", fmt.Errorf("Incorrect filename is provided for '%s'", d.Url) + } + filename = path.Join(context.Scratchdir, filename) + return filename, nil +} + +func (d *DownloadAction) archive(filename string) (debos.Archive, error) { + archive, err := debos.NewArchive(filename) + if err != nil { + return archive, err + } + switch archive.Type() { + case debos.Tar: + if len(d.Compression) > 0 { + if err := archive.AddOption("tarcompression", d.Compression); err != nil { + return archive, err + } + } + default: + } + return archive, nil +} + func (d *DownloadAction) Verify(context *debos.DebosContext) error { + var filename string if len(d.Name) == 0 { return fmt.Errorf("Property 'name' is mandatory for download action\n") } - _, err := d.validateUrl() - return err + + url, err := d.validateUrl() + if err != nil { + return err + } + filename, err = d.validateFilename(context, url) + if err != nil { + return err + } + if d.Unpack == true { + if _, err := d.archive(filename); err != nil { + return err + } + } + return nil } func (d *DownloadAction) Run(context *debos.DebosContext) error { @@ -55,16 +132,10 @@ func (d *DownloadAction) Run(context *debos.DebosContext) error { return err } - if len(d.Filename) == 0 { - // Trying to guess the name from URL Path - filename = path.Base(url.Path) - } else { - filename = path.Base(d.Filename) + filename, err = d.validateFilename(context, url) + if err != nil { + return err } - if len(filename) == 0 { - return fmt.Errorf("Incorrect filename is provided for '%s'", d.Url) - } - filename = path.Join(context.Scratchdir, filename) originPath := filename switch url.Scheme { @@ -78,8 +149,13 @@ func (d *DownloadAction) Run(context *debos.DebosContext) error { } if d.Unpack == true { + archive, err := d.archive(filename) + if err != nil { + return err + } + targetdir := filename + ".d" - err := debos.UnpackTarArchive(filename, targetdir, d.Compression, "--no-same-owner", "--no-same-permissions") + err = archive.RelaxedUnpack(targetdir) if err != nil { return err } diff --git a/actions/filesystem_deploy_action.go b/actions/filesystem_deploy_action.go index 0b85fd5..5fcab9e 100644 --- a/actions/filesystem_deploy_action.go +++ b/actions/filesystem_deploy_action.go @@ -1,3 +1,22 @@ +/* +FilesystemDeploy Action + +Deploy prepared root filesystem to output image. This action requires +'image-partition' action to be executed before it. + +Yaml syntax: + - action: filesystem-deploy + setup-fstab: bool + setup-kernel-cmdline: bool + +Optional properties: + +- setup-fstab -- generate '/etc/fstab' file according to information provided +by 'image-partition' action. By default is 'true'. + +- setup-kernel-cmdline -- add location of root partition to '/etc/kernel/cmdline' +file on target image. By default is 'true'. +*/ package actions import ( @@ -14,7 +33,7 @@ import ( ) type FilesystemDeployAction struct { - debos.BaseAction `yaml:",inline"` + debos.BaseAction `yaml:",inline"` SetupFSTab bool `yaml:"setup-fstab"` SetupKernelCmdline bool `yaml:"setup-kernel-cmdline"` } diff --git a/actions/image_partition_action.go b/actions/image_partition_action.go index d50bac1..6d8991c 100644 --- a/actions/image_partition_action.go +++ b/actions/image_partition_action.go @@ -1,3 +1,105 @@ +/* +ImagePartition Action + +This action creates an image file, partitions it and formats the filesystems. + +Yaml syntax: + - action: image-partition + imagename: image_name + imagesize: size + partitiontype: gpt + partitions: + + mountpoints: + + +Mandatory properties: + +- imagename -- the name of the image file. + +- imagesize -- generated image size in human-readable form, examples: 100MB, 1GB, etc. + +- partitiontype -- partition table type. Currently only 'gpt' and 'msdos' +partition tables are supported. + +- partitions -- list of partitions, at least one partition is needed. +Partition properties are described below. + +- mountpoints -- list of mount points for partitions. +Properties for mount points are described below. + +Yaml syntax for partitions: + + partitions: + - name: label + name: partition name + fs: filesystem + start: offset + end: offset + flags: list of flags + +Mandatory properties: + +- name -- is used for referencing named partition for mount points +configuration (below) and label the filesystem located on this partition. + +- fs -- filesystem type used for formatting. + +NB: the FAT (vfat) filesystem is named 'fat32' in configuration file. + +- start -- offset from beginning of the disk there the partition starts. + +- end -- offset from beginning of the disk there the partition ends. + +For 'start' and 'end' properties offset can be written in human readable +form -- '32MB', '1GB' or as disk percentage -- '100%'. + +Optional properties: + +- flags -- list of additional flags for partition compatible with parted(8) +'set' command. + +Yaml syntax for mount points: + + mountpoints: + - mountpoint: path + partition: partition label + options: list of options + +Mandatory properties: + +- partition -- partition name for mounting. + +- mountpoint -- path in the target root filesystem where the named partition +should be mounted. + +Optional properties: + +- options -- list of options to be added to appropriate entry in fstab file. + +Layout example for Raspberry PI 3: + + - action: image-partition + imagename: "debian-rpi3.img" + imagesize: 1GB + partitiontype: msdos + mountpoints: + - mountpoint: / + partition: root + - mountpoint: /boot/firmware + partition: firmware + options: [ x-systemd.automount ] + partitions: + - name: firmware + fs: fat32 + start: 0% + end: 64MB + - name: root + fs: ext4 + start: 64MB + end: 100% + flags: [ boot ] +*/ package actions import ( @@ -32,14 +134,14 @@ type Mountpoint struct { } type ImagePartitionAction struct { - debos.BaseAction `yaml:",inline"` - ImageName string - ImageSize string - PartitionType string - Partitions []Partition - Mountpoints []Mountpoint - size int64 - usingLoop bool + debos.BaseAction `yaml:",inline"` + ImageName string + ImageSize string + PartitionType string + Partitions []Partition + Mountpoints []Mountpoint + size int64 + usingLoop bool } func (i *ImagePartitionAction) generateFSTab(context *debos.DebosContext) error { diff --git a/actions/ostree_commit_action.go b/actions/ostree_commit_action.go index dfed866..6d41b89 100644 --- a/actions/ostree_commit_action.go +++ b/actions/ostree_commit_action.go @@ -1,3 +1,28 @@ +/* +OstreeCommit Action + +Create OSTree commit from rootfs. + +Yaml syntax: + - action: ostree-commit + repository: repository name + branch: branch name + subject: commit message + +Mandatory properties: + +- repository -- path to repository with OSTree structure; the same path is +used by 'ostree' tool with '--repo' argument. +This path is relative to 'artifact' directory. +Please keep in mind -- you will need a root privileges for 'bare' repository +type (https://ostree.readthedocs.io/en/latest/manual/repo/#repository-types-and-locations). + +- branch -- OSTree branch name that should be used for the commit. + +Optional properties: + +- subject -- one line message with commit description. +*/ package actions import ( @@ -11,10 +36,10 @@ import ( type OstreeCommitAction struct { debos.BaseAction `yaml:",inline"` - Repository string - Branch string - Subject string - Command string + Repository string + Branch string + Subject string + Command string } func emptyDir(dir string) { diff --git a/actions/ostree_deploy_action.go b/actions/ostree_deploy_action.go index c5a40ed..643e1b4 100644 --- a/actions/ostree_deploy_action.go +++ b/actions/ostree_deploy_action.go @@ -1,3 +1,44 @@ +/* +OstreeDeploy Action + +Deploy the OSTree branch to the image. +If any preparation has been done for rootfs, it can be overwritten +during this step. + +Action 'image-partition' must be called prior to OSTree deploy. + +Yaml syntax: + - action: ostree-deploy + repository: repository name + remote_repository: URL + branch: branch name + os: os name + setup-fstab: bool + setup-kernel-cmdline: bool + appendkernelcmdline: arguments + +Mandatory properties: + +- remote_repository -- URL to remote OSTree repository for pulling stateroot branch. +Currently not implemented, please prepare local repository instead. + +- repository -- path to repository with OSTree structure. +This path is relative to 'artifact' directory. + +- os -- os deployment name, as explained in: +https://ostree.readthedocs.io/en/latest/manual/deployment/ + +- branch -- branch of the repository to use for populating the image. + +Optional properties: + +- setup-fstab -- create '/etc/fstab' file for image + +- setup-kernel-cmdline -- add the information from the 'image-partition' +action to the configured commandline. + +- appendkernelcmdline -- additional kernel command line arguments passed to kernel. +*/ package actions import ( @@ -12,13 +53,13 @@ import ( ) type OstreeDeployAction struct { - debos.BaseAction `yaml:",inline"` + debos.BaseAction `yaml:",inline"` Repository string RemoteRepository string "remote_repository" Branch string Os string - SetupFSTab bool `yaml:"setup-fstab"` - SetupKernelCmdline bool `yaml:"setup-kernel-cmdline"` + SetupFSTab bool `yaml:"setup-fstab"` + SetupKernelCmdline bool `yaml:"setup-kernel-cmdline"` AppendKernelCmdline string } diff --git a/actions/overlay_action.go b/actions/overlay_action.go index ec90312..f17ecb3 100644 --- a/actions/overlay_action.go +++ b/actions/overlay_action.go @@ -1,3 +1,27 @@ +/* +Overlay Action + +Recursive copy of directory or file to target filesystem. + +Yaml syntax: + - action: overlay + origin: name + source: directory + destination: directory + +Mandatory properties: + +- source -- relative path to the directory or file located in path referenced by `origin`. +In case if this property is absent then pure path referenced by 'origin' will be used. + +Optional properties: + +- origin -- reference to named file or directory. + +- destination -- absolute path in the target rootfs where 'source' will be copied. +All existing files will be overwritten. +If destination isn't set '/' of the rootfs will be usedi. +*/ package actions import ( diff --git a/actions/pack_action.go b/actions/pack_action.go index 4e01289..a90cb1d 100644 --- a/actions/pack_action.go +++ b/actions/pack_action.go @@ -1,3 +1,20 @@ +/* +Pack Action + +Create tarball with filesystem. + +Yaml syntax: + - action: pack + file: filename.ext + compression: gz + +Mandatory properties: + +- file -- name of the output tarball. + +- compression -- compression type to use. Only 'gz' is supported at the moment. + +*/ package actions import ( @@ -8,9 +25,9 @@ import ( ) type PackAction struct { - debos.BaseAction `yaml:",inline"` - Compression string - File string + debos.BaseAction `yaml:",inline"` + Compression string + File string } func (pf *PackAction) Run(context *debos.DebosContext) error { diff --git a/actions/raw_action.go b/actions/raw_action.go index d4ad85f..c36f060 100644 --- a/actions/raw_action.go +++ b/actions/raw_action.go @@ -1,3 +1,28 @@ +/* +Raw Action + +Directly write a file to the output image at a given offset. +This is typically useful for bootloaders. + +Yaml syntax: + - action: raw + origin: name + source: filename + offset: bytes + +Mandatory properties: + +- origin -- reference to named file or directory. + +- source -- the name of file located in 'origin' to be written into the output image. + +Optional properties: + +- offset -- offset in bytes for output image file. +It is possible to use internal templating mechanism of debos to calculate offset +with sectors (512 bytes) instead of bytes, for instance: '{{ sector 256 }}'. +The default value is zero. +*/ package actions import ( diff --git a/actions/run_action.go b/actions/run_action.go index 0880238..f37fc57 100644 --- a/actions/run_action.go +++ b/actions/run_action.go @@ -1,3 +1,35 @@ +/* +Run Action + +Allows to run any available command or script in the filesystem or +in host environment. + +Yaml syntax: + - action: run + chroot: bool + postprocess: bool + script: script name + command: command line + +Properties 'command' and 'script' are mutually exclusive. + +- command -- command with arguments; the command expected to be accessible in +host's or chrooted environment -- depending on 'chroot' property. + +- script -- script with arguments; script must be located in recipe directory. + +Optional properties: + +- chroot -- run script or command in target filesystem if set to true. +In other case the command or script is executed within the build process, with +access to the filesystem and the image. In both cases it is run with root privileges. + +- postprocess -- if set script or command is executed after all other commands and +has access to the image file. + + +Properties 'chroot' and 'postprocess' are mutually exclusive. +*/ package actions import ( @@ -10,11 +42,11 @@ import ( ) type RunAction struct { - debos.BaseAction `yaml:",inline"` - Chroot bool - PostProcess bool - Script string - Command string + debos.BaseAction `yaml:",inline"` + Chroot bool + PostProcess bool + Script string + Command string } func (run *RunAction) Verify(context *debos.DebosContext) error { diff --git a/actions/unpack_action.go b/actions/unpack_action.go index 1d9fdc6..d4993b1 100644 --- a/actions/unpack_action.go +++ b/actions/unpack_action.go @@ -1,3 +1,35 @@ +/* +Unpack Action + +Unpack files from archive to the filesystem. +Useful for creating target rootfs from saved tarball with prepared file structure. + +Only (compressed) tar archives are supported currently. + +Yaml syntax: + - action: unpack + origin: name + file: file.ext + compression: gz + +Mandatory properties: + +- file -- archive's file name. It is possible to skip this property if 'origin' +referenced to downloaded file. + +One of the mandatory properties may be omitted with limitations mentioned above. +It is expected to find archive with name pointed in `file` property inside of `origin` in case if both properties are used. + +Optional properties: + +- origin -- reference to a named file or directory. +The default value is 'artifacts' directory in case if this property is omitted. + +- compression -- optional hint for unpack allowing to use proper compression method. + +Currently only 'gz', bzip2' and 'xz' compression types are supported. +If not provided an attempt to autodetect the compression type will be done. +*/ package actions import ( @@ -18,9 +50,17 @@ func (pf *UnpackAction) Verify(context *debos.DebosContext) error { return fmt.Errorf("Filename can't be empty. Please add 'file' and/or 'origin' property.") } - unpackTarOpt := debos.TarOptions(pf.Compression) - if len(pf.Compression) > 0 && len(unpackTarOpt) == 0 { - return fmt.Errorf("Compression '%s' is not supported.\n", pf.Compression) + archive, err := debos.NewArchive(pf.File) + if err != nil { + return err + } + if len(pf.Compression) > 0 { + if archive.Type() != debos.Tar { + return fmt.Errorf("Option 'compression' is supported for Tar archives only.") + } + if err := archive.AddOption("tarcompression", pf.Compression); err != nil { + return fmt.Errorf("'%s': %s", pf.File, err) + } } return nil @@ -46,5 +86,15 @@ func (pf *UnpackAction) Run(context *debos.DebosContext) error { return err } - return debos.UnpackTarArchive(infile, context.Rootdir, pf.Compression) + archive, err := debos.NewArchive(infile) + if err != nil { + return err + } + if len(pf.Compression) > 0 { + if err := archive.AddOption("tarcompression", pf.Compression); err != nil { + return err + } + } + + return archive.Unpack(context.Rootdir) } diff --git a/archiver.go b/archiver.go index 357e7f3..e67fdf6 100644 --- a/archiver.go +++ b/archiver.go @@ -1,33 +1,217 @@ package debos import ( - "log" + "fmt" "os" + "path/filepath" + "strings" ) -func TarOptions(compression string) string { +type ArchiveType int + +// Supported types +const ( + _ ArchiveType = iota // Guess archive type from file extension + Tar + Zip + Deb +) + +type ArchiveBase struct { + file string // Path to archive file + atype ArchiveType + options map[interface{}]interface{} // Archiver-depending map with additional hints +} +type ArchiveTar struct { + ArchiveBase +} +type ArchiveZip struct { + ArchiveBase +} +type ArchiveDeb struct { + ArchiveBase +} + +type Unpacker interface { + Unpack(destination string) error + RelaxedUnpack(destination string) error +} + +type Archiver interface { + Type() ArchiveType + AddOption(key, value interface{}) error + Unpacker +} + +type Archive struct { + Archiver +} + +// Unpack archive as is +func (arc *ArchiveBase) Unpack(destination string) error { + return fmt.Errorf("Unpack is not supported for '%s'", arc.file) +} + +/* +RelaxedUnpack unpack archive in relaxed mode allowing to ignore or +avoid minor issues with unpacker tool or framework. +*/ +func (arc *ArchiveBase) RelaxedUnpack(destination string) error { + return arc.Unpack(destination) +} + +func (arc *ArchiveBase) AddOption(key, value interface{}) error { + if arc.options == nil { + arc.options = make(map[interface{}]interface{}) + } + arc.options[key] = value + return nil +} + +func (arc *ArchiveBase) Type() ArchiveType { return arc.atype } + +// Helper function for unpacking with external tool +func unpack(command []string, destination string) error { + if err := os.MkdirAll(destination, 0755); err != nil { + return err + } + return Command{}.Run("unpack", command...) +} + +// Helper function for checking allowed compression types +// Returns empty string for unknown +func tarOptions(compression string) string { unpackTarOpts := map[string]string{ "gz": "-z", "bzip2": "-j", "xz": "-J", - } // Trying to guess all other supported formats + } // Trying to guess all other supported compression types return unpackTarOpts[compression] } -func UnpackTarArchive(infile, destination, compression string, options ...string) error { - if err := os.MkdirAll(destination, 0755); err != nil { - return err - } - log.Printf("Unpacking %s\n", infile) - +func (tar *ArchiveTar) Unpack(destination string) error { command := []string{"tar"} - command = append(command, options...) - command = append(command, "-x") - if unpackTarOpt := TarOptions(compression); len(unpackTarOpt) > 0 { - command = append(command, unpackTarOpt) + if options, ok := tar.options["taroptions"].([]string); ok { + for _, option := range options { + command = append(command, option) + } } - command = append(command, "-f", infile, "-C", destination) + command = append(command, "-C", destination) + command = append(command, "-x") - return Command{}.Run("unpack", command...) + if compression, ok := tar.options["tarcompression"]; ok { + if unpackTarOpt := tarOptions(compression.(string)); len(unpackTarOpt) > 0 { + command = append(command, unpackTarOpt) + } + } + command = append(command, "-f", tar.file) + + return unpack(command, destination) +} + +func (tar *ArchiveTar) RelaxedUnpack(destination string) error { + + taroptions := []string{"--no-same-owner", "--no-same-permissions"} + options, ok := tar.options["taroptions"].([]string) + defer func() { tar.options["taroptions"] = options }() + + if ok { + for _, option := range options { + taroptions = append(taroptions, option) + } + } + tar.options["taroptions"] = taroptions + + return tar.Unpack(destination) +} + +func (tar *ArchiveTar) AddOption(key, value interface{}) error { + + switch key { + case "taroptions": + // expect a slice + options, ok := value.([]string) + if !ok { + return fmt.Errorf("Wrong type for value") + } + tar.options["taroptions"] = options + + case "tarcompression": + compression, ok := value.(string) + if !ok { + return fmt.Errorf("Wrong type for value") + } + option := tarOptions(compression) + if len(option) == 0 { + return fmt.Errorf("Compression '%s' is not supported", compression) + } + tar.options["tarcompression"] = compression + + default: + return fmt.Errorf("Option '%v' is not supported for tar archive type", key) + } + return nil +} + +func (zip *ArchiveZip) Unpack(destination string) error { + command := []string{"unzip", zip.file, "-d", destination} + return unpack(command, destination) +} + +func (zip *ArchiveZip) RelaxedUnpack(destination string) error { + return zip.Unpack(destination) +} + +func (deb *ArchiveDeb) Unpack(destination string) error { + command := []string{"dpkg", "-x", deb.file, destination} + return unpack(command, destination) +} + +func (deb *ArchiveDeb) RelaxedUnpack(destination string) error { + return deb.Unpack(destination) +} + +/* +NewArchive associate correct structure and methods according to +archive type. If ArchiveType is omitted -- trying to guess the type. +Return ArchiveType or nil in case of error. +*/ +func NewArchive(file string, arcType ...ArchiveType) (Archive, error) { + var archive Archive + var atype ArchiveType + + if len(arcType) == 0 { + ext := filepath.Ext(file) + ext = strings.ToLower(ext) + + switch ext { + case ".deb": + atype = Deb + case ".zip": + atype = Zip + default: + //FIXME: guess Tar maybe? + atype = Tar + } + } else { + atype = arcType[0] + } + + common := ArchiveBase{} + common.file = file + common.atype = atype + common.options = make(map[interface{}]interface{}) + + switch atype { + case Tar: + archive = Archive{&ArchiveTar{ArchiveBase: common}} + case Zip: + archive = Archive{&ArchiveZip{ArchiveBase: common}} + case Deb: + archive = Archive{&ArchiveDeb{ArchiveBase: common}} + default: + return archive, fmt.Errorf("Unsupported archive '%s'", file) + } + return archive, nil } diff --git a/archiver_test.go b/archiver_test.go new file mode 100644 index 0000000..3023c12 --- /dev/null +++ b/archiver_test.go @@ -0,0 +1,157 @@ +package debos_test + +import ( + _ "fmt" + "github.com/go-debos/debos" + "github.com/stretchr/testify/assert" + _ "reflect" + _ "strings" + "testing" +) + +func TestBase(t *testing.T) { + + // New archive + // Expect Tar by default + _, err := debos.NewArchive("test.base", 0) + assert.EqualError(t, err, "Unsupported archive 'test.base'") + + // Test base + archive := debos.ArchiveBase{} + arcType := archive.Type() + assert.Equal(t, 0, int(arcType)) + + // Add option + err = archive.AddOption("someoption", "somevalue") + assert.Empty(t, err) + + err = archive.Unpack("/tmp/test") + assert.EqualError(t, err, "Unpack is not supported for ''") + err = archive.RelaxedUnpack("/tmp/test") + assert.EqualError(t, err, "Unpack is not supported for ''") +} + +func TestTar_default(t *testing.T) { + + // New archive + // Expect Tar by default + archive, err := debos.NewArchive("test.tar.gz") + assert.NotEmpty(t, archive) + assert.Empty(t, err) + + // Type must be Tar by default + arcType := archive.Type() + assert.Equal(t, debos.Tar, arcType) + + // Test unpack + err = archive.Unpack("/tmp/test") + // Expect unpack failure + assert.EqualError(t, err, "exit status 2") + + // Expect failure for RelaxedUnpack + err = archive.RelaxedUnpack("/tmp/test") + assert.EqualError(t, err, "exit status 2") + + // Check options + err = archive.AddOption("taroptions", []string{"--option1"}) + assert.Empty(t, err) + err = archive.Unpack("/tmp/test") + assert.EqualError(t, err, "exit status 64") + err = archive.Unpack("/proc/debostest") + assert.EqualError(t, err, "mkdir /proc/debostest: no such file or directory") + err = archive.RelaxedUnpack("/tmp/test") + assert.EqualError(t, err, "exit status 64") + + // Add wrong option + err = archive.AddOption("someoption", "somevalue") + assert.EqualError(t, err, "Option 'someoption' is not supported for tar archive type") +} + +// Check supported compression types +func TestTar_compression(t *testing.T) { + compressions := map[string]string{ + "gz": "tar -C test -x -z -f test.tar.gz", + "bzip2": "tar -C test -x -j -f test.tar.gz", + "xz": "tar -C test -x -J -f test.tar.gz", + } + + // Force type + archive, err := debos.NewArchive("test.tar.gz", debos.Tar) + assert.NotEmpty(t, archive) + assert.Empty(t, err) + // Type must be Tar + arcType := archive.Type() + assert.Equal(t, debos.Tar, arcType) + + for compression, _ := range compressions { + err = archive.AddOption("tarcompression", compression) + assert.Empty(t, err) + err := archive.Unpack("test") + assert.EqualError(t, err, "exit status 2") + } + // Check of unsupported compression type + err = archive.AddOption("tarcompression", "fake") + assert.EqualError(t, err, "Compression 'fake' is not supported") + + // Pass incorrect type + err = archive.AddOption("taroptions", nil) + assert.EqualError(t, err, "Wrong type for value") + err = archive.AddOption("tarcompression", nil) + assert.EqualError(t, err, "Wrong type for value") +} + +func TestDeb(t *testing.T) { + + // Guess Deb + archive, err := debos.NewArchive("test.deb") + assert.NotEmpty(t, archive) + assert.Empty(t, err) + + // Type must be guessed as Deb + arcType := archive.Type() + assert.Equal(t, debos.Deb, arcType) + + // Force Deb type + archive, err = debos.NewArchive("test.deb", debos.Deb) + assert.NotEmpty(t, archive) + assert.Empty(t, err) + + // Type must be Deb + arcType = archive.Type() + assert.Equal(t, debos.Deb, arcType) + + // Expect unpack failure + err = archive.Unpack("/tmp/test") + assert.EqualError(t, err, "exit status 2") + err = archive.Unpack("/proc/debostest") + assert.EqualError(t, err, "mkdir /proc/debostest: no such file or directory") + err = archive.RelaxedUnpack("/tmp/test") + assert.EqualError(t, err, "exit status 2") +} + +func TestZip(t *testing.T) { + // Guess zip + archive, err := debos.NewArchive("test.ZiP") + assert.NotEmpty(t, archive) + assert.Empty(t, err) + // Type must be guessed as Zip + arcType := archive.Type() + assert.Equal(t, debos.Zip, arcType) + + // Force Zip type + archive, err = debos.NewArchive("test.zip", debos.Zip) + assert.NotEmpty(t, archive) + assert.Empty(t, err) + + // Type must be Zip + arcType = archive.Type() + assert.Equal(t, debos.Zip, arcType) + + // Expect unpack failure + err = archive.Unpack("/tmp/test") + assert.EqualError(t, err, "exit status 9") + err = archive.Unpack("/proc/debostest") + assert.EqualError(t, err, "mkdir /proc/debostest: no such file or directory") + err = archive.RelaxedUnpack("/tmp/test") + assert.EqualError(t, err, "exit status 9") +}