Compare commits

..

16 Commits

Author SHA1 Message Date
Alex Crawford
c8e864fef5 coreos-cloudinit: bump to v1.1.0 2014-12-30 16:51:24 +01:00
Alex Crawford
60a3377e7c Merge pull request #290 from crawford/yaml
Improved YAML parsing
2014-12-30 16:24:12 +01:00
Alex Crawford
5527f09778 config: fix parsing of file permissions
These reintroduces the braindead '744' syntax for file permissions. Even
though this number isn't octal, it is assumed by convention to be. In
order to pull this off, coerceNodes() was introduced to try to
counteract the type inferrencing that occurs during the yaml
unmarshalling. The config is unmarshalled twice: once into an empty
interface and once into the CloudConfig structure. The two resulting
node structures are combined together. The nodes from the CloudConfig
process replace those from the interface{} when the types of the two
nodes are compatible. For example, with the input `0744`, yaml
interprets that as the integer 484 giving us the nodes '0744'(string)
and 484(int). Because the types string and int are compatible, we opt to
take the string node instead of the integer.
2014-12-30 16:20:21 +01:00
Alex Crawford
54a64454b9 validate: fix printing for non-string values 2014-12-30 16:20:21 +01:00
Alex Crawford
0e70d4f01f config: add validity check for file permissions 2014-12-30 16:20:21 +01:00
Alex Crawford
af8e590575 config: change valid tag to use regexp
A regular expression is much more useful than a list of strings.
2014-12-30 16:20:21 +01:00
Alex Crawford
40d943fb7a reboot-strategy: remove the 'false' value
Since we no longer do a two-stage unmarshal, the 'false' value will no
longer be necessary.
2014-12-30 16:20:21 +01:00
Alex Crawford
248536a5cd config: use a YAML transform to normalize keys
This removes the problematic two-pass unmarshalling.
2014-12-30 16:20:21 +01:00
Alex Crawford
4ed1d03c97 godeps: bump github.com/coreos/yaml 2014-12-30 16:20:20 +01:00
Alex Crawford
057ab37364 config: seperate the CoreOS type from CloudConfig
Renamed Coreos to CoreOS while I was at it.
2014-12-30 16:20:20 +01:00
Alex Crawford
182241c8d3 config: clean up and remove some tests
Small modification to make these align with our test-table-style tests.
Also removed TestCloudConfigInvalidKeys since it hasn't been a useful
test since d3294bcb86.
2014-12-30 16:19:00 +01:00
Michael Marineau
edced59fa6 Merge pull request #281 from thommay/flannel_env_file
Create an environment file for flannel
2014-12-29 15:07:08 -08:00
Thom May
9be836df31 Create an environment file for flannel
Rather than using a systemd overlay, allow docker to load the
environment file. This is due to coreos/coreos-overlay#1002
2014-12-29 10:27:22 +00:00
Jonathan Boulle
4e54447b8e Merge pull request #286 from jonboulle/master
Godeps: switch to coreos/yaml
2014-12-20 15:43:55 -08:00
Jonathan Boulle
999c38b09b Godeps: switch to coreos/yaml 2014-12-20 15:31:02 -08:00
Alex Crawford
06d13de5c3 coreos-cloudinit: bump to v1.0.2+git 2014-12-12 17:38:28 -08:00
40 changed files with 510 additions and 259 deletions

10
Godeps/Godeps.json generated
View File

@@ -1,6 +1,6 @@
{ {
"ImportPath": "github.com/coreos/coreos-cloudinit", "ImportPath": "github.com/coreos/coreos-cloudinit",
"GoVersion": "go1.3.1", "GoVersion": "go1.3.3",
"Packages": [ "Packages": [
"./..." "./..."
], ],
@@ -13,6 +13,10 @@
"ImportPath": "github.com/coreos/go-systemd/dbus", "ImportPath": "github.com/coreos/go-systemd/dbus",
"Rev": "4fbc5060a317b142e6c7bfbedb65596d5f0ab99b" "Rev": "4fbc5060a317b142e6c7bfbedb65596d5f0ab99b"
}, },
{
"ImportPath": "github.com/coreos/yaml",
"Rev": "6b16a5714269b2f70720a45406b1babd947a17ef"
},
{ {
"ImportPath": "github.com/dotcloud/docker/pkg/netlink", "ImportPath": "github.com/dotcloud/docker/pkg/netlink",
"Comment": "v0.11.1-359-g55d41c3e21e1", "Comment": "v0.11.1-359-g55d41c3e21e1",
@@ -25,10 +29,6 @@
{ {
"ImportPath": "github.com/tarm/goserial", "ImportPath": "github.com/tarm/goserial",
"Rev": "cdabc8d44e8e84f58f18074ae44337e1f2f375b9" "Rev": "cdabc8d44e8e84f58f18074ae44337e1f2f375b9"
},
{
"ImportPath": "gopkg.in/yaml.v1",
"Rev": "9f9df34309c04878acc86042b16630b0f696e1de"
} }
] ]
} }

View File

@@ -1,3 +1,6 @@
Note: This is a fork of https://github.com/go-yaml/yaml. The following README
doesn't necessarily apply to this fork.
# YAML support for the Go language # YAML support for the Go language
Introduction Introduction

View File

