Compare commits

...

33 Commits

Author SHA1 Message Date
Alex Crawford
fb6f52b360 coreos-cloudinit: bump to 0.11.1 2014-11-18 12:14:29 -08:00
Alex Crawford
786cd2a539 Merge pull request #259 from crawford/hyphen
config/validate: disable - vs _ message for now
2014-11-18 12:12:26 -08:00
Alex Crawford
45793f1254 config/validate: disable - vs _ message for now 2014-11-18 12:11:50 -08:00
Alex Crawford
b621756d92 Merge pull request #258 from crawford/header
config/validate: fix line number for header check
2014-11-18 12:11:35 -08:00
Alex Crawford
a5b5c700a6 config/validate: fix line number for header check 2014-11-18 12:02:23 -08:00
Alex Crawford
d7602f3c08 Merge pull request #244 from eyakubovich/master
flannel: added flannel support and helper to make dropins
2014-11-14 10:46:19 -08:00
Eugene Yakubovich
a20addd05e flannel: added flannel support and helper to make dropins
fleet, flannel, and etcd all generate dropins from config.
To reduce code duplication, factor out a helper to do that.
2014-11-14 10:45:23 -08:00
Alex Crawford
d9d89a6fa0 coreos-cloudinit: bump to 0.11.0+git 2014-11-14 10:42:00 -08:00
Alex Crawford
3c26376326 coreos-cloudinit: bump to 0.11.0 2014-11-14 10:41:47 -08:00
Alex Crawford
d3294bcb86 Merge pull request #254 from crawford/validator
config: add new validator
2014-11-12 17:40:16 -08:00
Alex Crawford
dda314b518 flags: add validate flag
This will allow the user to run a standalone validation.
2014-11-12 16:48:57 -08:00
Alex Crawford
055a3c339a config/validate: add new config validator
This validator is still experimental and is going to need new rules in the
future. This lays out the general framework.
2014-11-12 16:48:57 -08:00
Alex Crawford
51f37100a1 config: remove config validator 2014-11-07 10:18:08 -08:00
Alex Crawford
88e8265cd6 config: seperate AssertValid and AssertStructValid
Added an error structure to make it possible to get the specifics of the failure.
2014-11-07 10:14:34 -08:00
Alex Crawford
6e2db882e6 script: move Script into config package 2014-11-07 10:13:52 -08:00
Alex Crawford
3e2823df1b Merge pull request #256 from crawford/hyphen
config: deprecate - in favor of _ for key names
2014-11-03 14:54:23 -08:00
Alex Crawford
46cb51cf91 Merge pull request #257 from crawford/networkd
networkd: remove double-restart workaround
2014-11-03 14:25:38 -08:00
Alex Crawford
1a6cee5305 networkd: remove double-restart workaround
The kernel fixed the underlying issue in 763e0ec and e721f87.
2014-11-03 14:11:15 -08:00
Alex Crawford
d02aa18839 config: deprecate - in favor of _ for key names
In all of the YAML tags, - has been replaced with _. normalizeConfig() and
normalizeKeys() have also been added to perform the normalization of the input
cloud-config.

As part of the normalization process, falsey values are converted to "false".
The "off" update strategy is no exception and as a result the "off" update
strategy has been changed to "false".
2014-11-03 12:09:52 -08:00
Alex Crawford
e9bda98b54 Merge pull request #252 from crawford/vet
go vet
2014-10-23 12:03:01 -07:00
Alex Crawford
badc874b74 travis: install go vet 2014-10-23 11:47:24 -07:00
Alex Crawford
c9e8c887b8 test: run go vet 2014-10-23 11:46:40 -07:00
Alex Crawford
8be307de49 *: fix warnings from go vet 2014-10-23 11:46:08 -07:00
Alex Crawford
562c474275 system: embed config within EtcHosts and Update 2014-10-23 11:44:15 -07:00
Jonathan Boulle
5c5834863b Merge pull request #250 from jonboulle/master
*: switch to Godeps
2014-10-20 12:09:04 -07:00
Jonathan Boulle
44f0a949c5 *: switch to Godeps 2014-10-20 12:04:03 -07:00
Jonathan Boulle
106c4e7a2c Merge pull request #249 from jonboulle/license_header
*: add license header to all source files
2014-10-17 15:42:20 -07:00
Jonathan Boulle
6c1ba590aa *: add license header to all source files 2014-10-17 15:36:22 -07:00
Alex Crawford
45da664c59 Merge pull request #246 from crawford/master
Add support for Azure
2014-10-12 21:37:34 -07:00
Alex Crawford
2a71551ef2 azure: add support for azure (via azure agent) 2014-10-11 09:19:47 -07:00
Alex Crawford
84e1cb3242 datasource/waagent: add support for WAAgent metadata 2014-10-11 09:19:47 -07:00
Jonathan Boulle
5214ead926 Merge pull request #245 from jonboulle/units
init: simplify CloudConfigUnit interface
2014-10-06 15:26:36 -07:00
Jonathan Boulle
e2c24c4cef init: simplify CloudConfigUnit interface 2014-10-06 15:14:29 -07:00
198 changed files with 3457 additions and 1469 deletions

View File

@@ -5,6 +5,7 @@ go:
install:
- go get code.google.com/p/go.tools/cmd/cover
- go get code.google.com/p/go.tools/cmd/vet
script:
- ./test

View File

