*: Support yaml override on the command line
Resolves #32 Signed-off-by: Thomas Hipp <thomas.hipp@canonical.com>
This commit is contained in:
parent
b8d4fca5ed
commit
13be123f2d
@ -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)
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user