Compare commits
45 Commits
0.10-relea
...
v1.0.0
Author | SHA1 | Date | |
---|---|---|---|
|
521ecfdab5 | ||
|
6d0fdf1a47 | ||
|
ffc54b028c | ||
|
420f7cf202 | ||
|
624df676d0 | ||
|
75ed8dacf9 | ||
|
dcaabe4d4a | ||
|
92c57423ba | ||
|
7447e133c9 | ||
|
4e466c12da | ||
|
333468dba3 | ||
|
55c3a793ad | ||
|
eca51031c8 | ||
|
19522bcb82 | ||
|
62248ea33d | ||
|
d2a19cc86d | ||
|
08131ffab1 | ||
|
4a0019c669 | ||
|
3275ead1ec | ||
|
32b6a55724 | ||
|
6c43644369 | ||
|
e6593d49e6 | ||
|
ab752b239f | ||
|
0742e4d357 | ||
|
78f586ec9e | ||
|
6f91b76d79 | ||
|
5c80ccacc4 | ||
|
97758b343b | ||
|
fb6f52b360 | ||
|
786cd2a539 | ||
|
45793f1254 | ||
|
b621756d92 | ||
|
a5b5c700a6 | ||
|
d7602f3c08 | ||
|
a20addd05e | ||
|
d9d89a6fa0 | ||
|
3c26376326 | ||
|
d3294bcb86 | ||
|
dda314b518 | ||
|
055a3c339a | ||
|
51f37100a1 | ||
|
88e8265cd6 | ||
|
6e2db882e6 | ||
|
3e2823df1b | ||
|
d02aa18839 |
@@ -16,7 +16,7 @@ We've designed our implementation to allow the same cloud-config file to work ac
|
||||
|
||||
The cloud-config file uses the [YAML][yaml] file format, which uses whitespace and new-lines to delimit lists, associative arrays, and values.
|
||||
|
||||
A cloud-config file should contain `#cloud-config`, followed by an associative array which has zero or more of the following keys:
|
||||
A cloud-config file must contain `#cloud-config`, followed by an associative array which has zero or more of the following keys:
|
||||
|
||||
- `coreos`
|
||||
- `ssh_authorized_keys`
|
||||
@@ -46,13 +46,13 @@ If the platform environment supports the templating feature of coreos-cloudinit
|
||||
#cloud-config
|
||||
|
||||
coreos:
|
||||
etcd:
|
||||
name: node001
|
||||
# generate a new token for each unique cluster from https://discovery.etcd.io/new
|
||||
discovery: https://discovery.etcd.io/<token>
|
||||
# multi-region and multi-cloud deployments need to use $public_ipv4
|
||||
addr: $public_ipv4:4001
|
||||
peer-addr: $private_ipv4:7001
|
||||
etcd:
|
||||
name: node001
|
||||
# generate a new token for each unique cluster from https://discovery.etcd.io/new
|
||||
discovery: https://discovery.etcd.io/<token>
|
||||
# multi-region and multi-cloud deployments need to use $public_ipv4
|
||||
addr: $public_ipv4:4001
|
||||
peer-addr: $private_ipv4:7001
|
||||
```
|
||||
|
||||
...will generate a systemd unit drop-in like this:
|
||||
@@ -66,7 +66,6 @@ Environment="ETCD_PEER_ADDR=192.0.2.13:7001"
|
||||
```
|
||||
|
||||
For more information about the available configuration parameters, see the [etcd documentation][etcd-config].
|
||||
Note that hyphens in the coreos.etcd.* keys are mapped to underscores.
|
||||
|
||||
_Note: The `$private_ipv4` and `$public_ipv4` substitution variables referenced in other documents are only supported on Amazon EC2, Google Compute Engine, OpenStack, Rackspace, DigitalOcean, and Vagrant._
|
||||
|
||||
@@ -80,9 +79,9 @@ The `coreos.fleet.*` parameters work very similarly to `coreos.etcd.*`, and allo
|
||||
#cloud-config
|
||||
|
||||
coreos:
|
||||
fleet:
|
||||
public-ip: $public_ipv4
|
||||
metadata: region=us-west
|
||||
fleet:
|
||||
public-ip: $public_ipv4
|
||||
metadata: region=us-west
|
||||
```
|
||||
|
||||
...will generate a systemd unit drop-in like this:
|
||||
@@ -97,6 +96,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.
|
||||
@@ -135,6 +157,10 @@ Each item is an object with the following fields:
|
||||
- **content**: Plaintext string representing entire unit file. If no value is provided, the unit is assumed to exist already.
|
||||
- **command**: Command to execute on unit: start, stop, reload, restart, try-restart, reload-or-restart, reload-or-try-restart. The default behavior is to not execute any commands.
|
||||
- **mask**: Whether to mask the unit file by symlinking it to `/dev/null` (analogous to `systemctl mask <name>`). Note that unlike `systemctl mask`, **this will destructively remove any existing unit file** located at `/etc/systemd/system/<unit>`, to ensure that the mask succeeds. The default value is false.
|
||||
- **drop-ins**: A list of unit drop-ins with the following fields:
|
||||
- **name**: String representing unit's name. Required.
|
||||
- **content**: Plaintext string representing entire file. Required.
|
||||
|
||||
|
||||
**NOTE:** The command field is ignored for all network, netdev, and link units. The systemd-networkd.service unit will be restarted in their place.
|
||||
|
||||
@@ -146,19 +172,34 @@ Write a unit to disk, automatically starting it.
|
||||
#cloud-config
|
||||
|
||||
coreos:
|
||||
units:
|
||||
- name: docker-redis.service
|
||||
command: start
|
||||
content: |
|
||||
[Unit]
|
||||
Description=Redis container
|
||||
Author=Me
|
||||
After=docker.service
|
||||
units:
|
||||
- name: docker-redis.service
|
||||
command: start
|
||||
content: |
|
||||
[Unit]
|
||||
Description=Redis container
|
||||
Author=Me
|
||||
After=docker.service
|
||||
|
||||
[Service]
|
||||
Restart=always
|
||||
ExecStart=/usr/bin/docker start -a redis_server
|
||||
ExecStop=/usr/bin/docker stop -t 2 redis_server
|
||||
[Service]
|
||||
Restart=always
|
||||
ExecStart=/usr/bin/docker start -a redis_server
|
||||
ExecStop=/usr/bin/docker stop -t 2 redis_server
|
||||
```
|
||||
|
||||
Add the DOCKER_OPTS environment variable to docker.service.
|
||||
|
||||
```yaml
|
||||
#cloud-config
|
||||
|
||||
coreos:
|
||||
units:
|
||||
- name: docker.service
|
||||
drop-ins:
|
||||
- name: 50-insecure-registry.conf
|
||||
content: |
|
||||
[Service]
|
||||
Environment=DOCKER_OPTS='--insecure-registry="10.0.1.0/24"'
|
||||
```
|
||||
|
||||
Start the built-in `etcd` and `fleet` services:
|
||||
@@ -167,11 +208,11 @@ Start the built-in `etcd` and `fleet` services:
|
||||
#cloud-config
|
||||
|
||||
coreos:
|
||||
units:
|
||||
- name: etcd.service
|
||||
command: start
|
||||
- name: fleet.service
|
||||
command: start
|
||||
units:
|
||||
- name: etcd.service
|
||||
command: start
|
||||
- name: fleet.service
|
||||
command: start
|
||||
```
|
||||
|
||||
### ssh_authorized_keys
|
||||
@@ -303,7 +344,7 @@ Each item in the list may have the following keys:
|
||||
|
||||
- **path**: Absolute location on disk where contents should be written
|
||||
- **content**: Data to write at the provided `path`
|
||||
- **permissions**: String representing file permissions in octal notation (i.e. '0644')
|
||||
- **permissions**: Integer representing file permissions, typically in octal notation (i.e. 0644)
|
||||
- **owner**: User and group that should own the file written to disk. This is equivalent to the `<user>:<group>` argument to `chown <user>:<group> <path>`.
|
||||
|
||||
Explicitly not implemented is the **encoding** attribute.
|
||||
|
174
config/config.go
174
config/config.go
@@ -18,7 +18,6 @@ package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
@@ -31,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"`
|
||||
@@ -45,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
|
||||
}
|
||||
|
||||
@@ -76,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++ {
|
||||
@@ -88,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:
|
||||
@@ -116,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
|
||||
}
|
||||
|
@@ -17,8 +17,6 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -43,7 +41,7 @@ func TestIsZero(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAssertValid(t *testing.T) {
|
||||
func TestAssertStructValid(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
c interface{}
|
||||
err error
|
||||
@@ -60,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},
|
||||
@@ -72,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)
|
||||
}
|
||||
}
|
||||
@@ -147,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"
|
||||
@@ -200,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
|
||||
@@ -217,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
|
||||
@@ -286,9 +261,6 @@ Address=10.209.171.177/19
|
||||
if u.Name != "50-eth0.network" {
|
||||
t.Errorf("Unit has incorrect name %s", u.Name)
|
||||
}
|
||||
if u.Type() != "network" {
|
||||
t.Errorf("Unit has incorrect type '%s'", u.Type())
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.Coreos.OEM.ID != "rackspace" {
|
||||
@@ -301,6 +273,40 @@ Address=10.209.171.177/19
|
||||
if cfg.Coreos.Update.RebootStrategy != "reboot" {
|
||||
t.Errorf("Failed to parse locksmith strategy")
|
||||
}
|
||||
|
||||
contents = `
|
||||
coreos:
|
||||
write_files:
|
||||
- path: /home/me/notes
|
||||
permissions: 0744
|
||||
`
|
||||
cfg, err = NewCloudConfig(contents)
|
||||
if err != nil {
|
||||
t.Fatalf("Encountered unexpected error :%v", err)
|
||||
}
|
||||
|
||||
if len(cfg.WriteFiles) != 1 {
|
||||
t.Error("Failed to parse correct number of write_files")
|
||||
} else {
|
||||
wf := cfg.WriteFiles[0]
|
||||
if wf.Content != "" {
|
||||
t.Errorf("WriteFile has incorrect contents '%s'", wf.Content)
|
||||
}
|
||||
if wf.Encoding != "" {
|
||||
t.Errorf("WriteFile has incorrect encoding %s", wf.Encoding)
|
||||
}
|
||||
// Verify that the normalization of the config converted 0744 to its decimal
|
||||
// representation, 484.
|
||||
if wf.RawFilePermissions != "484" {
|
||||
t.Errorf("WriteFile has incorrect permissions %s", wf.RawFilePermissions)
|
||||
}
|
||||
if wf.Path != "/home/me/notes" {
|
||||
t.Errorf("WriteFile has incorrect path %s", wf.Path)
|
||||
}
|
||||
if wf.Owner != "" {
|
||||
t.Errorf("WriteFile has incorrect owner %s", wf.Owner)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Assert that our interface conversion doesn't panic
|
||||
@@ -329,43 +335,23 @@ func TestCloudConfigSerializationHeader(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestDropInIgnored asserts that users are unable to set DropIn=True on units
|
||||
func TestDropInIgnored(t *testing.T) {
|
||||
contents := `
|
||||
coreos:
|
||||
units:
|
||||
- name: test
|
||||
dropin: true
|
||||
`
|
||||
cfg, err := NewCloudConfig(contents)
|
||||
if err != nil || len(cfg.Coreos.Units) != 1 {
|
||||
t.Fatalf("Encountered unexpected error: %v", err)
|
||||
}
|
||||
if len(cfg.Coreos.Units) != 1 || cfg.Coreos.Units[0].Name != "test" {
|
||||
t.Fatalf("Expected 1 unit, but got %d: %v", len(cfg.Coreos.Units), cfg.Coreos.Units)
|
||||
}
|
||||
if cfg.Coreos.Units[0].DropIn {
|
||||
t.Errorf("dropin option on unit in cloud-config was not ignored!")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloudConfigUsers(t *testing.T) {
|
||||
contents := `
|
||||
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 {
|
||||
@@ -404,11 +390,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 {
|
||||
@@ -423,7 +409,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 {
|
||||
@@ -431,7 +417,7 @@ users:
|
||||
}
|
||||
|
||||
if !user.NoLogInit {
|
||||
t.Errorf("Failed to parse no-log-init field")
|
||||
t.Errorf("Failed to parse no_log_init field")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -440,7 +426,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 {
|
||||
@@ -466,7 +452,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 {
|
||||
@@ -487,3 +473,31 @@ 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"},
|
||||
{"coreos:\n update:\n reboot-strategy: 'off'\n", "coreos:\n update:\n reboot_strategy: \"off\"\n"},
|
||||
} {
|
||||
out, err := normalizeConfig(tt.in)
|
||||
if err != nil {
|
||||
t.Fatalf("bad error (%q): want nil, got %s", tt.in, err)
|
||||
}
|
||||
if string(out) != tt.out {
|
||||
t.Fatalf("bad normalization (%q): want %q, got %q", tt.in, tt.out, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -17,32 +17,37 @@
|
||||
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"`
|
||||
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"`
|
||||
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"`
|
||||
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"`
|
||||
Peers string `yaml:"peers" env:"ETCD_PEERS"`
|
||||
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"`
|
||||
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 int `yaml:"cluster_active_size" env:"ETCD_CLUSTER_ACTIVE_SIZE"`
|
||||
ClusterRemoveDelay float64 `yaml:"cluster_remove_delay" env:"ETCD_CLUSTER_REMOVE_DELAY"`
|
||||
ClusterSyncInterval float64 `yaml:"cluster_sync_interval" env:"ETCD_CLUSTER_SYNC_INTERVAL"`
|
||||
CorsOrigins string `yaml:"cors" env:"ETCD_CORS"`
|
||||
DataDir string `yaml:"data_dir" env:"ETCD_DATA_DIR"`
|
||||
Discovery string `yaml:"discovery" env:"ETCD_DISCOVERY"`
|
||||
GraphiteHost string `yaml:"graphite_host" env:"ETCD_GRAPHITE_HOST"`
|
||||
HTTPReadTimeout float64 `yaml:"http_read_timeout" env:"ETCD_HTTP_READ_TIMEOUT"`
|
||||
HTTPWriteTimeout float64 `yaml:"http_write_timeout" env:"ETCD_HTTP_WRITE_TIMEOUT"`
|
||||
KeyFile string `yaml:"key_file" env:"ETCD_KEY_FILE"`
|
||||
MaxResultBuffer int `yaml:"max_result_buffer" env:"ETCD_MAX_RESULT_BUFFER"`
|
||||
MaxRetryAttempts int `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"`
|
||||
PeerElectionTimeout int `yaml:"peer_election_timeout" env:"ETCD_PEER_ELECTION_TIMEOUT"`
|
||||
PeerHeartbeatInterval int `yaml:"peer_heartbeat_interval" env:"ETCD_PEER_HEARTBEAT_INTERVAL"`
|
||||
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"`
|
||||
RetryInterval float64 `yaml:"retry_interval" env:"ETCD_RETRY_INTERVAL"`
|
||||
Snapshot bool `yaml:"snapshot" env:"ETCD_SNAPSHOT"`
|
||||
SnapshotCount int `yaml:"snapshot_count" env:"ETCD_SNAPSHOTCOUNT"`
|
||||
StrTrace string `yaml:"trace" env:"ETCD_TRACE"`
|
||||
Verbose bool `yaml:"verbose" env:"ETCD_VERBOSE"`
|
||||
VeryVerbose bool `yaml:"very_verbose" env:"ETCD_VERY_VERBOSE"`
|
||||
VeryVeryVerbose bool `yaml:"very_very_verbose" env:"ETCD_VERY_VERY_VERBOSE"`
|
||||
}
|
||||
|
9
config/flannel.go
Normal file
9
config/flannel.go
Normal 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"`
|
||||
}
|
@@ -17,14 +17,15 @@
|
||||
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"`
|
||||
Metadata string `yaml:"metadata" env:"FLEET_METADATA"`
|
||||
PublicIP string `yaml:"public-ip" env:"FLEET_PUBLIC_IP"`
|
||||
Verbosity string `yaml:"verbosity" env:"FLEET_VERBOSITY"`
|
||||
AgentTTL string `yaml:"agent_ttl" env:"FLEET_AGENT_TTL"`
|
||||
EngineReconcileInterval float64 `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"`
|
||||
EtcdKeyPrefix string `yaml:"etcd_key_prefix" env:"FLEET_ETCD_KEY_PREFIX"`
|
||||
EtcdRequestTimeout float64 `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"`
|
||||
Verbosity int `yaml:"verbosity" env:"FLEET_VERBOSITY"`
|
||||
}
|
||||
|
@@ -19,7 +19,7 @@ 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
32
config/script.go
Normal 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
|
||||
}
|
@@ -16,35 +16,17 @@
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Unit struct {
|
||||
Name string `yaml:"name"`
|
||||
Mask bool `yaml:"mask"`
|
||||
Enable bool `yaml:"enable"`
|
||||
Runtime bool `yaml:"runtime"`
|
||||
Content string `yaml:"content"`
|
||||
Command string `yaml:"command" valid:"start,stop,restart,reload,try-restart,reload-or-restart,reload-or-try-restart"`
|
||||
DropIns []UnitDropIn `yaml:"drop_ins"`
|
||||
}
|
||||
|
||||
type UnitDropIn struct {
|
||||
Name string `yaml:"name"`
|
||||
Mask bool `yaml:"mask"`
|
||||
Enable bool `yaml:"enable"`
|
||||
Runtime bool `yaml:"runtime"`
|
||||
Content string `yaml:"content"`
|
||||
Command string `yaml:"command"`
|
||||
|
||||
// For drop-in units, a cloudinit.conf is generated.
|
||||
// This is currently unbound in YAML (and hence unsettable in cloud-config files)
|
||||
// until the correct behaviour for multiple drop-in units is determined.
|
||||
DropIn bool `yaml:"-"`
|
||||
}
|
||||
|
||||
func (u *Unit) Type() string {
|
||||
ext := filepath.Ext(u.Name)
|
||||
return strings.TrimLeft(ext, ".")
|
||||
}
|
||||
|
||||
func (u *Unit) Group() string {
|
||||
switch u.Type() {
|
||||
case "network", "netdev", "link":
|
||||
return "network"
|
||||
default:
|
||||
return "system"
|
||||
}
|
||||
}
|
||||
|
@@ -17,7 +17,7 @@
|
||||
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,off,false"`
|
||||
Group string `yaml:"group" env:"GROUP"`
|
||||
Server string `yaml:"server" env:"SERVER"`
|
||||
}
|
||||
|
@@ -19,15 +19,15 @@ 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"`
|
||||
}
|
||||
|
54
config/validate/context.go
Normal file
54
config/validate/context.go
Normal 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
|
||||
}
|
133
config/validate/context_test.go
Normal file
133
config/validate/context_test.go
Normal 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
159
config/validate/node.go
Normal 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, reflect.Float64:
|
||||
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
|
||||
}
|
286
config/validate/node_test.go
Normal file
286
config/validate/node_test.go
Normal 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
90
config/validate/report.go
Normal 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))
|
||||
}
|
||||
}
|
98
config/validate/report_test.go
Normal file
98
config/validate/report_test.go
Normal 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
115
config/validate/rules.go
Normal 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.Float64, 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.Error(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.Float64, 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.Float64 || n == reflect.Bool
|
||||
case reflect.Struct:
|
||||
return n == reflect.Struct || n == reflect.Map
|
||||
case reflect.Bool, reflect.Slice, reflect.Int, reflect.Float64:
|
||||
return n == g
|
||||
default:
|
||||
panic(fmt.Sprintf("isCompatible(): unhandled kind %s", g))
|
||||
}
|
||||
}
|
251
config/validate/rules_test.go
Normal file
251
config/validate/rules_test.go
Normal 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{{entryError, "invalid value lol", 3}},
|
||||
},
|
||||
|
||||
// struct
|
||||
{
|
||||
config: "coreos:\n update:\n reboot_strategy: off",
|
||||
},
|
||||
{
|
||||
config: "coreos:\n update:\n reboot_strategy: always",
|
||||
entries: []Entry{{entryError, "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
114
config/validate/validate.go
Normal 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
|
||||
}
|
121
config/validate/validate_test.go
Normal file
121
config/validate/validate_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
@@ -24,6 +24,7 @@ import (
|
||||
"time"
|
||||
|
||||
"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/configdrive"
|
||||
"github.com/coreos/coreos-cloudinit/datasource/file"
|
||||
@@ -39,7 +40,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
version = "0.10.4+git"
|
||||
version = "1.0.0"
|
||||
datasourceInterval = 100 * time.Millisecond
|
||||
datasourceMaxInterval = 30 * time.Second
|
||||
datasourceTimeout = 5 * time.Minute
|
||||
@@ -64,6 +65,7 @@ var (
|
||||
workspace string
|
||||
sshKeyName 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.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.BoolVar(&flags.validate, "validate", false, "[EXPERIMENTAL] Validate the user-data but do not apply it to the system")
|
||||
}
|
||||
|
||||
type oemConfig map[string]string
|
||||
@@ -158,6 +161,22 @@ func main() {
|
||||
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())
|
||||
metadataBytes, err := ds.FetchMetadata()
|
||||
if err != nil {
|
||||
@@ -180,7 +199,7 @@ func main() {
|
||||
userdata := env.Apply(string(userdataBytes))
|
||||
|
||||
var ccm, ccu *config.CloudConfig
|
||||
var script *system.Script
|
||||
var script *config.Script
|
||||
if ccm, err = initialize.ParseMetaData(string(metadataBytes)); err != nil {
|
||||
fmt.Printf("Failed to parse meta-data: %v\n", err)
|
||||
os.Exit(1)
|
||||
@@ -203,7 +222,7 @@ func main() {
|
||||
switch t := ud.(type) {
|
||||
case *config.CloudConfig:
|
||||
ccu = t
|
||||
case system.Script:
|
||||
case config.Script:
|
||||
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
|
||||
func runScript(script system.Script, env *initialize.Environment) error {
|
||||
func runScript(script config.Script, env *initialize.Environment) error {
|
||||
err := initialize.PrepWorkspace(env.Workspace())
|
||||
if err != nil {
|
||||
fmt.Printf("Failed preparing workspace: %v\n", err)
|
||||
|
@@ -131,6 +131,7 @@ func Apply(cfg config.CloudConfig, env *Environment) error {
|
||||
for _, ccu := range []CloudConfigUnit{
|
||||
system.Etcd{Etcd: cfg.Coreos.Etcd},
|
||||
system.Fleet{Fleet: cfg.Coreos.Fleet},
|
||||
system.Flannel{Flannel: cfg.Coreos.Flannel},
|
||||
system.Update{Update: cfg.Coreos.Update, ReadConfig: system.DefaultReadConfig},
|
||||
} {
|
||||
units = append(units, ccu.Units()...)
|
||||
@@ -194,66 +195,82 @@ func Apply(cfg config.CloudConfig, env *Environment) error {
|
||||
// commands against units. It returns any error encountered.
|
||||
func processUnits(units []system.Unit, root string, um system.UnitManager) error {
|
||||
type action struct {
|
||||
unit string
|
||||
unit system.Unit
|
||||
command string
|
||||
}
|
||||
actions := make([]action, 0, len(units))
|
||||
reload := false
|
||||
for _, unit := range units {
|
||||
dst := unit.Destination(root)
|
||||
if unit.Name == "" {
|
||||
log.Printf("Skipping unit without name")
|
||||
continue
|
||||
}
|
||||
|
||||
if unit.Content != "" {
|
||||
log.Printf("Writing unit %s to filesystem at path %s", unit.Name, dst)
|
||||
if err := um.PlaceUnit(&unit, dst); err != nil {
|
||||
log.Printf("Writing unit %q to filesystem", unit.Name)
|
||||
if err := um.PlaceUnit(unit); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("Placed unit %s at %s", unit.Name, dst)
|
||||
log.Printf("Wrote unit %q", unit.Name)
|
||||
reload = true
|
||||
}
|
||||
|
||||
for _, dropin := range unit.DropIns {
|
||||
if dropin.Name != "" && dropin.Content != "" {
|
||||
log.Printf("Writing drop-in unit %q to filesystem", dropin.Name)
|
||||
if err := um.PlaceUnitDropIn(unit, dropin); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("Wrote drop-in unit %q", dropin.Name)
|
||||
reload = true
|
||||
}
|
||||
}
|
||||
|
||||
if unit.Mask {
|
||||
log.Printf("Masking unit file %s", unit.Name)
|
||||
if err := um.MaskUnit(&unit); err != nil {
|
||||
log.Printf("Masking unit file %q", unit.Name)
|
||||
if err := um.MaskUnit(unit); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if unit.Runtime {
|
||||
log.Printf("Ensuring runtime unit file %s is unmasked", unit.Name)
|
||||
if err := um.UnmaskUnit(&unit); err != nil {
|
||||
log.Printf("Ensuring runtime unit file %q is unmasked", unit.Name)
|
||||
if err := um.UnmaskUnit(unit); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if unit.Enable {
|
||||
if unit.Group() != "network" {
|
||||
log.Printf("Enabling unit file %s", unit.Name)
|
||||
if err := um.EnableUnitFile(unit.Name, unit.Runtime); err != nil {
|
||||
log.Printf("Enabling unit file %q", unit.Name)
|
||||
if err := um.EnableUnitFile(unit); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("Enabled unit %s", unit.Name)
|
||||
log.Printf("Enabled unit %q", unit.Name)
|
||||
} else {
|
||||
log.Printf("Skipping enable for network-like unit %s", unit.Name)
|
||||
log.Printf("Skipping enable for network-like unit %q", unit.Name)
|
||||
}
|
||||
}
|
||||
|
||||
if unit.Group() == "network" {
|
||||
actions = append(actions, action{"systemd-networkd.service", "restart"})
|
||||
networkd := system.Unit{Unit: config.Unit{Name: "systemd-networkd.service"}}
|
||||
actions = append(actions, action{networkd, "restart"})
|
||||
} else if unit.Command != "" {
|
||||
actions = append(actions, action{unit.Name, unit.Command})
|
||||
actions = append(actions, action{unit, unit.Command})
|
||||
}
|
||||
}
|
||||
|
||||
if reload {
|
||||
if err := um.DaemonReload(); err != nil {
|
||||
return errors.New(fmt.Sprintf("failed systemd daemon-reload: %v", err))
|
||||
return errors.New(fmt.Sprintf("failed systemd daemon-reload: %s", err))
|
||||
}
|
||||
}
|
||||
|
||||
for _, action := range actions {
|
||||
log.Printf("Calling unit command '%s %s'", action.command, action.unit)
|
||||
res, err := um.RunUnitCommand(action.command, action.unit)
|
||||
log.Printf("Calling unit command %q on %q'", action.command, action.unit.Name)
|
||||
res, err := um.RunUnitCommand(action.unit, action.command)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("Result of '%s %s': %s", action.command, action.unit, res)
|
||||
log.Printf("Result of %q on %q': %s", action.command, action.unit.Name, res)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@@ -17,6 +17,7 @@
|
||||
package initialize
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/config"
|
||||
@@ -32,99 +33,171 @@ type TestUnitManager struct {
|
||||
reload bool
|
||||
}
|
||||
|
||||
func (tum *TestUnitManager) PlaceUnit(unit *system.Unit, dst string) error {
|
||||
tum.placed = append(tum.placed, unit.Name)
|
||||
func (tum *TestUnitManager) PlaceUnit(u system.Unit) error {
|
||||
tum.placed = append(tum.placed, u.Name)
|
||||
return nil
|
||||
}
|
||||
func (tum *TestUnitManager) EnableUnitFile(unit string, runtime bool) error {
|
||||
tum.enabled = append(tum.enabled, unit)
|
||||
func (tum *TestUnitManager) PlaceUnitDropIn(u system.Unit, d config.UnitDropIn) error {
|
||||
tum.placed = append(tum.placed, u.Name+".d/"+d.Name)
|
||||
return nil
|
||||
}
|
||||
func (tum *TestUnitManager) RunUnitCommand(command, unit string) (string, error) {
|
||||
func (tum *TestUnitManager) EnableUnitFile(u system.Unit) error {
|
||||
tum.enabled = append(tum.enabled, u.Name)
|
||||
return nil
|
||||
}
|
||||
func (tum *TestUnitManager) RunUnitCommand(u system.Unit, c string) (string, error) {
|
||||
tum.commands = make(map[string]string)
|
||||
tum.commands[unit] = command
|
||||
tum.commands[u.Name] = c
|
||||
return "", nil
|
||||
}
|
||||
func (tum *TestUnitManager) DaemonReload() error {
|
||||
tum.reload = true
|
||||
return nil
|
||||
}
|
||||
func (tum *TestUnitManager) MaskUnit(unit *system.Unit) error {
|
||||
tum.masked = append(tum.masked, unit.Name)
|
||||
func (tum *TestUnitManager) MaskUnit(u system.Unit) error {
|
||||
tum.masked = append(tum.masked, u.Name)
|
||||
return nil
|
||||
}
|
||||
func (tum *TestUnitManager) UnmaskUnit(unit *system.Unit) error {
|
||||
tum.unmasked = append(tum.unmasked, unit.Name)
|
||||
func (tum *TestUnitManager) UnmaskUnit(u system.Unit) error {
|
||||
tum.unmasked = append(tum.unmasked, u.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestProcessUnits(t *testing.T) {
|
||||
tum := &TestUnitManager{}
|
||||
units := []system.Unit{
|
||||
system.Unit{Unit: config.Unit{
|
||||
Name: "foo",
|
||||
Mask: true,
|
||||
}},
|
||||
}
|
||||
if err := processUnits(units, "", tum); err != nil {
|
||||
t.Fatalf("unexpected error calling processUnits: %v", err)
|
||||
}
|
||||
if len(tum.masked) != 1 || tum.masked[0] != "foo" {
|
||||
t.Errorf("expected foo to be masked, but found %v", tum.masked)
|
||||
tests := []struct {
|
||||
units []system.Unit
|
||||
|
||||
result TestUnitManager
|
||||
}{
|
||||
{
|
||||
units: []system.Unit{
|
||||
system.Unit{Unit: config.Unit{
|
||||
Name: "foo",
|
||||
Mask: true,
|
||||
}},
|
||||
},
|
||||
result: TestUnitManager{
|
||||
masked: []string{"foo"},
|
||||
},
|
||||
},
|
||||
{
|
||||
units: []system.Unit{
|
||||
system.Unit{Unit: config.Unit{
|
||||
Name: "bar.network",
|
||||
}},
|
||||
},
|
||||
result: TestUnitManager{
|
||||
commands: map[string]string{
|
||||
"systemd-networkd.service": "restart",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
units: []system.Unit{
|
||||
system.Unit{Unit: config.Unit{
|
||||
Name: "baz.service",
|
||||
Content: "[Service]\nExecStart=/bin/true",
|
||||
}},
|
||||
},
|
||||
result: TestUnitManager{
|
||||
placed: []string{"baz.service"},
|
||||
reload: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
units: []system.Unit{
|
||||
system.Unit{Unit: config.Unit{
|
||||
Name: "locksmithd.service",
|
||||
Runtime: true,
|
||||
}},
|
||||
},
|
||||
result: TestUnitManager{
|
||||
unmasked: []string{"locksmithd.service"},
|
||||
},
|
||||
},
|
||||
{
|
||||
units: []system.Unit{
|
||||
system.Unit{Unit: config.Unit{
|
||||
Name: "woof",
|
||||
Enable: true,
|
||||
}},
|
||||
},
|
||||
result: TestUnitManager{
|
||||
enabled: []string{"woof"},
|
||||
},
|
||||
},
|
||||
{
|
||||
units: []system.Unit{
|
||||
system.Unit{Unit: config.Unit{
|
||||
Name: "hi.service",
|
||||
Runtime: true,
|
||||
Content: "[Service]\nExecStart=/bin/echo hi",
|
||||
DropIns: []config.UnitDropIn{
|
||||
{
|
||||
Name: "lo.conf",
|
||||
Content: "[Service]\nExecStart=/bin/echo lo",
|
||||
},
|
||||
{
|
||||
Name: "bye.conf",
|
||||
Content: "[Service]\nExecStart=/bin/echo bye",
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
result: TestUnitManager{
|
||||
placed: []string{"hi.service", "hi.service.d/lo.conf", "hi.service.d/bye.conf"},
|
||||
unmasked: []string{"hi.service"},
|
||||
reload: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
units: []system.Unit{
|
||||
system.Unit{Unit: config.Unit{
|
||||
DropIns: []config.UnitDropIn{
|
||||
{
|
||||
Name: "lo.conf",
|
||||
Content: "[Service]\nExecStart=/bin/echo lo",
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
result: TestUnitManager{},
|
||||
},
|
||||
{
|
||||
units: []system.Unit{
|
||||
system.Unit{Unit: config.Unit{
|
||||
Name: "hi.service",
|
||||
DropIns: []config.UnitDropIn{
|
||||
{
|
||||
Content: "[Service]\nExecStart=/bin/echo lo",
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
result: TestUnitManager{},
|
||||
},
|
||||
{
|
||||
units: []system.Unit{
|
||||
system.Unit{Unit: config.Unit{
|
||||
Name: "hi.service",
|
||||
DropIns: []config.UnitDropIn{
|
||||
{
|
||||
Name: "lo.conf",
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
result: TestUnitManager{},
|
||||
},
|
||||
}
|
||||
|
||||
tum = &TestUnitManager{}
|
||||
units = []system.Unit{
|
||||
system.Unit{Unit: config.Unit{
|
||||
Name: "bar.network",
|
||||
}},
|
||||
}
|
||||
if err := processUnits(units, "", tum); err != nil {
|
||||
t.Fatalf("unexpected error calling processUnits: %v", err)
|
||||
}
|
||||
if _, ok := tum.commands["systemd-networkd.service"]; !ok {
|
||||
t.Errorf("expected systemd-networkd.service to be reloaded!")
|
||||
}
|
||||
|
||||
tum = &TestUnitManager{}
|
||||
units = []system.Unit{
|
||||
system.Unit{Unit: config.Unit{
|
||||
Name: "baz.service",
|
||||
Content: "[Service]\nExecStart=/bin/true",
|
||||
}},
|
||||
}
|
||||
if err := processUnits(units, "", tum); err != nil {
|
||||
t.Fatalf("unexpected error calling processUnits: %v", err)
|
||||
}
|
||||
if len(tum.placed) != 1 || tum.placed[0] != "baz.service" {
|
||||
t.Fatalf("expected baz.service to be written, but got %v", tum.placed)
|
||||
}
|
||||
|
||||
tum = &TestUnitManager{}
|
||||
units = []system.Unit{
|
||||
system.Unit{Unit: config.Unit{
|
||||
Name: "locksmithd.service",
|
||||
Runtime: true,
|
||||
}},
|
||||
}
|
||||
if err := processUnits(units, "", tum); err != nil {
|
||||
t.Fatalf("unexpected error calling processUnits: %v", err)
|
||||
}
|
||||
if len(tum.unmasked) != 1 || tum.unmasked[0] != "locksmithd.service" {
|
||||
t.Fatalf("expected locksmithd.service to be unmasked, but got %v", tum.unmasked)
|
||||
}
|
||||
|
||||
tum = &TestUnitManager{}
|
||||
units = []system.Unit{
|
||||
system.Unit{Unit: config.Unit{
|
||||
Name: "woof",
|
||||
Enable: true,
|
||||
}},
|
||||
}
|
||||
if err := processUnits(units, "", tum); err != nil {
|
||||
t.Fatalf("unexpected error calling processUnits: %v", err)
|
||||
}
|
||||
if len(tum.enabled) != 1 || tum.enabled[0] != "woof" {
|
||||
t.Fatalf("expected woof to be enabled, but got %v", tum.enabled)
|
||||
for _, tt := range tests {
|
||||
tum := &TestUnitManager{}
|
||||
if err := processUnits(tt.units, "", tum); err != nil {
|
||||
t.Errorf("bad error (%+v): want nil, got %s", tt.units, err)
|
||||
}
|
||||
if !reflect.DeepEqual(tt.result, *tum) {
|
||||
t.Errorf("bad result (%+v): want %+v, got %+v", tt.units, tt.result, tum)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -17,32 +17,25 @@
|
||||
package initialize
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"errors"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/config"
|
||||
"github.com/coreos/coreos-cloudinit/system"
|
||||
)
|
||||
|
||||
func ParseUserData(contents string) (interface{}, error) {
|
||||
if len(contents) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
header := strings.SplitN(contents, "\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.TrimSpace(header)
|
||||
|
||||
if strings.HasPrefix(header, "#!") {
|
||||
switch {
|
||||
case config.IsScript(contents):
|
||||
log.Printf("Parsing user-data as script")
|
||||
return system.Script(contents), nil
|
||||
} else if header == "#cloud-config" {
|
||||
return config.NewScript(contents)
|
||||
case config.IsCloudConfig(contents):
|
||||
log.Printf("Parsing user-data as cloud-config")
|
||||
return config.NewCloudConfig(contents)
|
||||
} else {
|
||||
return nil, fmt.Errorf("Unrecognized user-data header: %s", header)
|
||||
default:
|
||||
return nil, errors.New("Unrecognized user-data format")
|
||||
}
|
||||
}
|
||||
|
@@ -38,7 +38,7 @@ func PrepWorkspace(workspace string) error {
|
||||
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")
|
||||
tmp, err := ioutil.TempFile(scriptsPath, "")
|
||||
if err != nil {
|
||||
|
@@ -19,19 +19,21 @@ package system
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/config"
|
||||
)
|
||||
|
||||
// dropinContents generates the contents for a drop-in unit given the config.
|
||||
// The argument must be a struct from the 'config' package.
|
||||
func dropinContents(e interface{}) string {
|
||||
func serviceContents(e interface{}) string {
|
||||
et := reflect.TypeOf(e)
|
||||
ev := reflect.ValueOf(e)
|
||||
|
||||
var out string
|
||||
for i := 0; i < et.NumField(); i++ {
|
||||
if val := ev.Field(i).String(); val != "" {
|
||||
if val := ev.Field(i).Interface(); !config.IsZero(val) {
|
||||
key := et.Field(i).Tag.Get("env")
|
||||
out += fmt.Sprintf("Environment=\"%s=%s\"\n", key, val)
|
||||
out += fmt.Sprintf("Environment=\"%s=%v\"\n", key, val)
|
||||
}
|
||||
}
|
||||
|
||||
|
55
system/env_test.go
Normal file
55
system/env_test.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestServiceContents(t *testing.T) {
|
||||
tests := []struct {
|
||||
Config interface{}
|
||||
Contents string
|
||||
}{
|
||||
{
|
||||
struct{}{},
|
||||
"",
|
||||
},
|
||||
{
|
||||
struct {
|
||||
A string `env:"A"`
|
||||
B int `env:"B"`
|
||||
C bool `env:"C"`
|
||||
D float64 `env:"D"`
|
||||
}{
|
||||
"hi", 1, true, 0.12345,
|
||||
},
|
||||
`[Service]
|
||||
Environment="A=hi"
|
||||
Environment="B=1"
|
||||
Environment="C=true"
|
||||
Environment="D=0.12345"
|
||||
`,
|
||||
},
|
||||
{
|
||||
struct {
|
||||
A float64 `env:"A"`
|
||||
B float64 `env:"B"`
|
||||
C float64 `env:"C"`
|
||||
D float64 `env:"D"`
|
||||
}{
|
||||
0.000001, 1, 0.9999999, 0.1,
|
||||
},
|
||||
`[Service]
|
||||
Environment="A=1e-06"
|
||||
Environment="B=1"
|
||||
Environment="C=0.9999999"
|
||||
Environment="D=0.1"
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
if c := serviceContents(tt.Config); c != tt.Contents {
|
||||
t.Errorf("bad contents (%+v): want %q, got %q", tt, tt.Contents, c)
|
||||
}
|
||||
}
|
||||
}
|
@@ -28,14 +28,12 @@ type Etcd struct {
|
||||
|
||||
// Units creates a Unit file drop-in for etcd, using any configured options.
|
||||
func (ee Etcd) Units() []Unit {
|
||||
content := dropinContents(ee.Etcd)
|
||||
if content == "" {
|
||||
return nil
|
||||
}
|
||||
return []Unit{{config.Unit{
|
||||
Name: "etcd.service",
|
||||
Runtime: true,
|
||||
DropIn: true,
|
||||
Content: content,
|
||||
DropIns: []config.UnitDropIn{{
|
||||
Name: "20-cloudinit.conf",
|
||||
Content: serviceContents(ee.Etcd),
|
||||
}},
|
||||
}}}
|
||||
}
|
||||
|
@@ -30,7 +30,11 @@ func TestEtcdUnits(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
config.Etcd{},
|
||||
nil,
|
||||
[]Unit{{config.Unit{
|
||||
Name: "etcd.service",
|
||||
Runtime: true,
|
||||
DropIns: []config.UnitDropIn{{Name: "20-cloudinit.conf"}},
|
||||
}}},
|
||||
},
|
||||
{
|
||||
config.Etcd{
|
||||
@@ -40,11 +44,13 @@ func TestEtcdUnits(t *testing.T) {
|
||||
[]Unit{{config.Unit{
|
||||
Name: "etcd.service",
|
||||
Runtime: true,
|
||||
DropIn: true,
|
||||
Content: `[Service]
|
||||
DropIns: []config.UnitDropIn{{
|
||||
Name: "20-cloudinit.conf",
|
||||
Content: `[Service]
|
||||
Environment="ETCD_DISCOVERY=http://disco.example.com/foobar"
|
||||
Environment="ETCD_PEER_BIND_ADDR=127.0.0.1:7002"
|
||||
`,
|
||||
}},
|
||||
}}},
|
||||
},
|
||||
{
|
||||
@@ -56,18 +62,20 @@ Environment="ETCD_PEER_BIND_ADDR=127.0.0.1:7002"
|
||||
[]Unit{{config.Unit{
|
||||
Name: "etcd.service",
|
||||
Runtime: true,
|
||||
DropIn: true,
|
||||
Content: `[Service]
|
||||
DropIns: []config.UnitDropIn{{
|
||||
Name: "20-cloudinit.conf",
|
||||
Content: `[Service]
|
||||
Environment="ETCD_DISCOVERY=http://disco.example.com/foobar"
|
||||
Environment="ETCD_NAME=node001"
|
||||
Environment="ETCD_PEER_BIND_ADDR=127.0.0.1:7002"
|
||||
`,
|
||||
}},
|
||||
}}},
|
||||
},
|
||||
} {
|
||||
units := Etcd{tt.config}.Units()
|
||||
if !reflect.DeepEqual(tt.units, units) {
|
||||
t.Errorf("bad units (%q): want %#v, got %#v", tt.config, tt.units, units)
|
||||
t.Errorf("bad units (%+v): want %#v, got %#v", tt.config, tt.units, units)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -17,9 +17,9 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
@@ -39,22 +39,23 @@ func (f *File) Permissions() (os.FileMode, error) {
|
||||
return os.FileMode(0644), nil
|
||||
}
|
||||
|
||||
// Parse string representation of file mode as octal
|
||||
perm, err := strconv.ParseInt(f.RawFilePermissions, 8, 32)
|
||||
// Parse string representation of file mode as integer
|
||||
perm, err := strconv.ParseInt(f.RawFilePermissions, 0, 32)
|
||||
if err != nil {
|
||||
return 0, errors.New("Unable to parse file permissions as octal integer")
|
||||
return 0, fmt.Errorf("Unable to parse file permissions %q as integer", f.RawFilePermissions)
|
||||
}
|
||||
return os.FileMode(perm), nil
|
||||
}
|
||||
|
||||
func WriteFile(f *File, root string) (string, error) {
|
||||
fullpath := path.Join(root, f.Path)
|
||||
dir := path.Dir(fullpath)
|
||||
log.Printf("Writing file to %q", fullpath)
|
||||
|
||||
if f.Encoding != "" {
|
||||
return "", fmt.Errorf("Unable to write file with encoding %s", f.Encoding)
|
||||
}
|
||||
|
||||
fullpath := path.Join(root, f.Path)
|
||||
dir := path.Dir(fullpath)
|
||||
|
||||
if err := EnsureDirectoryExists(dir); err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -95,6 +96,7 @@ func WriteFile(f *File, root string) (string, error) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
log.Printf("Wrote file to %q", fullpath)
|
||||
return fullpath, nil
|
||||
}
|
||||
|
||||
|
@@ -85,6 +85,38 @@ func TestWriteFileInvalidPermission(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecimalFilePermissions(t *testing.T) {
|
||||
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to create tempdir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
fn := "foo"
|
||||
fullPath := path.Join(dir, fn)
|
||||
|
||||
wf := File{config.File{
|
||||
Path: fn,
|
||||
RawFilePermissions: "484", // Decimal representation of 0744
|
||||
}}
|
||||
|
||||
path, err := WriteFile(&wf, dir)
|
||||
if err != nil {
|
||||
t.Fatalf("Processing of WriteFile failed: %v", err)
|
||||
} else if path != fullPath {
|
||||
t.Fatalf("WriteFile returned bad path: want %s, got %s", fullPath, path)
|
||||
}
|
||||
|
||||
fi, err := os.Stat(fullPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to stat file: %v", err)
|
||||
}
|
||||
|
||||
if fi.Mode() != os.FileMode(0744) {
|
||||
t.Errorf("File has incorrect mode: %v", fi.Mode())
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteFilePermissions(t *testing.T) {
|
||||
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
|
||||
if err != nil {
|
||||
|
24
system/flannel.go
Normal file
24
system/flannel.go
Normal file
@@ -0,0 +1,24 @@
|
||||
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 []Unit{{config.Unit{
|
||||
Name: "flanneld.service",
|
||||
Runtime: true,
|
||||
DropIns: []config.UnitDropIn{{
|
||||
Name: "20-cloudinit.conf",
|
||||
Content: serviceContents(fl.Flannel),
|
||||
}},
|
||||
}}}
|
||||
}
|
46
system/flannel_test.go
Normal file
46
system/flannel_test.go
Normal file
@@ -0,0 +1,46 @@
|
||||
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{},
|
||||
[]Unit{{config.Unit{
|
||||
Name: "flanneld.service",
|
||||
Runtime: true,
|
||||
DropIns: []config.UnitDropIn{{Name: "20-cloudinit.conf"}},
|
||||
}}},
|
||||
},
|
||||
{
|
||||
config.Flannel{
|
||||
EtcdEndpoint: "http://12.34.56.78:4001",
|
||||
EtcdPrefix: "/coreos.com/network/tenant1",
|
||||
},
|
||||
[]Unit{{config.Unit{
|
||||
Name: "flanneld.service",
|
||||
Runtime: true,
|
||||
DropIns: []config.UnitDropIn{{
|
||||
Name: "20-cloudinit.conf",
|
||||
Content: `[Service]
|
||||
Environment="FLANNELD_ETCD_ENDPOINT=http://12.34.56.78:4001"
|
||||
Environment="FLANNELD_ETCD_PREFIX=/coreos.com/network/tenant1"
|
||||
`,
|
||||
}},
|
||||
}}},
|
||||
},
|
||||
} {
|
||||
units := Flannel{tt.config}.Units()
|
||||
if !reflect.DeepEqual(units, tt.units) {
|
||||
t.Errorf("bad units (%q): want %v, got %v", tt.config, tt.units, units)
|
||||
}
|
||||
}
|
||||
}
|
@@ -29,14 +29,12 @@ type Fleet struct {
|
||||
// Units generates a Unit file drop-in for fleet, if any fleet options were
|
||||
// configured in cloud-config
|
||||
func (fe Fleet) Units() []Unit {
|
||||
content := dropinContents(fe.Fleet)
|
||||
if content == "" {
|
||||
return nil
|
||||
}
|
||||
return []Unit{{config.Unit{
|
||||
Name: "fleet.service",
|
||||
Runtime: true,
|
||||
DropIn: true,
|
||||
Content: content,
|
||||
DropIns: []config.UnitDropIn{{
|
||||
Name: "20-cloudinit.conf",
|
||||
Content: serviceContents(fe.Fleet),
|
||||
}},
|
||||
}}}
|
||||
}
|
||||
|
@@ -30,25 +30,31 @@ func TestFleetUnits(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
config.Fleet{},
|
||||
nil,
|
||||
[]Unit{{config.Unit{
|
||||
Name: "fleet.service",
|
||||
Runtime: true,
|
||||
DropIns: []config.UnitDropIn{{Name: "20-cloudinit.conf"}},
|
||||
}}},
|
||||
},
|
||||
{
|
||||
config.Fleet{
|
||||
PublicIP: "12.34.56.78",
|
||||
},
|
||||
[]Unit{{config.Unit{
|
||||
Name: "fleet.service",
|
||||
Content: `[Service]
|
||||
Name: "fleet.service",
|
||||
Runtime: true,
|
||||
DropIns: []config.UnitDropIn{{
|
||||
Name: "20-cloudinit.conf",
|
||||
Content: `[Service]
|
||||
Environment="FLEET_PUBLIC_IP=12.34.56.78"
|
||||
`,
|
||||
Runtime: true,
|
||||
DropIn: true,
|
||||
}},
|
||||
}}},
|
||||
},
|
||||
} {
|
||||
units := Fleet{tt.config}.Units()
|
||||
if !reflect.DeepEqual(units, tt.units) {
|
||||
t.Errorf("bad units (%q): want %#v, got %#v", tt.config, tt.units, units)
|
||||
t.Errorf("bad units (%+v): want %#v, got %#v", tt.config, tt.units, units)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -96,7 +96,8 @@ func maybeProbeBonding(interfaces []network.InterfaceGenerator) error {
|
||||
|
||||
func restartNetworkd() error {
|
||||
log.Printf("Restarting networkd.service\n")
|
||||
_, err := NewUnitManager("").RunUnitCommand("restart", "systemd-networkd.service")
|
||||
networkd := Unit{config.Unit{Name: "systemd-networkd.service"}}
|
||||
_, err := NewUnitManager("").RunUnitCommand(networkd, "restart")
|
||||
return err
|
||||
}
|
||||
|
||||
|
@@ -23,7 +23,6 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/Godeps/_workspace/src/github.com/coreos/go-systemd/dbus"
|
||||
@@ -42,49 +41,51 @@ type systemd struct {
|
||||
// never be used as a true MachineID
|
||||
const fakeMachineID = "42000000000000000000000000000042"
|
||||
|
||||
// PlaceUnit writes a unit file at the provided destination, creating
|
||||
// parent directories as necessary.
|
||||
func (s *systemd) PlaceUnit(u *Unit, dst string) error {
|
||||
dir := filepath.Dir(dst)
|
||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(dir, os.FileMode(0755)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// PlaceUnit writes a unit file at its desired destination, creating parent
|
||||
// directories as necessary.
|
||||
func (s *systemd) PlaceUnit(u Unit) error {
|
||||
file := File{config.File{
|
||||
Path: filepath.Base(dst),
|
||||
Path: u.Destination(s.root),
|
||||
Content: u.Content,
|
||||
RawFilePermissions: "0644",
|
||||
}}
|
||||
|
||||
_, err := WriteFile(&file, dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
_, err := WriteFile(&file, "/")
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *systemd) EnableUnitFile(unit string, runtime bool) error {
|
||||
// PlaceUnitDropIn writes a unit drop-in file at its desired destination,
|
||||
// creating parent directories as necessary.
|
||||
func (s *systemd) PlaceUnitDropIn(u Unit, d config.UnitDropIn) error {
|
||||
file := File{config.File{
|
||||
Path: u.DropInDestination(s.root, d),
|
||||
Content: d.Content,
|
||||
RawFilePermissions: "0644",
|
||||
}}
|
||||
|
||||
_, err := WriteFile(&file, "/")
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *systemd) EnableUnitFile(u Unit) error {
|
||||
conn, err := dbus.New()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
units := []string{unit}
|
||||
_, _, err = conn.EnableUnitFiles(units, runtime, true)
|
||||
units := []string{u.Name}
|
||||
_, _, err = conn.EnableUnitFiles(units, u.Runtime, true)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *systemd) RunUnitCommand(command, unit string) (string, error) {
|
||||
func (s *systemd) RunUnitCommand(u Unit, c string) (string, error) {
|
||||
conn, err := dbus.New()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var fn func(string, string) (string, error)
|
||||
switch command {
|
||||
switch c {
|
||||
case "start":
|
||||
fn = conn.StartUnit
|
||||
case "stop":
|
||||
@@ -100,10 +101,10 @@ func (s *systemd) RunUnitCommand(command, unit string) (string, error) {
|
||||
case "reload-or-try-restart":
|
||||
fn = conn.ReloadOrTryRestartUnit
|
||||
default:
|
||||
return "", fmt.Errorf("Unsupported systemd command %q", command)
|
||||
return "", fmt.Errorf("Unsupported systemd command %q", c)
|
||||
}
|
||||
|
||||
return fn(unit, "replace")
|
||||
return fn(u.Name, "replace")
|
||||
}
|
||||
|
||||
func (s *systemd) DaemonReload() error {
|
||||
@@ -119,8 +120,8 @@ func (s *systemd) DaemonReload() error {
|
||||
// /dev/null, analogous to `systemctl mask`.
|
||||
// N.B.: Unlike `systemctl mask`, this function will *remove any existing unit
|
||||
// file at the location*, to ensure that the mask will succeed.
|
||||
func (s *systemd) MaskUnit(unit *Unit) error {
|
||||
masked := unit.Destination(s.root)
|
||||
func (s *systemd) MaskUnit(u Unit) error {
|
||||
masked := u.Destination(s.root)
|
||||
if _, err := os.Stat(masked); os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(path.Dir(masked), os.FileMode(0755)); err != nil {
|
||||
return err
|
||||
@@ -134,8 +135,8 @@ func (s *systemd) MaskUnit(unit *Unit) error {
|
||||
// UnmaskUnit is analogous to systemd's unit_file_unmask. If the file
|
||||
// associated with the given Unit is empty or appears to be a symlink to
|
||||
// /dev/null, it is removed.
|
||||
func (s *systemd) UnmaskUnit(unit *Unit) error {
|
||||
masked := unit.Destination(s.root)
|
||||
func (s *systemd) UnmaskUnit(u Unit) error {
|
||||
masked := u.Destination(s.root)
|
||||
ne, err := nullOrEmpty(masked)
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
|
@@ -17,6 +17,7 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
@@ -25,133 +26,109 @@ import (
|
||||
"github.com/coreos/coreos-cloudinit/config"
|
||||
)
|
||||
|
||||
func TestPlaceNetworkUnit(t *testing.T) {
|
||||
u := Unit{config.Unit{
|
||||
Name: "50-eth0.network",
|
||||
Runtime: true,
|
||||
Content: `[Match]
|
||||
Name=eth47
|
||||
|
||||
[Network]
|
||||
Address=10.209.171.177/19
|
||||
`,
|
||||
}}
|
||||
|
||||
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to create tempdir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
sd := &systemd{dir}
|
||||
|
||||
dst := u.Destination(dir)
|
||||
expectDst := path.Join(dir, "run", "systemd", "network", "50-eth0.network")
|
||||
if dst != expectDst {
|
||||
t.Fatalf("unit.Destination returned %s, expected %s", dst, expectDst)
|
||||
func TestPlaceUnit(t *testing.T) {
|
||||
tests := []config.Unit{
|
||||
{
|
||||
Name: "50-eth0.network",
|
||||
Runtime: true,
|
||||
Content: "[Match]\nName=eth47\n\n[Network]\nAddress=10.209.171.177/19\n",
|
||||
},
|
||||
{
|
||||
Name: "media-state.mount",
|
||||
Content: "[Mount]\nWhat=/dev/sdb1\nWhere=/media/state\n",
|
||||
},
|
||||
}
|
||||
|
||||
if err := sd.PlaceUnit(&u, dst); err != nil {
|
||||
t.Fatalf("PlaceUnit failed: %v", err)
|
||||
}
|
||||
for _, tt := range tests {
|
||||
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Unable to create tempdir: %v", err))
|
||||
}
|
||||
|
||||
fi, err := os.Stat(dst)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to stat file: %v", err)
|
||||
}
|
||||
u := Unit{tt}
|
||||
sd := &systemd{dir}
|
||||
|
||||
if fi.Mode() != os.FileMode(0644) {
|
||||
t.Errorf("File has incorrect mode: %v", fi.Mode())
|
||||
}
|
||||
if err := sd.PlaceUnit(u); err != nil {
|
||||
t.Fatalf("PlaceUnit(): bad error (%+v): want nil, got %s", tt, err)
|
||||
}
|
||||
|
||||
contents, err := ioutil.ReadFile(dst)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to read expected file: %v", err)
|
||||
}
|
||||
fi, err := os.Stat(u.Destination(dir))
|
||||
if err != nil {
|
||||
t.Fatalf("Stat(): bad error (%+v): want nil, got %s", tt, err)
|
||||
}
|
||||
|
||||
expectContents := `[Match]
|
||||
Name=eth47
|
||||
if mode := fi.Mode(); mode != os.FileMode(0644) {
|
||||
t.Errorf("bad filemode (%+v): want %v, got %v", tt, os.FileMode(0644), mode)
|
||||
}
|
||||
|
||||
[Network]
|
||||
Address=10.209.171.177/19
|
||||
`
|
||||
if string(contents) != expectContents {
|
||||
t.Fatalf("File has incorrect contents '%s'.\nExpected '%s'", string(contents), expectContents)
|
||||
c, err := ioutil.ReadFile(u.Destination(dir))
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFile(): bad error (%+v): want nil, got %s", tt, err)
|
||||
}
|
||||
|
||||
if string(c) != tt.Content {
|
||||
t.Errorf("bad contents (%+v): want %q, got %q", tt, tt.Content, string(c))
|
||||
}
|
||||
|
||||
os.RemoveAll(dir)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnitDestination(t *testing.T) {
|
||||
dir := "/some/dir"
|
||||
name := "foobar.service"
|
||||
|
||||
u := Unit{config.Unit{
|
||||
Name: name,
|
||||
DropIn: false,
|
||||
}}
|
||||
|
||||
dst := u.Destination(dir)
|
||||
expectDst := path.Join(dir, "etc", "systemd", "system", "foobar.service")
|
||||
if dst != expectDst {
|
||||
t.Errorf("unit.Destination returned %s, expected %s", dst, expectDst)
|
||||
func TestPlaceUnitDropIn(t *testing.T) {
|
||||
tests := []config.Unit{
|
||||
{
|
||||
Name: "false.service",
|
||||
Runtime: true,
|
||||
DropIns: []config.UnitDropIn{
|
||||
{
|
||||
Name: "00-true.conf",
|
||||
Content: "[Service]\nExecStart=\nExecStart=/usr/bin/true\n",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "true.service",
|
||||
DropIns: []config.UnitDropIn{
|
||||
{
|
||||
Name: "00-false.conf",
|
||||
Content: "[Service]\nExecStart=\nExecStart=/usr/bin/false\n",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
u.DropIn = true
|
||||
for _, tt := range tests {
|
||||
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Unable to create tempdir: %v", err))
|
||||
}
|
||||
|
||||
dst = u.Destination(dir)
|
||||
expectDst = path.Join(dir, "etc", "systemd", "system", "foobar.service.d", cloudConfigDropIn)
|
||||
if dst != expectDst {
|
||||
t.Errorf("unit.Destination returned %s, expected %s", dst, expectDst)
|
||||
}
|
||||
}
|
||||
u := Unit{tt}
|
||||
sd := &systemd{dir}
|
||||
|
||||
func TestPlaceMountUnit(t *testing.T) {
|
||||
u := Unit{config.Unit{
|
||||
Name: "media-state.mount",
|
||||
Runtime: false,
|
||||
Content: `[Mount]
|
||||
What=/dev/sdb1
|
||||
Where=/media/state
|
||||
`,
|
||||
}}
|
||||
if err := sd.PlaceUnitDropIn(u, u.DropIns[0]); err != nil {
|
||||
t.Fatalf("PlaceUnit(): bad error (%+v): want nil, got %s", tt, err)
|
||||
}
|
||||
|
||||
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to create tempdir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
fi, err := os.Stat(u.DropInDestination(dir, u.DropIns[0]))
|
||||
if err != nil {
|
||||
t.Fatalf("Stat(): bad error (%+v): want nil, got %s", tt, err)
|
||||
}
|
||||
|
||||
sd := &systemd{dir}
|
||||
if mode := fi.Mode(); mode != os.FileMode(0644) {
|
||||
t.Errorf("bad filemode (%+v): want %v, got %v", tt, os.FileMode(0644), mode)
|
||||
}
|
||||
|
||||
dst := u.Destination(dir)
|
||||
expectDst := path.Join(dir, "etc", "systemd", "system", "media-state.mount")
|
||||
if dst != expectDst {
|
||||
t.Fatalf("unit.Destination returned %s, expected %s", dst, expectDst)
|
||||
}
|
||||
c, err := ioutil.ReadFile(u.DropInDestination(dir, u.DropIns[0]))
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFile(): bad error (%+v): want nil, got %s", tt, err)
|
||||
}
|
||||
|
||||
if err := sd.PlaceUnit(&u, dst); err != nil {
|
||||
t.Fatalf("PlaceUnit failed: %v", err)
|
||||
}
|
||||
if string(c) != u.DropIns[0].Content {
|
||||
t.Errorf("bad contents (%+v): want %q, got %q", tt, u.DropIns[0].Content, string(c))
|
||||
}
|
||||
|
||||
fi, err := os.Stat(dst)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to stat file: %v", err)
|
||||
}
|
||||
|
||||
if fi.Mode() != os.FileMode(0644) {
|
||||
t.Errorf("File has incorrect mode: %v", fi.Mode())
|
||||
}
|
||||
|
||||
contents, err := ioutil.ReadFile(dst)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to read expected file: %v", err)
|
||||
}
|
||||
|
||||
expectContents := `[Mount]
|
||||
What=/dev/sdb1
|
||||
Where=/media/state
|
||||
`
|
||||
if string(contents) != expectContents {
|
||||
t.Fatalf("File has incorrect contents '%s'.\nExpected '%s'", string(contents), expectContents)
|
||||
os.RemoveAll(dir)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,7 +157,7 @@ func TestMaskUnit(t *testing.T) {
|
||||
sd := &systemd{dir}
|
||||
|
||||
// Ensure mask works with units that do not currently exist
|
||||
uf := &Unit{config.Unit{Name: "foo.service"}}
|
||||
uf := Unit{config.Unit{Name: "foo.service"}}
|
||||
if err := sd.MaskUnit(uf); err != nil {
|
||||
t.Fatalf("Unable to mask new unit: %v", err)
|
||||
}
|
||||
@@ -194,7 +171,7 @@ func TestMaskUnit(t *testing.T) {
|
||||
}
|
||||
|
||||
// Ensure mask works with unit files that already exist
|
||||
ub := &Unit{config.Unit{Name: "bar.service"}}
|
||||
ub := Unit{config.Unit{Name: "bar.service"}}
|
||||
barPath := path.Join(dir, "etc", "systemd", "system", "bar.service")
|
||||
if _, err := os.Create(barPath); err != nil {
|
||||
t.Fatalf("Error creating new unit file: %v", err)
|
||||
@@ -220,12 +197,12 @@ func TestUnmaskUnit(t *testing.T) {
|
||||
|
||||
sd := &systemd{dir}
|
||||
|
||||
nilUnit := &Unit{config.Unit{Name: "null.service"}}
|
||||
nilUnit := Unit{config.Unit{Name: "null.service"}}
|
||||
if err := sd.UnmaskUnit(nilUnit); err != nil {
|
||||
t.Errorf("unexpected error from unmasking nonexistent unit: %v", err)
|
||||
}
|
||||
|
||||
uf := &Unit{config.Unit{Name: "foo.service", Content: "[Service]\nExecStart=/bin/true"}}
|
||||
uf := Unit{config.Unit{Name: "foo.service", Content: "[Service]\nExecStart=/bin/true"}}
|
||||
dst := uf.Destination(dir)
|
||||
if err := os.MkdirAll(path.Dir(dst), os.FileMode(0755)); err != nil {
|
||||
t.Fatalf("Unable to create unit directory: %v", err)
|
||||
@@ -245,7 +222,7 @@ func TestUnmaskUnit(t *testing.T) {
|
||||
t.Errorf("unmask of non-empty unit mutated unit contents unexpectedly")
|
||||
}
|
||||
|
||||
ub := &Unit{config.Unit{Name: "bar.service"}}
|
||||
ub := Unit{config.Unit{Name: "bar.service"}}
|
||||
dst = ub.Destination(dir)
|
||||
if err := os.Symlink("/dev/null", dst); err != nil {
|
||||
t.Fatalf("Unable to create masked unit: %v", err)
|
||||
|
@@ -19,42 +19,66 @@ package system
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/config"
|
||||
)
|
||||
|
||||
// Name for drop-in service configuration files created by cloudconfig
|
||||
const cloudConfigDropIn = "20-cloudinit.conf"
|
||||
|
||||
type UnitManager interface {
|
||||
PlaceUnit(unit *Unit, dst string) error
|
||||
EnableUnitFile(unit string, runtime bool) error
|
||||
RunUnitCommand(command, unit string) (string, error)
|
||||
PlaceUnit(unit Unit) error
|
||||
PlaceUnitDropIn(unit Unit, dropIn config.UnitDropIn) error
|
||||
EnableUnitFile(unit Unit) error
|
||||
RunUnitCommand(unit Unit, command string) (string, error)
|
||||
MaskUnit(unit Unit) error
|
||||
UnmaskUnit(unit Unit) error
|
||||
DaemonReload() error
|
||||
MaskUnit(unit *Unit) error
|
||||
UnmaskUnit(unit *Unit) error
|
||||
}
|
||||
|
||||
// Unit is a top-level structure which embeds its underlying configuration,
|
||||
// config.Unit, and provides the system-specific Destination().
|
||||
// config.Unit, and provides the system-specific Destination(), Type(), and
|
||||
// Group().
|
||||
type Unit struct {
|
||||
config.Unit
|
||||
}
|
||||
|
||||
type Script []byte
|
||||
// Type returns the extension of the unit (everything that follows the final
|
||||
// period).
|
||||
func (u Unit) Type() string {
|
||||
ext := filepath.Ext(u.Name)
|
||||
return strings.TrimLeft(ext, ".")
|
||||
}
|
||||
|
||||
// Destination builds the appropriate absolute file path for
|
||||
// the Unit. The root argument indicates the effective base
|
||||
// directory of the system (similar to a chroot).
|
||||
func (u *Unit) Destination(root string) string {
|
||||
// Group returns "network" or "system" depending on whether or not the unit is
|
||||
// a network unit or otherwise.
|
||||
func (u Unit) Group() string {
|
||||
switch u.Type() {
|
||||
case "network", "netdev", "link":
|
||||
return "network"
|
||||
default:
|
||||
return "system"
|
||||
}
|
||||
}
|
||||
|
||||
// Destination builds the appropriate absolute file path for the Unit. The root
|
||||
// argument indicates the effective base directory of the system (similar to a
|
||||
// chroot).
|
||||
func (u Unit) Destination(root string) string {
|
||||
return path.Join(u.prefix(root), u.Name)
|
||||
}
|
||||
|
||||
// DropInDestination builds the appropriate absolute file path for the
|
||||
// UnitDropIn. The root argument indicates the effective base directory of the
|
||||
// system (similar to a chroot) and the dropIn argument is the UnitDropIn for
|
||||
// which the destination is being calculated.
|
||||
func (u Unit) DropInDestination(root string, dropIn config.UnitDropIn) string {
|
||||
return path.Join(u.prefix(root), fmt.Sprintf("%s.d", u.Name), dropIn.Name)
|
||||
}
|
||||
|
||||
func (u Unit) prefix(root string) string {
|
||||
dir := "etc"
|
||||
if u.Runtime {
|
||||
dir = "run"
|
||||
}
|
||||
|
||||
if u.DropIn {
|
||||
return path.Join(root, dir, "systemd", u.Group(), fmt.Sprintf("%s.d", u.Name), cloudConfigDropIn)
|
||||
} else {
|
||||
return path.Join(root, dir, "systemd", u.Group(), u.Name)
|
||||
}
|
||||
return path.Join(root, dir, "systemd", u.Group())
|
||||
}
|
||||
|
138
system/unit_test.go
Normal file
138
system/unit_test.go
Normal file
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
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 system
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/config"
|
||||
)
|
||||
|
||||
func TestType(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
typ string
|
||||
}{
|
||||
{},
|
||||
{"test.service", "service"},
|
||||
{"hello", ""},
|
||||
{"lots.of.dots", "dots"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
u := Unit{config.Unit{
|
||||
Name: tt.name,
|
||||
}}
|
||||
if typ := u.Type(); tt.typ != typ {
|
||||
t.Errorf("bad type (%+v): want %q, got %q", tt, tt.typ, typ)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroup(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
group string
|
||||
}{
|
||||
{"test.service", "system"},
|
||||
{"test.link", "network"},
|
||||
{"test.network", "network"},
|
||||
{"test.netdev", "network"},
|
||||
{"test.conf", "system"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
u := Unit{config.Unit{
|
||||
Name: tt.name,
|
||||
}}
|
||||
if group := u.Group(); tt.group != group {
|
||||
t.Errorf("bad group (%+v): want %q, got %q", tt, tt.group, group)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDestination(t *testing.T) {
|
||||
tests := []struct {
|
||||
root string
|
||||
name string
|
||||
runtime bool
|
||||
|
||||
destination string
|
||||
}{
|
||||
{
|
||||
root: "/some/dir",
|
||||
name: "foobar.service",
|
||||
destination: "/some/dir/etc/systemd/system/foobar.service",
|
||||
},
|
||||
{
|
||||
root: "/some/dir",
|
||||
name: "foobar.service",
|
||||
runtime: true,
|
||||
destination: "/some/dir/run/systemd/system/foobar.service",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
u := Unit{config.Unit{
|
||||
Name: tt.name,
|
||||
Runtime: tt.runtime,
|
||||
}}
|
||||
if d := u.Destination(tt.root); tt.destination != d {
|
||||
t.Errorf("bad destination (%+v): want %q, got %q", tt, tt.destination, d)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDropInDestination(t *testing.T) {
|
||||
tests := []struct {
|
||||
root string
|
||||
unitName string
|
||||
dropInName string
|
||||
runtime bool
|
||||
|
||||
destination string
|
||||
}{
|
||||
{
|
||||
root: "/some/dir",
|
||||
unitName: "foo.service",
|
||||
dropInName: "bar.conf",
|
||||
destination: "/some/dir/etc/systemd/system/foo.service.d/bar.conf",
|
||||
},
|
||||
{
|
||||
root: "/some/dir",
|
||||
unitName: "foo.service",
|
||||
dropInName: "bar.conf",
|
||||
runtime: true,
|
||||
destination: "/some/dir/run/systemd/system/foo.service.d/bar.conf",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
u := Unit{config.Unit{
|
||||
Name: tt.unitName,
|
||||
Runtime: tt.runtime,
|
||||
DropIns: []config.UnitDropIn{{
|
||||
Name: tt.dropInName,
|
||||
}},
|
||||
}}
|
||||
if d := u.DropInDestination(tt.root, u.DropIns[0]); tt.destination != d {
|
||||
t.Errorf("bad destination (%+v): want %q, got %q", tt, tt.destination, d)
|
||||
}
|
||||
}
|
||||
}
|
@@ -61,7 +61,7 @@ func (uc Update) File() (*File, error) {
|
||||
if config.IsZero(uc.Update) {
|
||||
return nil, nil
|
||||
}
|
||||
if err := config.AssertValid(uc.Update); err != nil {
|
||||
if err := config.AssertStructValid(uc.Update); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -126,7 +126,7 @@ func (uc Update) Units() []Unit {
|
||||
Runtime: true,
|
||||
}}
|
||||
|
||||
if uc.Update.RebootStrategy == "off" {
|
||||
if uc.Update.RebootStrategy == "false" || uc.Update.RebootStrategy == "off" {
|
||||
ls.Command = "stop"
|
||||
ls.Mask = true
|
||||
}
|
||||
|
@@ -17,7 +17,6 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"reflect"
|
||||
"strings"
|
||||
@@ -72,6 +71,15 @@ func TestUpdateUnits(t *testing.T) {
|
||||
Runtime: true,
|
||||
}}},
|
||||
},
|
||||
{
|
||||
config: config.Update{RebootStrategy: "false"},
|
||||
units: []Unit{{config.Unit{
|
||||
Name: "locksmithd.service",
|
||||
Command: "stop",
|
||||
Runtime: true,
|
||||
Mask: true,
|
||||
}}},
|
||||
},
|
||||
{
|
||||
config: config.Update{RebootStrategy: "off"},
|
||||
units: []Unit{{config.Unit{
|
||||
@@ -101,7 +109,7 @@ func TestUpdateFile(t *testing.T) {
|
||||
},
|
||||
{
|
||||
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", "off", "false"}},
|
||||
},
|
||||
{
|
||||
config: config.Update{Group: "master", Server: "http://foo.com"},
|
||||
@@ -135,6 +143,14 @@ func TestUpdateFile(t *testing.T) {
|
||||
RawFilePermissions: "0644",
|
||||
}},
|
||||
},
|
||||
{
|
||||
config: config.Update{RebootStrategy: "false"},
|
||||
file: &File{config.File{
|
||||
Content: "REBOOT_STRATEGY=false\n",
|
||||
Path: "etc/coreos/update.conf",
|
||||
RawFilePermissions: "0644",
|
||||
}},
|
||||
},
|
||||
{
|
||||
config: config.Update{RebootStrategy: "off"},
|
||||
file: &File{config.File{
|
||||
|
Reference in New Issue
Block a user