mirror of
https://github.com/outbackdingo/debos.git
synced 2026-03-21 23:41:31 +00:00
Merge pull request #6 from d4s/docs
actions: add description of actions
This commit is contained in:
51
.gitignore
vendored
Normal file
51
.gitignore
vendored
Normal file
@@ -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
|
||||
6
actions/actions_doc.go
Normal file
6
actions/actions_doc.go
Normal file
@@ -0,0 +1,6 @@
|
||||
// Copyright 2017, Collabora Ltd.
|
||||
|
||||
/*
|
||||
Package 'actions' implements 'debos' modules used for OS creation.
|
||||
*/
|
||||
package actions
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,3 +1,32 @@
|
||||
/*
|
||||
Debootstrap Action
|
||||
|
||||
Construct the target rootfs with debootstrap tool.
|
||||
|
||||
Yaml syntax:
|
||||
- action: debootstrap
|
||||
mirror: URL
|
||||
suite: "name"
|
||||
components: <list of 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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
<list of partitions>
|
||||
mountpoints:
|
||||
<list of mount points>
|
||||
|
||||
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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
214
archiver.go
214
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
|
||||
}
|
||||
|
||||
157
archiver_test.go
Normal file
157
archiver_test.go
Normal file
@@ -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")
|
||||
}
|
||||
Reference in New Issue
Block a user