@@ -97,6 +97,29 @@ For more information on fleet configuration, see the [fleet documentation][fleet
[fleet-config]: https://github.com/coreos/fleet/blob/master/Documentation/deployment-and-configuration.md#configuration
#### flannel
The `coreos.flannel.*` parameters also work very similarly to `coreos.etcd.*` and `coreos.fleet.*`. They can be used to set enviornment variables for flanneld. Given the following cloud-config...
```yaml
#cloud-config
coreos:
flannel:
etcd-prefix: /coreos.com/network2
```
...will generate systemd unit drop-in like so:
```
[Service]
Environment="FLANNELD_ETCD_PREFIX=/coreos.com/network2"
```
For complete list of flannel configuraion parameters, see the [flannel documentation][flannel-readme].
[flannel-readme]: https://github.com/coreos/flannel/blob/master/README.md
#### update
The `coreos.update.*` parameters manipulate settings related to how CoreOS instances are updated.

34
Godeps/Godeps.json generated Normal file
View File

@@ -0,0 +1,34 @@
{
"ImportPath": "github.com/coreos/coreos-cloudinit",
"GoVersion": "go1.3.1",
"Packages": [
"./..."
],
"Deps": [
{
"ImportPath": "github.com/cloudsigma/cepgo",
"Rev": "1bfc4895bf5c4d3b599f3f6ee142299488c8739b"
},
{
"ImportPath": "github.com/coreos/go-systemd/dbus",
"Rev": "4fbc5060a317b142e6c7bfbedb65596d5f0ab99b"
},
{
"ImportPath": "github.com/dotcloud/docker/pkg/netlink",
"Comment": "v0.11.1-359-g55d41c3e21e1",
"Rev": "55d41c3e21e1593b944c06196ffb2ac57ab7f653"
},
{
"ImportPath": "github.com/guelfey/go.dbus",
"Rev": "f6a3a2366cc39b8479cadc499d3c735fb10fbdda"
},
{
"ImportPath": "github.com/tarm/goserial",
"Rev": "cdabc8d44e8e84f58f18074ae44337e1f2f375b9"
},
{
"ImportPath": "gopkg.in/yaml.v1",
"Rev": "feb4ca79644e8e7e39c06095246ee54b1282c118"
}
]
}

5
Godeps/Readme generated Normal file
View File

@@ -0,0 +1,5 @@
This directory tree is generated automatically by godep.
Please do not edit.
See https://github.com/tools/godep for more information.

2
Godeps/_workspace/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,2 @@
/pkg
/bin

View File

@@ -0,0 +1,23 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test

View File

@@ -48,7 +48,7 @@ import (
"fmt"
"runtime"
"github.com/coreos/coreos-cloudinit/third_party/github.com/tarm/goserial"
"github.com/coreos/coreos-cloudinit/Godeps/_workspace/src/github.com/tarm/goserial"
)
const (

View File

@@ -23,7 +23,7 @@ import (
"strings"
"sync"
"github.com/coreos/coreos-cloudinit/third_party/github.com/guelfey/go.dbus"
"github.com/coreos/coreos-cloudinit/Godeps/_workspace/src/github.com/guelfey/go.dbus"
)
const signalBuffer = 100

View File

@@ -18,7 +18,7 @@ package dbus
import (
"errors"
"github.com/coreos/coreos-cloudinit/third_party/github.com/guelfey/go.dbus"
"github.com/coreos/coreos-cloudinit/Godeps/_workspace/src/github.com/guelfey/go.dbus"
)
func (c *Conn) initJobs() {
@@ -208,7 +208,7 @@ func (c *Conn) SetUnitProperties(name string, runtime bool, properties ...Proper
}
func (c *Conn) GetUnitTypeProperty(unit string, unitType string, propertyName string) (*Property, error) {
return c.getProperty(unit, "org.freedesktop.systemd1." + unitType, propertyName)
return c.getProperty(unit, "org.freedesktop.systemd1."+unitType, propertyName)
}
// ListUnits returns an array with all currently loaded units. Note that

View File

@@ -18,7 +18,7 @@ package dbus
import (
"fmt"
"github.com/coreos/coreos-cloudinit/third_party/github.com/guelfey/go.dbus"
"github.com/coreos/coreos-cloudinit/Godeps/_workspace/src/github.com/guelfey/go.dbus"
"math/rand"
"os"
"path/filepath"

View File

@@ -17,7 +17,7 @@ limitations under the License.
package dbus
import (
"github.com/coreos/coreos-cloudinit/third_party/github.com/guelfey/go.dbus"
"github.com/coreos/coreos-cloudinit/Godeps/_workspace/src/github.com/guelfey/go.dbus"
)
// From the systemd docs:

View File

@@ -20,7 +20,7 @@ import (
"errors"
"time"
"github.com/coreos/coreos-cloudinit/third_party/github.com/guelfey/go.dbus"
"github.com/coreos/coreos-cloudinit/Godeps/_workspace/src/github.com/guelfey/go.dbus"
)
const (
@@ -101,7 +101,7 @@ func (c *Conn) SubscribeUnits(interval time.Duration) (<-chan map[string]*UnitSt
// SubscribeUnitsCustom is like SubscribeUnits but lets you specify the buffer
// size of the channels, the comparison function for detecting changes and a filter
// function for cutting down on the noise that your channel receives.
func (c *Conn) SubscribeUnitsCustom(interval time.Duration, buffer int, isChanged func(*UnitStatus, *UnitStatus) bool, filterUnit func (string) bool) (<-chan map[string]*UnitStatus, <-chan error) {
func (c *Conn) SubscribeUnitsCustom(interval time.Duration, buffer int, isChanged func(*UnitStatus, *UnitStatus) bool, filterUnit func(string) bool) (<-chan map[string]*UnitStatus, <-chan error) {
old := make(map[string]*UnitStatus)
statusChan := make(chan map[string]*UnitStatus, buffer)
errChan := make(chan error, buffer)

View File

@@ -2,7 +2,7 @@ package introspect
import (
"encoding/xml"
"github.com/coreos/coreos-cloudinit/third_party/github.com/guelfey/go.dbus"
"github.com/coreos/coreos-cloudinit/Godeps/_workspace/src/github.com/guelfey/go.dbus"
"strings"
)

View File

@@ -2,7 +2,7 @@ package introspect
import (
"encoding/xml"
"github.com/coreos/coreos-cloudinit/third_party/github.com/guelfey/go.dbus"
"github.com/coreos/coreos-cloudinit/Godeps/_workspace/src/github.com/guelfey/go.dbus"
"reflect"
)

View File

@@ -3,8 +3,8 @@
package prop
import (
"github.com/coreos/coreos-cloudinit/third_party/github.com/guelfey/go.dbus"
"github.com/coreos/coreos-cloudinit/third_party/github.com/guelfey/go.dbus/introspect"
"github.com/coreos/coreos-cloudinit/Godeps/_workspace/src/github.com/guelfey/go.dbus"
"github.com/coreos/coreos-cloudinit/Godeps/_workspace/src/github.com/guelfey/go.dbus/introspect"
"sync"
)

View File

@@ -0,0 +1,61 @@
package serial
import (
"testing"
"time"
)
func TestConnection(t *testing.T) {
c0 := &Config{Name: "/dev/ttyUSB0", Baud: 115200}
c1 := &Config{Name: "/dev/ttyUSB1", Baud: 115200}
s1, err := OpenPort(c0)
if err != nil {
t.Fatal(err)
}
s2, err := OpenPort(c1)
if err != nil {
t.Fatal(err)
}
ch := make(chan int, 1)
go func() {
buf := make([]byte, 128)
var readCount int
for {
n, err := s2.Read(buf)
if err != nil {
t.Fatal(err)
}
readCount++
t.Logf("Read %v %v bytes: % 02x %s", readCount, n, buf[:n], buf[:n])
select {
case <-ch:
ch <- readCount
close(ch)
default:
}
}
}()
if _, err = s1.Write([]byte("hello")); err != nil {
t.Fatal(err)
}
if _, err = s1.Write([]byte(" ")); err != nil {
t.Fatal(err)
}
time.Sleep(time.Second)
if _, err = s1.Write([]byte("world")); err != nil {
t.Fatal(err)
}
time.Sleep(time.Second / 10)
ch <- 0
s1.Write([]byte(" ")) // We could be blocked in the read without this
c := <-ch
exp := 5
if c >= exp {
t.Fatalf("Expected less than %v read, got %v", exp, c)
}
}

View File

@@ -2,7 +2,7 @@ package yaml_test
import (
. "gopkg.in/check.v1"
"gopkg.in/yaml.v1"
"github.com/coreos/coreos-cloudinit/Godeps/_workspace/src/gopkg.in/yaml.v1"
"math"
"reflect"
"time"
@@ -372,7 +372,7 @@ var unmarshalTests = []struct {
map[string]time.Duration{"a": 3 * time.Second},
},
// Issue #24.
// Issue #24.
{
"a: <foo>",
map[string]string{"a": "<foo>"},

View File

@@ -2,7 +2,7 @@ package yaml_test
import (
"fmt"
"gopkg.in/yaml.v1"
"github.com/coreos/coreos-cloudinit/Godeps/_workspace/src/gopkg.in/yaml.v1"
. "gopkg.in/check.v1"
"math"
"strconv"
@@ -220,7 +220,7 @@ var marshalTests = []struct {
"a: 3s\n",
},
// Issue #24.
// Issue #24.
{
map[string]string{"a": "<foo>"},
"a: <foo>\n",

View File

@@ -1,12 +1,27 @@
/*
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 (
"fmt"
"log"
"reflect"
"strings"
"github.com/coreos/coreos-cloudinit/third_party/gopkg.in/yaml.v1"
"github.com/coreos/coreos-cloudinit/Godeps/_workspace/src/gopkg.in/yaml.v1"
)
// CloudConfig encapsulates the entire cloud-config configuration file and maps
@@ -15,11 +30,12 @@ import (
type CloudConfig struct {
SSHAuthorizedKeys []string `yaml:"ssh_authorized_keys"`
Coreos struct {
Etcd Etcd `yaml:"etcd"`
Fleet Fleet `yaml:"fleet"`
OEM OEM `yaml:"oem"`
Update Update `yaml:"update"`
Units []Unit `yaml:"units"`
Etcd Etcd `yaml:"etcd"`
Flannel Flannel `yaml:"flannel"`
Fleet Fleet `yaml:"fleet"`
OEM OEM `yaml:"oem"`
Update Update `yaml:"update"`
Units []Unit `yaml:"units"`
} `yaml:"coreos"`
WriteFiles []File `yaml:"write_files"`
Hostname string `yaml:"hostname"`
@@ -29,16 +45,29 @@ type CloudConfig struct {
NetworkConfig string `yaml:"-"`
}
func IsCloudConfig(userdata string) bool {
header := strings.SplitN(userdata, "\n", 2)[0]
// Explicitly trim the header so we can handle user-data from
// non-unix operating systems. The rest of the file is parsed
// by yaml, which correctly handles CRLF.
header = strings.TrimSuffix(header, "\r")
return (header == "#cloud-config")
}
// NewCloudConfig instantiates a new CloudConfig from the given contents (a
// string of YAML), returning any error encountered. It will ignore unknown
// fields but log encountering them.
func NewCloudConfig(contents string) (*CloudConfig, error) {
var cfg CloudConfig
err := yaml.Unmarshal([]byte(contents), &cfg)
ncontents, err := normalizeConfig(contents)
if err != nil {
return &cfg, err
}
warnOnUnrecognizedKeys(contents, log.Printf)
if err = yaml.Unmarshal(ncontents, &cfg); err != nil {
return &cfg, err
}
return &cfg, nil
}
@@ -60,10 +89,20 @@ func IsZero(c interface{}) bool {
return isZero(reflect.ValueOf(c))
}
// AssertValid checks the fields in the structure and makes sure that they
// contain valid values as specified by the 'valid' flag. Empty fields are
type ErrorValid struct {
Value string
Valid []string
Field string
}
func (e ErrorValid) Error() string {
return fmt.Sprintf("invalid value %q for option %q (valid options: %q)", e.Value, e.Field, e.Valid)
}
// AssertStructValid checks the fields in the structure and makes sure that
// they contain valid values as specified by the 'valid' flag. Empty fields are
// implicitly valid.
func AssertValid(c interface{}) error {
func AssertStructValid(c interface{}) error {
ct := reflect.TypeOf(c)
cv := reflect.ValueOf(c)
for i := 0; i < ct.NumField(); i++ {
@@ -72,15 +111,33 @@ func AssertValid(c interface{}) error {
continue
}
valid := ft.Tag.Get("valid")
val := cv.Field(i)
if !isValid(val, valid) {
return fmt.Errorf("invalid value \"%v\" for option %q (valid options: %q)", val.Interface(), ft.Name, valid)
if err := AssertValid(cv.Field(i), ft.Tag.Get("valid")); err != nil {
err.Field = ft.Name
return err
}
}
return nil
}
// AssertValid checks to make sure that the given value is in the list of
// valid values. Zero values are implicitly valid.
func AssertValid(value reflect.Value, valid string) *ErrorValid {
if valid == "" || isZero(value) {
return nil
}
vs := fmt.Sprintf("%v", value.Interface())
valids := strings.Split(valid, ",")
for _, valid := range valids {
if vs == valid {
return nil
}
}
return &ErrorValid{
Value: vs,
Valid: valids,
}
}
func isZero(v reflect.Value) bool {
switch v.Kind() {
case reflect.Struct:
@@ -100,99 +157,30 @@ func isFieldExported(f reflect.StructField) bool {
return f.PkgPath == ""
}
func isValid(v reflect.Value, valid string) bool {
if valid == "" || isZero(v) {
return true
func normalizeConfig(config string) ([]byte, error) {
var cfg map[interface{}]interface{}
if err := yaml.Unmarshal([]byte(config), &cfg); err != nil {
return nil, err
}
vs := fmt.Sprintf("%v", v.Interface())
for _, valid := range strings.Split(valid, ",") {
if vs == valid {
return true
}
}
return false
return yaml.Marshal(normalizeKeys(cfg))
}
type warner func(format string, v ...interface{})
// warnOnUnrecognizedKeys parses the contents of a cloud-config file and calls
// warn(msg, key) for every unrecognized key (i.e. those not present in CloudConfig)
func warnOnUnrecognizedKeys(contents string, warn warner) {
// Generate a map of all understood cloud config options
var cc map[string]interface{}
b, _ := yaml.Marshal(&CloudConfig{})
yaml.Unmarshal(b, &cc)
// Now unmarshal the entire provided contents
var c map[string]interface{}
yaml.Unmarshal([]byte(contents), &c)
// Check that every key in the contents exists in the cloud config
for k, _ := range c {
if _, ok := cc[k]; !ok {
warn("Warning: unrecognized key %q in provided cloud config - ignoring section", k)
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)
}
}
// Check for unrecognized coreos options, if any are set
if coreos, ok := c["coreos"]; ok {
if set, ok := coreos.(map[interface{}]interface{}); ok {
known := cc["coreos"].(map[interface{}]interface{})
for k, _ := range set {
if key, ok := k.(string); ok {
if _, ok := known[key]; !ok {
warn("Warning: unrecognized key %q in coreos section of provided cloud config - ignoring", key)
}
} else {
warn("Warning: unrecognized key %q in coreos section of provided cloud config - ignoring", k)
}
}
}
}
// Check for any badly-specified users, if any are set
if users, ok := c["users"]; ok {
var known map[string]interface{}
b, _ := yaml.Marshal(&User{})
yaml.Unmarshal(b, &known)
if set, ok := users.([]interface{}); ok {
for _, u := range set {
if user, ok := u.(map[interface{}]interface{}); ok {
for k, _ := range user {
if key, ok := k.(string); ok {
if _, ok := known[key]; !ok {
warn("Warning: unrecognized key %q in user section of cloud config - ignoring", key)
}
} else {
warn("Warning: unrecognized key %q in user section of cloud config - ignoring", k)
}
}
}
}
}
}
// Check for any badly-specified files, if any are set
if files, ok := c["write_files"]; ok {
var known map[string]interface{}
b, _ := yaml.Marshal(&File{})
yaml.Unmarshal(b, &known)
if set, ok := files.([]interface{}); ok {
for _, f := range set {
if file, ok := f.(map[interface{}]interface{}); ok {
for k, _ := range file {
if key, ok := k.(string); ok {
if _, ok := known[key]; !ok {
warn("Warning: unrecognized key %q in file section of cloud config - ignoring", key)
}
} else {
warn("Warning: unrecognized key %q in file section of cloud config - ignoring", k)
}
}
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

@@ -1,8 +1,22 @@
/*
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 (
"errors"
"fmt"
"reflect"
"strings"
"testing"
@@ -22,12 +36,12 @@ func TestIsZero(t *testing.T) {
{struct{ A int }{A: 1}, false},
} {
if empty := IsZero(tt.c); tt.empty != empty {
t.Errorf("bad result (%q): want %q, got %q", tt.c, tt.empty, empty)
t.Errorf("bad result (%q): want %t, got %t", tt.c, tt.empty, empty)
}
}
}
func TestAssertValid(t *testing.T) {
func TestAssertStructValid(t *testing.T) {
for _, tt := range []struct {
c interface{}
err error
@@ -44,7 +58,7 @@ func TestAssertValid(t *testing.T) {
}{A: "1", b: "hello"}, nil},
{struct {
A, b string `valid:"1,2"`
}{A: "hello", b: "2"}, errors.New("invalid value \"hello\" for option \"A\" (valid options: \"1,2\")")},
}{A: "hello", b: "2"}, &ErrorValid{Value: "hello", Field: "A", Valid: []string{"1", "2"}}},
{struct {
A, b int `valid:"1,2"`
}{}, nil},
@@ -56,9 +70,9 @@ func TestAssertValid(t *testing.T) {
}{A: 1, b: 9}, nil},
{struct {
A, b int `valid:"1,2"`
}{A: 9, b: 2}, errors.New("invalid value \"9\" for option \"A\" (valid options: \"1,2\")")},
}{A: 9, b: 2}, &ErrorValid{Value: "9", Field: "A", Valid: []string{"1", "2"}}},
} {
if err := AssertValid(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)
}
}
@@ -131,29 +145,6 @@ hostname:
if len(cfg.Users) < 1 || cfg.Users[0].Name != "fry" || cfg.Users[0].PasswordHash != "somehash" {
t.Fatalf("users section not correctly set when invalid keys are present")
}
var warnings string
catchWarn := func(f string, v ...interface{}) {
warnings += fmt.Sprintf(f, v...)
}
warnOnUnrecognizedKeys(contents, catchWarn)
if !strings.Contains(warnings, "coreos_unknown") {
t.Errorf("warnings did not catch unrecognized coreos option coreos_unknown")
}
if !strings.Contains(warnings, "bare_unknown") {
t.Errorf("warnings did not catch unrecognized key bare_unknown")
}
if !strings.Contains(warnings, "section_unknown") {
t.Errorf("warnings did not catch unrecognized key section_unknown")
}
if !strings.Contains(warnings, "user_unknown") {
t.Errorf("warnings did not catch unrecognized user key user_unknown")
}
if !strings.Contains(warnings, "file_unknown") {
t.Errorf("warnings did not catch unrecognized file key file_unknown")
}
}
// Assert that the parsing of a cloud config file "generally works"
@@ -184,7 +175,7 @@ coreos:
etcd:
discovery: "https://discovery.etcd.io/827c73219eeb2fa5530027c37bf18877"
update:
reboot-strategy: reboot
reboot_strategy: reboot
units:
- name: 50-eth0.network
runtime: yes
@@ -201,9 +192,9 @@ coreos:
oem:
id: rackspace
name: Rackspace Cloud Servers
version-id: 168.0.0
home-url: https://www.rackspace.com/cloud/servers/
bug-report-url: https://github.com/coreos/coreos-overlay
version_id: 168.0.0
home_url: https://www.rackspace.com/cloud/servers/
bug_report_url: https://github.com/coreos/coreos-overlay
ssh_authorized_keys:
- foobar
- foobaz
@@ -338,18 +329,18 @@ func TestCloudConfigUsers(t *testing.T) {
users:
- name: elroy
passwd: somehash
ssh-authorized-keys:
ssh_authorized_keys:
- somekey
gecos: arbitrary comment
homedir: /home/place
no-create-home: yes
primary-group: things
no_create_home: yes
primary_group: things
groups:
- ping
- pong
no-user-group: true
no_user_group: true
system: y
no-log-init: True
no_log_init: True
`
cfg, err := NewCloudConfig(contents)
if err != nil {
@@ -357,7 +348,7 @@ users:
}
if len(cfg.Users) != 1 {
t.Fatalf("Parsed %d users, expected 1", cfg.Users)
t.Fatalf("Parsed %d users, expected 1", len(cfg.Users))
}
user := cfg.Users[0]
@@ -388,11 +379,11 @@ users:
}
if !user.NoCreateHome {
t.Errorf("Failed to parse no-create-home field")
t.Errorf("Failed to parse no_create_home field")
}
if user.PrimaryGroup != "things" {
t.Errorf("Failed to parse primary-group field, got %q", user.PrimaryGroup)
t.Errorf("Failed to parse primary_group field, got %q", user.PrimaryGroup)
}
if len(user.Groups) != 2 {
@@ -407,7 +398,7 @@ users:
}
if !user.NoUserGroup {
t.Errorf("Failed to parse no-user-group field")
t.Errorf("Failed to parse no_user_group field")
}
if !user.System {
@@ -415,7 +406,7 @@ users:
}
if !user.NoLogInit {
t.Errorf("Failed to parse no-log-init field")
t.Errorf("Failed to parse no_log_init field")
}
}
@@ -424,7 +415,7 @@ func TestCloudConfigUsersGithubUser(t *testing.T) {
contents := `
users:
- name: elroy
coreos-ssh-import-github: bcwaldon
coreos_ssh_import_github: bcwaldon
`
cfg, err := NewCloudConfig(contents)
if err != nil {
@@ -432,7 +423,7 @@ users:
}
if len(cfg.Users) != 1 {
t.Fatalf("Parsed %d users, expected 1", cfg.Users)
t.Fatalf("Parsed %d users, expected 1", len(cfg.Users))
}
user := cfg.Users[0]
@@ -450,7 +441,7 @@ func TestCloudConfigUsersSSHImportURL(t *testing.T) {
contents := `
users:
- name: elroy
coreos-ssh-import-url: https://token:x-auth-token@github.enterprise.com/api/v3/polvi/keys
coreos_ssh_import_url: https://token:x-auth-token@github.enterprise.com/api/v3/polvi/keys
`
cfg, err := NewCloudConfig(contents)
if err != nil {
@@ -458,7 +449,7 @@ users:
}
if len(cfg.Users) != 1 {
t.Fatalf("Parsed %d users, expected 1", cfg.Users)
t.Fatalf("Parsed %d users, expected 1", len(cfg.Users))
}
user := cfg.Users[0]
@@ -471,3 +462,30 @@ users:
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"},
} {
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

@@ -1,3 +1,19 @@
/*
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
type EtcHosts string

View File

@@ -1,32 +1,48 @@
/*
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
type Etcd struct {
Addr string `yaml:"addr" env:"ETCD_ADDR"`
BindAddr string `yaml:"bind-addr" env:"ETCD_BIND_ADDR"`
CAFile string `yaml:"ca-file" env:"ETCD_CA_FILE"`
CertFile string `yaml:"cert-file" env:"ETCD_CERT_FILE"`
ClusterActiveSize string `yaml:"cluster-active-size" env:"ETCD_CLUSTER_ACTIVE_SIZE"`
ClusterRemoveDelay string `yaml:"cluster-remove-delay" env:"ETCD_CLUSTER_REMOVE_DELAY"`
ClusterSyncInterval string `yaml:"cluster-sync-interval" env:"ETCD_CLUSTER_SYNC_INTERVAL"`
BindAddr string `yaml:"bind_addr" env:"ETCD_BIND_ADDR"`
CAFile string `yaml:"ca_file" env:"ETCD_CA_FILE"`
CertFile string `yaml:"cert_file" env:"ETCD_CERT_FILE"`
ClusterActiveSize string `yaml:"cluster_active_size" env:"ETCD_CLUSTER_ACTIVE_SIZE"`
ClusterRemoveDelay string `yaml:"cluster_remove_delay" env:"ETCD_CLUSTER_REMOVE_DELAY"`
ClusterSyncInterval string `yaml:"cluster_sync_interval" env:"ETCD_CLUSTER_SYNC_INTERVAL"`
Cors string `yaml:"cors" env:"ETCD_CORS"`
CPUProfileFile string `yaml:"cpu-profile-file" env:"ETCD_CPU_PROFILE_FILE"`
DataDir string `yaml:"data-dir" env:"ETCD_DATA_DIR"`
CPUProfileFile string `yaml:"cpu_profile_file" env:"ETCD_CPU_PROFILE_FILE"`
DataDir string `yaml:"data_dir" env:"ETCD_DATA_DIR"`
Discovery string `yaml:"discovery" env:"ETCD_DISCOVERY"`
HTTPReadTimeout string `yaml:"http-read-timeout" env:"ETCD_HTTP_READ_TIMEOUT"`
HTTPWriteTimeout string `yaml:"http-write-timeout" env:"ETCD_HTTP_WRITE_TIMEOUT"`
KeyFile string `yaml:"key-file" env:"ETCD_KEY_FILE"`
MaxClusterSize string `yaml:"max-cluster-size" env:"ETCD_MAX_CLUSTER_SIZE"`
MaxResultBuffer string `yaml:"max-result-buffer" env:"ETCD_MAX_RESULT_BUFFER"`
MaxRetryAttempts string `yaml:"max-retry-attempts" env:"ETCD_MAX_RETRY_ATTEMPTS"`
HTTPReadTimeout string `yaml:"http_read_timeout" env:"ETCD_HTTP_READ_TIMEOUT"`
HTTPWriteTimeout string `yaml:"http_write_timeout" env:"ETCD_HTTP_WRITE_TIMEOUT"`
KeyFile string `yaml:"key_file" env:"ETCD_KEY_FILE"`
MaxClusterSize string `yaml:"max_cluster_size" env:"ETCD_MAX_CLUSTER_SIZE"`
MaxResultBuffer string `yaml:"max_result_buffer" env:"ETCD_MAX_RESULT_BUFFER"`
MaxRetryAttempts string `yaml:"max_retry_attempts" env:"ETCD_MAX_RETRY_ATTEMPTS"`
Name string `yaml:"name" env:"ETCD_NAME"`
PeerAddr string `yaml:"peer-addr" env:"ETCD_PEER_ADDR"`
PeerBindAddr string `yaml:"peer-bind-addr" env:"ETCD_PEER_BIND_ADDR"`
PeerCAFile string `yaml:"peer-ca-file" env:"ETCD_PEER_CA_FILE"`
PeerCertFile string `yaml:"peer-cert-file" env:"ETCD_PEER_CERT_FILE"`
PeerKeyFile string `yaml:"peer-key-file" env:"ETCD_PEER_KEY_FILE"`
PeerAddr string `yaml:"peer_addr" env:"ETCD_PEER_ADDR"`
PeerBindAddr string `yaml:"peer_bind_addr" env:"ETCD_PEER_BIND_ADDR"`
PeerCAFile string `yaml:"peer_ca_file" env:"ETCD_PEER_CA_FILE"`
PeerCertFile string `yaml:"peer_cert_file" env:"ETCD_PEER_CERT_FILE"`
PeerKeyFile string `yaml:"peer_key_file" env:"ETCD_PEER_KEY_FILE"`
Peers string `yaml:"peers" env:"ETCD_PEERS"`
PeersFile string `yaml:"peers-file" env:"ETCD_PEERS_FILE"`
PeersFile string `yaml:"peers_file" env:"ETCD_PEERS_FILE"`
Snapshot string `yaml:"snapshot" env:"ETCD_SNAPSHOT"`
Verbose string `yaml:"verbose" env:"ETCD_VERBOSE"`
VeryVerbose string `yaml:"very-verbose" env:"ETCD_VERY_VERBOSE"`
VeryVerbose string `yaml:"very_verbose" env:"ETCD_VERY_VERBOSE"`
}

View File

@@ -1,3 +1,19 @@
/*
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
type File struct {

9
config/flannel.go Normal file
View File

@@ -0,0 +1,9 @@
package config
type Flannel struct {
EtcdEndpoint string `yaml:"etcd-endpoint" env:"FLANNELD_ETCD_ENDPOINT"`
EtcdPrefix string `yaml:"etcd-prefix" env:"FLANNELD_ETCD_PREFIX"`
IPMasq string `yaml:"ip-masq" env:"FLANNELD_IP_MASQ"`
SubnetFile string `yaml:"subnet-file" env:"FLANNELD_SUBNET_FILE"`
Iface string `yaml:"interface" env:"FLANNELD_IFACE"`
}

View File

@@ -1,14 +1,30 @@
/*
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
type Fleet struct {
AgentTTL string `yaml:"agent-ttl" env:"FLEET_AGENT_TTL"`
EngineReconcileInterval string `yaml:"engine-reconcile-interval" env:"FLEET_ENGINE_RECONCILE_INTERVAL"`
EtcdCAFile string `yaml:"etcd-cafile" env:"FLEET_ETCD_CAFILE"`
EtcdCertFile string `yaml:"etcd-certfile" env:"FLEET_ETCD_CERTFILE"`
EtcdKeyFile string `yaml:"etcd-keyfile" env:"FLEET_ETCD_KEYFILE"`
EtcdRequestTimeout string `yaml:"etcd-request-timeout" env:"FLEET_ETCD_REQUEST_TIMEOUT"`
EtcdServers string `yaml:"etcd-servers" env:"FLEET_ETCD_SERVERS"`
AgentTTL string `yaml:"agent_ttl" env:"FLEET_AGENT_TTL"`
EngineReconcileInterval string `yaml:"engine_reconcile_interval" env:"FLEET_ENGINE_RECONCILE_INTERVAL"`
EtcdCAFile string `yaml:"etcd_cafile" env:"FLEET_ETCD_CAFILE"`
EtcdCertFile string `yaml:"etcd_certfile" env:"FLEET_ETCD_CERTFILE"`
EtcdKeyFile string `yaml:"etcd_keyfile" env:"FLEET_ETCD_KEYFILE"`
EtcdRequestTimeout string `yaml:"etcd_request_timeout" env:"FLEET_ETCD_REQUEST_TIMEOUT"`
EtcdServers string `yaml:"etcd_servers" env:"FLEET_ETCD_SERVERS"`
Metadata string `yaml:"metadata" env:"FLEET_METADATA"`
PublicIP string `yaml:"public-ip" env:"FLEET_PUBLIC_IP"`
PublicIP string `yaml:"public_ip" env:"FLEET_PUBLIC_IP"`
Verbosity string `yaml:"verbosity" env:"FLEET_VERBOSITY"`
}

View File

@@ -1,9 +1,25 @@
/*
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
type OEM struct {
ID string `yaml:"id"`
Name string `yaml:"name"`
VersionID string `yaml:"version-id"`
HomeURL string `yaml:"home-url"`
BugReportURL string `yaml:"bug-report-url"`
VersionID string `yaml:"version_id"`
HomeURL string `yaml:"home_url"`
BugReportURL string `yaml:"bug_report_url"`
}

32
config/script.go Normal file
View File

@@ -0,0 +1,32 @@
/*
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 (
"strings"
)
type Script []byte
func IsScript(userdata string) bool {
header := strings.SplitN(userdata, "\n", 2)[0]
return strings.HasPrefix(header, "#!")
}
func NewScript(userdata string) (Script, error) {
return Script(userdata), nil
}

View File

@@ -1,3 +1,19 @@
/*
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 (
@@ -11,7 +27,7 @@ type Unit struct {
Enable bool `yaml:"enable"`
Runtime bool `yaml:"runtime"`
Content string `yaml:"content"`
Command string `yaml:"command"`
Command string `yaml:"command" valid:"start,stop,restart,reload,try-restart,reload-or-restart,reload-or-try-restart"`
// For drop-in units, a cloudinit.conf is generated.
// This is currently unbound in YAML (and hence unsettable in cloud-config files)

View File

@@ -1,7 +1,23 @@
/*
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
type Update struct {
RebootStrategy string `yaml:"reboot-strategy" env:"REBOOT_STRATEGY" valid:"best-effort,etcd-lock,reboot,off"`
RebootStrategy string `yaml:"reboot_strategy" env:"REBOOT_STRATEGY" valid:"best-effort,etcd-lock,reboot,false"`
Group string `yaml:"group" env:"GROUP"`
Server string `yaml:"server" env:"SERVER"`
}

View File

@@ -1,17 +1,33 @@
/*
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
type User struct {
Name string `yaml:"name"`
PasswordHash string `yaml:"passwd"`
SSHAuthorizedKeys []string `yaml:"ssh-authorized-keys"`
SSHImportGithubUser string `yaml:"coreos-ssh-import-github"`
SSHImportURL string `yaml:"coreos-ssh-import-url"`
SSHAuthorizedKeys []string `yaml:"ssh_authorized_keys"`
SSHImportGithubUser string `yaml:"coreos_ssh_import_github"`
SSHImportURL string `yaml:"coreos_ssh_import_url"`
GECOS string `yaml:"gecos"`
Homedir string `yaml:"homedir"`
NoCreateHome bool `yaml:"no-create-home"`
PrimaryGroup string `yaml:"primary-group"`
NoCreateHome bool `yaml:"no_create_home"`
PrimaryGroup string `yaml:"primary_group"`
Groups []string `yaml:"groups"`
NoUserGroup bool `yaml:"no-user-group"`
NoUserGroup bool `yaml:"no_user_group"`
System bool `yaml:"system"`
NoLogInit bool `yaml:"no-log-init"`
NoLogInit bool `yaml:"no_log_init"`
}

View File

@@ -0,0 +1,54 @@
/*
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 validate
import (
"strings"
)
// context represents the current position within a newline-delimited string.
// Each line is loaded, one by one, into currentLine (newline omitted) and
// lineNumber keeps track of its position within the original string.
type context struct {
currentLine string
remainingLines string
lineNumber int
}
// Increment moves the context to the next line (if available).
func (c *context) Increment() {
if c.currentLine == "" && c.remainingLines == "" {
return
}
lines := strings.SplitN(c.remainingLines, "\n", 2)
c.currentLine = lines[0]
if len(lines) == 2 {
c.remainingLines = lines[1]
} else {
c.remainingLines = ""
}
c.lineNumber++
}
// NewContext creates a context from the provided data. It strips out all
// carriage returns and moves to the first line (if available).
func NewContext(content []byte) context {
c := context{remainingLines: strings.Replace(string(content), "\r", "", -1)}
c.Increment()
return c
}

View File

@@ -0,0 +1,133 @@
/*
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 validate
import (
"reflect"
"testing"
)
func TestNewContext(t *testing.T) {
tests := []struct {
in string
out context
}{
{
out: context{
currentLine: "",
remainingLines: "",
lineNumber: 0,
},
},
{
in: "this\r\nis\r\na\r\ntest",
out: context{
currentLine: "this",
remainingLines: "is\na\ntest",
lineNumber: 1,
},
},
}
for _, tt := range tests {
if out := NewContext([]byte(tt.in)); !reflect.DeepEqual(tt.out, out) {
t.Errorf("bad context (%q): want %#v, got %#v", tt.in, tt.out, out)
}
}
}
func TestIncrement(t *testing.T) {
tests := []struct {
init context
op func(c *context)
res context
}{
{
init: context{
currentLine: "",
remainingLines: "",
lineNumber: 0,
},
res: context{
currentLine: "",
remainingLines: "",
lineNumber: 0,
},
op: func(c *context) {
c.Increment()
},
},
{
init: context{
currentLine: "test",
remainingLines: "",
lineNumber: 1,
},
res: context{
currentLine: "",
remainingLines: "",
lineNumber: 2,
},
op: func(c *context) {
c.Increment()
c.Increment()
c.Increment()
},
},
{
init: context{
currentLine: "this",
remainingLines: "is\na\ntest",
lineNumber: 1,
},
res: context{
currentLine: "is",
remainingLines: "a\ntest",
lineNumber: 2,
},
op: func(c *context) {
c.Increment()
},
},
{
init: context{
currentLine: "this",
remainingLines: "is\na\ntest",
lineNumber: 1,
},
res: context{
currentLine: "test",
remainingLines: "",
lineNumber: 4,
},
op: func(c *context) {
c.Increment()
c.Increment()
c.Increment()
},
},
}
for i, tt := range tests {
res := tt.init
if tt.op(&res); !reflect.DeepEqual(tt.res, res) {
t.Errorf("bad context (%d, %#v): want %#v, got %#v", i, tt.init, tt.res, res)
}
}
}

159
config/validate/node.go Normal file
View File

@@ -0,0 +1,159 @@
/*
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 validate
import (
"fmt"
"reflect"
"regexp"
)
var (
yamlKey = regexp.MustCompile(`^ *-? ?(?P<key>.*?):`)
yamlElem = regexp.MustCompile(`^ *-`)
)
type node struct {
name string
line int
children []node
field reflect.StructField
reflect.Value
}
// Child attempts to find the child with the given name in the node's list of
// children. If no such child is found, an invalid node is returned.
func (n node) Child(name string) node {
for _, c := range n.children {
if c.name == name {
return c
}
}
return node{}
}
// HumanType returns the human-consumable string representation of the type of
// the node.
func (n node) HumanType() string {
switch k := n.Kind(); k {
case reflect.Slice:
c := n.Type().Elem()
return "[]" + node{Value: reflect.New(c).Elem()}.HumanType()
default:
return k.String()
}
}
// NewNode returns the node representation of the given value. The context
// will be used in an attempt to determine line numbers for the given value.
func NewNode(value interface{}, context context) node {
var n node
toNode(value, context, &n)
return n
}
// toNode converts the given value into a node and then recursively processes
// each of the nodes components (e.g. fields, array elements, keys).
func toNode(v interface{}, c context, n *node) {
vv := reflect.ValueOf(v)
if !vv.IsValid() {
return
}
n.Value = vv
switch vv.Kind() {
case reflect.Struct:
// Walk over each field in the structure, skipping unexported fields,
// and create a node for it.
for i := 0; i < vv.Type().NumField(); i++ {
ft := vv.Type().Field(i)
k := ft.Tag.Get("yaml")
if k == "-" || k == "" {
continue
}
cn := node{name: k, field: ft}
c, ok := findKey(cn.name, c)
if ok {
cn.line = c.lineNumber
}
toNode(vv.Field(i).Interface(), c, &cn)
n.children = append(n.children, cn)
}
case reflect.Map:
// Walk over each key in the map and create a node for it.
v := v.(map[interface{}]interface{})
for k, cv := range v {
cn := node{name: fmt.Sprintf("%s", k)}
c, ok := findKey(cn.name, c)
if ok {
cn.line = c.lineNumber
}
toNode(cv, c, &cn)
n.children = append(n.children, cn)
}
case reflect.Slice:
// Walk over each element in the slice and create a node for it.
// While iterating over the slice, preserve the context after it
// is modified. This allows the line numbers to reflect the current
// element instead of the first.
for i := 0; i < vv.Len(); i++ {
cn := node{
name: fmt.Sprintf("%s[%d]", n.name, i),
field: n.field,
}
var ok bool
c, ok = findElem(c)
if ok {
cn.line = c.lineNumber
}
toNode(vv.Index(i).Interface(), c, &cn)
n.children = append(n.children, cn)
c.Increment()
}
case reflect.String, reflect.Int, reflect.Bool:
default:
panic(fmt.Sprintf("toNode(): unhandled kind %s", vv.Kind()))
}
}
// findKey attempts to find the requested key within the provided context.
// A modified copy of the context is returned with every line up to the key
// incremented past. A boolean, true if the key was found, is also returned.
func findKey(key string, context context) (context, bool) {
return find(yamlKey, key, context)
}
// findElem attempts to find an array element within the provided context.
// A modified copy of the context is returned with every line up to the array
// element incremented past. A boolean, true if the key was found, is also
// returned.
func findElem(context context) (context, bool) {
return find(yamlElem, "", context)
}
func find(exp *regexp.Regexp, key string, context context) (context, bool) {
for len(context.currentLine) > 0 || len(context.remainingLines) > 0 {
matches := exp.FindStringSubmatch(context.currentLine)
if len(matches) > 0 && (key == "" || matches[1] == key) {
return context, true
}
context.Increment()
}
return context, false
}

View File

@@ -0,0 +1,286 @@
/*
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 validate
import (
"reflect"
"testing"
)
func TestChild(t *testing.T) {
tests := []struct {
parent node
name string
child node
}{
{},
{
name: "c1",
},
{
parent: node{
children: []node{
node{name: "c1"},
node{name: "c2"},
node{name: "c3"},
},
},
},
{
parent: node{
children: []node{
node{name: "c1"},
node{name: "c2"},
node{name: "c3"},
},
},
name: "c2",
child: node{name: "c2"},
},
}
for _, tt := range tests {
if child := tt.parent.Child(tt.name); !reflect.DeepEqual(tt.child, child) {
t.Errorf("bad child (%q): want %#v, got %#v", tt.name, tt.child, child)
}
}
}
func TestHumanType(t *testing.T) {
tests := []struct {
node node
humanType string
}{
{
humanType: "invalid",
},
{
node: node{Value: reflect.ValueOf("hello")},
humanType: "string",
},
{
node: node{
Value: reflect.ValueOf([]int{1, 2}),
children: []node{
node{Value: reflect.ValueOf(1)},
node{Value: reflect.ValueOf(2)},
}},
humanType: "[]int",
},
}
for _, tt := range tests {
if humanType := tt.node.HumanType(); tt.humanType != humanType {
t.Errorf("bad type (%q): want %q, got %q", tt.node, tt.humanType, humanType)
}
}
}
func TestToNode(t *testing.T) {
tests := []struct {
value interface{}
context context
node node
}{
{},
{
value: struct{}{},
node: node{Value: reflect.ValueOf(struct{}{})},
},
{
value: struct {
A int `yaml:"a"`
}{},
node: node{
children: []node{
node{
name: "a",
field: reflect.TypeOf(struct {
A int `yaml:"a"`
}{}).Field(0),
},
},
},
},
{
value: struct {
A []int `yaml:"a"`
}{},
node: node{
children: []node{
node{
name: "a",
field: reflect.TypeOf(struct {
A []int `yaml:"a"`
}{}).Field(0),
},
},
},
},
{
value: map[interface{}]interface{}{
"a": map[interface{}]interface{}{
"b": 2,
},
},
context: NewContext([]byte("a:\n b: 2")),
node: node{
children: []node{
node{
line: 1,
name: "a",
children: []node{
node{name: "b", line: 2},
},
},
},
},
},
{
value: struct {
A struct {
Jon bool `yaml:"b"`
} `yaml:"a"`
}{},
node: node{
children: []node{
node{
name: "a",
children: []node{
node{
name: "b",
field: reflect.TypeOf(struct {
Jon bool `yaml:"b"`
}{}).Field(0),
Value: reflect.ValueOf(false),
},
},
field: reflect.TypeOf(struct {
A struct {
Jon bool `yaml:"b"`
} `yaml:"a"`
}{}).Field(0),
Value: reflect.ValueOf(struct {
Jon bool `yaml:"b"`
}{}),
},
},
Value: reflect.ValueOf(struct {
A struct {
Jon bool `yaml:"b"`
} `yaml:"a"`
}{}),
},
},
}
for _, tt := range tests {
var node node
toNode(tt.value, tt.context, &node)
if !nodesEqual(tt.node, node) {
t.Errorf("bad node (%#v): want %#v, got %#v", tt.value, tt.node, node)
}
}
}
func TestFindKey(t *testing.T) {
tests := []struct {
key string
context context
found bool
}{
{},
{
key: "key1",
context: NewContext([]byte("key1: hi")),
found: true,
},
{
key: "key2",
context: NewContext([]byte("key1: hi")),
found: false,
},
{
key: "key3",
context: NewContext([]byte("key1:\n key2:\n key3: hi")),
found: true,
},
{
key: "key4",
context: NewContext([]byte("key1:\n - key4: hi")),
found: true,
},
{
key: "key5",
context: NewContext([]byte("#key5")),
found: false,
},
}
for _, tt := range tests {
if _, found := findKey(tt.key, tt.context); tt.found != found {
t.Errorf("bad find (%q): want %t, got %t", tt.key, tt.found, found)
}
}
}
func TestFindElem(t *testing.T) {
tests := []struct {
context context
found bool
}{
{},
{
context: NewContext([]byte("test: hi")),
found: false,
},
{
context: NewContext([]byte("test:\n - a\n -b")),
found: true,
},
{
context: NewContext([]byte("test:\n -\n a")),
found: true,
},
}
for _, tt := range tests {
if _, found := findElem(tt.context); tt.found != found {
t.Errorf("bad find (%q): want %t, got %t", tt.context, tt.found, found)
}
}
}
func nodesEqual(a, b node) bool {
if a.name != b.name ||
a.line != b.line ||
!reflect.DeepEqual(a.field, b.field) ||
len(a.children) != len(b.children) {
return false
}
for i := 0; i < len(a.children); i++ {
if !nodesEqual(a.children[i], b.children[i]) {
return false
}
}
return true
}

Some files were not shown because too many files have changed in this diff Show More