*: Support yaml override on the command line

Resolves #32

Signed-off-by: Thomas Hipp <thomas.hipp@canonical.com>
This commit is contained in:
Thomas Hipp 2018-03-14 22:34:46 +01:00
parent b8d4fca5ed
commit 13be123f2d
No known key found for this signature in database
GPG Key ID: 993408D1137B7D51
3 changed files with 151 additions and 3 deletions

View File

@ -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)

View File

@ -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
}

View File

@ -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")
}
}