summaryrefslogtreecommitdiff
path: root/actions
diff options
context:
space:
mode:
Diffstat (limited to 'actions')
-rw-r--r--actions/apt_action.go7
-rw-r--r--actions/debootstrap_action.go5
-rw-r--r--actions/ostree_commit_action.go36
-rw-r--r--actions/ostree_deploy_action.go8
-rw-r--r--actions/recipe.go267
-rw-r--r--actions/recipe_action.go143
-rw-r--r--actions/recipe_test.go370
7 files changed, 834 insertions, 2 deletions
diff --git a/actions/apt_action.go b/actions/apt_action.go
index 681c069..6f3988f 100644
--- a/actions/apt_action.go
+++ b/actions/apt_action.go
@@ -6,6 +6,7 @@ Install packages and their dependencies to the target rootfs with 'apt'.
Yaml syntax:
- action: apt
recommends: bool
+ unauthenticated: bool
packages:
- package1
- package2
@@ -17,6 +18,7 @@ Mandatory properties:
Optional properties:
- recommends -- boolean indicating if suggested packages will be installed
+- unauthenticated -- boolean indicating if unauthenticated packages can be installed
*/
package actions
@@ -27,6 +29,7 @@ import (
type AptAction struct {
debos.BaseAction `yaml:",inline"`
Recommends bool
+ Unauthenticated bool
Packages []string
}
@@ -38,6 +41,10 @@ func (apt *AptAction) Run(context *debos.DebosContext) error {
aptOptions = append(aptOptions, "--no-install-recommends")
}
+ if apt.Unauthenticated {
+ aptOptions = append(aptOptions, "--allow-unauthenticated")
+ }
+
aptOptions = append(aptOptions, "install")
aptOptions = append(aptOptions, apt.Packages...)
diff --git a/actions/debootstrap_action.go b/actions/debootstrap_action.go
index 02cbb15..b4d6730 100644
--- a/actions/debootstrap_action.go
+++ b/actions/debootstrap_action.go
@@ -81,6 +81,11 @@ func (d *DebootstrapAction) RunSecondStage(context debos.DebosContext) error {
"--no-check-gpg",
"--second-stage"}
+ if d.Components != nil {
+ s := strings.Join(d.Components, ",")
+ cmdline = append(cmdline, fmt.Sprintf("--components=%s", s))
+ }
+
c := debos.NewChrootCommandForContext(context)
// Can't use nspawn for debootstrap as it wants to create device nodes
c.ChrootMethod = debos.CHROOT_METHOD_CHROOT
diff --git a/actions/ostree_commit_action.go b/actions/ostree_commit_action.go
index a0d8333..85b3fda 100644
--- a/actions/ostree_commit_action.go
+++ b/actions/ostree_commit_action.go
@@ -8,6 +8,13 @@ Yaml syntax:
repository: repository name
branch: branch name
subject: commit message
+ collection-id: org.apertis.example
+ ref-binding:
+ - branch1
+ - branch2
+ metadata:
+ key: value
+ vendor.key: somevalue
Mandatory properties:
@@ -22,10 +29,18 @@ type (https://ostree.readthedocs.io/en/latest/manual/repo/#repository-types-and-
Optional properties:
- subject -- one line message with commit description.
+
+- collection-id -- Collection ID ref binding (requires libostree 2018.6).
+
+- ref-binding -- enforce that the commit was retrieved from one of the branch names in this array.
+ If 'collection-id' is set and 'ref-binding' is empty, will default to the branch name.
+
+- metadata -- key-value pairs of meta information to be added into commit.
*/
package actions
import (
+ "fmt"
"log"
"os"
"path"
@@ -40,6 +55,9 @@ type OstreeCommitAction struct {
Branch string
Subject string
Command string
+ CollectionID string `yaml:"collection-id"`
+ RefBinding []string `yaml:"ref-binding"`
+ Metadata map[string]string
}
func emptyDir(dir string) {
@@ -54,7 +72,7 @@ func emptyDir(dir string) {
for _, f := range files {
err := os.RemoveAll(path.Join(dir, f))
if err != nil {
- log.Fatalf("Failed to remove file: %v", err)
+ log.Fatalf("Failed to remove file: %v", err)
}
}
}
@@ -77,6 +95,22 @@ func (ot *OstreeCommitAction) Run(context *debos.DebosContext) error {
opts := otbuiltin.NewCommitOptions()
opts.Subject = ot.Subject
+ for k, v := range ot.Metadata {
+ str := fmt.Sprintf("%s=%s", k, v)
+ opts.AddMetadataString = append(opts.AddMetadataString, str)
+ }
+
+ if ot.CollectionID != "" {
+ opts.CollectionID = ot.CollectionID
+ if len(ot.RefBinding) == 0 {
+ // Add current branch if not explitely set via 'ref-binding'
+ opts.RefBinding = append(opts.RefBinding, ot.Branch)
+ }
+ }
+
+ // Add values from 'ref-binding' if any
+ opts.RefBinding = append(opts.RefBinding, ot.RefBinding...)
+
ret, err := repo.Commit(context.Rootdir, ot.Branch, opts)
if err != nil {
return err
diff --git a/actions/ostree_deploy_action.go b/actions/ostree_deploy_action.go
index dab2265..7d6e6bd 100644
--- a/actions/ostree_deploy_action.go
+++ b/actions/ostree_deploy_action.go
@@ -18,6 +18,7 @@ Yaml syntax:
setup-fstab: bool
setup-kernel-cmdline: bool
appendkernelcmdline: arguments
+ collection-id: org.apertis.example
Mandatory properties:
@@ -44,6 +45,8 @@ action to the configured commandline.
- tls-client-cert-path -- path to client certificate to use for the remote repository
- tls-client-key-path -- path to client certificate key to use for the remote repository
+
+- collection-id -- Collection ID ref binding (require libostree 2018.6).
*/
package actions
@@ -70,6 +73,7 @@ type OstreeDeployAction struct {
AppendKernelCmdline string `yaml:"append-kernel-cmdline"`
TlsClientCertPath string `yaml:"tls-client-cert-path"`
TlsClientKeyPath string `yaml:"tls-client-key-path"`
+ CollectionID string `yaml:"collection-id"`
}
func NewOstreeDeployAction() *OstreeDeployAction {
@@ -140,7 +144,9 @@ func (ot *OstreeDeployAction) Run(context *debos.DebosContext) error {
/* FIXME: add support for gpg signing commits so this is no longer needed */
opts := ostree.RemoteOptions{NoGpgVerify: true,
TlsClientCertPath: ot.TlsClientCertPath,
- TlsClientKeyPath: ot.TlsClientKeyPath}
+ TlsClientKeyPath: ot.TlsClientKeyPath,
+ CollectionId: ot.CollectionID,
+ }
err = dstRepo.RemoteAdd("origin", ot.RemoteRepository, opts, nil)
if err != nil {
diff --git a/actions/recipe.go b/actions/recipe.go
new file mode 100644
index 0000000..1da6a6b
--- /dev/null
+++ b/actions/recipe.go
@@ -0,0 +1,267 @@
+/*
+Package 'recipe' implements actions mapping to YAML recipe.
+
+Recipe syntax
+
+Recipe is a YAML file which is pre-processed though Golang
+text templating engine (https://golang.org/pkg/text/template)
+
+Recipe is composed of 2 parts:
+
+- header
+
+- actions
+
+Comments are allowed and should be prefixed with '#' symbol.
+
+ # Declare variable 'Var'
+ {{- $Var := "Value" -}}
+
+ # Header
+ architecture: arm64
+
+ # Actions are executed in listed order
+ actions:
+ - action: ActionName1
+ property1: true
+
+ - action: ActionName2
+ # Use value of variable 'Var' defined above
+ property2: {{$Var}}
+
+Mandatory properties for receipt:
+
+- architecture -- target architecture
+
+- actions -- at least one action should be listed
+
+Supported actions
+
+- apt -- https://godoc.org/github.com/go-debos/debos/actions#hdr-Apt_Action
+
+- debootstrap -- https://godoc.org/github.com/go-debos/debos/actions#hdr-Debootstrap_Action
+
+- download -- https://godoc.org/github.com/go-debos/debos/actions#hdr-Download_Action
+
+- filesystem-deploy -- https://godoc.org/github.com/go-debos/debos/actions#hdr-FilesystemDeploy_Action
+
+- image-partition -- https://godoc.org/github.com/go-debos/debos/actions#hdr-ImagePartition_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
+
+- overlay -- https://godoc.org/github.com/go-debos/debos/actions#hdr-Overlay_Action
+
+- pack -- https://godoc.org/github.com/go-debos/debos/actions#hdr-Pack_Action
+
+- raw -- https://godoc.org/github.com/go-debos/debos/actions#hdr-Raw_Action
+
+- recipe -- https://godoc.org/github.com/go-debos/debos/actions#hdr-Recipe_Action
+
+- run -- https://godoc.org/github.com/go-debos/debos/actions#hdr-Run_Action
+
+- unpack -- https://godoc.org/github.com/go-debos/debos/actions#hdr-Unpack_Action
+*/
+package actions
+
+import (
+ "bytes"
+ "fmt"
+ "github.com/go-debos/debos"
+ "gopkg.in/yaml.v2"
+ "path"
+ "text/template"
+ "log"
+ "strings"
+ "reflect"
+)
+
+/* the YamlAction just embed the Action interface and implements the
+ * UnmarshalYAML function so it can select the concrete implementer of a
+ * specific action at unmarshaling time */
+type YamlAction struct {
+ debos.Action
+}
+
+type Recipe struct {
+ Architecture string
+ Actions []YamlAction
+}
+
+func (y *YamlAction) UnmarshalYAML(unmarshal func(interface{}) error) error {
+ var aux debos.BaseAction
+
+ err := unmarshal(&aux)
+ if err != nil {
+ return err
+ }
+
+ switch aux.Action {
+ case "debootstrap":
+ y.Action = NewDebootstrapAction()
+ case "pack":
+ y.Action = &PackAction{}
+ case "unpack":
+ y.Action = &UnpackAction{}
+ case "run":
+ y.Action = &RunAction{}
+ case "apt":
+ y.Action = &AptAction{}
+ case "ostree-commit":
+ y.Action = &OstreeCommitAction{}
+ case "ostree-deploy":
+ y.Action = NewOstreeDeployAction()
+ case "overlay":
+ y.Action = &OverlayAction{}
+ case "image-partition":
+ y.Action = &ImagePartitionAction{}
+ case "filesystem-deploy":
+ y.Action = NewFilesystemDeployAction()
+ case "raw":
+ y.Action = &RawAction{}
+ case "download":
+ y.Action = &DownloadAction{}
+ case "recipe":
+ y.Action = &RecipeAction{}
+ default:
+ return fmt.Errorf("Unknown action: %v", aux.Action)
+ }
+
+ unmarshal(y.Action)
+
+ return nil
+}
+
+func sector(s int) int {
+ return s * 512
+}
+
+func DumpActionStruct(iface interface{}) string {
+ var a []string
+
+ s := reflect.ValueOf(iface)
+ t := reflect.TypeOf(iface)
+
+ for i := 0; i < t.NumField(); i++ {
+ f := s.Field(i)
+ // Dump only exported entries
+ if f.CanInterface() {
+ str := fmt.Sprintf("%s: %v", s.Type().Field(i).Name, f.Interface())
+ a = append(a, str)
+ }
+ }
+
+ return strings.Join(a, ", ")
+}
+
+const tabs = 2
+
+func DumpActions(iface interface{}, depth int) {
+ tab := strings.Repeat(" ", depth * tabs)
+ entries := reflect.ValueOf(iface)
+
+ for i := 0; i < entries.NumField(); i++ {
+ if entries.Type().Field(i).Name == "Actions" {
+ log.Printf("%s %s:\n", tab, entries.Type().Field(i).Name)
+ actions := reflect.ValueOf(entries.Field(i).Interface())
+ for j := 0; j < actions.Len(); j++ {
+ yaml := reflect.ValueOf(actions.Index(j).Interface())
+ DumpActionFields(yaml.Field(0).Interface(), depth + 1)
+ }
+ } else {
+ log.Printf("%s %s: %v\n", tab, entries.Type().Field(i).Name, entries.Field(i).Interface())
+ }
+ }
+}
+
+func DumpActionFields(iface interface{}, depth int) {
+ tab := strings.Repeat(" ", depth * tabs)
+ entries := reflect.ValueOf(iface).Elem()
+
+ for i := 0; i < entries.NumField(); i++ {
+ f := entries.Field(i)
+ // Dump only exported entries
+ if f.CanInterface() {
+ switch f.Kind() {
+ case reflect.Struct:
+ if entries.Type().Field(i).Type.String() == "debos.BaseAction" {
+ // BaseAction is the only struct embbed in Action ActionFields
+ // dump it at the same level
+ log.Printf("%s- %s", tab, DumpActionStruct(f.Interface()))
+ }
+
+ case reflect.Slice:
+ s := reflect.ValueOf(f.Interface())
+ if s.Len() > 0 && s.Index(0).Kind() == reflect.Struct {
+ log.Printf("%s %s:\n", tab, entries.Type().Field(i).Name)
+ for j := 0; j < s.Len(); j++ {
+ if s.Index(j).Kind() == reflect.Struct {
+ log.Printf("%s { %s }", tab, DumpActionStruct(s.Index(j).Interface()))
+ }
+ }
+ } else {
+ log.Printf("%s %s: %s\n", tab, entries.Type().Field(i).Name, f)
+ }
+
+ default:
+ log.Printf("%s %s: %v\n", tab, entries.Type().Field(i).Name, f.Interface())
+ }
+ }
+ }
+}
+
+/*
+Parse method reads YAML recipe file and map all steps to appropriate actions.
+
+- file -- is the path to configuration file
+
+- templateVars -- optional argument allowing to use custom map for templating
+engine. Multiple template maps have no effect; only first map will be used.
+*/
+func (r *Recipe) Parse(file string, printRecipe bool, dump bool, templateVars ...map[string]string) error {
+ t := template.New(path.Base(file))
+ funcs := template.FuncMap{
+ "sector": sector,
+ }
+ t.Funcs(funcs)
+
+ if _, err := t.ParseFiles(file); err != nil {
+ return err
+ }
+
+ if len(templateVars) == 0 {
+ templateVars = append(templateVars, make(map[string]string))
+ }
+
+ data := new(bytes.Buffer)
+ if err := t.Execute(data, templateVars[0]); err != nil {
+ return err
+ }
+
+ if printRecipe || dump {
+ log.Printf("Recipe '%s':", file)
+ }
+
+ if printRecipe {
+ log.Printf("%s", data)
+ }
+
+ if err := yaml.Unmarshal(data.Bytes(), &r); err != nil {
+ return err
+ }
+
+ if dump {
+ DumpActions(reflect.ValueOf(*r).Interface(), 0)
+ }
+
+ if len(r.Architecture) == 0 {
+ return fmt.Errorf("Recipe file must have 'architecture' property")
+ }
+
+ if len(r.Actions) == 0 {
+ return fmt.Errorf("Recipe file must have at least one action")
+ }
+
+ return nil
+}
diff --git a/actions/recipe_action.go b/actions/recipe_action.go
new file mode 100644
index 0000000..e48f650
--- /dev/null
+++ b/actions/recipe_action.go
@@ -0,0 +1,143 @@
+/*
+Recipe Action
+
+Include a recipe.
+
+Yaml syntax:
+ - action: recipe
+ recipe: path to recipe
+ variables:
+ key: value
+
+Mandatory properties:
+
+- recipe -- includes the recipe actions at the given path.
+
+Optional properties:
+
+- variables -- overrides or adds new template variables.
+
+*/
+package actions
+
+import (
+ "errors"
+ "fmt"
+ "os"
+ "path/filepath"
+ "github.com/go-debos/debos"
+ "github.com/go-debos/fakemachine"
+)
+
+type RecipeAction struct {
+ debos.BaseAction `yaml:",inline"`
+ Recipe string
+ Variables map[string]string
+ Actions Recipe `yaml:"-"`
+ templateVars map[string]string
+}
+
+func (recipe *RecipeAction) Verify(context *debos.DebosContext) error {
+ if len(recipe.Recipe) == 0 {
+ return errors.New("'recipe' property can't be empty")
+ }
+
+ file := recipe.Recipe
+ if !filepath.IsAbs(file) {
+ file = filepath.Clean(context.RecipeDir + "/" + recipe.Recipe)
+ }
+
+ if _, err := os.Stat(file); os.IsNotExist(err) {
+ return err
+ }
+
+ // Initialise template vars
+ recipe.templateVars = make(map[string]string)
+ recipe.templateVars["included_recipe"] = "true"
+ recipe.templateVars["architecture"] = context.Architecture
+
+ // Add Variables to template vars
+ for k, v := range recipe.Variables {
+ recipe.templateVars[k] = v
+ }
+
+ if err := recipe.Actions.Parse(file, context.PrintRecipe, context.Verbose, recipe.templateVars); err != nil {
+ return err
+ }
+
+ if context.Architecture != recipe.Actions.Architecture {
+ return fmt.Errorf("Expect architecture '%s' but got '%s'", context.Architecture, recipe.Actions.Architecture)
+ }
+
+ for _, a := range recipe.Actions.Actions {
+ if err := a.Verify(context); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func (recipe *RecipeAction) PreMachine(context *debos.DebosContext, m *fakemachine.Machine, args *[]string) error {
+ // TODO: check args?
+
+ for _, a := range recipe.Actions.Actions {
+ if err := a.PreMachine(context, m, args); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func (recipe *RecipeAction) PreNoMachine(context *debos.DebosContext) error {
+ for _, a := range recipe.Actions.Actions {
+ if err := a.PreNoMachine(context); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func (recipe *RecipeAction) Run(context *debos.DebosContext) error {
+ recipe.LogStart()
+
+ for _, a := range recipe.Actions.Actions {
+ if err := a.Run(context); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func (recipe *RecipeAction) Cleanup(context *debos.DebosContext) error {
+ for _, a := range recipe.Actions.Actions {
+ if err := a.Cleanup(context); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func (recipe *RecipeAction) PostMachine(context *debos.DebosContext) error {
+ for _, a := range recipe.Actions.Actions {
+ if err := a.PostMachine(context); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func (recipe *RecipeAction) PostMachineCleanup(context *debos.DebosContext) error {
+ for _, a := range recipe.Actions.Actions {
+ if err := a.PostMachineCleanup(context); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
diff --git a/actions/recipe_test.go b/actions/recipe_test.go
new file mode 100644
index 0000000..972bf61
--- /dev/null
+++ b/actions/recipe_test.go
@@ -0,0 +1,370 @@
+package actions_test
+
+import (
+ "github.com/go-debos/debos"
+ "github.com/go-debos/debos/actions"
+ "github.com/stretchr/testify/assert"
+ "io/ioutil"
+ "os"
+ "testing"
+ "strings"
+)
+
+type testRecipe struct {
+ recipe string
+ err string
+}
+
+// Test if incorrect file has been passed
+func TestParse_incorrect_file(t *testing.T) {
+ var err error
+
+ var tests = []struct {
+ filename string
+ err string
+ }{
+ {
+ "non-existing.yaml",
+ "open non-existing.yaml: no such file or directory",
+ },
+ {
+ "/proc",
+ "read /proc: is a directory",
+ },
+ }
+
+ for _, test := range tests {
+ r := actions.Recipe{}
+ err = r.Parse(test.filename, false, false)
+ assert.EqualError(t, err, test.err)
+ }
+}
+
+// Check common recipe syntax
+func TestParse_syntax(t *testing.T) {
+
+ var tests = []testRecipe{
+ // Test if all actions are supported
+ {`
+architecture: arm64
+
+actions:
+ - action: apt
+ - action: debootstrap
+ - action: download
+ - action: filesystem-deploy
+ - action: image-partition
+ - action: ostree-commit
+ - action: ostree-deploy
+ - action: overlay
+ - action: pack
+ - action: raw
+ - action: run
+ - action: unpack
+ - action: recipe
+`,
+ "", // Do not expect failure
+ },
+ // Test of unknown action in list
+ {`
+architecture: arm64
+
+actions:
+ - action: test_unknown_action
+`,
+ "Unknown action: test_unknown_action",
+ },
+ // Test if 'architecture' property absence
+ {`
+actions:
+ - action: raw
+`,
+ "Recipe file must have 'architecture' property",
+ },
+ // Test if no actions listed
+ {`
+architecture: arm64
+`,
+ "Recipe file must have at least one action",
+ },
+ // Test of wrong syntax in Yaml
+ {`wrong`,
+ "yaml: unmarshal errors:\n line 1: cannot unmarshal !!str `wrong` into actions.Recipe",
+ },
+ // Test if no actions listed
+ {`
+architecture: arm64
+`,
+ "Recipe file must have at least one action",
+ },
+ }
+
+ for _, test := range tests {
+ runTest(t, test)
+ }
+}
+
+// Check template engine
+func TestParse_template(t *testing.T) {
+
+ var test = testRecipe{
+ // Test template variables
+ `
+{{ $action:= or .action "download" }}
+architecture: arm64
+actions:
+ - action: {{ $action }}
+`,
+ "", // Do not expect failure
+ }
+
+ { // Test of embedded template
+ r := runTest(t, test)
+ assert.Equalf(t, r.Actions[0].String(), "download",
+ "Fail to use embedded variable definition from recipe:%s\n",
+ test.recipe)
+ }
+
+ { // Test of user-defined template variable
+ var templateVars = map[string]string{
+ "action": "pack",
+ }
+
+ r := runTest(t, test, templateVars)
+ assert.Equalf(t, r.Actions[0].String(), "pack",
+ "Fail to redefine variable with user-defined map:%s\n",
+ test.recipe)
+ }
+}
+
+// Test of 'sector' function embedded to recipe package
+func TestParse_sector(t *testing.T) {
+ var testSector = testRecipe{
+ // Fail with unknown action
+ `
+architecture: arm64
+
+actions:
+ - action: {{ sector 42 }}
+`,
+ "Unknown action: 21504",
+ }
+ runTest(t, testSector)
+}
+
+func runTest(t *testing.T, test testRecipe, templateVars ...map[string]string) actions.Recipe {
+ file, err := ioutil.TempFile(os.TempDir(), "recipe")
+ assert.Empty(t, err)
+ defer os.Remove(file.Name())
+
+ file.WriteString(test.recipe)
+ file.Close()
+
+ r := actions.Recipe{}
+ if len(templateVars) == 0 {
+ err = r.Parse(file.Name(), false, false)
+ } else {
+ err = r.Parse(file.Name(), false, false, templateVars[0])
+ }
+
+ failed := false
+
+ if len(test.err) > 0 {
+ // Expected error?
+ failed = !assert.EqualError(t, err, test.err)
+ } else {
+ // Unexpected error
+ failed = !assert.Empty(t, err)
+ }
+
+ if failed {
+ t.Logf("Failed recipe:%s\n", test.recipe)
+ }
+
+ return r
+}
+
+type subRecipe struct {
+ name string
+ recipe string
+}
+
+type testSubRecipe struct {
+ recipe string
+ subrecipe subRecipe
+ err string
+}
+
+func TestSubRecipe(t *testing.T) {
+ // Embedded recipes
+ var recipeAmd64 = subRecipe {
+ "amd64.yaml",
+ `
+architecture: amd64
+
+actions:
+ - action: run
+ command: ok.sh
+`,
+ }
+ var recipeInheritedArch = subRecipe {
+ "inherited.yaml",
+ `
+{{- $architecture := or .architecture "armhf" }}
+architecture: {{ $architecture }}
+
+actions:
+ - action: run
+ command: ok.sh
+`,
+ }
+ var recipeArmhf = subRecipe {
+ "armhf.yaml",
+ `
+architecture: armhf
+
+actions:
+ - action: run
+ command: ok.sh
+`,
+ }
+ var recipeIncluded = subRecipe {
+ "included.yaml",
+ `
+{{- $included_recipe := or .included_recipe "false"}}
+architecture: amd64
+
+actions:
+ - action: run
+ command: ok.sh
+ {{- if ne $included_recipe "true" }}
+ - action: recipe
+ recipe: armhf.yaml
+ {{- end }}
+`,
+ }
+
+ // test recipes
+ var tests = []testSubRecipe {
+ {
+ // Test recipe same architecture OK
+ `
+architecture: amd64
+
+actions:
+ - action: recipe
+ recipe: amd64.yaml
+`,
+ recipeAmd64,
+ "", // Do not expect failure
+ },
+ {
+ // Test recipe with inherited architecture OK
+ `
+architecture: amd64
+
+actions:
+ - action: recipe
+ recipe: inherited.yaml
+`,
+ recipeInheritedArch,
+ "", // Do not expect failure
+ },
+ {
+ // Fail with unknown recipe
+ `
+architecture: amd64
+
+actions:
+ - action: recipe
+ recipe: unknown_recipe.yaml
+`,
+ recipeAmd64,
+ "stat /tmp/unknown_recipe.yaml: no such file or directory",
+ },
+ {
+ // Fail with different architecture recipe
+ `
+architecture: amd64
+
+actions:
+ - action: recipe
+ recipe: armhf.yaml
+`,
+ recipeArmhf,
+ "Expect architecture 'amd64' but got 'armhf'",
+ },
+ {
+ // Test included_recipe prevents parsing OK
+ `
+architecture: amd64
+
+actions:
+ - action: recipe
+ recipe: included.yaml
+`,
+ recipeIncluded,
+ "", // Do not expect failure
+ },
+ }
+
+ for _, test := range tests {
+ runTestWithSubRecipes(t, test)
+ }
+}
+
+func runTestWithSubRecipes(t *testing.T, test testSubRecipe, templateVars ...map[string]string) actions.Recipe {
+ var context debos.DebosContext
+ dir, err := ioutil.TempDir("", "go-debos")
+ assert.Empty(t, err)
+ defer os.RemoveAll(dir)
+
+ file, err := ioutil.TempFile(dir, "recipe")
+ assert.Empty(t, err)
+ defer os.Remove(file.Name())
+
+ file.WriteString(test.recipe)
+ file.Close()
+
+ file_subrecipe, err := os.Create(dir + "/" + test.subrecipe.name)
+ assert.Empty(t, err)
+ defer os.Remove(file_subrecipe.Name())
+
+ file_subrecipe.WriteString(test.subrecipe.recipe)
+ file_subrecipe.Close()
+
+ r := actions.Recipe{}
+ if len(templateVars) == 0 {
+ err = r.Parse(file.Name(), false, false)
+ } else {
+ err = r.Parse(file.Name(), false, false, templateVars[0])
+ }
+
+ // Should not expect error during parse
+ failed := !assert.Empty(t, err)
+
+ if !failed {
+ context.Architecture = r.Architecture
+ context.RecipeDir = dir
+
+ for _, a := range r.Actions {
+ if err = a.Verify(&context); err != nil {
+ break
+ }
+ }
+
+ if len(test.err) > 0 {
+ // Expected error?
+ failed = !assert.EqualError(t, err, strings.Replace(test.err, "/tmp", dir, 1))
+ } else {
+ // Unexpected error
+ failed = !assert.Empty(t, err)
+ }
+ }
+
+ if failed {
+ t.Logf("Failed recipe:%s\n", test.recipe)
+ }
+
+ return r
+}