Compare commits

...

17 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
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
34 changed files with 1758 additions and 234 deletions

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 [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 #### update
The `coreos.update.*` parameters manipulate settings related to how CoreOS instances are updated. The `coreos.update.*` parameters manipulate settings related to how CoreOS instances are updated.

View File

@@ -18,7 +18,6 @@ package config
import ( import (
"fmt" "fmt"
"log"
"reflect" "reflect"
"strings" "strings"
@@ -31,11 +30,12 @@ import (
type CloudConfig struct { type CloudConfig struct {
SSHAuthorizedKeys []string `yaml:"ssh_authorized_keys"` SSHAuthorizedKeys []string `yaml:"ssh_authorized_keys"`
Coreos struct { Coreos struct {
Etcd Etcd `yaml:"etcd"` Etcd Etcd `yaml:"etcd"`
Fleet Fleet `yaml:"fleet"` Flannel Flannel `yaml:"flannel"`
OEM OEM `yaml:"oem"` Fleet Fleet `yaml:"fleet"`
Update Update `yaml:"update"` OEM OEM `yaml:"oem"`
Units []Unit `yaml:"units"` Update Update `yaml:"update"`
Units []Unit `yaml:"units"`
} `yaml:"coreos"` } `yaml:"coreos"`
WriteFiles []File `yaml:"write_files"` WriteFiles []File `yaml:"write_files"`
Hostname string `yaml:"hostname"` Hostname string `yaml:"hostname"`
@@ -45,16 +45,29 @@ type CloudConfig struct {
NetworkConfig string `yaml:"-"` 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 // NewCloudConfig instantiates a new CloudConfig from the given contents (a
// 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) {
var cfg CloudConfig var cfg CloudConfig
err := yaml.Unmarshal([]byte(contents), &cfg) ncontents, err := normalizeConfig(contents)
if err != nil { if err != nil {
return &cfg, err return &cfg, err
} }
warnOnUnrecognizedKeys(contents, log.Printf) if err = yaml.Unmarshal(ncontents, &cfg); err != nil {
return &cfg, err
}
return &cfg, nil return &cfg, nil
} }
@@ -76,10 +89,20 @@ func IsZero(c interface{}) bool {
return isZero(reflect.ValueOf(c)) return isZero(reflect.ValueOf(c))
} }
// AssertValid checks the fields in the structure and makes sure that they type ErrorValid struct {
// contain valid values as specified by the 'valid' flag. Empty fields are 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. // implicitly valid.
func AssertValid(c interface{}) error { func AssertStructValid(c interface{}) error {
ct := reflect.TypeOf(c) ct := reflect.TypeOf(c)
cv := reflect.ValueOf(c) cv := reflect.ValueOf(c)
for i := 0; i < ct.NumField(); i++ { for i := 0; i < ct.NumField(); i++ {
@@ -88,15 +111,33 @@ func AssertValid(c interface{}) error {
continue continue
} }
valid := ft.Tag.Get("valid") if err := AssertValid(cv.Field(i), ft.Tag.Get("valid")); err != nil {
val := cv.Field(i) err.Field = ft.Name
if !isValid(val, valid) { return err
return fmt.Errorf("invalid value \"%v\" for option %q (valid options: %q)", val.Interface(), ft.Name, valid)
} }
} }
return nil 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 { func isZero(v reflect.Value) bool {
switch v.Kind() { switch v.Kind() {
case reflect.Struct: case reflect.Struct:
@@ -116,99 +157,30 @@ func isFieldExported(f reflect.StructField) bool {
return f.PkgPath == "" return f.PkgPath == ""
} }
func isValid(v reflect.Value, valid string) bool { func normalizeConfig(config string) ([]byte, error) {
if valid == "" || isZero(v) { var cfg map[interface{}]interface{}
return true if err := yaml.Unmarshal([]byte(config), &cfg); err != nil {
return nil, err
} }
vs := fmt.Sprintf("%v", v.Interface()) return yaml.Marshal(normalizeKeys(cfg))
for _, valid := range strings.Split(valid, ",") {
if vs == valid {
return true
}
}
return false
} }
type warner func(format string, v ...interface{}) func normalizeKeys(m map[interface{}]interface{}) map[interface{}]interface{} {
for k, v := range m {
// warnOnUnrecognizedKeys parses the contents of a cloud-config file and calls if m, ok := m[k].(map[interface{}]interface{}); ok {
// warn(msg, key) for every unrecognized key (i.e. those not present in CloudConfig) normalizeKeys(m)
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)
} }
}
// Check for unrecognized coreos options, if any are set if s, ok := m[k].([]interface{}); ok {
if coreos, ok := c["coreos"]; ok { for _, e := range s {
if set, ok := coreos.(map[interface{}]interface{}); ok { if m, ok := e.(map[interface{}]interface{}); ok {
known := cc["coreos"].(map[interface{}]interface{}) normalizeKeys(m)
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)
}
}
} }
} }
} }
delete(m, k)
m[strings.Replace(fmt.Sprint(k), "-", "_", -1)] = v
} }
return m
} }

View File

