[lxc-devel] [distrobuilder/master] *: Support yaml override on the command line

monstermunchkin on Github lxc-bot at linuxcontainers.org
Wed Mar 14 21:35:57 UTC 2018


A non-text attachment was scrubbed...
Name: not available
Type: text/x-mailbox
Size: 379 bytes
Desc: not available
URL: <http://lists.linuxcontainers.org/pipermail/lxc-devel/attachments/20180314/fe8b9cd9/attachment.bin>
-------------- next part --------------
From 13be123f2dfc98b9347fa60cbb359a803c26a1f6 Mon Sep 17 00:00:00 2001
From: Thomas Hipp <thomas.hipp at canonical.com>
Date: Wed, 14 Mar 2018 22:34:46 +0100
Subject: [PATCH] *: Support yaml override on the command line

Resolves #32

Signed-off-by: Thomas Hipp <thomas.hipp at canonical.com>
---
 distrobuilder/main.go     | 24 +++++++++++++--
 shared/definition.go      | 75 +++++++++++++++++++++++++++++++++++++++++++++++
 shared/definition_test.go | 55 ++++++++++++++++++++++++++++++++++
 3 files changed, 151 insertions(+), 3 deletions(-)

diff --git a/distrobuilder/main.go b/distrobuilder/main.go
index c101f21..7636361 100644
--- a/distrobuilder/main.go
+++ b/distrobuilder/main.go
@@ -54,10 +54,12 @@ import "C"
 import (
 	"bufio"
 	"bytes"
+	"errors"
 	"fmt"
 	"io"
 	"os"
 	"path/filepath"
+	"strings"
 
 	lxd "github.com/lxc/lxd/shared"
 	"github.com/spf13/cobra"
@@ -70,6 +72,7 @@ import (
 type cmdGlobal struct {
 	flagCleanup  bool
 	flagCacheDir string
+	flagOptions  []string
 
 	definition *shared.Definition
 	sourceDir  string
@@ -97,6 +100,8 @@ func main() {
 		"Clean up cache directory")
 	app.PersistentFlags().StringVar(&globalCmd.flagCacheDir, "cache-dir",
 		"/var/cache/distrobuilder", "Cache directory"+"``")
+	app.PersistentFlags().StringSliceVarP(&globalCmd.flagOptions, "options", "o",
+		[]string{}, "Override options (list of key=value)"+"``")
 
 	// LXC sub-commands
 	LXCCmd := cmdLXC{global: &globalCmd}
@@ -152,7 +157,7 @@ func (c *cmdGlobal) preRunBuild(cmd *cobra.Command, args []string) error {
 	}
 
 	// Get the image definition
-	c.definition, err = getDefinition(args[0])
+	c.definition, err = getDefinition(args[0], c.flagOptions)
 	if err != nil {
 		return err
 	}
@@ -235,7 +240,7 @@ func (c *cmdGlobal) preRunPack(cmd *cobra.Command, args []string) error {
 	}
 
 	// Get the image definition
-	c.definition, err = getDefinition(args[0])
+	c.definition, err = getDefinition(args[0], c.flagOptions)
 	if err != nil {
 		return err
 	}
@@ -252,7 +257,7 @@ func (c *cmdGlobal) postRun(cmd *cobra.Command, args []string) error {
 	return nil
 }
 
-func getDefinition(fname string) (*shared.Definition, error) {
+func getDefinition(fname string, options []string) (*shared.Definition, error) {
 	// Read the provided file, or if none was given, read from stdin
 	var buf bytes.Buffer
 	if fname == "" || fname == "-" {
@@ -280,6 +285,19 @@ func getDefinition(fname string) (*shared.Definition, error) {
 		return nil, err
 	}
 
+	// Set options from the command line
+	for _, o := range options {
+		parts := strings.Split(o, "=")
+		if len(parts) != 2 {
+			return nil, errors.New("Options need to be of type key=value")
+		}
+
+		err := def.SetValue(parts[0], parts[1])
+		if err != nil {
+			return nil, fmt.Errorf("Failed to set option %s: %s", o, err)
+		}
+	}
+
 	// Apply some defaults on top of the provided configuration
 	shared.SetDefinitionDefaults(&def)
 
diff --git a/shared/definition.go b/shared/definition.go
index 9e58456..9a44c38 100644
--- a/shared/definition.go
+++ b/shared/definition.go
@@ -3,6 +3,8 @@ package shared
 import (
 	"errors"
 	"fmt"
+	"reflect"
+	"strconv"
 	"strings"
 	"time"
 
@@ -93,6 +95,40 @@ type Definition struct {
 	Mappings DefinitionMappings `yaml:"mappings,omitempty"`
 }
 
+// SetValue writes the provided value to a field represented by the yaml tag 'key'.
+func (d *Definition) SetValue(key string, value interface{}) error {
+	// Walk through the definition and find the field with the given key
+	field, err := getFieldByTag(reflect.ValueOf(d).Elem(), reflect.TypeOf(d).Elem(), key)
+	if err != nil {
+		return err
+	}
+
+	// Fail if the field cannot be set
+	if !field.CanSet() {
+		return fmt.Errorf("Cannot set value for %s", key)
+	}
+
+	if reflect.TypeOf(value).Kind() != field.Kind() {
+		return fmt.Errorf("Cannot assign %s value to %s",
+			reflect.TypeOf(value).Kind(), field.Kind())
+	}
+
+	switch reflect.TypeOf(value).Kind() {
+	case reflect.Bool:
+		field.SetBool(value.(bool))
+	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+		field.SetInt(value.(int64))
+	case reflect.String:
+		field.SetString(value.(string))
+	case reflect.Uint, reflect.Uintptr, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+		field.SetUint(value.(uint64))
+	default:
+		return fmt.Errorf("Unknown value type %s", reflect.TypeOf(value).Kind())
+	}
+
+	return nil
+}
+
 // SetDefinitionDefaults sets some default values for the given Definition.
 func SetDefinitionDefaults(def *Definition) {
 	// default to local arch
@@ -187,3 +223,42 @@ func ValidateDefinition(def Definition) error {
 
 	return nil
 }
+
+func getFieldByTag(v reflect.Value, t reflect.Type, tag string) (reflect.Value, error) {
+	parts := strings.SplitN(tag, ".", 2)
+
+	if t.Kind() == reflect.Slice {
+		// Get index, e.g. '0' from tag 'foo.0'
+		value, err := strconv.Atoi(parts[0])
+		if err != nil {
+			return reflect.Value{}, err
+		}
+
+		if t.Elem().Kind() == reflect.Struct {
+			// Make sure we are in range, otherwise return error
+			if value < 0 || value >= v.Len() {
+				return reflect.Value{}, errors.New("Index out of range")
+			}
+			return getFieldByTag(v.Index(value), t.Elem(), parts[1])
+		}
+
+		// Primitive type
+		return v.Index(value), nil
+	}
+
+	if t.Kind() == reflect.Struct {
+		// Find struct field with correct tag
+		for i := 0; i < t.NumField(); i++ {
+			value, ok := t.Field(i).Tag.Lookup("yaml")
+			if ok && strings.Split(value, ",")[0] == parts[0] {
+				if len(parts) == 1 {
+					return v.Field(i), nil
+				}
+				return getFieldByTag(v.Field(i), t.Field(i).Type, parts[1])
+			}
+		}
+	}
+
+	// Return its value if it's a primitive type
+	return v, nil
+}
diff --git a/shared/definition_test.go b/shared/definition_test.go
index ecb2f42..75e9506 100644
--- a/shared/definition_test.go
+++ b/shared/definition_test.go
@@ -201,3 +201,58 @@ func TestValidateDefinition(t *testing.T) {
 		}
 	}
 }
+
+func TestDefinitionSetValue(t *testing.T) {
+	d := Definition{
+		Image: DefinitionImage{
+			Distribution: "ubuntu",
+			Release:      "artful",
+		},
+		Source: DefinitionSource{
+			Downloader: "debootstrap",
+			URL:        "https://ubuntu.com",
+			Keys:       []string{"0xCODE"},
+		},
+		Packages: DefinitionPackages{
+			Manager: "apt",
+		},
+		Actions: []DefinitionAction{
+			{
+				Trigger: "post-update",
+				Action:  "/bin/true",
+			},
+			{
+				Trigger: "post-packages",
+				Action:  "/bin/false",
+			},
+		},
+	}
+
+	err := d.SetValue("image.release", "bionic")
+	if err != nil {
+		t.Fatalf("Unexpected error: %s", err)
+	}
+	if d.Image.Release != "bionic" {
+		t.Fatalf("Expected '%s', got '%s'", "bionic", d.Image.Release)
+	}
+
+	err = d.SetValue("actions.0.trigger", "post-files")
+	if err != nil {
+		t.Fatalf("Unexpected error: %s", err)
+	}
+	if d.Actions[0].Trigger != "post-files" {
+		t.Fatalf("Expected '%s', got '%s'", "post-files", d.Actions[0].Trigger)
+	}
+
+	// Index out of bounds
+	err = d.SetValue("actions.3.trigger", "post-files")
+	if err == nil || err.Error() != "Index out of range" {
+		t.Fatal("Expected index out of range")
+	}
+
+	// Nonsense
+	err = d.SetValue("image", "[foo: bar]")
+	if err == nil || err.Error() != "Cannot assign string value to struct" {
+		t.Fatal("Expected unsupported assignment")
+	}
+}


More information about the lxc-devel mailing list