@@ -30,13 +30,15 @@ type node struct {
// Parser, produces a node tree out of a libyaml event stream. // Parser, produces a node tree out of a libyaml event stream.
type parser struct { type parser struct {
parser yaml_parser_t parser yaml_parser_t
event yaml_event_t event yaml_event_t
doc *node doc *node
transform transformString
} }
func newParser(b []byte) *parser { func newParser(b []byte, t transformString) *parser {
p := parser{} p := parser{transform: t}
if !yaml_parser_initialize(&p.parser) { if !yaml_parser_initialize(&p.parser) {
panic("Failed to initialize YAML emitter") panic("Failed to initialize YAML emitter")
} }
@@ -175,7 +177,10 @@ func (p *parser) mapping() *node {
p.anchor(n, p.event.anchor) p.anchor(n, p.event.anchor)
p.skip() p.skip()
for p.event.typ != yaml_MAPPING_END_EVENT { for p.event.typ != yaml_MAPPING_END_EVENT {
n.children = append(n.children, p.parse(), p.parse()) key := p.parse()
key.value = p.transform(key.value)
value := p.parse()
n.children = append(n.children, key, value)
} }
p.skip() p.skip()
return n return n

View File

@@ -1,8 +1,8 @@
package yaml_test package yaml_test
import ( import (
"github.com/coreos/yaml"
. "gopkg.in/check.v1" . "gopkg.in/check.v1"
"gopkg.in/yaml.v1"
"math" "math"
"reflect" "reflect"
"strings" "strings"
@@ -557,6 +557,23 @@ func (s *S) TestUnmarshalWithFalseSetterIgnoresValue(c *C) {
c.Assert(m["ghi"].value, Equals, 3) c.Assert(m["ghi"].value, Equals, 3)
} }
func (s *S) TestUnmarshalWithTransform(c *C) {
data := `{a_b: 1, c-d: 2, e-f_g: 3, h_i-j: 4}`
expect := map[string]int{
"a_b": 1,
"c_d": 2,
"e_f_g": 3,
"h_i_j": 4,
}
m := map[string]int{}
yaml.UnmarshalMappingKeyTransform = func(i string) string {
return strings.Replace(i, "-", "_", -1)
}
err := yaml.Unmarshal([]byte(data), m)
c.Assert(err, IsNil)
c.Assert(m, DeepEquals, expect)
}
// From http://yaml.org/type/merge.html // From http://yaml.org/type/merge.html
var mergeTests = ` var mergeTests = `
anchors: anchors:

View File

@@ -7,8 +7,8 @@ import (
"strings" "strings"
"time" "time"
"github.com/coreos/yaml"
. "gopkg.in/check.v1" . "gopkg.in/check.v1"
"gopkg.in/yaml.v1"
) )
var marshalIntTest = 123 var marshalIntTest = 123

View File

@@ -84,7 +84,7 @@ type Getter interface {
func Unmarshal(in []byte, out interface{}) (err error) { func Unmarshal(in []byte, out interface{}) (err error) {
defer handleErr(&err) defer handleErr(&err)
d := newDecoder() d := newDecoder()
p := newParser(in) p := newParser(in, UnmarshalMappingKeyTransform)
defer p.destroy() defer p.destroy()
node := p.parse() node := p.parse()
if node != nil { if node != nil {
@@ -146,6 +146,17 @@ func Marshal(in interface{}) (out []byte, err error) {
return return
} }
// UnmarshalMappingKeyTransform is a string transformation that is applied to
// each mapping key in a YAML document before it is unmarshalled. By default,
// UnmarshalMappingKeyTransform is an identity transform (no modification).
var UnmarshalMappingKeyTransform transformString = identityTransform
type transformString func(in string) (out string)
func identityTransform(in string) (out string) {
return in
}
// -------------------------------------------------------------------------- // --------------------------------------------------------------------------
// Maintain a mapping of keys to structure field indexes // Maintain a mapping of keys to structure field indexes

View File

@@ -19,9 +19,10 @@ package config
import ( import (
"fmt" "fmt"
"reflect" "reflect"
"regexp"
"strings" "strings"
"github.com/coreos/coreos-cloudinit/Godeps/_workspace/src/gopkg.in/yaml.v1" "github.com/coreos/coreos-cloudinit/Godeps/_workspace/src/github.com/coreos/yaml"
) )
// CloudConfig encapsulates the entire cloud-config configuration file and maps // CloudConfig encapsulates the entire cloud-config configuration file and maps
@@ -29,15 +30,7 @@ import (
// used for internal use) have the YAML tag '-' so that they aren't marshalled. // used for internal use) have the YAML tag '-' so that they aren't marshalled.
type CloudConfig struct { type CloudConfig struct {
SSHAuthorizedKeys []string `yaml:"ssh_authorized_keys"` SSHAuthorizedKeys []string `yaml:"ssh_authorized_keys"`
Coreos struct { CoreOS CoreOS `yaml:"coreos"`
Etcd Etcd `yaml:"etcd"`
Flannel Flannel `yaml:"flannel"`
Fleet Fleet `yaml:"fleet"`
Locksmith Locksmith `yaml:"locksmith"`
OEM OEM `yaml:"oem"`
Update Update `yaml:"update"`
Units []Unit `yaml:"units"`
} `yaml:"coreos"`
WriteFiles []File `yaml:"write_files"` WriteFiles []File `yaml:"write_files"`
Hostname string `yaml:"hostname"` Hostname string `yaml:"hostname"`
Users []User `yaml:"users"` Users []User `yaml:"users"`
@@ -46,6 +39,16 @@ type CloudConfig struct {
NetworkConfig string `yaml:"-"` NetworkConfig string `yaml:"-"`
} }
type CoreOS struct {
Etcd Etcd `yaml:"etcd"`
Flannel Flannel `yaml:"flannel"`
Fleet Fleet `yaml:"fleet"`
Locksmith Locksmith `yaml:"locksmith"`
OEM OEM `yaml:"oem"`
Update Update `yaml:"update"`
Units []Unit `yaml:"units"`
}
func IsCloudConfig(userdata string) bool { func IsCloudConfig(userdata string) bool {
header := strings.SplitN(userdata, "\n", 2)[0] header := strings.SplitN(userdata, "\n", 2)[0]
@@ -61,15 +64,12 @@ func IsCloudConfig(userdata string) bool {
// string of YAML), returning any error encountered. It will ignore unknown // string of YAML), returning any error encountered. It will ignore unknown
// fields but log encountering them. // fields but log encountering them.
func NewCloudConfig(contents string) (*CloudConfig, error) { func NewCloudConfig(contents string) (*CloudConfig, error) {
yaml.UnmarshalMappingKeyTransform = func(nameIn string) (nameOut string) {
return strings.Replace(nameIn, "-", "_", -1)
}
var cfg CloudConfig var cfg CloudConfig
ncontents, err := normalizeConfig(contents) err := yaml.Unmarshal([]byte(contents), &cfg)
if err != nil { return &cfg, err
return &cfg, err
}
if err = yaml.Unmarshal(ncontents, &cfg); err != nil {
return &cfg, err
}
return &cfg, nil
} }
func (cc CloudConfig) String() string { func (cc CloudConfig) String() string {
@@ -92,7 +92,7 @@ func IsZero(c interface{}) bool {
type ErrorValid struct { type ErrorValid struct {
Value string Value string
Valid []string Valid string
Field string Field string
} }
@@ -126,16 +126,15 @@ func AssertValid(value reflect.Value, valid string) *ErrorValid {
if valid == "" || isZero(value) { if valid == "" || isZero(value) {
return nil return nil
} }
vs := fmt.Sprintf("%v", value.Interface()) vs := fmt.Sprintf("%v", value.Interface())
valids := strings.Split(valid, ",") if m, _ := regexp.MatchString(valid, vs); m {
for _, valid := range valids { return nil
if vs == valid {
return nil
}
} }
return &ErrorValid{ return &ErrorValid{
Value: vs, Value: vs,
Valid: valids, Valid: valid,
} }
} }
@@ -157,31 +156,3 @@ func isZero(v reflect.Value) bool {
func isFieldExported(f reflect.StructField) bool { func isFieldExported(f reflect.StructField) bool {
return f.PkgPath == "" return f.PkgPath == ""
} }
func normalizeConfig(config string) ([]byte, error) {
var cfg map[interface{}]interface{}
if err := yaml.Unmarshal([]byte(config), &cfg); err != nil {
return nil, err
}
return yaml.Marshal(normalizeKeys(cfg))
}
func normalizeKeys(m map[interface{}]interface{}) map[interface{}]interface{} {
for k, v := range m {
if m, ok := m[k].(map[interface{}]interface{}); ok {
normalizeKeys(m)
}
if s, ok := m[k].([]interface{}); ok {
for _, e := range s {
if m, ok := e.(map[interface{}]interface{}); ok {
normalizeKeys(m)
}
}
}
delete(m, k)
m[strings.Replace(fmt.Sprint(k), "-", "_", -1)] = v
}
return m
}

View File

@@ -18,13 +18,67 @@ package config
import ( import (
"reflect" "reflect"
"regexp"
"strings" "strings"
"testing" "testing"
) )
func TestNewCloudConfig(t *testing.T) {
tests := []struct {
contents string
config CloudConfig
}{
{},
{
contents: "#cloud-config\nwrite_files:\n - path: underscore",
config: CloudConfig{WriteFiles: []File{File{Path: "underscore"}}},
},
{
contents: "#cloud-config\nwrite-files:\n - path: hyphen",
config: CloudConfig{WriteFiles: []File{File{Path: "hyphen"}}},
},
{
contents: "#cloud-config\ncoreos:\n update:\n reboot-strategy: off",
config: CloudConfig{CoreOS: CoreOS{Update: Update{RebootStrategy: "off"}}},
},
{
contents: "#cloud-config\ncoreos:\n update:\n reboot-strategy: false",
config: CloudConfig{CoreOS: CoreOS{Update: Update{RebootStrategy: "false"}}},
},
{
contents: "#cloud-config\nwrite_files:\n - permissions: 0744",
config: CloudConfig{WriteFiles: []File{File{RawFilePermissions: "0744"}}},
},
{
contents: "#cloud-config\nwrite_files:\n - permissions: 744",
config: CloudConfig{WriteFiles: []File{File{RawFilePermissions: "744"}}},
},
{
contents: "#cloud-config\nwrite_files:\n - permissions: '0744'",
config: CloudConfig{WriteFiles: []File{File{RawFilePermissions: "0744"}}},
},
{
contents: "#cloud-config\nwrite_files:\n - permissions: '744'",
config: CloudConfig{WriteFiles: []File{File{RawFilePermissions: "744"}}},
},
}
for i, tt := range tests {
config, err := NewCloudConfig(tt.contents)
if err != nil {
t.Errorf("bad error (test case #%d): want %v, got %s", i, nil, err)
}
if !reflect.DeepEqual(&tt.config, config) {
t.Errorf("bad config (test case #%d): want %#v, got %#v", i, tt.config, config)
}
}
}
func TestIsZero(t *testing.T) { func TestIsZero(t *testing.T) {
for _, tt := range []struct { tests := []struct {
c interface{} c interface{}
empty bool empty bool
}{ }{
{struct{}{}, true}, {struct{}{}, true},
@@ -34,7 +88,9 @@ func TestIsZero(t *testing.T) {
{struct{ A string }{A: "hello"}, false}, {struct{ A string }{A: "hello"}, false},
{struct{ A int }{}, true}, {struct{ A int }{}, true},
{struct{ A int }{A: 1}, false}, {struct{ A int }{A: 1}, false},
} { }
for _, tt := range tests {
if empty := IsZero(tt.c); tt.empty != empty { if empty := IsZero(tt.c); tt.empty != empty {
t.Errorf("bad result (%q): want %t, got %t", tt.c, tt.empty, empty) t.Errorf("bad result (%q): want %t, got %t", tt.c, tt.empty, empty)
} }
@@ -42,66 +98,68 @@ func TestIsZero(t *testing.T) {
} }
func TestAssertStructValid(t *testing.T) { func TestAssertStructValid(t *testing.T) {
for _, tt := range []struct { tests := []struct {
c interface{} c interface{}
err error err error
}{ }{
{struct{}{}, nil}, {struct{}{}, nil},
{struct { {struct {
A, b string `valid:"1,2"` A, b string `valid:"^1|2$"`
}{}, nil}, }{}, nil},
{struct { {struct {
A, b string `valid:"1,2"` A, b string `valid:"^1|2$"`
}{A: "1", b: "2"}, nil}, }{A: "1", b: "2"}, nil},
{struct { {struct {
A, b string `valid:"1,2"` A, b string `valid:"^1|2$"`
}{A: "1", b: "hello"}, nil}, }{A: "1", b: "hello"}, nil},
{struct { {struct {
A, b string `valid:"1,2"` A, b string `valid:"^1|2$"`
}{A: "hello", b: "2"}, &ErrorValid{Value: "hello", Field: "A", Valid: []string{"1", "2"}}}, }{A: "hello", b: "2"}, &ErrorValid{Value: "hello", Field: "A", Valid: "^1|2$"}},
{struct { {struct {
A, b int `valid:"1,2"` A, b int `valid:"^1|2$"`
}{}, nil}, }{}, nil},
{struct { {struct {
A, b int `valid:"1,2"` A, b int `valid:"^1|2$"`
}{A: 1, b: 2}, nil}, }{A: 1, b: 2}, nil},
{struct { {struct {
A, b int `valid:"1,2"` A, b int `valid:"^1|2$"`
}{A: 1, b: 9}, nil}, }{A: 1, b: 9}, nil},
{struct { {struct {
A, b int `valid:"1,2"` A, b int `valid:"^1|2$"`
}{A: 9, b: 2}, &ErrorValid{Value: "9", Field: "A", Valid: []string{"1", "2"}}}, }{A: 9, b: 2}, &ErrorValid{Value: "9", Field: "A", Valid: "^1|2$"}},
} { }
for _, tt := range tests {
if err := AssertStructValid(tt.c); !reflect.DeepEqual(tt.err, err) { if err := AssertStructValid(tt.c); !reflect.DeepEqual(tt.err, err) {
t.Errorf("bad result (%q): want %q, got %q", tt.c, tt.err, err) t.Errorf("bad result (%q): want %q, got %q", tt.c, tt.err, err)
} }
} }
} }
func TestCloudConfigInvalidKeys(t *testing.T) { func TestConfigCompile(t *testing.T) {
defer func() { tests := []interface{}{
if r := recover(); r != nil { Etcd{},
t.Fatalf("panic while instantiating CloudConfig with nil keys: %v", r) File{},
} Flannel{},
}() Fleet{},
Locksmith{},
OEM{},
Unit{},
Update{},
}
for _, tt := range []struct { for _, tt := range tests {
contents string ttt := reflect.TypeOf(tt)
}{ for i := 0; i < ttt.NumField(); i++ {
{"coreos:"}, ft := ttt.Field(i)
{"ssh_authorized_keys:"}, if !isFieldExported(ft) {
{"ssh_authorized_keys:\n -"}, continue
{"ssh_authorized_keys:\n - 0:"}, }
{"write_files:"},
{"write_files:\n -"}, if _, err := regexp.Compile(ft.Tag.Get("valid")); err != nil {
{"write_files:\n - 0:"}, t.Errorf("bad regexp(%s.%s): want %v, got %s", ttt.Name(), ft.Name, nil, err)
{"users:"}, }
{"users:\n -"},
{"users:\n - 0:"},
} {
_, err := NewCloudConfig(tt.contents)
if err != nil {
t.Fatalf("error instantiating CloudConfig with invalid keys: %v", err)
} }
} }
} }
@@ -136,7 +194,7 @@ hostname:
if cfg.Hostname != "foo" { if cfg.Hostname != "foo" {
t.Fatalf("hostname not correctly set when invalid keys are present") t.Fatalf("hostname not correctly set when invalid keys are present")
} }
if cfg.Coreos.Etcd.Discovery != "https://discovery.etcd.io/827c73219eeb2fa5530027c37bf18877" { if cfg.CoreOS.Etcd.Discovery != "https://discovery.etcd.io/827c73219eeb2fa5530027c37bf18877" {
t.Fatalf("etcd section not correctly set when invalid keys are present") t.Fatalf("etcd section not correctly set when invalid keys are present")
} }
if len(cfg.WriteFiles) < 1 || cfg.WriteFiles[0].Content != "fun" || cfg.WriteFiles[0].Path != "/var/party" { if len(cfg.WriteFiles) < 1 || cfg.WriteFiles[0].Content != "fun" || cfg.WriteFiles[0].Path != "/var/party" {
@@ -242,10 +300,10 @@ hostname: trontastic
} }
} }
if len(cfg.Coreos.Units) != 1 { if len(cfg.CoreOS.Units) != 1 {
t.Error("Failed to parse correct number of units") t.Error("Failed to parse correct number of units")
} else { } else {
u := cfg.Coreos.Units[0] u := cfg.CoreOS.Units[0]
expect := `[Match] expect := `[Match]
Name=eth47 Name=eth47
@@ -263,50 +321,16 @@ Address=10.209.171.177/19
} }
} }
if cfg.Coreos.OEM.ID != "rackspace" { if cfg.CoreOS.OEM.ID != "rackspace" {
t.Errorf("Failed parsing coreos.oem. Expected ID 'rackspace', got %q.", cfg.Coreos.OEM.ID) t.Errorf("Failed parsing coreos.oem. Expected ID 'rackspace', got %q.", cfg.CoreOS.OEM.ID)
} }
if cfg.Hostname != "trontastic" { if cfg.Hostname != "trontastic" {
t.Errorf("Failed to parse hostname") t.Errorf("Failed to parse hostname")
} }
if cfg.Coreos.Update.RebootStrategy != "reboot" { if cfg.CoreOS.Update.RebootStrategy != "reboot" {
t.Errorf("Failed to parse locksmith strategy") t.Errorf("Failed to parse locksmith strategy")
} }
contents = `
coreos:
write_files:
- path: /home/me/notes
permissions: 0744
`
cfg, err = NewCloudConfig(contents)
if err != nil {
t.Fatalf("Encountered unexpected error :%v", err)
}
if len(cfg.WriteFiles) != 1 {
t.Error("Failed to parse correct number of write_files")
} else {
wf := cfg.WriteFiles[0]
if wf.Content != "" {
t.Errorf("WriteFile has incorrect contents '%s'", wf.Content)
}
if wf.Encoding != "" {
t.Errorf("WriteFile has incorrect encoding %s", wf.Encoding)
}
// Verify that the normalization of the config converted 0744 to its decimal
// representation, 484.
if wf.RawFilePermissions != "484" {
t.Errorf("WriteFile has incorrect permissions %s", wf.RawFilePermissions)
}
if wf.Path != "/home/me/notes" {
t.Errorf("WriteFile has incorrect path %s", wf.Path)
}
if wf.Owner != "" {
t.Errorf("WriteFile has incorrect owner %s", wf.Owner)
}
}
} }
// Assert that our interface conversion doesn't panic // Assert that our interface conversion doesn't panic
@@ -473,31 +497,3 @@ users:
t.Errorf("ssh import url is %q, expected 'https://token:x-auth-token@github.enterprise.com/api/v3/polvi/keys'", user.SSHImportURL) t.Errorf("ssh import url is %q, expected 'https://token:x-auth-token@github.enterprise.com/api/v3/polvi/keys'", user.SSHImportURL)
} }
} }
func TestNormalizeKeys(t *testing.T) {
for _, tt := range []struct {
in string
out string
}{
{"my_key_name: the-value\n", "my_key_name: the-value\n"},
{"my-key_name: the-value\n", "my_key_name: the-value\n"},
{"my-key-name: the-value\n", "my_key_name: the-value\n"},
{"a:\n- key_name: the-value\n", "a:\n- key_name: the-value\n"},
{"a:\n- key-name: the-value\n", "a:\n- key_name: the-value\n"},
{"a:\n b:\n - key_name: the-value\n", "a:\n b:\n - key_name: the-value\n"},
{"a:\n b:\n - key-name: the-value\n", "a:\n b:\n - key_name: the-value\n"},
{"coreos:\n update:\n reboot-strategy: off\n", "coreos:\n update:\n reboot_strategy: false\n"},
{"coreos:\n update:\n reboot-strategy: 'off'\n", "coreos:\n update:\n reboot_strategy: \"off\"\n"},
} {
out, err := normalizeConfig(tt.in)
if err != nil {
t.Fatalf("bad error (%q): want nil, got %s", tt.in, err)
}
if string(out) != tt.out {
t.Fatalf("bad normalization (%q): want %q, got %q", tt.in, tt.out, out)
}
}
}

View File

@@ -17,9 +17,9 @@
package config package config
type File struct { type File struct {
Encoding string `yaml:"encoding" valid:"base64,b64,gz,gzip,gz+base64,gzip+base64,gz+b64,gzip+b64"` Encoding string `yaml:"encoding" valid:"^(base64|b64|gz|gzip|gz\\+base64|gzip\\+base64|gz\\+b64|gzip\\+b64)$"`
Content string `yaml:"content"` Content string `yaml:"content"`
Owner string `yaml:"owner"` Owner string `yaml:"owner"`
Path string `yaml:"path"` Path string `yaml:"path"`
RawFilePermissions string `yaml:"permissions"` RawFilePermissions string `yaml:"permissions" valid:"^0?[0-7]{3,4}$"`
} }

71
config/file_test.go Normal file
View File

@@ -0,0 +1,71 @@
/*
Copyright 2014 CoreOS, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package config
import (
"testing"
)
func TestEncodingValid(t *testing.T) {
tests := []struct {
value string
isValid bool
}{
{value: "base64", isValid: true},
{value: "b64", isValid: true},
{value: "gz", isValid: true},
{value: "gzip", isValid: true},
{value: "gz+base64", isValid: true},
{value: "gzip+base64", isValid: true},
{value: "gz+b64", isValid: true},
{value: "gzip+b64", isValid: true},
{value: "gzzzzbase64", isValid: false},
{value: "gzipppbase64", isValid: false},
{value: "unknown", isValid: false},
}
for _, tt := range tests {
isValid := (nil == AssertStructValid(File{Encoding: tt.value}))
if tt.isValid != isValid {
t.Errorf("bad assert (%s): want %t, got %t", tt.value, tt.isValid, isValid)
}
}
}
func TestRawFilePermissionsValid(t *testing.T) {
tests := []struct {
value string
isValid bool
}{
{value: "744", isValid: true},
{value: "0744", isValid: true},
{value: "1744", isValid: true},
{value: "01744", isValid: true},
{value: "11744", isValid: false},
{value: "rwxr--r--", isValid: false},
{value: "800", isValid: false},
}
for _, tt := range tests {
isValid := (nil == AssertStructValid(File{RawFilePermissions: tt.value}))
if tt.isValid != isValid {
t.Errorf("bad assert (%s): want %t, got %t", tt.value, tt.isValid, isValid)
}
}
}

View File

@@ -22,7 +22,7 @@ type Unit struct {
Enable bool `yaml:"enable"` Enable bool `yaml:"enable"`
Runtime bool `yaml:"runtime"` Runtime bool `yaml:"runtime"`
Content string `yaml:"content"` Content string `yaml:"content"`
Command string `yaml:"command" valid:"start,stop,restart,reload,try-restart,reload-or-restart,reload-or-try-restart"` Command string `yaml:"command" valid:"^(start|stop|restart|reload|try-restart|reload-or-restart|reload-or-try-restart)$"`
DropIns []UnitDropIn `yaml:"drop_ins"` DropIns []UnitDropIn `yaml:"drop_ins"`
} }

46
config/unit_test.go Normal file
View File

@@ -0,0 +1,46 @@
/*
Copyright 2014 CoreOS, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package config
import (
"testing"
)
func TestCommandValid(t *testing.T) {
tests := []struct {
value string
isValid bool
}{
{value: "start", isValid: true},
{value: "stop", isValid: true},
{value: "restart", isValid: true},
{value: "reload", isValid: true},
{value: "try-restart", isValid: true},
{value: "reload-or-restart", isValid: true},
{value: "reload-or-try-restart", isValid: true},
{value: "tryrestart", isValid: false},
{value: "unknown", isValid: false},
}
for _, tt := range tests {
isValid := (nil == AssertStructValid(Unit{Command: tt.value}))
if tt.isValid != isValid {
t.Errorf("bad assert (%s): want %t, got %t", tt.value, tt.isValid, isValid)
}
}
}

View File

@@ -17,7 +17,7 @@
package config package config
type Update struct { type Update struct {
RebootStrategy string `yaml:"reboot_strategy" env:"REBOOT_STRATEGY" valid:"best-effort,etcd-lock,reboot,off,false"` RebootStrategy string `yaml:"reboot_strategy" env:"REBOOT_STRATEGY" valid:"^(best-effort|etcd-lock|reboot|off)$"`
Group string `yaml:"group" env:"GROUP"` Group string `yaml:"group" env:"GROUP"`
Server string `yaml:"server" env:"SERVER"` Server string `yaml:"server" env:"SERVER"`
} }

43
config/update_test.go Normal file
View File

@@ -0,0 +1,43 @@
/*
Copyright 2014 CoreOS, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package config
import (
"testing"
)
func TestRebootStrategyValid(t *testing.T) {
tests := []struct {
value string
isValid bool
}{
{value: "best-effort", isValid: true},
{value: "etcd-lock", isValid: true},
{value: "reboot", isValid: true},
{value: "off", isValid: true},
{value: "besteffort", isValid: false},
{value: "unknown", isValid: false},
}
for _, tt := range tests {
isValid := (nil == AssertStructValid(Update{RebootStrategy: tt.value}))
if tt.isValid != isValid {
t.Errorf("bad assert (%s): want %t, got %t", tt.value, tt.isValid, isValid)
}
}
}

View File

@@ -76,7 +76,7 @@ func checkValidity(cfg node, report *Report) {
func checkNodeValidity(n, g node, r *Report) { func checkNodeValidity(n, g node, r *Report) {
if err := config.AssertValid(n.Value, g.field.Tag.Get("valid")); err != nil { if err := config.AssertValid(n.Value, g.field.Tag.Get("valid")); err != nil {
r.Error(n.line, fmt.Sprintf("invalid value %v", n.Value)) r.Error(n.line, fmt.Sprintf("invalid value %v", n.Value.Interface()))
} }
switch g.Kind() { switch g.Kind() {
case reflect.Struct: case reflect.Struct:

View File

@@ -25,7 +25,7 @@ import (
"github.com/coreos/coreos-cloudinit/config" "github.com/coreos/coreos-cloudinit/config"
"github.com/coreos/coreos-cloudinit/Godeps/_workspace/src/gopkg.in/yaml.v1" "github.com/coreos/coreos-cloudinit/Godeps/_workspace/src/github.com/coreos/yaml"
) )
var ( var (
@@ -65,7 +65,6 @@ func validateCloudConfig(config []byte, rules []rule) (report Report, err error)
return report, err return report, err
} }
c = normalizeNodeNames(c, &report)
for _, r := range rules { for _, r := range rules {
r(c, &report) r(c, &report)
} }
@@ -75,30 +74,79 @@ func validateCloudConfig(config []byte, rules []rule) (report Report, err error)
// parseCloudConfig parses the provided config into a node structure and logs // parseCloudConfig parses the provided config into a node structure and logs
// any parsing issues into the provided report. Unrecoverable errors are // any parsing issues into the provided report. Unrecoverable errors are
// returned as an error. // returned as an error.
func parseCloudConfig(config []byte, report *Report) (n node, err error) { func parseCloudConfig(cfg []byte, report *Report) (node, error) {
var raw map[interface{}]interface{} yaml.UnmarshalMappingKeyTransform = func(nameIn string) (nameOut string) {
if err := yaml.Unmarshal(config, &raw); err != nil { return nameIn
}
// unmarshal the config into an implicitly-typed form. The yaml library
// will implicitly convert types into their normalized form
// (e.g. 0744 -> 484, off -> false).
var weak map[interface{}]interface{}
if err := yaml.Unmarshal(cfg, &weak); err != nil {
matches := yamlLineError.FindStringSubmatch(err.Error()) matches := yamlLineError.FindStringSubmatch(err.Error())
if len(matches) == 3 { if len(matches) == 3 {
line, err := strconv.Atoi(matches[1]) line, err := strconv.Atoi(matches[1])
if err != nil { if err != nil {
return n, err return node{}, err
} }
msg := matches[2] msg := matches[2]
report.Error(line, msg) report.Error(line, msg)
return n, nil return node{}, nil
} }
matches = yamlError.FindStringSubmatch(err.Error()) matches = yamlError.FindStringSubmatch(err.Error())
if len(matches) == 2 { if len(matches) == 2 {
report.Error(1, matches[1]) report.Error(1, matches[1])
return n, nil return node{}, nil
} }
return n, errors.New("couldn't parse yaml error") return node{}, errors.New("couldn't parse yaml error")
}
w := NewNode(weak, NewContext(cfg))
w = normalizeNodeNames(w, report)
// unmarshal the config into the explicitly-typed form.
yaml.UnmarshalMappingKeyTransform = func(nameIn string) (nameOut string) {
return strings.Replace(nameIn, "-", "_", -1)
}
var strong config.CloudConfig
if err := yaml.Unmarshal([]byte(cfg), &strong); err != nil {
return node{}, err
}
s := NewNode(strong, NewContext(cfg))
// coerceNodes weak nodes and strong nodes. strong nodes replace weak nodes
// if they are compatible types (this happens when the yaml library
// converts the input).
// (e.g. weak 484 is replaced by strong 0744, weak 4 is not replaced by
// strong false)
return coerceNodes(w, s), nil
}
// coerceNodes recursively evaluates two nodes, returning a new node containing
// either the weak or strong node's value and its recursively processed
// children. The strong node's value is used if the two nodes are leafs, are
// both valid, and are compatible types (defined by isCompatible()). The weak
// node is returned in all other cases. coerceNodes is used to counteract the
// effects of yaml's automatic type conversion. The weak node is the one
// resulting from unmarshalling into an empty interface{} (the type is
// inferred). The strong node is the one resulting from unmarshalling into a
// struct. If the two nodes are of compatible types, the yaml library correctly
// parsed the value into the strongly typed unmarshalling. In this case, we
// prefer the strong node because its actually the type we are expecting.
func coerceNodes(w, s node) node {
n := w
n.children = nil
if len(w.children) == 0 && len(s.children) == 0 &&
w.IsValid() && s.IsValid() &&
isCompatible(w.Kind(), s.Kind()) {
n.Value = s.Value
} }
return NewNode(raw, NewContext(config)), nil for _, cw := range w.children {
n.children = append(n.children, coerceNodes(cw, s.Child(cw.name)))
}
return n
} }
// normalizeNodeNames replaces all occurences of '-' with '_' within key names // normalizeNodeNames replaces all occurences of '-' with '_' within key names

View File

@@ -65,6 +65,31 @@ func TestValidateCloudConfig(t *testing.T) {
rules: []rule{func(_ node, _ *Report) { panic("something happened") }}, rules: []rule{func(_ node, _ *Report) { panic("something happened") }},
err: errors.New("something happened"), err: errors.New("something happened"),
}, },
{
config: "write_files:\n - permissions: 0744",
rules: Rules,
},
{
config: "write_files:\n - permissions: '0744'",
rules: Rules,
},
{
config: "write_files:\n - permissions: 744",
rules: Rules,
},
{
config: "write_files:\n - permissions: '744'",
rules: Rules,
},
{
config: "coreos:\n update:\n reboot-strategy: off",
rules: Rules,
},
{
config: "coreos:\n update:\n reboot-strategy: false",
rules: Rules,
report: Report{entries: []Entry{{entryError, "invalid value false", 3}}},
},
} }
for _, tt := range tests { for _, tt := range tests {

View File

@@ -40,7 +40,7 @@ import (
) )
const ( const (
version = "1.0.2" version = "1.1.0"
datasourceInterval = 100 * time.Millisecond datasourceInterval = 100 * time.Millisecond
datasourceMaxInterval = 30 * time.Second datasourceMaxInterval = 30 * time.Second
datasourceTimeout = 5 * time.Minute datasourceTimeout = 5 * time.Minute

View File

@@ -110,9 +110,10 @@ func Apply(cfg config.CloudConfig, env *Environment) error {
} }
for _, ccf := range []CloudConfigFile{ for _, ccf := range []CloudConfigFile{
system.OEM{OEM: cfg.Coreos.OEM}, system.OEM{OEM: cfg.CoreOS.OEM},
system.Update{Update: cfg.Coreos.Update, ReadConfig: system.DefaultReadConfig}, system.Update{Update: cfg.CoreOS.Update, ReadConfig: system.DefaultReadConfig},
system.EtcHosts{EtcHosts: cfg.ManageEtcHosts}, system.EtcHosts{EtcHosts: cfg.ManageEtcHosts},
system.Flannel{Flannel: cfg.CoreOS.Flannel},
} { } {
f, err := ccf.File() f, err := ccf.File()
if err != nil { if err != nil {
@@ -124,16 +125,15 @@ func Apply(cfg config.CloudConfig, env *Environment) error {
} }
var units []system.Unit var units []system.Unit
for _, u := range cfg.Coreos.Units { for _, u := range cfg.CoreOS.Units {
units = append(units, system.Unit{Unit: u}) units = append(units, system.Unit{Unit: u})
} }
for _, ccu := range []CloudConfigUnit{ for _, ccu := range []CloudConfigUnit{
system.Etcd{Etcd: cfg.Coreos.Etcd}, system.Etcd{Etcd: cfg.CoreOS.Etcd},
system.Fleet{Fleet: cfg.Coreos.Fleet}, system.Fleet{Fleet: cfg.CoreOS.Fleet},
system.Locksmith{Locksmith: cfg.Coreos.Locksmith}, system.Locksmith{Locksmith: cfg.CoreOS.Locksmith},
system.Flannel{Flannel: cfg.Coreos.Flannel}, system.Update{Update: cfg.CoreOS.Update, ReadConfig: system.DefaultReadConfig},
system.Update{Update: cfg.Coreos.Update, ReadConfig: system.DefaultReadConfig},
} { } {
units = append(units, ccu.Units()...) units = append(units, ccu.Units()...)
} }

View File

@@ -23,22 +23,32 @@ import (
"github.com/coreos/coreos-cloudinit/config" "github.com/coreos/coreos-cloudinit/config"
) )
// dropinContents generates the contents for a drop-in unit given the config. // serviceContents generates the contents for a drop-in unit given the config.
// The argument must be a struct from the 'config' package. // The argument must be a struct from the 'config' package.
func serviceContents(e interface{}) string { func serviceContents(e interface{}) string {
vars := getEnvVars(e)
if len(vars) == 0 {
return ""
}
out := "[Service]\n"
for _, v := range vars {
out += fmt.Sprintf("Environment=\"%s\"\n", v)
}
return out
}
func getEnvVars(e interface{}) []string {
et := reflect.TypeOf(e) et := reflect.TypeOf(e)
ev := reflect.ValueOf(e) ev := reflect.ValueOf(e)
var out string vars := []string{}
for i := 0; i < et.NumField(); i++ { for i := 0; i < et.NumField(); i++ {
if val := ev.Field(i).Interface(); !config.IsZero(val) { if val := ev.Field(i).Interface(); !config.IsZero(val) {
key := et.Field(i).Tag.Get("env") key := et.Field(i).Tag.Get("env")
out += fmt.Sprintf("Environment=\"%s=%v\"\n", key, val) vars = append(vars, fmt.Sprintf("%s=%v", key, val))
} }
} }
if out == "" { return vars
return ""
}
return "[Service]\n" + out
} }

View File

@@ -43,7 +43,7 @@ func (f *File) Permissions() (os.FileMode, error) {
} }
// Parse string representation of file mode as integer // Parse string representation of file mode as integer
perm, err := strconv.ParseInt(f.RawFilePermissions, 0, 32) perm, err := strconv.ParseInt(f.RawFilePermissions, 8, 32)
if err != nil { if err != nil {
return 0, fmt.Errorf("Unable to parse file permissions %q as integer", f.RawFilePermissions) return 0, fmt.Errorf("Unable to parse file permissions %q as integer", f.RawFilePermissions)
} }

View File

@@ -97,7 +97,7 @@ func TestDecimalFilePermissions(t *testing.T) {
wf := File{config.File{ wf := File{config.File{
Path: fn, Path: fn,
RawFilePermissions: "484", // Decimal representation of 0744 RawFilePermissions: "744",
}} }}
path, err := WriteFile(&wf, dir) path, err := WriteFile(&wf, dir)

View File

@@ -1,6 +1,9 @@
package system package system
import ( import (
"path"
"strings"
"github.com/coreos/coreos-cloudinit/config" "github.com/coreos/coreos-cloudinit/config"
) )
@@ -10,15 +13,18 @@ type Flannel struct {
config.Flannel config.Flannel
} }
// Units generates a Unit file drop-in for flannel, if any flannel options were func (fl Flannel) envVars() string {
// configured in cloud-config return strings.Join(getEnvVars(fl.Flannel), "\n")
func (fl Flannel) Units() []Unit { }
return []Unit{{config.Unit{
Name: "flanneld.service", func (fl Flannel) File() (*File, error) {
Runtime: true, vars := fl.envVars()
DropIns: []config.UnitDropIn{{ if vars == "" {
Name: "20-cloudinit.conf", return nil, nil
Content: serviceContents(fl.Flannel), }
}}, return &File{config.File{
}}} Path: path.Join("run", "flannel", "options.env"),
RawFilePermissions: "0644",
Content: vars,
}}, nil
} }

View File

@@ -7,40 +7,56 @@ import (
"github.com/coreos/coreos-cloudinit/config" "github.com/coreos/coreos-cloudinit/config"
) )
func TestFlannelUnits(t *testing.T) { func TestFlannelEnvVars(t *testing.T) {
for _, tt := range []struct { for _, tt := range []struct {
config config.Flannel config config.Flannel
units []Unit contents string
}{ }{
{ {
config.Flannel{}, config.Flannel{},
[]Unit{{config.Unit{ "",
Name: "flanneld.service",
Runtime: true,
DropIns: []config.UnitDropIn{{Name: "20-cloudinit.conf"}},
}}},
}, },
{ {
config.Flannel{ config.Flannel{
EtcdEndpoint: "http://12.34.56.78:4001", EtcdEndpoint: "http://12.34.56.78:4001",
EtcdPrefix: "/coreos.com/network/tenant1", EtcdPrefix: "/coreos.com/network/tenant1",
}, },
[]Unit{{config.Unit{ `FLANNELD_ETCD_ENDPOINT=http://12.34.56.78:4001
Name: "flanneld.service", FLANNELD_ETCD_PREFIX=/coreos.com/network/tenant1`,
Runtime: true,
DropIns: []config.UnitDropIn{{
Name: "20-cloudinit.conf",
Content: `[Service]
Environment="FLANNELD_ETCD_ENDPOINT=http://12.34.56.78:4001"
Environment="FLANNELD_ETCD_PREFIX=/coreos.com/network/tenant1"
`,
}},
}}},
}, },
} { } {
units := Flannel{tt.config}.Units() out := Flannel{tt.config}.envVars()
if !reflect.DeepEqual(units, tt.units) { if out != tt.contents {
t.Errorf("bad units (%q): want %v, got %v", tt.config, tt.units, units) t.Errorf("bad contents (%+v): want %q, got %q", tt, tt.contents, out)
}
}
}
func TestFlannelFile(t *testing.T) {
for _, tt := range []struct {
config config.Flannel
file *File
}{
{
config.Flannel{},
nil,
},
{
config.Flannel{
EtcdEndpoint: "http://12.34.56.78:4001",
EtcdPrefix: "/coreos.com/network/tenant1",
},
&File{config.File{
Path: "run/flannel/options.env",
RawFilePermissions: "0644",
Content: `FLANNELD_ETCD_ENDPOINT=http://12.34.56.78:4001
FLANNELD_ETCD_PREFIX=/coreos.com/network/tenant1`,
}},
},
} {
file, _ := Flannel{tt.config}.File()
if !reflect.DeepEqual(tt.file, file) {
t.Errorf("bad units (%q): want %#v, got %#v", tt.config, tt.file, file)
} }
} }
} }

View File

@@ -126,7 +126,7 @@ func (uc Update) Units() []Unit {
Runtime: true, Runtime: true,
}} }}
if uc.Update.RebootStrategy == "false" || uc.Update.RebootStrategy == "off" { if uc.Update.RebootStrategy == "off" {
ls.Command = "stop" ls.Command = "stop"
ls.Mask = true ls.Mask = true
} }

View File

@@ -71,15 +71,6 @@ func TestUpdateUnits(t *testing.T) {
Runtime: true, Runtime: true,
}}}, }}},
}, },
{
config: config.Update{RebootStrategy: "false"},
units: []Unit{{config.Unit{
Name: "locksmithd.service",
Command: "stop",
Runtime: true,
Mask: true,
}}},
},
{ {
config: config.Update{RebootStrategy: "off"}, config: config.Update{RebootStrategy: "off"},
units: []Unit{{config.Unit{ units: []Unit{{config.Unit{
@@ -109,7 +100,7 @@ func TestUpdateFile(t *testing.T) {
}, },
{ {
config: config.Update{RebootStrategy: "wizzlewazzle"}, config: config.Update{RebootStrategy: "wizzlewazzle"},
err: &config.ErrorValid{Value: "wizzlewazzle", Field: "RebootStrategy", Valid: []string{"best-effort", "etcd-lock", "reboot", "off", "false"}}, err: &config.ErrorValid{Value: "wizzlewazzle", Field: "RebootStrategy", Valid: "^(best-effort|etcd-lock|reboot|off)$"},
}, },
{ {
config: config.Update{Group: "master", Server: "http://foo.com"}, config: config.Update{Group: "master", Server: "http://foo.com"},
@@ -143,14 +134,6 @@ func TestUpdateFile(t *testing.T) {
RawFilePermissions: "0644", RawFilePermissions: "0644",
}}, }},
}, },
{
config: config.Update{RebootStrategy: "false"},
file: &File{config.File{
Content: "REBOOT_STRATEGY=false\n",
Path: "etc/coreos/update.conf",
RawFilePermissions: "0644",
}},
},
{ {
config: config.Update{RebootStrategy: "off"}, config: config.Update{RebootStrategy: "off"},
file: &File{config.File{ file: &File{config.File{