@@ -17,8 +17,6 @@
package config package config
import ( import (
"errors"
"fmt"
"reflect" "reflect"
"strings" "strings"
"testing" "testing"
@@ -43,7 +41,7 @@ func TestIsZero(t *testing.T) {
} }
} }
func TestAssertValid(t *testing.T) { func TestAssertStructValid(t *testing.T) {
for _, tt := range []struct { for _, tt := range []struct {
c interface{} c interface{}
err error err error
@@ -60,7 +58,7 @@ func TestAssertValid(t *testing.T) {
}{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"}, 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 { {struct {
A, b int `valid:"1,2"` A, b int `valid:"1,2"`
}{}, nil}, }{}, nil},
@@ -72,9 +70,9 @@ func TestAssertValid(t *testing.T) {
}{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}, 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) t.Errorf("bad result (%q): want %q, got %q", tt.c, tt.err, err)
} }
} }
@@ -147,29 +145,6 @@ hostname:
if len(cfg.Users) < 1 || cfg.Users[0].Name != "fry" || cfg.Users[0].PasswordHash != "somehash" { 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") 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" // Assert that the parsing of a cloud config file "generally works"
@@ -200,7 +175,7 @@ coreos:
etcd: etcd:
discovery: "https://discovery.etcd.io/827c73219eeb2fa5530027c37bf18877" discovery: "https://discovery.etcd.io/827c73219eeb2fa5530027c37bf18877"
update: update:
reboot-strategy: reboot reboot_strategy: reboot
units: units:
- name: 50-eth0.network - name: 50-eth0.network
runtime: yes runtime: yes
@@ -217,9 +192,9 @@ coreos:
oem: oem:
id: rackspace id: rackspace
name: Rackspace Cloud Servers name: Rackspace Cloud Servers
version-id: 168.0.0 version_id: 168.0.0
home-url: https://www.rackspace.com/cloud/servers/ home_url: https://www.rackspace.com/cloud/servers/
bug-report-url: https://github.com/coreos/coreos-overlay bug_report_url: https://github.com/coreos/coreos-overlay
ssh_authorized_keys: ssh_authorized_keys:
- foobar - foobar
- foobaz - foobaz
@@ -354,18 +329,18 @@ func TestCloudConfigUsers(t *testing.T) {
users: users:
- name: elroy - name: elroy
passwd: somehash passwd: somehash
ssh-authorized-keys: ssh_authorized_keys:
- somekey - somekey
gecos: arbitrary comment gecos: arbitrary comment
homedir: /home/place homedir: /home/place
no-create-home: yes no_create_home: yes
primary-group: things primary_group: things
groups: groups:
- ping - ping
- pong - pong
no-user-group: true no_user_group: true
system: y system: y
no-log-init: True no_log_init: True
` `
cfg, err := NewCloudConfig(contents) cfg, err := NewCloudConfig(contents)
if err != nil { if err != nil {
@@ -404,11 +379,11 @@ users:
} }
if !user.NoCreateHome { 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" { 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 { if len(user.Groups) != 2 {
@@ -423,7 +398,7 @@ users:
} }
if !user.NoUserGroup { if !user.NoUserGroup {
t.Errorf("Failed to parse no-user-group field") t.Errorf("Failed to parse no_user_group field")
} }
if !user.System { if !user.System {
@@ -431,7 +406,7 @@ users:
} }
if !user.NoLogInit { if !user.NoLogInit {
t.Errorf("Failed to parse no-log-init field") t.Errorf("Failed to parse no_log_init field")
} }
} }
@@ -440,7 +415,7 @@ func TestCloudConfigUsersGithubUser(t *testing.T) {
contents := ` contents := `
users: users:
- name: elroy - name: elroy
coreos-ssh-import-github: bcwaldon coreos_ssh_import_github: bcwaldon
` `
cfg, err := NewCloudConfig(contents) cfg, err := NewCloudConfig(contents)
if err != nil { if err != nil {
@@ -466,7 +441,7 @@ func TestCloudConfigUsersSSHImportURL(t *testing.T) {
contents := ` contents := `
users: users:
- name: elroy - 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) cfg, err := NewCloudConfig(contents)
if err != nil { if err != nil {
@@ -487,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) 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

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

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

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

View File

@@ -19,7 +19,7 @@ package config
type OEM struct { type OEM struct {
ID string `yaml:"id"` ID string `yaml:"id"`
Name string `yaml:"name"` Name string `yaml:"name"`
VersionID string `yaml:"version-id"` VersionID string `yaml:"version_id"`
HomeURL string `yaml:"home-url"` HomeURL string `yaml:"home_url"`
BugReportURL string `yaml:"bug-report-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

@@ -27,7 +27,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"` 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. // For drop-in units, a cloudinit.conf is generated.
// This is currently unbound in YAML (and hence unsettable in cloud-config files) // This is currently unbound in YAML (and hence unsettable in cloud-config files)

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"` RebootStrategy string `yaml:"reboot_strategy" env:"REBOOT_STRATEGY" valid:"best-effort,etcd-lock,reboot,false"`
Group string `yaml:"group" env:"GROUP"` Group string `yaml:"group" env:"GROUP"`
Server string `yaml:"server" env:"SERVER"` Server string `yaml:"server" env:"SERVER"`
} }

View File

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

90
config/validate/report.go Normal file
View File

@@ -0,0 +1,90 @@
/*
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 (
"encoding/json"
"fmt"
)
// Report represents the list of entries resulting from validation.
type Report struct {
entries []Entry
}
// Error adds an error entry to the report.
func (r *Report) Error(line int, message string) {
r.entries = append(r.entries, Entry{entryError, message, line})
}
// Warning adds a warning entry to the report.
func (r *Report) Warning(line int, message string) {
r.entries = append(r.entries, Entry{entryWarning, message, line})
}
// Info adds an info entry to the report.
func (r *Report) Info(line int, message string) {
r.entries = append(r.entries, Entry{entryInfo, message, line})
}
// Entries returns the list of entries in the report.
func (r *Report) Entries() []Entry {
return r.entries
}
// Entry represents a single generic item in the report.
type Entry struct {
kind entryKind
message string
line int
}
// String returns a human-readable representation of the entry.
func (e Entry) String() string {
return fmt.Sprintf("line %d: %s: %s", e.line, e.kind, e.message)
}
// MarshalJSON satisfies the json.Marshaler interface, returning the entry
// encoded as a JSON object.
func (e Entry) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]interface{}{
"kind": e.kind.String(),
"message": e.message,
"line": e.line,
})
}
type entryKind int
const (
entryError entryKind = iota
entryWarning
entryInfo
)
func (k entryKind) String() string {
switch k {
case entryError:
return "error"
case entryWarning:
return "warning"
case entryInfo:
return "info"
default:
panic(fmt.Sprintf("invalid kind %d", k))
}
}

View File

@@ -0,0 +1,98 @@
/*
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 (
"bytes"
"reflect"
"testing"
)
func TestEntry(t *testing.T) {
tests := []struct {
entry Entry
str string
json []byte
}{
{
Entry{entryInfo, "test info", 1},
"line 1: info: test info",
[]byte(`{"kind":"info","line":1,"message":"test info"}`),
},
{
Entry{entryWarning, "test warning", 1},
"line 1: warning: test warning",
[]byte(`{"kind":"warning","line":1,"message":"test warning"}`),
},
{
Entry{entryError, "test error", 2},
"line 2: error: test error",
[]byte(`{"kind":"error","line":2,"message":"test error"}`),
},
}
for _, tt := range tests {
if str := tt.entry.String(); tt.str != str {
t.Errorf("bad string (%q): want %q, got %q", tt.entry, tt.str, str)
}
json, err := tt.entry.MarshalJSON()
if err != nil {
t.Errorf("bad error (%q): want %v, got %q", tt.entry, nil, err)
}
if !bytes.Equal(tt.json, json) {
t.Errorf("bad JSON (%q): want %q, got %q", tt.entry, tt.json, json)
}
}
}
func TestReport(t *testing.T) {
type reportFunc struct {
fn func(*Report, int, string)
line int
message string
}
tests := []struct {
fs []reportFunc
es []Entry
}{
{
[]reportFunc{
{(*Report).Warning, 1, "test warning 1"},
{(*Report).Error, 2, "test error 2"},
{(*Report).Info, 10, "test info 10"},
},
[]Entry{
Entry{entryWarning, "test warning 1", 1},
Entry{entryError, "test error 2", 2},
Entry{entryInfo, "test info 10", 10},
},
},
}
for _, tt := range tests {
r := Report{}
for _, f := range tt.fs {
f.fn(&r, f.line, f.message)
}
if es := r.Entries(); !reflect.DeepEqual(tt.es, es) {
t.Errorf("bad entries (%v): want %#v, got %#v", tt.fs, tt.es, es)
}
}
}

115
config/validate/rules.go Normal file
View File

@@ -0,0 +1,115 @@
/*
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"
"github.com/coreos/coreos-cloudinit/config"
)
type rule func(config node, report *Report)
// Rules contains all of the validation rules.
var Rules []rule = []rule{
checkStructure,
checkValidity,
}
// checkStructure compares the provided config to the empty config.CloudConfig
// structure. Each node is checked to make sure that it exists in the known
// structure and that its type is compatible.
func checkStructure(cfg node, report *Report) {
g := NewNode(config.CloudConfig{}, NewContext([]byte{}))
checkNodeStructure(cfg, g, report)
}
func checkNodeStructure(n, g node, r *Report) {
if !isCompatible(n.Kind(), g.Kind()) {
r.Warning(n.line, fmt.Sprintf("incorrect type for %q (want %s)", n.name, g.HumanType()))
return
}
switch g.Kind() {
case reflect.Struct:
for _, cn := range n.children {
if cg := g.Child(cn.name); cg.IsValid() {
checkNodeStructure(cn, cg, r)
} else {
r.Warning(cn.line, fmt.Sprintf("unrecognized key %q", cn.name))
}
}
case reflect.Slice:
for _, cn := range n.children {
var cg node
c := g.Type().Elem()
toNode(reflect.New(c).Elem().Interface(), context{}, &cg)
checkNodeStructure(cn, cg, r)
}
case reflect.String, reflect.Int, reflect.Bool:
default:
panic(fmt.Sprintf("checkNodeStructure(): unhandled kind %s", g.Kind()))
}
}
// checkValidity checks the value of every node in the provided config by
// running config.AssertValid() on it.
func checkValidity(cfg node, report *Report) {
g := NewNode(config.CloudConfig{}, NewContext([]byte{}))
checkNodeValidity(cfg, g, report)
}
func checkNodeValidity(n, g node, r *Report) {
if err := config.AssertValid(n.Value, g.field.Tag.Get("valid")); err != nil {
r.Warning(n.line, fmt.Sprintf("invalid value %v", n.Value))
}
switch g.Kind() {
case reflect.Struct:
for _, cn := range n.children {
if cg := g.Child(cn.name); cg.IsValid() {
checkNodeValidity(cn, cg, r)
}
}
case reflect.Slice:
for _, cn := range n.children {
var cg node
c := g.Type().Elem()
toNode(reflect.New(c).Elem().Interface(), context{}, &cg)
checkNodeValidity(cn, cg, r)
}
case reflect.String, reflect.Int, reflect.Bool:
default:
panic(fmt.Sprintf("checkNodeValidity(): unhandled kind %s", g.Kind()))
}
}
// isCompatible determines if the type of kind n can be converted to the type
// of kind g in the context of YAML. This is not an exhaustive list, but its
// enough for the purposes of cloud-config validation.
func isCompatible(n, g reflect.Kind) bool {
switch g {
case reflect.String:
return n == reflect.String || n == reflect.Int || n == reflect.Bool
case reflect.Struct:
return n == reflect.Struct || n == reflect.Map
case reflect.Bool, reflect.Slice:
return n == g
default:
panic(fmt.Sprintf("isCompatible(): unhandled kind %s", g))
}
}

View File

@@ -0,0 +1,251 @@
/*
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 TestCheckStructure(t *testing.T) {
tests := []struct {
config string
entries []Entry
}{
{},
// Test for unrecognized keys
{
config: "test:",
entries: []Entry{{entryWarning, "unrecognized key \"test\"", 1}},
},
{
config: "coreos:\n etcd:\n bad:",
entries: []Entry{{entryWarning, "unrecognized key \"bad\"", 3}},
},
{
config: "coreos:\n etcd:\n discovery: good",
},
// Test for error on list of nodes
{
config: "coreos:\n units:\n - hello\n - goodbye",
entries: []Entry{
{entryWarning, "incorrect type for \"units[0]\" (want struct)", 3},
{entryWarning, "incorrect type for \"units[1]\" (want struct)", 4},
},
},
// Test for incorrect types
// Want boolean
{
config: "coreos:\n units:\n - enable: true",
},
{
config: "coreos:\n units:\n - enable: 4",
entries: []Entry{{entryWarning, "incorrect type for \"enable\" (want bool)", 3}},
},
{
config: "coreos:\n units:\n - enable: bad",
entries: []Entry{{entryWarning, "incorrect type for \"enable\" (want bool)", 3}},
},
{
config: "coreos:\n units:\n - enable:\n bad:",
entries: []Entry{{entryWarning, "incorrect type for \"enable\" (want bool)", 3}},
},
{
config: "coreos:\n units:\n - enable:\n - bad",
entries: []Entry{{entryWarning, "incorrect type for \"enable\" (want bool)", 3}},
},
// Want string
{
config: "hostname: true",
},
{
config: "hostname: 4",
},
{
config: "hostname: host",
},
{
config: "hostname:\n name:",
entries: []Entry{{entryWarning, "incorrect type for \"hostname\" (want string)", 1}},
},
{
config: "hostname:\n - name",
entries: []Entry{{entryWarning, "incorrect type for \"hostname\" (want string)", 1}},
},
// Want struct
{
config: "coreos: true",
entries: []Entry{{entryWarning, "incorrect type for \"coreos\" (want struct)", 1}},
},
{
config: "coreos: 4",
entries: []Entry{{entryWarning, "incorrect type for \"coreos\" (want struct)", 1}},
},
{
config: "coreos: hello",
entries: []Entry{{entryWarning, "incorrect type for \"coreos\" (want struct)", 1}},
},
{
config: "coreos:\n etcd:\n discovery: fire in the disco",
},
{
config: "coreos:\n - hello",
entries: []Entry{{entryWarning, "incorrect type for \"coreos\" (want struct)", 1}},
},
// Want []string
{
config: "ssh_authorized_keys: true",
entries: []Entry{{entryWarning, "incorrect type for \"ssh_authorized_keys\" (want []string)", 1}},
},
{
config: "ssh_authorized_keys: 4",
entries: []Entry{{entryWarning, "incorrect type for \"ssh_authorized_keys\" (want []string)", 1}},
},
{
config: "ssh_authorized_keys: key",
entries: []Entry{{entryWarning, "incorrect type for \"ssh_authorized_keys\" (want []string)", 1}},
},
{
config: "ssh_authorized_keys:\n key: value",
entries: []Entry{{entryWarning, "incorrect type for \"ssh_authorized_keys\" (want []string)", 1}},
},
{
config: "ssh_authorized_keys:\n - key",
},
{
config: "ssh_authorized_keys:\n - key: value",
entries: []Entry{{entryWarning, "incorrect type for \"ssh_authorized_keys[0]\" (want string)", 2}},
},
// Want []struct
{
config: "users:\n true",
entries: []Entry{{entryWarning, "incorrect type for \"users\" (want []struct)", 1}},
},
{
config: "users:\n 4",
entries: []Entry{{entryWarning, "incorrect type for \"users\" (want []struct)", 1}},
},
{
config: "users:\n bad",
entries: []Entry{{entryWarning, "incorrect type for \"users\" (want []struct)", 1}},
},
{
config: "users:\n bad:",
entries: []Entry{{entryWarning, "incorrect type for \"users\" (want []struct)", 1}},
},
{
config: "users:\n - name: good",
},
// Want struct within array
{
config: "users:\n - true",
entries: []Entry{{entryWarning, "incorrect type for \"users[0]\" (want struct)", 2}},
},
{
config: "users:\n - name: hi\n - true",
entries: []Entry{{entryWarning, "incorrect type for \"users[1]\" (want struct)", 3}},
},
{
config: "users:\n - 4",
entries: []Entry{{entryWarning, "incorrect type for \"users[0]\" (want struct)", 2}},
},
{
config: "users:\n - bad",
entries: []Entry{{entryWarning, "incorrect type for \"users[0]\" (want struct)", 2}},
},
{
config: "users:\n - - bad",
entries: []Entry{{entryWarning, "incorrect type for \"users[0]\" (want struct)", 2}},
},
}
for i, tt := range tests {
r := Report{}
n, err := parseCloudConfig([]byte(tt.config), &r)
if err != nil {
panic(err)
}
checkStructure(n, &r)
if e := r.Entries(); !reflect.DeepEqual(tt.entries, e) {
t.Errorf("bad report (%d, %q): want %#v, got %#v", i, tt.config, tt.entries, e)
}
}
}
func TestCheckValidity(t *testing.T) {
tests := []struct {
config string
entries []Entry
}{
// string
{
config: "hostname: test",
},
// int
{
config: "coreos:\n fleet:\n verbosity: 2",
},
// bool
{
config: "coreos:\n units:\n - enable: true",
},
// slice
{
config: "coreos:\n units:\n - command: start\n - name: stop",
},
{
config: "coreos:\n units:\n - command: lol",
entries: []Entry{{entryWarning, "invalid value lol", 3}},
},
// struct
{
config: "coreos:\n update:\n reboot_strategy: off",
},
{
config: "coreos:\n update:\n reboot_strategy: always",
entries: []Entry{{entryWarning, "invalid value always", 3}},
},
// unknown
{
config: "unknown: hi",
},
}
for i, tt := range tests {
r := Report{}
n, err := parseCloudConfig([]byte(tt.config), &r)
if err != nil {
panic(err)
}
checkValidity(n, &r)
if e := r.Entries(); !reflect.DeepEqual(tt.entries, e) {
t.Errorf("bad report (%d, %q): want %#v, got %#v", i, tt.config, tt.entries, e)
}
}
}

114
config/validate/validate.go Normal file
View File

@@ -0,0 +1,114 @@
/*
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 (
"errors"
"fmt"
"regexp"
"strconv"
"strings"
"github.com/coreos/coreos-cloudinit/config"
"github.com/coreos/coreos-cloudinit/Godeps/_workspace/src/gopkg.in/yaml.v1"
)
var (
yamlLineError = regexp.MustCompile(`^YAML error: line (?P<line>[[:digit:]]+): (?P<msg>.*)$`)
yamlError = regexp.MustCompile(`^YAML error: (?P<msg>.*)$`)
)
// Validate runs a series of validation tests against the given userdata and
// returns a report detailing all of the issues. Presently, only cloud-configs
// can be validated.
func Validate(userdataBytes []byte) (Report, error) {
switch {
case config.IsScript(string(userdataBytes)):
return Report{}, nil
case config.IsCloudConfig(string(userdataBytes)):
return validateCloudConfig(userdataBytes, Rules)
default:
return Report{entries: []Entry{
Entry{kind: entryError, message: `must be "#cloud-config" or begin with "#!"`, line: 1},
}}, nil
}
}
// validateCloudConfig runs all of the validation rules in Rules and returns
// the resulting report and any errors encountered.
func validateCloudConfig(config []byte, rules []rule) (report Report, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("%v", r)
}
}()
c, err := parseCloudConfig(config, &report)
if err != nil {
return report, err
}
c = normalizeNodeNames(c, &report)
for _, r := range rules {
r(c, &report)
}
return report, nil
}
// parseCloudConfig parses the provided config into a node structure and logs
// any parsing issues into the provided report. Unrecoverable errors are
// returned as an error.
func parseCloudConfig(config []byte, report *Report) (n node, err error) {
var raw map[interface{}]interface{}
if err := yaml.Unmarshal(config, &raw); err != nil {
matches := yamlLineError.FindStringSubmatch(err.Error())
if len(matches) == 3 {
line, err := strconv.Atoi(matches[1])
if err != nil {
return n, err
}
msg := matches[2]
report.Error(line, msg)
return n, nil
}
matches = yamlError.FindStringSubmatch(err.Error())
if len(matches) == 2 {
report.Error(1, matches[1])
return n, nil
}
return n, errors.New("couldn't parse yaml error")
}
return NewNode(raw, NewContext(config)), nil
}
// normalizeNodeNames replaces all occurences of '-' with '_' within key names
// and makes a note of each replacement in the report.
func normalizeNodeNames(node node, report *Report) node {
if strings.Contains(node.name, "-") {
// TODO(crawford): Enable this message once the new validator hits stable.
//report.Info(node.line, fmt.Sprintf("%q uses '-' instead of '_'", node.name))
node.name = strings.Replace(node.name, "-", "_", -1)
}
for i := range node.children {
node.children[i] = normalizeNodeNames(node.children[i], report)
}
return node
}

View File

@@ -0,0 +1,121 @@
/*
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 (
"errors"
"reflect"
"testing"
)
func TestParseCloudConfig(t *testing.T) {
tests := []struct {
config string
entries []Entry
}{
{},
{
config: " ",
entries: []Entry{{entryError, "found character that cannot start any token", 1}},
},
{
config: "a:\na",
entries: []Entry{{entryError, "could not find expected ':'", 2}},
},
{
config: "#hello\na:\na",
entries: []Entry{{entryError, "could not find expected ':'", 3}},
},
}
for _, tt := range tests {
r := Report{}
parseCloudConfig([]byte(tt.config), &r)
if e := r.Entries(); !reflect.DeepEqual(tt.entries, e) {
t.Errorf("bad report (%s): want %#v, got %#v", tt.config, tt.entries, e)
}
}
}
func TestValidateCloudConfig(t *testing.T) {
tests := []struct {
config string
rules []rule
report Report
err error
}{
{
rules: []rule{func(_ node, _ *Report) { panic("something happened") }},
err: errors.New("something happened"),
},
}
for _, tt := range tests {
r, err := validateCloudConfig([]byte(tt.config), tt.rules)
if !reflect.DeepEqual(tt.err, err) {
t.Errorf("bad error (%s): want %v, got %v", tt.config, tt.err, err)
}
if !reflect.DeepEqual(tt.report, r) {
t.Errorf("bad report (%s): want %+v, got %+v", tt.config, tt.report, r)
}
}
}
func BenchmarkValidate(b *testing.B) {
config := `#cloud-config
hostname: test
coreos:
etcd:
name: node001
discovery: https://discovery.etcd.io/disco
addr: $public_ipv4:4001
peer-addr: $private_ipv4:7001
fleet:
verbosity: 2
metadata: "hi"
update:
reboot-strategy: off
units:
- name: hi.service
command: start
enable: true
- name: bye.service
command: stop
ssh_authorized_keys:
- ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC0g+ZTxC7weoIJLUafOgrm+h...
- ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC0g+ZTxC7weoIJLUafOgrm+h...
users:
- name: me
write_files:
- path: /etc/yes
content: "Hi"
manage_etc_hosts: localhost`
for i := 0; i < b.N; i++ {
if _, err := Validate([]byte(config)); err != nil {
panic(err)
}
}
}

View File

@@ -24,6 +24,7 @@ import (
"time" "time"
"github.com/coreos/coreos-cloudinit/config" "github.com/coreos/coreos-cloudinit/config"
"github.com/coreos/coreos-cloudinit/config/validate"
"github.com/coreos/coreos-cloudinit/datasource" "github.com/coreos/coreos-cloudinit/datasource"
"github.com/coreos/coreos-cloudinit/datasource/configdrive" "github.com/coreos/coreos-cloudinit/datasource/configdrive"
"github.com/coreos/coreos-cloudinit/datasource/file" "github.com/coreos/coreos-cloudinit/datasource/file"
@@ -39,7 +40,7 @@ import (
) )
const ( const (
version = "0.10.4+git" version = "0.11.1"
datasourceInterval = 100 * time.Millisecond datasourceInterval = 100 * time.Millisecond
datasourceMaxInterval = 30 * time.Second datasourceMaxInterval = 30 * time.Second
datasourceTimeout = 5 * time.Minute datasourceTimeout = 5 * time.Minute
@@ -64,6 +65,7 @@ var (
workspace string workspace string
sshKeyName string sshKeyName string
oem string oem string
validate bool
}{} }{}
) )
@@ -83,6 +85,7 @@ func init() {
flag.StringVar(&flags.convertNetconf, "convert-netconf", "", "Read the network config provided in cloud-drive and translate it from the specified format into networkd unit files") flag.StringVar(&flags.convertNetconf, "convert-netconf", "", "Read the network config provided in cloud-drive and translate it from the specified format into networkd unit files")
flag.StringVar(&flags.workspace, "workspace", "/var/lib/coreos-cloudinit", "Base directory coreos-cloudinit should use to store data") flag.StringVar(&flags.workspace, "workspace", "/var/lib/coreos-cloudinit", "Base directory coreos-cloudinit should use to store data")
flag.StringVar(&flags.sshKeyName, "ssh-key-name", initialize.DefaultSSHKeyName, "Add SSH keys to the system with the given name") flag.StringVar(&flags.sshKeyName, "ssh-key-name", initialize.DefaultSSHKeyName, "Add SSH keys to the system with the given name")
flag.BoolVar(&flags.validate, "validate", false, "[EXPERIMENTAL] Validate the user-data but do not apply it to the system")
} }
type oemConfig map[string]string type oemConfig map[string]string
@@ -158,6 +161,22 @@ func main() {
failure = true failure = true
} }
if report, err := validate.Validate(userdataBytes); err == nil {
ret := 0
for _, e := range report.Entries() {
fmt.Println(e)
ret = 1
}
if flags.validate {
os.Exit(ret)
}
} else {
fmt.Printf("Failed while validating user_data (%q)\n", err)
if flags.validate {
os.Exit(1)
}
}
fmt.Printf("Fetching meta-data from datasource of type %q\n", ds.Type()) fmt.Printf("Fetching meta-data from datasource of type %q\n", ds.Type())
metadataBytes, err := ds.FetchMetadata() metadataBytes, err := ds.FetchMetadata()
if err != nil { if err != nil {
@@ -180,7 +199,7 @@ func main() {
userdata := env.Apply(string(userdataBytes)) userdata := env.Apply(string(userdataBytes))
var ccm, ccu *config.CloudConfig var ccm, ccu *config.CloudConfig
var script *system.Script var script *config.Script
if ccm, err = initialize.ParseMetaData(string(metadataBytes)); err != nil { if ccm, err = initialize.ParseMetaData(string(metadataBytes)); err != nil {
fmt.Printf("Failed to parse meta-data: %v\n", err) fmt.Printf("Failed to parse meta-data: %v\n", err)
os.Exit(1) os.Exit(1)
@@ -203,7 +222,7 @@ func main() {
switch t := ud.(type) { switch t := ud.(type) {
case *config.CloudConfig: case *config.CloudConfig:
ccu = t ccu = t
case system.Script: case config.Script:
script = &t script = &t
} }
} }
@@ -362,7 +381,7 @@ func selectDatasource(sources []datasource.Datasource) datasource.Datasource {
} }
// TODO(jonboulle): this should probably be refactored and moved into a different module // TODO(jonboulle): this should probably be refactored and moved into a different module
func runScript(script system.Script, env *initialize.Environment) error { func runScript(script config.Script, env *initialize.Environment) error {
err := initialize.PrepWorkspace(env.Workspace()) err := initialize.PrepWorkspace(env.Workspace())
if err != nil { if err != nil {
fmt.Printf("Failed preparing workspace: %v\n", err) fmt.Printf("Failed preparing workspace: %v\n", err)

View File

@@ -131,6 +131,7 @@ func Apply(cfg config.CloudConfig, env *Environment) error {
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.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

@@ -17,32 +17,25 @@
package initialize package initialize
import ( import (
"fmt" "errors"
"log" "log"
"strings"
"github.com/coreos/coreos-cloudinit/config" "github.com/coreos/coreos-cloudinit/config"
"github.com/coreos/coreos-cloudinit/system"
) )
func ParseUserData(contents string) (interface{}, error) { func ParseUserData(contents string) (interface{}, error) {
if len(contents) == 0 { if len(contents) == 0 {
return nil, nil return nil, nil
} }
header := strings.SplitN(contents, "\n", 2)[0]
// Explicitly trim the header so we can handle user-data from switch {
// non-unix operating systems. The rest of the file is parsed case config.IsScript(contents):
// by yaml, which correctly handles CRLF.
header = strings.TrimSpace(header)
if strings.HasPrefix(header, "#!") {
log.Printf("Parsing user-data as script") log.Printf("Parsing user-data as script")
return system.Script(contents), nil return config.NewScript(contents)
} else if header == "#cloud-config" { case config.IsCloudConfig(contents):
log.Printf("Parsing user-data as cloud-config") log.Printf("Parsing user-data as cloud-config")
return config.NewCloudConfig(contents) return config.NewCloudConfig(contents)
} else { default:
return nil, fmt.Errorf("Unrecognized user-data header: %s", header) return nil, errors.New("Unrecognized user-data format")
} }
} }

View File

@@ -38,7 +38,7 @@ func PrepWorkspace(workspace string) error {
return nil return nil
} }
func PersistScriptInWorkspace(script system.Script, workspace string) (string, error) { func PersistScriptInWorkspace(script config.Script, workspace string) (string, error) {
scriptsPath := path.Join(workspace, "scripts") scriptsPath := path.Join(workspace, "scripts")
tmp, err := ioutil.TempFile(scriptsPath, "") tmp, err := ioutil.TempFile(scriptsPath, "")
if err != nil { if err != nil {

View File

@@ -19,6 +19,8 @@ package system
import ( import (
"fmt" "fmt"
"reflect" "reflect"
"github.com/coreos/coreos-cloudinit/config"
) )
// dropinContents generates the contents for a drop-in unit given the config. // dropinContents generates the contents for a drop-in unit given the config.
@@ -40,3 +42,16 @@ func dropinContents(e interface{}) string {
} }
return "[Service]\n" + out return "[Service]\n" + out
} }
func dropinFromConfig(cfg interface{}, name string) []Unit {
content := dropinContents(cfg)
if content == "" {
return nil
}
return []Unit{{config.Unit{
Name: name,
Runtime: true,
DropIn: true,
Content: content,
}}}
}

View File

@@ -28,14 +28,5 @@ type Etcd struct {
// Units creates a Unit file drop-in for etcd, using any configured options. // Units creates a Unit file drop-in for etcd, using any configured options.
func (ee Etcd) Units() []Unit { func (ee Etcd) Units() []Unit {
content := dropinContents(ee.Etcd) return dropinFromConfig(ee.Etcd, "etcd.service")
if content == "" {
return nil
}
return []Unit{{config.Unit{
Name: "etcd.service",
Runtime: true,
DropIn: true,
Content: content,
}}}
} }

17
system/flannel.go Normal file
View File

@@ -0,0 +1,17 @@
package system
import (
"github.com/coreos/coreos-cloudinit/config"
)
// flannel is a top-level structure which embeds its underlying configuration,
// config.Flannel, and provides the system-specific Unit().
type Flannel struct {
config.Flannel
}
// Units generates a Unit file drop-in for flannel, if any flannel options were
// configured in cloud-config
func (fl Flannel) Units() []Unit {
return dropinFromConfig(fl.Flannel, "flannel.service")
}

40
system/flannel_test.go Normal file
View File

@@ -0,0 +1,40 @@
package system
import (
"reflect"
"testing"
"github.com/coreos/coreos-cloudinit/config"
)
func TestFlannelUnits(t *testing.T) {
for _, tt := range []struct {
config config.Flannel
units []Unit
}{
{
config.Flannel{},
nil,
},
{
config.Flannel{
EtcdEndpoint: "http://12.34.56.78:4001",
EtcdPrefix: "/coreos.com/network/tenant1",
},
[]Unit{{config.Unit{
Name: "flannel.service",
Content: `[Service]
Environment="FLANNELD_ETCD_ENDPOINT=http://12.34.56.78:4001"
Environment="FLANNELD_ETCD_PREFIX=/coreos.com/network/tenant1"
`,
Runtime: true,
DropIn: true,
}}},
},
} {
units := Flannel{tt.config}.Units()
if !reflect.DeepEqual(units, tt.units) {
t.Errorf("bad units (%q): want %v, got %v", tt.config, tt.units, units)
}
}
}

View File

@@ -29,14 +29,5 @@ type Fleet struct {
// Units generates a Unit file drop-in for fleet, if any fleet options were // Units generates a Unit file drop-in for fleet, if any fleet options were
// configured in cloud-config // configured in cloud-config
func (fe Fleet) Units() []Unit { func (fe Fleet) Units() []Unit {
content := dropinContents(fe.Fleet) return dropinFromConfig(fe.Fleet, "fleet.service")
if content == "" {
return nil
}
return []Unit{{config.Unit{
Name: "fleet.service",
Runtime: true,
DropIn: true,
Content: content,
}}}
} }

View File

@@ -41,8 +41,6 @@ type Unit struct {
config.Unit config.Unit
} }
type Script []byte
// Destination builds the appropriate absolute file path for // Destination builds the appropriate absolute file path for
// the Unit. The root argument indicates the effective base // the Unit. The root argument indicates the effective base
// directory of the system (similar to a chroot). // directory of the system (similar to a chroot).

View File

@@ -61,7 +61,7 @@ func (uc Update) File() (*File, error) {
if config.IsZero(uc.Update) { if config.IsZero(uc.Update) {
return nil, nil return nil, nil
} }
if err := config.AssertValid(uc.Update); err != nil { if err := config.AssertStructValid(uc.Update); err != nil {
return nil, err return nil, err
} }
@@ -126,7 +126,7 @@ func (uc Update) Units() []Unit {
Runtime: true, Runtime: true,
}} }}
if uc.Update.RebootStrategy == "off" { if uc.Update.RebootStrategy == "false" {
ls.Command = "stop" ls.Command = "stop"
ls.Mask = true ls.Mask = true
} }

View File

@@ -17,7 +17,6 @@
package system package system
import ( import (
"errors"
"io" "io"
"reflect" "reflect"
"strings" "strings"
@@ -73,7 +72,7 @@ func TestUpdateUnits(t *testing.T) {
}}}, }}},
}, },
{ {
config: config.Update{RebootStrategy: "off"}, config: config.Update{RebootStrategy: "false"},
units: []Unit{{config.Unit{ units: []Unit{{config.Unit{
Name: "locksmithd.service", Name: "locksmithd.service",
Command: "stop", Command: "stop",
@@ -101,7 +100,7 @@ func TestUpdateFile(t *testing.T) {
}, },
{ {
config: config.Update{RebootStrategy: "wizzlewazzle"}, config: config.Update{RebootStrategy: "wizzlewazzle"},
err: errors.New("invalid value \"wizzlewazzle\" for option \"RebootStrategy\" (valid options: \"best-effort,etcd-lock,reboot,off\")"), err: &config.ErrorValid{Value: "wizzlewazzle", Field: "RebootStrategy", Valid: []string{"best-effort", "etcd-lock", "reboot", "false"}},
}, },
{ {
config: config.Update{Group: "master", Server: "http://foo.com"}, config: config.Update{Group: "master", Server: "http://foo.com"},
@@ -136,9 +135,9 @@ func TestUpdateFile(t *testing.T) {
}}, }},
}, },
{ {
config: config.Update{RebootStrategy: "off"}, config: config.Update{RebootStrategy: "false"},
file: &File{config.File{ file: &File{config.File{
Content: "REBOOT_STRATEGY=off\n", Content: "REBOOT_STRATEGY=false\n",
Path: "etc/coreos/update.conf", Path: "etc/coreos/update.conf",
RawFilePermissions: "0644", RawFilePermissions: "0644",
}}, }},

1
test
View File

@@ -15,6 +15,7 @@ source ./build
declare -a TESTPKGS=( declare -a TESTPKGS=(
config config
config/validate
datasource datasource
datasource/configdrive datasource/configdrive
datasource/file datasource/file